Skip to content

Commit

Permalink
Add XtfFileMerger to combine base data and patches
Browse files Browse the repository at this point in the history
  • Loading branch information
domi-b committed Dec 7, 2023
1 parent 7bbfffa commit fccf184
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 0 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 addOrReplaceChild(String entryId, Node node) {
var originalEntry = objects().get(entryId);
if (originalEntry == null) {
element().appendChild(node);
} else {
element().replaceChild(node, originalEntry);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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 {
private static final Logger LOGGER = LogManager.getLogger();
private static final String BASKET_ID = "BID";
private static final String OBJECT_ID = "TID";

private final DocumentBuilderFactory factory;

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

/**
* 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.
*/
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);
return true;
} catch (Exception e) {
LOGGER.error(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 patchBasket : patchBaskets.entrySet()) {
var basketId = patchBasket.getKey();

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

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

var importedNode = document.importNode(patchEntry.getValue(), true);
originalBasket.addOrReplaceChild(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 = e.hasAttribute(BASKET_ID);
if (!hasId) {
LOGGER.warn("Basket without " + BASKET_ID + " found.");
}
return hasId;
})
.collect(Collectors.toMap(e -> e.getAttribute(BASKET_ID), XtfFileMerger::collectBasket));
return Optional.of(baskets);
}

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

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);
}
}
27 changes: 27 additions & 0 deletions src/test/data/xtf-merger/data.xtf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<ili:transfer xmlns:ili="http://www.interlis.ch/xtf/2.4/INTERLIS">
<ili:headersection>
<ili:models>
</ili:models>
<ili:sender>interlis-testbed-runner</ili:sender>
</ili:headersection>
<ili:datasection>
<ModelA.TopicA BID="B1">
<ModelA.TopicA.ClassA TID="A1">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
<ModelA.TopicA.ClassA TID="A2">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>

<ModelA.TopicA BID="B2">
<ModelA.TopicA.ClassA TID="A1">
<attr1>Some text</attr1>
<attr2>Some more text</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>
</ili:datasection>
</ili:transfer>
15 changes: 15 additions & 0 deletions src/test/data/xtf-merger/patch.xtf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<ili:transfer xmlns:ili="http://www.interlis.ch/xtf/2.4/INTERLIS">
<ili:datasection>
<ModelA.TopicA BID="B1">
<ModelA.TopicA.ClassA TID="A2">
<attr1>New value for attr1</attr1>
<!-- missing attribute attr2 -->
</ModelA.TopicA.ClassA>
<ModelA.TopicA.ClassA TID="A3">
<attr1>New entry</attr1>
<attr2>Attr2</attr2>
</ModelA.TopicA.ClassA>
</ModelA.TopicA>
</ili:datasection>
</ili:transfer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ch.geowerkstatt.interlis.testbed.runner.xtf;

import ch.geowerkstatt.interlis.testbed.runner.TestLogAppender;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

public final class XtfFileMergerTest {
private static final String BASE_PATH = "src/test/data/xtf-merger";

private TestLogAppender appender;

@BeforeEach
public void setup() {
appender = TestLogAppender.registerAppender(XtfFileMerger.class);
}

@AfterEach
public void teardown() {
appender.stop();
appender.unregister();
}

@Test
public void validateMergedXtf() throws Exception {
var baseFile = Path.of(BASE_PATH, "data.xtf");
var patchFile = Path.of(BASE_PATH, "patch.xtf");
var outputFile = Path.of(BASE_PATH, "output", "merged.xtf");

var merger = new XtfFileMerger();

var mergeResult = merger.merge(baseFile, patchFile, outputFile);

assertTrue(mergeResult, "Merging should have been successful.");
assertTrue(Files.exists(outputFile), "Output file should have been created.");

assertEquals(0, appender.getErrorMessages().size(), "No errors should have been logged.");

var documentBuilder = merger.createDocumentBuilder();
var mergedDocument = documentBuilder.parse(outputFile.toFile());
var baskets = XtfFileMerger.findBaskets(mergedDocument);
assertTrue(baskets.isPresent(), "Baskets should have been found in merged file.");

var b1 = baskets.get().get("B1");
assertNotNull(b1, "Basket B1 should have been found in merged file.");
assertIterableEquals(List.of("A1", "A2", "A3"), b1.objects().keySet());

var b2 = baskets.get().get("B2");
assertNotNull(b2, "Basket B2 should have been found in merged file.");
assertIterableEquals(List.of("A1"), b2.objects().keySet());
}
}

0 comments on commit fccf184

Please sign in to comment.