diff --git a/build.gradle b/build.gradle index deb8000c..4e39e948 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,6 @@ editorconfig { check.dependsOn(editorconfigCheck) subprojects { - ext { versions = [ 'ace': '1.3.3', @@ -48,6 +47,8 @@ subprojects { apply from: "${rootDir}/gradle/source-layout.gradle" + check.dependsOn(javadoc) + sourceCompatibility = '1.8' targetCompatibility = '1.8' @@ -76,6 +77,17 @@ subprojects { options.compilerArgs << '-Werror' } + javadoc { + // Can't exclude generated files based on source path + // (cf. https://stackoverflow.com/a/47711311). + //exclude '**/xtext-gen' + source = source.filter(f -> !f.getPath().contains('xtext-gen')) + + options { + addBooleanOption 'Xwerror', true + } + } + publishing { publications { mavenArtifacts(MavenPublication) { diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 7d7b827f..d2677cc1 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -69,6 +69,7 @@ + @@ -92,14 +93,15 @@ - + + @@ -151,8 +153,8 @@ - + diff --git a/gradle/source-layout.gradle b/gradle/source-layout.gradle index 2103fff4..a5d55599 100644 --- a/gradle/source-layout.gradle +++ b/gradle/source-layout.gradle @@ -1,11 +1,11 @@ sourceSets { main { - java.srcDirs = ['src/main/java', 'src/main/xtext-gen'] - resources.srcDirs = ['src/main/resources', 'src/main/xtext-gen'] + java.srcDir 'src/main/xtext-gen' + resources.srcDir 'src/main/xtext-gen' } test { - java.srcDirs = ['src/test/java', 'src/test/xtext-gen'] - resources.srcDirs = ['src/test/resources', 'src/test/xtext-gen'] + java.srcDir 'src/test/xtext-gen' + resources.srcDir 'src/test/xtext-gen' } } 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 6b8f1ff4..c4415c6b 100644 --- a/metafix/src/main/java/org/metafacture/metafix/FixMethod.java +++ b/metafix/src/main/java/org/metafacture/metafix/FixMethod.java @@ -18,14 +18,9 @@ import org.metafacture.metamorph.maps.FileMap; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -35,110 +30,107 @@ enum FixMethod { // RECORD-LEVEL METHODS: set_field { - public void apply(final Map record, final List params, - final Map options) { - record.remove(params.get(0)); - insert(InsertMode.REPLACE, record, split(params.get(0)), params.get(1)); + public void apply(final Record record, final List params, final Map options) { + final String field = params.get(0); + + record.remove(field); + record.replace(field, params.get(1)); } }, set_array { - public void apply(final Map record, final List params, - final Map options) { - final String key = params.get(0); + public void apply(final Record record, final List params, final Map options) { + final String field = params.get(0); final List toAdd = params.subList(1, params.size()); - if (key.endsWith(DOT_APPEND)) { - Metafix.addAll(record, key.replace(DOT_APPEND, EMPTY), toAdd); + if (field.endsWith(DOT_APPEND)) { + record.addAll(field.replace(DOT_APPEND, EMPTY), toAdd); } else { - record.put(key, toAdd); + record.put(field, Value.newArray(a -> toAdd.forEach(s -> a.add(new Value(s))))); } } }, set_hash { - @SuppressWarnings("unchecked") - public void apply(final Map record, final List params, - final Map options) { - final String key = params.get(0); - final Object val = record.get(key.replace(DOT_APPEND, EMPTY)); - if (key.endsWith(DOT_APPEND) && val instanceof List) { - ((List) val).add(options); + public void apply(final Record record, final List params, final Map options) { + final String field = params.get(0); + + final Value value = record.get(field.replace(DOT_APPEND, EMPTY)); + final Value newValue = Value.newHash(h -> options.forEach((f, v) -> h.put(f, new Value(v)))); + + if (field.endsWith(DOT_APPEND) && value.isArray()) { + value.asArray().add(newValue); } else { - record.put(key, options); + record.put(field, newValue); } } }, array { // array-from-hash - public void apply(final Map record, final List params, - final Map options) { - final String fieldName = params.get(0); - Metafix.asList(record.get(fieldName)).forEach(recordEntry -> { - if (recordEntry instanceof Map) { - record.remove(fieldName); - ((Map) recordEntry).entrySet().forEach(mapEntry -> { - Metafix.add(record, fieldName, mapEntry.getKey()); - Metafix.add(record, fieldName, mapEntry.getValue()); + public void apply(final Record record, final List params, final Map options) { + final String field = params.get(0); + + record.getList(field, a -> a.forEach(recordEntry -> { + if (recordEntry.isHash()) { + record.remove(field); + + recordEntry.asHash().forEach((subField, value) -> { + record.add(field, new Value(subField)); + record.add(field, value); }); } - }); + })); } }, hash { // hash-from-array - public void apply(final Map record, final List params, - final Map options) { - final List values = Metafix.asList(record.get(params.get(0))); - final Map result = new HashMap<>(); - for (int i = 0; i < values.size(); i = i + 1) { - if (i % 2 == 1) { - result.put(values.get(i - 1).toString(), values.get(i)); + public void apply(final Record record, final List params, final Map options) { + final String field = params.get(0); + + record.getList(field, a -> record.put(field, Value.newHash(h -> { + for (int i = 1; i < a.size(); i = i + 2) { + h.put(a.get(i - 1).toString(), a.get(i)); } - } - record.put(params.get(0), result); + }))); } }, add_field { - public void apply(final Map record, final List params, - final Map options) { - insert(InsertMode.APPEND, record, split(params.get(0)), params.get(1)); + public void apply(final Record record, final List params, final Map options) { + record.append(params.get(0), params.get(1)); } }, move_field { - public void apply(final Map record, final List params, - final Map options) { - copy(record, params); - remove(record, split(params.get(0))); + public void apply(final Record record, final List params, final Map options) { + record.copy(params); + record.removeNested(params.get(0)); } }, copy_field { - public void apply(final Map record, final List params, - final Map options) { - copy(record, params); + public void apply(final Record record, final List params, final Map options) { + record.copy(params); } }, remove_field { - public void apply(final Map record, final List params, - final Map options) { - params.forEach(p -> { - remove(record, split(p)); - }); + public void apply(final Record record, final List params, final Map options) { + params.forEach(record::removeNested); } }, format { - public void apply(final Map record, final List 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)); + public void apply(final Record record, final List params, final Map options) { + final String field = params.get(0); + + record.getList(field, oldValues -> { + final String newValue = String.format(params.get(1), oldValues.stream().toArray()); + record.replace(field, new Value(Arrays.asList(new Value(newValue)))); + }); } }, parse_text { - public void apply(final Map record, final List params, - final Map options) { - Metafix.asList(record.get(params.get(0))).forEach(v -> { + public void apply(final Record record, final List params, final Map options) { + final String field = params.get(0); + + record.getList(field, a -> a.forEach(v -> { final Pattern p = Pattern.compile(params.get(1)); final Matcher m = p.matcher(v.toString()); if (m.matches()) { - record.remove(params.get(0)); + record.remove(field); /** * {@code Pattern.namedGroups()} not available as API, @@ -148,34 +140,32 @@ public void apply(final Map record, final List params, * 2. Named groups are not mixed with unnamed groups. */ final Matcher groupMatcher = NAMED_GROUP_PATTERN.matcher(p.pattern()); - final Map result = new LinkedHashMap<>(); - - while (groupMatcher.find()) { - final String group = groupMatcher.group(1); - result.put(group, m.group(group)); - } + final Value value = Value.newHash(h -> { + 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()) { + record.add(field, value); } else { for (int i = 1; i <= m.groupCount(); i = i + 1) { - Metafix.add(record, params.get(0), m.group(i)); + record.add(field, new Value(m.group(i))); } } } - }); + })); } }, paste { - public void apply(final Map record, final List params, - final Map options) { + public void apply(final Record record, final List params, final Map options) { final String joinChar = options.get("join_char"); - insert(InsertMode.REPLACE, record, split(params.get(0)), - params.subList(1, params.size()).stream() - .filter(k -> 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 : " "))); + record.replace(params.get(0), params.subList(1, params.size()).stream() + .filter(f -> literalString(f) || record.find(f) != null) + .map(f -> literalString(f) ? new Value(f.substring(1)) : record.findList(f, null).asArray().get(0)) + .map(Value::toString).collect(Collectors.joining(joinChar != null ? joinChar : " "))); } private boolean literalString(final String s) { @@ -183,61 +173,51 @@ private boolean literalString(final String s) { } }, reject { - public void apply(final Map record, final List params, - final Map options) { - record.put("__reject", null); + public void apply(final Record record, final List params, final Map options) { + record.setReject(true); } }, retain { - public void apply(final Map record, final List params, - final Map options) { - record.keySet().retainAll(params); + public void apply(final Record record, final List params, final Map options) { + record.retainFields(params); } }, vacuum { - public void apply(final Map record, final List params, - final Map options) { - record.values().removeIf(EMPTY::equals); + public void apply(final Record record, final List params, final Map options) { + record.removeEmptyValues(); } }, // FIELD-LEVEL METHODS: substring { @SuppressWarnings("checkstyle:MagicNumber") // TODO: switch to morph-style named params in general? - public void apply(final Map record, final List params, - final Map options) { - applyToFields(record, params, - s -> s.substring(Integer.parseInt(params.get(1)), Integer.parseInt(params.get(2)) - 1)); + public void apply(final Record record, final List params, final Map options) { + record.transformFields(params, s -> s.substring(Integer.parseInt(params.get(1)), Integer.parseInt(params.get(2)) - 1)); } }, trim { - public void apply(final Map record, final List params, - final Map options) { - applyToFields(record, params, s -> s.trim()); + public void apply(final Record record, final List params, final Map options) { + record.transformFields(params, String::trim); } }, upcase { - public void apply(final Map record, final List params, - final Map options) { - applyToFields(record, params, s -> s.toUpperCase()); + public void apply(final Record record, final List params, final Map options) { + record.transformFields(params, String::toUpperCase); } }, downcase { - public void apply(final Map record, final List params, - final Map options) { - applyToFields(record, params, s -> s.toLowerCase()); + public void apply(final Record record, final List params, final Map options) { + record.transformFields(params, String::toLowerCase); } }, capitalize { - public void apply(final Map record, final List params, - final Map options) { - applyToFields(record, params, s -> s.substring(0, 1).toUpperCase() + s.substring(1)); + public void apply(final Record record, final List params, final Map options) { + record.transformFields(params, s -> s.substring(0, 1).toUpperCase() + s.substring(1)); } }, lookup { - public void apply(final Map record, final List params, - final Map options) { - applyToFields(record, params, s -> { + public void apply(final Record record, final List params, final Map options) { + record.transformFields(params, s -> { final Map map = buildMap(options, params.size() <= 1 ? null : params.get(1)); return map.getOrDefault(s, map.get("__default")); // TODO Catmandu uses 'default' }); @@ -262,171 +242,9 @@ private Map fileMap(final String location, final String separato private static final Pattern NAMED_GROUP_PATTERN = Pattern.compile("\\(\\?<(.+?)>"); - private static final String NESTED = "Nested non-map / non-list: "; private static final String EMPTY = ""; - private static final String APPEND = "$append"; - private static final String DOT_APPEND = "." + APPEND; - private static final String LAST = "$last"; - - private static void applyToFields(final Map record, final List params, - final Function fun) { - final String key = params.get(0); - final Object found = find(record, split(key)); - final boolean containsKey = found != null; - if (containsKey) { - 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); - } - }); - } - } - - private static Object insert(final InsertMode mode, final Map map, final String[] keys, final String value) { - final String currentKey = keys[0]; - if (keys.length == 1) { - mode.apply(map, currentKey, value); - return map; - } - final String[] remainingKeys = Arrays.copyOfRange(keys, 1, keys.length); - final Object nested = insertNested(mode, map, value, currentKey, remainingKeys); - map.put(currentKey, nested); - return map; - } - - @SuppressWarnings("unchecked") - private static Object insertNested(final InsertMode mode, final Map map, final String value, final String currentKey, - final String[] remainingKeys) { - if (!map.containsKey(currentKey)) { - map.put(currentKey, new LinkedHashMap()); - } - final Object nested = map.get(currentKey); - final Object result; - if (nested instanceof Map) { - result = insert(mode, (Map) nested, remainingKeys, value); - } - else if (nested instanceof List) { - processList(mode, value, remainingKeys, nested); - result = map.get(currentKey); - } - else { - throw new IllegalStateException(NESTED + nested); - } - 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 Map nestedMap; - switch (remainingKeys[0]) { - case APPEND: - nestedMap = new LinkedHashMap<>(); - nestedList.add(nestedMap); - insert(mode, nestedMap, Arrays.copyOfRange(remainingKeys, 1, remainingKeys.length), value); - break; - case LAST: - final Object last = nestedList.get(nestedList.size() - 1); - if (last instanceof Map) { - nestedMap = (Map) last; - insert(mode, nestedMap, Arrays.copyOfRange(remainingKeys, 1, remainingKeys.length), value); - } - break; - default: - nestedMap = new LinkedHashMap<>(); - nestedList.add(nestedMap); - insert(mode, nestedMap, remainingKeys, value); - break; - } - } - - @SuppressWarnings("checkstyle:ReturnCount") - static Object find(final Map map, final String[] keys) { - final String currentKey = keys[0]; - if (!map.containsKey(currentKey)) { - return null; - } - if (keys.length == 1) { - return map.get(currentKey); - } - final String[] remainingKeys = Arrays.copyOfRange(keys, 1, keys.length); - return findNested(map, currentKey, remainingKeys); - } - - private static Object findNested(final Map map, final String currentKey, - final String[] remainingKeys) { - final Object nested = map.get(currentKey); - // TODO: array of maps, like in insertNested - if (nested instanceof List) { - return ((List) nested).stream().map(o -> findNested(map, currentKey, remainingKeys)) - .collect(Collectors.toList()); - } - if (nested instanceof Map) { - @SuppressWarnings("unchecked") - final Object result = find((Map) nested, remainingKeys); - return result; - } - throw new IllegalStateException(NESTED + nested); - } + private static final String DOT_APPEND = "." + Value.Hash.APPEND_FIELD; - private static Object remove(final Map map, final String[] keys) { - final String currentKey = keys[0]; - if (keys.length == 1) { - map.remove(currentKey); - } - if (!map.containsKey(currentKey)) { - return map; - } - final String[] remainingKeys = Arrays.copyOfRange(keys, 1, keys.length); - return removeNested(map, currentKey, remainingKeys); - } - - private static Object removeNested(final Map map, final String currentKey, - final String[] remainingKeys) { - final Object nested = map.get(currentKey); - if (!(nested instanceof Map)) { - throw new IllegalStateException(NESTED + nested); - } - @SuppressWarnings("unchecked") - final Object result = remove((Map) nested, remainingKeys); - return result; - } - - private static void copy(final Map 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()); - } - } - } - - static String[] split(final String s) { - return s.split("\\."); - } - - private enum InsertMode { - REPLACE { - @Override - void apply(final Map map, final String key, final String value) { - map.put(key, value); - } - }, - APPEND { - @Override - void apply(final Map map, final String key, final String value) { - final Object object = map.get(key); - map.put(key, object == null ? value : Metafix.merged(object, value)); - } - }; - abstract void apply(Map map, String key, String value); - } + abstract void apply(Record record, List params, Map options); - abstract void apply(Map record, List params, Map options); } diff --git a/metafix/src/main/java/org/metafacture/metafix/FixPredicate.java b/metafix/src/main/java/org/metafacture/metafix/FixPredicate.java index 6d880836..866eafc2 100644 --- a/metafix/src/main/java/org/metafacture/metafix/FixPredicate.java +++ b/metafix/src/main/java/org/metafacture/metafix/FixPredicate.java @@ -17,62 +17,64 @@ package org.metafacture.metafix; import java.util.List; -import java.util.Map; import java.util.function.Predicate; import java.util.stream.Stream; 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 Map 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 Map 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 Map 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 Map 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 = record.find(fieldName); + return value != null && p.test(value.asList(null).asArray().stream()); } - abstract boolean test(Map 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 af50372c..1e03be87 100644 --- a/metafix/src/main/java/org/metafacture/metafix/Metafix.java +++ b/metafix/src/main/java/org/metafacture/metafix/Metafix.java @@ -34,10 +34,8 @@ 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.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -60,7 +58,7 @@ public class Metafix implements StreamPipe { private static final Logger LOG = LoggerFactory.getLogger(Metafix.class); // TODO: Use SimpleRegexTrie / WildcardTrie for wildcard, alternation and character class support - private Map currentRecord = new LinkedHashMap<>(); + private Record currentRecord = new Record(); private Fix fix; private final List expressions = new ArrayList<>(); private Map vars = NO_VARS; @@ -69,7 +67,7 @@ public class Metafix implements StreamPipe { private int entityCount; private StreamReceiver outputStreamReceiver; private String recordIdentifier; - private List> entities = new ArrayList<>(); + private List entities = new ArrayList<>(); public Metafix() { init(); @@ -114,7 +112,7 @@ private void buildPipeline(final Reader fixDef, final Map theVar @Override public void startRecord(final String identifier) { - currentRecord = new LinkedHashMap<>(); + currentRecord = new Record(); LOG.debug("Start record: {}", identifier); flattener.startRecord(identifier); entityCountStack.clear(); @@ -135,42 +133,42 @@ public void endRecord() { LOG.debug("End record, walking fix: {}", currentRecord); final RecordTransformer transformer = new RecordTransformer(currentRecord, vars, fix); currentRecord = transformer.transform(); - if (!currentRecord.containsKey("__reject")) { + if (!currentRecord.getReject()) { outputStreamReceiver.startRecord(recordIdentifier); LOG.debug("Sending results to {}", outputStreamReceiver); - currentRecord.keySet().stream().filter(k -> !k.startsWith("_")).forEach(k -> { - emit(k, currentRecord.get(k)); + currentRecord.forEach((f, v) -> { + if (!f.startsWith("_")) { + emit(f, v); + } }); outputStreamReceiver.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 Map) { - final Map nested = (Map) value; - outputStreamReceiver.startEntity(isMulti ? "" : key.toString()); - nested.entrySet().forEach(nestedEntry -> { - emit(nestedEntry.getKey(), nestedEntry.getValue()); - }); - outputStreamReceiver.endEntity(); + private void emit(final String field, final Value value) { + Value.asList(value, array -> { + final boolean isMulti = array.size() > 1 || value.isArray(); + if (isMulti) { + outputStreamReceiver.startEntity(field + "[]"); } - else { - outputStreamReceiver.literal(isMulti ? (i + 1) + "" : key.toString(), value.toString()); + + for (int i = 0; i < array.size(); ++i) { + final Value arrayValue = array.get(i); + + if (arrayValue.isHash()) { + outputStreamReceiver.startEntity(isMulti ? "" : field); + arrayValue.asHash().forEach(this::emit); + outputStreamReceiver.endEntity(); + } + else { + outputStreamReceiver.literal(isMulti ? (i + 1) + "" : field, arrayValue.toString()); + } } - } - if (isMulti) { - outputStreamReceiver.endEntity(); - } + + if (isMulti) { + outputStreamReceiver.endEntity(); + } + }); } @Override @@ -180,24 +178,23 @@ public void startEntity(final String name) { } ++entityCount; final Integer currentEntityIndex = entityCountStack.peek() - 1; - final Map previousEntity = currentEntityIndex < 0 || + final Value.Hash previousEntity = currentEntityIndex < 0 || entities.size() <= currentEntityIndex ? null : entities.get(currentEntityIndex); entityCountStack.push(Integer.valueOf(entityCount)); flattener.startEntity(name); - entities.add(currentEntity(name, previousEntity == null && entities.size() >= 0 ? currentRecord : previousEntity)); + entities.add(currentEntity(name, previousEntity != null ? previousEntity : currentRecord)); } - private Map currentEntity(final String name, final Map previousEntity) { - final Object existingValue = previousEntity != null ? previousEntity.get(name) : null; - final Map currentEntity; - if (existingValue != null && existingValue instanceof Map) { - @SuppressWarnings("unchecked") - final Map existingEntity = (Map) previousEntity.get(name); - currentEntity = existingEntity; + private Value.Hash currentEntity(final String name, final Value.Hash previousEntity) { + final Value existingValue = previousEntity != null ? previousEntity.get(name) : null; + final Value.Hash currentEntity; + if (existingValue != null && existingValue.isHash()) { + currentEntity = previousEntity.get(name).asHash(); } else { - currentEntity = new LinkedHashMap<>(); - add(previousEntity != null ? previousEntity : currentRecord, name, currentEntity); + final Value value = Value.newHash(); + currentEntity = value.asHash(); + (previousEntity != null ? previousEntity : currentRecord).add(name, value); } return currentEntity; } @@ -212,9 +209,9 @@ public void endEntity() { public void literal(final String name, final String value) { LOG.debug("Putting '{}': '{}'", name, value); final Integer currentEntityIndex = entityCountStack.peek() - 1; - final Map currentEntity = currentEntityIndex < 0 || + final Value.Hash currentEntity = currentEntityIndex < 0 || entities.size() <= currentEntityIndex ? null : entities.get(currentEntityIndex); - add(currentEntity != null ? currentEntity : currentRecord, name, value); + (currentEntity != null ? currentEntity : currentRecord).add(name, new Value(value)); // TODO: keep flattener as option? // flattener.literal(name, value); } @@ -249,44 +246,8 @@ public Map getVars() { return vars; } - public Map getCurrentRecord() { + public Record getCurrentRecord() { return currentRecord; } - static void addAll(final Map record, final String fieldName, final List values) { - values.forEach(value -> { - add(record, fieldName, value); - }); - } - - static void addAll(final Map record, final Map values) { - values.entrySet().forEach(value -> { - add(record, value.getKey(), value.getValue()); - }); - } - - static void add(final Map record, final String name, final Object newValue) { - final Object 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 Map && object2 instanceof Map) { - ((Map) object1).putAll((Map) object2); - return object1; - } - final List list = asList(object1); - asList(object2).forEach(e -> { - list.add(e); - }); - return list; - } - - @SuppressWarnings("unchecked") - static List asList(final Object object) { - return new ArrayList<>( - object instanceof List ? (List) object : Arrays.asList(object)); - } - } diff --git a/metafix/src/main/java/org/metafacture/metafix/Record.java b/metafix/src/main/java/org/metafacture/metafix/Record.java new file mode 100644 index 00000000..3f2602e5 --- /dev/null +++ b/metafix/src/main/java/org/metafacture/metafix/Record.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 hbz NRW + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.metafacture.metafix; + +/** + * Represents a metadata record, i.e., a {@link Value.Hash Hash} of fields + * and values. + */ +public class Record extends Value.Hash { + + private boolean reject; + + /** + * Creates an empty instance of {@link Record}. + */ + public Record() { + } + + /** + * Returns a shallow clone of this record. + * + * @return a new record pre-populated with all entries from this record + */ + public Record shallowClone() { + final Record clone = new Record(); + + clone.setReject(reject); + forEach(clone::put); + + return clone; + } + + /** + * Flags whether this record should be rejected. + * + * @param reject true if this record should not be emitted, false otherwise + */ + public void setReject(final boolean reject) { + this.reject = reject; + } + + /** + * Checks whether this record should be rejected. + * + * @return true if this record should not be emitted, false otherwise + */ + public boolean getReject() { + return reject; + } + + @Override + public String toString() { + // TODO: Improve string representation? Include reject status, etc.? + return super.toString(); + } + +} diff --git a/metafix/src/main/java/org/metafacture/metafix/RecordTransformer.java b/metafix/src/main/java/org/metafacture/metafix/RecordTransformer.java index 500423ee..2ced5c0a 100644 --- a/metafix/src/main/java/org/metafacture/metafix/RecordTransformer.java +++ b/metafix/src/main/java/org/metafacture/metafix/RecordTransformer.java @@ -29,13 +29,11 @@ import org.metafacture.metafix.fix.Options; import org.metafacture.metafix.fix.Unless; -import com.google.common.collect.ImmutableMap; import org.eclipse.emf.common.util.EList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -51,21 +49,21 @@ class RecordTransformer { private static final Logger LOG = LoggerFactory.getLogger(RecordTransformer.class); private Fix fix; - private Map record; + private Record record; private Map vars; - RecordTransformer(final Map record, final Map vars, final Fix fix) { - this.record = new LinkedHashMap<>(record); + RecordTransformer(final Record record, final Map vars, final Fix fix) { + this.record = record.shallowClone(); this.vars = vars; this.fix = fix; } - Map transform() { + Record transform() { processSubexpressions(fix.getElements()); return record; } - Map getRecord() { + Record getRecord() { return record; } @@ -94,16 +92,20 @@ else if (sub instanceof Unless) { 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 Map fullRecord = new LinkedHashMap<>(record); - final Object values = FixMethod.find(record, FixMethod.split(options.get("path"))); - Metafix.asList(values).stream().filter(val -> val != null).forEach(val -> { - // for each val, bind the current record/scope/context to the given var name: - record = new LinkedHashMap<>(ImmutableMap.of(options.get("var"), val)); + final Record fullRecord = record.shallowClone(); + + record.findList(options.get("path"), a -> a.forEach(value -> { + // for each value, bind the current record/scope/context to the given var name: + record = new Record(); + record.put(options.get("var"), value); + processSubexpressions(theDo.getElements()); record.remove(options.get("var")); + // and remember the things we added while bound (this probably needs some tweaking): - Metafix.addAll(fullRecord, record); - }); + fullRecord.addAll(record); + })); + record = fullRecord; } else { @@ -139,7 +141,7 @@ private boolean testConditional(final String conditional, final EList pa LOG.debug(": {} parameters: {}", conditional, params); boolean result = false; if ("exists".equals(conditional)) { - return record.containsKey(params.get(0)); + return record.containsField(params.get(0)); } if (!conditional.contains("_")) { throw new IllegalArgumentException("Missing quantifier prefix (all_, any_, none_) for " + conditional); diff --git a/metafix/src/main/java/org/metafacture/metafix/Value.java b/metafix/src/main/java/org/metafacture/metafix/Value.java new file mode 100644 index 00000000..f31f4259 --- /dev/null +++ b/metafix/src/main/java/org/metafacture/metafix/Value.java @@ -0,0 +1,584 @@ +/* + * Copyright 2021 hbz NRW + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.metafacture.metafix; + +import java.util.ArrayList; +import java.util.Arrays; +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.function.UnaryOperator; +import java.util.stream.Stream; + +/** + * Represents a record value, i.e., either an {@link Array}, a {@link Hash}, + * or a {@link java.lang.String String}. + */ +public class Value { + + private final Array array; + private final Hash hash; + private final String string; + + private final Type type; + + public Value(final 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 Array() : null); + + if (array != null) { + array.forEach(this.array::add); + } + } + + public Value(final 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 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 Array array = new 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 Hash hash = new 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 Array asArray() { + if (isArray()) { + return array; + } + else { + throw new IllegalStateException("expected array, got " + type); + } + } + + public 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); + } + } + + public static Value asList(final Value value, final Consumer consumer) { + return isNull(value) ? null : value.asList(consumer); + } + + public Value asList(final Consumer consumer) { + if (isArray()) { + if (consumer != null) { + consumer.accept(asArray()); + } + + return this; + } + else { + return newArray(a -> { + a.add(this); + + if (consumer != null) { + consumer.accept(a); + } + }); + } + } + + public Value merge(final Value value) { + if (isHash() && value.isHash()) { + final Hash asHash = asHash(); + value.asHash().forEach(asHash::put); + return this; + } + else { + return asList(a1 -> value.asList(a2 -> a2.forEach(a1::add))); + } + } + + @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 an array of metadata values. + */ + public static class Array extends AbstractValueType { + + private final List list = new ArrayList<>(); + + /** + * Creates an empty instance of {@link 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 { + + /*package-private*/ static final String APPEND_FIELD = "$append"; + private static final String LAST_FIELD = "$last"; + + private static final String FIELD_PATH_SEPARATOR = "\\."; + + private final Map map = new LinkedHashMap<>(); + + /** + * Creates an empty instance of {@link Hash}. + */ + protected Hash() { + } + + /** + * Checks whether this hash contains the metadata field. + * + * @param field the field name + * @return true if this hash contains the metadata field, false otherwise + */ + public boolean containsField(final String field) { + return map.containsKey(field); + } + + /** + * Checks whether this hash is empty. + * + * @return true if this hash is empty, false otherwise + */ + public boolean isEmpty() { + return map.isEmpty(); + } + + /** + * Gets the number of field/value pairs in this hash. + * + * @return the number of field/value pairs in this hash + */ + public int size() { + return map.size(); + } + + /** + * Adds a field/value pair to this hash, provided it's not {@code null}. + * + * @param field the field name + * @param value the metadata value + */ + public void put(final String field, final Value value) { + if (!isNull(value)) { + map.put(field, value); + } + } + + /** + * {@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 Value value) { + if (containsField(field)) { + put(field, value); + } + } + + public Value replace(final String fieldPath, final String newValue) { + return insert(InsertMode.REPLACE, fieldPath, newValue); + } + + public Value append(final String fieldPath, final String newValue) { + return insert(InsertMode.APPEND, fieldPath, newValue); + } + + /** + * Retrieves the field value from this hash. + * + * @param field the field name + * @return the metadata value + */ + public Value get(final String field) { + return map.get(field); + } + + public Value find(final String fieldPath) { + return find(split(fieldPath)); + } + + private Value find(final String[] fields) { + final String field = fields[0]; + + return fields.length == 1 || !containsField(field) ? get(field) : + findNested(field, Arrays.copyOfRange(fields, 1, fields.length)); + } + + private Value findNested(final String field, final String[] remainingFields) { + final Value value = get(field); + + // TODO: array of maps, like in insert nested + + if (value.isHash()) { + return value.asHash().find(remainingFields); + } + + throw new IllegalStateException("expected hash, got " + value.type); + } + + public Value findList(final String fieldPath, final Consumer consumer) { + return asList(find(fieldPath), consumer); + } + + public Value getList(final String field, final Consumer consumer) { + return asList(get(field), consumer); + } + + private String[] split(final String fieldPath) { + return fieldPath.split(FIELD_PATH_SEPARATOR); + } + + public void addAll(final String field, final List values) { + values.forEach(value -> add(field, new Value(value))); + } + + public void addAll(final Hash hash) { + hash.forEach(this::add); + } + + public void add(final String field, final Value newValue) { + final Value oldValue = get(field); + put(field, oldValue == null ? newValue : oldValue.merge(newValue)); + } + + public Value insert(final InsertMode mode, final String fieldPath, final String newValue) { + return insert(mode, split(fieldPath), newValue); + } + + private Value insert(final InsertMode mode, final String[] fields, final String newValue) { + final String field = fields[0]; + + if (fields.length == 1) { + mode.apply(this, field, newValue); + } + else { + if (!containsField(field)) { + put(field, newHash()); + } + + final String[] remainingFields = Arrays.copyOfRange(fields, 1, fields.length); + final String[] nestedFields = Arrays.copyOfRange(remainingFields, 1, remainingFields.length); + final Value value = get(field); + + if (value.isHash()) { + value.asHash().insert(mode, remainingFields, newValue); + } + else if (value.isArray()) { + final Array array = value.asArray(); + + switch (remainingFields[0]) { + case APPEND_FIELD: + array.add(newHash(h -> h.insert(mode, nestedFields, newValue))); + break; + case LAST_FIELD: + final Value last = array.get(array.size() - 1); + if (last.isHash()) { + last.asHash().insert(mode, nestedFields, newValue); + } + break; + default: + array.add(newHash(h -> h.insert(mode, remainingFields, newValue))); + break; + } + } + else { + throw new IllegalStateException("expected array or hash, got " + value.type); + } + } + + return new Value(this); + } + + /** + * Removes the given field/value pair from this hash. + * + * @param field the field name + */ + public void remove(final String field) { + map.remove(field); + } + + public void removeNested(final String fieldPath) { + removeNested(split(fieldPath)); + } + + private void removeNested(final String[] fields) { + final String field = fields[0]; + + if (fields.length == 1) { + remove(field); + } + else if (containsField(field)) { + get(field).asHash().removeNested(Arrays.copyOfRange(fields, 1, fields.length)); + } + } + + public void copy(final List params) { + final String oldName = params.get(0); + final String newName = params.get(1); + findList(oldName, a -> a.forEach(v -> append(newName, v.toString()))); + } + + public void transformFields(final List params, final UnaryOperator operator) { + final String field = params.get(0); + final Value value = find(field); + + if (value != null) { + removeNested(field); + + if (operator != null) { + value.asList(a -> a.forEach(v -> append(field, operator.apply(v.toString())))); + } + } + } + + /** + * Retains only the given field/value pairs in this hash. + * + * @param fields the field names + */ + public void retainFields(final Collection fields) { + map.keySet().retainAll(fields); + } + + /** + * Removes all field/value pairs from this hash whose value is empty. + */ + public void removeEmptyValues() { + // TODO: + // + // - Remove empty arrays/hashes? + // - Remove empty strings(/arrays/hashes) recursively? + // + // => Compare Catmandu behaviour + map.values().removeIf(v -> v.isString() && v.asString().isEmpty()); + } + + /** + * Iterates over all field/value pairs in this hash. + * + * @param consumer the action to be performed for each field/value pair + */ + public void forEach(final BiConsumer consumer) { + map.forEach(consumer); + } + + @Override + public String asString() { + return map.toString(); + } + + private enum InsertMode { + + REPLACE { + @Override + void apply(final Hash hash, final String field, final String value) { + hash.put(field, new Value(value)); + } + }, + APPEND { + @Override + void apply(final Hash hash, final String field, final String value) { + final Value oldValue = hash.get(field); + final Value newValue = new Value(value); + hash.put(field, oldValue == null ? newValue : oldValue.merge(newValue)); + } + }; + + abstract void apply(Hash hash, String field, String value); + + } + + } + +} diff --git a/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java b/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java new file mode 100644 index 00000000..a26e6030 --- /dev/null +++ b/metafix/src/test/java/org/metafacture/metafix/HashValueTest.java @@ -0,0 +1,224 @@ +/* + * Copyright 2021 hbz NRW + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.metafacture.metafix; + +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 { + + private static final String FIELD = "field"; + private static final String OTHER_FIELD = "other field"; + + 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 = newHash(); + Assertions.assertFalse(hash.containsField(FIELD)); + } + + @Test + public void shouldContainExistingField() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + + Assertions.assertTrue(hash.containsField(FIELD)); + } + + @Test + public void shouldNotContainNullValue() { + final Value.Hash hash = newHash(); + hash.put(FIELD, null); + + Assertions.assertFalse(hash.containsField(FIELD)); + } + + @Test + public void shouldBeEmptyByDefault() { + final Value.Hash hash = newHash(); + Assertions.assertTrue(hash.isEmpty()); + } + + @Test + public void shouldNotBeEmptyAfterAddingValue() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + + Assertions.assertFalse(hash.isEmpty()); + } + + @Test + public void shouldNotAddNullValue() { + final Value.Hash hash = newHash(); + hash.put(FIELD, null); + + Assertions.assertTrue(hash.isEmpty()); + } + + @Test + public void shouldGetSizeOfDefaultMapping() { + final Value.Hash hash = newHash(); + Assertions.assertEquals(0, hash.size()); + } + + @Test + public void shouldGetSizeAfterAddingValues() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + hash.put(OTHER_FIELD, OTHER_VALUE); + + Assertions.assertEquals(2, hash.size()); + } + + @Test + public void shouldNotGetMissingField() { + final Value.Hash hash = newHash(); + Assertions.assertNull(hash.get(FIELD)); + } + + @Test + public void shouldGetExistingField() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + + Assertions.assertEquals(VALUE, hash.get(FIELD)); + } + + @Test + public void shouldNotReplaceMissingField() { + final Value.Hash hash = newHash(); + hash.replace(FIELD, VALUE); + + Assertions.assertNull(hash.get(FIELD)); + Assertions.assertFalse(hash.containsField(FIELD)); + } + + @Test + public void shouldReplaceExistingField() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + hash.replace(FIELD, OTHER_VALUE); + + Assertions.assertEquals(OTHER_VALUE, hash.get(FIELD)); + } + + @Test + public void shouldNotReplaceExistingFieldWithNullValue() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + hash.replace(FIELD, (Value) null); + + Assertions.assertEquals(VALUE, hash.get(FIELD)); + } + + @Test + public void shouldRemoveMissingField() { + final Value.Hash hash = newHash(); + hash.remove(FIELD); + + Assertions.assertNull(hash.get(FIELD)); + Assertions.assertFalse(hash.containsField(FIELD)); + } + + @Test + public void shouldRemoveExistingField() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + hash.remove(FIELD); + + Assertions.assertNull(hash.get(FIELD)); + Assertions.assertFalse(hash.containsField(FIELD)); + } + + @Test + public void shouldRetainFields() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + hash.put(OTHER_FIELD, OTHER_VALUE); + + hash.retainFields(Arrays.asList(FIELD)); + + Assertions.assertTrue(hash.containsField(FIELD)); + Assertions.assertFalse(hash.containsField(OTHER_FIELD)); + } + + @Test + public void shouldRetainNoFields() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + + hash.retainFields(Arrays.asList()); + + Assertions.assertTrue(hash.isEmpty()); + } + + @Test + public void shouldNotRetainMissingFields() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + + hash.retainFields(Arrays.asList(FIELD, OTHER_FIELD)); + + Assertions.assertTrue(hash.containsField(FIELD)); + Assertions.assertFalse(hash.containsField(OTHER_FIELD)); + } + + @Test + public void shouldRemoveEmptyValues() { + final Value.Hash hash = newHash(); + hash.put(FIELD, VALUE); + hash.put(OTHER_FIELD, new Value("")); + + hash.removeEmptyValues(); + + Assertions.assertTrue(hash.containsField(FIELD)); + Assertions.assertFalse(hash.containsField(OTHER_FIELD)); + } + + @Test + public void shouldIterateOverFieldValuePairs() { + 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); + } + + private Value.Hash newHash() { + return Value.newHash().asHash(); + } + +} diff --git a/metafix/src/test/java/org/metafacture/metafix/MetafixRecordTest.java b/metafix/src/test/java/org/metafacture/metafix/MetafixRecordTest.java index 86ae8cb0..7911fa59 100644 --- a/metafix/src/test/java/org/metafacture/metafix/MetafixRecordTest.java +++ b/metafix/src/test/java/org/metafacture/metafix/MetafixRecordTest.java @@ -56,7 +56,7 @@ public void entitiesPassThrough() { i.startRecord("1"); i.startEntity("deep"); i.startEntity("nested"); - i.literal("key", "val"); + i.literal("field", "value"); i.endEntity(); i.endEntity(); i.endRecord(); @@ -64,7 +64,7 @@ public void entitiesPassThrough() { o.get().startRecord("1"); o.get().startEntity("deep"); o.get().startEntity("nested"); - o.get().literal("key", "val"); + o.get().literal("field", "value"); f.apply(2).endEntity(); o.get().endRecord(); }); @@ -92,20 +92,20 @@ public void entitiesPassThroughRepeatEntity() { i -> { i.startRecord("1"); i.startEntity("some"); - i.literal("key", "val1"); + i.literal("field", "value1"); i.endEntity(); i.startEntity("some"); - i.literal("key", "val2"); + i.literal("field", "value2"); i.endEntity(); i.endRecord(); }, (o, f) -> { o.get().startRecord("1"); o.get().startEntity("some[]"); o.get().startEntity(""); - o.get().literal("key", "val1"); + o.get().literal("field", "value1"); o.get().endEntity(); o.get().startEntity(""); - o.get().literal("key", "val2"); + o.get().literal("field", "value2"); f.apply(2).endEntity(); o.get().endRecord(); }); @@ -120,10 +120,10 @@ public void entitiesPassThroughRepeatNestedEntity() { i.startRecord("1"); i.startEntity("deep"); i.startEntity("nested"); - i.literal("key", "val1"); + i.literal("field", "value1"); i.endEntity(); i.startEntity("nested"); - i.literal("key", "val2"); + i.literal("field", "value2"); i.endEntity(); i.endEntity(); i.endRecord(); @@ -131,9 +131,9 @@ public void entitiesPassThroughRepeatNestedEntity() { o.get().startRecord("1"); o.get().startEntity("deep"); o.get().startEntity("nested"); - o.get().startEntity("key[]"); - o.get().literal("1", "val1"); - o.get().literal("2", "val2"); + o.get().startEntity("field[]"); + o.get().literal("1", "value1"); + o.get().literal("2", "value2"); f.apply(3).endEntity(); o.get().endRecord(); }); diff --git a/metafix/src/test/java/org/metafacture/metafix/RecordTest.java b/metafix/src/test/java/org/metafacture/metafix/RecordTest.java new file mode 100644 index 00000000..fdc0381c --- /dev/null +++ b/metafix/src/test/java/org/metafacture/metafix/RecordTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2021 hbz NRW + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.metafacture.metafix; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class RecordTest { + + private static final String FIELD = "field"; + + private static final Value VALUE = new Value("value"); + private static final Value OTHER_VALUE = new Value("other value"); + + public RecordTest() { + } + + @Test + public void shouldCreateShallowCloneFromEmptyRecord() { + final Record record = new Record(); + final Record clone = record.shallowClone(); + + Assertions.assertNotSame(record, clone); + } + + @Test + public void shouldCreateShallowCloneFromNonEmptyRecord() { + final Record record = new Record(); + record.put(FIELD, VALUE); + + final Record clone = record.shallowClone(); + + Assertions.assertNotSame(record, clone); + Assertions.assertSame(record.get(FIELD), clone.get(FIELD)); + } + + @Test + public void shouldNotModifyTopLevelFromShallowClone() { + final Record record = new Record(); + + final Record clone = record.shallowClone(); + clone.put(FIELD, VALUE); + + Assertions.assertNotSame(record, clone); + Assertions.assertNull(record.get(FIELD)); + Assertions.assertNotNull(clone.get(FIELD)); + } + + @Test + public void shouldNotModifyOverwrittenValueFromShallowClone() { + final Record record = new Record(); + record.put(FIELD, VALUE); + + final Record clone = record.shallowClone(); + clone.put(FIELD, OTHER_VALUE); + + Assertions.assertNotSame(record, clone); + Assertions.assertEquals(VALUE, record.get(FIELD)); + Assertions.assertEquals(OTHER_VALUE, clone.get(FIELD)); + } + + @Test + public void shouldModifySubLevelFromShallowClone() { + final Record record = new Record(); + record.put(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 shouldEmitRecordByDefault() { + final Record record = new Record(); + Assertions.assertFalse(record.getReject()); + } + + @Test + public void shouldNotEmitRecordIfRejected() { + final Record record = new Record(); + record.setReject(true); + + Assertions.assertTrue(record.getReject()); + } + + @Test + public void shouldEmitCloneOfDefaultRecord() { + final Record record = new Record(); + final Record clone = record.shallowClone(); + + Assertions.assertFalse(clone.getReject()); + } + + @Test + public void shouldNotEmitCloneOfRejectedRecord() { + final Record record = new Record(); + record.setReject(true); + + final Record clone = record.shallowClone(); + + Assertions.assertTrue(clone.getReject()); + } + + @Test + public void shouldNotEmitCloneOfDefaultRecordIfRejected() { + final Record record = new Record(); + + final Record clone = record.shallowClone(); + clone.setReject(true); + + Assertions.assertFalse(record.getReject()); + Assertions.assertTrue(clone.getReject()); + } + +}