/******************************************************************************* * Copyright (c) 2009-2013 Red Hat, Inc. * Distributed under license by Red Hat, Inc. All rights reserved. * This program is 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: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ package org.jboss.tools.jsf.web.validation; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.text.MessageFormat; 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.apache.xerces.xni.parser.XMLParseException; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.wst.validation.ValidationResult; import org.eclipse.wst.validation.ValidationState; import org.eclipse.wst.validation.internal.provisional.core.IMessage; import org.eclipse.wst.xml.core.internal.validation.XMLValidationInfo; import org.eclipse.wst.xml.core.internal.validation.core.NestedValidatorContext; import org.eclipse.wst.xml.core.internal.validation.core.ValidationMessage; import org.eclipse.wst.xml.core.internal.validation.core.ValidationReport; import org.eclipse.wst.xml.core.internal.validation.eclipse.Validator; import org.jboss.tools.common.util.FileUtil; import org.jboss.tools.jsf.JSFModelPlugin; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.LocatorImpl; /** * @author Victor V. Rubezhny */ @SuppressWarnings("restriction") public class XHTMLValidator extends Validator { public static final String PROBLEM_ID = JSFModelPlugin.PLUGIN_ID + "xhtmlsyntaxproblem"; private static final String XHML_VALIDATOR_CONTEXT = "org.jboss.tools.jsf.web.validation.xhtmlValidatorContext"; //$NON-NLS-1$ IProgressMonitor monitor; IResource resource; private String[] SAX_PARSER_FEATURES_TO_DISABLE = { "http://xml.org/sax/features/namespaces", "http://xml.org/sax/features/use-entity-resolver2", "http://xml.org/sax/features/validation", "http://apache.org/xml/features/validation/dynamic", "http://apache.org/xml/features/validation/schema", "http://apache.org/xml/features/validation/schema-full-checking", "http://apache.org/xml/features/nonvalidating/load-external-dtd", "http://apache.org/xml/features/nonvalidating/load-dtd-grammar", "http://apache.org/xml/features/xinclude", "http://xml.org/sax/features/resolve-dtd-uris" }; private void setSAXParserFeatures(XMLReader reader, String[] features, boolean set) { for (String feature : features) { try { reader.setFeature(feature, set); } catch (SAXException e) { JSFModelPlugin.getDefault().logError(e); } } } private void setSAXParserProperty(XMLReader reader, String property, Object value) { try { reader.setProperty(property, value); } catch (SAXException e) { JSFModelPlugin.getDefault().logError(e); } } private IDocument getDocument(IFile file) { if (file == null) { return null; } String content; try { content = FileUtil.readStream(file); } catch (CoreException e) { JSFModelPlugin.getDefault().logError(e); return null; } return (content == null ? null : new Document(content)); } /* * (non-Javadoc) * @see org.eclipse.wst.xml.core.internal.validation.core.AbstractNestedValidator#validate(org.eclipse.core.resources.IResource, int, org.eclipse.wst.validation.ValidationState, org.eclipse.core.runtime.IProgressMonitor) */ @Override public ValidationResult validate(IResource resource, int kind, ValidationState state, IProgressMonitor monitor) { displaySubtask(monitor, JSFValidationMessage.XHTML_VALIDATION, resource.getFullPath()); this.resource = resource; return super.validate(resource, kind, state, monitor); } @Override public ValidationReport validate(String uri, InputStream inputstream, NestedValidatorContext context) { displaySubtask(JSFValidationMessage.XHTML_VALIDATION, uri); this.resource = null; return super.validate(uri, inputstream, context); } @Override public ValidationReport validate(String uri, InputStream inputstream, NestedValidatorContext context, ValidationResult result) { displaySubtask(JSFValidationMessage.XHTML_VALIDATION, uri); IDocument doc = resource instanceof IFile ? getDocument((IFile)resource) : null; XMLValidationInfo report = new XMLValidationInfo(uri); if (doc == null || !isXHTML(doc)) { // System.out.println("XHTML is NOT Detected: " + resource.getFullPath().toString()); return report; // } else { // System.out.println("XHTML is Detected: " + resource.getFullPath().toString()); } XHTMLElementHandler handler = new XHTMLElementHandler(uri, (resource instanceof IFile ? getDocument((IFile)resource) : null), report); try { XMLReader xmlReader = new MySAXParser(); setSAXParserFeatures(xmlReader, SAX_PARSER_FEATURES_TO_DISABLE, false); setSAXParserProperty(xmlReader, "http://xml.org/sax/properties/lexical-handler", handler); xmlReader.setProperty("http://apache.org/xml/properties/internal/entity-resolver", new NullXMLEntityResolver()); xmlReader.setContentHandler(handler); xmlReader.setDTDHandler(handler); xmlReader.setErrorHandler(handler); xmlReader.setEntityResolver(handler); xmlReader.parse(uri); } catch (IOException e) { JSFModelPlugin.getDefault().logError(e); } catch (SAXNotRecognizedException e) { JSFModelPlugin.getDefault().logError(e); } catch (SAXNotSupportedException e) { JSFModelPlugin.getDefault().logError(e); } catch (SAXException e) { int max = handler.document.getLength(); int currentLocation = handler.getCurrentLocation(); int length = 0; if(max>0) { if(currentLocation+1>max) { currentLocation--; } else { length = 1; } } ElementLocation location = new ElementLocation(handler.locator.getLineNumber(), handler.locator.getColumnNumber(), currentLocation, length); report.addError(e.getLocalizedMessage(), handler.locator.getLineNumber(), handler.locator.getColumnNumber(), uri, null, new Object[] {location}); } return report; } public boolean isXHTML(IDocument document) { XHTMLDetector detector = new XHTMLDetector(new StringReader(document.get())); return detector.detect(); } /* * (non-Javadoc) * @see org.eclipse.wst.xml.core.internal.validation.eclipse.Validator#addInfoToMessage(org.eclipse.wst.xml.core.internal.validation.core.ValidationMessage, org.eclipse.wst.validation.internal.provisional.core.IMessage) */ @Override protected void addInfoToMessage(ValidationMessage validationMessage, IMessage message) { ElementLocation location = validationMessage.getMessageArguments() == null || validationMessage.getMessageArguments().length < 1 ? null: (ElementLocation)validationMessage.getMessageArguments()[0]; if (location != null) { message.setLineNo(location.getLine()); message.setOffset(location.getStart()); message.setLength(location.getLength()); } } /* * (non-Javadoc) * @see org.eclipse.wst.xml.core.internal.validation.eclipse.Validator#validationStarting(org.eclipse.core.resources.IProject, org.eclipse.wst.validation.ValidationState, org.eclipse.core.runtime.IProgressMonitor) */ @Override public void validationStarting(IProject project, ValidationState state, IProgressMonitor monitor) { if (project != null) { NestedValidatorContext context = getNestedContext(state, false); if (context == null) { context = getNestedContext(state, true); if (context != null) context.setProject(project); setupValidation(context); state.put(XHML_VALIDATOR_CONTEXT, context); } this.monitor = monitor; // Do not call the super.validationStarting(...) here (otherwise it // will break XML Validator context) } } /* * (non-Javadoc) * @see org.eclipse.wst.xml.core.internal.validation.eclipse.Validator#validationFinishing(org.eclipse.core.resources.IProject, org.eclipse.wst.validation.ValidationState, org.eclipse.core.runtime.IProgressMonitor) */ @Override public void validationFinishing(IProject project, ValidationState state, IProgressMonitor monitor) { if (project != null) { super.validationFinishing(project, state, monitor); NestedValidatorContext context = getNestedContext(state, false); if (context != null) { teardownValidation(context); state.put(XHML_VALIDATOR_CONTEXT, null); } } } private void displaySubtask(String message, Object... arguments) { displaySubtask(monitor, message, arguments); } private void displaySubtask(IProgressMonitor monitor, String message, Object... arguments) { if(monitor!=null) { monitor.subTask(MessageFormat.format(message, arguments)); } } class ElementLocation { int line; int column; int start; int length; ElementLocation (int line, int column, int start, int length) { this.line = line; this.column = column; this.start = start; this.length = length; } int getLine() { return line; } int getColumn() { return column; } int getStart() { return start; } int getLength() { return length; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("'"); sb.append("Line: "); sb.append(line); sb.append(", Column: "); sb.append(column); sb.append(", start: "); sb.append(start); sb.append(", end: "); sb.append((start + length)); sb.append(", length: "); sb.append(length); return sb.toString(); } } class NullXMLEntityResolver implements XMLEntityResolver { public XMLInputSource resolveEntity(XMLResourceIdentifier rid) throws XNIException, IOException { return new XMLInputSource(rid.getPublicId(), rid.getBaseSystemId()==null?rid.getLiteralSystemId():rid.getExpandedSystemId(), rid.getBaseSystemId(), new StringReader(""), null); } } class XHTMLElementHandler extends DefaultHandler implements LexicalHandler { private Locator locator; private IDocument document; public XHTMLElementHandler(String uri, IDocument document, XMLValidationInfo valinfo) { super(); this.document = document; } @Override public void setDocumentLocator(Locator locator) { this.locator = locator; } @Override public InputSource resolveEntity(String publicId, String systemId) throws IOException, SAXException { return new InputSource(new StringReader("")); //$NON-NLS-1$ } private int getCurrentLocation() { if (locator == null) { return 0; } int line = locator.getLineNumber() - 1; int lineOffset = locator.getColumnNumber() - 1; try { return document.getLineOffset(line) + lineOffset; } catch (BadLocationException e) { JSFModelPlugin.getDefault().logError(e); } return 0; } @Override public void endDTD() throws SAXException { } @Override public void startEntity(String name) throws SAXException { } @Override public void endEntity(String name) throws SAXException { } @Override public void startCDATA() throws SAXException { } @Override public void endCDATA() throws SAXException { } @Override public void comment(char[] ch, int start, int length) throws SAXException { } @Override public void startDTD(String name, String publicId, String systemId) throws SAXException { } } class MyXMLInputSource extends XMLInputSource { public MyXMLInputSource(String publicId, String systemId, String baseSystemId) { super(publicId, systemId, baseSystemId); } /* * (non-Javadoc) * @see org.apache.xerces.xni.parser.XMLInputSource#getByteStream() */ @Override public InputStream getByteStream() { InputStream stream = null; try { URL location = new URL(getSystemId()); String protocol = location.getProtocol(); if(!"http".equalsIgnoreCase(protocol) && !"https".equalsIgnoreCase(protocol)) { URLConnection connect = location.openConnection(); if (!(connect instanceof HttpURLConnection)) { stream = connect.getInputStream(); } } } catch (MalformedURLException e) { // Ignore (null will be returned as result) JSFModelPlugin.getPluginLog().logError(e); } catch (IOException e) { // Ignore (null will be returned as result) JSFModelPlugin.getPluginLog().logError(e); } return stream; } } class MySAXParser extends org.apache.xerces.parsers.SAXParser { /* * (non-Javadoc) * @see org.apache.xerces.parsers.AbstractSAXParser#parse(java.lang.String) */ @Override public void parse(String systemId) throws SAXException, IOException { // parse document XMLInputSource source = new MyXMLInputSource(null, systemId, null); try { parse(source); } // wrap XNI exceptions as SAX exceptions catch (XMLParseException e) { Exception ex = e.getException(); if (ex == null) { // must be a parser exception; mine it for locator info and throw // a SAXParseException LocatorImpl locatorImpl = new LocatorImpl(){ public String getXMLVersion() { return fVersion; } // since XMLParseExceptions know nothing about encoding, // we cannot return anything meaningful in this context. // We *could* consult the LocatorProxy, but the // application can do this itself if it wishes to possibly // be mislead. public String getEncoding() { return null; } }; locatorImpl.setPublicId(e.getPublicId()); locatorImpl.setSystemId(e.getExpandedSystemId()); locatorImpl.setLineNumber(e.getLineNumber()); locatorImpl.setColumnNumber(e.getColumnNumber()); throw new SAXParseException(e.getMessage(), locatorImpl); } if (ex instanceof SAXException) { // why did we create an XMLParseException? throw (SAXException)ex; } if (ex instanceof IOException) { throw (IOException)ex; } throw new SAXException(ex); } catch (XNIException e) { Exception ex = e.getException(); if (ex == null) { throw new SAXException(e.getMessage()); } if (ex instanceof SAXException) { throw (SAXException)ex; } if (ex instanceof IOException) { throw (IOException)ex; } throw new SAXException(ex); } } @Override public void parse(InputSource inputSource) throws SAXException, IOException { // parse document try { XMLInputSource xmlInputSource = new XMLInputSource(inputSource.getPublicId(), inputSource.getSystemId(), null); xmlInputSource.setByteStream(inputSource.getByteStream()); xmlInputSource.setCharacterStream(inputSource.getCharacterStream()); xmlInputSource.setEncoding(inputSource.getEncoding()); parse(xmlInputSource); } // wrap XNI exceptions as SAX exceptions catch (XMLParseException e) { Exception ex = e.getException(); if (ex == null) { // must be a parser exception; mine it for locator info and throw // a SAXParseException LocatorImpl locatorImpl = new LocatorImpl() { public String getXMLVersion() { return fVersion; } // since XMLParseExceptions know nothing about encoding, // we cannot return anything meaningful in this context. // We *could* consult the LocatorProxy, but the // application can do this itself if it wishes to possibly // be mislead. public String getEncoding() { return null; } }; locatorImpl.setPublicId(e.getPublicId()); locatorImpl.setSystemId(e.getExpandedSystemId()); locatorImpl.setLineNumber(e.getLineNumber()); locatorImpl.setColumnNumber(e.getColumnNumber()); throw new SAXParseException(e.getMessage(), locatorImpl); } if (ex instanceof SAXException) { // why did we create an XMLParseException? throw (SAXException)ex; } if (ex instanceof IOException) { throw (IOException)ex; } throw new SAXException(ex); } catch (XNIException e) { Exception ex = e.getException(); if (ex == null) { throw new SAXException(e.getMessage()); } if (ex instanceof SAXException) { throw (SAXException)ex; } if (ex instanceof IOException) { throw (IOException)ex; } throw new SAXException(ex); } } } }