Skip to content

Commit

Permalink
[xml-model] XML Completion based on DTD/XML Schema by using xml-model
Browse files Browse the repository at this point in the history
Fixes #698

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed May 21, 2020
1 parent 4fe9cfb commit 5e1dfe8
Show file tree
Hide file tree
Showing 31 changed files with 881 additions and 336 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,17 @@
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;

/**
* XML document.
*
*/
public class DOMDocument extends DOMNode implements Document {

private static final String XML_MODEL_PI = "xml-model";

private SchemaLocation schemaLocation;
private NoNamespaceSchemaLocation noNamespaceSchemaLocation;
private List<XMLModel> xmlModels;

private boolean referencedExternalGrammarInitialized;
private boolean referencedSchemaInitialized;
private final URIResolverExtensionManager resolverExtensionManager;
Expand Down Expand Up @@ -255,13 +254,13 @@ private void initializeReferencedSchemaIfNeeded() {

/**
* Initialize namespaces and schema location declaration .
*
* @return
*/
private synchronized void initializeReferencedSchema() {
if (referencedSchemaInitialized) {
return;
}
// xml-model
xmlModels = XMLModel.createXMLModels(this);
// Get root element
DOMElement documentElement = getDocumentElement();
if (documentElement == null) {
Expand Down Expand Up @@ -308,15 +307,16 @@ public boolean hasProlog() {
return (children != null && !children.isEmpty() && children.get(0).isProlog());
}

private SchemaLocation createSchemaLocation(DOMNode root, String schemaInstancePrefix) {
private static SchemaLocation createSchemaLocation(DOMNode root, String schemaInstancePrefix) {
DOMAttr attr = root.getAttributeNode(getPrefixedName(schemaInstancePrefix, "schemaLocation"));
if (attr == null) {
return null;
}
return new SchemaLocation(root.getOwnerDocument().getDocumentURI(), attr);
}

private NoNamespaceSchemaLocation createNoNamespaceSchemaLocation(DOMNode root, String schemaInstancePrefix) {
private static NoNamespaceSchemaLocation createNoNamespaceSchemaLocation(DOMNode root,
String schemaInstancePrefix) {
DOMAttr attr = root.getAttributeNode(getPrefixedName(schemaInstancePrefix, "noNamespaceSchemaLocation"));
if (attr == null || attr.getValue() == null) {
return null;
Expand Down Expand Up @@ -344,18 +344,20 @@ public boolean hasDTD() {
* @return true if XML document has a xml-model processing declaration and false
* otherwise.
*/
private boolean hasXMLModel() {
List<DOMNode> children = getChildren();
if (children != null && !children.isEmpty()) {
return children.stream().anyMatch(child -> {
return isXMLModel(child);
});
}
return false;
public boolean hasXMLModel() {
return !getXMLModels().isEmpty();
}

private static boolean isXMLModel(DOMNode node) {
return node.isProcessingInstruction() && XML_MODEL_PI.equals(((ProcessingInstruction) node).getTarget());
/**
* Returns the list of xml-model processing instruction declared in the
* document.
*
* @return the list of xml-model processing instruction declared in the
* document.
*/
public List<XMLModel> getXMLModels() {
initializeReferencedSchemaIfNeeded();
return xmlModels;
}

// -------------------------- External Grammar (XML file associations, catalog)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*******************************************************************************
* 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.dom;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import org.eclipse.lemminx.extensions.xerces.xmlmodel.XMLModelDeclaration;
import org.w3c.dom.ProcessingInstruction;

/**
* XML model processing instruction.
*
* <pre>
* &lt;?xml-model href="http://www.docbook.org/xml/5.0/xsd/docbook.xsd"?&gt;
* </pre>
*
*
* @see https://www.w3.org/TR/xml-model/
*/
public class XMLModel {

private static final String XML_MODEL_PI = "xml-model";

private final DOMProcessingInstruction processingInstruction;
private XMLModelDeclaration declaration;

public XMLModel(DOMProcessingInstruction processingInstruction) {
this.processingInstruction = processingInstruction;
}

/**
* Returns the location of the referenced schema
*
* @return the location of the referenced schema
*/
public String getHref() {
String data = processingInstruction.getData();
if (data == null) {
return null;
}
if (declaration == null) {
declaration = XMLModelDeclaration.parse(data.toCharArray(), 0, data.length());
}
return declaration.getHref();
}

/**
* Returns the declared xml-model list.
*
* @param document the DOM document.
*
* @return the declared xml-model list.
*/
static List<XMLModel> createXMLModels(DOMDocument document) {
List<DOMNode> children = document.getChildren();
if (children != null && !children.isEmpty()) {
return children.stream().filter(XMLModel::isXMLModel)
.map(node -> new XMLModel((DOMProcessingInstruction) node)).collect(Collectors.toList());
}
return Collections.emptyList();
}

/**
* Returns true if the given node is a xml-model processing instruction and
* false otherwise.
*
* @param node the DOM node.
* @return true if the given node is a xml-model processing instruction and
* false otherwise.
*/
public static boolean isXMLModel(DOMNode node) {
return node.isProcessingInstruction() && XML_MODEL_PI.equals(((ProcessingInstruction) node).getTarget());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,25 @@ public interface CMDocument {
* @return the declared element which matches the given XML element and null
* otherwise.
*/
CMElementDeclaration findCMElement(DOMElement element, String namespace);
default CMElementDeclaration findCMElement(DOMElement element) {
return findCMElement(element, element.getNamespaceURI());
}

/**
* Returns the root URI of the model document.
* Returns the declared element which matches the given XML element and null
* otherwise.
*
* @return the root URI of the model document.
* @param element the XML element
* @param namespace the given namespace
* @return the declared element which matches the given XML element and null
* otherwise.
*/
String getURI();
CMElementDeclaration findCMElement(DOMElement element, String namespace);

default CMAttributeDeclaration findCMAttribute(DOMElement element, String attributeName) {
CMElementDeclaration elementDeclaration = findCMElement(element);
return elementDeclaration != null ? elementDeclaration.findCMAttribute(attributeName) : null;
}

/**
* Returns the location of the type definition of the given node.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -63,31 +64,78 @@ public ContentModelManager(URIResolverExtensionManager resolverManager) {
setUseCache(true);
}

public CMElementDeclaration findCMElement(DOMElement element) throws Exception {
return findCMElement(element, element.getNamespaceURI());
/**
* Returns the owner document of the declared element which matches the given
* XML element and null otherwise.
*
* @param element the XML element
*
* @return the owner document of the declared element which matches the given
* XML element and null otherwise.
*/
public Collection<CMDocument> findCMDocument(DOMElement element) {
return findCMDocument(element.getOwnerDocument(), element.getNamespaceURI());
}

/**
* Returns the declared element which matches the given XML element and null
* otherwise.
* Returns the owner document of the declared element which matches the given
* XML element and null otherwise.
*
* @param element the XML element
* @return the declared element which matches the given XML element and null
* otherwise.
*
* @return the owner document of the declared element which matches the given
* XML element and null otherwise.
*/
public CMElementDeclaration findCMElement(DOMElement element, String namespaceURI) throws Exception {
CMDocument cmDocument = findCMDocument(element, namespaceURI);
return cmDocument != null ? cmDocument.findCMElement(element, namespaceURI) : null;
public Collection<CMDocument> findCMDocument(DOMElement element, String namespaceURI) {
return findCMDocument(element.getOwnerDocument(), namespaceURI);
}

public CMDocument findCMDocument(DOMElement element, String namespaceURI) {
return findCMDocument(element.getOwnerDocument(), namespaceURI);
public Collection<CMDocument> findCMDocument(DOMDocument xmlDocument, String namespaceURI) {
return findCMDocument(xmlDocument, namespaceURI, true);
}

public CMDocument findCMDocument(DOMDocument xmlDocument, String namespaceURI) {
ContentModelProvider modelProvider = getModelProviderByStandardAssociation(xmlDocument, false);
String systemId = modelProvider != null ? modelProvider.getSystemId(xmlDocument, namespaceURI) : null;
return findCMDocument(xmlDocument.getDocumentURI(), namespaceURI, systemId, modelProvider);
/**
* Returns the declared documents which match the given DOM document and null
* otherwise.
*
* @param xmlDocument the DOM document.
* @param namespaceURI the namespace URI
* @return the declared documents which match the given DOM document and null
* otherwise.
*/
public Collection<CMDocument> findCMDocument(DOMDocument xmlDocument, String namespaceURI, boolean withInternal) {
Collection<CMDocument> documents = new ArrayList<>();
for (ContentModelProvider modelProvider : modelProviders) {
// internal grammar
if (withInternal) {
CMDocument internalCMDocument = modelProvider.createInternalCMDocument(xmlDocument);
if (internalCMDocument != null) {
documents.add(internalCMDocument);
}
}
// external grammar
if (modelProvider.adaptFor(xmlDocument, false)) {
// The content model provider can collect the system ids
// ex for <?xml-model , the model provider which takes care of xml-model returns
// the href of xml-model.
Collection<String> systemIds = modelProvider.getSystemIds(xmlDocument, namespaceURI);
for (String systemId : systemIds) {
// get the content model document from the current system id
CMDocument cmDocument = findCMDocument(xmlDocument.getDocumentURI(), namespaceURI, systemId,
modelProvider);
if (cmDocument != null) {
documents.add(cmDocument);
}
}
}
}
if (documents.isEmpty()) {
CMDocument cmDocument = findCMDocument(xmlDocument.getDocumentURI(), namespaceURI, null, null);
if (cmDocument != null) {
documents.add(cmDocument);
}
}
return documents;
}

/**
Expand All @@ -103,10 +151,18 @@ public boolean dependsOnGrammar(DOMDocument document, String grammarURI) {
if (StringUtils.isEmpty(grammarURI)) {
return false;
}
ContentModelProvider modelProvider = getModelProviderByStandardAssociation(document, false);
String systemId = modelProvider != null ? modelProvider.getSystemId(document, document.getNamespaceURI())
: null;
String key = resolverManager.resolve(document.getDocumentURI(), null, systemId);
for (ContentModelProvider modelProvider : modelProviders) {
if (modelProvider.adaptFor(document, false)) {
Collection<String> systemIds = modelProvider.getSystemIds(document, document.getNamespaceURI());
for (String systemId : systemIds) {
String key = resolverManager.resolve(document.getDocumentURI(), null, systemId);
if (grammarURI.equals(key)) {
return true;
}
}
}
}
String key = resolverManager.resolve(document.getDocumentURI(), null, null);
return grammarURI.equals(key);
}

Expand Down Expand Up @@ -187,55 +243,14 @@ private void cache(String key, CMDocument cmDocument) {
}
}

public CMElementDeclaration findInternalCMElement(DOMElement element) throws Exception {
return findInternalCMElement(element, element.getNamespaceURI());
}

/**
* Returns the declared element which matches the given XML element and null
* otherwise.
* Returns the model provider by the given uri and null otherwise.
*
* @param element the XML element
* @return the declared element which matches the given XML element and null
* otherwise.
*/
public CMElementDeclaration findInternalCMElement(DOMElement element, String namespaceURI) throws Exception {
CMDocument cmDocument = findInternalCMDocument(element, namespaceURI);
return cmDocument != null ? cmDocument.findCMElement(element, namespaceURI) : null;
}

public CMDocument findInternalCMDocument(DOMElement element, String namespaceURI) {
return findInternalCMDocument(element.getOwnerDocument(), namespaceURI);
}

public CMDocument findInternalCMDocument(DOMDocument xmlDocument, String namespaceURI) {
ContentModelProvider modelProvider = getModelProviderByStandardAssociation(xmlDocument, true);
if (modelProvider != null) {
return modelProvider.createInternalCMDocument(xmlDocument);
}
return null;
}

/**
* Returns the content model provider by using standard association
* (xsi:schemaLocation, xsi:noNamespaceSchemaLocation, doctype) an dnull
* otherwise.
* @param uri the grammar URI
*
* @param xmlDocument
* @return the content model provider by using standard association
* (xsi:schemaLocation, xsi:noNamespaceSchemaLocation, doctype) an dnull
* otherwise.
* @return the model provider by the given uri and null otherwise.
*/
private ContentModelProvider getModelProviderByStandardAssociation(DOMDocument xmlDocument, boolean internal) {
for (ContentModelProvider modelProvider : modelProviders) {
if (modelProvider.adaptFor(xmlDocument, internal)) {
return modelProvider;
}
}
return null;
}

private ContentModelProvider getModelProviderByURI(String uri) {
public ContentModelProvider getModelProviderByURI(String uri) {
for (ContentModelProvider modelProvider : modelProviders) {
if (modelProvider.adaptFor(uri)) {
return modelProvider;
Expand Down
Loading

0 comments on commit 5e1dfe8

Please sign in to comment.