blob: 0d7349acd5675e22c35077e73dfe9cf7e067d544 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008-2011 Sonatype, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Sonatype, Inc. - initial API and implementation
* Andrew Eisenberg - Work on Bug 350414
*******************************************************************************/
package org.eclipse.m2e.core.ui.internal.editing;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.undo.IStructuredTextUndoManager;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.format.FormatProcessorXML;
/**
* this class contains tools for editing the pom files using dom tree operations.
*
* @author mkleint
*/
@SuppressWarnings("restriction")
public class PomEdits {
public static final String NAMESPACE = "http://maven.apache.org/POM/4.0.0"; //$NON-NLS-1$
public static final String NAMESPACE_LOCATION = "http://maven.apache.org/xsd/maven-4.0.0.xsd"; //$NON-NLS-1$
public static final String PROJECT = "project"; //$NON-NLS-1$
public static final String MODEL_VERSION = "modelVersion"; //$NON-NLS-1$
public static final String MODEL_VERSION_VALUE = "4.0.0"; //$NON-NLS-1$
public static final String DEPENDENCIES = "dependencies"; //$NON-NLS-1$
public static final String GROUP_ID = "groupId";//$NON-NLS-1$
public static final String ARTIFACT_ID = "artifactId"; //$NON-NLS-1$
public static final String DEPENDENCY = "dependency"; //$NON-NLS-1$
public static final String DEPENDENCY_MANAGEMENT = "dependencyManagement"; //$NON-NLS-1$
public static final String EXCLUSIONS = "exclusions"; //$NON-NLS-1$
public static final String EXCLUSION = "exclusion"; //$NON-NLS-1$
public static final String VERSION = "version"; //$NON-NLS-1$
public static final String PLUGIN = "plugin"; //$NON-NLS-1$
public static final String CONFIGURATION = "configuration";//$NON-NLS-1$
public static final String PLUGINS = "plugins";//$NON-NLS-1$
public static final String PLUGIN_MANAGEMENT = "pluginManagement";//$NON-NLS-1$
public static final String BUILD = "build";//$NON-NLS-1$
public static final String PARENT = "parent";//$NON-NLS-1$
public static final String RELATIVE_PATH = "relativePath";//$NON-NLS-1$
public static final String TYPE = "type";//$NON-NLS-1$
public static final String CLASSIFIER = "classifier";//$NON-NLS-1$
public static final String OPTIONAL = "optional";//$NON-NLS-1$
public static final String SCOPE = "scope";//$NON-NLS-1$
public static final String MODULES = "modules";//$NON-NLS-1$
public static final String MODULE = "module";//$NON-NLS-1$
public static final String PROFILE = "profile";//$NON-NLS-1$
public static final String ID = "id";//$NON-NLS-1$
public static final String NAME = "name"; //$NON-NLS-1$
public static final String URL = "url";//$NON-NLS-1$
public static final String DESCRIPTION = "description";//$NON-NLS-1$
public static final String INCEPTION_YEAR = "inceptionYear";//$NON-NLS-1$
public static final String ORGANIZATION = "organization"; //$NON-NLS-1$
public static final String SCM = "scm"; //$NON-NLS-1$
public static final String CONNECTION = "connection";//$NON-NLS-1$
public static final String DEV_CONNECTION = "developerConnection";//$NON-NLS-1$
public static final String TAG = "tag";//$NON-NLS-1$
public static final String ISSUE_MANAGEMENT = "issueManagement"; //$NON-NLS-1$
public static final String SYSTEM = "system"; //$NON-NLS-1$
public static final String SYSTEM_PATH = "systemPath"; //$NON-NLS-1$
public static final String CI_MANAGEMENT = "ciManagement"; //$NON-NLS-1$
public static final String PACKAGING = "packaging"; //$NON-NLS-1$
public static final String PROPERTIES = "properties"; //$NON-NLS-1$
public static final String EXTENSION = "extension"; //$NON-NLS-1$
public static final String EXTENSIONS = "extensions"; //$NON-NLS-1$
public static final String PROFILES = "profiles";//$NON-NLS-1$
public static final String EXECUTIONS = "executions"; //$NON-NLS-1$
public static final String EXECUTION = "execution";//$NON-NLS-1$
public static final String GOAL = "goal";//$NON-NLS-1$
public static final String GOALS = "goals";//$NON-NLS-1$
public static Element findChild(Element parent, String name) {
if(parent == null) {
return null;
}
NodeList rootList = parent.getChildNodes();
for(int i = 0; i < rootList.getLength(); i++ ) {
Node nd = rootList.item(i);
if(nd instanceof Element) {
Element el = (Element) nd;
if(name.equals(el.getNodeName())) {
return el;
}
}
}
return null;
}
public static List<Element> findChilds(Element parent, String name) {
List<Element> toRet = new ArrayList<Element>();
if(parent != null) {
NodeList rootList = parent.getChildNodes();
for(int i = 0; i < rootList.getLength(); i++ ) {
Node nd = rootList.item(i);
if(nd instanceof Element) {
Element el = (Element) nd;
if(name.equals(el.getNodeName())) {
toRet.add(el);
}
}
}
}
return toRet;
}
public static String getTextValue(Node element) {
if(element == null)
return null;
StringBuffer buff = new StringBuffer();
NodeList list = element.getChildNodes();
for(int i = 0; i < list.getLength(); i++ ) {
Node child = list.item(i);
if(child instanceof Text) {
Text text = (Text) child;
buff.append(text.getData().trim()); //352416 the value is trimmed because of the multiline values
//that get trimmed by maven itself as well, any comparison to resolved model needs to do the trimming
// or risks false negative results.
}
}
return buff.toString();
}
/**
* finds exactly one (first) occurence of child element with the given name (eg. dependency) that fulfills conditions
* expressed by the Matchers (eg. groupId/artifactId match)
*
* @param parent
* @param name
* @param matchers
* @return
*/
public static Element findChild(Element parent, String name, Matcher... matchers) {
OUTTER: for(Element el : findChilds(parent, name)) {
for(Matcher match : matchers) {
if(!match.matches(el)) {
continue OUTTER;
}
}
return el;
}
return null;
}
/**
* helper method, creates a subelement with text embedded. does not format the result. primarily to be used in cases
* like <code>&lt;goals&gt;&lt;goal&gt;xxx&lt;/goal&gt;&lt;/goals&gt;</code>
*
* @param parent
* @param name
* @param value
* @return
*/
public static Element createElementWithText(Element parent, String name, String value) {
Document doc = parent.getOwnerDocument();
Element newElement = doc.createElement(name);
parent.appendChild(newElement);
newElement.appendChild(doc.createTextNode(value));
return newElement;
}
/**
* helper method, creates a subelement, does not format result.
*
* @param parent the parent element
* @param name the name of the new element
* @return the created element
*/
public static Element createElement(Element parent, String name) {
Document doc = parent.getOwnerDocument();
Element newElement = doc.createElement(name);
parent.appendChild(newElement);
return newElement;
}
/**
* sets text value to the given element. any existing text children are removed and replaced by this new one.
*
* @param element
* @param value
*/
public static void setText(Element element, String value) {
NodeList list = element.getChildNodes();
List<Node> toRemove = new ArrayList<Node>();
for(int i = 0; i < list.getLength(); i++ ) {
Node child = list.item(i);
if(child instanceof Text) {
toRemove.add(child);
}
}
for(Node rm : toRemove) {
element.removeChild(rm);
}
Document doc = element.getOwnerDocument();
element.appendChild(doc.createTextNode(value));
}
/**
* unlike the findChild() equivalent, this one creates the element if not present and returns it. Therefore it shall
* only be invoked within the PomEdits.Operation
*
* @param parent
* @param names chain of element names to find/create
* @return
*/
public static Element getChild(Element parent, String... names) {
Element toFormat = null;
Element toRet = null;
if(names.length == 0) {
throw new IllegalArgumentException("At least one child name has to be specified");
}
for(String name : names) {
toRet = findChild(parent, name);
if(toRet == null) {
toRet = parent.getOwnerDocument().createElement(name);
parent.appendChild(toRet);
if(toFormat == null) {
toFormat = toRet;
}
}
parent = toRet;
}
if(toFormat != null) {
format(toFormat);
}
return toRet;
}
/**
* proper remove of a child element
*/
public static void removeChild(Element parent, Element child) {
if(child != null) {
Node prev = child.getPreviousSibling();
if(prev instanceof Text) {
Text txt = (Text) prev;
int lastnewline = getLastEolIndex(txt.getData());
if(lastnewline >= 0) {
txt.setData(txt.getData().substring(0, lastnewline));
}
}
parent.removeChild(child);
}
}
private static int getLastEolIndex(String s) {
if(s == null || s.length() == 0) {
return -1;
}
for(int i = s.length() - 1; i >= 0; i-- ) {
char c = s.charAt(i);
if(c == '\r') {
return i;
}
if(c == '\n') {
if(i > 0 && s.charAt(i - 1) == '\r') {
return i - 1;
}
return i;
}
}
return -1;
}
/**
* remove the current element if it doesn't contain any sublements, useful for lists etc, works recursively removing
* all parents up that don't have any children elements.
*
* @param el
*/
public static void removeIfNoChildElement(Element el) {
NodeList nl = el.getChildNodes();
boolean hasChilds = false;
for(int i = 0; i < nl.getLength(); i++ ) {
Node child = nl.item(i);
if(child instanceof Element) {
hasChilds = true;
}
}
if(!hasChilds) {
Node parent = el.getParentNode();
if(parent != null && parent instanceof Element) {
removeChild((Element) parent, el);
removeIfNoChildElement((Element) parent);
}
}
}
public static Element insertAt(Element newElement, int offset) {
Document doc = newElement.getOwnerDocument();
if(doc instanceof IDOMDocument) {
IDOMDocument domDoc = (IDOMDocument) doc;
IndexedRegion ir = domDoc.getModel().getIndexedRegion(offset);
Node parent = ((Node) ir).getParentNode();
if(ir instanceof Text) {
Text txt = (Text) ir;
String data = txt.getData();
int dataSplitIndex = offset - ir.getStartOffset();
String beforeText = data.substring(0, dataSplitIndex);
String afterText = data.substring(dataSplitIndex);
Text after = doc.createTextNode(afterText);
Text before = doc.createTextNode(beforeText);
parent.replaceChild(after, txt);
parent.insertBefore(newElement, after);
parent.insertBefore(before, newElement);
} else if(ir instanceof Element) {
if(ir.getStartOffset() == offset) {
// caret is before the tag, not within its bounds
parent.insertBefore(newElement, (Element) ir);
} else {
((Element) ir).appendChild(newElement);
}
} else {
throw new IllegalArgumentException();
}
} else {
throw new IllegalArgumentException();
}
return newElement;
}
/**
* finds the element at offset, if other type of node at offset, will return it's parent element (if any)
*
* @param doc
* @param offset
* @return
*/
public static Element elementAtOffset(Document doc, int offset) {
if(doc instanceof IDOMDocument) {
IDOMDocument domDoc = (IDOMDocument) doc;
IndexedRegion ir = domDoc.getModel().getIndexedRegion(offset);
if(ir instanceof Element) {
Element elem = (Element) ir;
if(ir.getStartOffset() == offset) {
// caret is before the tag, not within its bounds
elem = (Element) elem.getParentNode();
}
return elem;
}
Node parent = ((Node) ir).getParentNode();
if(parent instanceof Element) {
return (Element) parent;
}
}
return null;
}
/**
* formats the node (and content). please make sure to only format the node you have created..
*
* @param newNode
*/
public static void format(Node newNode) {
Node parentNode = newNode.getParentNode();
if(parentNode != null && newNode.equals(parentNode.getLastChild())) {
//add a new line to get the newly generated content correctly formatted.
Document ownerDocument;
if(parentNode instanceof Document) {
ownerDocument = (Document) parentNode;
} else {
ownerDocument = parentNode.getOwnerDocument();
}
parentNode.appendChild(ownerDocument.createTextNode("\n")); //$NON-NLS-1$
}
FormatProcessorXML formatProcessor = new FormatProcessorXML();
//ignore any line width settings, causes wrong formatting of <foo>bar</foo>
formatProcessor.getFormatPreferences().setLineWidth(2000);
formatProcessor.formatNode(newNode);
}
/**
* performs an modifying operation on top the
*
* @param file
* @param operation
* @throws IOException
* @throws CoreException
*/
public static void performOnDOMDocument(PomEdits.OperationTuple... fileOperations) throws IOException, CoreException {
for(OperationTuple tuple : fileOperations) {
IDOMModel domModel = null;
//TODO we might want to attempt iterating opened editors and somehow initialize those
// that were not yet initialized. Then we could avoid saving a file that is actually opened, but was never used so far (after restart)
try {
DocumentRewriteSession session = null;
IStructuredTextUndoManager undo = null;
if(tuple.isReadOnly()) {
domModel = (IDOMModel) StructuredModelManager.getModelManager().getExistingModelForRead(tuple.getDocument());
if(domModel == null) {
domModel = (IDOMModel) StructuredModelManager.getModelManager().getModelForRead(
(IStructuredDocument) tuple.getDocument());
}
} else {
domModel = tuple.getModel() != null ? tuple.getModel()
: (tuple.getFile() != null ? (IDOMModel) StructuredModelManager.getModelManager().getModelForEdit(
tuple.getFile()) : (IDOMModel) StructuredModelManager.getModelManager().getExistingModelForEdit(
tuple.getDocument())); //existing shall be ok here..
//let the model know we make changes
domModel.aboutToChangeModel();
undo = domModel.getStructuredDocument().getUndoManager();
//let the document know we make changes
if(domModel.getStructuredDocument() instanceof IDocumentExtension4) {
IDocumentExtension4 ext4 = (IDocumentExtension4) domModel.getStructuredDocument();
session = ext4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL);
}
undo.beginRecording(domModel);
// fill with minimal pom content
Document doc = domModel.getDocument();
if(doc.getDocumentElement() == null) {
Node first = doc.getFirstChild();
if(first == null || !(first instanceof ProcessingInstruction)) {
doc.insertBefore(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""), first); //$NON-NLS-1$ //$NON-NLS-2$
doc.insertBefore(doc.createTextNode("\n"), first); //$NON-NLS-1$
}
Element project = doc.createElement(PROJECT);
project.setAttribute("xmlns", NAMESPACE); //$NON-NLS-1$
project.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); //$NON-NLS-1$ //$NON-NLS-2$
project.setAttribute("xsi:schemaLocation", NAMESPACE + " " + NAMESPACE_LOCATION); //$NON-NLS-1$ //$NON-NLS-2$
doc.appendChild(project);
Element modelVersion = doc.createElement(MODEL_VERSION);
modelVersion.appendChild(doc.createTextNode(MODEL_VERSION_VALUE)); //$NON-NLS-1$
project.appendChild(modelVersion);
format(project);
}
}
try {
tuple.getOperation().process(domModel.getDocument());
} finally {
if(!tuple.isReadOnly()) {
undo.endRecording(domModel);
if(session != null && domModel.getStructuredDocument() instanceof IDocumentExtension4) {
IDocumentExtension4 ext4 = (IDocumentExtension4) domModel.getStructuredDocument();
ext4.stopRewriteSession(session);
}
domModel.changedModel();
}
}
} finally {
if(domModel != null) {
if(tuple.isReadOnly()) {
domModel.releaseFromRead();
} else if(domModel.getId() != null) { // id will be null for files outside of workspace
//for ducuments saving shall generally only happen when the model is not held elsewhere (eg. in opened view)
//for files, save always
if(tuple.isForceSave() || domModel.getReferenceCountForEdit() == 1) {
domModel.save();
}
domModel.releaseFromEdit();
}
}
}
}
}
public static final class OperationTuple {
private final PomEdits.Operation operation;
private final IFile file;
private final IDocument document;
private final IDOMModel model;
private boolean readOnly = false;
private boolean forceSave = false;
/**
* operation on top of IFile is always saved
*
* @param file
* @param operation
*/
public OperationTuple(IFile file, PomEdits.Operation operation) {
assert file != null;
assert operation != null;
this.file = file;
this.operation = operation;
document = null;
model = null;
forceSave = true;
}
/**
* operation on top of IDocument is only saved when noone else is editing the document.
*
* @param document
* @param operation
*/
public OperationTuple(IDocument document, PomEdits.Operation operation) {
this(document, operation, false);
}
/**
* operation on top of IDocument is only saved when noone else is editing the document.
*
* @param document
* @param operation
* @param readonly operation that doesn't modify the content. Will only get the read, not edit model, up to the user
* of the code to ensure no edits happen
*/
public OperationTuple(IDocument document, PomEdits.Operation operation, boolean readOnly) {
assert operation != null;
this.document = document;
this.operation = operation;
file = null;
model = null;
this.readOnly = readOnly;
}
/**
* only use for unmanaged models
*
* @param model
* @param operation
*/
public OperationTuple(IDOMModel model, PomEdits.Operation operation) {
assert model != null;
this.operation = operation;
this.model = model;
document = null;
file = null;
}
/**
* force saving the document after performing the operation
*/
public void setForceSave() {
forceSave = true;
}
public boolean isForceSave() {
return forceSave;
}
/**
* @return Returns the readOnly.
*/
public boolean isReadOnly() {
return readOnly;
}
public IFile getFile() {
return file;
}
public PomEdits.Operation getOperation() {
return operation;
}
public IDocument getDocument() {
return document;
}
public IDOMModel getModel() {
return model;
}
}
/**
* operation to perform on top of the DOM document. see performOnDOMDocument()
*
* @author mkleint
*/
public static interface Operation {
void process(Document document);
}
/**
* an Operation instance that aggregates multiple operations and performs then in given order.
*
* @author mkleint
*/
public static final class CompoundOperation implements Operation {
private final Operation[] operations;
public CompoundOperation(Operation... operations) {
this.operations = operations;
}
public void process(Document document) {
for(Operation oper : operations) {
oper.process(document);
}
}
}
/**
* an interface for identifying child elements that fulfill conditions expressed by the matcher.
*
* @author mkleint
*/
public static interface Matcher {
/**
* returns true if the given element matches the condition.
*
* @param child
* @return
*/
boolean matches(Element element);
}
public static Matcher childEquals(final String elementName, final String matchingValue) {
return new Matcher() {
public boolean matches(Element child) {
String toMatch = PomEdits.getTextValue(PomEdits.findChild(child, elementName));
return toMatch != null && toMatch.trim().equals(matchingValue);
}
};
}
public static Matcher textEquals(final String matchingValue) {
return new Matcher() {
public boolean matches(Element child) {
String toMatch = PomEdits.getTextValue(child);
return toMatch != null && toMatch.trim().equals(matchingValue);
}
};
}
public static Matcher childMissingOrEqual(final String elementName, final String matchingValue) {
return new Matcher() {
public boolean matches(Element child) {
Element match = PomEdits.findChild(child, elementName);
if(match == null) {
return true;
}
String toMatch = PomEdits.getTextValue(match);
return toMatch != null && toMatch.trim().equals(matchingValue);
}
};
}
/**
* keeps internal state, needs to be recreated for each query, when used in conjunction with out matchers shall
* probably be placed last.
*
* @param elementName
* @param index
* @return
*/
public static Matcher childAt(final int index) {
return new Matcher() {
int count = 0;
public boolean matches(Element child) {
if(count == index) {
return true;
}
count++ ;
return false;
}
};
}
}