Skip to content

Commit

Permalink
Added document links for xsi:schemaLocation
Browse files Browse the repository at this point in the history
Every second token for the value of xsi:schemaLocation is turned into a
document link to the referenced schema file.

Closes #666

Signed-off-by: David Thompson <[email protected]>
  • Loading branch information
datho7561 committed Jun 11, 2020
1 parent 3b5242e commit 7c51a08
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ private static SchemaLocation createSchemaLocation(DOMNode root, String schemaIn
if (attr == null) {
return null;
}
return new SchemaLocation(root.getOwnerDocument().getDocumentURI(), attr);
return new SchemaLocation(attr);
}

private static NoNamespaceSchemaLocation createNoNamespaceSchemaLocation(DOMNode root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,73 @@
*/
package org.eclipse.lemminx.dom;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
*
*
* The declared "xsi:schemaLocation"
*/
public class SchemaLocation {

private final Map<String, String> schemaLocationValuePairs;
private final Map<String, SchemaLocationHint> schemaLocationValuePairs;

private final DOMAttr attr;

public SchemaLocation(String base, DOMAttr attr) {
// The text to match is of the form:
// http://example.org/schema/root root.xsd http://example.org/schema/bison bison.xsd http://example.org/schema/potato potato.xsd
private static final Pattern SCHEMA_LOCATION_PAIR_PATTERN = Pattern.compile("\\s*([^\\s]+)\\s+([^\\s]+)\\s*");

public SchemaLocation(DOMAttr attr) {
this.attr = attr;
this.schemaLocationValuePairs = new HashMap<>();
String value = attr.getValue();
StringTokenizer st = new StringTokenizer(value);
do {
String namespaceURI = st.hasMoreTokens() ? st.nextToken() : null;
String locationHint = st.hasMoreTokens() ? st.nextToken() : null;
if (namespaceURI == null || locationHint == null)
Matcher locPairMatcher = SCHEMA_LOCATION_PAIR_PATTERN.matcher(value);
while (locPairMatcher.find()) {
String namespaceURI = locPairMatcher.group(1);
String locationHint = locPairMatcher.group(2);
if (namespaceURI == null || locationHint == null) {
break;
schemaLocationValuePairs.put(namespaceURI, locationHint);
} while (true);
}
DOMNode valNode = attr.getNodeAttrValue();
// matcher matches 1 char ahead, but valNode's start is at beginning of "
int start = valNode.getStart() + locPairMatcher.start(2);
// matcher matches end correctly, but range needs to be one past end
// to highlight the end, and one to make up for "
int end = valNode.getStart() + locPairMatcher.end(2) + 2;
schemaLocationValuePairs.put(namespaceURI,
new SchemaLocationHint(start, end, locationHint, this));
}
}

/**
* Get the location hint, as a string, that is associated with the given namespace URI.
*
* If the given namespace URI was not referred to in this xsi:schemaLocation, then null is returned.
*
* @param namespaceURI The namespace URI to find the location hint for
* @return The associated location hint, as a string, or null
* if the namespace was not referred to in xsi:schemaLocation
*/
public String getLocationHint(String namespaceURI) {
return schemaLocationValuePairs.get(namespaceURI);
SchemaLocationHint locHint = schemaLocationValuePairs.get(namespaceURI);
return locHint == null ? null : locHint.getHint();
}

public DOMAttr getAttr() {
return attr;
}

/**
* Returns all the location hints given in this xsi:schemaLocation attribute
*
* @return A Collection of all the location hints as <code>SchemaLocationHint</code>
*/
public Collection<SchemaLocationHint> getSchemaLocationHints() {
return schemaLocationValuePairs.values();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2020 Red Hat Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* 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;

/**
* Represents one of the location hints provided in an xsi:schemaLocation
* attribute.
*/
public class SchemaLocationHint implements DOMRange {

private final int start, end;

private final String hint;

private final SchemaLocation parent;

/**
* Create a new SchemaLocationHint object that represents one of the location
* hints given in the parent <code>SchemaLocation</code>.
*
* @param start The offset from the beginning of the document where this
* location hint starts
* @param end The offset from the beginning of the document where this
* location hint ends
* @param hint The hint to the location of a schema (A URI that points to a
* schema)
* @param parent The <coed>SchemaLocation</code> in which this hint was given
*/
public SchemaLocationHint(int start, int end, String hint, SchemaLocation parent) {
this.start = start;
this.end = end;
this.hint = hint;
this.parent = parent;
}

/**
* Returns the location hint that this SchemaLocationHint represents
*
* @return The location hint, a URI to a schema, as a String
*/
public String getHint() {
return this.hint;
}

/**
* Returns the offset from the beginning of the document where this location
* hint starts
*
* @return The offset from the beginning of the document where this location
* hint starts
*/
@Override
public int getStart() {
return this.start;
}

/**
* Returns the offset from the beginning of the document where this location
* hint ends
*
* @return The offset from the beginning of the document where this location
* hint ends
*/
@Override
public int getEnd() {
return this.end;
}

@Override
public DOMDocument getOwnerDocument() {
return parent.getAttr().getOwnerDocument();
}

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

import static org.eclipse.lemminx.utils.XMLPositionUtility.createDocumentLink;

import java.util.Collection;
import java.util.List;

import org.apache.xerces.impl.XMLEntityManager;
Expand All @@ -23,18 +24,21 @@
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DOMRange;
import org.eclipse.lemminx.dom.NoNamespaceSchemaLocation;
import org.eclipse.lemminx.dom.SchemaLocation;
import org.eclipse.lemminx.dom.SchemaLocationHint;
import org.eclipse.lemminx.dom.XMLModel;
import org.eclipse.lemminx.services.extensions.IDocumentLinkParticipant;
import org.eclipse.lsp4j.DocumentLink;

/**
* Document link for :
*
*
* <ul>
* <li>XML Schema xsi:noNamespaceSchemaLocation</li>
* <li>DTD SYSTEM (ex : <!DOCTYPE root-element SYSTEM "./extended.dtd" )</li>
* <li>XML Schema xsi:schemaLocation</li>
* </ul>
*
*
* @author Angelo ZERR
*
*/
Expand Down Expand Up @@ -88,11 +92,27 @@ public void findDocumentLinks(DOMDocument document, List<DocumentLink> links) {
}
}
}
// Doc link for xsi:schemaLocation
SchemaLocation schemaLocation = document.getSchemaLocation();
if (schemaLocation != null) {
try {
Collection<SchemaLocationHint> schemaLocationHints = schemaLocation.getSchemaLocationHints();
String location;
for (SchemaLocationHint schemaLocationHint : schemaLocationHints) {
location = getResolvedLocation(document.getDocumentURI(), schemaLocationHint.getHint());
if (location != null) {
links.add(createDocumentLink(schemaLocationHint, location));
}
}
} catch (BadLocationException e) {
// Do nothing
}
}
}

/**
* Returns the expanded system location
*
*
* @return the expanded system location
*/
private static String getResolvedLocation(String documentURI, String location) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,83 @@ public void noNamespaceSchemaLocation() throws BadLocationException {
XMLAssert.testDocumentLinkFor(xml, "src/test/resources/Format.xml",
dl(r(1, 100, 1, 114), "src/test/resources/xsd/Format.xsd"));
}

@Test
public void singleSchemaLocation() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" + //
"<Configuration xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://example.org/schema/format xsd/Format.xsd\">\r\n"
+ //
" <ViewDefinitions>\r\n" + //
" <View>";
XMLAssert.testDocumentLinkFor(xml, "src/test/resources/Format.xml",
dl(r(1, 122, 1, 136), "src/test/resources/xsd/Format.xsd"));
}

@Test
public void manySchemaLocation() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + //
"<beans xmlns=\"http://www.springframework.org/schema/beans\"\r\n" + //
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n" + //
" xmlns:camel=\"http://camel.apache.org/schema/spring\"\r\n" + //
" xmlns:cxf=\"http://camel.apache.org/schema/cxf\"\r\n" + //
" xsi:schemaLocation=\"http://www.example.org/schema/beans xsd/spring-beans.xsd http://www.example.org/schema/spring xsd/camel-spring.xsd http://www.example.org/schema/salad xsd/salad.xsd\">\r\n";
XMLAssert.testDocumentLinkFor(xml, "src/test/resources/beans-and-salad.xml",
dl(r(5, 63, 5, 83), "src/test/resources/xsd/spring-beans.xsd"),
dl(r(5, 178, 5, 191), "src/test/resources/xsd/salad.xsd"),
dl(r(5, 121, 5, 141), "src/test/resources/xsd/camel-spring.xsd")
);
}

