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 9, 2021
1 parent bdfda9b commit d3f5f4c
Show file tree
Hide file tree
Showing 16 changed files with 503 additions and 79 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,143 @@
/*******************************************************************************
* 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 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;

/**
* Associate a grammar to a given XML command.
*
* @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;
}
}

@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:xsi="http://www.w3.org/2001/XMLSchema-instance"
// xsi:noNamespaceSchemaLocation="xsd/tag.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);
}
return null;
}

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,6 +16,7 @@
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;

Expand Down Expand Up @@ -48,8 +49,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 d3f5f4c

Please sign in to comment.