diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java index da811905f..d4b8ed94c 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.Set; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.comparison.CanonicalResourceComparer.ChangeAnalysisState; import org.hl7.fhir.r5.comparison.ResourceComparer.MessageCounts; import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.model.CanonicalType; @@ -31,11 +32,26 @@ import org.hl7.fhir.utilities.xhtml.XhtmlNode; public abstract class CanonicalResourceComparer extends ResourceComparer { + public enum ChangeAnalysisState { + Unknown, NotChanged, Changed, CannotEvaluate; + + boolean noteable() { + return this == Changed || this == CannotEvaluate; + } + } + + public abstract class CanonicalResourceComparison extends ResourceComparison { protected T left; protected T right; protected T union; protected T intersection; + + private ChangeAnalysisState changedMetadata = ChangeAnalysisState.Unknown; + private ChangeAnalysisState changedDefinitions = ChangeAnalysisState.Unknown; + private ChangeAnalysisState changedContent = ChangeAnalysisState.Unknown; + private ChangeAnalysisState changedContentInterpretation = ChangeAnalysisState.Unknown; + protected Map> metadata = new HashMap<>(); public CanonicalResourceComparison(T left, T right) { @@ -80,6 +96,59 @@ public abstract class CanonicalResourceComparer extends ResourceComparer { this.intersection = intersection; } + private ChangeAnalysisState updateState(ChangeAnalysisState newState, ChangeAnalysisState oldState) { + switch (newState) { + case CannotEvaluate: + return ChangeAnalysisState.CannotEvaluate; + case Changed: + if (oldState != ChangeAnalysisState.CannotEvaluate) { + return ChangeAnalysisState.Changed; + } + break; + case NotChanged: + if (oldState == ChangeAnalysisState.Unknown) { + return ChangeAnalysisState.NotChanged; + } + break; + case Unknown: + default: + break; + } + return oldState; + } + + public void updatedMetadataState(ChangeAnalysisState state) { + changedMetadata = updateState(state, changedMetadata); + } + + public void updateDefinitionsState(ChangeAnalysisState state) { + changedDefinitions = updateState(state, changedDefinitions); + } + + public void updateContentState(ChangeAnalysisState state) { + changedContent = updateState(state, changedContent); + } + + public void updateContentInterpretationState(ChangeAnalysisState state) { + changedContentInterpretation = updateState(state, changedContentInterpretation); + } + + public void updatedMetadataState(boolean state) { + changedMetadata = updateState(state ? ChangeAnalysisState.Changed : ChangeAnalysisState.NotChanged, changedMetadata); + } + + public void updateDefinitionsState(boolean state) { + changedDefinitions = updateState(state ? ChangeAnalysisState.Changed : ChangeAnalysisState.NotChanged, changedDefinitions); + } + + public void updateContentState(boolean state) { + changedContent = updateState(state ? ChangeAnalysisState.Changed : ChangeAnalysisState.NotChanged, changedContent); + } + + public void updateContentInterpretationState(boolean state) { + changedContentInterpretation = updateState(state ? ChangeAnalysisState.Changed : ChangeAnalysisState.NotChanged, changedContentInterpretation); + } + @Override protected String toTable() { String s = ""; @@ -96,34 +165,75 @@ public abstract class CanonicalResourceComparer extends ResourceComparer { sm.countMessages(cnts); } } + + protected String changeSummary() { + if (!(changedMetadata.noteable() || changedDefinitions.noteable() || changedContent.noteable() || changedContentInterpretation.noteable())) { + return null; + }; + CommaSeparatedStringBuilder bc = new CommaSeparatedStringBuilder(); + if (changedMetadata == ChangeAnalysisState.CannotEvaluate) { + bc.append("Metadata"); + } + if (changedDefinitions == ChangeAnalysisState.CannotEvaluate) { + bc.append("Definitions"); + } + if (changedContent == ChangeAnalysisState.CannotEvaluate) { + bc.append("Content"); + } + if (changedContentInterpretation == ChangeAnalysisState.CannotEvaluate) { + bc.append("Interpretation"); + } + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + if (changedMetadata == ChangeAnalysisState.Changed) { + b.append("Metadata"); + } + if (changedDefinitions == ChangeAnalysisState.Changed) { + b.append("Definitions"); + } + if (changedContent == ChangeAnalysisState.Changed) { + b.append("Content"); + } + if (changedContentInterpretation == ChangeAnalysisState.Changed) { + b.append("Interpretation"); + } + return (bc.length() == 0 ? "" : "Error Checking: "+bc.toString()+"; ")+ "Changed: "+b.toString(); + } } public CanonicalResourceComparer(ComparisonSession session) { super(session); } - protected void compareMetadata(CanonicalResource left, CanonicalResource right, Map> comp, CanonicalResourceComparison res) { - comparePrimitives("url", left.getUrlElement(), right.getUrlElement(), comp, IssueSeverity.ERROR, res); - comparePrimitives("version", left.getVersionElement(), right.getVersionElement(), comp, IssueSeverity.ERROR, res); - comparePrimitives("name", left.getNameElement(), right.getNameElement(), comp, IssueSeverity.INFORMATION, res); - comparePrimitives("title", left.getTitleElement(), right.getTitleElement(), comp, IssueSeverity.INFORMATION, res); - comparePrimitives("status", left.getStatusElement(), right.getStatusElement(), comp, IssueSeverity.INFORMATION, res); - comparePrimitives("experimental", left.getExperimentalElement(), right.getExperimentalElement(), comp, IssueSeverity.WARNING, res); - comparePrimitives("date", left.getDateElement(), right.getDateElement(), comp, IssueSeverity.INFORMATION, res); - comparePrimitives("publisher", left.getPublisherElement(), right.getPublisherElement(), comp, IssueSeverity.INFORMATION, res); - comparePrimitives("description", left.getDescriptionElement(), right.getDescriptionElement(), comp, IssueSeverity.NULL, res); - comparePrimitives("purpose", left.getPurposeElement(), right.getPurposeElement(), comp, IssueSeverity.NULL, res); - comparePrimitives("copyright", left.getCopyrightElement(), right.getCopyrightElement(), comp, IssueSeverity.INFORMATION, res); - compareCodeableConceptList("jurisdiction", left.getJurisdiction(), right.getJurisdiction(), comp, IssueSeverity.INFORMATION, res, res.getUnion().getJurisdiction(), res.getIntersection().getJurisdiction()); + protected boolean compareMetadata(CanonicalResource left, CanonicalResource right, Map> comp, CanonicalResourceComparison res) { + var changed = false; + changed = comparePrimitives("url", left.getUrlElement(), right.getUrlElement(), comp, IssueSeverity.ERROR, res) || changed; + if (session.getForVersion() == null) { + changed = comparePrimitives("version", left.getVersionElement(), right.getVersionElement(), comp, IssueSeverity.ERROR, res) || changed; + } + changed = comparePrimitives("name", left.getNameElement(), right.getNameElement(), comp, IssueSeverity.INFORMATION, res) || changed; + changed = comparePrimitives("title", left.getTitleElement(), right.getTitleElement(), comp, IssueSeverity.INFORMATION, res) || changed; + changed = comparePrimitives("status", left.getStatusElement(), right.getStatusElement(), comp, IssueSeverity.INFORMATION, res) || changed; + changed = comparePrimitives("experimental", left.getExperimentalElement(), right.getExperimentalElement(), comp, IssueSeverity.WARNING, res) || changed; + if (session.getForVersion() == null) { + changed = comparePrimitives("date", left.getDateElement(), right.getDateElement(), comp, IssueSeverity.INFORMATION, res) || changed; + } + changed = comparePrimitives("publisher", left.getPublisherElement(), right.getPublisherElement(), comp, IssueSeverity.INFORMATION, res) || changed; + changed = comparePrimitives("description", left.getDescriptionElement(), right.getDescriptionElement(), comp, IssueSeverity.NULL, res) || changed; + changed = comparePrimitives("purpose", left.getPurposeElement(), right.getPurposeElement(), comp, IssueSeverity.NULL, res) || changed; + changed = comparePrimitives("copyright", left.getCopyrightElement(), right.getCopyrightElement(), comp, IssueSeverity.INFORMATION, res) || changed; + changed = compareCodeableConceptList("jurisdiction", left.getJurisdiction(), right.getJurisdiction(), comp, IssueSeverity.INFORMATION, res, res.getUnion().getJurisdiction(), res.getIntersection().getJurisdiction()) || changed; + return changed; } - protected void compareCodeableConceptList(String name, List left, List right, Map> comp, IssueSeverity level, CanonicalResourceComparison res, List union, List intersection ) { + protected boolean compareCodeableConceptList(String name, List left, List right, Map> comp, IssueSeverity level, CanonicalResourceComparison res, List union, List intersection ) { + boolean result = false; List matchR = new ArrayList<>(); StructuralMatch combined = new StructuralMatch(); for (CodeableConcept l : left) { CodeableConcept r = findCodeableConceptInList(right, l); if (r == null) { union.add(l); + result = true; combined.getChildren().add(new StructuralMatch(gen(l), vm(IssueSeverity.INFORMATION, "Removed the item '"+gen(l)+"'", fhirType()+"."+name, res.getMessages()))); } else { matchR.add(r); @@ -131,15 +241,20 @@ public abstract class CanonicalResourceComparer extends ResourceComparer { intersection.add(r); StructuralMatch sm = new StructuralMatch(gen(l), gen(r)); combined.getChildren().add(sm); + if (sm.isDifferent()) { + result = true; + } } } for (CodeableConcept r : right) { if (!matchR.contains(r)) { union.add(r); + result = true; combined.getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Added the item '"+gen(r)+"'", fhirType()+"."+name, res.getMessages()), gen(r))); } } - comp.put(name, combined); + comp.put(name, combined); + return result; } @@ -233,7 +348,7 @@ public abstract class CanonicalResourceComparer extends ResourceComparer { } @SuppressWarnings("rawtypes") - protected void comparePrimitives(String name, PrimitiveType l, PrimitiveType r, Map> comp, IssueSeverity level, CanonicalResourceComparison res) { + protected boolean comparePrimitives(String name, PrimitiveType l, PrimitiveType r, Map> comp, IssueSeverity level, CanonicalResourceComparison res) { StructuralMatch match = null; if (l.isEmpty() && r.isEmpty()) { match = new StructuralMatch<>(null, null, null); @@ -255,7 +370,8 @@ public abstract class CanonicalResourceComparer extends ResourceComparer { res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, fhirType()+"."+name, "Values for "+name+" differ: '"+l.primitiveValue()+"' vs '"+r.primitiveValue()+"'", level)); } } - comp.put(name, match); + comp.put(name, match); + return match.isDifferent(); } protected abstract String fhirType(); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java index ced654a50..ad74339d9 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java @@ -33,7 +33,6 @@ public class CodeSystemComparer extends CanonicalResourceComparer { private StructuralMatch combined; private Map propMap = new HashMap<>(); // right to left; left retains it's name - public CodeSystemComparison(CodeSystem left, CodeSystem right) { super(left, right); combined = new StructuralMatch(); // base @@ -54,9 +53,15 @@ public class CodeSystemComparer extends CanonicalResourceComparer { @Override protected String summary() { - return "CodeSystem: "+left.present()+" vs "+right.present(); + String res = "CodeSystem: "+left.present()+" vs "+right.present(); + String ch = changeSummary(); + if (ch != null) { + res = res + ". "+ch; + } + return res; } + @Override protected String fhirType() { return "CodeSystem"; @@ -114,14 +119,18 @@ public class CodeSystemComparer extends CanonicalResourceComparer { cs1.setDate(new Date()); cs1.getProperty().addAll(cs.getProperty()); - compareMetadata(left, right, res.getMetadata(), res); - comparePrimitives("caseSensitive", left.getCaseSensitiveElement(), right.getCaseSensitiveElement(), res.getMetadata(), IssueSeverity.ERROR, res); - comparePrimitives("hierarchyMeaning", left.getHierarchyMeaningElement(), right.getHierarchyMeaningElement(), res.getMetadata(), IssueSeverity.ERROR, res); - comparePrimitives("compositional", left.getCompositionalElement(), right.getCompositionalElement(), res.getMetadata(), IssueSeverity.WARNING, res); - comparePrimitives("versionNeeded", left.getVersionNeededElement(), right.getVersionNeededElement(), res.getMetadata(), IssueSeverity.INFORMATION, res); - comparePrimitives("content", left.getContentElement(), right.getContentElement(), res.getMetadata(), IssueSeverity.WARNING, res); - - compareConcepts(left.getConcept(), right.getConcept(), res.getCombined(), res.getUnion().getConcept(), res.getIntersection().getConcept(), res.getUnion(), res.getIntersection(), res, "CodeSystem.concept"); + boolean ch = compareMetadata(left, right, res.getMetadata(), res); + ch = comparePrimitives("versionNeeded", left.getVersionNeededElement(), right.getVersionNeededElement(), res.getMetadata(), IssueSeverity.INFORMATION, res) || ch; + ch = comparePrimitives("compositional", left.getCompositionalElement(), right.getCompositionalElement(), res.getMetadata(), IssueSeverity.WARNING, res) || ch; + res.updatedMetadataState(ch); + ch = false; + ch = comparePrimitives("caseSensitive", left.getCaseSensitiveElement(), right.getCaseSensitiveElement(), res.getMetadata(), IssueSeverity.ERROR, res) || ch; + ch = comparePrimitives("hierarchyMeaning", left.getHierarchyMeaningElement(), right.getHierarchyMeaningElement(), res.getMetadata(), IssueSeverity.ERROR, res) || ch; + ch = comparePrimitives("content", left.getContentElement(), right.getContentElement(), res.getMetadata(), IssueSeverity.WARNING, res); + + ch = compareConcepts(left.getConcept(), right.getConcept(), res.getCombined(), res.getUnion().getConcept(), res.getIntersection().getConcept(), res.getUnion(), res.getIntersection(), res, "CodeSystem.concept") || ch; + res.updateDefinitionsState(ch); + return res; } @@ -153,13 +162,15 @@ public class CodeSystemComparer extends CanonicalResourceComparer { } - private void compareConcepts(List left, List right, StructuralMatch combined, + private boolean compareConcepts(List left, List right, StructuralMatch combined, List union, List intersection, CodeSystem csU, CodeSystem csI, CodeSystemComparison res, String path) { + boolean result = false; List matchR = new ArrayList<>(); for (ConceptDefinitionComponent l : left) { ConceptDefinitionComponent r = findInList(right, l); if (r == null) { union.add(l); + res.updateContentState(true); combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed this concept", path))); } else { matchR.add(r); @@ -168,17 +179,23 @@ public class CodeSystemComparer extends CanonicalResourceComparer { union.add(cdM); intersection.add(cdI); StructuralMatch sm = new StructuralMatch(l, r); - compare(sm.getMessages(), l, r, path+".where(code='"+l.getCode()+"')", res); + if (compare(sm.getMessages(), l, r, path+".where(code='"+l.getCode()+"')", res)) { + result = true; + } combined.getChildren().add(sm); - compareConcepts(l.getConcept(), r.getConcept(), sm, cdM.getConcept(), cdI.getConcept(), csU, csI, res, path+".where(code='"+l.getCode()+"').concept"); + if (compareConcepts(l.getConcept(), r.getConcept(), sm, cdM.getConcept(), cdI.getConcept(), csU, csI, res, path+".where(code='"+l.getCode()+"').concept")) { + result = true; + } } } for (ConceptDefinitionComponent r : right) { if (!matchR.contains(r)) { union.add(r); + res.updateContentState(true); combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Added this concept", path), r)); } } + return result; } private ConceptDefinitionComponent findInList(List list, ConceptDefinitionComponent item) { @@ -190,24 +207,30 @@ public class CodeSystemComparer extends CanonicalResourceComparer { return null; } - private void compare(List msgs, ConceptDefinitionComponent l, ConceptDefinitionComponent r, String path, CodeSystemComparison res) { - compareStrings(path, msgs, l.getDisplay(), r.getDisplay(), "display", IssueSeverity.WARNING, res); - compareStrings(path, msgs, l.getDefinition(), r.getDefinition(), "definition", IssueSeverity.INFORMATION, res); + private boolean compare(List msgs, ConceptDefinitionComponent l, ConceptDefinitionComponent r, String path, CodeSystemComparison res) { + boolean result = false; + result = compareStrings(path, msgs, l.getDisplay(), r.getDisplay(), "display", IssueSeverity.WARNING, res) || result; + result = compareStrings(path, msgs, l.getDefinition(), r.getDefinition(), "definition", IssueSeverity.INFORMATION, res) || result; + return result; } - private void compareStrings(String path, List msgs, String left, String right, String name, IssueSeverity level, CodeSystemComparison res) { + private boolean compareStrings(String path, List msgs, String left, String right, String name, IssueSeverity level, CodeSystemComparison res) { if (!Utilities.noString(right)) { if (Utilities.noString(left)) { msgs.add(vmI(level, "Value for "+name+" added", path)); + return true; } else if (!left.equals(right)) { if (level != IssueSeverity.NULL) { res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path+"."+name, "Changed value for "+name+": '"+left+"' vs '"+right+"'", level)); } msgs.add(vmI(level, name+" changed from left to right", path)); + return true; } } else if (!Utilities.noString(left)) { msgs.add(vmI(level, "Value for "+name+" removed", path)); + return true; } + return false; } private ConceptDefinitionComponent merge(ConceptDefinitionComponent l, ConceptDefinitionComponent r, List destProps, CodeSystemComparison res) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonRenderer.java index be29d518d..feeeaf077 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonRenderer.java @@ -175,6 +175,7 @@ public class ComparisonRenderer implements IEvaluationContext { vars.put("rightId", new StringType(comp.getRight().getId())); vars.put("leftUrl", new StringType(comp.getLeft().getUrl())); vars.put("rightUrl", new StringType(comp.getRight().getUrl())); + vars.put("summary", new StringType(comp.summary())); vars.put("errors", new StringType(new XhtmlComposer(true).compose(cs.renderErrors(comp)))); vars.put("metadata", new StringType(new XhtmlComposer(true).compose(cs.renderMetadata(comp, "", "")))); vars.put("concepts", new StringType(new XhtmlComposer(true).compose(cs.renderConcepts(comp, "", "")))); @@ -198,6 +199,7 @@ public class ComparisonRenderer implements IEvaluationContext { vars.put("rightId", new StringType(comp.getRight().getId())); vars.put("leftUrl", new StringType(comp.getLeft().getUrl())); vars.put("rightUrl", new StringType(comp.getRight().getUrl())); + vars.put("summary", new StringType(comp.summary())); vars.put("errors", new StringType(new XhtmlComposer(true).compose(cs.renderErrors(comp)))); vars.put("metadata", new StringType(new XhtmlComposer(true).compose(cs.renderMetadata(comp, "", "")))); vars.put("compose", new StringType(new XhtmlComposer(true).compose(cs.renderCompose(comp, "", "")))); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonSession.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonSession.java index 75abf98d9..522e2e7ac 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonSession.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonSession.java @@ -32,6 +32,8 @@ public class ComparisonSession { private String sessiondId; private int count; private boolean debug; + private String forVersion; + private boolean annotate; private String title; private ProfileKnowledgeProvider pkpLeft; private ProfileKnowledgeProvider pkpRight; @@ -164,4 +166,22 @@ public class ComparisonSession { public ProfileKnowledgeProvider getPkpRight() { return pkpRight; } + + public String getForVersion() { + return forVersion; + } + + public void setForVersion(String forVersion) { + this.forVersion = forVersion; + } + + public boolean isAnnotate() { + return annotate; + } + + public void setAnnotate(boolean annotate) { + this.annotate = annotate; + } + + } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/StructuralMatch.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/StructuralMatch.java index 7e96e8115..6ea63ebe3 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/StructuralMatch.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/StructuralMatch.java @@ -114,6 +114,8 @@ public class StructuralMatch { return this; } - + public boolean isDifferent() { + return (left == null) != (right == null) || !messages.isEmpty(); + } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java index c0261c3f2..9d1cbdfb0 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java @@ -8,6 +8,7 @@ import java.util.List; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.comparison.ResourceComparer.MessageCounts; +import org.hl7.fhir.r5.comparison.VersionComparisonAnnotation.AnotationType; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.Element; @@ -67,7 +68,12 @@ public class ValueSetComparer extends CanonicalResourceComparer { @Override protected String summary() { - return "ValueSet: "+left.present()+" vs "+right.present(); + String res = "ValueSet: "+left.present()+" vs "+right.present(); + String ch = changeSummary(); + if (ch != null) { + res = res + ". "+ch; + } + return res; } @Override @@ -118,28 +124,34 @@ public class ValueSetComparer extends CanonicalResourceComparer { vs1.setStatus(left.getStatus()); vs1.setDate(new Date()); - compareMetadata(left, right, res.getMetadata(), res); - comparePrimitives("immutable", left.getImmutableElement(), right.getImmutableElement(), res.getMetadata(), IssueSeverity.WARNING, res); + var ch = compareMetadata(left, right, res.getMetadata(), res); + var def = false; + ch = comparePrimitives("immutable", left.getImmutableElement(), right.getImmutableElement(), res.getMetadata(), IssueSeverity.WARNING, res) || ch; if (left.hasCompose() || right.hasCompose()) { - comparePrimitives("compose.lockedDate", left.getCompose().getLockedDateElement(), right.getCompose().getLockedDateElement(), res.getMetadata(), IssueSeverity.WARNING, res); - comparePrimitives("compose.inactive", left.getCompose().getInactiveElement(), right.getCompose().getInactiveElement(), res.getMetadata(), IssueSeverity.WARNING, res); + ch = comparePrimitives("compose.lockedDate", left.getCompose().getLockedDateElement(), right.getCompose().getLockedDateElement(), res.getMetadata(), IssueSeverity.WARNING, res) || ch; + def = comparePrimitives("compose.inactive", left.getCompose().getInactiveElement(), right.getCompose().getInactiveElement(), res.getMetadata(), IssueSeverity.WARNING, res) || def; } - - compareCompose(left.getCompose(), right.getCompose(), res, res.getUnion().getCompose(), res.getIntersection().getCompose()); + res.updatedMetadataState(ch); + + def = compareCompose(left.getCompose(), right.getCompose(), res, res.getUnion().getCompose(), res.getIntersection().getCompose()) || def; + res.updateDefinitionsState(def); compareExpansions(left, right, res); return res; } - - - private void compareCompose(ValueSetComposeComponent left, ValueSetComposeComponent right, ValueSetComparison res, ValueSetComposeComponent union, ValueSetComposeComponent intersection) { + private boolean compareCompose(ValueSetComposeComponent left, ValueSetComposeComponent right, ValueSetComparison res, ValueSetComposeComponent union, ValueSetComposeComponent intersection) { + boolean def = false; // first, the includes List matchR = new ArrayList<>(); for (ConceptSetComponent l : left.getInclude()) { ConceptSetComponent r = findInList(right.getInclude(), l, left.getInclude()); if (r == null) { union.getInclude().add(l); + res.updateContentState(true); res.getIncludes().getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed Include", "ValueSet.compose.include"))); + if (session.isAnnotate()) { + VersionComparisonAnnotation.markDeleted(right, session.getForVersion(), "include", l); + } } else { matchR.add(r); ConceptSetComponent csM = new ConceptSetComponent(); @@ -148,13 +160,17 @@ public class ValueSetComparer extends CanonicalResourceComparer { intersection.getInclude().add(csI); StructuralMatch sm = new StructuralMatch(l, r); res.getIncludes().getChildren().add(sm); - compareDefinitions(l, r, sm, csM, csI); + def = compareDefinitions(l, r, sm, csM, csI, res) || def; } } for (ConceptSetComponent r : right.getInclude()) { if (!matchR.contains(r)) { union.getInclude().add(r); - res.getIncludes().getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Added Include", "ValueSet.compose.include"), r)); + res.updateContentState(true); + res.getIncludes().getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Added Include", "ValueSet.compose.include"), r)); + if (session.isAnnotate()) { + VersionComparisonAnnotation.markAdded(r, session.getForVersion()); + } } } @@ -164,6 +180,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { ConceptSetComponent r = findInList(right.getExclude(), l, left.getExclude()); if (r == null) { union.getExclude().add(l); + res.updateContentState(true); res.getExcludes().getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed Exclude", "ValueSet.compose.exclude"))); } else { matchR.add(r); @@ -173,15 +190,17 @@ public class ValueSetComparer extends CanonicalResourceComparer { intersection.getExclude().add(csI); StructuralMatch sm = new StructuralMatch(l, r); res.getExcludes().getChildren().add(sm); - compareDefinitions(l, r, sm, csM, csI); + def = compareDefinitions(l, r, sm, csM, csI, res) || def; } } for (ConceptSetComponent r : right.getExclude()) { if (!matchR.contains(r)) { union.getExclude().add(r); + res.updateContentState(true); res.getExcludes().getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Added Exclude", "ValueSet.compose.exclude"), r)); } } + return def; } private ConceptSetComponent findInList(List matches, ConceptSetComponent item, List source) { @@ -218,13 +237,15 @@ public class ValueSetComparer extends CanonicalResourceComparer { } - private void compareDefinitions(ConceptSetComponent left, ConceptSetComponent right, StructuralMatch combined, ConceptSetComponent union, ConceptSetComponent intersection) { + private boolean compareDefinitions(ConceptSetComponent left, ConceptSetComponent right, StructuralMatch combined, ConceptSetComponent union, ConceptSetComponent intersection, ValueSetComparison res) { + boolean def = false; // system must match, but the rest might not. we're going to do the full comparison whatever, so the outcome looks consistent to the user List matchVSR = new ArrayList<>(); for (CanonicalType l : left.getValueSet()) { CanonicalType r = findInList(right.getValueSet(), l, left.getValueSet()); if (r == null) { union.getValueSet().add(l); + res.updateContentState(true); combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed ValueSet", "ValueSet.compose.include.valueSet"))); } else { matchVSR.add(r); @@ -234,8 +255,10 @@ public class ValueSetComparer extends CanonicalResourceComparer { StructuralMatch sm = new StructuralMatch(l, r, null); combined.getChildren().add(sm); } else { + // it's not possible to get here? union.getValueSet().add(l); union.getValueSet().add(r); + res.updateContentState(true); StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.INFORMATION, "Values are different", "ValueSet.compose.include.valueSet")); combined.getChildren().add(sm); } @@ -244,6 +267,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { for (CanonicalType r : right.getValueSet()) { if (!matchVSR.contains(r)) { union.getValueSet().add(r); + res.updateContentState(true); combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Add ValueSet", "ValueSet.compose.include.valueSet"), r)); } } @@ -253,6 +277,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { ConceptReferenceComponent r = findInList(right.getConcept(), l, left.getConcept()); if (r == null) { union.getConcept().add(l); + res.updateContentState(true); combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed this Concept", "ValueSet.compose.include.concept"))); } else { matchCR.add(r); @@ -263,12 +288,14 @@ public class ValueSetComparer extends CanonicalResourceComparer { intersection.getConcept().add(ci); StructuralMatch sm = new StructuralMatch(l, r); combined.getChildren().add(sm); - compareConcepts(l, r, sm, cu, ci); + def = compareConcepts(l, r, sm, cu, ci) || def; } else { + // not that it's possible to get here? union.getConcept().add(l); union.getConcept().add(r); StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.INFORMATION, "Concepts are different", "ValueSet.compose.include.concept")); combined.getChildren().add(sm); + res.updateContentState(true); compareConcepts(l, r, sm, null, null); } } @@ -276,6 +303,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { for (ConceptReferenceComponent r : right.getConcept()) { if (!matchCR.contains(r)) { union.getConcept().add(r); + res.updateContentState(true); combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Added this Concept", "ValueSet.compose.include.concept"), r)); } } @@ -285,6 +313,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { ConceptSetFilterComponent r = findInList(right.getFilter(), l, left.getFilter()); if (r == null) { union.getFilter().add(l); + res.updateContentState(true); combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed this item", "ValueSet.compose.include.filter"))); } else { matchFR.add(r); @@ -295,11 +324,14 @@ public class ValueSetComparer extends CanonicalResourceComparer { intersection.getFilter().add(ci); StructuralMatch sm = new StructuralMatch(l, r); combined.getChildren().add(sm); - compareFilters(l, r, sm, cu, ci); + if (!compareFilters(l, r, sm, cu, ci)) { + res.updateContentState(true); + } } else { union.getFilter().add(l); union.getFilter().add(r); StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.INFORMATION, "Codes are different", "ValueSet.compose.include.filter")); + res.updateContentState(true); combined.getChildren().add(sm); compareFilters(l, r, sm, null, null); } @@ -308,12 +340,15 @@ public class ValueSetComparer extends CanonicalResourceComparer { for (ConceptSetFilterComponent r : right.getFilter()) { if (!matchFR.contains(r)) { union.getFilter().add(r); + res.updateContentState(true); combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Added this item", "ValueSet.compose.include.filter"), r)); } } + return def; } - private void compareConcepts(ConceptReferenceComponent l, ConceptReferenceComponent r, StructuralMatch sm, ConceptReferenceComponent cu, ConceptReferenceComponent ci) { + private boolean compareConcepts(ConceptReferenceComponent l, ConceptReferenceComponent r, StructuralMatch sm, ConceptReferenceComponent cu, ConceptReferenceComponent ci) { + boolean def = false; sm.getChildren().add(new StructuralMatch(l.getCodeElement(), r.getCodeElement(), l.getCode().equals(r.getCode()) ? null : vmI(IssueSeverity.INFORMATION, "Codes do not match", "ValueSet.compose.include.concept"))); if (ci != null) { ci.setCode(l.getCode()); @@ -325,24 +360,28 @@ public class ValueSetComparer extends CanonicalResourceComparer { ci.setDisplay(r.getDisplay()); cu.setDisplay(r.getDisplay()); } + def = !l.getDisplay().equals(r.getDisplay()); } else if (l.hasDisplay()) { sm.getChildren().add(new StructuralMatch(l.getDisplayElement(), null, vmI(IssueSeverity.INFORMATION, "Display Removed", "ValueSet.compose.include.concept"))); if (ci != null) { ci.setDisplay(l.getDisplay()); cu.setDisplay(l.getDisplay()); } + def = true; } else if (r.hasDisplay()) { sm.getChildren().add(new StructuralMatch(null, r.getDisplayElement(), vmI(IssueSeverity.INFORMATION, "Display added", "ValueSet.compose.include.concept"))); if (ci != null) { ci.setDisplay(r.getDisplay()); cu.setDisplay(r.getDisplay()); } + def = true; } else { sm.getChildren().add(new StructuralMatch(null, null, vmI(IssueSeverity.INFORMATION, "No Display", "ValueSet.compose.include.concept"))); } + return def; } - private void compareFilters(ConceptSetFilterComponent l, ConceptSetFilterComponent r, StructuralMatch sm, ConceptSetFilterComponent cu, ConceptSetFilterComponent ci) { + private boolean compareFilters(ConceptSetFilterComponent l, ConceptSetFilterComponent r, StructuralMatch sm, ConceptSetFilterComponent cu, ConceptSetFilterComponent ci) { sm.getChildren().add(new StructuralMatch(l.getPropertyElement(), r.getPropertyElement(), l.getProperty().equals(r.getProperty()) ? null : vmI(IssueSeverity.INFORMATION, "Properties do not match", "ValueSet.compose.include.concept"))); sm.getChildren().add(new StructuralMatch(l.getOpElement(), r.getOpElement(), l.getOp().equals(r.getOp()) ? null : vmI(IssueSeverity.INFORMATION, "Filter Operations do not match", "ValueSet.compose.include.concept"))); sm.getChildren().add(new StructuralMatch(l.getValueElement(), r.getValueElement(), l.getValue().equals(r.getValue()) ? null : vmI(IssueSeverity.INFORMATION, "Values do not match", "ValueSet.compose.include.concept"))); @@ -354,6 +393,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { cu.setOp(l.getOp()); cu.setValue(l.getValue()); } + return !l.getProperty().equals(r.getProperty()); } private CanonicalType findInList(List matches, CanonicalType item, List source) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/VersionComparisonAnnotation.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/VersionComparisonAnnotation.java new file mode 100644 index 000000000..d94f88925 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/VersionComparisonAnnotation.java @@ -0,0 +1,143 @@ +package org.hl7.fhir.r5.comparison; + +import java.util.List; +import java.util.Map; +import java.util.ArrayList; +import java.util.HashMap; + +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; +import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; + +public class VersionComparisonAnnotation { + + public enum AnotationType { + Added, Changed, Deleted; + } + + public static final String USER_DATA_NAME = "versoin-annotation"; + + private AnotationType type; +// private String comment; +// private String link; + private Map> deletedChildren; + + private String version; + + private VersionComparisonAnnotation(AnotationType type, String version) { + super(); + this.type = type; + this.version = version; + } +// +// private VersionComparisonAnnotation(AnotationType type, String comment) { +// super(); +// this.type = type; +// this.comment = comment; +// } +// private VersionComparisonAnnotation(AnotationType type, String comment, String link) { +// super(); +// this.type = type; +// this.comment = comment; +// this.link = link; +// } +// + public static void markAdded(Base focus, String version) { + focus.setUserData(USER_DATA_NAME, new VersionComparisonAnnotation(AnotationType.Added, version)); + } + + public static void markDeleted(Base parent, String name, String version, Base other) { + VersionComparisonAnnotation vca = null; + if (parent.hasUserData(USER_DATA_NAME)) { + vca = (VersionComparisonAnnotation) parent.getUserData(USER_DATA_NAME); + assert vca.type != AnotationType.Added; + } else { + vca = new VersionComparisonAnnotation(AnotationType.Changed, version); + parent.setUserData(USER_DATA_NAME, vca); + } + if (vca.deletedChildren == null) { + vca.deletedChildren = new HashMap<>(); + } + if (!vca.deletedChildren.containsKey(name)) { + vca.deletedChildren.put(name, new ArrayList<>()); + } + other.setUserData(USER_DATA_NAME, new VersionComparisonAnnotation(AnotationType.Deleted, version)); + vca.deletedChildren.get(name).add(other); + } + + public AnotationType getType() { + return type; + } + public void setType(AnotationType type) { + this.type = type; + } + +// public String getComment() { +// return comment; +// } +// public void setComment(String comment) { +// this.comment = comment; +// } +// public String getLink() { +// return link; +// } +// public void setLink(String link) { +// this.link = link; +// } + + public static XhtmlNode render(Base b, XhtmlNode x) { + if (b.hasUserData(USER_DATA_NAME)) { + VersionComparisonAnnotation self = (VersionComparisonAnnotation) b.getUserData(USER_DATA_NAME); + return self.render(x); + } else { + return x; + } + } + + private XhtmlNode render(XhtmlNode x) { + switch (type) { + case Added: + XhtmlNode span = x.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", null); + span.img("icon-change-add.png", "This content has been added since version "+version); + span.tx(" Added:"); + return x; + case Changed: + return x; + case Deleted: + span = x.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", null); + span.img("icon-change-remove.png", "This content has been removed since version "+version); + span.tx(" Removed:"); + return x.strikethrough(); + default: + return x; + } + } + + public static boolean hasDeleted(Base base, String... names) { + boolean result = false; + if (base.hasUserData(USER_DATA_NAME)) { + VersionComparisonAnnotation self = (VersionComparisonAnnotation) base.getUserData(USER_DATA_NAME); + for (String name : names) { + if (self.deletedChildren != null && self.deletedChildren.containsKey(name)) { + result = true; + } + } + } + return result; + } + + public static List getDeleted(Base base, String... names) { + List result = new ArrayList<>(); + if (base.hasUserData(USER_DATA_NAME)) { + VersionComparisonAnnotation self = (VersionComparisonAnnotation) base.getUserData(USER_DATA_NAME); + for (String name : names) { + if (self.deletedChildren != null && self.deletedChildren.containsKey(name)) { + result.addAll(self.deletedChildren.get(name)); + } + } + } + return result; + } + +} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java index 22a320ace..c04102fa8 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java @@ -88,6 +88,17 @@ public abstract class Base implements Serializable, IBase, IElement { } + private static ThreadLocal copyUserData = new ThreadLocal<>(); + + public static boolean isCopyUserData() { + Boolean res = copyUserData.get(); + return res != null && res; + } + + public static void setCopyUserData(boolean value) { + copyUserData.set(value); + } + /** * User appended data items - allow users to add extra information to the class */ @@ -456,7 +467,11 @@ public abstract class Base implements Serializable, IBase, IElement { public abstract Base copy(); - public void copyValues(Base dst) { + public void copyValues(Base dst) { + if (isCopyUserData() && userData != null) { + dst.userData = new HashMap<>(); + dst.userData.putAll(userData); + } } /** diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java index ccc11cc96..95983dc20 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java @@ -14,8 +14,10 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.exceptions.TerminologyServiceException; +import org.hl7.fhir.r5.comparison.VersionComparisonAnnotation; import org.hl7.fhir.r5.context.IWorkerContext.CodingValidationRequest; import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; +import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.BooleanType; import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.model.CodeSystem; @@ -924,7 +926,7 @@ public class ValueSetRenderer extends TerminologyRenderer { generateCopyright(x, vs); } int index = 0; - if (vs.getCompose().getInclude().size() == 1 && vs.getCompose().getExclude().size() == 0) { + if (vs.getCompose().getInclude().size() == 1 && vs.getCompose().getExclude().size() == 0 && !VersionComparisonAnnotation.hasDeleted(vs.getCompose(), "include", "exclude")) { hasExtensions = genInclude(x.ul(), vs.getCompose().getInclude().get(0), "Include", langs, doDesignations, maps, designations, index, vs) || hasExtensions; } else { XhtmlNode p = x.para(); @@ -934,7 +936,11 @@ public class ValueSetRenderer extends TerminologyRenderer { hasExtensions = genInclude(ul, inc, "Include", langs, doDesignations, maps, designations, index, vs) || hasExtensions; index++; } - if (vs.getCompose().hasExclude()) { + for (Base inc : VersionComparisonAnnotation.getDeleted(vs.getCompose(), "include")) { + genInclude(ul, (ConceptSetComponent) inc, "Include", langs, doDesignations, maps, designations, index, vs); + index++; + } + if (vs.getCompose().hasExclude() || VersionComparisonAnnotation.hasDeleted(vs.getCompose(), "exclude")) { p = x.para(); p.tx("This value set excludes codes based on the following rules:"); ul = x.ul(); @@ -942,6 +948,10 @@ public class ValueSetRenderer extends TerminologyRenderer { hasExtensions = genInclude(ul, exc, "Exclude", langs, doDesignations, maps, designations, index, vs) || hasExtensions; index++; } + for (Base inc : VersionComparisonAnnotation.getDeleted(vs.getCompose(), "exclude")) { + genInclude(ul, (ConceptSetComponent) inc, "Exclude", langs, doDesignations, maps, designations, index, vs); + index++; + } } } @@ -1131,6 +1141,8 @@ public class ValueSetRenderer extends TerminologyRenderer { boolean hasExtensions = false; XhtmlNode li; li = ul.li(); + li = VersionComparisonAnnotation.render(inc, li); + Map definitions = new HashMap<>(); if (inc.hasSystem()) { @@ -1144,7 +1156,7 @@ public class ValueSetRenderer extends TerminologyRenderer { addCsRef(inc, li, e); if (inc.hasVersion()) { li.addText(" version "); - li.code(inc.getVersion()); + li.code(inc.getVersion()); } // for performance reasons, we do all the fetching in one batch diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java index ea1a6efd1..b26c53617 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java @@ -867,4 +867,9 @@ public class XhtmlNode extends XhtmlFluent implements IBaseXhtml { return res; } + + public XhtmlNode strikethrough() { + return addTag("s"); + } + } \ No newline at end of file