@Test
public void lineBreakManySchemaLocation() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + //
"<beans xmlns=\"http://www.springframework.org/schema/beans\"\r\n" + //
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n" + //
" xmlns:camel=\"http://camel.apache.org/schema/spring\"\r\n" + //
" xmlns:cxf=\"http://camel.apache.org/schema/cxf\"\r\n" + //
" xsi:schemaLocation=\"\r\n" + //
" http://www.example.org/schema/beans xsd/spring-beans.xsd\r\n" + //
" http://www.example.org/schema/spring xsd/camel-spring.xsd\r\n" + //
" http://www.example.org/schema/salad xsd/salad.xsd\">\r\n";
XMLAssert.testDocumentLinkFor(xml, "src/test/resources/beans-and-salad.xml",
dl(r(6, 45, 6, 65), "src/test/resources/xsd/spring-beans.xsd"),
dl(r(8, 45, 8, 58), "src/test/resources/xsd/salad.xsd"),
dl(r(7, 46, 7, 66), "src/test/resources/xsd/camel-spring.xsd"));
}

@Test
public void blankSchemaLocation() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + //
"<beans xmlns=\"http://www.springframework.org/schema/beans\"\r\n" + //
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n" + //
" xmlns:camel=\"http://camel.apache.org/schema/spring\"\r\n" + //
" xmlns:cxf=\"http://camel.apache.org/schema/cxf\"\r\n" + //
" xsi:schemaLocation=\"\" />";
XMLAssert.testDocumentLinkFor(xml, "src/test/resources/xsd/bean.xml");

}

@Test
public void incompleteSchemaLocation() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" + //
"<Configuration xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://example.org/schema/format\">\r\n"
+ //
" <ViewDefinitions>\r\n" + //
" <View>";
XMLAssert.testDocumentLinkFor(xml, "src/test/resources/Format.xml");
}

@Test
public void secondTokenIsIncompleteSchemaLocation() throws BadLocationException {
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + //
"<beans xmlns=\"http://www.springframework.org/schema/beans\"\r\n" + //
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n" + //
" xmlns:camel=\"http://camel.apache.org/schema/spring\"\r\n" + //
" xmlns:cxf=\"http://camel.apache.org/schema/cxf\"\r\n" + //
" xsi:schemaLocation=\"\r\n" + //
" http://www.example.org/schema/beans xsd/spring-beans.xsd\r\n" + //
" http://www.example.org/schema/spring\" />\r\n";
XMLAssert.testDocumentLinkFor(xml, "src/test/resources/beans-and-salad.xml",
dl(r(6, 45, 6, 65), "src/test/resources/xsd/spring-beans.xsd"));
}
}

0 comments on commit 7c51a08

Please sign in to comment.