[84177] add XML "Squiggle as you type" Validation
diff --git a/bundles/org.eclipse.wst.xml.ui/plugin.xml b/bundles/org.eclipse.wst.xml.ui/plugin.xml
index 2c12893..3c0dfe6 100644
--- a/bundles/org.eclipse.wst.xml.ui/plugin.xml
+++ b/bundles/org.eclipse.wst.xml.ui/plugin.xml
@@ -30,6 +30,8 @@
<import plugin="org.eclipse.wst.xml.uriresolver"/>
<import plugin="org.eclipse.wst.xml.core"/>
<import plugin="org.eclipse.wst.common.ui"/>
+ <import plugin="org.eclipse.wst.validation"/>
+
</requires>
@@ -354,6 +356,21 @@
<initializer class="org.eclipse.wst.xml.ui.internal.preferences.XMLUIPreferenceInitializer"/>
</extension>
+ <extension
+ point="org.eclipse.wst.sse.ui.reconcileValidator">
+ <validator
+ scope="total"
+ class="org.eclipse.wst.xml.ui.reconcile.DelegatingReconcileValidatorForXML"
+ id="org.eclipse.wst.xml.ui.reconcile.DelegatingReconcileValidatorForXML">
+ <contentTypeIdentifier
+ id="org.eclipse.core.runtime.xml">
+ <partitionType
+ id="org.eclipse.wst.xml.DEFAULT_XML">
+ </partitionType>
+ </contentTypeIdentifier>
+ </validator>
+ </extension>
+
<!--======================================================================================-->
<!-- Document provider for ExternalFileEditorInput -->
<!--======================================================================================-->
diff --git a/bundles/org.eclipse.wst.xml.ui/src/org/eclipse/wst/xml/ui/reconcile/DelegatingReconcileValidator.java b/bundles/org.eclipse.wst.xml.ui/src/org/eclipse/wst/xml/ui/reconcile/DelegatingReconcileValidator.java
new file mode 100644
index 0000000..cfebd56
--- /dev/null
+++ b/bundles/org.eclipse.wst.xml.ui/src/org/eclipse/wst/xml/ui/reconcile/DelegatingReconcileValidator.java
@@ -0,0 +1,395 @@
+/*******************************************************************************
+ * 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
+ *******************************************************************************/
+
+package org.eclipse.wst.xml.ui.reconcile;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.wst.sse.core.IModelManager;
+import org.eclipse.wst.sse.core.IStructuredModel;
+import org.eclipse.wst.sse.core.IndexedRegion;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.validation.core.IFileDelta;
+import org.eclipse.wst.validation.core.IHelper;
+import org.eclipse.wst.validation.core.IMessage;
+import org.eclipse.wst.validation.core.IMessageAccess;
+import org.eclipse.wst.validation.core.IReporter;
+import org.eclipse.wst.validation.core.IValidator;
+import org.eclipse.wst.validation.core.MessageLimitException;
+import org.eclipse.wst.validation.core.ValidationException;
+import org.eclipse.wst.xml.core.document.XMLAttr;
+import org.eclipse.wst.xml.core.document.XMLDocument;
+import org.eclipse.wst.xml.core.document.XMLElement;
+import org.eclipse.wst.xml.core.document.XMLModel;
+import org.eclipse.wst.xml.core.document.XMLNode;
+import org.eclipse.wst.xml.core.document.XMLText;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+/**
+ * A DelegatingReconcileValidator calls its delegate validator
+ * to get a list of validation error IMessages. Using information
+ * in this IMessage the DelegatingReconcileValidator sets
+ * an offset and a length that should be "squiggled"
+ *
+ * @author Mark Hutchinson
+ *
+ */
+public class DelegatingReconcileValidator implements IValidator
+{ //these are the selection Strategies:
+ public static final String ATTRIBUTE = "ATTRIBUTE";
+ public static final String ATTRIBUTE_NAME ="ATTRIBUTE_NAME";
+ public static final String ATTRIBUTE_VALUE = "ATTRIBUTE_VALUE";
+ public static final String START_TAG = "START_TAG";
+ public static final String TEXT = "TEXT";
+ public static final String NAME_OF_ATTRIBUTE_WITH_GIVEN_VALUE = "NAME_OF_ATTRIBUTE_WITH_GIVEN_VALUE";
+
+ public DelegatingReconcileValidator()
+ { super(); //constructor
+ }
+
+ public void cleanup(IReporter arg0)
+ { // don't need to implement
+ }
+
+ // My Implementation of IHelper
+ class MyHelper implements IHelper
+ {
+ InputStream inputStream;
+
+ IFile file;
+
+ public MyHelper(InputStream inputStream, IFile file)
+ { this.inputStream = inputStream;
+ this.file = file;
+ }
+
+ public Object loadModel(String symbolicName, Object[] parms)
+ { if (symbolicName.equals("getFile"))
+ { return file;
+ }
+ return null;
+ }
+
+ public Object loadModel(String symbolicName)
+ { if (symbolicName.equals("inputStream"))
+ { return inputStream;
+ }
+ return null;
+ }
+ }
+ //My Implementation of IReporter
+ class MyReporter implements IReporter
+ {
+ List list = new ArrayList();
+
+ public MyReporter()
+ { super();
+ }
+
+ public void addMessage(IValidator origin, IMessage message) throws MessageLimitException
+ { list.add(message);
+ }
+
+ public void displaySubtask(IValidator validator, IMessage message)
+ {//do not need to implement
+ }
+
+ public IMessageAccess getMessageAccess()
+ { return null; //do not need to implement
+ }
+
+ public boolean isCancelled()
+ { return false; //do not need to implement
+ }
+
+ public void removeAllMessages(IValidator origin, Object object)
+ { //do not need to implement
+ }
+
+ public void removeAllMessages(IValidator origin)
+ {//do not need to implement
+ }
+
+ public void removeMessageSubset(IValidator validator, Object obj, String groupName)
+ {//do not need to implement
+ }
+ }
+
+ protected IValidator getDelegateValidator()
+ { return null;//this class must be overridden by a subclass
+ }
+
+ /**
+ * Calls a delegate validator getting and updates it's list of ValidationMessages
+ * with a good squiggle offset and length.
+ *
+ * @param helper
+ * loads an object.
+ * @param reporter
+ * Is an instance of an IReporter interface, which is used for
+ * interaction with the user.
+ * @param delta
+ * an array containing the file to be validated. Only position 0 will be validated
+ */
+ public void validate(IHelper helper, IReporter reporter, IFileDelta[] delta) throws ValidationException
+ {
+ if (delta.length > 0)
+ {
+ // get the file, model and document:
+ IFile file = getFile(delta[0]);
+ XMLModel xmlModel = getModelForResource(file);
+ try
+ {
+ XMLDocument document = xmlModel.getDocument();
+
+ // store the file to a byte array:
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
+ xmlModel.save(outputStream);
+ byte[] byteArray = outputStream.toByteArray();
+
+ //Get the Validator:
+ IValidator validator = getDelegateValidator();
+ if (validator != null)
+ {
+ //Validate the file:
+ IHelper vHelper = new MyHelper(new ByteArrayInputStream(byteArray), file);
+ MyReporter vReporter = new MyReporter();
+ validator.validate(vHelper, vReporter, delta);
+ List messages = vReporter.list;
+
+ //set the offset and length
+ updateValidationMessages(messages, document, reporter);
+ }
+ }
+ catch (Exception e)
+ { //e.printStackTrace();
+ }
+ finally
+ {
+ if (xmlModel != null)
+ xmlModel.releaseFromRead();
+ }
+ }
+ }
+
+ /**
+ * iterates through the messages and calculates a "better" offset and length
+ *
+ * @param messages - a List of IMessages
+ * @param document - the document
+ * @param reporter - the reporter the messages are to be added to
+ */
+ protected void updateValidationMessages(List messages, XMLDocument document, IReporter reporter)
+ {
+ for (int i = 0; i < messages.size(); i++)
+ {
+ IMessage message = (IMessage) messages.get(i);
+ try
+ { //get the extra information stored in the parameters:
+ String[] params = message.getParams();
+ int column = new Integer(params[0]).intValue();
+ String key = params[1];
+ String nameOrValue = params[2];//The name or value of what should be squiggled
+
+ // convert the line and Column numbers to an offset:
+ int start = document.getStructuredDocument().getLineOffset(message.getLineNo() - 1) + column - 1;
+
+ // calculate the "better" start and end offset:
+ int[] result = computeStartEndLocation(start, message.getText(), key, nameOrValue, document);
+ if (result != null)
+ {
+ message.setOffset(result[0]);
+ message.setLength(result[1] - result[0]);
+ reporter.addMessage(this, message);
+ }
+ }
+ catch (Exception e)
+ { //e.printStackTrace();
+ }
+ }
+ }
+ /**
+ * @param delta the IFileDelta containing the file name to get
+ * @return the IFile
+ */
+ public IFile getFile(IFileDelta delta)
+ {
+ IResource res = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(delta.getFileName()));
+ return res instanceof IFile ? (IFile) res : null;
+ }
+ /**
+ *
+ * @param file the file to get the model for
+ * @return the file's XMLModel
+ */
+ protected XMLModel getModelForResource(IFile file)
+ {
+ IStructuredModel model = null;
+ IModelManager manager = StructuredModelManager.getModelManager();
+
+ try
+ { model = manager.getModelForRead(file);
+ // TODO.. HTML validator tries again to get a model a 2nd way
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ return model instanceof XMLModel ? (XMLModel) model : null;
+ }
+
+ /**
+ * Calculates the "better" offsets.
+ *
+ * @param startOffset - the offset given by Xerces
+ * @param errorMessage - the Xerces error Message
+ * @param key - the selectionStrategy
+ * @param document - the document
+ * @return int[] - position 0 has the start offset of the squiggle range, position 1 has the endOffset
+ */
+ /*
+ * The way the offsets is calculated is:
+ *
+ * - find the indexed region (element) closest to the given offset
+ * - if we are between two elements, the one on the left is the one we want
+ * - based on the key select a strategy to find the better offset
+ * - use information from nameOrValue and the DOM to get better offsets
+ *
+ */
+ protected int[] computeStartEndLocation(int startOffset, String errorMessage, String key, String nameOrValue, XMLDocument document)
+ {
+ try
+ {
+ int startEndPositions[] = new int[2];
+
+ IndexedRegion region = document.getModel().getIndexedRegion(startOffset);
+ IndexedRegion prevRegion = document.getModel().getIndexedRegion(startOffset - 1);
+
+ if (prevRegion != region)
+ { // if between two regions, the one on the left is the one we are interested in
+ region = prevRegion;
+ }
+
+ // initialize start and end positions to be the start positions this means if the
+ // special case is not taken care of below the start and end offset are set to be
+ // the start of the region where the error was
+ startEndPositions[0] = region.getStartOffset();
+ startEndPositions[1] = startEndPositions[0];
+
+ if (region instanceof Node)
+ {
+ Node node = (Node) region;
+
+ if (key.equals(START_TAG))
+ {// then we want to highlight the opening tag
+ if (node.getNodeType() == Node.ELEMENT_NODE)
+ {
+ XMLElement element = (XMLElement) node;
+ startEndPositions[0] = element.getStartOffset() + 1;
+ startEndPositions[1] = startEndPositions[0] + element.getTagName().length();
+ }
+ }
+ else if (key.equals(ATTRIBUTE_NAME))
+ { // in this case we want to underline the offending attribute name
+ if (node.getNodeType() == Node.ELEMENT_NODE)
+ {
+ XMLElement element = (XMLElement) node;
+ XMLNode attributeNode = (XMLNode) (element.getAttributeNode(nameOrValue));
+ if (attributeNode != null)
+ {
+ startEndPositions[0] = attributeNode.getStartOffset();
+ startEndPositions[1] = attributeNode.getStartOffset() + nameOrValue.length();
+ }
+ }
+ }
+ else if (key.equals(ATTRIBUTE_VALUE))
+ {// in this case we want to underline the attribute's value
+ if (node.getNodeType() == Node.ELEMENT_NODE)
+ {
+ XMLElement element = (XMLElement) node;
+ XMLAttr attributeNode = (XMLAttr) (element.getAttributeNode(nameOrValue));
+ if (attributeNode != null)
+ {
+ startEndPositions[0] = attributeNode.getValueRegionStartOffset();
+ startEndPositions[1] = startEndPositions[0] + attributeNode.getValueRegionText().length();
+ }
+ }
+ }
+ else if (key.equals(TEXT))
+ {// in this case we want to underline the text (but not any extra whitespace)
+ if (node.getNodeType() == Node.TEXT_NODE)
+ {
+ XMLText textNode = (XMLText) node;
+ int start = textNode.getStartOffset();
+ String value = textNode.getNodeValue();
+ int index = 0;
+ char curChar = value.charAt(index);
+ //here we are finding start offset by skipping over whitespace:
+ while (curChar == '\n' || curChar == '\t' || curChar == '\r' || curChar == ' ')
+ {
+ curChar = value.charAt(index);
+ index++;
+ start++;
+ }
+ startEndPositions[0] = start - 1;
+ startEndPositions[1] = start + value.trim().length();
+ }
+ else if (node.getNodeType() == Node.ELEMENT_NODE)
+ {
+ XMLElement element = (XMLElement) node;
+ Node child = element.getFirstChild();
+ if (child instanceof XMLNode)
+ {
+ XMLNode xmlChild = ((XMLNode) child);
+ startEndPositions[0] = xmlChild.getStartOffset();
+ startEndPositions[1] = xmlChild.getEndOffset();
+ }
+ }
+ }
+
+ else if (key.equals(NAME_OF_ATTRIBUTE_WITH_GIVEN_VALUE))
+ {//underline the name of the attribute containing the given value
+ if (node.getNodeType() == Node.ELEMENT_NODE)
+ {
+ //here we will search through all attributes for the one with the
+ //with the value we want:
+ NamedNodeMap attributes = node.getAttributes();
+ for (int i=0; i<attributes.getLength(); i++)
+ {
+ XMLAttr attr = (XMLAttr)attributes.item(i);
+ String nodeValue = attr.getNodeValue().trim();
+ if (nodeValue.equals(nameOrValue))
+ { startEndPositions[0] = attr.getValueRegionStartOffset() + 1;
+ startEndPositions[1] = startEndPositions[0] + nodeValue.length();
+ break;
+ }
+ }
+ }
+ }
+ }
+ return startEndPositions;
+ }
+ catch (Exception e)
+ { //e.printStackTrace();
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.eclipse.wst.xml.ui/src/org/eclipse/wst/xml/ui/reconcile/DelegatingReconcileValidatorForXML.java b/bundles/org.eclipse.wst.xml.ui/src/org/eclipse/wst/xml/ui/reconcile/DelegatingReconcileValidatorForXML.java
new file mode 100644
index 0000000..0511d6a
--- /dev/null
+++ b/bundles/org.eclipse.wst.xml.ui/src/org/eclipse/wst/xml/ui/reconcile/DelegatingReconcileValidatorForXML.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * 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
+ *******************************************************************************/
+
+package org.eclipse.wst.xml.ui.reconcile;
+
+import org.eclipse.wst.validation.core.IValidator;
+import org.eclipse.wst.validation.internal.ValidationRegistryReader;
+import org.eclipse.wst.validation.internal.ValidatorMetaData;
+
+/**
+ * @author Mark Hutchinson
+ *
+ */
+public class DelegatingReconcileValidatorForXML extends DelegatingReconcileValidator
+{
+ private final static String VALIDATOR_CLASS = "org.eclipse.wst.xml.validation.internal.ui.eclipse.Validator";
+
+ public DelegatingReconcileValidatorForXML()
+ { super();
+ }
+
+ protected IValidator getDelegateValidator()
+ {
+ try
+ {
+ //Get the validator:
+ ValidatorMetaData validatorData = ValidationRegistryReader.getReader().getValidatorMetaData(VALIDATOR_CLASS);
+ return validatorData.getValidator();
+ }
+ catch (Exception e)
+ { //
+ }
+ return null;
+ }
+}