Skip to content

Commit

Permalink
#43: implement xml merger (#363)
Browse files Browse the repository at this point in the history
  • Loading branch information
salimbouch committed Jun 20, 2024
1 parent fa37227 commit d195546
Show file tree
Hide file tree
Showing 39 changed files with 1,027 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import com.devonfw.tools.ide.merge.xmlmerger.XmlMerger;
import com.devonfw.tools.ide.util.FilenameUtil;
import org.jline.utils.Log;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package com.devonfw.tools.ide.merge;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
package com.devonfw.tools.ide.merge.xmlmerger;

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import com.devonfw.tools.ide.merge.FileMerger;
import com.devonfw.tools.ide.merge.xmlmerger.matcher.ElementMatcher;
import com.devonfw.tools.ide.merge.xmlmerger.model.MergeElement;
import com.devonfw.tools.ide.merge.xmlmerger.strategy.OverrideStrategy;
import com.devonfw.tools.ide.merge.xmlmerger.strategy.Strategy;
import com.devonfw.tools.ide.merge.xmlmerger.strategy.StrategyFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
Expand All @@ -19,18 +16,25 @@
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

/**
* Implementation of {@link FileMerger} for XML files.
*/
public class XmlMerger extends FileMerger {

private static final DocumentBuilder DOCUMENT_BUILDER;

private static final TransformerFactory TRANSFORMER_FACTORY;

public static final String MERGE_NS_URI = "https://github.com/devonfw/IDEasy/merge";

static {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
Expand All @@ -42,11 +46,6 @@ public class XmlMerger extends FileMerger {
}
}

/**
* The constructor.
*
* @param context the {@link #context}.
*/
public XmlMerger(IdeContext context) {

super(context);
Expand All @@ -70,55 +69,18 @@ public void merge(Path setup, Path update, EnvironmentVariables resolver, Path w
document = load(update);
} else {
Document updateDocument = load(update);
merge(updateDocument, document, true, true);
merge(updateDocument, document);
}
}
resolve(document, resolver, false, workspace.getFileName());
save(document, workspace);
}

private void merge(Document sourceDocument, Document targetDocument, boolean override, boolean add) {

assert (override || add);
merge(sourceDocument.getDocumentElement(), targetDocument.getDocumentElement(), override, add);
}

private void merge(Element sourceElement, Element targetElement, boolean override, boolean add) {

merge(sourceElement.getAttributes(), targetElement, override, add);
NodeList sourceChildNodes = sourceElement.getChildNodes();
int length = sourceChildNodes.getLength();
for (int i = 0; i < length; i++) {
Node child = sourceChildNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
public void merge(Document sourceDocument, Document targetDocument) {

} else if (child.getNodeType() == Node.TEXT_NODE) {

} else if (child.getNodeType() == Node.CDATA_SECTION_NODE) {

}
}
}

private void merge(NamedNodeMap sourceAttributes, Element targetElement, boolean override, boolean add) {

int length = sourceAttributes.getLength();
for (int i = 0; i < length; i++) {
Attr sourceAttribute = (Attr) sourceAttributes.item(i);
String namespaceURI = sourceAttribute.getNamespaceURI();
// String localName = sourceAttribute.getLocalName();
String name = sourceAttribute.getName();
Attr targetAttribute = targetElement.getAttributeNodeNS(namespaceURI, name);
if (targetAttribute == null) {
if (add) {
// ridiculous but JDK does not provide namespace support by default...
targetElement.setAttributeNS(namespaceURI, name, sourceAttribute.getValue());
// targetElement.setAttribute(name, sourceAttribute.getValue());
}
} else if (override) {
targetAttribute.setValue(sourceAttribute.getValue());
}
}
MergeElement updateRootElement = new MergeElement(sourceDocument.getDocumentElement());
Strategy strategy = StrategyFactory.createStrategy(updateRootElement.getMergingStrategy(), new ElementMatcher());
strategy.merge(updateRootElement, targetDocument);
}

@Override
Expand All @@ -129,17 +91,15 @@ public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean
}
Document updateDocument = load(update);
Document workspaceDocument = load(workspace);
merge(workspaceDocument, updateDocument, true, addNewProperties);
Strategy strategy = new OverrideStrategy(null);
MergeElement rootElement = new MergeElement(workspaceDocument.getDocumentElement());
strategy.merge(rootElement, updateDocument);
resolve(updateDocument, variables, true, workspace.getFileName());
save(updateDocument, update);
this.context.debug("Saved changes in {} to {}", workspace.getFileName(), update);
}

