diff --git a/CHANGELOG.md b/CHANGELOG.md index e48e49ab4..f80eb5567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,12 @@ Note: version releases in the 0.x.y range may introduce breaking changes. ## [unreleased] - ### Added - ### Fixed +### Added +### Changed + +- FlatPathDto is now immutable ([#425](https://github.com/ehrbase/openEHR_SDK/pull/425)) + +### Fixed ## [1.24.0] ### Added diff --git a/serialisation/src/main/java/org/ehrbase/serialisation/flatencoding/structured/StructuredHelper.java b/serialisation/src/main/java/org/ehrbase/serialisation/flatencoding/structured/StructuredHelper.java index 86fc48a66..257f17752 100644 --- a/serialisation/src/main/java/org/ehrbase/serialisation/flatencoding/structured/StructuredHelper.java +++ b/serialisation/src/main/java/org/ehrbase/serialisation/flatencoding/structured/StructuredHelper.java @@ -25,7 +25,15 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ValueNode; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.StreamSupport; @@ -101,9 +109,8 @@ private static Map convertFlatToStructured(Map> sharedStartMap = flatMap.entrySet().stream() .collect(Collectors.groupingBy( e -> { - FlatPathDto startFlatPathDto = new FlatPathDto(); - startFlatPathDto.setName(e.getKey().getName()); - startFlatPathDto.setCount(e.getKey().getCount()); + FlatPathDto startFlatPathDto = new FlatPathDto( + e.getKey().getName(), null, e.getKey().getCount(), null); return startFlatPathDto; }, LinkedHashMap::new, @@ -112,30 +119,18 @@ private static Map convertFlatToStructured(Map structured = new LinkedHashMap<>(); sharedStartMap.forEach((k, v) -> { - if (!structured.containsKey(k.getName())) { - structured.put(k.getName(), new ArrayList<>()); - } - - List values = (List) structured.get(k.getName()); - - LinkedHashMap subMap = new LinkedHashMap<>(); // find sub entries - Map subEntriesMap = - convertFlatToStructured((Map) v.entrySet().stream() - .filter(e -> e.getKey().getChild() != null) - .collect(Collectors.toMap( - e -> FlatPathDto.removeStart(e.getKey(), new FlatPathDto(k)), - Map.Entry::getValue, - (u, t) -> u, - LinkedHashMap::new))); - - if (!subEntriesMap.isEmpty()) { - subMap.putAll(subEntriesMap); - } + Map subMap = convertFlatToStructured((Map) v.entrySet().stream() + .filter(e -> e.getKey().getChild() != null) + .collect(Collectors.toMap( + e -> FlatPathDto.removeStart(e.getKey(), k), + Map.Entry::getValue, + (u, t) -> u, + LinkedHashMap::new))); // find attributes - Map collect = v.entrySet().stream() + Map attributes = v.entrySet().stream() .filter(e -> e.getKey().getChild() == null) .collect(Collectors.toMap( e -> Optional.ofNullable(e.getKey().getAttributeName()) @@ -145,19 +140,18 @@ private static Map convertFlatToStructured(Map u, LinkedHashMap::new)); + List values = (List) structured.computeIfAbsent(k.getName(), n -> new ArrayList<>()); + // singe valued Attributes have no name - if (collect.size() == 1 && collect.containsKey("") && subMap.isEmpty()) { - values.add(collect.entrySet().stream() - .findAny() - .map(Map.Entry::getValue) - .orElse("")); - } else if (!collect.isEmpty()) { - - if (collect.containsKey("")) { - collect.put("|value", collect.get("")); - collect.remove(""); + if (attributes.size() == 1 && attributes.containsKey("") && subMap.isEmpty()) { + values.add(attributes.values().stream().findAny().orElse("")); + + } else { + if (attributes.containsKey("")) { + subMap.put("|value", attributes.get("")); + attributes.remove(""); } - subMap.putAll(collect); + subMap.putAll(attributes); } if (!subMap.isEmpty()) { diff --git a/serialisation/src/main/java/org/ehrbase/serialisation/walker/FlatHelper.java b/serialisation/src/main/java/org/ehrbase/serialisation/walker/FlatHelper.java index 20bbdd886..a9fa3e5ef 100644 --- a/serialisation/src/main/java/org/ehrbase/serialisation/walker/FlatHelper.java +++ b/serialisation/src/main/java/org/ehrbase/serialisation/walker/FlatHelper.java @@ -27,13 +27,13 @@ import static org.ehrbase.util.rmconstants.RmConstants.PARTY_RELATED; import static org.ehrbase.util.rmconstants.RmConstants.PARTY_SELF; import static org.ehrbase.util.rmconstants.RmConstants.RM_OBJECT; -import static org.ehrbase.webtemplate.parser.OPTParser.PATH_DIVIDER; import static org.ehrbase.webtemplate.parser.OPTParser.buildId; import com.nedap.archie.rm.datastructures.Event; import com.nedap.archie.rminfo.RMTypeInfo; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; +import java.util.Deque; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -41,173 +41,202 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.ehrbase.client.classgenerator.EnumValueSet; import org.ehrbase.util.exception.SdkException; import org.ehrbase.util.rmconstants.RmConstants; import org.ehrbase.webtemplate.model.WebTemplateNode; import org.ehrbase.webtemplate.path.flat.FlatPathDto; +import org.ehrbase.webtemplate.path.flat.FlatPathParser; public class FlatHelper { - private Map> pathCountMap = new HashMap<>(); + private final Map, Integer>> pathCountMap = new HashMap<>(); public String buildNamePath(Context context, boolean addCount) { - StringBuilder sb = new StringBuilder(); - StringBuilder fullPath = new StringBuilder(); - WebTemplateNode node = null; + StringBuilder namePathBuilder = new StringBuilder(); + List nodeIdPath = new ArrayList<>(); + + WebTemplateNode child = null; for (Iterator iterator = context.getNodeDeque().descendingIterator(); iterator.hasNext(); ) { - WebTemplateNode parent = node; - node = iterator.next(); - boolean skip = skip(node, parent); + WebTemplateNode parent = child; + child = iterator.next(); + boolean skip = skip(child, parent); + boolean parentIsSkippedElement = parent != null && ELEMENT.equals(parent.getRmType()) && skip(parent, null); + WebTemplateNode node = parentIsSkippedElement ? parent : child; - if (parentIsSkippedElement) { - fullPath.append(parent.getId()); - } else { - fullPath.append(node.getId()); - } + nodeIdPath.add(node.getId(true)); if (!skip) { - String key; + if (namePathBuilder.length() > 0) { + namePathBuilder.append('/'); + } - if (parentIsSkippedElement) { - key = sb + "/" + parent.getId(false); - } else { + // Value inherits id of skipped Element, if applicable + namePathBuilder.append(node.getId(false)); - key = sb + "/" + node.getId(false); - } - if (!pathCountMap.containsKey(key)) { - pathCountMap.put(key, new HashMap<>()); - } + // Note: the key is still without pathCount and count + String key = namePathBuilder.toString(); - if (!pathCountMap.get(key).containsKey(fullPath.toString())) { - pathCountMap - .get(key) - .put( - fullPath.toString(), - pathCountMap.get(key).values().stream() - .max(Comparator.naturalOrder()) - .map(i -> i + 1) - .orElse(1)); - } + Map, Integer> pathCounts = pathCountMap.computeIfAbsent(key, l -> new HashMap<>()); - Integer integer = Optional.ofNullable(pathCountMap.get(key)) - .map(m -> m.get(fullPath.toString())) - .orElse(1); - - if (integer != 1) { - if (parentIsSkippedElement) { - // Value inherits id of skipped Element - sb.append(parent.getId(false) + integer); - } else { - sb.append(node.getId(false) + integer); - } - } else { - if (parentIsSkippedElement) { - // Value inherits id of skipped Element - sb.append(parent.getId(false)); - } else { - sb.append(node.getId(false)); - } + List nodeIdPathKey = List.copyOf(nodeIdPath); + int pathCount = pathCounts.computeIfAbsent(nodeIdPathKey, k -> 1 + maxValue(pathCounts)); + if (pathCount != 1) { + namePathBuilder.append(pathCount); } - } - - if (parentIsSkippedElement) { - addCount(context, addCount, sb, parent, skip); - } else { - addCount(context, addCount, sb, node, skip); - } - if (!skip && iterator.hasNext()) { - sb.append("/"); - } - if (!pathCountMap.containsKey(sb.toString())) { - pathCountMap.put(sb.toString(), new HashMap<>()); - } + appendCount(node, context, addCount, namePathBuilder); - if (!pathCountMap.get(sb.toString()).containsKey(fullPath.toString())) { - pathCountMap - .get(sb.toString()) - .put( - fullPath.toString(), - pathCountMap.get(sb.toString()).values().stream() - .max(Comparator.naturalOrder()) - .map(i -> i + 1) - .orElse(1)); + Map, Integer> map = + pathCountMap.computeIfAbsent(namePathBuilder.toString(), l -> new HashMap<>()); + map.computeIfAbsent(nodeIdPath, k -> 1 + maxValue(map)); } } - return StringUtils.removeEnd(sb.toString(), "/"); + + return namePathBuilder.toString(); } - private void addCount(Context context, boolean addCount, StringBuilder sb, WebTemplateNode node, boolean skip) { - if (!skip - && node.getMax() != 1 - && context.getCountMap().containsKey(new NodeId(node)) - && (addCount || context.getCountMap().get(new NodeId(node)) != 0)) { - sb.append(":").append(context.getCountMap().get(new NodeId(node))); + private static int maxValue(Map map) { + return map.values().stream().mapToInt(Integer::intValue).max().orElse(0); + } + + /** + * For {@code nodes} with {@code max} != 0 and {@code count} != 0 or {@code forceAppend}' the count from the {@code context}, prefixed with ':', is added to the {@code StringBuilder} + * + * @param node + * @param context + * @param forceAppend + * @param sb + */ + private static void appendCount(WebTemplateNode node, Context context, boolean forceAppend, StringBuilder sb) { + if (node.getMax() == 1) { + return; } + Optional.of(node) + .map(NodeId::new) + .map(context.getCountMap()::get) + .filter(c -> forceAppend || c != 0) + .ifPresent(c -> sb.append(":").append(c)); } public static boolean isExactlyDvCodedText(Map values, String path) { - return values.keySet().stream().anyMatch(e -> e.isEqualTo(path + "|code")); + FlatPathDto codeAtt = FlatPathParser.parse(path).pathWithAttributeName("code"); + return values.keySet().stream().anyMatch(e -> e.isEqualTo(codeAtt)); } public static boolean isExactlyPartySelf(Map values, String path, WebTemplateNode node) { - - if (node != null && !List.of(RM_OBJECT, PARTY_PROXY, PARTY_SELF).contains(node.getRmType())) { - + if (node != null && !rmTypeMatches(node, RM_OBJECT, PARTY_PROXY, PARTY_SELF)) { return false; } - return values.entrySet().stream() - .noneMatch(e -> e.getKey().isEqualTo(path + "|name") - || e.getKey().isEqualTo(path + "|id") - || e.getKey().startsWith(path + "/relationship") - || e.getKey().startsWith(path + "/_identifier")) - || values.entrySet().stream() - .anyMatch(e -> e.getKey().isEqualTo(path + "|_type") - && PARTY_SELF.equals(StringUtils.unwrap(e.getValue(), '"'))); + + FlatPathDto pathDto = FlatPathParser.parse(path); + FlatPathDto typePath = pathDto.pathWithAttributeName("_type"); + // attributes from PartyIdentified + FlatPathDto namePath = pathDto.pathWithAttributeName("name"); + FlatPathDto idPath = pathDto.pathWithAttributeName("id"); + // has sub-path from PartyIdentified or PartyRelated? + FlatPathDto relationshipPath = pathDto.pathWithChild(FlatPathParser.parse("relationship")); + FlatPathDto identifierPath = pathDto.pathWithChild(FlatPathParser.parse("_identifier")); + + var valueIt = subEntries(values, path).iterator(); + boolean hasAttributeFromDifferentType = false; + while (valueIt.hasNext()) { + Map.Entry e = valueIt.next(); + if (keyAndValueMatches(e, typePath, PARTY_SELF)) { + return true; + } + FlatPathDto key = e.getKey(); + if (!hasAttributeFromDifferentType) { + hasAttributeFromDifferentType = key.isEqualTo(namePath) + || key.isEqualTo(idPath) + || key.startsWith(relationshipPath) + || key.startsWith(identifierPath); + } + } + return !hasAttributeFromDifferentType; } + /** + * + * @param values + * @param path + * @param node if given, the rmType is also checked + * @return + */ public static boolean isExactlyPartyRelated(Map values, String path, WebTemplateNode node) { - - if (node != null - && !List.of(RM_OBJECT, PARTY_PROXY, PARTY_IDENTIFIED, PARTY_RELATED) - .contains(node.getRmType())) { - + if (node != null && !rmTypeMatches(node, RM_OBJECT, PARTY_PROXY, PARTY_IDENTIFIED, PARTY_RELATED)) { return false; } - return values.keySet().stream().anyMatch(e -> e.startsWith(path + "/relationship")); + FlatPathDto pathDto = FlatPathParser.parse(path); + FlatPathDto relationshipPath = pathDto.pathWithChild(FlatPathParser.parse("relationship")); + + return subEntries(values, path).anyMatch(e -> e.getKey().startsWith(relationshipPath)); } public static boolean isExactlyPartyIdentified(Map values, String path, WebTemplateNode node) { - - if (node != null && !List.of(RM_OBJECT, PARTY_PROXY, PARTY_IDENTIFIED).contains(node.getRmType())) { - + if (node != null && !rmTypeMatches(node, RM_OBJECT, PARTY_PROXY, PARTY_IDENTIFIED)) { return false; } - return values.entrySet().stream().noneMatch(e -> e.getKey().startsWith(path + "/relationship")) - && values.entrySet().stream() - .noneMatch(e -> e.getKey().isEqualTo(path + "|_type") - && PARTY_SELF.equals(StringUtils.unwrap(e.getValue(), '"'))) - && values.entrySet().stream() - .anyMatch(e -> e.getKey().isEqualTo(path + "|name") - || (e.getKey().isEqualTo(path + "|id")) - || e.getKey().startsWith(path + "/_identifier")); + FlatPathDto pathDto = FlatPathParser.parse(path); + FlatPathDto typePath = pathDto.pathWithAttributeName("_type"); + // is sub-path from subclass? + FlatPathDto relationshipPath = pathDto.pathWithChild(new FlatPathDto("relationship", null, null, null)); + FlatPathDto namePath = pathDto.pathWithAttributeName("name"); + FlatPathDto idPath = pathDto.pathWithAttributeName("id"); + FlatPathDto identifierPath = pathDto.pathWithChild(new FlatPathDto("_identifier", null, null, null)); + + var valueIt = subEntries(values, path).iterator(); + boolean hasAttributeFromType = false; + while (valueIt.hasNext()) { + Map.Entry e = valueIt.next(); + FlatPathDto key = e.getKey(); + if (keyAndValueMatches(e, typePath, PARTY_SELF) || key.startsWith(relationshipPath)) { + return false; + } + if (!hasAttributeFromType) { + hasAttributeFromType = + key.isEqualTo(namePath) || key.isEqualTo(idPath) || key.startsWith(identifierPath); + } + } + return hasAttributeFromType; + } + + private static Stream> subEntries(Map values, String path) { + return values.entrySet().stream().filter(e -> e.getKey().startsWith(path)); + } + + private static boolean keyAndValueMatches( + Map.Entry entry, FlatPathDto typePath, String value) { + return entry.getKey().isEqualTo(typePath) + && Optional.of(entry) + .map(Map.Entry::getValue) + .map(v -> StringUtils.unwrap(v, '"')) + .filter(value::equals) + .isPresent(); + } + + private static boolean rmTypeMatches(WebTemplateNode node, String... rmTypNames) { + return ArrayUtils.contains(rmTypNames, node.getRmType()); } public static boolean isExactlyIntervalEvent(Map values, String path) { - return values.keySet().stream().anyMatch(e -> e.startsWith(path + "/math_function")); + FlatPathDto mathFunctionPath = + FlatPathParser.parse(path).pathWithChild(new FlatPathDto("math_function", null, null, null)); + return values.keySet().stream().anyMatch(k -> k.startsWith(mathFunctionPath)); } public boolean skip(Context context) { - WebTemplateNode node = context.getNodeDeque().poll(); - WebTemplateNode parent = context.getNodeDeque().peek(); - context.getNodeDeque().push(node); - boolean skip = skip(node, parent); - return skip; + Deque nodes = context.getNodeDeque(); + WebTemplateNode node = nodes.poll(); + WebTemplateNode parent = nodes.peek(); + nodes.push(node); + return skip(node, parent); } public boolean skip(WebTemplateNode node, WebTemplateNode parent) { @@ -225,14 +254,14 @@ public boolean skip(WebTemplateNode node, WebTemplateNode parent) { return true; } - if (List.of( - RmConstants.HISTORY, - RmConstants.ITEM_TREE, - RmConstants.ITEM_LIST, - RmConstants.ITEM_SINGLE, - RmConstants.ITEM_TABLE, - RmConstants.ITEM_STRUCTURE) - .contains(node.getRmType())) { + if (rmTypeMatches( + node, + RmConstants.HISTORY, + RmConstants.ITEM_TREE, + RmConstants.ITEM_LIST, + RmConstants.ITEM_SINGLE, + RmConstants.ITEM_TABLE, + RmConstants.ITEM_STRUCTURE)) { return true; } else if (parent != null && isEvent(node)) { // a corresponding RM-tree would contain at maximum 1 event. @@ -267,7 +296,8 @@ public boolean isEvent(WebTemplateNode node) { public boolean isNonMandatoryRmAttribute(WebTemplateNode node, WebTemplateNode parent) { RMTypeInfo typeInfo = Walker.ARCHIE_RM_INFO_LOOKUP.getTypeInfo(parent.getRmType()); - boolean nonMandatoryRmAttribute = typeInfo.getAttributes().containsKey(node.getName()) && node.getMin() == 0; + String nodeName = node.getName(); + boolean nonMandatoryRmAttribute = typeInfo.getAttributes().containsKey(nodeName) && node.getMin() == 0; boolean mandatoryNotInWebTemplate = List.of( "name", "archetype_node_id", @@ -277,16 +307,13 @@ public boolean isNonMandatoryRmAttribute(WebTemplateNode node, WebTemplateNode p "lower_included", "upper_unbounded", "lower_unbounded") - .contains(node.getName()); - boolean nonMandatoryInWebTemplate = typeInfo.getRmName().equals("ACTIVITY") - && node.getName().equals("timing") - || typeInfo.getRmName().equals(RmConstants.INSTRUCTION) - && node.getName().equals("expiry_time") - || typeInfo.getRmName().equals(RmConstants.INTERVAL_EVENT) - && node.getName().equals("width") - || typeInfo.getRmName().equals(RmConstants.INTERVAL_EVENT) - && node.getName().equals("math_function") - || typeInfo.getRmName().equals(ISM_TRANSITION) && node.getName().equals("transition"); + .contains(nodeName); + String rmName = typeInfo.getRmName(); + boolean nonMandatoryInWebTemplate = rmName.equals(RmConstants.ACTIVITY) && nodeName.equals("timing") + || rmName.equals(RmConstants.INSTRUCTION) && nodeName.equals("expiry_time") + || rmName.equals(RmConstants.INTERVAL_EVENT) && nodeName.equals("width") + || rmName.equals(RmConstants.INTERVAL_EVENT) && nodeName.equals("math_function") + || rmName.equals(ISM_TRANSITION) && nodeName.equals("transition"); return (nonMandatoryRmAttribute || mandatoryNotInWebTemplate) && !nonMandatoryInWebTemplate; } @@ -317,22 +344,30 @@ public static void consumeAllMatching( */ public static Map> extractMultiValued( String currentTerm, String childTerm, Map values) { - final String otherPath; - if (childTerm != null) { - - otherPath = currentTerm + PATH_DIVIDER + childTerm; + FlatPathDto currentTermDto = new FlatPathDto(currentTerm); + final FlatPathDto otherPath; + if (childTerm == null) { + otherPath = currentTermDto; } else { - otherPath = currentTerm; + otherPath = currentTermDto.pathWithChild(FlatPathParser.parse(childTerm)); } return values.entrySet().stream() .filter(s -> s.getKey().startsWith(otherPath)) .collect(Collectors.groupingBy( - e -> Optional.ofNullable(FlatPathDto.removeStart(e.getKey(), new FlatPathDto(currentTerm)) + e -> Optional.ofNullable(FlatPathDto.removeStart(e.getKey(), currentTermDto) .getCount()) .orElse(0), Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); } + /** + * Filter map by path prefix (string comparison) + * + * @param values + * @param path filter path + * @param includeRaw if raw nodes should be included + * @return a new map with the filtered entries + */ public static Map filter(Map values, String path, boolean includeRaw) { return values.entrySet().stream() @@ -343,29 +378,22 @@ public static Map filter(Map values, S public static Map convertAttributeToFlat( Map values, String path, String attr, String node) { - Map map; + FlatPathDto pathDto = FlatPathParser.parse(path); + String attrPostfix = attr + "_"; - map = values.entrySet().stream() + return values.entrySet().stream() .collect(Collectors.toMap( e1 -> { String attributeName = e1.getKey().getLast().getAttributeName(); - if (StringUtils.contains(attributeName, attr + "_")) { - Integer integer = Integer.valueOf(StringUtils.substringAfter(attributeName, ":")); - - return new FlatPathDto(path - + "/" - + node - + ":" - + integer - + "|" - + StringUtils.substringBetween(attributeName, attr + "_", ":")); + if (StringUtils.contains(attributeName, attrPostfix)) { + Integer count = Integer.valueOf(StringUtils.substringAfter(attributeName, ":")); + String attribute = StringUtils.substringBetween(attributeName, attrPostfix, ":"); + return pathDto.pathWithChild(new FlatPathDto(node, null, count, attribute)); } else { return e1.getKey(); } }, Map.Entry::getValue)); - - return map; } public static E findEnumValueOrThrow(String value, Class clazz) { diff --git a/util/src/main/java/org/ehrbase/util/rmconstants/RmConstants.java b/util/src/main/java/org/ehrbase/util/rmconstants/RmConstants.java index 6bc8ef86c..de108ab53 100644 --- a/util/src/main/java/org/ehrbase/util/rmconstants/RmConstants.java +++ b/util/src/main/java/org/ehrbase/util/rmconstants/RmConstants.java @@ -51,6 +51,7 @@ private RmConstants() {} public static final String ITEM_TABLE = "ITEM_TABLE"; public static final String ITEM_STRUCTURE = "ITEM_STRUCTURE"; + public static final String ACTIVITY = "ACTIVITY"; public static final String ISM_TRANSITION = "ISM_TRANSITION"; public static final String ELEMENT = "ELEMENT"; public static final String CODE_PHRASE = "CODE_PHRASE"; diff --git a/web-template/src/main/java/org/ehrbase/webtemplate/path/flat/FlatPathDto.java b/web-template/src/main/java/org/ehrbase/webtemplate/path/flat/FlatPathDto.java index 66655600e..65338aa4a 100644 --- a/web-template/src/main/java/org/ehrbase/webtemplate/path/flat/FlatPathDto.java +++ b/web-template/src/main/java/org/ehrbase/webtemplate/path/flat/FlatPathDto.java @@ -18,61 +18,114 @@ package org.ehrbase.webtemplate.path.flat; import java.util.AbstractMap; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.function.UnaryOperator; +import org.apache.commons.lang3.ObjectUtils; -public class FlatPathDto { +public final class FlatPathDto { - private String name; - private FlatPathDto child; - private String attributeName; - private Integer count; - - public FlatPathDto() {} - - public FlatPathDto(String path) { + private final String name; + private final FlatPathDto child; + private final String attributeName; + private final Integer count; + public FlatPathDto(CharSequence path) { this(FlatPathParser.parse(path)); } - public FlatPathDto(FlatPathDto flatPathDto) { - + private FlatPathDto(FlatPathDto flatPathDto) { this.name = flatPathDto.getName(); this.child = flatPathDto.getChild(); this.attributeName = flatPathDto.getAttributeName(); this.count = flatPathDto.getCount(); } - public String getName() { - return name; + public FlatPathDto(String name, FlatPathDto child, Integer count, String attributeName) { + this.name = name; + this.child = child; + this.count = count; + this.attributeName = attributeName; } - public void setName(String name) { - this.name = name; + public String getName() { + return name; } public FlatPathDto getChild() { return child; } - public void setChild(FlatPathDto child) { - this.child = child; + /** + * Clones this node and appends the child + * + * @param child + * @return + */ + public FlatPathDto nodeWithChild(FlatPathDto child) { + return new FlatPathDto(name, child, count, attributeName); + } + + /** + * Appends the child to a clone of this path + * @param child + * @return + */ + public FlatPathDto pathWithChild(FlatPathDto child) { + return replaceEnd(this, n -> n.nodeWithChild(child)); } public String getAttributeName() { return attributeName; } - public void setAttributeName(String attributeName) { - this.attributeName = attributeName; + /** + * Clones this node and sets the attributeName property + * + * @param attributeName + * @return + */ + public FlatPathDto nodeWithAttributeName(String attributeName) { + if (child != null) { + throw new IllegalStateException("attributeName can only be set for leaf nodes"); + } + return new FlatPathDto(name, child, count, attributeName); + } + + /** + * Clones the whole path and sets the attributeName property of the trailing node + * @param attributeName + * @return + */ + public FlatPathDto pathWithAttributeName(String attributeName) { + return replaceEnd(this, n -> n.nodeWithAttributeName(attributeName)); } public Integer getCount() { return count; } - public void setCount(Integer count) { - this.count = count; + /** + * Clones this node and sets the count property + * + * @param count + * @return + */ + public FlatPathDto nodeWithCount(Integer count) { + return new FlatPathDto(name, child, count, attributeName); + } + + /** + * Clones the whole path and sets the count property of the trailing node + * @param count + * @return + */ + public FlatPathDto pathWithCount(Integer count) { + return replaceEnd(this, n -> n.nodeWithCount(count)); } public String format() { @@ -108,53 +161,87 @@ public FlatPathDto getLast() { public static FlatPathDto removeEnd(FlatPathDto path, FlatPathDto remove) { - FlatPathDto me = new FlatPathDto(path); - FlatPathDto other = new FlatPathDto(remove); - FlatPathDto newMe = null; - do { + Deque nodes = new LinkedList<>(); + for (FlatPathDto n = path; n != null; n = n.getChild()) { + nodes.add(n); + } + + Deque removeNodes = new LinkedList(); + for (FlatPathDto n = remove; n != null; n = n.getChild()) { + removeNodes.add(n); + } - if (isNodeEqual(me, other)) break; + if (nodes.size() < removeNodes.size()) { + return null; + } - FlatPathDto newChild = new FlatPathDto(me); - newChild.setChild(null); - if (newMe == null) { - newMe = newChild; - } else { - newMe.getLast().setChild(newChild); + // check + skip tail + Iterator nIt = nodes.descendingIterator(); + Iterator rIt = removeNodes.descendingIterator(); + while (rIt.hasNext()) { + FlatPathDto nNode = nIt.next(); + FlatPathDto rNode = rIt.next(); + if (!isNodeEqual(nNode, rNode)) { + return null; } + } - me = me.child; + // construct result + FlatPathDto newPath = null; + while (nIt.hasNext()) { + FlatPathDto nNode = nIt.next(); + newPath = new FlatPathDto(nNode.name, newPath, nNode.count, nNode.attributeName); + } + return newPath; + } - } while (me != null); + public static FlatPathDto replaceEnd(FlatPathDto path, UnaryOperator replacement) { - if (me != null && me.isEqualTo(other.format())) { - return newMe; - } else { - return me; + if (path == null) { + return null; + } + + Deque nodes = new LinkedList<>(); + for (FlatPathDto n = path; n != null; n = n.getChild()) { + nodes.add(n); } + + Iterator nIt = nodes.descendingIterator(); + + FlatPathDto node = replacement.apply(nIt.next()); + + while (nIt.hasNext()) { + node = nIt.next().nodeWithChild(node); + } + return node; } public static boolean isNodeEqual(FlatPathDto me, FlatPathDto other) { + return isNodeEqual(me, other, false, false); + } + + public static boolean isNodeEqual(FlatPathDto me, FlatPathDto other, boolean ignoreCount, boolean ignoreAttribute) { if (!Objects.equals(me.getName(), other.getName())) { return false; } - if (!Objects.equals(me.getCount(), other.getCount()) + if (!ignoreCount + && !Objects.equals(me.getCount(), other.getCount()) && !(me.getCount() == null && Objects.equals(other.getCount(), 0)) && !(Objects.equals(me.getCount(), 0) && other.getCount() == null)) { return false; } - if (!Objects.equals(me.getAttributeName(), other.getAttributeName())) { + if (!ignoreAttribute && !Objects.equals(me.getAttributeName(), other.getAttributeName())) { return false; } return true; } public static FlatPathDto removeStart(FlatPathDto path, FlatPathDto remove) { - FlatPathDto other = new FlatPathDto(remove); - FlatPathDto me = new FlatPathDto(path); + FlatPathDto other = remove; + FlatPathDto me = path; do { if (!isNodeEqual(me, other)) break; @@ -165,14 +252,24 @@ public static FlatPathDto removeStart(FlatPathDto path, FlatPathDto remove) { if (other == null) { return me; } else { - return new FlatPathDto(path); + return path; } } public static FlatPathDto addEnd(FlatPathDto path, FlatPathDto add) { - var flatPath = new FlatPathDto(path); - flatPath.getLast().setChild(new FlatPathDto(add)); - return flatPath; + if (add == null) { + return path; + } + Deque nodes = new LinkedList(); + for (FlatPathDto n = path; n != null; n = n.getChild()) { + nodes.add(n); + } + FlatPathDto child = add; + Iterator it = nodes.descendingIterator(); + while (it.hasNext()) { + child = it.next().nodeWithChild(child); + } + return child; } @Override @@ -180,23 +277,17 @@ public String toString() { return format(); } - public boolean startsWith(String otherPath) { - FlatPathDto other = new FlatPathDto(otherPath); - FlatPathDto me = new FlatPathDto(this); - do { + public boolean startsWith(CharSequence otherPath) { + return startsWith(FlatPathParser.parse(otherPath)); + } - String tempAttributeName = me.getAttributeName(); - if (other.getAttributeName() == null) { - me.setAttributeName(null); - } + public boolean startsWith(FlatPathDto other) { + FlatPathDto me = this; + do { + boolean ignoreCount = other.getChild() == null && other.count == null; + boolean ignoreAttribute = other.getAttributeName() == null; - Integer tempCount = me.getCount(); - if (other.getChild() == null && other.count == null) { - me.setCount(null); - } - boolean nodeEqual = isNodeEqual(me, other); - me.setAttributeName(tempAttributeName); - me.setCount(tempCount); + boolean nodeEqual = isNodeEqual(me, other, ignoreCount, ignoreAttribute); if (!nodeEqual) break; other = other.getChild(); @@ -207,30 +298,46 @@ public boolean startsWith(String otherPath) { return other == null; } - public boolean isEqualTo(String otherPath) { + /** + * + * For {@code count} 0 and null are considered equal. + * + * @param otherPath + * @return + */ + public boolean isEqualTo(CharSequence otherPath) { + return isEqualTo(this, FlatPathParser.parse(otherPath), false, false); + } - FlatPathDto other = new FlatPathDto(otherPath); - FlatPathDto me = new FlatPathDto(this); - do { + public boolean isEqualTo(FlatPathDto otherPath) { + return isEqualTo(this, otherPath, false, false); + } - if (!Objects.equals(me.getName(), other.getName())) { - break; - } - if (!Objects.equals(me.getAttributeName(), other.getAttributeName())) { - break; - } + public boolean isEqualTo(FlatPathDto otherPath, boolean ignoreCount, boolean ignoreAttribute) { + return isEqualTo(this, otherPath, ignoreCount, ignoreAttribute); + } - if (!Objects.equals(me.getCount(), other.getCount()) - && !(me.getCount() == null && Objects.equals(other.getCount(), 0)) - && !(Objects.equals(me.getCount(), 0) && other.getCount() == null)) { - break; - } + public static boolean isEqualTo(FlatPathDto me, FlatPathDto other, boolean ignoreCount, boolean ignoreAttribute) { + if (me == other) { + return true; + } + if (ObjectUtils.anyNull(me, other)) { + return false; + } + if (!Objects.equals(me.getName(), other.getName())) { + return false; + } + if (!ignoreAttribute && !Objects.equals(me.getAttributeName(), other.getAttributeName())) { + return false; + } - other = other.getChild(); - me = me.child; - } while (other != null && me != null); + if (!ignoreCount + && Optional.ofNullable(me.getCount()).orElse(0).intValue() + != Optional.ofNullable(other.getCount()).orElse(0).intValue()) { + return false; + } - return other == null && me == null; + return isEqualTo(me.getChild(), other.getChild(), ignoreCount, ignoreAttribute); } @Override @@ -250,10 +357,11 @@ public int hashCode() { } public static Map.Entry get(Map map, String otherPath) { + FlatPathDto other = FlatPathParser.parse(otherPath); return map.entrySet().stream() - .filter(d -> d.getKey().isEqualTo(otherPath)) + .filter(d -> d.getKey().isEqualTo(other, false, false)) .findAny() - .orElse(new AbstractMap.SimpleEntry<>(null, null)); + .orElseGet(() -> new AbstractMap.SimpleEntry<>(null, null)); } } diff --git a/web-template/src/main/java/org/ehrbase/webtemplate/path/flat/FlatPathParser.java b/web-template/src/main/java/org/ehrbase/webtemplate/path/flat/FlatPathParser.java index 3100aa632..5293cb941 100644 --- a/web-template/src/main/java/org/ehrbase/webtemplate/path/flat/FlatPathParser.java +++ b/web-template/src/main/java/org/ehrbase/webtemplate/path/flat/FlatPathParser.java @@ -22,45 +22,50 @@ import org.apache.commons.lang3.StringUtils; -public class FlatPathParser { +public final class FlatPathParser { private FlatPathParser() {} public static FlatPathDto parse(CharSequence path) { - FlatPathDto dto = new FlatPathDto(); + if (StringUtils.equals("/", path)) { + return new FlatPathDto(null, null, null, null); + } - if (!StringUtils.equals("/", path)) { - CharSequence[] tempSplit; - CharSequence tempSubPath; + String name; + FlatPathDto child = null; + String attributeName = null; + Integer count = null; - // extract Children - tempSplit = splitFirst(removeStart(path, "/"), '/'); - tempSubPath = tempSplit[0]; + CharSequence[] tempSplit; + CharSequence tempSubPath; - if (tempSplit.length > 1) { - dto.setChild(parse(tempSplit[1])); - } + // extract Children + tempSplit = splitFirst(removeStart(path, "/"), '/'); + tempSubPath = tempSplit[0]; - // extract AttributeName - tempSplit = splitFirst(tempSubPath, '|'); - tempSubPath = tempSplit[0]; + if (tempSplit.length > 1) { + child = parse(tempSplit[1]); + } - if (tempSplit.length > 1) { - dto.setAttributeName(tempSplit[1].toString()); - } + // extract AttributeName + tempSplit = splitFirst(tempSubPath, '|'); + tempSubPath = tempSplit[0]; - // extract Count - tempSplit = splitFirst(tempSubPath, ':'); - tempSubPath = tempSplit[0]; + if (tempSplit.length > 1) { + attributeName = tempSplit[1].toString(); + } - if (tempSplit.length > 1) { - dto.setCount(Integer.valueOf(tempSplit[1].toString())); - } + // extract Count + tempSplit = splitFirst(tempSubPath, ':'); + tempSubPath = tempSplit[0]; - // Rest is the name - dto.setName(tempSubPath.toString()); + if (tempSplit.length > 1) { + count = Integer.parseUnsignedInt(tempSplit[1], 0, tempSplit[1].length(), 10); } - return dto; + // Rest is the name + name = tempSubPath.toString(); + + return new FlatPathDto(name, child, count, attributeName); } }