blob: 3973babd6f8899118b0914e0f82f9176b8eda9f3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2008 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.internal.validation;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.validation.internal.core.IMessageAccess;
import org.eclipse.wst.validation.internal.core.ValidationException;
import org.eclipse.wst.validation.internal.provisional.core.IMessage;
import org.eclipse.wst.validation.internal.provisional.core.IProjectValidationContext;
import org.eclipse.wst.validation.internal.provisional.core.IReporter;
import org.eclipse.wst.validation.internal.provisional.core.IValidationContext;
import org.eclipse.wst.validation.internal.provisional.core.IValidator;
import org.eclipse.wst.validation.internal.provisional.core.IValidatorJob;
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.IDOMText;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* A DelegatingReconcileValidator calls its delegate validator to get a list
* of validation error IMessages. Using information in this IMessage the
* DelegatingReconcileValidator updates the IMessage with an offset and length
* to give a good range to be "squiggled" and adds the messages to the
* IReporter
*
* @author Mark Hutchinson
*
*/
public abstract class DelegatingSourceValidator implements IValidator {
// the selection strategies:
protected static final String ALL_ATTRIBUTES = "ALL_ATTRIBUTES"; //$NON-NLS-1$
protected static final String ATTRIBUTE_NAME = "ATTRIBUTE_NAME"; //$NON-NLS-1$
protected static final String ATTRIBUTE_VALUE = "ATTRIBUTE_VALUE"; //$NON-NLS-1$
protected static final String START_TAG = "START_TAG"; //$NON-NLS-1$
protected static final String TEXT = "TEXT"; //$NON-NLS-1$
protected static final String FIRST_NON_WHITESPACE_TEXT = "FIRST_NON_WHITESPACE_TEXT"; //$NON-NLS-1$
protected static final String TEXT_ENTITY_REFERENCE = "TEXT_ENTITY_REFERENCE"; //$NON-NLS-1$
protected static final String VALUE_OF_ATTRIBUTE_WITH_GIVEN_VALUE = "VALUE_OF_ATTRIBUTE_WITH_GIVEN_VALUE"; //$NON-NLS-1$
protected static final String END_TAG = "END_TAG"; //$NON-NLS-1$
/**
* This constant specifies the attribute name that specifies the side of
* the 'start tag' that the validator has used to report an error. A
* validator may choose to report a message at the left (the start of the
* start tag)or at the right (the end of the start tag). When this
* attribute is not specified error ERROR_SIDE_LEFT is the default.
*/
protected static final String ERROR_SIDE = "ERROR_SIDE"; //$NON-NLS-1$
/**
* When the ERROR_SIDE attribute specifies the ERROR_SIDE_LEFT value it is
* assumed that the message specifies a location to the left of the start
* tag
*/
protected static final String ERROR_SIDE_LEFT = "ERROR_SIDE_LEFT"; //$NON-NLS-1$
/**
* When the ERROR_SIDE attribute specifies the ERROR_SIDE_RIGHT value it
* is assumed that the message specifies a location to the right of the
* start tag
*/
protected static final String ERROR_SIDE_RIGHT = "ERROR_SIDE_RIGHT"; //$NON-NLS-1$
protected static final String COLUMN_NUMBER_ATTRIBUTE = "columnNumber"; //$NON-NLS-1$
protected static final String SQUIGGLE_SELECTION_STRATEGY_ATTRIBUTE = "squiggleSelectionStrategy"; //$NON-NLS-1$
protected static final String SQUIGGLE_NAME_OR_VALUE_ATTRIBUTE = "squiggleNameOrValue"; //$NON-NLS-1$
public DelegatingSourceValidator() {
super(); // constructor
}
public void cleanup(IReporter arg0) { // don't need to implement
}
// My Implementation of IHelper
class MyHelper implements IProjectValidationContext {
InputStream inputStream;
IFile file;
public MyHelper(InputStream inputStream, IFile file) {
this.inputStream = inputStream;
this.file = file;
}
public int getBuildKind() {
return 0;
}
public Object loadModel(String symbolicName, Object[] parms) {
if (symbolicName.equals("getFile")) { //$NON-NLS-1$
return file;
}
return null;
}
public Object loadModel(String symbolicName) {
if (symbolicName.equals("inputStream")) { //$NON-NLS-1$
return inputStream;
}
return null;
}
public String[] getURIs() {
if (file != null) {
return new String[]{file.getFullPath().toString()};
}
return new String[0];
}
public IProject getProject() {
if (file != null) {
return file.getProject();
}
return null;
}
}
// My Implementation of IReporter
class MyReporter implements IReporter {
List list = new ArrayList();
public MyReporter() {
super();
}
public void addMessage(IValidator origin, IMessage message) {
list.add(message);
}
public void displaySubtask(IValidator validator, IMessage message) {
/* do not need to implement */
}
public IMessageAccess getMessageAccess() {
return null;
}
public boolean isCancelled() {
return false;
}
public void removeAllMessages(IValidator origin, Object object) { // do
/* 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
/* do not need to implement */
}
public List getMessages() {
return list;
}
}
protected abstract IValidator getDelegateValidator();
/**
* 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.
*/
public void validate(IValidationContext helper, IReporter reporter) throws ValidationException {
String[] delta = helper.getURIs();
if (delta.length > 0) {
// get the file, model and document:
IFile file = getFile(delta[0]);
IDOMModel xmlModel = getModelForResource(file);
try {
IDOMDocument document = xmlModel.getDocument();
// store the text in a byte array; make a full copy to ease
// any threading problems
byte[] byteArray;
try {
byteArray = xmlModel.getStructuredDocument().get().getBytes("UTF-8");
}
catch (UnsupportedEncodingException e) {
// Not likely to happen
byteArray = xmlModel.getStructuredDocument().get().getBytes();
}
if (isDelegateValidatorEnabled(file)) {
IValidator validator = getDelegateValidator();
if (validator != null) {
// Validate the file:
IValidationContext vHelper = new MyHelper(new ByteArrayInputStream(byteArray), file);
MyReporter vReporter = new MyReporter();
if (validator instanceof IValidatorJob) {
((IValidatorJob) validator).validateInJob(vHelper, vReporter);
}
else {
validator.validate(vHelper, vReporter);
}
List messages = vReporter.list;
// set the offset and length
updateValidationMessages(messages, document, reporter);
}
}
}
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, IDOMDocument document, IReporter reporter) {
for (int i = 0; i < messages.size(); i++) {
IMessage message = (IMessage) messages.get(i);
try {
if (message.getAttribute(COLUMN_NUMBER_ATTRIBUTE) != null) {
int column = ((Integer) message.getAttribute(COLUMN_NUMBER_ATTRIBUTE)).intValue();
String selectionStrategy = (String) message.getAttribute(SQUIGGLE_SELECTION_STRATEGY_ATTRIBUTE);
String nameOrValue = (String) message.getAttribute(SQUIGGLE_NAME_OR_VALUE_ATTRIBUTE);
// convert the line and Column numbers to an offset:
int start = document.getStructuredDocument().getLineOffset(message.getLineNumber() - 1) + column - 1;
// calculate the "better" start and end offset:
int[] result = computeStartAndEndLocation(start, selectionStrategy, getErrorSide(message), nameOrValue, document);
if (result != null) {
message.setOffset(result[0]);
message.setLength(result[1] - result[0]);
reporter.addMessage(this, message);
}
}
}
catch (BadLocationException e) { // this exception should not
// occur - it is thrown if
// trying to convert an
// invalid line number to and
// offset
}
}
}
/**
* @param delta
* the IFileDelta containing the file name to get
* @return the IFile
*/
public IFile getFile(String delta) {
IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(delta));
if (file != null && file.exists())
return file;
return null;
}
/**
*
* @param file
* the file to get the model for
* @return the file's XMLModel
*/
protected IDOMModel 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 IDOMModel ? (IDOMModel) model : null;
}
/**
* @deprecated use computeStartEndLocation(int startOffset, String
* errorMessage, String selectionStrategy, boolean leftError,
* String nameOrValue, IDOMDocument document) {
*
*/
protected int[] computeStartEndLocation(int startOffset, String errorMessage, String selectionStrategy, String nameOrValue, IDOMDocument document) {
return computeStartAndEndLocation(startOffset, selectionStrategy, ERROR_SIDE_RIGHT, nameOrValue, document);
}
/**
* Calculates the "better" offsets.
*
* @param startOffset -
* the offset given by Xerces
* @param errorMessage -
* the Xerces error Message
* @param selectionStrategy -
* 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,
* choosing left or right element will depend on parameter 'errorSide' -
* based on the selectionStrategy choose the underlining strategy (eg
* START_TAG means underline the start tag of that element) - use
* information from nameOrValue and the DOM to get better offsets
*
*/
protected int[] computeStartAndEndLocation(int startOffset, String selectionStrategy, String errorSide, String nameOrValue, IDOMDocument 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 we use the 'errorSide' to understand
// which
// element is applicable. if we know the error has been
// reported to the
// right of the tag, then we can assume we need to step back
// to the previous
// region to land at the 'correct location. Otherwise assume
// we're
// exactly where we need to be.
if (ERROR_SIDE_LEFT.equals(errorSide)) {
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
if (region != null) {
startEndPositions[0] = region.getStartOffset();
startEndPositions[1] = startEndPositions[0];
}
else { // this will message will not get added to the IReporter
// since the length is 0
startEndPositions[0] = 0;
startEndPositions[1] = 0;
}
if (region instanceof Node) {
Node node = (Node) region;
if (START_TAG.equals(selectionStrategy)) {// then we want to
// underline the opening tag
if (node.getNodeType() == Node.ELEMENT_NODE) {
IDOMElement element = (IDOMElement) node;
startEndPositions[0] = element.getStartOffset() + 1;
startEndPositions[1] = startEndPositions[0] + element.getTagName().length();
}
}
else if (END_TAG.equals(selectionStrategy)) {// then we want to
// underline the end tag
if (node.getNodeType() == Node.ELEMENT_NODE) {
IDOMElement element = (IDOMElement) node;
startEndPositions[0] = element.getEndStartOffset();
startEndPositions[1] = element.getEndOffset();
}
}
else if (ATTRIBUTE_NAME.equals(selectionStrategy)) { // in
// underline the attribute's name
if (node.getNodeType() == Node.ELEMENT_NODE) {
IDOMElement element = (IDOMElement) node;
IDOMNode attributeNode = (IDOMNode) (element.getAttributeNode(nameOrValue));
if (attributeNode != null) {
startEndPositions[0] = attributeNode.getStartOffset();
startEndPositions[1] = attributeNode.getStartOffset() + nameOrValue.length();
}
}
}
else if (ATTRIBUTE_VALUE.equals(selectionStrategy)) {
// underline the attribute's value
if (node.getNodeType() == Node.ELEMENT_NODE) {
IDOMElement element = (IDOMElement) node;
IDOMAttr attributeNode = (IDOMAttr) (element.getAttributeNode(nameOrValue));
if (attributeNode != null) {
startEndPositions[0] = attributeNode.getValueRegionStartOffset();
startEndPositions[1] = startEndPositions[0] + attributeNode.getValueRegionText().length();
}
}
}
else if (ALL_ATTRIBUTES.equals(selectionStrategy)) {
// underline all attributes
if (node.getNodeType() == Node.ELEMENT_NODE) {
IDOMElement element = (IDOMElement) node;
NamedNodeMap attributes = element.getAttributes();
if (attributes != null) {
IDOMNode first = (IDOMNode) attributes.item(0);
IDOMNode last = (IDOMNode) attributes.item(attributes.getLength() - 1);
if ((first != null) && (last != null)) {
startEndPositions[0] = first.getStartOffset();
startEndPositions[1] = last.getEndOffset();
}
}
}
}
else if (TEXT.equals(selectionStrategy)) {
// underline the text between the tags
if (node.getNodeType() == Node.TEXT_NODE) {
IDOMText textNode = (IDOMText) 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++;
}
if (index > 0) {
index--;
}
start = start + index;
startEndPositions[0] = start + index;
startEndPositions[1] = start + value.trim().length();
}
else if (node.getNodeType() == Node.ELEMENT_NODE) {
IDOMElement element = (IDOMElement) node;
Node child = element.getFirstChild();
if (child instanceof IDOMNode) {
IDOMNode xmlChild = ((IDOMNode) child);
startEndPositions[0] = xmlChild.getStartOffset();
startEndPositions[1] = xmlChild.getEndOffset();
}
}
}
else if (FIRST_NON_WHITESPACE_TEXT.equals(selectionStrategy)) {
// search through all child nodes and return range of
// first non-whitespace
// text node
if (node.getNodeType() == Node.ELEMENT_NODE) {
NodeList nodes = node.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Node currentNode = nodes.item(i);
if (currentNode.getNodeType() == Node.TEXT_NODE) {
// TODO (Trung) I don't think we should call
// getNodeValue(), trim(), length()
// repeatedly.
// This is inefficient, to improve use local
// variables to store values.
IDOMText textNode = (IDOMText) currentNode;
if (textNode.getNodeValue().trim().length() > 0) {
String value = textNode.getNodeValue();
int index = 0;
int start = textNode.getStartOffset();
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++;
}
if (index > 0) {
index--;
}
start = start + index;
startEndPositions[0] = start;
startEndPositions[1] = start + value.trim().length();
break;
}
}
}
}
}
else if (TEXT_ENTITY_REFERENCE.equals(selectionStrategy)) {
if (node.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
startEndPositions[0] = region.getStartOffset();
startEndPositions[1] = region.getEndOffset();
}
else if (node.getNodeType() == Node.ELEMENT_NODE) {
/*
* In this case the undeclared entity might be in one
* of the attribute values. Search through the
* attributes to find the range of the undeclared
* entity.
*/
String entity = "&" + nameOrValue + ";"; //$NON-NLS-1$ //$NON-NLS-2$
NamedNodeMap attributes = node.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
IDOMAttr attr = (IDOMAttr) attributes.item(i);
String nodeValue = attr.getNodeValue();
int index = nodeValue.indexOf(entity);
if (index != -1) {
startEndPositions[0] = attr.getValueRegionStartOffset() + index + 1;
startEndPositions[1] = startEndPositions[0] + entity.length();
}
}
}
}
else if (VALUE_OF_ATTRIBUTE_WITH_GIVEN_VALUE.equals(selectionStrategy)) {
// TODO (Trung) do we really need this strategy ?
// If we know the name of the name of the attribute, we
// can retrieve its value.
// Hence, we can incoperate this strategy with
// ATTRIBUTE_VALUE ?
if (node.getNodeType() == Node.ELEMENT_NODE) {
// here we will search through all attributes for the
// one with the
// with the value we want:
// TODO (Trung) I see a potential problem here.
// What happens when there is another attribute having
// the same value
// with this attribute's buggy value ?
// Need to solve when time permits.
NamedNodeMap attributes = node.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
IDOMAttr attr = (IDOMAttr) 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();
// }
finally {
}
// return null;
}
/**
* Returns true if delegate validator is enabled based on Validation
* preferences
*
* @param file
* @return false if delegate validator is not enabled based on Validatoin
* preferences, true otherwise
*/
protected boolean isDelegateValidatorEnabled(IFile file) {
return true;
}
protected String getErrorSide(IMessage message) {
// note that if the ERROR_SIDE is unspecified we return the default
// value ERROR_SIDE_LEFT
Object value = message.getAttribute(ERROR_SIDE);
return ERROR_SIDE_RIGHT.equals(value) ? ERROR_SIDE_RIGHT : ERROR_SIDE_LEFT;
}
}