/**
* @param file the {@link Path} to load.
* @return the loaded XML {@link Document}.
*/
public static Document load(Path file) {
public Document load(Path file) {

try (InputStream in = Files.newInputStream(file)) {
return DOCUMENT_BUILDER.parse(in);
Expand All @@ -148,22 +108,43 @@ public static Document load(Path file) {
}
}

/**
* @param document the XML {@link Document} to save.
* @param file the {@link Path} to save to.
*/
public static void save(Document document, Path file) {
public void save(Document document, Path file) {

ensureParentDirectoryExists(file);
try {
Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.STANDALONE, "no");

// Remove whitespace from the target document before saving, because if target XML Document is already formatted
// then indent 2 keeps adding empty lines for nothing, and if we don't use indentation then appending/ overriding
// isn't properly formatted.
removeWhitespace(document.getDocumentElement());

DOMSource source = new DOMSource(document);
StreamResult result = new StreamResult(file.toFile());
transformer.transform(source, result);
} catch (Exception e) {
throw new IllegalStateException("Failed to save XML to file: " + file, e);
}
}

private void removeWhitespace(Node node) {

NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
if (child.getTextContent().trim().isEmpty()) {
node.removeChild(child);
i--;
}
} else {
removeWhitespace(child);
}
}
}

private void resolve(Document document, EnvironmentVariables resolver, boolean inverse, Object src) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.devonfw.tools.ide.merge.xmlmerger.matcher;

import com.devonfw.tools.ide.merge.xmlmerger.model.MergeElement;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import java.util.HashMap;
import java.util.Map;

/**
* The ElementMatcher class is responsible for matching XML elements in a target document based on the provided update elements.
*/
public class ElementMatcher {

private final Map<QName, IdComputer> qNameIdMap;

public ElementMatcher() {

qNameIdMap = new HashMap<>();
}

/**
* Updates the ID strategy for a given QName (qualified name) of an XML element.
*
* @param qname the QName of the XML element
* @param id the ID value to be used for matching the element
*/
public void updateId(QName qname, String id) {

qNameIdMap.put(qname, new IdComputer(id));
}

/**
* Matches an update element in the target document.
*
* @param updateElement the update element to be matched
* @param targetDocument the target document in which to match the element
* @return the matched MergeElement if found, or {@code null} if not found
*/
public MergeElement matchElement(MergeElement updateElement, Document targetDocument) {

if (updateElement.isRootElement()) {
Element sourceRoot = updateElement.getElement();
Element targetRoot = targetDocument.getDocumentElement();
if (sourceRoot.getNamespaceURI() != null || targetRoot.getNamespaceURI() != null) {
if (!sourceRoot.getNamespaceURI().equals(targetRoot.getNamespaceURI())) {
throw new IllegalStateException("URI of elements don't match. Found " + sourceRoot.getNamespaceURI() + "and " + targetRoot.getNamespaceURI());
}
}
return new MergeElement(targetRoot);
}

String id = updateElement.getId();
if (id.isEmpty()) {
IdComputer idComputer = qNameIdMap.get(updateElement.getQName());
if (idComputer == null) {
throw new IllegalStateException("no Id value was defined for " + updateElement.getXPath());
}
Element matchedNode = idComputer.evaluateExpression(updateElement, targetDocument);
if (matchedNode != null) {
return new MergeElement(matchedNode);
}
} else {
updateId(updateElement.getQName(), id);
IdComputer idComputer = qNameIdMap.get(updateElement.getQName());
Element matchedNode = idComputer.evaluateExpression(updateElement, targetDocument);
if (matchedNode != null) {
return new MergeElement(matchedNode);
}
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.devonfw.tools.ide.merge.xmlmerger.matcher;

import com.devonfw.tools.ide.merge.xmlmerger.model.MergeElement;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

/**
* The IdComputer class is responsible for building XPath expressions and evaluating those expressions to match elements in a target document.
*/
public class IdComputer {

private final String id;

private static XPathFactory xPathFactory = XPathFactory.newInstance();

public IdComputer(String id) {

this.id = id;
}

/**
* Evaluates the XPath expression for the given merge element in the target document.
*
* @param mergeElement the merge element for which to build the XPath expression
* @param targetDocument the target document in which to evaluate the XPath expression
* @return the matched Element if found, or null if not found
*/

public Element evaluateExpression(MergeElement mergeElement, Document targetDocument) {

try {
XPath xpath = xPathFactory.newXPath();
String xpathExpr = buildXPathExpression(mergeElement);
XPathExpression xpathExpression = xpath.compile(xpathExpr);
return (Element) xpathExpression.evaluate(targetDocument, XPathConstants.NODE);
} catch (XPathExpressionException e) {
throw new IllegalStateException("Failed to match " + mergeElement.getXPath(), e);
}
}

/**
* Builds the XPath expression for the given merge element based on the ID value.
*
* @param mergeElement the merge element for which to build the XPath expression
* @return the XPath expression as a String
*/
private String buildXPathExpression(MergeElement mergeElement) {

String xPath = mergeElement.getXPath();
if (id.startsWith(".")) {
return xPath + "/" + id;
} else if (id.startsWith("/")) {
return id;
} else if (id.startsWith("@")) {
String attributeName = id.substring(1);
String attributeValue = mergeElement.getElement().getAttribute(attributeName);
return xPath + String.format("[@%s='%s']", attributeName, attributeValue);
} else if (id.equals("name()")) {
String tagName = mergeElement.getElement().getTagName();
return xPath + String.format("[name()='%s']", tagName);
} else if (id.equals("text()")) {
String textContent = mergeElement.getElement().getTextContent();
return xPath + String.format("[text()='%s']", textContent);
}
return null;
}

}
Loading

0 comments on commit d195546

Please sign in to comment.