/******************************************************************************* * 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( "Generate '" + schemaFileName + "' and bind 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( "Generate '" + schemaFileName + "' and bind 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( "Generate '" + dtdFileName + "' and bind 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( "Generate '" + dtdFileName + "' and bind 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. */ static String getGrammarURI(String documentURI, String fileExtension) { int index = documentURI.lastIndexOf('.'); if (index > 1 && documentURI.charAt(index - 1) == '/') { // case with file which starts with '.' (ex .project, .classpath). index = -1; } 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; } 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; } }