From 7074268416e14b6082f42dfdbf1930e14c178643 Mon Sep 17 00:00:00 2001 From: Jens Wille Date: Wed, 1 Dec 2021 11:03:15 +0100 Subject: [PATCH] Introduce concept of virtual (internal) fields. (#63) Virtual fields can be accessed like regular metadata fields, but aren't emitted by default. --- .../java/org/metafacture/metafix/Record.java | 69 ++++++++- .../java/org/metafacture/metafix/Value.java | 2 +- .../metafacture/metafix/HashValueTest.java | 24 ++- .../metafix/MetafixTestHelpers.java | 18 ++- .../org/metafacture/metafix/RecordTest.java | 140 ++++++++++++++++++ 5 files changed, 236 insertions(+), 17 deletions(-) diff --git a/metafix/src/main/java/org/metafacture/metafix/Record.java b/metafix/src/main/java/org/metafacture/metafix/Record.java index 3f2602e5..ce597d6a 100644 --- a/metafix/src/main/java/org/metafacture/metafix/Record.java +++ b/metafix/src/main/java/org/metafacture/metafix/Record.java @@ -16,12 +16,18 @@ package org.metafacture.metafix; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + /** * Represents a metadata record, i.e., a {@link Value.Hash Hash} of fields * and values. */ public class Record extends Value.Hash { + private final Map virtualFields = new LinkedHashMap<>(); + private boolean reject; /** @@ -40,6 +46,7 @@ public Record shallowClone() { clone.setReject(reject); forEach(clone::put); + virtualFields.forEach(clone::putVirtualField); return clone; } @@ -62,10 +69,70 @@ public boolean getReject() { return reject; } + /** + * Checks whether this record contains the virtual field. + * + * @param field the field name + * @return true if this record contains the virtual field, false otherwise + */ + public boolean containsVirtualField(final String field) { + return virtualFields.containsKey(field); + } + + /** + * Adds a virtual field/value pair to this record, provided it's not + * {@link Value#isNull(Value) null}. Virtual fields can be + * {@link #get(String) accessed} like regular metadata fields, but aren't + * {@link #forEach(BiConsumer) emitted} by default. + * + * @param field the field name + * @param value the metadata value + * + * @see #retainFields(Collection) + */ + public void putVirtualField(final String field, final Value value) { + if (!Value.isNull(value)) { + virtualFields.put(field, value); + } + } + @Override public String toString() { - // TODO: Improve string representation? Include reject status, etc.? + // TODO: Improve string representation? Include reject status, virtual fields, etc.? return super.toString(); } + /** + * Retrieves the field value from this record. Falls back to retrieving the + * virtual field if the field name is not already + * {@link #containsField(String) present}. + * + * @param field the field name + * @return the metadata value + */ + @Override + public Value get(final String field) { + return containsField(field) ? super.get(field) : virtualFields.get(field); + } + + /** + * Retains only the given field/value pairs in this record. Turns + * virtual fields into regular metadata fields if they're not already + * {@link #containsField(String) present}. + * + * @param fields the field names + */ + @Override + public void retainFields(final Collection fields) { + virtualFields.keySet().retainAll(fields); + + virtualFields.forEach((f, v) -> { + if (!containsField(f)) { + put(f, v); + } + }); + + super.retainFields(fields); + } + } diff --git a/metafix/src/main/java/org/metafacture/metafix/Value.java b/metafix/src/main/java/org/metafacture/metafix/Value.java index fc20dc45..bb40f4fb 100644 --- a/metafix/src/main/java/org/metafacture/metafix/Value.java +++ b/metafix/src/main/java/org/metafacture/metafix/Value.java @@ -472,7 +472,7 @@ public int size() { } /** - * Adds a field/value pair to this hash, provided it's not {@code null}. + * Adds a field/value pair to this hash, provided it's not {@link #isNull(Value) null}. * * @param field the field name * @param value the metadata value diff --git a/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java b/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java index a26e6030..1b4d6c4d 100644 --- a/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java +++ b/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java @@ -19,9 +19,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; public class HashValueTest { @@ -200,21 +198,19 @@ public void shouldRemoveEmptyValues() { @Test public void shouldIterateOverFieldValuePairs() { + final Value emptyValue = new Value(""); + final Value specialValue = new Value("1"); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.put(OTHER_FIELD, OTHER_VALUE); - hash.put("empty field", new Value("")); - hash.put("_special field", new Value("1")); - - final List fields = new ArrayList<>(); - final List values = new ArrayList<>(); - hash.forEach((f, v) -> { - fields.add(f); - values.add(v.asString()); - }); - - Assertions.assertEquals(Arrays.asList(FIELD, OTHER_FIELD, "empty field", "_special field"), fields); - Assertions.assertEquals(Arrays.asList(VALUE.asString(), OTHER_VALUE.asString(), "", "1"), values); + hash.put("empty field", emptyValue); + hash.put("_special field", specialValue); + + MetafixTestHelpers.assertEmittedFields(hash, + Arrays.asList(FIELD, OTHER_FIELD, "empty field", "_special field"), + Arrays.asList(VALUE, OTHER_VALUE, emptyValue, specialValue) + ); } private Value.Hash newHash() { diff --git a/metafix/src/test/java/org/metafacture/metafix/MetafixTestHelpers.java b/metafix/src/test/java/org/metafacture/metafix/MetafixTestHelpers.java index b3c5f9b9..c99af192 100644 --- a/metafix/src/test/java/org/metafacture/metafix/MetafixTestHelpers.java +++ b/metafix/src/test/java/org/metafacture/metafix/MetafixTestHelpers.java @@ -18,11 +18,13 @@ import org.metafacture.framework.StreamReceiver; +import org.junit.jupiter.api.Assertions; import org.mockito.InOrder; import org.mockito.Mockito; import org.mockito.exceptions.base.MockitoAssertionError; import java.io.FileNotFoundException; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; @@ -38,7 +40,8 @@ */ public final class MetafixTestHelpers { - private MetafixTestHelpers() { } + private MetafixTestHelpers() { + } public static void assertFix(final StreamReceiver receiver, final List fixDef, final Consumer in, final Consumer> out) { @@ -90,4 +93,17 @@ private static Metafix fix(final StreamReceiver receiver, final String fix, fina return metafix; } + public static void assertEmittedFields(final Value.Hash hash, final List expectedFields, final List expectedValues) { + final List actualFields = new ArrayList<>(); + final List actualValues = new ArrayList<>(); + + hash.forEach((f, v) -> { + actualFields.add(f); + actualValues.add(v); + }); + + Assertions.assertEquals(expectedFields, actualFields); + Assertions.assertEquals(expectedValues, actualValues); + } + } diff --git a/metafix/src/test/java/org/metafacture/metafix/RecordTest.java b/metafix/src/test/java/org/metafacture/metafix/RecordTest.java index fdc0381c..9cb38c45 100644 --- a/metafix/src/test/java/org/metafacture/metafix/RecordTest.java +++ b/metafix/src/test/java/org/metafacture/metafix/RecordTest.java @@ -19,6 +19,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.Arrays; + public class RecordTest { private static final String FIELD = "field"; @@ -131,4 +133,142 @@ public void shouldNotEmitCloneOfDefaultRecordIfRejected() { Assertions.assertTrue(clone.getReject()); } + @Test + public void shouldNotContainMissingVirtualField() { + final Record record = new Record(); + Assertions.assertFalse(record.containsVirtualField(FIELD)); + } + + @Test + public void shouldContainExistingVirtualField() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + Assertions.assertTrue(record.containsVirtualField(FIELD)); + } + + @Test + public void shouldNotContainVirtualFieldWithNullValue() { + final Record record = new Record(); + record.putVirtualField(FIELD, null); + + Assertions.assertFalse(record.containsVirtualField(FIELD)); + } + + @Test + public void shouldGetVirtualField() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + Assertions.assertEquals(VALUE, record.get(FIELD)); + } + + @Test + public void shouldGetRegularFieldInsteadOfVirtualField() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + record.put(FIELD, OTHER_VALUE); + + Assertions.assertEquals(OTHER_VALUE, record.get(FIELD)); + } + + @Test + public void shouldNotEmitVirtualFieldsByDefault() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + MetafixTestHelpers.assertEmittedFields(record, Arrays.asList(), Arrays.asList()); + } + + @Test + public void shouldEmitVirtualFieldsWhenRetained() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + record.retainFields(Arrays.asList(FIELD)); + + MetafixTestHelpers.assertEmittedFields(record, Arrays.asList(FIELD), Arrays.asList(VALUE)); + } + + @Test + public void shouldEmitVirtualFieldsWhenCopied() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + record.put(FIELD, record.get(FIELD)); + + MetafixTestHelpers.assertEmittedFields(record, Arrays.asList(FIELD), Arrays.asList(VALUE)); + } + + @Test + public void shouldEmitVirtualFieldsWhenAdded() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + record.put(FIELD, OTHER_VALUE); + + MetafixTestHelpers.assertEmittedFields(record, Arrays.asList(FIELD), Arrays.asList(OTHER_VALUE)); + } + + @Test + public void shouldCreateShallowCloneFromRecordWithVirtualFields() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + final Record clone = record.shallowClone(); + + Assertions.assertNotSame(record, clone); + Assertions.assertSame(record.get(FIELD), clone.get(FIELD)); + } + + @Test + public void shouldNotModifyTopLevelFromShallowCloneWithVirtualFields() { + final Record record = new Record(); + + final Record clone = record.shallowClone(); + clone.putVirtualField(FIELD, VALUE); + + Assertions.assertNotSame(record, clone); + Assertions.assertNull(record.get(FIELD)); + Assertions.assertNotNull(clone.get(FIELD)); + } + + @Test + public void shouldNotModifyOverwrittenValueFromShallowCloneWithVirtualFields() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + final Record clone = record.shallowClone(); + clone.putVirtualField(FIELD, OTHER_VALUE); + + Assertions.assertNotSame(record, clone); + Assertions.assertEquals(VALUE, record.get(FIELD)); + Assertions.assertEquals(OTHER_VALUE, clone.get(FIELD)); + } + + @Test + public void shouldModifySubLevelFromShallowCloneWithVirtualFields() { + final Record record = new Record(); + record.putVirtualField(FIELD, Value.newArray(a -> a.add(VALUE))); + + final Record clone = record.shallowClone(); + clone.get(FIELD).asArray().add(OTHER_VALUE); + + Assertions.assertNotSame(record, clone); + Assertions.assertSame(record.get(FIELD), clone.get(FIELD)); + + final Value.Array array = clone.get(FIELD).asArray(); + Assertions.assertEquals(OTHER_VALUE, array.get(1)); + } + + @Test + public void shouldNotEmitVirtualFieldsFromShallowClone() { + final Record record = new Record(); + record.putVirtualField(FIELD, VALUE); + + final Record clone = record.shallowClone(); + MetafixTestHelpers.assertEmittedFields(clone, Arrays.asList(), Arrays.asList()); + } + }