Skip to content

Commit

Permalink
Bind XSD, DTD with CodeLens
Browse files Browse the repository at this point in the history
  • Loading branch information
angelozerr committed Jun 10, 2021
1 parent bdfda9b commit 32fbf69
Show file tree
Hide file tree
Showing 16 changed files with 559 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
}
return CompletableFutures.computeAsync(cancelChecker -> {
try {
return handler.executeCommand(params, cancelChecker);
return handler.executeCommand(params, xmlLanguageServer.getSharedSettings(), cancelChecker);
} catch (Exception e) {
if (e instanceof ResponseErrorException) {
throw (ResponseErrorException) e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import java.util.Objects;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.extensions.contentmodel.commands.AssociateGrammarCommand;
import org.eclipse.lemminx.extensions.contentmodel.commands.XMLValidationAllFilesCommand;
import org.eclipse.lemminx.extensions.contentmodel.commands.XMLValidationFileCommand;
import org.eclipse.lemminx.extensions.contentmodel.model.ContentModelManager;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCodeActionParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCodeLensParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelCompletionParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelDocumentLinkParticipant;
import org.eclipse.lemminx.extensions.contentmodel.participants.ContentModelHoverParticipant;
Expand All @@ -36,6 +38,7 @@
import org.eclipse.lemminx.services.extensions.ITypeDefinitionParticipant;
import org.eclipse.lemminx.services.extensions.IXMLExtension;
import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry;
import org.eclipse.lemminx.services.extensions.codelens.ICodeLensParticipant;
import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService;
import org.eclipse.lemminx.services.extensions.diagnostics.IDiagnosticsParticipant;
import org.eclipse.lemminx.services.extensions.save.ISaveContext;
Expand Down Expand Up @@ -68,6 +71,8 @@ public class ContentModelPlugin implements IXMLExtension {

private ContentModelSymbolsProviderParticipant symbolsProviderParticipant;

private final ICodeLensParticipant codeLensParticipant;

ContentModelManager contentModelManager;

private ContentModelSettings cmSettings;
Expand All @@ -80,6 +85,7 @@ public ContentModelPlugin() {
diagnosticsParticipant = new ContentModelDiagnosticsParticipant(this);
codeActionParticipant = new ContentModelCodeActionParticipant();
typeDefinitionParticipant = new ContentModelTypeDefinitionParticipant();
codeLensParticipant = new ContentModelCodeLensParticipant();
}

@Override
Expand Down Expand Up @@ -176,6 +182,7 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) {
registry.registerTypeDefinitionParticipant(typeDefinitionParticipant);
symbolsProviderParticipant = new ContentModelSymbolsProviderParticipant(contentModelManager);
registry.registerSymbolsProviderParticipant(symbolsProviderParticipant);
registry.registerCodeLensParticipant(codeLensParticipant);

// Register custom commands to re-validate XML files
IXMLCommandService commandService = registry.getCommandService();
Expand All @@ -186,6 +193,8 @@ public void start(InitializeParams params, XMLExtensionsRegistry registry) {
new XMLValidationFileCommand(contentModelManager, documentProvider, validationService));
commandService.registerCommand(XMLValidationAllFilesCommand.COMMAND_ID,
new XMLValidationAllFilesCommand(contentModelManager, documentProvider, validationService));
commandService.registerCommand(AssociateGrammarCommand.COMMAND_ID,
new AssociateGrammarCommand(documentProvider));
}
}

Expand All @@ -198,12 +207,14 @@ public void stop(XMLExtensionsRegistry registry) {
registry.unregisterDocumentLinkParticipant(documentLinkParticipant);
registry.unregisterTypeDefinitionParticipant(typeDefinitionParticipant);
registry.unregisterSymbolsProviderParticipant(symbolsProviderParticipant);
registry.unregisterCodeLensParticipant(codeLensParticipant);

// Un-register custom commands to re-validate XML files
IXMLCommandService commandService = registry.getCommandService();
if (commandService != null) {
commandService.unregisterCommand(XMLValidationFileCommand.COMMAND_ID);
commandService.unregisterCommand(XMLValidationAllFilesCommand.COMMAND_ID);
commandService.unregisterCommand(AssociateGrammarCommand.COMMAND_ID);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*******************************************************************************
* Copyright (c) 2021 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.commands;

import static org.eclipse.lemminx.extensions.xsd.utils.XSDUtils.TARGET_NAMESPACE_ATTR;

import java.net.URL;
import java.nio.file.Path;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.NoGrammarConstraintsCodeAction;
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.extensions.commands.AbstractDOMDocumentCommandHandler;
import org.eclipse.lemminx.services.extensions.commands.ArgumentsUtils;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lemminx.utils.FilesUtils;
import org.eclipse.lemminx.utils.StringUtils;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
* XML Command "xml.associate.grammar.insert" to associate a grammar to a given
* DOM document.
*
* The command parameters {@link ExecuteCommandParams} must be filled with 3
* parameters:
*
* <ul>
* <li>document URI (String) : the DOM document file URI to bind with a grammar.
* </li>
* <li>grammar URI (String) : the XSD, DTD file URI to bind with the DOM
* document.</li>
* <li>binding type (String) : which can takes values "xsd", "dd", "xml-model"
* to know which binding type must be inserted in the DOM document.</li>
* </ul>
*
* @author Angelo ZERR
*
*/
public class AssociateGrammarCommand extends AbstractDOMDocumentCommandHandler {

public static final String COMMAND_ID = "xml.associate.grammar.insert";

public AssociateGrammarCommand(IXMLDocumentProvider documentProvider) {
super(documentProvider);
}

public enum GrammarBindingType {

XSD("xsd"), //
DTD("dtd"), //
XML_MODEL("xml-model");

private String name;

private GrammarBindingType(String name) {
this.name = name != null ? name : name();
}

public String getName() {
return name;
}
}

@SuppressWarnings({ "serial" })
public class UnknownBindingTypeException extends Exception {

public UnknownBindingTypeException(String bindingType) {
super("Unknown binding type '" + bindingType + "'. Allowed values are " + //
Stream.of(GrammarBindingType.values()) //
.map(GrammarBindingType::getName) //
.collect(Collectors.joining(", ", "[", "]")));
}

}

@Override
protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, SharedSettings sharedSettings,
CancelChecker cancelChecker) throws Exception {
String documentURI = document.getDocumentURI();
String fullPathGrammarURI = ArgumentsUtils.getArgAt(params, 1, String.class);
String bindingType = ArgumentsUtils.getArgAt(params, 2, String.class);
String grammarURI = getRelativeURI(fullPathGrammarURI, documentURI);

if (GrammarBindingType.XSD.getName().equals(bindingType)) {
// Check if XSD to bind declares a target namespace
String targetNamespace = getTargetNamespace(fullPathGrammarURI);
if (StringUtils.isEmpty(targetNamespace)) {
// Insert inside <foo /> ->
// xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance"
// xsi:noNamespaceSchemaLocation=\"xsd/tag.xsd\"
return NoGrammarConstraintsCodeAction.createXSINoNamespaceSchemaLocationEdit(grammarURI, document);
}
// Insert inside <foo /> ->
// xmlns="team_namespace"
// xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
// xsi:schemaLocation="team_namespace xsd/team.xsd"
return NoGrammarConstraintsCodeAction.createXSISchemaLocationEdit(grammarURI, targetNamespace, document);
} else if (GrammarBindingType.DTD.getName().equals(bindingType)) {
// Insert before <foo /> -> <!DOCTYPE foo SYSTEM "dtd/tag.dtd">
return NoGrammarConstraintsCodeAction.createDocTypeEdit(grammarURI, document, sharedSettings);
} else if (GrammarBindingType.XML_MODEL.getName().equals(bindingType)) {
// Insert before <foo /> -> <?xml-model href=\"dtd/tag.dtd\"?>
return NoGrammarConstraintsCodeAction.createXmlModelEdit(grammarURI, document, sharedSettings);
}
throw new UnknownBindingTypeException(bindingType);
}

private static String getRelativeURI(String fullPathGrammarURI, String documentURI) {
try {
Path grammarPath = FilesUtils.getPath(fullPathGrammarURI);
Path documentPath = FilesUtils.getPath(documentURI);
Path relativePath = documentPath.getParent().relativize(grammarPath);
return relativePath.toString().replaceAll("[\\\\]", "/");
} catch (Exception e) {
return fullPathGrammarURI;
}
}

private static String getTargetNamespace(String xsdURI) {
TargetNamespaceHandler handler = new TargetNamespaceHandler();
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(new URL(xsdURI).openStream(), handler);
} catch (Exception e) {

}
return handler.getTargetNamespace();
}

/**
* SAX handler which extract the targetNamespace attribute from the xs:schema
* root tag element and null otherwise.
*
*/
private static class TargetNamespaceHandler extends DefaultHandler {

private String targetNamespace;

@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
super.startElement(uri, localName, qName, attributes);
this.targetNamespace = attributes.getValue(TARGET_NAMESPACE_ATTR);
throw new SAXException();
}

public String getTargetNamespace() {
return targetNamespace;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.IXMLValidationService;
import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService.IDelegateCommandHandler;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

Expand Down Expand Up @@ -52,7 +53,8 @@ public XMLValidationAllFilesCommand(ContentModelManager contentModelManager, IXM
}

@Override
public Object executeCommand(ExecuteCommandParams params, CancelChecker cancelChecker) throws Exception {
public Object executeCommand(ExecuteCommandParams params, SharedSettings sharedSettings,
CancelChecker cancelChecker) throws Exception {
// 1. clear the Xerces grammar pool
// (used by the Xerces validation) and the content model documents cache (used
// by the XML completion/hover based on the grammar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.IXMLValidationService;
import org.eclipse.lemminx.services.extensions.commands.AbstractDOMDocumentCommandHandler;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

/**
* XML Command to revalidate a give XML file which means:
* XML Command "xml.validation.current.file" to revalidate a give XML file which
* means:
*
* <ul>
* <li>remove the referenced grammar in the XML file from the Xerces grammar
Expand Down Expand Up @@ -48,8 +50,8 @@ public XMLValidationFileCommand(ContentModelManager contentModelManager, IXMLDoc
}

@Override
protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, CancelChecker cancelChecker)
throws Exception {
protected Object executeCommand(DOMDocument document, ExecuteCommandParams params, SharedSettings sharedSettings,
CancelChecker cancelChecker) throws Exception {
// 1. remove the referenced grammar in the XML file from the Xerces grammar pool
// (used by the Xerces validation) and the content model documents cache (used
// by the XML completion/hover based on the grammar)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*******************************************************************************
* Copyright (c) 2021 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.participants;

import java.util.Arrays;
import java.util.List;

import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.extensions.contentmodel.commands.AssociateGrammarCommand;
import org.eclipse.lemminx.extensions.contentmodel.commands.AssociateGrammarCommand.GrammarBindingType;
import org.eclipse.lemminx.services.extensions.codelens.ICodeLensParticipant;
import org.eclipse.lemminx.services.extensions.codelens.ICodeLensRequest;
import org.eclipse.lemminx.utils.XMLPositionUtility;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;

/**
* When XML file is not associated to a grammar (XSD, DTD), this class generates
* several CodeLenses on the root of the DOM Document:
*
* <ul>
* <li>[Bind with XSD] : click on this Codelens open a folder dialog to select
* the XSD to bind.</li>
* <li>[Bind with DTD] : click on this Codelens open a folder dialog to select
* the DTD to bind.</li>
* <li>[Bind with xml-model] : click on this Codelens open a folder dialog to
* select the XSD, DTD to bind.</li>
* </ul>
*
* <p>
* Once the LSP client select the DTD, XSD, it should call the
* {@link AssociateGrammarCommand} to generate the proper syntax for binding.
* </p>
*
*/
public class ContentModelCodeLensParticipant implements ICodeLensParticipant {

private static final String COMMAND_ID = "xml.associate.grammar.selectFile";

@Override
public void doCodeLens(ICodeLensRequest request, List<CodeLens> lenses, CancelChecker cancelChecker) {
DOMDocument document = request.getDocument();
DOMElement documentElement = document.getDocumentElement();
if (documentElement == null || document.hasGrammar()) {
return;
}
String documentURI = document.getDocumentURI();
Range range = XMLPositionUtility.selectRootStartTag(document);

lenses.add(createAssociateLens(documentURI, "Bind with XSD", GrammarBindingType.XSD.getName(), range));
lenses.add(createAssociateLens(documentURI, "Bind with DTD", GrammarBindingType.DTD.getName(), range));
lenses.add(
createAssociateLens(documentURI, "Bind with xml-model", GrammarBindingType.XML_MODEL.getName(), range));
}

private static CodeLens createAssociateLens(String documentURI, String title, String bindingType, Range range) {
Command command = new Command(title, COMMAND_ID, Arrays.asList(documentURI, bindingType));
return new CodeLens(range, command, null);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import org.apache.xerces.xni.XMLLocator;
import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.dom.DOMRange;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ElementDeclUnterminatedCodeAction;
Expand Down
Loading

0 comments on commit 32fbf69

Please sign in to comment.