Skip to content

Commit

Permalink
Bind XML with no grammar constraints to XSD / DTD
Browse files Browse the repository at this point in the history
Fixes #151

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
azerr authored and angelozerr committed Jun 23, 2020
1 parent 68f9557 commit da67a76
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 31 deletions.
Expand Up @@ -60,42 +60,53 @@ public static CodeAction remove(String title, Range range, TextDocumentItem docu
* @param insertText
* @param document
* @param diagnostic
* @return
*
* @return the CodeAction to insert a new content at the end of the given range.
*/
public static CodeAction insert(String title, Position position, String insertText, TextDocumentItem document,
Diagnostic diagnostic) {
CodeAction insertContentAction = new CodeAction(title);
insertContentAction.setKind(CodeActionKind.QuickFix);
insertContentAction.setDiagnostics(Arrays.asList(diagnostic));
TextEdit edit = new TextEdit(new Range(position, position), insertText);
VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(
document.getUri(), document.getVersion());

TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier, Collections.singletonList(edit));
TextDocumentEdit textDocumentEdit = insertEdit(insertText, position, document);
WorkspaceEdit workspaceEdit = new WorkspaceEdit(Collections.singletonList(Either.forLeft(textDocumentEdit)));

insertContentAction.setEdit(workspaceEdit);
return insertContentAction;
}

/**
* Returns the text edit to insert a new content at the end of the given range.
*
* @param insertText text to insert.
* @param position the position.
* @param document the text document.
*
* @return the text edit to insert a new content at the end of the given range.
*/
public static TextDocumentEdit insertEdit(String insertText, Position position, TextDocumentItem document) {
TextEdit edit = new TextEdit(new Range(position, position), insertText);
VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(
document.getUri(), document.getVersion());
return new TextDocumentEdit(versionedTextDocumentIdentifier, Collections.singletonList(edit));
}

public static CodeAction replace(String title, Range range, String replaceText, TextDocumentItem document,
Diagnostic diagnostic) {
TextEdit replace = new TextEdit(range, replaceText);
return replace(title, Collections.singletonList(replace), document,
diagnostic);
return replace(title, Collections.singletonList(replace), document, diagnostic);
}

