From eebe562b1f9f22cdf2a9cf882f5ef356b34ea203 Mon Sep 17 00:00:00 2001 From: Jens Wille Date: Mon, 15 Nov 2021 16:23:33 +0100 Subject: [PATCH] Introduce type-safe record value abstraction. (#64) Obviates the need for `instanceof` checks and unchecked casts. A `Value` is a container/wrapper for either - an `Array` (which in turn is a wrapper for `List`), or - a `Hash` (which in turn is a wrapper for `Map`), or - a `String` (which is the terminal type) --- metafix/build.gradle | 4 + .../org/metafacture/metafix/FixMethod.java | 185 ++++++------ .../org/metafacture/metafix/FixPredicate.java | 44 +-- .../java/org/metafacture/metafix/Metafix.java | 111 +++++--- .../metafix/RecordTransformer.java | 6 +- .../java/org/metafacture/metafix/Value.java | 264 ++++++++++++++++-- .../metafacture/metafix/HashValueTest.java | 61 ++-- .../org/metafacture/metafix/RecordTest.java | 23 +- 8 files changed, 469 insertions(+), 229 deletions(-) diff --git a/metafix/build.gradle b/metafix/build.gradle index 8fd11152..db7e9788 100644 --- a/metafix/build.gradle +++ b/metafix/build.gradle @@ -43,6 +43,10 @@ dependencies { test { useJUnitPlatform() + + testlogger { + showFullStackTraces true + } } task install(dependsOn: publishToMavenLocal, diff --git a/metafix/src/main/java/org/metafacture/metafix/FixMethod.java b/metafix/src/main/java/org/metafacture/metafix/FixMethod.java index 55e6fb5e..1963c2f9 100644 --- a/metafix/src/main/java/org/metafacture/metafix/FixMethod.java +++ b/metafix/src/main/java/org/metafacture/metafix/FixMethod.java @@ -18,9 +18,7 @@ import org.metafacture.metamorph.maps.FileMap; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -46,49 +44,46 @@ public void apply(final Record record, final List params, final Map toAdd.forEach(s -> a.add(new Value(s))))); } } }, set_hash { - @SuppressWarnings("unchecked") public void apply(final Record record, final List params, final Map options) { final String key = params.get(0); - final Object val = record.get(key.replace(DOT_APPEND, EMPTY)); + final Value val = record.get(key.replace(DOT_APPEND, EMPTY)); - final Value.Hash result = new Value.Hash(); - options.forEach(result::put); + final Value value = Value.newHash(h -> options.forEach((k, v) -> h.put(k, new Value(v)))); - if (key.endsWith(DOT_APPEND) && val instanceof List) { - ((List) val).add(result); + if (key.endsWith(DOT_APPEND) && val.isArray()) { + val.asArray().add(value); } else { - record.put(key, result); + record.put(key, value); } } }, array { // array-from-hash public void apply(final Record record, final List params, final Map options) { final String fieldName = params.get(0); - Metafix.asList(record.get(fieldName)).forEach(recordEntry -> { - if (recordEntry instanceof Value.Hash) { + Metafix.asList(record.get(fieldName), a -> a.forEach(recordEntry -> { + if (recordEntry.isHash()) { record.remove(fieldName); - ((Value.Hash) recordEntry).forEach((subFieldName, value) -> { - Metafix.add(record, fieldName, subFieldName); + recordEntry.asHash().forEach((subFieldName, value) -> { + Metafix.add(record, fieldName, new Value(subFieldName)); Metafix.add(record, fieldName, value); }); } - }); + })); } }, hash { // hash-from-array public void apply(final Record record, final List params, final Map options) { - final List values = Metafix.asList(record.get(params.get(0))); - final Value.Hash result = new Value.Hash(); - for (int i = 1; i < values.size(); i = i + 2) { - result.put(values.get(i - 1).toString(), values.get(i)); - } - record.put(params.get(0), result); + Metafix.asList(record.get(params.get(0)), values -> record.put(params.get(0), Value.newHash(h -> { + for (int i = 1; i < values.size(); i = i + 2) { + h.put(values.get(i - 1).toString(), values.get(i)); + } + }))); } }, add_field { @@ -114,14 +109,15 @@ public void apply(final Record record, final List params, final Map params, final Map options) { - final Collection oldVals = Metafix.asList(record.get(params.get(0))); - final String newVal = String.format(params.get(1), oldVals.toArray(new Object[] {})); - record.replace(params.get(0), Arrays.asList(newVal)); + Metafix.asList(record.get(params.get(0)), oldVals -> { + final String newVal = String.format(params.get(1), oldVals.stream().toArray()); + record.replace(params.get(0), new Value(Arrays.asList(new Value(newVal)))); + }); } }, parse_text { public void apply(final Record record, final List params, final Map options) { - Metafix.asList(record.get(params.get(0))).forEach(v -> { + Metafix.asList(record.get(params.get(0)), a -> a.forEach(v -> { final Pattern p = Pattern.compile(params.get(1)); final Matcher m = p.matcher(v.toString()); if (m.matches()) { @@ -135,23 +131,23 @@ public void apply(final Record record, final List params, final Map { + while (groupMatcher.find()) { + final String group = groupMatcher.group(1); + h.put(group, new Value(m.group(group))); + } + }); - if (!result.isEmpty()) { - Metafix.add(record, params.get(0), result); + if (!value.asHash().isEmpty()) { + Metafix.add(record, params.get(0), value); } else { for (int i = 1; i <= m.groupCount(); i = i + 1) { - Metafix.add(record, params.get(0), m.group(i)); + Metafix.add(record, params.get(0), new Value(m.group(i))); } } } - }); + })); } }, paste { @@ -159,8 +155,8 @@ public void apply(final Record record, final List params, final Map literalString(k) || find(record, split(k)) != null) - .map(k -> literalString(k) ? k.substring(1) : Metafix.asList(find(record, split(k))).iterator().next()) - .map(Object::toString).collect(Collectors.joining(joinChar != null ? joinChar : " "))); + .map(k -> literalString(k) ? new Value(k.substring(1)) : Metafix.asList(find(record, split(k)), null).asArray().get(0)) + .map(Value::toString).collect(Collectors.joining(joinChar != null ? joinChar : " "))); } private boolean literalString(final String s) { @@ -246,42 +242,42 @@ private Map fileMap(final String location, final String separato private static void applyToFields(final Record record, final List params, final Function fun) { final String key = params.get(0); - final Object found = find(record, split(key)); + final Value found = find(record, split(key)); if (found != null) { remove(record, split(key)); - new ArrayList<>(Metafix.asList(found)).forEach(old -> { - if (fun != null && old != null) { - final String val = fun.apply(old.toString()); - insert(InsertMode.APPEND, record, split(key), val); - } - }); + + if (fun != null) { + Metafix.asList(found, a -> a.forEach(old -> insert(InsertMode.APPEND, record, split(key), fun.apply(old.toString())))); + } } } - private static Object insert(final InsertMode mode, final Value.Hash record, final String[] keys, final String value) { + private static Value insert(final InsertMode mode, final Value.Hash record, final String[] keys, final String value) { final String currentKey = keys[0]; + if (keys.length == 1) { mode.apply(record, currentKey, value); - return record; } - final String[] remainingKeys = Arrays.copyOfRange(keys, 1, keys.length); - final Object nested = insertNested(mode, record, value, currentKey, remainingKeys); - record.put(currentKey, nested); - return record; + else { + final String[] remainingKeys = Arrays.copyOfRange(keys, 1, keys.length); + final Value nested = insertNested(mode, record, value, currentKey, remainingKeys); + record.put(currentKey, nested); + } + + return new Value(record); } - @SuppressWarnings("unchecked") - private static Object insertNested(final InsertMode mode, final Value.Hash record, final String value, final String currentKey, final String[] remainingKeys) { + private static Value insertNested(final InsertMode mode, final Value.Hash record, final String value, final String currentKey, final String[] remainingKeys) { if (!record.containsField(currentKey)) { - record.put(currentKey, new Value.Hash()); + record.put(currentKey, Value.newHash()); } - final Object nested = record.get(currentKey); - final Object result; - if (nested instanceof Value.Hash) { - result = insert(mode, (Value.Hash) nested, remainingKeys, value); + final Value nested = record.get(currentKey); + final Value result; + if (nested.isHash()) { + result = insert(mode, nested.asHash(), remainingKeys, value); } - else if (nested instanceof List) { - processList(mode, value, remainingKeys, nested); + else if (nested.isArray()) { + processList(mode, value, remainingKeys, nested.asArray()); result = record.get(currentKey); } else { @@ -290,32 +286,25 @@ else if (nested instanceof List) { return result; } - @SuppressWarnings("unchecked") - private static void processList(final InsertMode mode, final String value, final String[] remainingKeys, final Object nested) { - final List nestedList = (List) nested; - final Value.Hash nestedMap; + private static void processList(final InsertMode mode, final String value, final String[] remainingKeys, final Value.Array nestedList) { + final Value nestedMap; switch (remainingKeys[0]) { case APPEND: - nestedMap = new Value.Hash(); - nestedList.add(nestedMap); - insert(mode, nestedMap, Arrays.copyOfRange(remainingKeys, 1, remainingKeys.length), value); + nestedList.add(Value.newHash(h -> insert(mode, h, Arrays.copyOfRange(remainingKeys, 1, remainingKeys.length), value))); break; case LAST: - final Object last = nestedList.get(nestedList.size() - 1); - if (last instanceof Value.Hash) { - nestedMap = (Value.Hash) last; - insert(mode, nestedMap, Arrays.copyOfRange(remainingKeys, 1, remainingKeys.length), value); + final Value last = nestedList.get(nestedList.size() - 1); + if (last.isHash()) { + insert(mode, last.asHash(), Arrays.copyOfRange(remainingKeys, 1, remainingKeys.length), value); } break; default: - nestedMap = new Value.Hash(); - nestedList.add(nestedMap); - insert(mode, nestedMap, remainingKeys, value); + nestedList.add(Value.newHash(h -> insert(mode, h, remainingKeys, value))); break; } } - static Object find(final Value.Hash record, final String[] keys) { + static Value find(final Value.Hash record, final String[] keys) { final String currentKey = keys[0]; if (!record.containsField(currentKey) || keys.length == 1) { return record.get(currentKey); @@ -324,39 +313,37 @@ static Object find(final Value.Hash record, final String[] keys) { return findNested(record, currentKey, remainingKeys); } - private static Object findNested(final Value.Hash record, final String currentKey, final String[] remainingKeys) { - final Object nested = record.get(currentKey); + private static Value findNested(final Value.Hash record, final String currentKey, final String[] remainingKeys) { + final Value nested = record.get(currentKey); + // TODO: array of maps, like in insertNested - if (nested instanceof List) { - return ((List) nested).stream().map(o -> findNested(record, currentKey, remainingKeys)) - .collect(Collectors.toList()); + if (nested.isArray()) { + return Value.newArray(a -> nested.asArray().forEach(v -> a.add(findNested(record, currentKey, remainingKeys)))); } - if (nested instanceof Value.Hash) { - @SuppressWarnings("unchecked") - final Object result = find((Value.Hash) nested, remainingKeys); - return result; + + if (nested.isHash()) { + return find(nested.asHash(), remainingKeys); } + throw new IllegalStateException(NESTED + nested); } - private static Object remove(final Value.Hash record, final String[] keys) { + private static Value remove(final Value.Hash record, final String[] keys) { final String currentKey = keys[0]; if (keys.length == 1) { record.remove(currentKey); } if (!record.containsField(currentKey)) { - return record; + return new Value(record); } final String[] remainingKeys = Arrays.copyOfRange(keys, 1, keys.length); return removeNested(record, currentKey, remainingKeys); } - private static Object removeNested(final Value.Hash record, final String currentKey, final String[] remainingKeys) { - final Object nested = record.get(currentKey); - if (nested instanceof Value.Hash) { - @SuppressWarnings("unchecked") - final Object result = remove((Value.Hash) nested, remainingKeys); - return result; + private static Value removeNested(final Value.Hash record, final String currentKey, final String[] remainingKeys) { + final Value nested = record.get(currentKey); + if (nested.isHash()) { + return remove(nested.asHash(), remainingKeys); } throw new IllegalStateException(NESTED + nested); } @@ -364,13 +351,8 @@ private static Object removeNested(final Value.Hash record, final String current private static void copy(final Record record, final List params) { final String oldName = params.get(0); final String newName = params.get(1); - final Object value = find(record, split(oldName)); - if (value != null) { - final List vs = Metafix.asList(value); - for (final Object v : vs.stream().filter(v -> v != null).collect(Collectors.toList())) { - insert(InsertMode.APPEND, record, split(newName), v.toString()); - } - } + final Value value = find(record, split(oldName)); + Metafix.asList(value, vs -> vs.forEach(v -> insert(InsertMode.APPEND, record, split(newName), v.toString()))); } static String[] split(final String s) { @@ -381,14 +363,15 @@ private enum InsertMode { REPLACE { @Override void apply(final Value.Hash record, final String key, final String value) { - record.put(key, value); + record.put(key, new Value(value)); } }, APPEND { @Override void apply(final Value.Hash record, final String key, final String value) { - final Object object = record.get(key); - record.put(key, object == null ? value : Metafix.merged(object, value)); + final Value oldValue = record.get(key); + final Value newValue = new Value(value); + record.put(key, oldValue == null ? newValue : Metafix.merged(oldValue, newValue)); } }; abstract void apply(Value.Hash record, String key, String value); diff --git a/metafix/src/main/java/org/metafacture/metafix/FixPredicate.java b/metafix/src/main/java/org/metafacture/metafix/FixPredicate.java index b63ba956..7d2795c6 100644 --- a/metafix/src/main/java/org/metafacture/metafix/FixPredicate.java +++ b/metafix/src/main/java/org/metafacture/metafix/FixPredicate.java @@ -23,52 +23,58 @@ enum FixPredicate { contain { - public Predicate of(final Object value) { - return v -> v.toString().contains(value.toString()); + @Override + public Predicate of(final String string) { + return v -> v.toString().contains(string); } }, equal { - public Predicate of(final Object value) { - return v -> v.toString().equals(value.toString()); + @Override + public Predicate of(final String string) { + return v -> v.toString().equals(string); } }, match { - public Predicate of(final Object value) { - return v -> v.toString().matches(value.toString()); + @Override + public Predicate of(final String string) { + return v -> v.toString().matches(string); } }; - abstract Predicate of(Object value); + abstract Predicate of(String string); enum Quantifier { all { @Override - public boolean test(final Record record, final FixPredicate p, final List params) { - return test(record, params.get(0), s -> s.allMatch(p.of(params.get(1)))); + protected boolean test(final Record record, final String fieldName, final Predicate p) { + return testStream(record, fieldName, s -> s.allMatch(p)); } }, any { @Override - public boolean test(final Record record, final FixPredicate p, final List params) { - return test(record, params.get(0), s -> s.anyMatch(p.of(params.get(1)))); + protected boolean test(final Record record, final String fieldName, final Predicate p) { + return testStream(record, fieldName, s -> s.anyMatch(p)); } }, none { @Override - public boolean test(final Record record, final FixPredicate p, final List params) { - final Object fieldValue = FixMethod.find(record, FixMethod.split(params.get(0))); - final String valueToTest = params.get(1); - return fieldValue == null || Metafix.asList(fieldValue).stream().noneMatch(p.of(valueToTest)); + protected boolean test(final Record record, final String fieldName, final Predicate p) { + return !any.test(record, fieldName, p); } }; - boolean test(final Record record, final String fieldName, final Predicate> f) { - final Object value = FixMethod.find(record, FixMethod.split(fieldName)); - return value != null && f.test(Metafix.asList(value).stream()); + boolean testStream(final Record record, final String fieldName, final Predicate> p) { + final Value value = FixMethod.find(record, FixMethod.split(fieldName)); + return value != null && p.test(Metafix.asList(value, null).asArray().stream()); } - abstract boolean test(Record record, FixPredicate p, List params); + public boolean test(final Record record, final FixPredicate p, final List params) { + return test(record, params.get(0), p.of(params.get(1))); + } + + protected abstract boolean test(Record record, String fieldName, Predicate p); } + } diff --git a/metafix/src/main/java/org/metafacture/metafix/Metafix.java b/metafix/src/main/java/org/metafacture/metafix/Metafix.java index 831e22a8..60438378 100644 --- a/metafix/src/main/java/org/metafacture/metafix/Metafix.java +++ b/metafix/src/main/java/org/metafacture/metafix/Metafix.java @@ -34,12 +34,12 @@ import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Deque; 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 @@ -146,30 +146,30 @@ public void endRecord() { } } - private void emit(final Object key, final Object val) { - if (val == null) { - return; - } - final List vals = asList(val); - final boolean isMulti = vals.size() > 1 || val instanceof List; - if (isMulti) { - outputStreamReceiver.startEntity(key.toString() + "[]"); - } - for (int i = 0; i < vals.size(); ++i) { - final Object value = vals.get(i); - if (value instanceof Value.Hash) { - final Value.Hash nested = (Value.Hash) value; - outputStreamReceiver.startEntity(isMulti ? "" : key.toString()); - nested.forEach(this::emit); - outputStreamReceiver.endEntity(); + private void emit(final String key, final Value val) { + asList(val, vals -> { + final boolean isMulti = vals.size() > 1 || val.isArray(); + if (isMulti) { + outputStreamReceiver.startEntity(key + "[]"); } - else { - outputStreamReceiver.literal(isMulti ? (i + 1) + "" : key.toString(), value.toString()); + + for (int i = 0; i < vals.size(); ++i) { + final Value value = vals.get(i); + + if (value.isHash()) { + outputStreamReceiver.startEntity(isMulti ? "" : key); + value.asHash().forEach(this::emit); + outputStreamReceiver.endEntity(); + } + else { + outputStreamReceiver.literal(isMulti ? (i + 1) + "" : key, value.toString()); + } } - } - if (isMulti) { - outputStreamReceiver.endEntity(); - } + + if (isMulti) { + outputStreamReceiver.endEntity(); + } + }); } @Override @@ -187,16 +187,15 @@ public void startEntity(final String name) { } private Value.Hash currentEntity(final String name, final Value.Hash previousEntity) { - final Object existingValue = previousEntity != null ? previousEntity.get(name) : null; + final Value existingValue = previousEntity != null ? previousEntity.get(name) : null; final Value.Hash currentEntity; - if (existingValue instanceof Value.Hash) { - @SuppressWarnings("unchecked") - final Value.Hash existingEntity = (Value.Hash) previousEntity.get(name); - currentEntity = existingEntity; + if (existingValue != null && existingValue.isHash()) { + currentEntity = previousEntity.get(name).asHash(); } else { - currentEntity = new Value.Hash(); - add(previousEntity != null ? previousEntity : currentRecord, name, currentEntity); + final Value value = Value.newHash(); + currentEntity = value.asHash(); + add(previousEntity != null ? previousEntity : currentRecord, name, value); } return currentEntity; } @@ -213,7 +212,7 @@ public void literal(final String name, final String value) { final Integer currentEntityIndex = entityCountStack.peek() - 1; final Value.Hash currentEntity = currentEntityIndex < 0 || entities.size() <= currentEntityIndex ? null : entities.get(currentEntityIndex); - add(currentEntity != null ? currentEntity : currentRecord, name, value); + add(currentEntity != null ? currentEntity : currentRecord, name, new Value(value)); // TODO: keep flattener as option? // flattener.literal(name, value); } @@ -253,33 +252,53 @@ public Record getCurrentRecord() { } static void addAll(final Value.Hash record, final String fieldName, final List values) { - values.forEach(value -> add(record, fieldName, value)); + values.forEach(value -> add(record, fieldName, new Value(value))); } static void addAll(final Value.Hash record, final Value.Hash values) { values.forEach((fieldName, value) -> add(record, fieldName, value)); } - static void add(final Value.Hash record, final String name, final Object newValue) { - final Object oldValue = record.get(name); + static void add(final Value.Hash record, final String name, final Value newValue) { + final Value oldValue = record.get(name); record.put(name, oldValue == null ? newValue : merged(oldValue, newValue)); } - @SuppressWarnings("unchecked") - static Object merged(final Object object1, final Object object2) { - if (object1 instanceof Value.Hash && object2 instanceof Value.Hash) { - final Value.Hash result = (Value.Hash) object1; - ((Value.Hash) object2).forEach(result::put); - return result; + static Value merged(final Value value1, final Value value2) { + if (value1.isHash() && value2.isHash()) { + final Value.Hash hash = value1.asHash(); + value2.asHash().forEach(hash::put); + return value1; + } + else { + return asList(value1, a1 -> asList(value2, a2 -> a2.forEach(a1::add))); } - final List list = asList(object1); - asList(object2).forEach(list::add); - return list; } - @SuppressWarnings("unchecked") - static List asList(final Object object) { - return new ArrayList<>(object instanceof List ? (List) object : Arrays.asList(object)); + static Value asList(final Value value, final Consumer consumer) { + final Value result; + + if (Value.isNull(value)) { + result = null; + } + else if (value.isArray()) { + if (consumer != null) { + consumer.accept(value.asArray()); + } + + result = value; + } + else { + result = Value.newArray(a -> { + a.add(value); + + if (consumer != null) { + consumer.accept(a); + } + }); + } + + return result; } } diff --git a/metafix/src/main/java/org/metafacture/metafix/RecordTransformer.java b/metafix/src/main/java/org/metafacture/metafix/RecordTransformer.java index f38f4fa2..22b3bdb5 100644 --- a/metafix/src/main/java/org/metafacture/metafix/RecordTransformer.java +++ b/metafix/src/main/java/org/metafacture/metafix/RecordTransformer.java @@ -93,9 +93,9 @@ private void processBind(final Do theDo, final EList params) { if (theDo.getName().equals("list")) { // TODO impl multiple binds via FixBind enum final Map options = options(theDo.getOptions()); final Record fullRecord = record.shallowClone(); - final Object values = FixMethod.find(record, FixMethod.split(options.get("path"))); + final Value values = FixMethod.find(record, FixMethod.split(options.get("path"))); - Metafix.asList(values).stream().filter(val -> val != null).forEach(val -> { + Metafix.asList(values, a -> a.forEach(val -> { // for each val, bind the current record/scope/context to the given var name: record = new Record(); record.put(options.get("var"), val); @@ -105,7 +105,7 @@ record = new Record(); // and remember the things we added while bound (this probably needs some tweaking): Metafix.addAll(fullRecord, record); - }); + })); record = fullRecord; } diff --git a/metafix/src/main/java/org/metafacture/metafix/Value.java b/metafix/src/main/java/org/metafacture/metafix/Value.java index f466f96f..511da671 100644 --- a/metafix/src/main/java/org/metafacture/metafix/Value.java +++ b/metafix/src/main/java/org/metafacture/metafix/Value.java @@ -16,33 +16,259 @@ package org.metafacture.metafix; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Stream; /** - * Represents a record value, i.e., either a {@link Value.Hash Hash}, - * a List, or a String. + * Represents a record value, i.e., either an {@link Value.Array Array}, + * a {@link Value.Hash Hash}, or a {@link java.lang.String String}. */ public class Value { - private Value() { + private final Value.Array array; + private final Value.Hash hash; + private final String string; + + private final Type type; + + public Value(final Value.Array array) { + type = array != null ? Type.Array : null; + + this.array = array; + this.hash = null; + this.string = null; + } + + public Value(final List array) { + this(array != null ? new Value.Array() : null); + + if (array != null) { + array.forEach(this.array::add); + } + } + + public Value(final Value.Hash hash) { + type = hash != null ? Type.Hash : null; + + this.array = null; + this.hash = hash; + this.string = null; + } + + public Value(final Map hash) { + this(hash != null ? new Value.Hash() : null); + + if (hash != null) { + hash.forEach(this.hash::put); + } + } + + public Value(final String string) { + type = string != null ? Type.String : null; + + this.array = null; + this.hash = null; + this.string = string; + } + + public static Value newArray() { + return newArray(null); + } + + public static Value newArray(final Consumer consumer) { + final Value.Array array = new Value.Array(); + + if (consumer != null) { + consumer.accept(array); + } + + return new Value(array); + } + + public static Value newHash() { + return newHash(null); + } + + public static Value newHash(final Consumer consumer) { + final Value.Hash hash = new Value.Hash(); + + if (consumer != null) { + consumer.accept(hash); + } + + return new Value(hash); + } + + public boolean isArray() { + return type == Type.Array; + } + + public boolean isHash() { + return type == Type.Hash; + } + + public boolean isString() { + return type == Type.String; + } + + public boolean isNull() { + final boolean result; + + if (type != null) { + switch (type) { + case Array: + result = array == null; + break; + case Hash: + result = hash == null; + break; + case String: + result = string == null; + break; + default: + result = true; + } + } + else { + result = true; + } + + return result; + } + + public static boolean isNull(final Value value) { + return value == null || value.isNull(); + } + + public Value.Array asArray() { + if (isArray()) { + return array; + } + else { + throw new IllegalStateException("expected array, got " + type); + } + } + + public Value.Hash asHash() { + if (isHash()) { + return hash; + } + else { + throw new IllegalStateException("expected hash, got " + type); + } + } + + public String asString() { + if (isString()) { + return string; + } + else { + throw new IllegalStateException("expected string, got " + type); + } + } + + @Override + public String toString() { + final String result; + + if (!isNull()) { + switch (type) { + case Array: + result = array.asString(); + break; + case Hash: + result = hash.asString(); + break; + case String: + result = string; + break; + default: + result = null; + } + } + else { + result = null; + } + + return result; + } + + enum Type { + Array, + Hash, + String + } + + private abstract static class AbstractValueType { + + @Override + public String toString() { + return asString(); + } + + public abstract String asString(); + } /** - * Represents a hash of metadata fields and values. + * Represents an array of metadata values. */ - public static class Hash { + public static class Array extends AbstractValueType { - private static final String EMPTY = ""; + private final List list = new ArrayList<>(); - private final Map map = new LinkedHashMap<>(); + /** + * Creates an empty instance of {@link Value.Array Array}. + */ + private Array() { + } + + public void add(final Value value) { + if (!isNull(value)) { + list.add(value); + } + } + + public int size() { + return list.size(); + } + + public Value get(final int index) { + return list.get(index); + } + + public Stream stream() { + return list.stream(); + } + + public void forEach(final Consumer consumer) { + list.forEach(consumer); + } + + @Override + public String asString() { + return list.toString(); + } + + } + + /** + * Represents a hash of metadata fields and values. + */ + public static class Hash extends AbstractValueType { + + private final Map map = new LinkedHashMap<>(); /** * Creates an empty instance of {@link Value.Hash Hash}. */ - public Hash() { + protected Hash() { } /** @@ -79,20 +305,20 @@ public int size() { * @param field the field name * @param value the metadata value */ - public void put(final String field, final Object value) { - if (value != null) { + public void put(final String field, final Value value) { + if (!isNull(value)) { map.put(field, value); } } /** - * {@link #put(String, Object) Replaces} a field/value pair in this hash, + * {@link #put(String, Value) Replaces} a field/value pair in this hash, * provided the field name is already {@link #containsField(String) present}. * * @param field the field name * @param value the metadata value */ - public void replace(final String field, final Object value) { + public void replace(final String field, final Value value) { if (containsField(field)) { put(field, value); } @@ -104,7 +330,7 @@ public void replace(final String field, final Object value) { * @param field the field name * @return the metadata value */ - public Object get(final String field) { + public Value get(final String field) { return map.get(field); } @@ -130,7 +356,13 @@ public void retainFields(final Collection fields) { * Removes all field/value pairs from this hash whose value is empty. */ public void removeEmptyValues() { - map.values().removeIf(EMPTY::equals); + // TODO: + // + // - Remove empty arrays/hashes? + // - Remove empty strings(/arrays/hashes) recursively? + // + // => Compare Catmandu behaviour + map.values().removeIf(v -> v.isString() && v.asString().isEmpty()); } /** @@ -138,12 +370,12 @@ public void removeEmptyValues() { * * @param consumer the action to be performed for each field/value pair */ - public void forEach(final BiConsumer consumer) { + public void forEach(final BiConsumer consumer) { map.forEach(consumer); } @Override - public String toString() { + public String asString() { return map.toString(); } diff --git a/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java b/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java index 9c4c9a3e..922e6d5e 100644 --- a/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java +++ b/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java @@ -27,21 +27,22 @@ public class HashValueTest { private static final String FIELD = "field"; private static final String OTHER_FIELD = "other field"; - private static final String VALUE = "value"; - private static final String OTHER_VALUE = "other value"; + + private static final Value VALUE = new Value("value"); + private static final Value OTHER_VALUE = new Value("other value"); public HashValueTest() { } @Test public void shouldNotContainMissingField() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); Assertions.assertFalse(hash.containsField(FIELD)); } @Test public void shouldContainExistingField() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); Assertions.assertTrue(hash.containsField(FIELD)); @@ -49,7 +50,7 @@ public void shouldContainExistingField() { @Test public void shouldNotContainNullValue() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, null); Assertions.assertFalse(hash.containsField(FIELD)); @@ -57,13 +58,13 @@ public void shouldNotContainNullValue() { @Test public void shouldBeEmptyByDefault() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); Assertions.assertTrue(hash.isEmpty()); } @Test public void shouldNotBeEmptyAfterAddingValue() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); Assertions.assertFalse(hash.isEmpty()); @@ -71,7 +72,7 @@ public void shouldNotBeEmptyAfterAddingValue() { @Test public void shouldNotAddNullValue() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, null); Assertions.assertTrue(hash.isEmpty()); @@ -79,13 +80,13 @@ public void shouldNotAddNullValue() { @Test public void shouldGetSizeOfDefaultMapping() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); Assertions.assertEquals(0, hash.size()); } @Test public void shouldGetSizeAfterAddingValues() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.put(OTHER_FIELD, OTHER_VALUE); @@ -94,13 +95,13 @@ public void shouldGetSizeAfterAddingValues() { @Test public void shouldNotGetMissingField() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); Assertions.assertNull(hash.get(FIELD)); } @Test public void shouldGetExistingField() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); Assertions.assertEquals(VALUE, hash.get(FIELD)); @@ -108,7 +109,7 @@ public void shouldGetExistingField() { @Test public void shouldNotReplaceMissingField() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.replace(FIELD, VALUE); Assertions.assertNull(hash.get(FIELD)); @@ -117,7 +118,7 @@ public void shouldNotReplaceMissingField() { @Test public void shouldReplaceExistingField() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.replace(FIELD, OTHER_VALUE); @@ -126,7 +127,7 @@ public void shouldReplaceExistingField() { @Test public void shouldNotReplaceExistingFieldWithNullValue() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.replace(FIELD, null); @@ -135,7 +136,7 @@ public void shouldNotReplaceExistingFieldWithNullValue() { @Test public void shouldRemoveMissingField() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.remove(FIELD); Assertions.assertNull(hash.get(FIELD)); @@ -144,7 +145,7 @@ public void shouldRemoveMissingField() { @Test public void shouldRemoveExistingField() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.remove(FIELD); @@ -154,7 +155,7 @@ public void shouldRemoveExistingField() { @Test public void shouldRetainFields() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.put(OTHER_FIELD, OTHER_VALUE); @@ -166,7 +167,7 @@ public void shouldRetainFields() { @Test public void shouldRetainNoFields() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.retainFields(Arrays.asList()); @@ -176,7 +177,7 @@ public void shouldRetainNoFields() { @Test public void shouldNotRetainMissingFields() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.retainFields(Arrays.asList(FIELD, OTHER_FIELD)); @@ -187,9 +188,9 @@ public void shouldNotRetainMissingFields() { @Test public void shouldRemoveEmptyValues() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); - hash.put(OTHER_FIELD, ""); + hash.put(OTHER_FIELD, new Value("")); hash.removeEmptyValues(); @@ -199,21 +200,25 @@ public void shouldRemoveEmptyValues() { @Test public void shouldIterateOverFieldValuePairs() { - final Value.Hash hash = new Value.Hash(); + final Value.Hash hash = newHash(); hash.put(FIELD, VALUE); hash.put(OTHER_FIELD, OTHER_VALUE); - hash.put("empty field", ""); - hash.put("_special field", 1); + hash.put("empty field", new Value("")); + hash.put("_special field", new Value("1")); final List fields = new ArrayList<>(); - final List values = new ArrayList<>(); + final List values = new ArrayList<>(); hash.forEach((k, v) -> { fields.add(k); - values.add(v); + values.add(v.asString()); }); Assertions.assertEquals(Arrays.asList(FIELD, OTHER_FIELD, "empty field", "_special field"), fields); - Assertions.assertEquals(Arrays.asList(VALUE, OTHER_VALUE, "", 1), values); + Assertions.assertEquals(Arrays.asList(VALUE.asString(), OTHER_VALUE.asString(), "", "1"), values); + } + + private Value.Hash newHash() { + return Value.newHash().asHash(); } } diff --git a/metafix/src/test/java/org/metafacture/metafix/RecordTest.java b/metafix/src/test/java/org/metafacture/metafix/RecordTest.java index 34990044..fdc0381c 100644 --- a/metafix/src/test/java/org/metafacture/metafix/RecordTest.java +++ b/metafix/src/test/java/org/metafacture/metafix/RecordTest.java @@ -19,14 +19,12 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.List; - public class RecordTest { private static final String FIELD = "field"; - private static final String VALUE = "value"; - private static final String OTHER_VALUE = "other value"; + + private static final Value VALUE = new Value("value"); + private static final Value OTHER_VALUE = new Value("other value"); public RecordTest() { } @@ -78,23 +76,16 @@ public void shouldNotModifyOverwrittenValueFromShallowClone() { @Test public void shouldModifySubLevelFromShallowClone() { final Record record = new Record(); - - final List list1 = new ArrayList<>(); - list1.add(VALUE); - record.put(FIELD, list1); + record.put(FIELD, Value.newArray(a -> a.add(VALUE))); final Record clone = record.shallowClone(); - - @SuppressWarnings("unchecked") - final List list2 = (List) clone.get(FIELD); - list2.add(OTHER_VALUE); + clone.get(FIELD).asArray().add(OTHER_VALUE); Assertions.assertNotSame(record, clone); Assertions.assertSame(record.get(FIELD), clone.get(FIELD)); - @SuppressWarnings("unchecked") - final List list3 = (List) clone.get(FIELD); - Assertions.assertEquals(OTHER_VALUE, list3.get(1)); + final Value.Array array = clone.get(FIELD).asArray(); + Assertions.assertEquals(OTHER_VALUE, array.get(1)); } @Test