blob: a8bc2617a05acb5f2c4968e1288e281e0bf59135 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2001, 2005 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.core.internal.validation;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import org.apache.xerces.impl.XMLErrorReporter;
import org.apache.xerces.parsers.StandardParserConfiguration;
import org.apache.xerces.xni.NamespaceContext;
import org.apache.xerces.xni.XMLLocator;
import org.apache.xerces.xni.XMLResourceIdentifier;
import org.apache.xerces.xni.XNIException;
import org.apache.xerces.xni.parser.XMLEntityResolver;
import org.apache.xerces.xni.parser.XMLInputSource;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolver;
import org.eclipse.wst.xml.core.internal.validation.core.LazyURLInputStream;
import org.eclipse.wst.xml.core.internal.validation.core.logging.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.DeclHandler;
/**
* This class performs validation using a xerces sax parser.
* Here's a quick overview of the details :
* - an ErrorHandler is used to collect errors into a list (so they may be displayed by the UI)
* - an EntityResolver is used along with the xerces "external-schemaLocation" property to implement XML Catalog support
*/
public class XMLValidator
{
protected URIResolver uriResolver = null;
//protected MyEntityResolver entityResolver = null;
protected Hashtable ingoredErrorKeyTable = new Hashtable();
protected static final String IGNORE_ALWAYS = "IGNORE_ALWAYS"; //$NON-NLS-1$
protected static final String IGNORE_IF_DTD_WITHOUT_ELEMENT_DECL = "IGNORE_IF_DTD_WITHOUT_ELEMENT_DECL"; //$NON-NLS-1$
protected static final String PREMATURE_EOF = "PrematureEOF"; //$NON-NLS-1$
protected static final String ROOT_ELEMENT_TYPE_MUST_MATCH_DOCTYPEDECL = "RootElementTypeMustMatchDoctypedecl"; //$NON-NLS-1$
protected static final String MSG_ELEMENT_NOT_DECLARED = "MSG_ELEMENT_NOT_DECLARED"; //$NON-NLS-1$
private static final String FILE_NOT_FOUND_KEY = "FILE_NOT_FOUND"; //$NON-NLS-1$
/**
* Constructor.
*/
public XMLValidator()
{
// Here we add some error keys that we need to filter out when we're validation
// against a DTD without any element declarations.
ingoredErrorKeyTable.put(PREMATURE_EOF, IGNORE_ALWAYS);
ingoredErrorKeyTable.put(ROOT_ELEMENT_TYPE_MUST_MATCH_DOCTYPEDECL, IGNORE_IF_DTD_WITHOUT_ELEMENT_DECL);
ingoredErrorKeyTable.put(MSG_ELEMENT_NOT_DECLARED, IGNORE_IF_DTD_WITHOUT_ELEMENT_DECL);
}
/**
* Set the URI Resolver to use.
*
* @param uriResolver The URI Resolver to use.
*/
public void setURIResolver(URIResolver uriResolver)
{
this.uriResolver = uriResolver;
//entityResolver = new MyEntityResolver(uriResolver);
}
/**
* Create an XML Reader.
*
* @return The newly created XML reader or null if unsuccessful.
* @throws Exception
*/
protected XMLReader createXMLReader(final XMLValidationInfo valinfo, XMLEntityResolver entityResolver) throws Exception
{
XMLReader reader = null;
// move to Xerces-2... add the contextClassLoader stuff
ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader();
try
{
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
StandardParserConfiguration configuration = new MyStandardParserConfiguration(valinfo);
reader = new org.apache.xerces.parsers.SAXParser(configuration)
{
public void startDocument(org.apache.xerces.xni.XMLLocator theLocator, java.lang.String encoding, NamespaceContext nscontext, org.apache.xerces.xni.Augmentations augs)
{
valinfo.setXMLLocator(theLocator);
super.startDocument(theLocator, encoding, nscontext, augs);
}
};
reader.setFeature("http://apache.org/xml/features/continue-after-fatal-error", false); //$NON-NLS-1$
reader.setFeature("http://xml.org/sax/features/namespace-prefixes", valinfo.isNamespaceEncountered()); //$NON-NLS-1$
reader.setFeature("http://xml.org/sax/features/namespaces", valinfo.isNamespaceEncountered()); //$NON-NLS-1$
reader.setFeature("http://xml.org/sax/features/validation", valinfo.isGrammarEncountered()); //$NON-NLS-1$
reader.setFeature("http://apache.org/xml/features/validation/schema", valinfo.isGrammarEncountered()); //$NON-NLS-1$
// MH make sure validation works even when a customer entityResolver is note set (i.e. via setURIResolver())
if (entityResolver != null)
{
reader.setProperty("http://apache.org/xml/properties/internal/entity-resolver", entityResolver); //$NON-NLS-1$
}
reader.setProperty("http://xml.org/sax/properties/declaration-handler", new MyDeclHandler()); //$NON-NLS-1$
}
catch(Exception e)
{
//TODO: log error message;
//e.printStackTrace();
}
finally
{
Thread.currentThread().setContextClassLoader(prevClassLoader);
}
return reader;
}
/**
* Validate the file located at the given URI.
*
* @param uri The URI of the file to validate.
* @return Returns an XML validation report.
*/
public XMLValidationReport validate(String uri)
{
return validate(uri, null);
}
final String createStringForInputStream(InputStream inputStream)
{
// Here we are reading the file and storing to a stringbuffer.
StringBuffer fileString = new StringBuffer();
try
{
InputStreamReader inputReader = new InputStreamReader(inputStream);
BufferedReader reader = new BufferedReader(inputReader);
char[] chars = new char[1024];
int numberRead = reader.read(chars);
while (numberRead != -1)
{
fileString.append(chars, 0, numberRead);
numberRead = reader.read(chars);
}
}
catch (Exception e)
{
//TODO: log error message
//e.printStackTrace();
}
return fileString.toString();
}
/**
* Validate the inputStream
*
* @param uri The URI of the file to validate.
* @param the inputStream of the file to validate
* @return Returns an XML validation report.
*/
public XMLValidationReport validate(String uri, InputStream inputStream)
{
Reader reader1 = null; // Used for the preparse.
Reader reader2 = null; // Used for validation parse.
if (inputStream != null)
{
String string = createStringForInputStream(inputStream);
reader1 = new StringReader(string);
reader2 = new StringReader(string);
}
XMLValidationInfo valinfo = new XMLValidationInfo(uri);
MyEntityResolver entityResolver = new MyEntityResolver(uriResolver);
ValidatorHelper helper = new ValidatorHelper();
try
{
helper.computeValidationInformation(uri, reader1, uriResolver);
valinfo.setDTDEncountered(helper.isDTDEncountered);
valinfo.setElementDeclarationCount(helper.numDTDElements);
valinfo.setNamespaceEncountered(helper.isNamespaceEncountered);
valinfo.setGrammarEncountered(helper.isGrammarEncountered);
XMLReader reader = createXMLReader(valinfo, entityResolver);
XMLErrorHandler errorhandler = new XMLErrorHandler(valinfo);
reader.setErrorHandler(errorhandler);
InputSource inputSource = new InputSource(uri);
inputSource.setCharacterStream(reader2);
reader.parse(inputSource);
}
catch (SAXParseException saxParseException)
{
// These errors are caught by the error handler.
//addValidationMessage(valinfo, saxParseException);
}
catch (IOException ioException)
{
addValidationMessage(valinfo, ioException);
}
catch (Exception exception)
{
LoggerFactory.getLoggerInstance().logError(exception.getLocalizedMessage(), exception);
}
return valinfo;
}
/**
* Add a validation message to the specified list.
*
* @param valinfo The validation info object to add the error to.
* @param exception The exception that contains the validation information.
*/
protected void addValidationMessage(XMLValidationInfo valinfo, IOException exception)
{
String validationMessageStr = exception.getMessage();
Throwable cause = exception.getCause() != null ? exception.getCause() : exception;
while(validationMessageStr == null && cause != null){
String localizedMessage = cause.getLocalizedMessage();
cause = cause.getCause();
if(cause == null && localizedMessage != null )
{
validationMessageStr = localizedMessage;
}
}
if (validationMessageStr != null)
{
if (cause instanceof FileNotFoundException)
{
validationMessageStr = NLS.bind(XMLValidationMessages._UI_PROBLEMS_VALIDATING_FILE_NOT_FOUND, new Object [] { validationMessageStr });
}
else if (cause instanceof UnknownHostException)
{
validationMessageStr = NLS.bind(XMLValidationMessages._UI_PROBLEMS_VALIDATING_UNKNOWN_HOST, new Object [] { validationMessageStr });
}
else if(cause instanceof ConnectException)
{
validationMessageStr = XMLValidationMessages._UI_PROBLEMS_CONNECTION_REFUSED;
}
}
if (validationMessageStr != null)
{
XMLLocator locator = valinfo.getXMLLocator();
valinfo.addWarning(validationMessageStr, locator != null ? locator.getLineNumber() : 1, locator != null ? locator.getColumnNumber() : 0, valinfo.getFileURI(), FILE_NOT_FOUND_KEY, null);
}
}
/**
* Add a validation message to the specified list.
*
* @param valinfo The validation info object to add the error to.
* @param exception The exception that contains the validation information.
*/
protected void addValidationMessage(XMLValidationInfo valinfo, SAXParseException exception)
{
if (exception.getMessage() != null)
{
valinfo.addError(exception.getLocalizedMessage(), exception.getLineNumber(), exception.getColumnNumber(), exception.getSystemId());
}
}
/**
* A custom entity resolver that uses the URI resolver specified to resolve entities.
*/
protected class MyEntityResolver implements XMLEntityResolver
{
private URIResolver uriResolver;
/**
* Constructor.
*
* @param uriResolver The URI resolver to use with this entity resolver.
*/
public MyEntityResolver(URIResolver uriResolver)
{
this.uriResolver = uriResolver;
}
/* (non-Javadoc)
* @see org.apache.xerces.xni.parser.XMLEntityResolver#resolveEntity(org.apache.xerces.xni.XMLResourceIdentifier)
*/
public XMLInputSource resolveEntity(XMLResourceIdentifier rid) throws XNIException, IOException
{
try
{
return _internalResolveEntity(uriResolver, rid);
}
catch(IOException e)
{
//e.printStackTrace();
}
return null;
}
}
// cs : I've refactored the common SAX based resolution code into this method for use by other validators
// (i.e. XML Schema, WSDL etc). The other approach is maintain a copy for each validator that has
// identical code. In any case we should strive to ensure that the validators perform resolution consistently.
public static XMLInputSource _internalResolveEntity(URIResolver uriResolver, XMLResourceIdentifier rid) throws IOException
{
XMLInputSource is = null;
if (uriResolver != null)
{
String id = rid.getPublicId();
if(id == null)
{
id = rid.getNamespace();
}
String location = null;
if (id != null || rid.getLiteralSystemId() != null)
{
location = uriResolver.resolve(rid.getBaseSystemId(), id, rid.getLiteralSystemId());
}
if (location != null)
{
String physical = uriResolver.resolvePhysicalLocation(rid.getBaseSystemId(), id, location);
is = new XMLInputSource(rid.getPublicId(), location, location);
is.setByteStream(new LazyURLInputStream(physical));
}
}
return is;
}
/**
* An error handler to catch errors encountered while parsing the XML document.
*/
protected class XMLErrorHandler implements org.xml.sax.ErrorHandler
{
private final int ERROR = 0;
private final int WARNING = 1;
private XMLValidationInfo valinfo;
/**
* Constructor.
*
* @param valinfo The XML validation info object that will hold the validation messages.
*/
public XMLErrorHandler(XMLValidationInfo valinfo)
{
this.valinfo = valinfo;
}
/**
* Add a validation message with the given severity.
*
* @param exception The exception that contains the message.
* @param severity The severity of the message.
*/
protected void addValidationMessage(SAXParseException exception, int severity)
{
if(exception.getSystemId() != null)
{
if(severity == WARNING)
{
valinfo.addWarning(exception.getLocalizedMessage(), exception.getLineNumber(), exception.getColumnNumber(), exception.getSystemId());
}
else
{
valinfo.addError(exception.getLocalizedMessage(), exception.getLineNumber(), exception.getColumnNumber(), exception.getSystemId(), valinfo.getCurrentErrorKey(), valinfo.getMessageArguments());
}
}
}
/* (non-Javadoc)
* @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
*/
public void error(SAXParseException exception) throws SAXException
{
addValidationMessage(exception, ERROR);
}
/* (non-Javadoc)
* @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
*/
public void fatalError(SAXParseException exception) throws SAXException
{
addValidationMessage(exception, ERROR);
}
/* (non-Javadoc)
* @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
*/
public void warning(SAXParseException exception) throws SAXException
{
addValidationMessage(exception, WARNING);
}
}
/**
* This class is used to count the elementDecls that are encountered in a DTD.
*/
protected class MyDeclHandler implements DeclHandler
{
/**
* Constructor.
*
* @param valinfo The XMLValidationInfo object that will count the declarations.
*/
public MyDeclHandler()
{
}
/* (non-Javadoc)
* @see org.xml.sax.ext.DeclHandler#attributeDecl(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
public void attributeDecl(String eName, String aName, String type, String valueDefault, String value)
{
}
/* (non-Javadoc)
* @see org.xml.sax.ext.DeclHandler#elementDecl(java.lang.String, java.lang.String)
*/
public void elementDecl(String name, String model)
{
//valinfo.increaseElementDeclarationCount();
}
/* (non-Javadoc)
* @see org.xml.sax.ext.DeclHandler#externalEntityDecl(java.lang.String, java.lang.String, java.lang.String)
*/
public void externalEntityDecl(String name, String publicId, String systemId)
{
}
/* (non-Javadoc)
* @see org.xml.sax.ext.DeclHandler#internalEntityDecl(java.lang.String, java.lang.String)
*/
public void internalEntityDecl(String name, String value)
{
}
}
/**
* A StandardParserConfiguration that creates an error reporter which can ignore
* DTD error messages for DTD's with no elements defined.
*/
protected class MyStandardParserConfiguration extends StandardParserConfiguration
{
XMLValidationInfo valinfo = null;
List reportedExceptions = new ArrayList();
/**
* Constructor.
*
* @param valinfo The XMLValidationInfo object to use.
*/
public MyStandardParserConfiguration(XMLValidationInfo valinfo)
{
this.valinfo = valinfo;
}
/* (non-Javadoc)
* @see org.apache.xerces.parsers.DTDConfiguration#createErrorReporter()
*/
protected XMLErrorReporter createErrorReporter()
{
return new XMLErrorReporter()
{
/* (non-Javadoc)
* @see org.apache.xerces.impl.XMLErrorReporter#reportError(java.lang.String, java.lang.String, java.lang.Object[], short)
*/
public void reportError(String domain, String key, Object[] arguments, short severity) throws XNIException
{
boolean reportError = true;
valinfo.setCurrentErrorKey(key);
valinfo.setMessageArguments(arguments);
String ignoreCondition = (String)ingoredErrorKeyTable.get(key);
if (ignoreCondition != null)
{
if (ignoreCondition.equals(XMLValidator.IGNORE_IF_DTD_WITHOUT_ELEMENT_DECL))
{
boolean isDTDWithoutElementDeclarationEncountered = valinfo.isDTDWithoutElementDeclarationEncountered();
reportError = !isDTDWithoutElementDeclarationEncountered;
}
else
{
reportError = false;
}
}
if ("schema_reference.4".equals(key) && arguments.length > 0) //$NON-NLS-1$
{
Object location = arguments[0];
if (location != null)
{
if(reportedExceptions.contains(location))
{
reportError = false;
}
else
{
reportedExceptions.add(location);
}
}
}
if (reportError)
{
super.reportError(domain, key, arguments, severity);
}
}
};
}
}
}