public static CodeAction replace(String title, List<TextEdit> replace, TextDocumentItem document,
Diagnostic diagnostic) {

CodeAction insertContentAction = new CodeAction(title);
insertContentAction.setKind(CodeActionKind.QuickFix);
insertContentAction.setDiagnostics(Arrays.asList(diagnostic));

VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(
document.getUri(), document.getVersion());
TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier,
replace);
TextDocumentEdit textDocumentEdit = new TextDocumentEdit(versionedTextDocumentIdentifier, replace);
WorkspaceEdit workspaceEdit = new WorkspaceEdit(Collections.singletonList(Either.forLeft(textDocumentEdit)));
insertContentAction.setEdit(workspaceEdit);
return insertContentAction;
Expand All @@ -106,10 +117,10 @@ public static CodeAction replaceAt(String title, String replaceText, TextDocumen
CodeAction insertContentAction = new CodeAction(title);
insertContentAction.setKind(CodeActionKind.QuickFix);
insertContentAction.setDiagnostics(Arrays.asList(diagnostic));

VersionedTextDocumentIdentifier versionedTextDocumentIdentifier = new VersionedTextDocumentIdentifier(
document.getUri(), document.getVersion());
ArrayList<TextEdit> edits = new ArrayList<TextEdit>();
List<TextEdit> edits = new ArrayList<TextEdit>();
for (Range range : ranges) {
TextEdit edit = new TextEdit(range, replaceText);
edits.add(edit);
Expand All @@ -124,21 +135,23 @@ public static CodeAction replaceAt(String title, String replaceText, TextDocumen
/**
* Makes a CodeAction to create a file and add content to the file.
*
* @param title The displayed name of the CodeAction
* @param docURI The file to create
* @param content The text to put into the newly created document.
* @param title The displayed name of the CodeAction
* @param docURI The file to create
* @param content The text to put into the newly created document.
* @param diagnostic The diagnostic that this CodeAction will fix
*/
public static CodeAction createFile(String title, String docURI, String content, Diagnostic diagnostic) {

List<Either<TextDocumentEdit, ResourceOperation>> actionsToTake = new ArrayList<>(2);


// 1. create an empty file
actionsToTake.add(Either.forRight(new CreateFile(docURI, new CreateFileOptions(false, true))));


// 2. update the created file with the given content
VersionedTextDocumentIdentifier identifier = new VersionedTextDocumentIdentifier(docURI, 0);
TextEdit te = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), content);
actionsToTake.add(Either.forLeft(new TextDocumentEdit(identifier, Collections.singletonList(te))));

WorkspaceEdit createAndAddContentEdit = new WorkspaceEdit(actionsToTake);

CodeAction codeAction = new CodeAction(title);
Expand Down
Expand Up @@ -61,7 +61,7 @@ private void registerCodeActionsIfNeeded(SharedSettings sharedSettings) {
if (!codeActionParticipants.isEmpty()) {
return;
}
XMLSyntaxErrorCode.registerCodeActionParticipants(codeActionParticipants);
XMLSyntaxErrorCode.registerCodeActionParticipants(codeActionParticipants, sharedSettings);
DTDErrorCode.registerCodeActionParticipants(codeActionParticipants, sharedSettings);
XMLSchemaErrorCode.registerCodeActionParticipants(codeActionParticipants, sharedSettings);
XSDErrorCode.registerCodeActionParticipants(codeActionParticipants);
Expand Down
Expand Up @@ -28,13 +28,16 @@
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.ElementUnterminatedCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.EqRequiredInAttributeCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.MarkupEntityMismatchCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.NoGrammarConstraintsCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.OpenQuoteExpectedCodeAction;
import org.eclipse.lemminx.extensions.contentmodel.participants.codeactions.RootElementTypeMustMatchDoctypedeclCodeAction;
import org.eclipse.lemminx.services.extensions.ICodeActionParticipant;
import org.eclipse.lemminx.services.extensions.diagnostics.IXMLErrorCode;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lemminx.utils.XMLPositionUtility;
import org.eclipse.lemminx.utils.XMLPositionUtility.EntityReferenceRange;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ResourceOperationKind;

/**
* XML error code.
Expand Down Expand Up @@ -79,7 +82,8 @@ public enum XMLSyntaxErrorCode implements IXMLErrorCode {
CustomETag, //
PrematureEOF, //
DoctypeNotAllowed, //
NoMorePseudoAttributes;
NoMorePseudoAttributes, //
NoGrammarConstraints;

private final String code;

Expand Down Expand Up @@ -269,13 +273,17 @@ public static Range toLSPRange(XMLLocator location, XMLSyntaxErrorCode code, Obj

}

public static void registerCodeActionParticipants(Map<String, ICodeActionParticipant> codeActions) {
public static void registerCodeActionParticipants(Map<String, ICodeActionParticipant> codeActions,
SharedSettings sharedSettings) {
codeActions.put(ElementUnterminated.getCode(), new ElementUnterminatedCodeAction());
codeActions.put(EqRequiredInAttribute.getCode(), new EqRequiredInAttributeCodeAction());
codeActions.put(OpenQuoteExpected.getCode(), new OpenQuoteExpectedCodeAction());
codeActions.put(MarkupEntityMismatch.getCode(), new MarkupEntityMismatchCodeAction());
codeActions.put(ETagRequired.getCode(), new ETagRequiredCodeAction());
codeActions.put(RootElementTypeMustMatchDoctypedecl.getCode(),
new RootElementTypeMustMatchDoctypedeclCodeAction());
if (sharedSettings.getWorkspaceSettings().isResourceOperationSupported(ResourceOperationKind.Create)) {
codeActions.put(NoGrammarConstraints.getCode(), new NoGrammarConstraintsCodeAction());
}
}
}
Expand Up @@ -26,7 +26,7 @@
import org.eclipse.lsp4j.Range;

/**
* Code Action which manages missing referenced grammar file (DTD, XSD°
* Code Action which manages missing referenced grammar file (DTD, XSD).
*/
public abstract class AbstractFixMissingGrammarCodeAction implements ICodeActionParticipant {

Expand All @@ -45,7 +45,7 @@ public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument documen
return;
}

// Generate XSD from the DOM doucment
// Generate XSD from the DOM document
FileContentGeneratorManager generator = componentProvider.getComponent(FileContentGeneratorManager.class);
String schemaTemplate = generator.generate(document, sharedSettings, getFileContentGeneratorSettings());

Expand Down
@@ -0,0 +1,167 @@
/*******************************************************************************
* Copyright (c) 2020 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.codeactions;

import java.io.File;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.lemminx.commons.BadLocationException;
import org.eclipse.lemminx.commons.CodeActionFactory;
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.extensions.generators.FileContentGeneratorManager;
import org.eclipse.lemminx.extensions.generators.xml2dtd.DTDGeneratorSettings;
import org.eclipse.lemminx.extensions.generators.xml2xsd.XMLSchemaGeneratorSettings;
import org.eclipse.lemminx.services.XMLCompletions;
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.XMLBuilder;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;

/**
* Code Action to bind a XML to a grammar (DTD, XSD) by generating the grammar.
*/
public class NoGrammarConstraintsCodeAction implements ICodeActionParticipant {

private static final Logger LOGGER = Logger.getLogger(XMLCompletions.class.getName());

@Override
public void doCodeAction(Diagnostic diagnostic, Range range, DOMDocument document, List<CodeAction> codeActions,
SharedSettings sharedSettings, IComponentProvider componentProvider) {
try {
DOMElement documentElement = document.getDocumentElement();
if (documentElement == null || StringUtils.isEmpty(documentElement.getTagName())) {
return;
}

FileContentGeneratorManager generator = componentProvider.getComponent(FileContentGeneratorManager.class);
String delimiter = document.lineDelimiter(0);
int beforeTagOffset = documentElement.getStartTagOpenOffset();
int afterTagOffset = beforeTagOffset + 1 + documentElement.getTagName().length();

// ---------- XSD

String schemaURI = getGrammarURI(document.getDocumentURI(), "xsd");
String schemaFileName = getFileName(schemaURI);
String schemaTemplate = generator.generate(document, sharedSettings, new XMLSchemaGeneratorSettings());

// xsi:noNamespaceSchemaLocation
// Create code action to create the XSD file with the generated XSD content
String insertText = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+ document.getTextDocument().lineDelimiter(0);
insertText += " xsi:noNamespaceSchemaLocation=\"" + schemaFileName + "\"";
CodeAction noNamespaceSchemaLocationAction = createGrammarFileAndBindIt(
"Bind to the generated XSD '" + schemaFileName + "' with xsi:noNamespaceSchemaLocation", schemaURI,
schemaTemplate, insertText, afterTagOffset, document, diagnostic);
codeActions.add(noNamespaceSchemaLocationAction);

// xml-model
XMLBuilder xsdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter);
xsdWithXmlModel.startPrologOrPI("xml-model");
xsdWithXmlModel.addSingleAttribute("href", schemaFileName, true);
xsdWithXmlModel.endPrologOrPI();
xsdWithXmlModel.linefeed();
CodeAction xsdWithXmlModelAction = createGrammarFileAndBindIt(
"Bind to the generated XSD '" + schemaFileName + "' with xml-model", schemaURI, schemaTemplate,
xsdWithXmlModel.toString(), beforeTagOffset, document, diagnostic);
codeActions.add(xsdWithXmlModelAction);

// ---------- DTD

String dtdURI = getGrammarURI(document.getDocumentURI(), "dtd");
String dtdFileName = getFileName(dtdURI);
String dtdTemplate = generator.generate(document, sharedSettings, new DTDGeneratorSettings());

// <!DOCTYPE ${1:root-element} SYSTEM \"${2:file}.dtd\">
XMLBuilder docType = new XMLBuilder(sharedSettings, null, delimiter);
docType.startDoctype();
docType.addParameter(documentElement.getLocalName());
docType.addContent(" SYSTEM \"");
docType.addContent(dtdFileName);
docType.addContent("\"");
docType.endDoctype();
docType.linefeed();
CodeAction docTypeAction = createGrammarFileAndBindIt(
"Bind to the generated DTD '" + dtdFileName + "' with DOCTYPE", dtdURI, dtdTemplate,
docType.toString(), beforeTagOffset, document, diagnostic);
codeActions.add(docTypeAction);

// xml-model
XMLBuilder dtdWithXmlModel = new XMLBuilder(sharedSettings, null, delimiter);
dtdWithXmlModel.startPrologOrPI("xml-model");
dtdWithXmlModel.addSingleAttribute("href", dtdFileName, true);
dtdWithXmlModel.endPrologOrPI();
dtdWithXmlModel.linefeed();
CodeAction dtdWithXmlModelAction = createGrammarFileAndBindIt(
"Bind to the generated DTD '" + dtdFileName + "' with xml-model", dtdURI, dtdTemplate,
dtdWithXmlModel.toString(), beforeTagOffset, document, diagnostic);
codeActions.add(dtdWithXmlModelAction);

} catch (BadLocationException e) {
LOGGER.log(Level.SEVERE, "In NoGrammarConstraintsCodeAction position error", e);
}
}

/**
* Returns the unique grammar URI file.
*
* @param documentURI the XML document URI.
* @param fileExtension the grammar file extension.
*
* @return the unique grammar URI file.
*/
private static String getGrammarURI(String documentURI, String fileExtension) {
int index = documentURI.lastIndexOf('.');
String grammarWithoutExtension = index != -1 ? documentURI.substring(0, index) : documentURI;
String grammarURI = grammarWithoutExtension + "." + fileExtension;
int i = 1;
try {
while (Files.exists(Paths.get(new URI(grammarURI)))) {
grammarURI = grammarWithoutExtension + (i++) + "." + fileExtension;
}
} catch (Exception e) {
// Do nothing
}
return grammarURI;
}

private static String getFileName(String schemaURI) {
return new File(schemaURI).getName();
}

private static CodeAction createGrammarFileAndBindIt(String title, String grammarURI, String grammarContent,
String insertText, int insertOffset, DOMDocument document, Diagnostic diagnostic)
throws BadLocationException {
Position position = document.positionAt(insertOffset);
TextDocumentEdit insertEdit = CodeActionFactory.insertEdit(insertText, position, document.getTextDocument());
return createGrammarFileAndBindIt(title, grammarURI, grammarContent, insertEdit, diagnostic);
}

private static CodeAction createGrammarFileAndBindIt(String title, String grammarURI, String grammarContent,
TextDocumentEdit boundEdit, Diagnostic diagnostic) {
CodeAction codeAction = CodeActionFactory.createFile(title, grammarURI, grammarContent, diagnostic);
codeAction.getEdit().getDocumentChanges().add(Either.forLeft(boundEdit));
return codeAction;
}
}
Expand Up @@ -26,6 +26,7 @@
import org.eclipse.lemminx.dom.DOMDocument;
import org.eclipse.lemminx.dom.DOMDocumentType;
import org.eclipse.lemminx.dom.DOMElement;
import org.eclipse.lemminx.extensions.contentmodel.participants.XMLSyntaxErrorCode;
import org.eclipse.lemminx.extensions.contentmodel.settings.ContentModelSettings;
import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationSettings;
import org.eclipse.lemminx.services.extensions.diagnostics.LSPContentHandler;
Expand Down Expand Up @@ -67,7 +68,7 @@ public static void doDiagnostics(DOMDocument document, XMLEntityResolver entityR
final LSPErrorReporterForXML reporter = new LSPErrorReporterForXML(document, diagnostics);

SAXParser parser = new LSPSAXParser(document, reporter, configuration, grammarPool);

// Add LSP content handler to stop XML parsing if monitor is canceled.
parser.setContentHandler(new LSPContentHandler(monitor));

Expand Down Expand Up @@ -162,7 +163,7 @@ private static void warnNoGrammar(DOMDocument document, List<Diagnostic> diagnos
range = new Range(new Position(0, 0), new Position(0, 0));
}
diagnostics.add(new Diagnostic(range, "No grammar constraints (DTD or XML Schema).", severity,
document.getDocumentURI(), "XML"));
document.getDocumentURI(), XMLSyntaxErrorCode.NoGrammarConstraints.name()));
}
}

Expand Down

0 comments on commit da67a76

Please sign in to comment.