Skip to content

Commit

Permalink
Merge partial transfer files with base data (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
domi-b authored Dec 11, 2023
2 parents b70c468 + 2bf0a74 commit 4ca313e
Show file tree
Hide file tree
Showing 33 changed files with 1,000 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ build/
!**/src/main/**/build/
!**/src/test/**/build/

# Files generated by interlis-testbed-runner
output/

### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies {

testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.xmlunit:xmlunit-core:2.9.1'
}

application {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package ch.geowerkstatt.interlis.testbed.runner.xtf;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.util.Map;

public record Basket(Element element, Map<String, Element> objects) {
/**
* Adds or replaces the child node with the given entry ID.
*
* @param entryId the entry ID
* @param node the node to add or replace
*/
public void addOrReplaceChildNode(String entryId, Node node) {
var originalEntry = objects().get(entryId);
if (originalEntry == null) {
element().appendChild(node);
} else {
element().replaceChild(node, originalEntry);
}
}

/**
* Removes the basket node from the XML document.
*/
public void removeBasketNode() {
element().getParentNode().removeChild(element());
}

/**
* Removes the child node with the given entry ID.
*
* @param entryId the ID of the entry to remove
* @return {@code true} if the entry was removed, {@code false} if the entry was not found
*/
public boolean removeChildNode(String entryId) {
var originalEntry = objects().get(entryId);
if (originalEntry == null) {
return false;
}

element().removeChild(originalEntry);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package ch.geowerkstatt.interlis.testbed.runner.xtf;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public final class XtfFileMerger implements XtfMerger {
private static final Logger LOGGER = LogManager.getLogger();
private static final String BASKET_ID = "BID";
private static final String OBJECT_ID = "TID";
private static final String DELETE_ATTRIBUTE = "DELETE";
private static final String DELETE_ATTRIBUTE_LOWERCASE = DELETE_ATTRIBUTE.toLowerCase();
private static final String INTERLIS24_NAMESPACE = "http://www.interlis.ch/xtf/2.4/INTERLIS";

private final DocumentBuilderFactory factory;

/**
* Creates a new instance of the XtfFileMerger class.
*/
public XtfFileMerger() {
factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
}

@Override
public boolean merge(Path baseFile, Path patchFile, Path outputFile) {
try {
LOGGER.info("Merging " + baseFile + " with " + patchFile + " into " + outputFile);
var documentBuilder = createDocumentBuilder();

var baseDocument = documentBuilder.parse(baseFile.toFile());
var patchDocument = documentBuilder.parse(patchFile.toFile());

var baseBaskets = findBaskets(baseDocument);
if (baseBaskets.isEmpty()) {
LOGGER.error("No baskets found in base file " + baseFile + ".");
return false;
}

var patchBaskets = findBaskets(patchDocument);
if (patchBaskets.isEmpty()) {
LOGGER.error("No baskets found in patch file " + patchFile + ".");
return false;
}

if (!mergeBaskets(baseDocument, baseBaskets.get(), patchBaskets.get())) {
return false;
}

writeMergedFile(baseDocument, outputFile);
LOGGER.info("Successfully merged files into " + outputFile);
return true;
} catch (Exception e) {
LOGGER.error("Failed to merge files.", e);
return false;
}
}

DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
return factory.newDocumentBuilder();
}

private static boolean mergeBaskets(Document document, Map<String, Basket> baseBaskets, Map<String, Basket> patchBaskets) {
var isValid = true;

for (var patchBasketEntry : patchBaskets.entrySet()) {
var basketId = patchBasketEntry.getKey();
var patchBasket = patchBasketEntry.getValue();

var originalBasket = baseBaskets.get(basketId);
if (originalBasket == null) {
LOGGER.error("Basket " + basketId + " not found in base file.");
isValid = false;
continue;
}

if (hasDeleteAttribute(patchBasket.element())) {
originalBasket.removeBasketNode();
continue;
}

for (var patchEntry : patchBasket.objects().entrySet()) {
var entryId = patchEntry.getKey();
var element = patchEntry.getValue();

if (hasDeleteAttribute(element)) {
if (!originalBasket.removeChildNode(entryId)) {
LOGGER.error("Could not remove entry " + entryId + " from basket " + basketId + " as it does not exist.");
isValid = false;
}
} else {
var importedNode = document.importNode(element, true);
originalBasket.addOrReplaceChildNode(entryId, importedNode);
}
}
}

return isValid;
}

private static void writeMergedFile(Document document, Path outputFile) throws IOException, TransformerException {
Files.createDirectories(outputFile.getParent());

var transformerFactory = TransformerFactory.newInstance();
var transformer = transformerFactory.newTransformer();
var source = new DOMSource(document);
var result = new StreamResult(outputFile.toFile());
transformer.transform(source, result);
}

static Optional<Map<String, Basket>> findBaskets(Document document) {
var dataSection = findDataSection(document);
if (dataSection.isEmpty()) {
return Optional.empty();
}

var baskets = streamChildElementNodes(dataSection.get())
.filter(e -> {
var hasId = hasInterlisAttribute(e, BASKET_ID);
if (!hasId) {
LOGGER.warn("Basket without " + BASKET_ID + " found.");
}
return hasId;
})
.collect(Collectors.toMap(e -> getInterlisAttribute(e, BASKET_ID), XtfFileMerger::collectBasket));
return Optional.of(baskets);
}

private static Basket collectBasket(Element basket) {
var objects = streamChildElementNodes(basket)
.filter(e -> {
var hasId = hasInterlisAttribute(e, OBJECT_ID);
if (!hasId) {
LOGGER.warn("Entry without " + OBJECT_ID + " found in basket " + basket.getAttribute(BASKET_ID) + ".");
}
return hasId;
})
.collect(Collectors.toMap(e -> getInterlisAttribute(e, OBJECT_ID), e -> e));
return new Basket(basket, objects);
}

private static boolean hasDeleteAttribute(Element element) {
return element.hasAttribute(DELETE_ATTRIBUTE) || element.hasAttribute(DELETE_ATTRIBUTE_LOWERCASE);
}

private static boolean hasInterlisAttribute(Element element, String attributeName) {
return getInterlisAttribute(element, attributeName) != null;
}

private static String getInterlisAttribute(Element element, String attributeName) {
if (element.hasAttribute(attributeName)) {
return element.getAttribute(attributeName);
}

var ili24Name = attributeName.toLowerCase();
if (element.hasAttributeNS(INTERLIS24_NAMESPACE, ili24Name)) {
return element.getAttributeNS(INTERLIS24_NAMESPACE, ili24Name);
}
return null;
}

private static Optional<Element> findDataSection(Document document) {
var transfer = document.getFirstChild();
return streamChildElementNodes(transfer)
.filter(n -> n.getLocalName().equalsIgnoreCase("datasection"))
.findFirst();
}

private static Stream<Element> streamChildElementNodes(Node node) {
var childNodes = node.getChildNodes();
return IntStream.range(0, childNodes.getLength())
.mapToObj(childNodes::item)
.filter(n -> n instanceof Element)
.map(n -> (Element) n);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ch.geowerkstatt.interlis.testbed.runner.xtf;

import java.nio.file.Path;

public interface XtfMerger {
/**
* Merges the patch file into the base file and writes the result to the output file.
*
* @param baseFile the base file
* @param patchFile the patch file
* @param outputFile the output file
* @return {@code true} if the merge was successful, {@code false} otherwise.
*/
boolean merge(Path baseFile, Path patchFile, Path outputFile);
}
25 changes: 25 additions & 0 deletions src/test/data/xtf-merger/ili23/add/data.xtf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<TRANSFER xmlns="http://www.interlis.ch/INTERLIS2.3">
<HEADERSECTION SENDER="interlis-testbed-runner" VERSION="2.3">
<MODELS>
</MODELS>
</HEADERSECTION>
<DATASECTION>
<ModelA.TopicA BID="B1">
<ModelA.TopicA.ClassA TID="A1_1">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>

<ModelA.TopicA BID="B2">
<ModelA.TopicA.ClassA TID="A2_1">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>

<ModelA.TopicA BID="B3">
</ModelA.TopicA>
</DATASECTION>
</TRANSFER>
33 changes: 33 additions & 0 deletions src/test/data/xtf-merger/ili23/add/expected.xtf
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<TRANSFER xmlns="http://www.interlis.ch/INTERLIS2.3">
<HEADERSECTION SENDER="interlis-testbed-runner" VERSION="2.3">
<MODELS>
</MODELS>
</HEADERSECTION>
<DATASECTION>
<ModelA.TopicA BID="B1">
<ModelA.TopicA.ClassA TID="A1_1">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
<ModelA.TopicA.ClassA TID="A1_2">
<attr1>New entry</attr1>
<attr2>Attr2</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>

<ModelA.TopicA BID="B2">
<ModelA.TopicA.ClassA TID="A2_1">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>

<ModelA.TopicA BID="B3">
<ModelA.TopicA.ClassA TID="A3_1">
<attr1>Attribute 1</attr1>
<attr2>Attribute 2</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>
</DATASECTION>
</TRANSFER>
18 changes: 18 additions & 0 deletions src/test/data/xtf-merger/ili23/add/patch.xtf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<TRANSFER xmlns="http://www.interlis.ch/INTERLIS2.3">
<DATASECTION>
<ModelA.TopicA BID="B1">
<ModelA.TopicA.ClassA TID="A1_2">
<attr1>New entry</attr1>
<attr2>Attr2</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>

<ModelA.TopicA BID="B3">
<ModelA.TopicA.ClassA TID="A3_1">
<attr1>Attribute 1</attr1>
<attr2>Attribute 2</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>
</DATASECTION>
</TRANSFER>
38 changes: 38 additions & 0 deletions src/test/data/xtf-merger/ili23/combined/data.xtf
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<TRANSFER xmlns="http://www.interlis.ch/INTERLIS2.3">
<HEADERSECTION SENDER="interlis-testbed-runner" VERSION="2.3">
<MODELS>
</MODELS>
</HEADERSECTION>
<DATASECTION>
<ModelA.TopicA BID="B1">
<ModelA.TopicA.ClassA TID="A1_1">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
<line>
<POLYLINE>
<COORD>
<C1>1.0</C1>
<C2>2.0</C2>
</COORD>
<COORD>
<C1>1.5</C1>
<C2>2.5</C2>
</COORD>
</POLYLINE>
</line>
</ModelA.TopicA.ClassA>
<ModelA.TopicA.ClassA TID="A1_2">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>

<ModelA.TopicA BID="B2">
<ModelA.TopicA.ClassA TID="A2_1">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>
</DATASECTION>
</TRANSFER>
Loading

0 comments on commit 4ca313e

Please sign in to comment.