blob: fc2b772da321b86ea3fbb7518beca7ed3b032378 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2001, 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Jens Lukowski/Innoopract - initial renaming/restructuring
*
*******************************************************************************/
package org.eclipse.wst.xml.core.internal.cleanup;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.wst.sse.core.internal.cleanup.IStructuredCleanupHandler;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNamedNodeMap;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.provisional.document.ISourceGenerator;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class ElementNodeCleanupHandler extends NodeCleanupHandler {
protected static final char DOUBLE_QUOTE = '\"'; //$NON-NLS-1$
protected static final String DOUBLE_QUOTES = "\"\""; //$NON-NLS-1$
protected static final String EMPTY_TAG_CLOSE = "/>"; //$NON-NLS-1$
protected static final String END_TAG_OPEN = "</"; //$NON-NLS-1$
protected static final char SINGLE_QUOTE = '\''; //$NON-NLS-1$
protected static final String SINGLE_QUOTES = "''"; //$NON-NLS-1$
/** Non-NLS strings */
protected static final String START_TAG_OPEN = "<"; //$NON-NLS-1$
protected static final String TAG_CLOSE = ">"; //$NON-NLS-1$
public Node cleanup(Node node) {
Node newNode = cleanupChildren(node);
IDOMNode renamedNode = newNode instanceof IDOMNode ? (IDOMNode) newNode : null;
// call quoteAttrValue() first so it will close any unclosed attr
// quoteAttrValue() will return the new start tag if there is a
// structure change
renamedNode = quoteAttrValue(renamedNode);
// insert tag close if missing
// if node is not comment tag
// and not implicit tag
if (!isCommentTag(renamedNode) && !isImplicitTag(renamedNode)) {
IDOMModel structuredModel = renamedNode.getModel();
// save start offset before insertTagClose()
// or else renamedNode.getStartOffset() will be zero if
// renamedNode replaced by insertTagClose()
int startTagStartOffset = renamedNode.getStartOffset();
// for start tag
IStructuredDocumentRegion startTagStructuredDocumentRegion = renamedNode.getStartStructuredDocumentRegion();
insertTagClose(structuredModel, startTagStructuredDocumentRegion);
// update renamedNode and startTagStructuredDocumentRegion after
// insertTagClose()
renamedNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset);
startTagStructuredDocumentRegion = renamedNode.getStartStructuredDocumentRegion();
// for end tag
IStructuredDocumentRegion endTagStructuredDocumentRegion = renamedNode.getEndStructuredDocumentRegion();
if (endTagStructuredDocumentRegion != startTagStructuredDocumentRegion)
insertTagClose(structuredModel, endTagStructuredDocumentRegion);
}
// call insertMissingTags() next, it will generate implicit tags if
// there are any
// insertMissingTags() will return the new missing start tag if one is
// missing
renamedNode = insertMissingTags(renamedNode);
renamedNode = compressEmptyElementTag(renamedNode);
renamedNode = insertRequiredAttrs(renamedNode);
return renamedNode;
}
protected Node cleanupChildren(Node node) {
Node parentNode = node;
if (node != null) {
Node childNode = node.getFirstChild();
while (childNode != null) {
// get cleanup handler
IStructuredCleanupHandler cleanupHandler = getCleanupHandler(childNode);
// cleanup each child
childNode = cleanupHandler.cleanup(childNode);
// get new parent node
parentNode = childNode.getParentNode();
// get next child node
childNode = childNode.getNextSibling();
}
}
return parentNode;
}
private IDOMNode compressEmptyElementTag(IDOMNode node) {
boolean compressEmptyElementTags = getCleanupPreferences().getCompressEmptyElementTags();
IDOMNode newNode = node;
IStructuredDocumentRegion startTagStructuredDocumentRegion = newNode.getFirstStructuredDocumentRegion();
IStructuredDocumentRegion endTagStructuredDocumentRegion = newNode.getLastStructuredDocumentRegion();
if (compressEmptyElementTags && startTagStructuredDocumentRegion != endTagStructuredDocumentRegion && startTagStructuredDocumentRegion != null) {
ITextRegionList regions = startTagStructuredDocumentRegion.getRegions();
ITextRegion lastRegion = regions.get(regions.size() - 1);
// format children and end tag if not empty element tag
if (lastRegion.getType() != DOMRegionContext.XML_EMPTY_TAG_CLOSE) {
NodeList childNodes = newNode.getChildNodes();
if (childNodes == null || childNodes.getLength() == 0 || (childNodes.getLength() == 1 && (childNodes.item(0)).getNodeType() == Node.TEXT_NODE && ((childNodes.item(0)).getNodeValue().trim().length() == 0))) {
IDOMModel structuredModel = newNode.getModel();
IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
int startTagStartOffset = newNode.getStartOffset();
int offset = endTagStructuredDocumentRegion.getStart();
int length = endTagStructuredDocumentRegion.getLength();
structuredDocument.replaceText(structuredDocument, offset, length, ""); //$NON-NLS-1$
newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
offset = startTagStructuredDocumentRegion.getStart() + lastRegion.getStart();
structuredDocument.replaceText(structuredDocument, offset, 0, "/"); //$NON-NLS-1$
newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
}
}
}
return newNode;
}
protected IStructuredCleanupHandler getCleanupHandler(Node node) {
short nodeType = node.getNodeType();
IStructuredCleanupHandler cleanupHandler = null;
switch (nodeType) {
case org.w3c.dom.Node.ELEMENT_NODE : {
cleanupHandler = new ElementNodeCleanupHandler();
break;
}
case org.w3c.dom.Node.TEXT_NODE : {
cleanupHandler = new NodeCleanupHandler();
break;
}
default : {
cleanupHandler = new NodeCleanupHandler();
}
}
// init CleanupPreferences
cleanupHandler.setCleanupPreferences(getCleanupPreferences());
return cleanupHandler;
}
protected ModelQuery getModelQuery(Node node) {
if (node.getNodeType() == Node.DOCUMENT_NODE) {
return ModelQueryUtil.getModelQuery((Document) node);
} else {
return ModelQueryUtil.getModelQuery(node.getOwnerDocument());
}
}
protected List getRequiredAttrs(Node node) {
List result = new ArrayList();
ModelQuery modelQuery = getModelQuery(node);
if (modelQuery != null) {
CMElementDeclaration elementDecl = modelQuery.getCMElementDeclaration((Element) node);
if (elementDecl != null) {
CMNamedNodeMap attrMap = elementDecl.getAttributes();
Iterator it = attrMap.iterator();
CMAttributeDeclaration attr = null;
while (it.hasNext()) {
attr = (CMAttributeDeclaration) it.next();
if (attr.getUsage() == CMAttributeDeclaration.REQUIRED) {
result.add(attr);
}
}
}
}
return result;
}
private IDOMNode insertEndTag(IDOMNode node) {
IDOMNode newNode = node;
IDOMElement element = (IDOMElement) node;
if (element.isCommentTag())
return node; // do nothing
int startTagStartOffset = node.getStartOffset();
IDOMModel structuredModel = node.getModel();
if (isEmptyElement(element)) {
IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
IStructuredDocumentRegion startStructuredDocumentRegion = node.getStartStructuredDocumentRegion();
ITextRegionList regions = startStructuredDocumentRegion.getRegions();
ITextRegion lastRegion = regions.get(regions.size() - 1);
structuredDocument.replaceText(structuredDocument, startStructuredDocumentRegion.getStartOffset(lastRegion), lastRegion.getLength(), EMPTY_TAG_CLOSE);
if (regions.size() > 1) {
ITextRegion regionBeforeTagClose = regions.get(regions.size() - 1 - 1);
// insert a space separator before tag close if the previous
// region does not have extra spaces
if (regionBeforeTagClose.getTextLength() == regionBeforeTagClose.getLength())
structuredDocument.replaceText(structuredDocument, startStructuredDocumentRegion.getStartOffset(lastRegion), 0, " "); //$NON-NLS-1$
}
} else {
String tagName = node.getNodeName();
String endTag = END_TAG_OPEN.concat(tagName).concat(TAG_CLOSE);
IDOMNode lastChild = (IDOMNode) node.getLastChild();
int endTagStartOffset = 0;
if (lastChild != null)
// if this node has children, insert the end tag after the
// last child
endTagStartOffset = lastChild.getEndOffset();
else
// if this node does not has children, insert the end tag
// after the start tag
endTagStartOffset = node.getEndOffset();
IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
structuredDocument.replaceText(structuredDocument, endTagStartOffset, 0, endTag);
}
newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
// new
// node
return newNode;
}
private IDOMNode insertMissingTags(IDOMNode node) {
boolean insertMissingTags = getCleanupPreferences().getInsertMissingTags();
IDOMNode newNode = node;
if (insertMissingTags) {
IStructuredDocumentRegion startTagStructuredDocumentRegion = node.getStartStructuredDocumentRegion();
if (startTagStructuredDocumentRegion == null) {
// implicit start tag; generate tag for it
newNode = insertStartTag(node);
startTagStructuredDocumentRegion = newNode.getStartStructuredDocumentRegion();
}
IStructuredDocumentRegion endTagStructuredDocumentRegion = newNode.getEndStructuredDocumentRegion();
ITextRegionList startStructuredDocumentRegionRegions = startTagStructuredDocumentRegion.getRegions();
if (startTagStructuredDocumentRegion != null && startStructuredDocumentRegionRegions != null && (startStructuredDocumentRegionRegions.get(startStructuredDocumentRegionRegions.size() - 1)).getType() == DOMRegionContext.XML_EMPTY_TAG_CLOSE) {
} else {
if (startTagStructuredDocumentRegion == null) {
// start tag missing
if (isStartTagRequired(newNode))
newNode = insertStartTag(newNode);
} else if (endTagStructuredDocumentRegion == null) {
// end tag missing
if (isEndTagRequired(newNode))
newNode = insertEndTag(newNode);
}
}
}
return newNode;
}
private IDOMNode insertRequiredAttrs(IDOMNode node) {
boolean insertRequiredAttrs = getCleanupPreferences().getInsertRequiredAttrs();
IDOMNode newNode = node;
if (insertRequiredAttrs) {
List requiredAttrs = getRequiredAttrs(newNode);
if (requiredAttrs.size() > 0) {
NamedNodeMap currentAttrs = node.getAttributes();
List insertAttrs = new ArrayList();
if (currentAttrs.getLength() == 0)
insertAttrs.addAll(requiredAttrs);
else {
for (int i = 0; i < requiredAttrs.size(); i++) {
String requiredAttrName = ((CMAttributeDeclaration) requiredAttrs.get(i)).getAttrName();
boolean found = false;
for (int j = 0; j < currentAttrs.getLength(); j++) {
String currentAttrName = currentAttrs.item(j).getNodeName();
if (requiredAttrName.compareToIgnoreCase(currentAttrName) == 0) {
found = true;
break;
}
}
if (!found)
insertAttrs.add(requiredAttrs.get(i));
}
}
if (insertAttrs.size() > 0) {
IStructuredDocumentRegion startStructuredDocumentRegion = newNode.getStartStructuredDocumentRegion();
int index = startStructuredDocumentRegion.getEndOffset();
ITextRegion lastRegion = startStructuredDocumentRegion.getLastRegion();
if (lastRegion.getType() == DOMRegionContext.XML_TAG_CLOSE) {
index--;
lastRegion = startStructuredDocumentRegion.getRegionAtCharacterOffset(index - 1);
} else if (lastRegion.getType() == DOMRegionContext.XML_EMPTY_TAG_CLOSE) {
index = index - 2;
lastRegion = startStructuredDocumentRegion.getRegionAtCharacterOffset(index - 1);
}
MultiTextEdit multiTextEdit = new MultiTextEdit();
try {
for (int i = insertAttrs.size() - 1; i >= 0; i--) {
CMAttributeDeclaration attrDecl = (CMAttributeDeclaration) insertAttrs.get(i);
String requiredAttributeName = attrDecl.getAttrName();
String defaultValue = attrDecl.getDefaultValue();
if (defaultValue == null)
defaultValue = ""; //$NON-NLS-1$
String nameAndDefaultValue = " "; //$NON-NLS-1$
if (i == 0 && lastRegion.getLength() > lastRegion.getTextLength())
nameAndDefaultValue = ""; //$NON-NLS-1$
nameAndDefaultValue += requiredAttributeName + "=\"" + defaultValue + "\""; //$NON-NLS-1$ //$NON-NLS-2$
multiTextEdit.addChild(new InsertEdit(index, nameAndDefaultValue));
// BUG3381: MultiTextEdit applies all child
// TextEdit's basing on offsets
// in the document before the first TextEdit, not
// after each
// child TextEdit. Therefore, do not need to
// advance the index.
//index += nameAndDefaultValue.length();
}
multiTextEdit.apply(newNode.getStructuredDocument());
} catch (BadLocationException e) {
// log for now, unless we find reason not to
Logger.log(Logger.INFO, e.getMessage());
}
}
}
}
return newNode;
}
private IDOMNode insertStartTag(IDOMNode node) {
IDOMNode newNode = node;
if (isCommentTag(node))
return node; // do nothing
String tagName = node.getNodeName();
String startTag = START_TAG_OPEN.concat(tagName).concat(TAG_CLOSE);
int startTagStartOffset = node.getStartOffset();
IDOMModel structuredModel = node.getModel();
IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
structuredDocument.replaceText(structuredDocument, startTagStartOffset, 0, startTag);
newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
// new
// node
return newNode;
}
private void insertTagClose(IDOMModel structuredModel, IStructuredDocumentRegion flatNode) {
if (flatNode != null) {
ITextRegionList flatnodeRegions = flatNode.getRegions();
if (flatnodeRegions != null) {
ITextRegion lastRegion = flatnodeRegions.get(flatnodeRegions.size() - 1);
if (lastRegion != null) {
String regionType = lastRegion.getType();
if ((regionType != DOMRegionContext.XML_EMPTY_TAG_CLOSE) && (regionType != DOMRegionContext.XML_TAG_CLOSE)) {
IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
// insert ">" after lastRegion of flatNode
// as in "<a</a>" if flatNode is for start tag, or in
// "<a></a" if flatNode is for end tag
structuredDocument.replaceText(structuredDocument, flatNode.getTextEndOffset(lastRegion), 0, ">"); //$NON-NLS-1$
}
}
}
}
}
/**
* @param renamedNode
* @return
*/
private boolean isCommentTag(Node renamedNode) {
boolean result = false;
if (renamedNode instanceof IDOMElement) {
IDOMElement element = (IDOMElement) renamedNode;
result = element.isCommentTag();
}
return result;
}
private boolean isEmptyElement(IDOMElement element) {
Document document = element.getOwnerDocument();
if (document == null)
// undefined tag, return default
return false;
ModelQuery modelQuery = ModelQueryUtil.getModelQuery(document);
if (modelQuery == null)
// undefined tag, return default
return false;
CMElementDeclaration decl = modelQuery.getCMElementDeclaration(element);
if (decl == null)
// undefined tag, return default
return false;
return (decl.getContentType() == CMElementDeclaration.EMPTY);
}
private boolean isEndTagRequired(IDOMNode node) {
if (node == null)
return false;
return node.isContainer();
}
/**
* A tag is implicit if it has not corresponding region in document.
*
* @param renamedNode
* @return
*/
private boolean isImplicitTag(IDOMNode renamedNode) {
return renamedNode.getStartStructuredDocumentRegion() == null;
}
/**
* The end tags of HTML EMPTY content type, such as IMG, and HTML
* undefined tags are parsed separately from the start tags. So inserting
* the missing start tag is useless and even harmful.
*/
private boolean isStartTagRequired(IDOMNode node) {
if (node == null)
return false;
return node.isContainer();
}
private boolean isXMLType(IDOMModel structuredModel) {
boolean result = false;
if (structuredModel != null && structuredModel != null) {
IDOMDocument document = structuredModel.getDocument();
if (document != null)
result = document.isXMLType();
}
return result;
}
private IDOMNode quoteAttrValue(IDOMNode node) {
IDOMNode newNode = node;
//XMLElement element = (XMLElement) node;
if (isCommentTag(node))
return node; // do nothing
boolean quoteAttrValues = getCleanupPreferences().getQuoteAttrValues();
if (quoteAttrValues) {
NamedNodeMap attributes = newNode.getAttributes();
if (attributes != null) {
int attributesLength = attributes.getLength();
ISourceGenerator generator = node.getModel().getGenerator();
for (int i = 0; i < attributesLength; i++) {
attributes = newNode.getAttributes();
attributesLength = attributes.getLength();
IDOMAttr eachAttr = (IDOMAttr) attributes.item(i);
//ITextRegion oldAttrValueRegion =
// eachAttr.getValueRegion();
String oldAttrValue = eachAttr.getValueRegionText();
if (oldAttrValue == null) {
IDOMModel structuredModel = node.getModel();
if (isXMLType(structuredModel)) {
String newAttrValue = "\"" + eachAttr.getNameRegionText() + "\""; //$NON-NLS-1$ //$NON-NLS-2$
IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
if (eachAttr.getEqualRegion() != null)
// equal region exists
structuredDocument.replaceText(structuredDocument, eachAttr.getEndOffset(), 0, newAttrValue);
else
// no equal region
structuredDocument.replaceText(structuredDocument, eachAttr.getNameRegionTextEndOffset(), 0, "=".concat(newAttrValue)); //$NON-NLS-1$
newNode = (IDOMNode) structuredModel.getIndexedRegion(node.getStartOffset()); // save
// new
// node
}
} else {
//String oldAttrValue = oldAttrValueRegion.getText();
char quote = StringUtils.isQuoted(oldAttrValue) ? oldAttrValue.charAt(0) : DOUBLE_QUOTE;
String newAttrValue = generator.generateAttrValue(eachAttr, quote);
// There is a problem in
// StructuredDocumentRegionUtil.getAttrValue(ITextRegion)
// when the region is instanceof ContextRegion.
// Workaround for now...
if (oldAttrValue.length() == 1) {
char firstChar = oldAttrValue.charAt(0);
if (firstChar == SINGLE_QUOTE)
newAttrValue = SINGLE_QUOTES;
else if (firstChar == DOUBLE_QUOTE)
newAttrValue = DOUBLE_QUOTES;
}
if (newAttrValue != null) {
if (newAttrValue.compareTo(oldAttrValue) != 0) {
int attrValueStartOffset = eachAttr.getValueRegionStartOffset();
int attrValueLength = oldAttrValue.length();
int startTagStartOffset = node.getStartOffset();
IDOMModel structuredModel = node.getModel();
IStructuredDocument structuredDocument = structuredModel.getStructuredDocument();
structuredDocument.replaceText(structuredDocument, attrValueStartOffset, attrValueLength, newAttrValue);
newNode = (IDOMNode) structuredModel.getIndexedRegion(startTagStartOffset); // save
// new
// node
}
}
}
}
}
}
return newNode;
}
}