Skip to content

Commit

Permalink
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 eclipse-lemminx#666

Signed-off-by: David Thompson <[email protected]>
  • Loading branch information
datho7561 committed Jun 9, 2020
1 parent ce6def2 commit e08f17a
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,57 @@
*/
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;

// The text to match is of the form:
// https://www.w3schools.com note.xsd https://www.w3schools.com note.xsd
private static final Pattern SCHEMA_LOCATION_PAIR_PATTERN = Pattern.compile("([^\\s]+)\\s+([^\\s]+)\\s*");

public SchemaLocation(String base, 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;
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, attr.getOwnerDocument()));
}
}

public String getLocationHint(String namespaceURI) {
return schemaLocationValuePairs.get(namespaceURI);
return schemaLocationValuePairs.get(namespaceURI).getHint();
}

public DOMAttr getAttr() {
return attr;
}

public Collection<SchemaLocationHint> getSchemaLocationHints() {
return schemaLocationValuePairs.values();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2018 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;

public class SchemaLocationHint implements DOMRange {

private final int start, end;

private final String hint;

private final DOMDocument document;

public SchemaLocationHint(int start, int end, String hint, DOMDocument document) {
this.start = start;
this.end = end;
this.hint = hint;
this.document = document;
}

/**
* Get the loation hint that was assigned to this
*/
public String getHint() {
return this.hint;
}

/**
* Get the start character of this SchemaLocationHint in the document.
*/
@Override
public int getStart() {
return this.start;
}

/**
* Get the end character of this SchemaLocationHint in the document.
*/
@Override
public int getEnd() {
return this.end;
}

@Override
public DOMDocument getOwnerDocument() {
return document;
}

}
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,6 +24,8 @@
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;
Expand All @@ -33,6 +36,7 @@
* <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,6 +92,22 @@ 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
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@

import org.apache.xerces.xni.XMLLocator;
import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.dom.DOMAttr;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.dom.NoNamespaceSchemaLocation;
import org.eclipse.lemminx.dom.SchemaLocation;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.TargetNamespace_1CodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.TargetNamespace_2CodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_attribute_3CodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_1CodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.cvc_complex_type_2_3CodeAction;
Expand Down Expand Up @@ -74,6 +77,7 @@ public enum XMLSchemaErrorCode implements IXMLErrorCode {
cvc_maxInclusive_valid("cvc-maxInclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-maxinclusive-valid
cvc_minExclusive_valid("cvc-minExclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-minexclusive-valid
cvc_minInclusive_valid("cvc-minInclusive-valid"), // https://wiki.xmldation.com/Support/validator/cvc-mininclusive-valid
TargetNamespace_1("TargetNamespace.1"), //
TargetNamespace_2("TargetNamespace.2"), SchemaLocation("SchemaLocation"), schema_reference_4("schema_reference.4"), //
src_element_3("src-element.3");

Expand Down Expand Up @@ -138,7 +142,6 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj
case cvc_elt_1_a:
case cvc_complex_type_4:
case src_element_3:
case TargetNamespace_2:
return XMLPositionUtility.selectStartTagName(offset, document);
case cvc_complex_type_3_2_2: {
String attrName = getString(arguments[1]);
Expand Down Expand Up @@ -245,6 +248,10 @@ public static Range toLSPRange(XMLLocator location, XMLSchemaErrorCode code, Obj
}
case cvc_type_3_1_2:
return XMLPositionUtility.selectStartTagName(offset, document);
case TargetNamespace_1:
return XMLPositionUtility.selectRootAttributeValue(DOMAttr.XMLNS_ATTR, document);
case TargetNamespace_2:
return XMLPositionUtility.selectRootStartTag(document);
default:
}
return null;
Expand All @@ -260,5 +267,7 @@ public static void registerCodeActionParticipants(Map<String, ICodeActionPartici
codeActions.put(cvc_complex_type_3_2_2.getCode(), new cvc_complex_type_3_2_2CodeAction());
codeActions.put(cvc_enumeration_valid.getCode(), new cvc_enumeration_validCodeAction());
codeActions.put(cvc_complex_type_2_1.getCode(), new cvc_complex_type_2_1CodeAction());
codeActions.put(TargetNamespace_1.getCode(), new TargetNamespace_1CodeAction());
codeActions.put(TargetNamespace_2.getCode(), new TargetNamespace_2CodeAction());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2020 Red Hat Inc. and others.
* 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.extensions.contentmodel.participants.codeactions;

import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.IComponentProvider;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lemminx.utils.StringUtils;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;

/**
* CodeAction to replace an incorrect namespace in an .xml document.
*
* Changes the value of the xmlns attribute of the root element of the .xml
* document to the declared namespace of the referenced .xsd document.
*/
public class TargetNamespace_1CodeAction implements ICodeActionParticipant {

private static final Pattern NAMESPACE_EXTRACTOR = Pattern.compile("'([^']+)'\\.");

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {

String namespace = extractNamespace(diagnostic.getMessage());
if (StringUtils.isEmpty(namespace)) {
return;
}
String quote = sharedSettings.getPreferences().getQuotationAsString();
// @formatter:off
CodeAction replaceNamespace = CodeActionFactory.replace(
"Replace with '" + namespace + "'",
diagnostic.getRange(),
quote + namespace + quote,
document.getTextDocument(),
diagnostic);
// @formatter:on
codeActions.add(replaceNamespace);
}

private static String extractNamespace(String diagnosticMessage) {
// The error message has this form:
// TargetNamespace.1: Expecting namespace 'NaN', but the target namespace of the
// schema document is 'http://two-letter-name'.
Matcher nsMatcher = NAMESPACE_EXTRACTOR.matcher(diagnosticMessage);
if (nsMatcher.find()) {
return nsMatcher.group(1);
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2020 Red Hat Inc. and others.
* 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.extensions.contentmodel.participants.codeactions;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMNode;
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.IComponentProvider;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lemminx.utils.StringUtils;
import org.eclipse.lemminx.utils.XMLPositionUtility;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;

/**
* Code action to add the missing xmlns declaration to the root element in an
* .xml document.
*
* Finds the namespace of the referenced .xsd. Adds the xmlns attribute to the
* root of .xml, and sets its value to the .xsd namespace.
*/
public class TargetNamespace_2CodeAction implements ICodeActionParticipant {

private static final Pattern NAMESPACE_EXTRACTOR = Pattern.compile("'([^']+)'\\.");

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {

String namespace = extractNamespace(diagnostic.getMessage());
if (StringUtils.isEmpty(namespace)) {
return;
}
DOMNode root = document.getDocumentElement();
if (root == null) {
return;
}
Position tagEnd = XMLPositionUtility.selectStartTagName(root).getEnd();
String quote = sharedSettings.getPreferences().getQuotationAsString();
// @formatter:off
CodeAction addNamespaceDecl = CodeActionFactory.insert(
"Declare '" + namespace + "' as the namespace",
tagEnd,
" xmlns=" + quote + namespace + quote,
document.getTextDocument(),
diagnostic);
// @formatter:on
codeActions.add(addNamespaceDecl);
}

private static String extractNamespace(String diagnosticMessage) {
// The error message has this form:
// TargetNamespace.2: Expecting no namespace, but the schema document has a
// target namespace of 'http://docbook.org/ns/docbook'.
Matcher nsMatcher = NAMESPACE_EXTRACTOR.matcher(diagnosticMessage);
if (nsMatcher.find()) {
return nsMatcher.group(1);
}
return null;
}

}
Loading

0 comments on commit e08f17a

Please sign in to comment.