From 4b8c2d71e20d6033483b52c3b7e374698e8a4bc6 Mon Sep 17 00:00:00 2001 From: Jens Wille Date: Tue, 14 Sep 2021 19:19:13 +0200 Subject: [PATCH 1/2] Correctly output more than two levels in nested pass-through (`_elseNested`). Keep track of nested entities and open/close them in the appropriate order. Fixes #378. Related to #107, #338. --- .../org/metafacture/metamorph/Metamorph.java | 96 +++++++--- .../metamorph/TestMetamorphBasics.java | 168 ++++++++++++++++++ 2 files changed, 238 insertions(+), 26 deletions(-) diff --git a/metamorph/src/main/java/org/metafacture/metamorph/Metamorph.java b/metamorph/src/main/java/org/metafacture/metamorph/Metamorph.java index a2feb6bce..7a6ea687c 100644 --- a/metamorph/src/main/java/org/metafacture/metamorph/Metamorph.java +++ b/metamorph/src/main/java/org/metafacture/metamorph/Metamorph.java @@ -53,6 +53,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; /** * Transforms a data stream sent via the {@link StreamReceiver} interface. Use @@ -100,8 +101,9 @@ public final class Metamorph implements StreamPipe, NamedValuePi private MorphErrorHandler errorHandler = new DefaultErrorHandler(); private int recordCount; private final List recordEndListener = new ArrayList<>(); + + private final Deque elseNestedEntities = new LinkedList<>(); private boolean elseNested; - private boolean elseNestedEntityStarted; private String currentLiteralName; protected Metamorph() { @@ -239,16 +241,16 @@ protected void registerNamedValueReceiver(final String source, final NamedValueR @Override public void startRecord(final String identifier) { flattener.startRecord(identifier); + elseNestedEntities.clear(); entityCountStack.clear(); entityCount = 0; currentEntityCount = 0; + entityCountStack.push(Integer.valueOf(entityCount)); ++recordCount; recordCount %= Integer.MAX_VALUE; - entityCountStack.add(Integer.valueOf(entityCount)); - final String identifierFinal = identifier; outputStreamReceiver.startRecord(identifierFinal); @@ -262,12 +264,13 @@ public void endRecord() { } outputStreamReceiver.endRecord(); - entityCountStack.removeLast(); - if (!entityCountStack.isEmpty()) { + flattener.endRecord(); + + entityCountStack.pop(); + + if (!elseNestedEntities.isEmpty() || !entityCountStack.isEmpty()) { throw new IllegalStateException(ENTITIES_NOT_BALANCED); } - - flattener.endRecord(); } @Override @@ -281,13 +284,16 @@ public void startEntity(final String name) { entityCountStack.push(Integer.valueOf(entityCount)); flattener.startEntity(name); + elseNestedEntities.push(new EntityEntry(flattener)); } @Override public void endEntity() { dispatch(flattener.getCurrentPath(), "", getElseSources(), true); - currentEntityCount = entityCountStack.pop().intValue(); flattener.endEntity(); + + elseNestedEntities.pop(); + currentEntityCount = entityCountStack.pop().intValue(); } @Override @@ -322,30 +328,38 @@ private void dispatch(final String path, final String value, final List send(escapeFeedbackChar(k), value, fallbackReceiver)); + } + } - if (entityName != null) { - if (getData(entityName) == null) { - if (!elseNestedEntityStarted) { - outputStreamReceiver.startEntity(entityName); - elseNestedEntityStarted = true; - } + private void dispatchFallback(final String path, final boolean endEntity, final Consumer consumer) { + final EntityEntry entityEntry = elseNested ? elseNestedEntities.peek() : null; - send(escapeFeedbackChar(currentLiteralName), value, fallbackReceiver); + if (endEntity) { + if (entityEntry != null && entityEntry.getStarted()) { + outputStreamReceiver.endEntity(); + } + } + else if (entityEntry != null) { + if (getData(entityEntry.getPath()) == null) { + final Deque entities = new LinkedList<>(); + + for (final EntityEntry e : elseNestedEntities) { + if (e.getStarted()) { + break; } + + e.setStarted(true); + entities.push(e.getName()); } - else { - send(escapeFeedbackChar(path), value, fallbackReceiver); - } + + entities.forEach(outputStreamReceiver::startEntity); + consumer.accept(currentLiteralName); } } + else { + consumer.accept(path); + } } private List getData(final String path) { @@ -468,4 +482,34 @@ public SourceLocation getSourceLocation() { return null; } + private static class EntityEntry { + + private final String name; + private final String path; + + private boolean started; + + EntityEntry(final StreamFlattener flattener) { + name = flattener.getCurrentEntityName(); + path = flattener.getCurrentPath(); + } + + private String getName() { + return name; + } + + private String getPath() { + return path; + } + + private void setStarted(final boolean started) { + this.started = started; + } + + private boolean getStarted() { + return started; + } + + } + } diff --git a/metamorph/src/test/java/org/metafacture/metamorph/TestMetamorphBasics.java b/metamorph/src/test/java/org/metafacture/metamorph/TestMetamorphBasics.java index b21b4c2e0..97adb5961 100644 --- a/metamorph/src/test/java/org/metafacture/metamorph/TestMetamorphBasics.java +++ b/metamorph/src/test/java/org/metafacture/metamorph/TestMetamorphBasics.java @@ -217,6 +217,174 @@ public void issue338_shouldPreserveSameEntitiesInElseNestedSource() { ); } + @Test + public void issue378_shouldOutputMoreThanTwoLevelsInElseNestedSource() { + assertMorph(receiver, + "" + + " " + + "", + i -> { + i.startRecord("1"); + i.startEntity("mods"); + i.literal("ID", "duepublico_mods_00074526"); + i.startEntity("name"); + i.literal("type", "personal"); + i.literal("type", "simple"); + i.startEntity("displayForm"); + i.literal("value", "Armbruster, André"); + i.endEntity(); + i.startEntity("role"); + i.startEntity("roleTerm"); + i.literal("authority", "marcrelator"); + i.literal("type", "code"); + i.literal("value", "aut"); + i.endEntity(); + i.startEntity("roleTerm"); + i.literal("authority", "marcrelator"); + i.literal("type", "text"); + i.literal("value", "Author"); + i.endEntity(); + i.endEntity(); + i.startEntity("nameIdentifier"); + i.literal("type", "gnd"); + i.literal("value", "1081830107"); + i.endEntity(); + i.startEntity("namePart"); + i.literal("type", "family"); + i.literal("value", "Armbruster"); + i.endEntity(); + i.startEntity("namePart"); + i.literal("type", "given"); + i.literal("value", "André"); + i.endEntity(); + i.endEntity(); + i.endEntity(); + i.endRecord(); + }, + (o, f) -> { + o.get().startRecord("1"); + o.get().startEntity("mods"); + o.get().literal("ID", "duepublico_mods_00074526"); + o.get().startEntity("name"); + o.get().literal("type", "personal"); + o.get().literal("type", "simple"); + o.get().startEntity("displayForm"); + o.get().literal("value", "Armbruster, André"); + o.get().endEntity(); + o.get().startEntity("role"); + o.get().startEntity("roleTerm"); + o.get().literal("authority", "marcrelator"); + o.get().literal("type", "code"); + o.get().literal("value", "aut"); + o.get().endEntity(); + o.get().startEntity("roleTerm"); + o.get().literal("authority", "marcrelator"); + o.get().literal("type", "text"); + o.get().literal("value", "Author"); + f.apply(2).endEntity(); + o.get().startEntity("nameIdentifier"); + o.get().literal("type", "gnd"); + o.get().literal("value", "1081830107"); + o.get().endEntity(); + o.get().startEntity("namePart"); + o.get().literal("type", "family"); + o.get().literal("value", "Armbruster"); + o.get().endEntity(); + o.get().startEntity("namePart"); + o.get().literal("type", "given"); + o.get().literal("value", "André"); + f.apply(3).endEntity(); + o.get().endRecord(); + } + ); + } + + @Test + public void shouldOutputMoreThanTwoLevelsInElseNestedSourceWithModifications() { + assertMorph(receiver, + "" + + " " + + " " + + " " + + " " + + " " + + "", + i -> { + i.startRecord("1"); + i.startEntity("mods"); + i.literal("ID", "duepublico_mods_00074526"); + i.startEntity("name"); + i.literal("type", "personal"); + i.literal("type", "simple"); + i.startEntity("displayForm"); + i.literal("value", "Armbruster, André"); + i.endEntity(); + i.startEntity("role"); + i.startEntity("roleTerm"); + i.literal("authority", "marcrelator"); + i.literal("type", "code"); + i.literal("value", "aut"); + i.endEntity(); + i.startEntity("roleTerm"); + i.literal("authority", "marcrelator"); + i.literal("type", "text"); + i.literal("value", "Author"); + i.endEntity(); + i.endEntity(); + i.startEntity("nameIdentifier"); + i.literal("type", "gnd"); + i.literal("value", "1081830107"); + i.endEntity(); + i.startEntity("namePart"); + i.literal("type", "family"); + i.literal("value", "Armbruster"); + i.endEntity(); + i.startEntity("namePart"); + i.literal("type", "given"); + i.literal("value", "André"); + i.endEntity(); + i.endEntity(); + i.endEntity(); + i.endRecord(); + }, + (o, f) -> { + o.get().startRecord("1"); + o.get().startEntity("mods"); + o.get().literal("ID", "duepublico_mods_00074526"); + o.get().startEntity("name"); + o.get().literal("type", "personal"); + o.get().literal("type", "simple"); + o.get().startEntity("role"); + o.get().startEntity("roleTerm"); + o.get().literal("authority", "marcrelator"); + o.get().literal("type", "code"); + o.get().literal("value", "aut"); + o.get().endEntity(); + o.get().startEntity("roleTerm"); + o.get().literal("authority", "marcrelator"); + o.get().literal("type", "text"); + o.get().literal("value", "Author"); + f.apply(2).endEntity(); + o.get().startEntity("nameIdentifier"); + o.get().literal("type", "gnd"); + o.get().literal("value", "1081830107"); + o.get().endEntity(); + o.get().startEntity("namePart"); + o.get().literal("type", "family"); + o.get().endEntity(); + o.get().startEntity("namePart"); + o.get().literal("type", "given"); + f.apply(3).endEntity(); + o.get().startEntity("name"); + o.get().literal("displayForm", "Armbruster, André"); + o.get().literal("mods.name.namePart.value", "Armbruster"); + o.get().literal("mods.name.namePart.value", "André"); + o.get().endEntity(); + o.get().endRecord(); + } + ); + } + @Test public void shouldHandleUnmatchedLiteralsAndEntitiesInElseNestedSource() { assertMorph(receiver, From 945fb6b567a269a01fcb78425ec95d230867e284 Mon Sep 17 00:00:00 2001 From: Jens Wille Date: Wed, 15 Sep 2021 21:47:20 +0200 Subject: [PATCH 2/2] Add test for array marker propagation in nested pass-through (#374). Fails in current master, passes with #392. --- .../metamorph/TestMetamorphBasics.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/metamorph/src/test/java/org/metafacture/metamorph/TestMetamorphBasics.java b/metamorph/src/test/java/org/metafacture/metamorph/TestMetamorphBasics.java index 97adb5961..e8aee5836 100644 --- a/metamorph/src/test/java/org/metafacture/metamorph/TestMetamorphBasics.java +++ b/metamorph/src/test/java/org/metafacture/metamorph/TestMetamorphBasics.java @@ -217,6 +217,58 @@ public void issue338_shouldPreserveSameEntitiesInElseNestedSource() { ); } + @Test + public void issue374_shouldPropagateArrayMarkersInElseNestedSource() { + assertMorph(receiver, + "" + + " " + + "", + i -> { + i.startRecord("1"); + i.startEntity("author[]"); + i.startEntity(""); + i.literal("@type", "Person"); + i.literal("name", "Katja Königstein-Lüdersdorff"); + i.endEntity(); + i.startEntity(""); + i.literal("@type", "Person"); + i.literal("name", "Corinna Peters"); + i.endEntity(); + i.startEntity(""); + i.literal("@type", "Person"); + i.literal("name", "Oleg Tjulenev"); + i.endEntity(); + i.startEntity(""); + i.literal("@type", "Person"); + i.literal("name", "Claudia Vogeler"); + i.endEntity(); + i.endEntity(); + i.endRecord(); + }, + (o, f) -> { + o.get().startRecord("1"); + o.get().startEntity("author[]"); + o.get().startEntity(""); + o.get().literal("@type", "Person"); + o.get().literal("name", "Katja Königstein-Lüdersdorff"); + o.get().endEntity(); + o.get().startEntity(""); + o.get().literal("@type", "Person"); + o.get().literal("name", "Corinna Peters"); + o.get().endEntity(); + o.get().startEntity(""); + o.get().literal("@type", "Person"); + o.get().literal("name", "Oleg Tjulenev"); + o.get().endEntity(); + o.get().startEntity(""); + o.get().literal("@type", "Person"); + o.get().literal("name", "Claudia Vogeler"); + f.apply(2).endEntity(); + o.get().endRecord(); + } + ); + } + @Test public void issue378_shouldOutputMoreThanTwoLevelsInElseNestedSource() { assertMorph(receiver,