-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add XtfFileMerger to combine base data and patches
- Loading branch information
Showing
6 changed files
with
292 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
src/main/java/ch/geowerkstatt/interlis/testbed/runner/xtf/Basket.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
163 changes: 163 additions & 0 deletions
163
src/main/java/ch/geowerkstatt/interlis/testbed/runner/xtf/XtfFileMerger.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
61 changes: 61 additions & 0 deletions
61
src/test/java/ch/geowerkstatt/interlis/testbed/runner/xtf/XtfFileMergerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |