From 7d75b83a0984f7adad3b2090ba016a64fa25d41f Mon Sep 17 00:00:00 2001 From: azerr Date: Mon, 11 May 2020 18:24:01 +0200 Subject: [PATCH] Validate XML with DTD/XML Schema by using xml-model See #697 Signed-off-by: azerr --- .../org/eclipse/lemminx/dom/DOMDocument.java | 41 +- .../participants/XMLSchemaErrorCode.java | 6 +- .../diagnostics/LSPErrorReporterForXML.java | 2 +- .../LSPXMLParserConfiguration.java | 4 +- .../diagnostics/XMLValidator.java | 2 +- .../xerces/AbstractLSPErrorReporter.java | 2 +- .../xerces/LSPMessageFormatter.java | 2 +- .../XMLModelAwareParserConfiguration.java | 117 ++++++ .../xerces/xmlmodel/XMLModelConstants.java | 26 ++ .../xerces/xmlmodel/XMLModelDTDValidator.java | 136 +++++++ .../xerces/xmlmodel/XMLModelDeclaration.java | 108 +++++ .../xerces/xmlmodel/XMLModelHandler.java | 381 ++++++++++++++++++ .../xmlmodel/XMLModelSchemaValidator.java | 99 +++++ .../xerces/xmlmodel/XMLModelValidator.java | 28 ++ .../diagnostics/LSPErrorReporterForXSD.java | 2 +- .../contentmodel/XMLModelDiagnosticsTest.java | 69 ++++ .../test/resources/xml-model/bound-to-dtd.xml | 5 + .../xml-model/bound-to-xsd-with-ns.xml | 8 + .../test/resources/xml-model/bound-to-xsd.xml | 6 + 19 files changed, 1034 insertions(+), 10 deletions(-) rename org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/{services => }/extensions/xerces/AbstractLSPErrorReporter.java (96%) rename org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/{services => }/extensions/xerces/LSPMessageFormatter.java (99%) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelAwareParserConfiguration.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelConstants.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelDTDValidator.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelDeclaration.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelHandler.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelSchemaValidator.java create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelValidator.java create mode 100644 org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLModelDiagnosticsTest.java create mode 100644 org.eclipse.lemminx/src/test/resources/xml-model/bound-to-dtd.xml create mode 100644 org.eclipse.lemminx/src/test/resources/xml-model/bound-to-xsd-with-ns.xml create mode 100644 org.eclipse.lemminx/src/test/resources/xml-model/bound-to-xsd.xml diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMDocument.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMDocument.java index af7fa6940..2abc46194 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMDocument.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom/DOMDocument.java @@ -43,6 +43,7 @@ import org.w3c.dom.DocumentFragment; import org.w3c.dom.EntityReference; import org.w3c.dom.NodeList; +import org.w3c.dom.ProcessingInstruction; /** * XML document. @@ -50,6 +51,8 @@ */ public class DOMDocument extends DOMNode implements Document { + private static final String XML_MODEL_PI = "xml-model"; + private SchemaLocation schemaLocation; private NoNamespaceSchemaLocation noNamespaceSchemaLocation; private boolean referencedExternalGrammarInitialized; @@ -148,7 +151,20 @@ public TextDocument getTextDocument() { * @return true if the document is bound to a grammar and false otherwise. */ public boolean hasGrammar() { - return hasDTD() || hasSchemaLocation() || hasNoNamespaceSchemaLocation() || hasExternalGrammar(); + return hasGrammar(false); + } + + /** + * Returns true if the document is bound to a grammar and false otherwise. + * + * @param excludeXMLModel true if xml-model must be excluded and false + * otherwise. + * + * @return true if the document is bound to a grammar and false otherwise. + */ + public boolean hasGrammar(boolean excludeXMLModel) { + return hasDTD() || hasSchemaLocation() || hasNoNamespaceSchemaLocation() || hasExternalGrammar() + || (!excludeXMLModel && hasXMLModel()); } // -------------------------- Grammar with XML Schema @@ -319,6 +335,29 @@ public boolean hasDTD() { return getDoctype() != null; } + // -------------------------- Grammar with children = getChildren(); + if (children != null && !children.isEmpty()) { + return children.stream().anyMatch(child -> { + return isXMLModel(child); + }); + } + return false; + } + + private static boolean isXMLModel(DOMNode node) { + return node.isProcessingInstruction() && XML_MODEL_PI.equals(((ProcessingInstruction) node).getTarget()); + } + // -------------------------- External Grammar (XML file associations, catalog) /** diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java index 617534e0d..5e8c7c728 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/XMLSchemaErrorCode.java @@ -166,7 +166,7 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj } case SchemaLocation: case schema_reference_4: { - DOMNode attrValueNode; + DOMNode attrValueNode = null; if (code.equals(SchemaLocation)) { SchemaLocation schemaLocation = document.getSchemaLocation(); attrValueNode = schemaLocation.getAttr().getNodeAttrValue(); @@ -176,7 +176,9 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj attrValueNode = noNamespaceSchemaLocation.getAttr().getNodeAttrValue(); } else { SchemaLocation schemaLocation = document.getSchemaLocation(); - attrValueNode = schemaLocation.getAttr().getNodeAttrValue(); + if (schemaLocation != null) { + attrValueNode = schemaLocation.getAttr().getNodeAttrValue(); + } } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/LSPErrorReporterForXML.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/LSPErrorReporterForXML.java index 4d3188e07..a5efcdb86 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/LSPErrorReporterForXML.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/LSPErrorReporterForXML.java @@ -19,7 +19,7 @@ import org.eclipse.lemminx.extensions.contentmodel.participants.DTDErrorCode; import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSchemaErrorCode; import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSyntaxErrorCode; -import org.eclipse.lemminx.services.extensions.xerces.AbstractLSPErrorReporter; +import org.eclipse.lemminx.extensions.xerces.AbstractLSPErrorReporter; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.Range; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/LSPXMLParserConfiguration.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/LSPXMLParserConfiguration.java index 51f9b8415..0bcdcfee3 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/LSPXMLParserConfiguration.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/LSPXMLParserConfiguration.java @@ -12,12 +12,12 @@ package org.eclipse.lemminx.extensions.contentmodel.participants.diagnostics; import org.apache.xerces.impl.dtd.XMLDTDValidator; -import org.apache.xerces.parsers.XIncludeAwareParserConfiguration; import org.apache.xerces.xni.XNIException; import org.apache.xerces.xni.grammars.XMLGrammarPool; import org.apache.xerces.xni.parser.XMLComponentManager; import org.apache.xerces.xni.parser.XMLConfigurationException; import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationSettings; +import org.eclipse.lemminx.extensions.xerces.xmlmodel.XMLModelAwareParserConfiguration; /** * Custom Xerces XML parser configuration to : @@ -31,7 +31,7 @@ * * */ -class LSPXMLParserConfiguration extends XIncludeAwareParserConfiguration { +class LSPXMLParserConfiguration extends XMLModelAwareParserConfiguration { private final boolean disableDTDValidation; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/XMLValidator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/XMLValidator.java index 4bad45cb7..3ee617f96 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/XMLValidator.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/participants/diagnostics/XMLValidator.java @@ -71,7 +71,7 @@ public static void doDiagnostics(DOMDocument document, XMLEntityResolver entityR // Add LSP content handler to stop XML parsing if monitor is canceled. parser.setContentHandler(new LSPContentHandler(monitor)); - boolean hasGrammar = document.hasGrammar(); + boolean hasGrammar = document.hasGrammar(true); // If diagnostics for Schema preference is enabled if ((validationSettings == null) || validationSettings.isSchema()) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/xerces/AbstractLSPErrorReporter.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/AbstractLSPErrorReporter.java similarity index 96% rename from org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/xerces/AbstractLSPErrorReporter.java rename to org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/AbstractLSPErrorReporter.java index a74031831..ceaa76a40 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/xerces/AbstractLSPErrorReporter.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/AbstractLSPErrorReporter.java @@ -10,7 +10,7 @@ * Contributors: * Angelo Zerr - initial API and implementation */ -package org.eclipse.lemminx.services.extensions.xerces; +package org.eclipse.lemminx.extensions.xerces; import java.util.List; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/xerces/LSPMessageFormatter.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/LSPMessageFormatter.java similarity index 99% rename from org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/xerces/LSPMessageFormatter.java rename to org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/LSPMessageFormatter.java index f7cc79159..93c6de6da 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/xerces/LSPMessageFormatter.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/LSPMessageFormatter.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.eclipse.lemminx.services.extensions.xerces; +package org.eclipse.lemminx.extensions.xerces; import static org.eclipse.lemminx.dom.parser.Constants.*; import static org.eclipse.lemminx.utils.StringUtils.getString; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelAwareParserConfiguration.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelAwareParserConfiguration.java new file mode 100644 index 000000000..c063c9731 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelAwareParserConfiguration.java @@ -0,0 +1,117 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.xerces.xmlmodel; + +import org.apache.xerces.parsers.XIncludeAwareParserConfiguration; +import org.apache.xerces.util.SymbolTable; +import org.apache.xerces.xni.XMLDocumentHandler; +import org.apache.xerces.xni.grammars.XMLGrammarPool; +import org.apache.xerces.xni.parser.XMLComponentManager; +import org.apache.xerces.xni.parser.XMLDocumentSource; + +/** + * This class is the configuration used to parse XML 1.0 and XML 1.1 documents + * and provides support for xml-model association. + * + * @see https://www.w3.org/TR/xml-model/ + */ +public class XMLModelAwareParserConfiguration extends XIncludeAwareParserConfiguration { + + protected boolean xmlModelEnabled = true; + private XMLModelHandler xmlModelHandler; + + /** Default constructor. */ + public XMLModelAwareParserConfiguration() { + this(null, null, null); + } // () + + /** + * Constructs a parser configuration using the specified symbol table. + * + * @param symbolTable The symbol table to use. + */ + public XMLModelAwareParserConfiguration(SymbolTable symbolTable) { + this(symbolTable, null, null); + } // (SymbolTable) + + /** + * Constructs a parser configuration using the specified symbol table and + * grammar pool. + *

