Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ public XMLInputSource resolveEntity(XMLResourceIdentifier resourceIdentifier) th
}

private boolean hasDTDorXMLSchema(String uri) {
if (extensionsRegistry.getDocumentProvider() == null) {
return false;
}
DOMDocument document = extensionsRegistry.getDocumentProvider().getDocument(uri);
if (document == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.lemminx.dom.DOMAttr;
import org.eclipse.lemminx.dom.DOMElement;
Expand Down Expand Up @@ -49,7 +51,7 @@ public Hover onTag(IHoverRequest hoverRequest, CancelChecker cancelChecker) thro
try {
ContentModelManager contentModelManager = hoverRequest.getComponent(ContentModelManager.class);
DOMElement element = (DOMElement) hoverRequest.getNode();
Collection<CMDocument> cmDocuments = contentModelManager.findCMDocument(element);
Collection<CMDocument> cmDocuments = collectCMDocuments(element, contentModelManager);
if (cmDocuments.isEmpty()) {
// no bound grammar -> no documentation
return null;
Expand All @@ -75,7 +77,7 @@ public Hover onAttributeName(IHoverRequest hoverRequest, CancelChecker cancelChe
DOMElement element = attribute.getOwnerElement();
try {
ContentModelManager contentModelManager = hoverRequest.getComponent(ContentModelManager.class);
Collection<CMDocument> cmDocuments = contentModelManager.findCMDocument(element);
Collection<CMDocument> cmDocuments = collectCMDocuments(element, contentModelManager);
if (cmDocuments.isEmpty()) {
// no bound grammar -> no documentation
return null;
Expand Down Expand Up @@ -113,7 +115,7 @@ public Hover onAttributeValue(IHoverRequest hoverRequest, CancelChecker cancelCh
DOMElement element = attribute.getOwnerElement();
try {
ContentModelManager contentModelManager = hoverRequest.getComponent(ContentModelManager.class);
Collection<CMDocument> cmDocuments = contentModelManager.findCMDocument(element);
Collection<CMDocument> cmDocuments = collectCMDocuments(element, contentModelManager);
if (cmDocuments.isEmpty()) {
// no bound grammar -> no documentation
return null;
Expand Down Expand Up @@ -147,7 +149,7 @@ public Hover onText(IHoverRequest hoverRequest, CancelChecker cancelChecker) thr
}
try {
ContentModelManager contentModelManager = hoverRequest.getComponent(ContentModelManager.class);
Collection<CMDocument> cmDocuments = contentModelManager.findCMDocument(element);
Collection<CMDocument> cmDocuments = collectCMDocuments(element, contentModelManager);
if (cmDocuments.isEmpty()) {
// no bound grammar -> no documentation
return null;
Expand Down Expand Up @@ -185,4 +187,43 @@ private static void fillHoverContent(MarkupContent content, List<MarkupContent>
}
}

/**
* Collect the content model documents which may declare the given element. When
* the element is inside an ancestor with an xsi:type attribute, the documents
* bound to the ancestor's namespace are also collected so that derived types
* declared in other schemas can be resolved.
*
* @param element the hovered XML element.
* @param contentModelManager the content model manager.
* @return the content model documents to search.
*/
private static Collection<CMDocument> collectCMDocuments(DOMElement element,
ContentModelManager contentModelManager) {
Set<CMDocument> documents = new LinkedHashSet<>(contentModelManager.findCMDocument(element));
DOMElement current = element;
while (current != null) {
DOMElement parent = current.getParentNode() instanceof DOMElement ? (DOMElement) current.getParentNode()
: null;
if (parent != null && hasXSIType(parent)) {
documents.addAll(contentModelManager.findCMDocument(parent));
}
current = parent;
}
return documents;
}

private static boolean hasXSIType(DOMElement element) {
org.w3c.dom.NamedNodeMap attrs = element.getAttributes();
if (attrs == null) {
return false;
}
for (int i = 0; i < attrs.getLength(); i++) {
org.w3c.dom.Node attr = attrs.item(i);
if ("type".equals(attr.getLocalName()) && XSISchemaModel.XSI_WEBSITE.equals(attr.getNamespaceURI())) {
return true;
}
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelProvider;
import org.eclipse.lemminx.extensions.xsd.contentmodel.CMXSDContentModelProvider;
import org.eclipse.lemminx.extensions.xsd.participants.XSDCodeLensParticipant;
import org.eclipse.lemminx.extensions.xsd.participants.XSDCompletionParticipant;
Expand Down Expand Up @@ -87,8 +86,9 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) {
uiResolver = new XSDURIResolverExtension(registry.getDocumentProvider());
registry.getResolverExtensionManager().registerResolver(uiResolver);
// register XSD content model provider
ContentModelProvider modelProvider = new CMXSDContentModelProvider(registry.getResolverExtensionManager());
CMXSDContentModelProvider modelProvider = new CMXSDContentModelProvider(registry.getResolverExtensionManager());
contentModelManager = registry.getComponent(ContentModelManager.class);
modelProvider.setContentModelManager(contentModelManager);
contentModelManager.registerModelProvider(modelProvider);
// register completion, diagnostic participant
registry.registerCompletionParticipant(completionParticipant);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.eclipse.lemminx.dom.SchemaLocation;
import org.eclipse.lemminx.dom.SchemaLocationHint;
import org.eclipse.lemminx.extensions.contentmodel.model.CMDocument;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelProvider;
import org.eclipse.lemminx.extensions.xerces.AbstractLSPErrorReporter;
import org.eclipse.lemminx.extensions.xerces.LSPXMLEntityManager;
Expand All @@ -48,11 +49,22 @@ public class CMXSDContentModelProvider implements ContentModelProvider {

private static final String XSI_NO_NAMESPACE_SCHEMA_LOCATION_BINDING_KIND = "xsi:noNamespaceSchemaLocation";
private final URIResolverExtensionManager resolverExtensionManager;
private ContentModelManager contentModelManager;

public CMXSDContentModelProvider(URIResolverExtensionManager resolverExtensionManager) {
this.resolverExtensionManager = resolverExtensionManager;
}

/**
* Sets the content model manager used to resolve cross-schema type
* definitions (ex: xsi:type from a different namespace).
*
* @param contentModelManager the content model manager.
*/
public void setContentModelManager(ContentModelManager contentModelManager) {
this.contentModelManager = contentModelManager;
}

@Override
public boolean adaptFor(DOMDocument document, boolean internal) {
if (internal) {
Expand Down Expand Up @@ -110,7 +122,7 @@ public CMDocument createCMDocument(String key, boolean resolveExternalEntities)
XSModel model = loader.loadURI(key);
if (model != null) {
// XML Schema can be loaded
return new CMXSDDocument(model, loader);
return new CMXSDDocument(model, loader, contentModelManager);
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.extensions.contentmodel.model.CMDocument;
import org.eclipse.lemminx.extensions.contentmodel.model.CMElementDeclaration;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.model.FilesChangedTracker;
import org.eclipse.lemminx.extensions.xerces.ReflectionUtils;
import org.eclipse.lemminx.extensions.xsd.utils.XSDUtils;
Expand Down Expand Up @@ -89,10 +90,16 @@ public class CMXSDDocument implements CMDocument, XSElementDeclHelper {
private final FilesChangedTracker tracker;

private final XSLoaderImpl xsLoader;
private final ContentModelManager contentModelManager;

public CMXSDDocument(XSModel model, XSLoaderImpl xsLoaderImpl) {
this(model, xsLoaderImpl, null);
}

public CMXSDDocument(XSModel model, XSLoaderImpl xsLoaderImpl, ContentModelManager contentModelManager) {
this.model = model;
this.xsLoader = xsLoaderImpl;
this.contentModelManager = contentModelManager;
this.elementMappings = new HashMap<>();
this.refinedElementMappings = new HashMap<>();
this.tracker = createFilesChangedTracker(model);
Expand Down Expand Up @@ -174,18 +181,14 @@ XSObjectList getSubstitutionGroup(XSElementDeclaration elementDeclaration) {

@Override
public CMElementDeclaration findCMElement(DOMElement element, String namespace) {
List<DOMElement> paths = new ArrayList<>();
while (element != null && (namespace == null || namespace.equals(element.getNamespaceURI()))) {
paths.add(0, element);
element = element.getParentNode() instanceof DOMElement ? (DOMElement) element.getParentNode() : null;
}
List<DOMElement> paths = collectPath(element, namespace);
CMXSDElementDeclaration declaration = null;
for (int i = 0; i < paths.size(); i++) {
DOMElement elt = paths.get(i);
if (i == 0) {
declaration = (CMXSDElementDeclaration) findElementDeclaration(elt.getLocalName(), namespace);
declaration = (CMXSDElementDeclaration) findElementDeclaration(elt.getLocalName());
} else {
declaration = (CMXSDElementDeclaration) declaration.findCMElement(elt.getLocalName(), namespace);
declaration = (CMXSDElementDeclaration) declaration.findCMElement(elt.getLocalName(), elt.getNamespaceURI());
}
if (declaration == null) {
break;
Expand All @@ -208,32 +211,92 @@ public CMElementDeclaration findCMElement(DOMElement element, String namespace)
return declaration;
}

private XSTypeDefinition findXsiType(DOMElement element) {
private static List<DOMElement> collectPath(DOMElement element, String namespace) {
List<DOMElement> paths = new ArrayList<>();
DOMElement current = element;
while (current != null && (namespace == null || namespace.equals(current.getNamespaceURI()))) {
paths.add(0, current);
current = current.getParentNode() instanceof DOMElement ? (DOMElement) current.getParentNode() : null;
}
if (hasXSITypeAncestor(current)) {
while (current != null) {
paths.add(0, current);
current = current.getParentNode() instanceof DOMElement ? (DOMElement) current.getParentNode() : null;
}
}
return paths;
}

private static boolean hasXSITypeAncestor(DOMElement element) {
DOMElement current = element;
while (current != null) {
if (hasXSITypeValue(current)) {
return true;
}
current = current.getParentNode() instanceof DOMElement ? (DOMElement) current.getParentNode() : null;
}
return false;
}

private static boolean hasXSITypeValue(DOMElement element) {
return getXSITypeValue(element) != null;
}

private static String getXSITypeValue(DOMElement element) {
org.w3c.dom.NamedNodeMap attrs = element.getAttributes();
if (attrs == null) {
return null;
}
for (int i = 0; i < attrs.getLength(); i++) {
Node attr = attrs.item(i);
if (attr.getLocalName().equals("type") && XSISchemaModel.XSI_WEBSITE.equals(attr.getNamespaceURI())) {
String[] possiblyQualifiedType = attr.getNodeValue().split(":", 2);
javax.xml.namespace.QName qualifiedType;
if (possiblyQualifiedType.length == 1) {
qualifiedType = new javax.xml.namespace.QName(
null,
possiblyQualifiedType[0]);
} else {
qualifiedType = new javax.xml.namespace.QName(
element.getNamespaceURI(possiblyQualifiedType[0]),
possiblyQualifiedType[1]);
return attr.getNodeValue();
}
}
return null;
}

private XSTypeDefinition findXsiType(DOMElement element) {
String typeValue = getXSITypeValue(element);
if (typeValue == null) {
return null;
}
String[] possiblyQualifiedType = typeValue.split(":", 2);
javax.xml.namespace.QName qualifiedType;
if (possiblyQualifiedType.length == 1) {
qualifiedType = new javax.xml.namespace.QName(null, possiblyQualifiedType[0]);
} else {
qualifiedType = new javax.xml.namespace.QName(
element.getNamespaceURI(possiblyQualifiedType[0]),
possiblyQualifiedType[1]);
}
// Try to find the type in the current schema model.
XSTypeDefinition exactType = (XSTypeDefinition) model.getComponents(XSConstants.TYPE_DEFINITION)
.get(qualifiedType);
if (exactType != null) {
return exactType;
}
// Try to find the type in the models bound to the XML document for the type namespace.
if (contentModelManager != null && qualifiedType.getNamespaceURI() != null) {
DOMDocument xmlDocument = element.getOwnerDocument();
if (xmlDocument != null) {
Collection<CMDocument> documents = contentModelManager.findCMDocument(xmlDocument,
qualifiedType.getNamespaceURI());
for (CMDocument document : documents) {
if (document instanceof CMXSDDocument) {
exactType = (XSTypeDefinition) ((CMXSDDocument) document).model
.getComponents(XSConstants.TYPE_DEFINITION).get(qualifiedType);
if (exactType != null) {
return exactType;
}
}
}
return (XSTypeDefinition) model.getComponents(XSConstants.TYPE_DEFINITION).get(qualifiedType);
}
}
return null;
}

private CMElementDeclaration findElementDeclaration(String tag, String namespace) {
private CMElementDeclaration findElementDeclaration(String tag) {
for (CMElementDeclaration cmElement : getElements()) {
if (cmElement.getLocalName().equals(tag)) {
return cmElement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,12 +425,67 @@ public boolean isOpen() {
}));
}

/**
* See https://github.com/eclipse-lemminx/lemminx/issues/1787
*
* Hover documentation for child elements of xsi:type-derived complex types
* across namespaces.
*
* @throws BadLocationException
* @throws MalformedURIException
*/
@Test
public void testHoverXSITypeDerivedChildAcrossNamespaces() throws BadLocationException, MalformedURIException {
String aSchemaURI = getXMLSchemaFileURI("xsi-type-derived/a.xsd");
String bSchemaURI = getXMLSchemaFileURI("xsi-type-derived/b.xsd");

// Hover on <a:Title> should show documentation from a.xsd
String xmlA = "<?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n" + //
"\u003cm:Root xmlns:m=\"http://main\"\n" + //
" xmlns:a=\"http://a\"\n" + //
" xmlns:b=\"http://b\"\n" + //
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + //
" xsi:schemaLocation=\"http://main main.xsd http://a a.xsd http://b b.xsd\"\u003e\n" + //
" \u003cm:Data xsi:type=\"a:ADerived\"\u003e\n" + //
" \u003ca:Tit|le/\u003e\n" + //
" \u003c/m:Data\u003e\n" + //
"\u003c/m:Root>";
assertHover(xmlA, "src/test/resources/xsd/xsi-type-derived/test.xml",
"Documentation for Title from namespace a" + //
System.lineSeparator() + //
System.lineSeparator() + "Source: [a.xsd](" + aSchemaURI + ")",
r(7, 5, 7, 12));

// Hover on <b:Title> should show documentation from b.xsd
String xmlB = "<?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n" + //
"\u003cm:Root xmlns:m=\"http://main\"\n" + //
" xmlns:a=\"http://a\"\n" + //
" xmlns:b=\"http://b\"\n" + //
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + //
" xsi:schemaLocation=\"http://main main.xsd http://a a.xsd http://b b.xsd\"\u003e\n" + //
" \u003cm:Data xsi:type=\"b:BDerived\"\u003e\n" + //
" \u003cb:Tit|le/\u003e\n" + //
" \u003c/m:Data\u003e\n" + //
"\u003c/m:Root>";
assertHover(xmlB, "src/test/resources/xsd/xsi-type-derived/test.xml",
"Documentation for Title from namespace b" + //
System.lineSeparator() + //
System.lineSeparator() + "Source: [b.xsd](" + bSchemaURI + ")",
r(7, 5, 7, 12));
}

private static void assertHover(String value, String expectedHoverLabel, Range expectedHoverRange)
throws BadLocationException {
XMLAssert.assertHover(new XMLLanguageService(), value, "src/test/resources/catalogs/catalog.xml", null,
expectedHoverLabel, expectedHoverRange);
}

private static void assertHover(String value, String fileURI, String expectedHoverLabel, Range expectedHoverRange)
throws BadLocationException {
XMLAssert.assertHover(new XMLLanguageService(), value, null, fileURI,
expectedHoverLabel, expectedHoverRange);
}

private static String getXMLSchemaFileURI(String schemaURI) throws MalformedURIException {
return XMLEntityManager.expandSystemId("xsd/" + schemaURI, "src/test/resources/test.xml", true).replace("///",
"/");
Expand Down
Loading
Loading