+ * + * @param symbolTable The symbol table to use. + * @param grammarPool The grammar pool to use. + */ + public XMLModelAwareParserConfiguration(SymbolTable symbolTable, XMLGrammarPool grammarPool) { + this(symbolTable, grammarPool, null); + } // (SymbolTable,XMLGrammarPool) + + /** + * Constructs a parser configuration using the specified symbol table, grammar + * pool, and parent settings. + *

+ * + * @param symbolTable The symbol table to use. + * @param grammarPool The grammar pool to use. + * @param parentSettings The parent settings. + */ + public XMLModelAwareParserConfiguration(SymbolTable symbolTable, XMLGrammarPool grammarPool, + XMLComponentManager parentSettings) { + super(symbolTable, grammarPool, parentSettings); + } + + @Override + protected void configurePipeline() { + super.configurePipeline(); + configureXMLModelPipeline(); + } + + @Override + protected void configureXML11Pipeline() { + super.configureXML11Pipeline(); + configureXMLModelPipeline(); + } + + private void configureXMLModelPipeline() { + if (xmlModelEnabled) { + // If the xml-model handler was not in the pipeline insert it. + if (xmlModelHandler == null) { + xmlModelHandler = new XMLModelHandler(); + // add XMLModel component + // setProperty(XMLModel_HANDLER, fXMLModelHandler); + addCommonComponent(xmlModelHandler); + xmlModelHandler.reset(this); + } + // configure XML document pipeline: insert after DTDValidator and + // before XML Schema validator + XMLDocumentSource prev = null; + if (fFeatures.get(XMLSCHEMA_VALIDATION) == Boolean.TRUE) { + // we don't have to worry about fSchemaValidator being null, since + // super.configurePipeline() instantiated it if the feature was set + prev = fSchemaValidator.getDocumentSource(); + } + // Otherwise, insert after the last component in the pipeline + else { + prev = fLastComponent; + fLastComponent = xmlModelHandler; + } + + XMLDocumentHandler next = prev.getDocumentHandler(); + prev.setDocumentHandler(xmlModelHandler); + xmlModelHandler.setDocumentSource(prev); + if (next != null) { + xmlModelHandler.setDocumentHandler(next); + next.setDocumentSource(xmlModelHandler); + } + } + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelConstants.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelConstants.java new file mode 100644 index 000000000..1fa6deb51 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelConstants.java @@ -0,0 +1,26 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.xerces.xmlmodel; + +/** + * XML model constants. + * + */ +public class XMLModelConstants { + + private XMLModelConstants() { + } + + public static final String XML_MODEL_PI = "xml-model"; + + public static final String HREF_ATTR = "href"; +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelDTDValidator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelDTDValidator.java new file mode 100644 index 000000000..75a96aec2 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelDTDValidator.java @@ -0,0 +1,136 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.xerces.xmlmodel; + +import java.io.IOException; +import java.lang.reflect.Field; + +import org.apache.xerces.impl.Constants; +import org.apache.xerces.impl.XMLEntityManager; +import org.apache.xerces.impl.dtd.DTDGrammar; +import org.apache.xerces.impl.dtd.XMLDTDDescription; +import org.apache.xerces.impl.dtd.XMLDTDLoader; +import org.apache.xerces.impl.dtd.XMLDTDValidator; +import org.apache.xerces.xni.Augmentations; +import org.apache.xerces.xni.QName; +import org.apache.xerces.xni.XMLAttributes; +import org.apache.xerces.xni.XMLLocator; +import org.apache.xerces.xni.XNIException; +import org.apache.xerces.xni.parser.XMLComponentManager; +import org.apache.xerces.xni.parser.XMLConfigurationException; +import org.apache.xerces.xni.parser.XMLInputSource; + +/** + * XML model validator which process validation with DTD: + * + *

+ * 	<?xml-model href="http://java.sun.com/dtd/web-app_2_3.dtd"?>
+ * 
+ * + */ +public class XMLModelDTDValidator extends XMLDTDValidator implements XMLModelValidator { + + private static final String ENTITY_MANAGER = Constants.XERCES_PROPERTY_PREFIX + Constants.ENTITY_MANAGER_PROPERTY; + + private String href; + private boolean rootElement; + private XMLLocator locator; + private XMLEntityManager entityManager; + + public XMLModelDTDValidator() { + rootElement = true; + fDTDValidation = true; + } + + @Override + public void setHref(String href) { + this.href = href; + } + + @Override + public void setLocator(XMLLocator locator) { + this.locator = locator; + } + + @Override + public void startElement(QName element, XMLAttributes attributes, Augmentations augs) throws XNIException { + if (rootElement) { + QName fRootElement = getRootElement(); + String rootElementName = element.localpart; + + // save root element state + fSeenDoctypeDecl = true; + fRootElement.setValues(null, rootElementName, rootElementName, null); + + String eid = null; + try { + eid = XMLEntityManager.expandSystemId(href, locator.getExpandedSystemId(), false); + } catch (java.io.IOException e) { + } + XMLDTDDescription grammarDesc = new XMLDTDDescription(null, href, locator.getExpandedSystemId(), eid, + rootElementName); + fDTDGrammar = fGrammarBucket.getGrammar(grammarDesc); + if (fDTDGrammar == null) { + // give grammar pool a chance... + // + // Do not bother checking the pool if no public or system identifier was + // provided. + // Since so many different DTDs have roots in common, using only a root name as + // the + // key may cause an unexpected grammar to be retrieved from the grammar pool. + // This scenario + // would occur when an ExternalSubsetResolver has been queried and the + // XMLInputSource returned contains an input stream but no external identifier. + // This can never happen when the instance document specified a DOCTYPE. -- + // mrglavas + if (fGrammarPool != null && (href != null)) { + fDTDGrammar = (DTDGrammar) fGrammarPool.retrieveGrammar(grammarDesc); + } + } + if (fDTDGrammar == null) { + + XMLDTDLoader loader = new XMLDTDLoader(fSymbolTable, fGrammarPool); + loader.setEntityResolver(entityManager); + try { + fDTDGrammar = (DTDGrammar) loader.loadGrammar(new XMLInputSource(null, eid, null)); + } catch (IOException e) { + // TODO : manage report error for DTD not found in xml-model/@ref + } + } else { + // we've found a cached one;so let's make sure not to read + // any external subset! + fValidationManager.setCachedDTD(true); + } + rootElement = false; + } + super.startElement(element, attributes, augs); + } + + private QName getRootElement() { + try { + // fRootElement is declared as private in the XMLDTDValidator, we must use ugly + // Java reflection to get the field. + Field f = XMLDTDValidator.class.getDeclaredField("fRootElement"); + f.setAccessible(true); + QName fRootElement = (QName) f.get(this); + return fRootElement; + } catch (Exception e) { + return null; + } + } + + @Override + public void reset(XMLComponentManager componentManager) throws XMLConfigurationException { + entityManager = (XMLEntityManager) componentManager.getProperty(ENTITY_MANAGER); + super.reset(componentManager); + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelDeclaration.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelDeclaration.java new file mode 100644 index 000000000..c6cb8c268 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelDeclaration.java @@ -0,0 +1,108 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.xerces.xmlmodel; + +import org.apache.xerces.xni.XMLString; + +/** + * XML model declaration. + * + *
+ * 	<?xml-model href="http://www.docbook.org/xml/5.0/xsd/docbook.xsd"?>
+ * 
+ * + * + * @see https://www.w3.org/TR/xml-model/ + * + */ +public class XMLModelDeclaration { + private String href; + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + private enum State { + Content, AttName, AfterAttName, AfterEquals, AttValue; + } + + public static XMLModelDeclaration parse(XMLString data) { + return parse(data.ch, data.offset, data.length); + } + + /** + * Returns the result of parse the data of xml-model processing insruction. + * + * @param data the xml-model processing instruction content data. + * @param offset the offset + * @param length the length + * @return the result of parse the data of xml-model processing insruction. + */ + public static XMLModelDeclaration parse(char[] data, int offset, int length) { + XMLModelDeclaration model = new XMLModelDeclaration(); + StringBuilder name = new StringBuilder(); + StringBuilder value = new StringBuilder(); + State state = State.Content; + char equals = '"'; + for (int i = offset; i < length; i++) { + char ch = data[i]; + switch (state) { + case Content: + if (!Character.isWhitespace(ch)) { + name.append(ch); + state = State.AttName; + } + break; + case AttName: + if (Character.isWhitespace(ch)) { + state = State.AfterAttName; + } else if (ch == '=') { + state = State.AfterEquals; + } else { + name.append(ch); + } + break; + case AfterAttName: + if (ch == '=') { + state = State.AfterEquals; + } + break; + case AfterEquals: + if (ch == '"' || ch == '\'') { + equals = ch; + state = State.AttValue; + } + break; + case AttValue: + if (ch == equals) { + state = State.Content; + switch (name.toString()) { + case XMLModelConstants.HREF_ATTR: + model.setHref(value.toString()); + break; + } + name.setLength(0); + value.setLength(0); + } else { + value.append(ch); + } + break; + } + } + + return model; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelHandler.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelHandler.java new file mode 100644 index 000000000..4be79f190 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelHandler.java @@ -0,0 +1,381 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.xerces.xmlmodel; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.xerces.impl.Constants; +import org.apache.xerces.xni.Augmentations; +import org.apache.xerces.xni.NamespaceContext; +import org.apache.xerces.xni.QName; +import org.apache.xerces.xni.XMLAttributes; +import org.apache.xerces.xni.XMLDocumentHandler; +import org.apache.xerces.xni.XMLLocator; +import org.apache.xerces.xni.XMLResourceIdentifier; +import org.apache.xerces.xni.XMLString; +import org.apache.xerces.xni.XNIException; +import org.apache.xerces.xni.parser.XMLComponent; +import org.apache.xerces.xni.parser.XMLComponentManager; +import org.apache.xerces.xni.parser.XMLConfigurationException; +import org.apache.xerces.xni.parser.XMLDocumentFilter; +import org.apache.xerces.xni.parser.XMLDocumentSource; +import org.eclipse.lemminx.utils.StringUtils; + +/** + * Xerces component which associates a XML with several grammar (XML Schema, + * DTD, ..) by using <?xml-model ?> processing instruction. + * + *
+ * 	<?xml-model href="http://www.docbook.org/xml/5.0/xsd/docbook.xsd"?>
+ *	<book xmlns="http://docbook.org/ns/docbook">
+ *		<title />
+ *	</book>
+ * 
+ * + * @see https://www.w3.org/TR/xml-model/ + * + */ +public class XMLModelHandler implements XMLComponent, XMLDocumentFilter { + + private static final String VALIDATION = Constants.SAX_FEATURE_PREFIX + Constants.VALIDATION_FEATURE; + + private static final String PARSER_SETTINGS = Constants.XERCES_FEATURE_PREFIX + Constants.PARSER_SETTINGS; + + private List xmlModelValidators; + private XMLComponentManager configuration; + + private XMLDocumentHandler documentHandler; + + private XMLDocumentSource documentSource; + + private XMLLocator locator; + + public XMLModelHandler() { + } + + @Override + public void reset(XMLComponentManager componentManager) throws XMLConfigurationException { + // XML model validators uses Xerces XMLDTDValidator (for DTD) and + // XMLSchemaValidator (for XML Schema). + // Those validators are created when a xml-model processing instruction is + // parsed and not before the XML parse (which is the case for standard DTD + // DOCTYPE or xsi:schemaLocation) + + // That's why we need to force some features,by wrapping the existing Xerces + // configuration to force to true the features: + // - http://apache.org/xml/features/internal/parser-settings + // - http://xml.org/sax/features/validation + configuration = new XMLComponentManager() { + + @Override + public Object getProperty(String propertyId) throws XMLConfigurationException { + return componentManager.getProperty(propertyId); + } + + @Override + public boolean getFeature(String featureId) throws XMLConfigurationException { + if (PARSER_SETTINGS.equals(featureId)) { + return true; + } + if (VALIDATION.equals(featureId)) { + return true; + } + return componentManager.getFeature(featureId); + } + }; + } + + @Override + public void processingInstruction(String target, XMLString data, Augmentations augs) throws XNIException { + if (XMLModelConstants.XML_MODEL_PI.equals(target)) { + XMLModelDeclaration model = XMLModelDeclaration.parse(data); + XMLModelValidator validator = createValidator(model); + if (validator != null) { + validator.reset(configuration); + validator.setHref(model.getHref()); + if (xmlModelValidators == null) { + xmlModelValidators = new ArrayList<>(); + } + xmlModelValidators.add(validator); + } + } + + if (documentHandler != null) { + documentHandler.processingInstruction(target, data, augs); + } + } + + private XMLModelValidator createValidator(XMLModelDeclaration model) { + String href = model.getHref(); + if (StringUtils.isEmpty(href)) { + return null; + } + if (href.endsWith("xsd")) { + return new XMLModelSchemaValidator(); + } else if (href.endsWith("dtd")) { + return new XMLModelDTDValidator(); + } + return null; + } + + @Override + public void startDocument(XMLLocator locator, String encoding, NamespaceContext namespaceContext, + Augmentations augs) throws XNIException { + this.locator = locator; + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.startDocument(locator, encoding, namespaceContext, augs); + } + } + + if (documentHandler != null) { + documentHandler.startDocument(locator, encoding, namespaceContext, augs); + } + } + + @Override + public void xmlDecl(String version, String encoding, String standalone, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.xmlDecl(version, encoding, standalone, augs); + } + } + + if (documentHandler != null) { + documentHandler.xmlDecl(version, encoding, standalone, augs); + } + } + + @Override + public void doctypeDecl(String rootElement, String publicId, String systemId, Augmentations augs) + throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.doctypeDecl(rootElement, publicId, systemId, augs); + } + } + + if (documentHandler != null) { + documentHandler.doctypeDecl(rootElement, publicId, systemId, augs); + } + } + + @Override + public void comment(XMLString text, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.comment(text, augs); + } + } + + if (documentHandler != null) { + documentHandler.comment(text, augs); + } + } + + @Override + public void startElement(QName element, XMLAttributes attributes, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.setLocator(locator); + validator.startElement(element, attributes, augs); + } + } + + if (documentHandler != null) { + documentHandler.startElement(element, attributes, augs); + } + } + + @Override + public void emptyElement(QName element, XMLAttributes attributes, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.emptyElement(element, attributes, augs); + } + } + + if (documentHandler != null) { + documentHandler.emptyElement(element, attributes, augs); + } + } + + @Override + public void startGeneralEntity(String name, XMLResourceIdentifier identifier, String encoding, Augmentations augs) + throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.startGeneralEntity(name, identifier, encoding, augs); + } + } + + if (documentHandler != null) { + documentHandler.startGeneralEntity(name, identifier, encoding, augs); + } + } + + @Override + public void textDecl(String version, String encoding, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.textDecl(version, encoding, augs); + } + } + + if (documentHandler != null) { + documentHandler.textDecl(version, encoding, augs); + } + } + + @Override + public void endGeneralEntity(String name, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.endGeneralEntity(name, augs); + } + } + + if (documentHandler != null) { + documentHandler.endGeneralEntity(name, augs); + } + } + + @Override + public void characters(XMLString text, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.characters(text, augs); + } + } + + if (documentHandler != null) { + documentHandler.characters(text, augs); + } + } + + @Override + public void ignorableWhitespace(XMLString text, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.ignorableWhitespace(text, augs); + } + } + + if (documentHandler != null) { + documentHandler.ignorableWhitespace(text, augs); + } + } + + @Override + public void endElement(QName element, Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.endElement(element, augs); + } + } + + if (documentHandler != null) { + documentHandler.endElement(element, augs); + } + } + + @Override + public void startCDATA(Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.startCDATA(augs); + } + } + + if (documentHandler != null) { + documentHandler.startCDATA(augs); + } + } + + @Override + public void endCDATA(Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.endCDATA(augs); + } + } + + if (documentHandler != null) { + documentHandler.endCDATA(augs); + } + } + + @Override + public void endDocument(Augmentations augs) throws XNIException { + if (xmlModelValidators != null) { + for (XMLModelValidator validator : xmlModelValidators) { + validator.endDocument(augs); + } + } + + if (documentHandler != null) { + documentHandler.endDocument(augs); + } + } + + @Override + public void setDocumentSource(XMLDocumentSource source) { + documentSource = source; + } + + @Override + public XMLDocumentSource getDocumentSource() { + return documentSource; + } + + @Override + public void setDocumentHandler(XMLDocumentHandler handler) { + documentHandler = handler; + } + + @Override + public XMLDocumentHandler getDocumentHandler() { + return documentHandler; + } + + @Override + public String[] getRecognizedFeatures() { + return null; + } + + @Override + public void setFeature(String featureId, boolean state) throws XMLConfigurationException { + + } + + @Override + public String[] getRecognizedProperties() { + return null; + } + + @Override + public void setProperty(String propertyId, Object value) throws XMLConfigurationException { + + } + + @Override + public Boolean getFeatureDefault(String featureId) { + return null; + } + + @Override + public Object getPropertyDefault(String propertyId) { + return null; + } + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelSchemaValidator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelSchemaValidator.java new file mode 100644 index 000000000..8df6b6473 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelSchemaValidator.java @@ -0,0 +1,99 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.xerces.xmlmodel; + +import org.apache.xerces.impl.XMLErrorReporter; +import org.apache.xerces.impl.xs.XMLSchemaLoader; +import org.apache.xerces.impl.xs.XMLSchemaValidator; +import org.apache.xerces.xni.Augmentations; +import org.apache.xerces.xni.QName; +import org.apache.xerces.xni.XMLAttributes; +import org.apache.xerces.xni.XMLLocator; +import org.apache.xerces.xni.XNIException; +import org.apache.xerces.xni.parser.XMLComponentManager; +import org.apache.xerces.xni.parser.XMLConfigurationException; +import org.eclipse.lemminx.utils.StringUtils; + +/** + * XML model validator which process validation with XML Schema: + * + *
+ * 	<?xml-model href="http://www.docbook.org/xml/5.0/xsd/docbook.xsd"?>
+ * 
+ * + */ +public class XMLModelSchemaValidator extends XMLSchemaValidator implements XMLModelValidator { + + private static final String XMLNS_ATTR = "xmlns"; + + private XMLErrorReporter errorReporter; + private boolean rootElement; + private String href; + + public XMLModelSchemaValidator() { + rootElement = true; + } + + @Override + public void reset(XMLComponentManager componentManager) throws XMLConfigurationException { + super.reset(componentManager); + // force XML Schema validation + fDoValidation = true; + // Get error reporter. + try { + XMLErrorReporter value = (XMLErrorReporter) componentManager.getProperty(ERROR_REPORTER); + if (value != null) { + errorReporter = value; + } + } catch (XMLConfigurationException e) { + errorReporter = null; + } + } + + public void setHref(String href) { + this.href = href; + } + + @Override + public void setLocator(XMLLocator locator) { + + } + + @Override + public void startElement(QName element, XMLAttributes attributes, Augmentations augs) throws XNIException { + if (rootElement) { + // on the document element, we associate the XML Schema declared in same support than + // xsi:noNamespaceSchemaLocation. Ex: + /** + * + **/ + String noNamespaceSchemaLocation = href; + XMLSchemaLoader.processExternalHints(null, noNamespaceSchemaLocation, fLocationPairs, errorReporter); + } else { + // XML defines a xmlns attribute in the root element -> same support than + // xsi:schemaLocation. Ex: + /** + * + * + **/ + String schemaLocation = new StringBuilder(defaultNamespace).append(' ').append(href).toString(); + XMLSchemaLoader.processExternalHints(schemaLocation, null, fLocationPairs, errorReporter); + } + rootElement = false; + } + super.startElement(element, attributes, augs); + } +} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelValidator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelValidator.java new file mode 100644 index 000000000..09f3b9e3e --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xerces/xmlmodel/XMLModelValidator.java @@ -0,0 +1,28 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.xerces.xmlmodel; + +import org.apache.xerces.xni.XMLLocator; +import org.apache.xerces.xni.parser.XMLComponent; +import org.apache.xerces.xni.parser.XMLDocumentFilter; + +/** + * XML model validator API. + * + */ +public interface XMLModelValidator extends XMLComponent, XMLDocumentFilter{ + + void setLocator(XMLLocator locator); + + void setHref(String href); + +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/participants/diagnostics/LSPErrorReporterForXSD.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/participants/diagnostics/LSPErrorReporterForXSD.java index 64063af2f..c76276386 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/participants/diagnostics/LSPErrorReporterForXSD.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/xsd/participants/diagnostics/LSPErrorReporterForXSD.java @@ -17,8 +17,8 @@ import org.apache.xerces.xni.XMLLocator; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSyntaxErrorCode; +import org.eclipse.lemminx.extensions.xerces.AbstractLSPErrorReporter; import org.eclipse.lemminx.extensions.xsd.participants.XSDErrorCode; -import org.eclipse.lemminx.services.extensions.xerces.AbstractLSPErrorReporter; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.Range; import org.xml.sax.ErrorHandler; diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLModelDiagnosticsTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLModelDiagnosticsTest.java new file mode 100644 index 000000000..5c9c6a781 --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/contentmodel/XMLModelDiagnosticsTest.java @@ -0,0 +1,69 @@ +/******************************************************************************* +* Copyright (c) 2020 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package org.eclipse.lemminx.extensions.contentmodel; + +import static org.eclipse.lemminx.XMLAssert.d; + +import org.eclipse.lemminx.XMLAssert; +import org.eclipse.lemminx.extensions.contentmodel.participants.DTDErrorCode; +import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSchemaErrorCode; +import org.eclipse.lsp4j.Diagnostic; +import org.junit.jupiter.api.Test; + +/** + * XML Validation test with xml-model processing instruction association. + * + */ +public class XMLModelDiagnosticsTest { + + @Test + public void xmlModelWithDTD() throws Exception { + String xml = " \r\n" + // + "\r\n" + // + "\r\n" + // + " \r\n" + // + ""; + testDiagnosticsFor(xml, d(3, 2, 5, DTDErrorCode.MSG_ELEMENT_NOT_DECLARED), + d(2, 1, 8, DTDErrorCode.MSG_CONTENT_INVALID)); + } + + @Test + public void xmlModelWithXSD() throws Exception { + String xml = "\r\n" + // + "\r\n" + // + "\r\n" + // + " \r\n" + // + " \r\n" + // + " \r\n" + // + " \r\n" + // + ""; + testDiagnosticsFor(xml, d(6, 3, 6, 11, XMLSchemaErrorCode.cvc_complex_type_2_4_f)); + } + + @Test + public void xmlModelWithXSDAndNamespace() throws Exception { + String xml = "\r\n" + // + "\r\n" + // + "\r\n" + // + " \r\n" + // + " XXXXXXXXXXXXX\r\n" + // <-- error + " \r\n" + // + ""; + Diagnostic d = d(4, 2, 4, 15, XMLSchemaErrorCode.cvc_complex_type_2_3, + "Element \'bean\' cannot contain text content.\nThe content type is defined as element-only.\n\nCode:"); + testDiagnosticsFor(xml, d); + } + + private static void testDiagnosticsFor(String xml, Diagnostic... expected) { + XMLAssert.testDiagnosticsFor(xml, "src/test/resources/catalogs/catalog.xml", expected); + } +} diff --git a/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-dtd.xml b/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-dtd.xml new file mode 100644 index 000000000..ca2d0998a --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-dtd.xml @@ -0,0 +1,5 @@ + + + Servlet 2.3 Web Application + + \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-xsd-with-ns.xml b/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-xsd-with-ns.xml new file mode 100644 index 000000000..fec53be69 --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-xsd-with-ns.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-xsd.xml b/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-xsd.xml new file mode 100644 index 000000000..188aa60f7 --- /dev/null +++ b/org.eclipse.lemminx/src/test/resources/xml-model/bound-to-xsd.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file