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 new file mode 100644 index 000000000..acf6f3e4a --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java @@ -0,0 +1,190 @@ +package org.hl7.fhir.r5.comparison; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.CanonicalResource; +import org.hl7.fhir.r5.model.PrimitiveType; +import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; +import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; +import org.hl7.fhir.utilities.xhtml.NodeType; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Title; + +public abstract class CanonicalResourceComparer extends ResourceComparer { + + + public class CanonicalResourceComparison extends ResourceCmparison { + + protected T left; + protected T right; + protected T union; + protected T intersection; + protected Map> metadata = new HashMap<>(); + + + public CanonicalResourceComparison(T left, T right) { + super(); + this.left = left; + this.right = right; + } + + + public T getLeft() { + return left; + } + + public T getRight() { + return right; + } + + public T getUnion() { + return union; + } + + + public T getIntersection() { + return intersection; + } + + + public Map> getMetadata() { + return metadata; + } + + public void setLeft(T left) { + this.left = left; + } + + + public void setRight(T right) { + this.right = right; + } + + + public void setUnion(T union) { + this.union = union; + } + + + public void setIntersection(T intersection) { + this.intersection = intersection; + } + + } + + public CanonicalResourceComparer(IWorkerContext context) { + super(context); + } + + protected void compareMetadata(CanonicalResource left, CanonicalResource right, Map> comp, CanonicalResourceComparison res) { + comparePrimitives("url", left.getUrlElement(), right.getUrlElement(), comp, IssueSeverity.INFORMATION, res); + comparePrimitives("version", left.getVersionElement(), right.getVersionElement(), comp, IssueSeverity.INFORMATION, 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); + } + + @SuppressWarnings("rawtypes") + protected void 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); + } else if (l.isEmpty()) { + match = new StructuralMatch<>(null, r.primitiveValue(), vm(IssueSeverity.INFORMATION, "Added this item", fhirType()+"."+name)); + } else if (r.isEmpty()) { + match = new StructuralMatch<>(l.primitiveValue(), null, vm(IssueSeverity.INFORMATION, "Removed this item", fhirType()+"."+name)); + } else if (!l.hasValue() && !r.hasValue()) { + match = new StructuralMatch<>(null, null, vm(IssueSeverity.INFORMATION, "No Value", fhirType()+"."+name)); + } else if (!l.hasValue()) { + match = new StructuralMatch<>(null, r.primitiveValue(), vm(IssueSeverity.INFORMATION, "No Value on Left", fhirType()+"."+name)); + } else if (!r.hasValue()) { + match = new StructuralMatch<>(l.primitiveValue(), null, vm(IssueSeverity.INFORMATION, "No Value on Right", fhirType()+"."+name)); + } else if (l.getValue().equals(r.getValue())) { + match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), null); + } else { + match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), vm(level, "Values Differ", fhirType()+"."+name)); + if (level != IssueSeverity.NULL) { + 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); + } + + protected abstract String fhirType(); + + public XhtmlNode renderMetadata(CanonicalResourceComparison comparison, String id, String prefix) throws FHIRException, IOException { + // columns: code, display (left|right), properties (left|right) + HierarchicalTableGenerator gen = new HierarchicalTableGenerator(Utilities.path("[tmp]", "compare"), false); + TableModel model = gen.new TableModel(id, true); + model.setAlternating(true); + model.getTitles().add(gen.new Title(null, null, "Name", "Property Name", null, 100)); + model.getTitles().add(gen.new Title(null, null, "Value", "The value of the property", null, 200, 2)); + model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200)); + + for (String n : sorted(comparison.getMetadata().keySet())) { + StructuralMatch t = comparison.getMetadata().get(n); + addRow(gen, model.getRows(), n, t); + } + return gen.generate(model, prefix, 0, null); + } + + private void addRow(HierarchicalTableGenerator gen, List rows, String name, StructuralMatch t) { + Row r = gen.new Row(); + rows.add(r); + r.getCells().add(gen.new Cell(null, null, name, null, null)); + if (t.hasLeft() && t.hasRight()) { + if (t.getLeft().equals(t.getRight())) { + r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null).span(2)); + } else { + r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + r.getCells().add(gen.new Cell(null, null, t.getRight(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + } + } else if (t.hasLeft()) { + r.setColor(COLOR_NO_ROW_RIGHT); + r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null)); + r.getCells().add(missingCell(gen)); + } else if (t.hasRight()) { + r.setColor(COLOR_NO_ROW_LEFT); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, t.getRight(), null, null)); + } else { + r.getCells().add(missingCell(gen).span(2)); + } + r.getCells().add(cellForMessages(gen, t.getMessages())); + } + + + private List sorted(Set keys) { + List res = new ArrayList<>(); + res.addAll(keys); + Collections.sort(res); + return res; + } + + +} 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 new file mode 100644 index 000000000..f92e42216 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java @@ -0,0 +1,423 @@ +package org.hl7.fhir.r5.comparison; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.CodeSystem; +import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent; +import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; +import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent; +import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Title; +import org.hl7.fhir.utilities.xhtml.XhtmlDocument; + +public class CodeSystemComparer extends CanonicalResourceComparer { + + + public class CodeSystemComparison extends CanonicalResourceComparison { + + 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 + } + + public Map getPropMap() { + return propMap; + } + + public StructuralMatch getCombined() { + return combined; + } + } + + private CodeSystem right; + + public CodeSystemComparer(IWorkerContext context) { + super(context); + this.context = context; + } + + public CodeSystemComparison compare(CodeSystem left, CodeSystem right) { + if (left == null) + throw new DefinitionException("No CodeSystem provided (left)"); + if (right == null) + throw new DefinitionException("No CodeSystem provided (right)"); + + + CodeSystemComparison res = new CodeSystemComparison(left, right); + CodeSystem cs = new CodeSystem(); + res.setUnion(cs); + cs.setId(UUID.randomUUID().toString().toLowerCase()); + cs.setUrl("urn:uuid:"+cs.getId()); + cs.setName("Union"+left.getName()+"And"+right.getName()); + cs.setTitle("Union of "+left.getTitle()+" And "+right.getTitle()); + cs.setStatus(left.getStatus()); + cs.setDate(new Date()); + for (PropertyComponent pL : left.getProperty()) { + cs.addProperty(pL.copy()); + } + for (PropertyComponent pR : left.getProperty()) { + PropertyComponent pL = findProperty(left, pR); + if (pL == null) { + String code = getUniqued(pR.getCode(), cs.getProperty()); + cs.addProperty(pR.copy().setCode(code)); + } else { + res.getPropMap().put(pR.getCode(), pL.getCode()); + } + } + + CodeSystem cs1 = new CodeSystem(); + res.setIntersection(cs1); + cs1.setId(UUID.randomUUID().toString().toLowerCase()); + cs1.setUrl("urn:uuid:"+cs1.getId()); + cs1.setName("Intersection"+left.getName()+"And"+right.getName()); + cs1.setTitle("Intersection of "+left.getTitle()+" And "+right.getTitle()); + cs1.setStatus(left.getStatus()); + 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"); + return res; + } + + private String getUniqued(String code, List list) { + int i = 0; + while (true) { + boolean ok = true; + String res = code+(i == 0 ? "" : i); + for (PropertyComponent t : list) { + if (res.equals(t.getCode())) { + ok = false; + } + } + if (ok) { + return res; + } + } + } + + private PropertyComponent findProperty(CodeSystem left, PropertyComponent p) { + for (PropertyComponent t : left.getProperty()) { + if (p.hasUri() && t.hasUri() && p.getUri().equals(t.getUri())) { + return t; + } else if (!p.hasUri() && !t.hasUri() && p.getCode().equals(t.getCode())) { + return t; + } + } + return null; + } + + + private void compareConcepts(List left, List right, StructuralMatch combined, + List union, List intersection, CodeSystem csU, CodeSystem csI, CodeSystemComparison res, String path) { + List matchR = new ArrayList<>(); + for (ConceptDefinitionComponent l : left) { + ConceptDefinitionComponent r = findInList(right, l); + if (r == null) { + union.add(l); + combined.getChildren().add(new StructuralMatch(l, vm(IssueSeverity.INFORMATION, "Removed this concept", path))); + } else { + matchR.add(r); + ConceptDefinitionComponent cdM = merge(l, r, csU.getProperty(), res); + ConceptDefinitionComponent cdI = intersect(l, r, res); + union.add(cdM); + intersection.add(cdI); + StructuralMatch sm = new StructuralMatch(l, r); + compare(sm.getMessages(), l, r, path+".where(code='"+l.getCode()+"')", res); + combined.getChildren().add(sm); + compareConcepts(l.getConcept(), r.getConcept(), sm, cdM.getConcept(), cdI.getConcept(), csU, csI, res, path+".where(code='"+l.getCode()+"').concept"); + } + } + for (ConceptDefinitionComponent r : right) { + if (!matchR.contains(r)) { + union.add(r); + combined.getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Added this concept", path), r)); + } + } + } + + private ConceptDefinitionComponent findInList(List list, ConceptDefinitionComponent item) { + for (ConceptDefinitionComponent t : list) { + if (t.getCode().equals(item.getCode())) { + return t; + } + } + 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 void 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(vm(level, "Value for "+name+" added", path)); + } 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(vm(level, name+" changed from left to right", path)); + } + } else if (!Utilities.noString(left)) { + msgs.add(vm(level, "Value for "+name+" removed", path)); + } + } + + private ConceptDefinitionComponent merge(ConceptDefinitionComponent l, ConceptDefinitionComponent r, List destProps, CodeSystemComparison res) { + ConceptDefinitionComponent cd = l.copy(); + if (!l.hasDisplay() && r.hasDisplay()) { + cd.setDisplay(r.getDisplay()); + } + if (!l.hasDefinition() && r.hasDefinition()) { + cd.setDefinition(r.getDefinition()); + } + mergeProps(cd, l, r, destProps, res); + mergeDesignations(cd, l, r); + return cd; + } + + private ConceptDefinitionComponent intersect(ConceptDefinitionComponent l, ConceptDefinitionComponent r, CodeSystemComparison res) { + ConceptDefinitionComponent cd = l.copy(); + if (l.hasDisplay() && !r.hasDisplay()) { + cd.setDisplay(null); + } + if (l.hasDefinition() && !r.hasDefinition()) { + cd.setDefinition(null); + } + intersectProps(cd, l, r, res); + // mergeDesignations(cd, l, r); + return cd; + } + + private void mergeDesignations(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r) { + for (ConceptDefinitionDesignationComponent td : l.getDesignation()) { + if (hasDesignation(td, r.getDesignation())) { + cd.getDesignation().add(td); + } + } + for (ConceptDefinitionDesignationComponent td : r.getDesignation()) { + if (hasDesignation(td, l.getDesignation())) { + cd.getDesignation().add(td); + } + } + } + + + private boolean hasDesignation(ConceptDefinitionDesignationComponent td, List designation) { + for (ConceptDefinitionDesignationComponent t : designation) { + if (designationsMatch(td, t)) { + return true; + } + } + return false; + } + + private boolean designationsMatch(ConceptDefinitionDesignationComponent l, ConceptDefinitionDesignationComponent r) { + if (l.hasUse() != r.hasUse()) { + return false; + } + if (l.hasLanguage() != r.hasLanguage()) { + return false; + } + if (l.hasValue() != r.hasValue()) { + return false; + } + if (l.hasUse()) { + if (l.getUse().equalsDeep(r.getUse())) { + return false; + } + } + if (l.hasLanguage()) { + if (l.getLanguageElement().equalsDeep(r.getLanguageElement())) { + return false; + } + } + if (l.hasValue()) { + if (l.getValueElement().equalsDeep(r.getValueElement())) { + return false; + } + } + return true; + } + + private void mergeProps(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r, List destProps, CodeSystemComparison res) { + List matchR = new ArrayList<>(); + for (ConceptPropertyComponent lp : l.getProperty()) { + ConceptPropertyComponent rp = findRightProp(r.getProperty(), lp, res); + if (rp == null) { + cd.getProperty().add(lp); + } else { + matchR.add(rp); + cd.getProperty().add(lp); + if (lp.getValue().equalsDeep(rp.getValue())) { + cd.getProperty().add(rp.setCode(res.getPropMap().get(rp.getCode()))); + } + } + } + for (ConceptPropertyComponent rp : r.getProperty()) { + if (!matchR.contains(rp)) { + cd.getProperty().add(rp.setCode(res.getPropMap().get(rp.getCode()))); + } + } + } + + private void intersectProps(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r, CodeSystemComparison res) { + for (ConceptPropertyComponent lp : l.getProperty()) { + ConceptPropertyComponent rp = findRightProp(r.getProperty(), lp, res); + if (rp != null) { + cd.getProperty().add(lp); + } + } + } + + private ConceptPropertyComponent findRightProp(List rightProperties, ConceptPropertyComponent lp, CodeSystemComparison res) { + for (ConceptPropertyComponent p : rightProperties) { + if (res.getPropMap().get(p.getCode()).equals(lp.getCode())) { + return p; + } + } + return null; + } + + + public XhtmlNode renderConcepts(CodeSystemComparison comparison, String id, String prefix) throws FHIRException, IOException { + // columns: code, display (left|right), properties (left|right) + HierarchicalTableGenerator gen = new HierarchicalTableGenerator(Utilities.path("[tmp]", "compare"), false); + TableModel model = gen.new TableModel(id, true); + model.setAlternating(true); + model.getTitles().add(gen.new Title(null, null, "Code", "The code for the concept", null, 100)); + model.getTitles().add(gen.new Title(null, null, "Display", "The display for the concept", null, 200, 2)); + for (PropertyComponent p : comparison.getUnion().getProperty()) { + model.getTitles().add(gen.new Title(null, null, p.getCode(), p.getDescription(), null, 100, 2)); + } + model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200)); + for (StructuralMatch t : comparison.getCombined().getChildren()) { + addRow(gen, model.getRows(), t, comparison); + } + return gen.generate(model, prefix, 0, null); + } + + private void addRow(HierarchicalTableGenerator gen, List rows, StructuralMatch t, CodeSystemComparison comparison) { + Row r = gen.new Row(); + rows.add(r); + r.getCells().add(gen.new Cell(null, null, t.either().getCode(), null, null)); + if (t.hasLeft() && t.hasRight()) { + if (t.getLeft().hasDisplay() && t.getRight().hasDisplay()) { + if (t.getLeft().getDisplay().equals(t.getRight().getDisplay())) { + r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2)); + } else { + r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + } + } else if (t.getLeft().hasDisplay()) { + r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null)); + r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); + } else if (t.getRight().hasDisplay()) { + r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); + r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null)); + } else { + r.getCells().add(missingCell(gen).span(2)); + } + for (PropertyComponent p : comparison.getUnion().getProperty()) { + ConceptPropertyComponent lp = getProp(t.getLeft(), p, false, comparison); + ConceptPropertyComponent rp = getProp(t.getRight(), p, true, comparison); + + if (lp != null && rp != null) { + if (lp.getValue().equals(rp.getValue())) { + r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2)); + } else { + r.getCells().add(gen.new Cell(null, null, lp.getValue().toString(), null, null)); + r.getCells().add(gen.new Cell(null, null, rp.getValue().toString(), null, null)); + } + } else if (lp != null) { + r.getCells().add(gen.new Cell(null, null, lp.getValue().toString(), null, null)); + r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); + } else if (rp != null) { + r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); + r.getCells().add(gen.new Cell(null, null, rp.getValue().toString(), null, null)); + } else { + r.getCells().add(missingCell(gen).span(2)); + } + + } + } else if (t.hasLeft()) { + r.setColor(COLOR_NO_ROW_RIGHT); + r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null)); + r.getCells().add(missingCell(gen)); + for (PropertyComponent p : comparison.getUnion().getProperty()) { + r.getCells().add(propertyCell(gen, t.getLeft(), p, false, comparison)); + r.getCells().add(missingCell(gen)); + } + } else { + r.setColor(COLOR_NO_ROW_LEFT); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null)); + for (PropertyComponent p : comparison.getUnion().getProperty()) { + r.getCells().add(missingCell(gen)); + r.getCells().add(propertyCell(gen, t.getLeft(), p, true, comparison)); + } + } + r.getCells().add(cellForMessages(gen, t.getMessages())); + } + + private Cell propertyCell(HierarchicalTableGenerator gen, ConceptDefinitionComponent cd, PropertyComponent p, boolean right, CodeSystemComparison comp) { + ConceptPropertyComponent cp = getProp(cd, p, right, comp); + if (cp == null) { + return missingCell(gen, right ? COLOR_NO_CELL_RIGHT : COLOR_NO_CELL_LEFT); + } else { + return gen.new Cell(null, null, cp.getValue().toString(), null, null); + } + } + + public ConceptPropertyComponent getProp(ConceptDefinitionComponent cd, PropertyComponent p, boolean right, CodeSystemComparison comp) { + String c = p.getCode(); + if (right) { + c = comp.getPropMap().get(c); + } + ConceptPropertyComponent cp = null; + for (ConceptPropertyComponent t : cd.getProperty()) { + if (t.getCode().equals(c)) { + cp = t; + } + } + return cp; + } + + @Override + protected String fhirType() { + return "CodeSystem"; + } + +} + diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/OldProfileComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/OldProfileComparer.java new file mode 100644 index 000000000..4def9effe --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/OldProfileComparer.java @@ -0,0 +1,1649 @@ +package org.hl7.fhir.r5.comparison; + +import java.io.BufferedOutputStream; + +/*- + * #%L + * org.hl7.fhir.r5 + * %% + * Copyright (C) 2014 - 2019 Health Level 7 + * %% + * 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. + * #L% + */ + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.comparison.OldProfileComparer.ProfileComparison; +import org.hl7.fhir.r5.conformance.ProfileUtilities; +import org.hl7.fhir.r5.conformance.ProfileUtilities.ProfileKnowledgeProvider; +import org.hl7.fhir.r5.conformance.ProfileUtilities.ProfileKnowledgeProvider.BindingResolution; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.formats.IParser; +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.ElementDefinition; +import org.hl7.fhir.r5.model.ElementDefinition.DiscriminatorType; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionConstraintComponent; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionMappingComponent; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingComponent; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent; +import org.hl7.fhir.r5.model.ElementDefinition.SlicingRules; +import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; +import org.hl7.fhir.r5.model.Enumerations.BindingStrength; +import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r5.model.IntegerType; +import org.hl7.fhir.r5.model.PrimitiveType; +import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; +import org.hl7.fhir.r5.model.DataType; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; +import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; +import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; +import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; +import org.hl7.fhir.r5.utils.DefinitionNavigator; +import org.hl7.fhir.r5.utils.KeyGenerator; +import org.hl7.fhir.r5.utils.NarrativeGenerator; +import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; +import org.hl7.fhir.utilities.Logger.LogMessageType; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.utilities.xhtml.XhtmlComposer; + +/** + * A engine that generates difference analysis between two sets of structure + * definitions, typically from 2 different implementation guides. + * + * How this class works is that you create it with access to a bunch of underying + * resources that includes all the structure definitions from both implementation + * guides + * + * Once the class is created, you repeatedly pass pairs of structure definitions, + * one from each IG, building up a web of difference analyses. This class will + * automatically process any internal comparisons that it encounters + * + * When all the comparisons have been performed, you can then generate a variety + * of output formats + * + * @author Grahame Grieve + * + */ +public class OldProfileComparer implements ProfileKnowledgeProvider { + + private class ValueSetComparison { + private ValueSet left; + private ValueSet right; + private String error; + } + + + + + + + private IWorkerContext context; + private KeyGenerator keygen; + private String folder; + + public OldProfileComparer(IWorkerContext context, KeyGenerator keygen, String folder) throws IOException { + super(); + this.context = context; + this.keygen = keygen; + this.folder = folder; + for (Entry e : context.getBinaries().entrySet()) { + TextFile.bytesToFile(e.getValue(), Utilities.path(folder, e.getKey())); + } + } + + public OldProfileComparer(IWorkerContext context, String folder) throws IOException { + super(); + this.context = context; + this.folder = folder; + for (Entry e : context.getBinaries().entrySet()) { + TextFile.bytesToFile(e.getValue(), Utilities.path(folder, e.getKey())); + } + } + + private static final int BOTH_NULL = 0; + private static final int EITHER_NULL = 1; + + public class ProfileComparison { + private String id; + /** + * the first of two structures that were compared to generate this comparison + * + * In a few cases - selection of example content and value sets - left gets + * preference over right + */ + private StructureDefinition left; + + /** + * the second of two structures that were compared to generate this comparison + * + * In a few cases - selection of example content and value sets - left gets + * preference over right + */ + private StructureDefinition right; + + + public String getId() { + return id; + } + private String leftName() { + return left.getName(); + } + private String rightName() { + return right.getName(); + } + + /** + * messages generated during the comparison. There are 4 grades of messages: + * information - a list of differences between structures + * warnings - notifies that the comparer is unable to fully compare the structures (constraints differ, open value sets) + * errors - where the structures are incompatible + * fatal errors - some error that prevented full analysis + * + * @return + */ + private List messages = new ArrayList(); + + /** + * The structure that describes all instances that will conform to both structures + */ + private StructureDefinition subset; + + /** + * The structure that describes all instances that will conform to either structures + */ + private StructureDefinition superset; + + public StructureDefinition getLeft() { + return left; + } + + public StructureDefinition getRight() { + return right; + } + + public List getMessages() { + return messages; + } + + public StructureDefinition getSubset() { + return subset; + } + + public StructureDefinition getSuperset() { + return superset; + } + + private boolean ruleEqual(String path, ElementDefinition ed, String vLeft, String vRight, String description, boolean nullOK) { + if (vLeft == null && vRight == null && nullOK) + return true; + if (vLeft == null && vRight == null) { + messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, description+" and not null (null/null)", ValidationMessage.IssueSeverity.ERROR)); + if (ed != null) + status(ed, ProfileUtilities.STATUS_ERROR); + } + if (vLeft == null || !vLeft.equals(vRight)) { + messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, description+" ("+vLeft+"/"+vRight+")", ValidationMessage.IssueSeverity.ERROR)); + if (ed != null) + status(ed, ProfileUtilities.STATUS_ERROR); + } + return true; + } + + private boolean ruleCompares(ElementDefinition ed, DataType vLeft, DataType vRight, String path, int nullStatus) throws IOException { + if (vLeft == null && vRight == null && nullStatus == BOTH_NULL) + return true; + if (vLeft == null && vRight == null) { + messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Must be the same and not null (null/null)", ValidationMessage.IssueSeverity.ERROR)); + status(ed, ProfileUtilities.STATUS_ERROR); + } + if (vLeft == null && nullStatus == EITHER_NULL) + return true; + if (vRight == null && nullStatus == EITHER_NULL) + return true; + if (vLeft == null || vRight == null || !Base.compareDeep(vLeft, vRight, false)) { + messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Must be the same ("+toString(vLeft)+"/"+toString(vRight)+")", ValidationMessage.IssueSeverity.ERROR)); + status(ed, ProfileUtilities.STATUS_ERROR); + } + return true; + } + + private boolean rule(ElementDefinition ed, boolean test, String path, String message) { + if (!test) { + messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, message, ValidationMessage.IssueSeverity.ERROR)); + status(ed, ProfileUtilities.STATUS_ERROR); + } + return test; + } + + private boolean ruleEqual(ElementDefinition ed, boolean vLeft, boolean vRight, String path, String elementName) { + if (vLeft != vRight) { + messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, elementName+" must be the same ("+vLeft+"/"+vRight+")", ValidationMessage.IssueSeverity.ERROR)); + status(ed, ProfileUtilities.STATUS_ERROR); + } + return true; + } + + private String toString(DataType val) throws IOException { + if (val instanceof PrimitiveType) + return "\"" + ((PrimitiveType) val).getValueAsString()+"\""; + + IParser jp = context.newJsonParser(); + return jp.composeString(val, "value"); + } + + public int getErrorCount() { + int c = 0; + for (ValidationMessage vm : messages) + if (vm.getLevel() == ValidationMessage.IssueSeverity.ERROR) + c++; + return c; + } + + public int getWarningCount() { + int c = 0; + for (ValidationMessage vm : messages) + if (vm.getLevel() == ValidationMessage.IssueSeverity.WARNING) + c++; + return c; + } + + public int getHintCount() { + int c = 0; + for (ValidationMessage vm : messages) + if (vm.getLevel() == ValidationMessage.IssueSeverity.INFORMATION) + c++; + return c; + } + } + + /** + * Value sets used in the subset and superset + */ + private List valuesets = new ArrayList(); + private List comparisons = new ArrayList(); + private String id; + private String title; + private String leftPrefix; + private String rightPrefix; + private String leftLink; + private String leftName; + private String rightLink; + private String rightName; + + + public List getValuesets() { + return valuesets; + } + + public void status(ElementDefinition ed, int value) { + ed.setUserData(ProfileUtilities.UD_ERROR_STATUS, Math.max(value, ed.getUserInt("error-status"))); + } + + public List getComparisons() { + return comparisons; + } + + /** + * Compare left and right structure definitions to see whether they are consistent or not + * + * Note that left and right are arbitrary choices. In one respect, left + * is 'preferred' - the left's example value and data sets will be selected + * over the right ones in the common structure definition + * @throws DefinitionException + * @throws IOException + * @throws FHIRFormatError + * + * @ + */ + public ProfileComparison compareProfiles(StructureDefinition left, StructureDefinition right) throws DefinitionException, IOException, FHIRFormatError { + ProfileComparison outcome = new ProfileComparison(); + outcome.left = left; + outcome.right = right; + + if (left == null) + throw new DefinitionException("No StructureDefinition provided (left)"); + if (right == null) + throw new DefinitionException("No StructureDefinition provided (right)"); + if (!left.hasSnapshot()) + throw new DefinitionException("StructureDefinition has no snapshot (left: "+outcome.leftName()+")"); + if (!right.hasSnapshot()) + throw new DefinitionException("StructureDefinition has no snapshot (right: "+outcome.rightName()+")"); + if (left.getSnapshot().getElement().isEmpty()) + throw new DefinitionException("StructureDefinition snapshot is empty (left: "+outcome.leftName()+")"); + if (right.getSnapshot().getElement().isEmpty()) + throw new DefinitionException("StructureDefinition snapshot is empty (right: "+outcome.rightName()+")"); + + for (ProfileComparison pc : comparisons) + if (pc.left.getUrl().equals(left.getUrl()) && pc.right.getUrl().equals(right.getUrl())) + return pc; + + outcome.id = Integer.toString(comparisons.size()+1); + comparisons.add(outcome); + + DefinitionNavigator ln = new DefinitionNavigator(context, left); + DefinitionNavigator rn = new DefinitionNavigator(context, right); + + // from here on in, any issues go in messages + outcome.superset = new StructureDefinition(); + outcome.subset = new StructureDefinition(); + keygen.genId(outcome.subset); + keygen.genId(outcome.superset); + if (outcome.ruleEqual(ln.path(), null,ln.path(), rn.path(), "Base Type is not compatible", false)) { + if (compareElements(outcome, ln.path(), ln, rn, null)) { + outcome.subset.setName("intersection of "+outcome.leftName()+" and "+outcome.rightName()); + outcome.subset.setStatus(PublicationStatus.DRAFT); + outcome.subset.setKind(outcome.left.getKind()); + outcome.subset.setType(outcome.left.getType()); + outcome.subset.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/"+outcome.subset.getType()); + outcome.subset.setDerivation(TypeDerivationRule.CONSTRAINT); + outcome.subset.setAbstract(false); + outcome.superset.setName("union of "+outcome.leftName()+" and "+outcome.rightName()); + outcome.superset.setStatus(PublicationStatus.DRAFT); + outcome.superset.setKind(outcome.left.getKind()); + outcome.superset.setType(outcome.left.getType()); + outcome.superset.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/"+outcome.subset.getType()); + outcome.superset.setAbstract(false); + outcome.superset.setDerivation(TypeDerivationRule.CONSTRAINT); + } else { + outcome.subset = null; + outcome.superset = null; + } + } + return outcome; + } + + /** + * left and right refer to the same element. Are they compatible? + * @param outcome + * @param outcome + * @param path + * @param left + * @param right + * @- if there's a problem that needs fixing in this code + * @throws DefinitionException + * @throws IOException + * @throws FHIRFormatError + */ + private boolean compareElements(ProfileComparison outcome, String path, DefinitionNavigator left, DefinitionNavigator right, String sliceName) throws DefinitionException, IOException, FHIRFormatError { +// preconditions: + assert(path != null); + assert(left != null); + assert(right != null); + assert(left.path().equals(right.path())); + + // we ignore slicing right now - we're going to clone the root one anyway, and then think about clones + // simple stuff + ElementDefinition subset = new ElementDefinition(); + subset.setPath(left.path()); + if (sliceName != null) + subset.setSliceName(sliceName); + + // not allowed to be different: + subset.getRepresentation().addAll(left.current().getRepresentation()); // can't be bothered even testing this one + if (!outcome.ruleCompares(subset, left.current().getDefaultValue(), right.current().getDefaultValue(), path+".defaultValue[x]", BOTH_NULL)) + return false; + subset.setDefaultValue(left.current().getDefaultValue()); + if (!outcome.ruleEqual(path, subset, left.current().getMeaningWhenMissing(), right.current().getMeaningWhenMissing(), "meaningWhenMissing Must be the same", true)) + return false; + subset.setMeaningWhenMissing(left.current().getMeaningWhenMissing()); + if (!outcome.ruleEqual(subset, left.current().getIsModifier(), right.current().getIsModifier(), path, "isModifier")) + return false; + subset.setIsModifier(left.current().getIsModifier()); + if (!outcome.ruleEqual(subset, left.current().getIsSummary(), right.current().getIsSummary(), path, "isSummary")) + return false; + subset.setIsSummary(left.current().getIsSummary()); + + // descriptive properties from ElementDefinition - merge them: + subset.setLabel(mergeText(subset, outcome, path, "label", left.current().getLabel(), right.current().getLabel())); + subset.setShort(mergeText(subset, outcome, path, "short", left.current().getShort(), right.current().getShort())); + subset.setDefinition(mergeText(subset, outcome, path, "definition", left.current().getDefinition(), right.current().getDefinition())); + subset.setComment(mergeText(subset, outcome, path, "comments", left.current().getComment(), right.current().getComment())); + subset.setRequirements(mergeText(subset, outcome, path, "requirements", left.current().getRequirements(), right.current().getRequirements())); + subset.getCode().addAll(mergeCodings(left.current().getCode(), right.current().getCode())); + subset.getAlias().addAll(mergeStrings(left.current().getAlias(), right.current().getAlias())); + subset.getMapping().addAll(mergeMappings(left.current().getMapping(), right.current().getMapping())); + // left will win for example + subset.setExample(left.current().hasExample() ? left.current().getExample() : right.current().getExample()); + + subset.setMustSupport(left.current().getMustSupport() || right.current().getMustSupport()); + ElementDefinition superset = subset.copy(); + + + // compare and intersect + superset.setMin(unionMin(left.current().getMin(), right.current().getMin())); + superset.setMax(unionMax(left.current().getMax(), right.current().getMax())); + subset.setMin(intersectMin(left.current().getMin(), right.current().getMin())); + subset.setMax(intersectMax(left.current().getMax(), right.current().getMax())); + outcome.rule(subset, subset.getMax().equals("*") || Integer.parseInt(subset.getMax()) >= subset.getMin(), path, "Cardinality Mismatch: "+card(left)+"/"+card(right)); + + superset.getType().addAll(unionTypes(path, left.current().getType(), right.current().getType())); + subset.getType().addAll(intersectTypes(subset, outcome, path, left.current().getType(), right.current().getType())); + outcome.rule(subset, !subset.getType().isEmpty() || (!left.current().hasType() && !right.current().hasType()), path, "Type Mismatch:\r\n "+typeCode(left)+"\r\n "+typeCode(right)); +// +// + superset.setMaxLengthElement(unionMaxLength(left.current().getMaxLength(), right.current().getMaxLength())); + subset.setMaxLengthElement(intersectMaxLength(left.current().getMaxLength(), right.current().getMaxLength())); + if (left.current().hasBinding() || right.current().hasBinding()) { + compareBindings(outcome, subset, superset, path, left.current(), right.current()); + } + + // note these are backwards + superset.getConstraint().addAll(intersectConstraints(path, left.current().getConstraint(), right.current().getConstraint())); + subset.getConstraint().addAll(unionConstraints(subset, outcome, path, left.current().getConstraint(), right.current().getConstraint())); + + // add the children + outcome.subset.getSnapshot().getElement().add(subset); + outcome.superset.getSnapshot().getElement().add(superset); + boolean ret = compareChildren(subset, outcome, path, left, right); + + // now process the slices + if (left.current().hasSlicing() || right.current().hasSlicing()) { + assert sliceName == null; + if (isExtension(left.path())) + return compareExtensions(outcome, path, superset, subset, left, right); +// return true; + else { + ElementDefinitionSlicingComponent slicingL = left.current().getSlicing(); + ElementDefinitionSlicingComponent slicingR = right.current().getSlicing(); + // well, this is tricky. If one is sliced, and the other is not, then in general, the union just ignores the slices, and the intersection is the slices. + if (left.current().hasSlicing() && !right.current().hasSlicing()) { + // the super set is done. Any restrictions in the slices are irrelevant to what the super set says, except that we're going sum up the value sets if we can (for documentation purposes) (todo) + // the minimum set is the slicing specified in the slicer + subset.setSlicing(slicingL); + // stick everything from the right to do with the slices to the subset + copySlices(outcome.subset.getSnapshot().getElement(), left.getStructure().getSnapshot().getElement(), left.slices()); + } else if (!left.current().hasSlicing() && right.current().hasSlicing()) { + // the super set is done. Any restrictions in the slices are irrelevant to what the super set says, except that we're going sum up the value sets if we can (for documentation purposes) (todo) + // the minimum set is the slicing specified in the slicer + subset.setSlicing(slicingR); + // stick everything from the right to do with the slices to the subset + copySlices(outcome.subset.getSnapshot().getElement(), right.getStructure().getSnapshot().getElement(), right.slices()); + } else if (isTypeSlicing(slicingL) || isTypeSlicing(slicingR)) { + superset.getSlicing().setRules(SlicingRules.OPEN).setOrdered(false).addDiscriminator().setType(DiscriminatorType.TYPE).setPath("$this"); + subset.getSlicing().setRules(slicingL.getRules() == SlicingRules.CLOSED || slicingR.getRules() == SlicingRules.CLOSED ? SlicingRules.OPEN : SlicingRules.CLOSED).setOrdered(false).addDiscriminator().setType(DiscriminatorType.TYPE).setPath("$this"); + + // the superset is the union of the types + // the subset is the intersection of them + List handled = new ArrayList<>(); + for (DefinitionNavigator t : left.slices()) { + DefinitionNavigator r = findMatchingSlice(right.slices(), t); + if (r == null) { + copySlice(outcome.superset.getSnapshot().getElement(), left.getStructure().getSnapshot().getElement(), t); + } else { + handled.add(r); + ret = compareElements(outcome, path+":"+t.current().getSliceName(), t, r, t.current().getSliceName()) && ret; + } + } + for (DefinitionNavigator t : right.slices()) { + if (!handled.contains(t)) { + copySlice(outcome.superset.getSnapshot().getElement(), right.getStructure().getSnapshot().getElement(), t); + } + } + } else if (slicingMatches(slicingL, slicingR)) { + // if it's the same, we can try matching the slices - though we might have to give up without getting matches correct + // there amy be implied consistency we can't reason about + throw new DefinitionException("Slicing matches but is not handled yet at "+left.current().getId()+": ("+ProfileUtilities.summarizeSlicing(slicingL)+")"); + } else { + // if the slicing is different, we can't compare them - or can we? + throw new DefinitionException("Slicing doesn't match at "+left.current().getId()+": ("+ProfileUtilities.summarizeSlicing(slicingL)+" / "+ProfileUtilities.summarizeSlicing(slicingR)+")"); + } + } + // todo: name + } + return ret; + } + + + private DefinitionNavigator findMatchingSlice(List slices, DefinitionNavigator tgt) { + for (DefinitionNavigator t : slices) { + if (sliceMatchesByType(t, tgt)) + return t; + } + return null; + } + + private boolean sliceMatchesByType(DefinitionNavigator t, DefinitionNavigator tgt) { + return t.current().typeSummary().equals(tgt.current().typeSummary()); + } + + private void copySlices(List target, List source, List list) { + for (DefinitionNavigator slice : list) { + copySlice(target, source, slice); + } + } + + public void copySlice(List target, List source, DefinitionNavigator slice) { + target.add(slice.current().copy()); + int i = source.indexOf(slice.current())+1; + while (i < source.size() && source.get(i).getPath().startsWith(slice.current().getPath()+".")) { + target.add(source.get(i).copy()); + i++; + } + } + + private boolean isTypeSlicing(ElementDefinitionSlicingComponent slicing) { + if (slicing.getDiscriminator().size() == 1 && slicing.getDiscriminatorFirstRep().getType() == DiscriminatorType.TYPE && "$this".equals(slicing.getDiscriminatorFirstRep().getPath())) + return true; + return false; + } + + private boolean slicingMatches(ElementDefinitionSlicingComponent l, ElementDefinitionSlicingComponent r) { + if (l.getDiscriminator().size() != r.getDiscriminator().size()) + return false; + for (int i = 0; i < l.getDiscriminator().size(); i++) { + if (!slicingMatches(l.getDiscriminator().get(i), r.getDiscriminator().get(i))) + return false; + } + return l.getOrdered() == r.getOrdered(); + } + + private boolean slicingMatches(ElementDefinitionSlicingDiscriminatorComponent l, ElementDefinitionSlicingDiscriminatorComponent r) { + return l.getType() == r.getType() && l.getPath().equals(r.getPath()); + } + + private class ExtensionUsage { + private DefinitionNavigator defn; + private int minSuperset; + private int minSubset; + private String maxSuperset; + private String maxSubset; + private boolean both = false; + + public ExtensionUsage(DefinitionNavigator defn, int min, String max) { + super(); + this.defn = defn; + this.minSubset = min; + this.minSuperset = min; + this.maxSubset = max; + this.maxSuperset = max; + } + + } + private boolean compareExtensions(ProfileComparison outcome, String path, ElementDefinition superset, ElementDefinition subset, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException { + // for now, we don't handle sealed (or ordered) extensions + + // for an extension the superset is all extensions, and the subset is.. all extensions - well, unless thay are sealed. + // but it's not useful to report that. instead, we collate the defined ones, and just adjust the cardinalities + Map map = new HashMap(); + + if (left.slices() != null) + for (DefinitionNavigator ex : left.slices()) { + String url = ex.current().getType().get(0).getProfile().get(0).getValue(); + if (map.containsKey(url)) + throw new DefinitionException("Duplicate Extension "+url+" at "+path); + else + map.put(url, new ExtensionUsage(ex, ex.current().getMin(), ex.current().getMax())); + } + if (right.slices() != null) + for (DefinitionNavigator ex : right.slices()) { + String url = ex.current().getType().get(0).getProfile().get(0).getValue(); + if (map.containsKey(url)) { + ExtensionUsage exd = map.get(url); + exd.minSuperset = unionMin(exd.defn.current().getMin(), ex.current().getMin()); + exd.maxSuperset = unionMax(exd.defn.current().getMax(), ex.current().getMax()); + exd.minSubset = intersectMin(exd.defn.current().getMin(), ex.current().getMin()); + exd.maxSubset = intersectMax(exd.defn.current().getMax(), ex.current().getMax()); + exd.both = true; + outcome.rule(subset, exd.maxSubset.equals("*") || Integer.parseInt(exd.maxSubset) >= exd.minSubset, path, "Cardinality Mismatch on extension: "+card(exd.defn)+"/"+card(ex)); + } else { + map.put(url, new ExtensionUsage(ex, ex.current().getMin(), ex.current().getMax())); + } + } + List names = new ArrayList(); + names.addAll(map.keySet()); + Collections.sort(names); + for (String name : names) { + ExtensionUsage exd = map.get(name); + if (exd.both) + outcome.subset.getSnapshot().getElement().add(exd.defn.current().copy().setMin(exd.minSubset).setMax(exd.maxSubset)); + outcome.superset.getSnapshot().getElement().add(exd.defn.current().copy().setMin(exd.minSuperset).setMax(exd.maxSuperset)); + } + return true; + } + + private boolean isExtension(String path) { + return path.endsWith(".extension") || path.endsWith(".modifierExtension"); + } + + private boolean compareChildren(ElementDefinition ed, ProfileComparison outcome, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError { + List lc = left.children(); + List rc = right.children(); + // it's possible that one of these profiles walks into a data type and the other doesn't + // if it does, we have to load the children for that data into the profile that doesn't + // walk into it + if (lc.isEmpty() && !rc.isEmpty() && right.current().getType().size() == 1 && left.hasTypeChildren(right.current().getType().get(0))) + lc = left.childrenFromType(right.current().getType().get(0)); + if (rc.isEmpty() && !lc.isEmpty() && left.current().getType().size() == 1 && right.hasTypeChildren(left.current().getType().get(0))) + rc = right.childrenFromType(left.current().getType().get(0)); + if (lc.size() != rc.size()) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Different number of children at "+path+" ("+Integer.toString(lc.size())+"/"+Integer.toString(rc.size())+")", ValidationMessage.IssueSeverity.ERROR)); + status(ed, ProfileUtilities.STATUS_ERROR); + return false; + } else { + for (int i = 0; i < lc.size(); i++) { + DefinitionNavigator l = lc.get(i); + DefinitionNavigator r = rc.get(i); + String cpath = comparePaths(l.path(), r.path(), path, l.nameTail(), r.nameTail()); + if (cpath != null) { + if (!compareElements(outcome, cpath, l, r, null)) + return false; + } else { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Different path at "+path+"["+Integer.toString(i)+"] ("+l.path()+"/"+r.path()+")", ValidationMessage.IssueSeverity.ERROR)); + status(ed, ProfileUtilities.STATUS_ERROR); + return false; + } + } + } + return true; + } + + private String comparePaths(String path1, String path2, String path, String tail1, String tail2) { + if (tail1.equals(tail2)) { + return path+"."+tail1; + } else if (tail1.endsWith("[x]") && tail2.startsWith(tail1.substring(0, tail1.length()-3))) { + return path+"."+tail1; + } else if (tail2.endsWith("[x]") && tail1.startsWith(tail2.substring(0, tail2.length()-3))) { + return path+"."+tail2; + } else + return null; + } + + private boolean compareBindings(ProfileComparison outcome, ElementDefinition subset, ElementDefinition superset, String path, ElementDefinition lDef, ElementDefinition rDef) throws FHIRFormatError { + assert(lDef.hasBinding() || rDef.hasBinding()); + if (!lDef.hasBinding()) { + subset.setBinding(rDef.getBinding()); + // technically, the super set is unbound, but that's not very useful - so we use the provided on as an example + superset.setBinding(rDef.getBinding().copy()); + superset.getBinding().setStrength(BindingStrength.EXAMPLE); + return true; + } + if (!rDef.hasBinding()) { + subset.setBinding(lDef.getBinding()); + superset.setBinding(lDef.getBinding().copy()); + superset.getBinding().setStrength(BindingStrength.EXAMPLE); + return true; + } + ElementDefinitionBindingComponent left = lDef.getBinding(); + ElementDefinitionBindingComponent right = rDef.getBinding(); + if (Base.compareDeep(left, right, false)) { + subset.setBinding(left); + superset.setBinding(right); + } + + // if they're both examples/preferred then: + // subset: left wins if they're both the same + // superset: + if (isPreferredOrExample(left) && isPreferredOrExample(right)) { + if (right.getStrength() == BindingStrength.PREFERRED && left.getStrength() == BindingStrength.EXAMPLE && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Example/preferred bindings differ at "+path+" using binding from "+outcome.rightName(), ValidationMessage.IssueSeverity.INFORMATION)); + status(subset, ProfileUtilities.STATUS_HINT); + subset.setBinding(right); + superset.setBinding(unionBindings(superset, outcome, path, left, right)); + } else { + if ((right.getStrength() != BindingStrength.EXAMPLE || left.getStrength() != BindingStrength.EXAMPLE) && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false) ) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Example/preferred bindings differ at "+path+" using binding from "+outcome.leftName(), ValidationMessage.IssueSeverity.INFORMATION)); + status(subset, ProfileUtilities.STATUS_HINT); + } + subset.setBinding(left); + superset.setBinding(unionBindings(superset, outcome, path, left, right)); + } + return true; + } + // if either of them are extensible/required, then it wins + if (isPreferredOrExample(left)) { + subset.setBinding(right); + superset.setBinding(unionBindings(superset, outcome, path, left, right)); + return true; + } + if (isPreferredOrExample(right)) { + subset.setBinding(left); + superset.setBinding(unionBindings(superset, outcome, path, left, right)); + return true; + } + + // ok, both are extensible or required. + ElementDefinitionBindingComponent subBinding = new ElementDefinitionBindingComponent(); + subset.setBinding(subBinding); + ElementDefinitionBindingComponent superBinding = new ElementDefinitionBindingComponent(); + superset.setBinding(superBinding); + subBinding.setDescription(mergeText(subset, outcome, path, "description", left.getDescription(), right.getDescription())); + superBinding.setDescription(mergeText(subset, outcome, null, "description", left.getDescription(), right.getDescription())); + if (left.getStrength() == BindingStrength.REQUIRED || right.getStrength() == BindingStrength.REQUIRED) + subBinding.setStrength(BindingStrength.REQUIRED); + else + subBinding.setStrength(BindingStrength.EXTENSIBLE); + if (left.getStrength() == BindingStrength.EXTENSIBLE || right.getStrength() == BindingStrength.EXTENSIBLE) + superBinding.setStrength(BindingStrength.EXTENSIBLE); + else + superBinding.setStrength(BindingStrength.REQUIRED); + + if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { + subBinding.setValueSet(left.getValueSet()); + superBinding.setValueSet(left.getValueSet()); + return true; + } else if (!left.hasValueSet()) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "No left Value set at "+path, ValidationMessage.IssueSeverity.ERROR)); + return true; + } else if (!right.hasValueSet()) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "No right Value set at "+path, ValidationMessage.IssueSeverity.ERROR)); + return true; + } else { + // ok, now we compare the value sets. This may be unresolvable. + ValueSet lvs = resolveVS(outcome.left, left.getValueSet()); + ValueSet rvs = resolveVS(outcome.right, right.getValueSet()); + if (lvs == null) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to resolve left value set "+left.getValueSet().toString()+" at "+path, ValidationMessage.IssueSeverity.ERROR)); + return true; + } else if (rvs == null) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to resolve right value set "+right.getValueSet().toString()+" at "+path, ValidationMessage.IssueSeverity.ERROR)); + return true; + } else { + // first, we'll try to do it by definition + ValueSet cvs = intersectByDefinition(lvs, rvs); + if(cvs == null) { + // if that didn't work, we'll do it by expansion + ValueSetExpansionOutcome le; + ValueSetExpansionOutcome re; + try { + le = context.expandVS(lvs, true, false); + re = context.expandVS(rvs, true, false); + if (le.getError() != null) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value set "+lvs.getUrl()+" could not be expanded", ValidationMessage.IssueSeverity.ERROR)); + } else if (re.getError() != null) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value set "+rvs.getUrl()+" could not be expanded", ValidationMessage.IssueSeverity.ERROR)); + } else if (!closed(le.getValueset())) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value set "+lvs.getUrl()+" is not closed, so can't be compased", ValidationMessage.IssueSeverity.ERROR)); + } else if (!closed(re.getValueset())) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value set "+rvs.getUrl()+" is not closed, so can't be compased", ValidationMessage.IssueSeverity.ERROR)); + } else { + cvs = intersectByExpansion(path, le.getValueset(), re.getValueset()); + if (!cvs.getCompose().hasInclude()) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value sets "+lvs.getUrl()+" and "+rvs.getUrl()+" do not intersect", ValidationMessage.IssueSeverity.ERROR)); + status(subset, ProfileUtilities.STATUS_ERROR); + return false; + } + } + } catch (Exception e){ + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to expand or process value sets "+lvs.getUrl()+" and "+rvs.getUrl()+": "+e.getMessage(), ValidationMessage.IssueSeverity.ERROR)); + status(subset, ProfileUtilities.STATUS_ERROR); + e.printStackTrace(); + return false; + } + } + if (cvs != null) { + subBinding.setValueSet("#"+addValueSet(cvs)); + superBinding.setValueSet("#"+addValueSet(unite(superset, outcome, path, lvs, rvs))); + } + } + } + return false; + } + + private ElementDefinitionBindingComponent unionBindings(ElementDefinition ed, ProfileComparison outcome, String path, ElementDefinitionBindingComponent left, ElementDefinitionBindingComponent right) throws FHIRFormatError { + ElementDefinitionBindingComponent union = new ElementDefinitionBindingComponent(); + if (left.getStrength().compareTo(right.getStrength()) < 0) + union.setStrength(left.getStrength()); + else + union.setStrength(right.getStrength()); + union.setDescription(mergeText(ed, outcome, path, "binding.description", left.getDescription(), right.getDescription())); + if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) + union.setValueSet(left.getValueSet()); + else { + ValueSet lvs = resolveVS(outcome.left, left.getValueSet()); + ValueSet rvs = resolveVS(outcome.left, right.getValueSet()); + if (lvs != null && rvs != null) + union.setValueSet("#"+addValueSet(unite(ed, outcome, path, lvs, rvs))); + else if (lvs != null) + union.setValueSet("#"+addValueSet(lvs)); + else if (rvs != null) + union.setValueSet("#"+addValueSet(rvs)); + } + return union; + } + + + private ValueSet unite(ElementDefinition ed, ProfileComparison outcome, String path, ValueSet lvs, ValueSet rvs) { + ValueSet vs = new ValueSet(); + vs.setName(path); + if (lvs.hasCompose()) { + for (ConceptSetComponent inc : lvs.getCompose().getInclude()) + vs.getCompose().getInclude().add(inc); + if (lvs.getCompose().hasExclude()) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value sets "+lvs.getUrl()+" has exclude statements, and no union involving it can be correctly determined", ValidationMessage.IssueSeverity.ERROR)); + status(ed, ProfileUtilities.STATUS_ERROR); + } + } + if (rvs.hasCompose()) { + for (ConceptSetComponent inc : rvs.getCompose().getInclude()) + if (!mergeIntoExisting(vs.getCompose().getInclude(), inc)) + vs.getCompose().getInclude().add(inc); + if (rvs.getCompose().hasExclude()) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value sets "+lvs.getUrl()+" has exclude statements, and no union involving it can be correctly determined", ValidationMessage.IssueSeverity.ERROR)); + status(ed, ProfileUtilities.STATUS_ERROR); + } + } + return vs; + } + + private boolean mergeIntoExisting(List include, ConceptSetComponent inc) { + for (ConceptSetComponent dst : include) { + if (Base.compareDeep(dst, inc, false)) + return true; // they're actually the same + if (dst.hasSystem() && dst.getSystem().equals(inc.getSystem())) { + if (inc.hasFilter() || dst.hasFilter()) { + return false; // just add the new one as a a parallel + } else if (inc.hasConcept() && dst.hasConcept()) { + for (ConceptReferenceComponent cc : inc.getConcept()) { + boolean found = false; + for (ConceptReferenceComponent dd : dst.getConcept()) { + if (dd.getCode().equals(cc.getCode())) + found = true; + if (found) { + if (cc.hasDisplay() && !dd.hasDisplay()) + dd.setDisplay(cc.getDisplay()); + break; + } + } + if (!found) + dst.getConcept().add(cc.copy()); + } + } else + dst.getConcept().clear(); // one of them includes the entire code system + } + } + return false; + } + + private ValueSet resolveVS(StructureDefinition ctxtLeft, String vsRef) { + if (vsRef == null) + return null; + return context.fetchResource(ValueSet.class, vsRef); + } + + private ValueSet intersectByDefinition(ValueSet lvs, ValueSet rvs) { + // this is just a stub. The idea is that we try to avoid expanding big open value sets from SCT, RxNorm, LOINC. + // there's a bit of long hand logic coming here, but that's ok. + return null; + } + + private ValueSet intersectByExpansion(String path, ValueSet lvs, ValueSet rvs) { + // this is pretty straight forward - we intersect the lists, and build a compose out of the intersection + ValueSet vs = new ValueSet(); + vs.setName(path); + vs.setStatus(PublicationStatus.DRAFT); + + Map left = new HashMap(); + scan(lvs.getExpansion().getContains(), left); + Map right = new HashMap(); + scan(rvs.getExpansion().getContains(), right); + Map inc = new HashMap(); + + for (String s : left.keySet()) { + if (right.containsKey(s)) { + ValueSetExpansionContainsComponent cc = left.get(s); + ConceptSetComponent c = inc.get(cc.getSystem()); + if (c == null) { + c = vs.getCompose().addInclude().setSystem(cc.getSystem()); + inc.put(cc.getSystem(), c); + } + c.addConcept().setCode(cc.getCode()).setDisplay(cc.getDisplay()); + } + } + return vs; + } + + private void scan(List list, Map map) { + for (ValueSetExpansionContainsComponent cc : list) { + if (cc.hasSystem() && cc.hasCode()) { + String s = cc.getSystem()+"::"+cc.getCode(); + if (!map.containsKey(s)) + map.put(s, cc); + } + if (cc.hasContains()) + scan(cc.getContains(), map); + } + } + + private boolean closed(ValueSet vs) { + return !ToolingExtensions.findBooleanExtension(vs.getExpansion(), ToolingExtensions.EXT_UNCLOSED); + } + + private boolean isPreferredOrExample(ElementDefinitionBindingComponent binding) { + return binding.getStrength() == BindingStrength.EXAMPLE || binding.getStrength() == BindingStrength.PREFERRED; + } + + private Collection intersectTypes(ElementDefinition ed, ProfileComparison outcome, String path, List left, List right) throws DefinitionException, IOException, FHIRFormatError { + List result = new ArrayList(); + for (TypeRefComponent l : left) { + if (l.hasAggregation()) + throw new DefinitionException("Aggregation not supported: "+path); + boolean pfound = false; + boolean tfound = false; + TypeRefComponent c = l.copy(); + for (TypeRefComponent r : right) { + if (r.hasAggregation()) + throw new DefinitionException("Aggregation not supported: "+path); + if (!l.hasProfile() && !r.hasProfile()) { + pfound = true; + } else if (!r.hasProfile()) { + pfound = true; + } else if (!l.hasProfile()) { + pfound = true; + c.setProfile(r.getProfile()); + } else { + StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), outcome.leftName()); + StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), outcome.rightName()); + if (sdl != null && sdr != null) { + if (sdl == sdr) { + pfound = true; + } else if (derivesFrom(sdl, sdr)) { + pfound = true; + } else if (derivesFrom(sdr, sdl)) { + c.setProfile(r.getProfile()); + pfound = true; + } else if (sdl.getType().equals(sdr.getType())) { + ProfileComparison comp = compareProfiles(sdl, sdr); + if (comp.getSubset() != null) { + pfound = true; + c.addProfile("#"+comp.id); + } + } + } + } + if (!l.hasTargetProfile() && !r.hasTargetProfile()) { + tfound = true; + } else if (!r.hasTargetProfile()) { + tfound = true; + } else if (!l.hasTargetProfile()) { + tfound = true; + c.setTargetProfile(r.getTargetProfile()); + } else { + StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getTargetProfile().get(0).getValue(), outcome.leftName()); + StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getTargetProfile().get(0).getValue(), outcome.rightName()); + if (sdl != null && sdr != null) { + if (sdl == sdr) { + tfound = true; + } else if (derivesFrom(sdl, sdr)) { + tfound = true; + } else if (derivesFrom(sdr, sdl)) { + c.setTargetProfile(r.getTargetProfile()); + tfound = true; + } else if (sdl.getType().equals(sdr.getType())) { + ProfileComparison comp = compareProfiles(sdl, sdr); + if (comp.getSubset() != null) { + tfound = true; + c.addTargetProfile("#"+comp.id); + } + } + } + } + } + if (pfound && tfound) + result.add(c); + } + return result; + } + + private StructureDefinition resolveProfile(ElementDefinition ed, ProfileComparison outcome, String path, String url, String name) { + StructureDefinition res = context.fetchResource(StructureDefinition.class, url); + if (res == null) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, path, "Unable to resolve profile "+url+" in profile "+name, ValidationMessage.IssueSeverity.WARNING)); + status(ed, ProfileUtilities.STATUS_HINT); + } + return res; + } + + private Collection unionTypes(String path, List left, List right) throws DefinitionException, IOException, FHIRFormatError { + List result = new ArrayList(); + for (TypeRefComponent l : left) + checkAddTypeUnion(path, result, l); + for (TypeRefComponent r : right) + checkAddTypeUnion(path, result, r); + return result; + } + + private void checkAddTypeUnion(String path, List results, TypeRefComponent nw) throws DefinitionException, IOException, FHIRFormatError { + boolean pfound = false; + boolean tfound = false; + nw = nw.copy(); + if (nw.hasAggregation()) + throw new DefinitionException("Aggregation not supported: "+path); + for (TypeRefComponent ex : results) { + if (Utilities.equals(ex.getWorkingCode(), nw.getWorkingCode())) { + if (!ex.hasProfile() && !nw.hasProfile()) + pfound = true; + else if (!ex.hasProfile()) { + pfound = true; + } else if (!nw.hasProfile()) { + pfound = true; + ex.setProfile(null); + } else { + // both have profiles. Is one derived from the other? + StructureDefinition sdex = context.fetchResource(StructureDefinition.class, ex.getProfile().get(0).getValue()); + StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, nw.getProfile().get(0).getValue()); + if (sdex != null && sdnw != null) { + if (sdex == sdnw) { + pfound = true; + } else if (derivesFrom(sdex, sdnw)) { + ex.setProfile(nw.getProfile()); + pfound = true; + } else if (derivesFrom(sdnw, sdex)) { + pfound = true; + } else if (sdnw.getSnapshot().getElement().get(0).getPath().equals(sdex.getSnapshot().getElement().get(0).getPath())) { + ProfileComparison comp = compareProfiles(sdex, sdnw); + if (comp.getSuperset() != null) { + pfound = true; + ex.addProfile("#"+comp.id); + } + } + } + } + if (!ex.hasTargetProfile() && !nw.hasTargetProfile()) + tfound = true; + else if (!ex.hasTargetProfile()) { + tfound = true; + } else if (!nw.hasTargetProfile()) { + tfound = true; + ex.setTargetProfile(null); + } else { + // both have profiles. Is one derived from the other? + StructureDefinition sdex = context.fetchResource(StructureDefinition.class, ex.getTargetProfile().get(0).getValue()); + StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, nw.getTargetProfile().get(0).getValue()); + if (sdex != null && sdnw != null) { + if (sdex == sdnw) { + tfound = true; + } else if (derivesFrom(sdex, sdnw)) { + ex.setTargetProfile(nw.getTargetProfile()); + tfound = true; + } else if (derivesFrom(sdnw, sdex)) { + tfound = true; + } else if (sdnw.getSnapshot().getElement().get(0).getPath().equals(sdex.getSnapshot().getElement().get(0).getPath())) { + ProfileComparison comp = compareProfiles(sdex, sdnw); + if (comp.getSuperset() != null) { + tfound = true; + ex.addTargetProfile("#"+comp.id); + } + } + } + } + } + } + if (!tfound || !pfound) + results.add(nw); + } + + + private boolean derivesFrom(StructureDefinition left, StructureDefinition right) { + // left derives from right if it's base is the same as right + // todo: recursive... + return left.hasBaseDefinition() && left.getBaseDefinition().equals(right.getUrl()); + } + + + private String mergeText(ElementDefinition ed, ProfileComparison outcome, String path, String name, String left, String right) { + if (left == null && right == null) + return null; + if (left == null) + return right; + if (right == null) + return left; + left = stripLinks(left); + right = stripLinks(right); + if (left.equalsIgnoreCase(right)) + return left; + if (path != null) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, path, "Elements differ in definition for "+name+":\r\n \""+left+"\"\r\n \""+right+"\"", + "Elements differ in definition for "+name+":
\""+Utilities.escapeXml(left)+"\"
\""+Utilities.escapeXml(right)+"\"", ValidationMessage.IssueSeverity.INFORMATION)); + status(ed, ProfileUtilities.STATUS_HINT); + } + return "left: "+left+"; right: "+right; + } + + + private String stripLinks(String s) { + while (s.contains("](")) { + int i = s.indexOf("]("); + int j = s.substring(i).indexOf(")"); + if (j == -1) + return s; + else + s = s.substring(0, i+1)+s.substring(i+j+1); + } + return s; + } + + private List mergeCodings(List left, List right) { + List result = new ArrayList(); + result.addAll(left); + for (Coding c : right) { + boolean found = false; + for (Coding ct : left) + if (Utilities.equals(c.getSystem(), ct.getSystem()) && Utilities.equals(c.getCode(), ct.getCode())) + found = true; + if (!found) + result.add(c); + } + return result; + } + + private List mergeStrings(List left, List right) { + List result = new ArrayList(); + result.addAll(left); + for (StringType c : right) { + boolean found = false; + for (StringType ct : left) + if (Utilities.equals(c.getValue(), ct.getValue())) + found = true; + if (!found) + result.add(c); + } + return result; + } + + private List mergeMappings(List left, List right) { + List result = new ArrayList(); + result.addAll(left); + for (ElementDefinitionMappingComponent c : right) { + boolean found = false; + for (ElementDefinitionMappingComponent ct : left) + if (Utilities.equals(c.getIdentity(), ct.getIdentity()) && Utilities.equals(c.getLanguage(), ct.getLanguage()) && Utilities.equals(c.getMap(), ct.getMap())) + found = true; + if (!found) + result.add(c); + } + return result; + } + + // we can't really know about constraints. We create warnings, and collate them + private List unionConstraints(ElementDefinition ed, ProfileComparison outcome, String path, List left, List right) { + List result = new ArrayList(); + for (ElementDefinitionConstraintComponent l : left) { + boolean found = false; + for (ElementDefinitionConstraintComponent r : right) + if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) + found = true; + if (!found) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "StructureDefinition "+outcome.leftName()+" has a constraint that is not found in "+outcome.rightName()+" and it is uncertain whether they are compatible ("+l.getXpath()+")", ValidationMessage.IssueSeverity.INFORMATION)); + status(ed, ProfileUtilities.STATUS_WARNING); + } + result.add(l); + } + for (ElementDefinitionConstraintComponent r : right) { + boolean found = false; + for (ElementDefinitionConstraintComponent l : left) + if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) + found = true; + if (!found) { + outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "StructureDefinition "+outcome.rightName()+" has a constraint that is not found in "+outcome.leftName()+" and it is uncertain whether they are compatible ("+r.getXpath()+")", ValidationMessage.IssueSeverity.INFORMATION)); + status(ed, ProfileUtilities.STATUS_WARNING); + result.add(r); + } + } + return result; + } + + + private List intersectConstraints(String path, List left, List right) { + List result = new ArrayList(); + for (ElementDefinitionConstraintComponent l : left) { + boolean found = false; + for (ElementDefinitionConstraintComponent r : right) + if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) + found = true; + if (found) + result.add(l); + } + return result; +} + + private String card(DefinitionNavigator defn) { + return Integer.toString(defn.current().getMin())+".."+defn.current().getMax(); + } + + private String typeCode(DefinitionNavigator defn) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (TypeRefComponent t : defn.current().getType()) + b.append(t.getWorkingCode()+(t.hasProfile() ? "("+t.getProfile()+")" : "")+(t.hasTargetProfile() ? "("+t.getTargetProfile()+")" : "")); // todo: other properties + return b.toString(); + } + + private int intersectMin(int left, int right) { + if (left > right) + return left; + else + return right; + } + + private int unionMin(int left, int right) { + if (left > right) + return right; + else + return left; + } + + private String intersectMax(String left, String right) { + int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left); + int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right); + if (l < r) + return left; + else + return right; + } + + private String unionMax(String left, String right) { + int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left); + int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right); + if (l < r) + return right; + else + return left; + } + + private IntegerType intersectMaxLength(int left, int right) { + if (left == 0) + left = Integer.MAX_VALUE; + if (right == 0) + right = Integer.MAX_VALUE; + if (left < right) + return left == Integer.MAX_VALUE ? null : new IntegerType(left); + else + return right == Integer.MAX_VALUE ? null : new IntegerType(right); + } + + private IntegerType unionMaxLength(int left, int right) { + if (left == 0) + left = Integer.MAX_VALUE; + if (right == 0) + right = Integer.MAX_VALUE; + if (left < right) + return right == Integer.MAX_VALUE ? null : new IntegerType(right); + else + return left == Integer.MAX_VALUE ? null : new IntegerType(left); + } + + + public String addValueSet(ValueSet cvs) { + String id = Integer.toString(valuesets.size()+1); + cvs.setId(id); + valuesets.add(cvs); + return id; + } + + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getLeftLink() { + return leftLink; + } + + public void setLeftLink(String leftLink) { + this.leftLink = leftLink; + } + + public String getLeftName() { + return leftName; + } + + public void setLeftName(String leftName) { + this.leftName = leftName; + } + + public String getRightLink() { + return rightLink; + } + + public void setRightLink(String rightLink) { + this.rightLink = rightLink; + } + + public String getRightName() { + return rightName; + } + + public void setRightName(String rightName) { + this.rightName = rightName; + } + + public String getLeftPrefix() { + return leftPrefix; + } + + public void setLeftPrefix(String leftPrefix) { + this.leftPrefix = leftPrefix; + } + + public String getRightPrefix() { + return rightPrefix; + } + + public void setRightPrefix(String rightPrefix) { + this.rightPrefix = rightPrefix; + } + + private String genPCLink(String name, String link, String prefix) { + if (link == null) { + return name; + } else if (!Utilities.isAbsoluteUrl(link) && !Utilities.noString(prefix)) { + return ""+Utilities.escapeXml(name)+""; + } else { + return ""+Utilities.escapeXml(name)+""; + } + } + + private String genValueSets(String base) throws IOException { + StringBuilder b = new StringBuilder(); + b.append("
    \r\n"); + for (ValueSet vs : getValuesets()) { + System.out.println(" .. Value set: "+vs.getName()); + b.append("
  • "); + b.append(" "+Utilities.escapeXml(vs.present())+""); + b.append("
  • \r\n"); + genValueSetFile(base+"-"+vs.getId()+".html", vs); + } + b.append("
\r\n"); + return b.toString(); + } + + private void genValueSetFile(String filename, ValueSet vs) throws IOException { + NarrativeGenerator gen = new NarrativeGenerator("", "http://hl7.org/fhir", context); + gen.setNoSlowLookup(true); + gen.generate(null, vs, false); + String s = new XhtmlComposer(XhtmlComposer.HTML).compose(vs.getText().getDiv()); + StringBuilder b = new StringBuilder(); + b.append(""); + b.append(""); + b.append(""+vs.present()+""); + b.append("\r\n"); + b.append(""); + b.append(""); + b.append("

"+vs.present()+"

"); + b.append(s); + b.append(""); + b.append(""); + TextFile.stringToFile(b.toString(), filename); + } + + private String genPCTable() { + StringBuilder b = new StringBuilder(); + + b.append("\r\n"); + b.append(""); + b.append(" "); + b.append(" "); + b.append(" "); + b.append(" "); + b.append(" "); + b.append(" "); + b.append(""); + + for (ProfileComparison cmp : getComparisons()) { + b.append(""); + b.append(" "); + b.append(" "); + b.append(" "); + b.append(" "); + b.append(" "); + b.append(" "); + b.append(""); + } + b.append("
LeftRightComparisonError #Warning #Hint #
"+Utilities.escapeXml(cmp.getLeft().getName())+""+Utilities.escapeXml(cmp.getRight().getName())+"Click Here"+cmp.getErrorCount()+""+cmp.getWarningCount()+""+cmp.getHintCount()+"
\r\n"); + + return b.toString(); + } + + + private String fixLink(String path, String pfx) { + return (pfx == null || Utilities.isAbsoluteUrl(path)) ? path : Utilities.pathURL(pfx, path); + } + + private String genCmpMessages(ProfileComparison cmp) { + StringBuilder b = new StringBuilder(); + b.append("\r\n"); + b.append("\r\n"); + b.append("\r\n"); + boolean found = false; + for (ValidationMessage vm : cmp.getMessages()) + if (vm.getLevel() == IssueSeverity.ERROR || vm.getLevel() == IssueSeverity.FATAL) { + found = true; + b.append("\r\n"); + } + if (!found) + b.append("\r\n"); + + boolean first = true; + for (ValidationMessage vm : cmp.getMessages()) + if (vm.getLevel() == IssueSeverity.WARNING) { + if (first) { + first = false; + b.append("\r\n"); + } + b.append("\r\n"); + } + first = true; + for (ValidationMessage vm : cmp.getMessages()) + if (vm.getLevel() == IssueSeverity.INFORMATION) { + if (first) { + b.append("\r\n"); + first = false; + } + b.append("\r\n"); + } + b.append("
PathMessage
Errors Detected
"+vm.getLocation()+""+vm.getHtml()+(vm.getLevel() == IssueSeverity.FATAL ? "(This error terminated the comparison process)" : "")+"
(None)
Warnings about the comparison
"+vm.getLocation()+""+vm.getHtml()+"
Notes about differences (e.g. definitions)
"+vm.getLocation()+""+vm.getHtml()+"
\r\n"); + return b.toString(); + } + + private String genCompModel(StructureDefinition sd, String name, String base, String prefix, String dest) throws FHIRException, IOException { + if (sd == null) + return "

No "+name+" could be generated

\r\n"; + return new XhtmlComposer(XhtmlComposer.HTML).compose(new ProfileUtilities(context, null, this).generateTable("?gen-cm?", sd, false, dest, false, base, true, prefix, prefix, false, false, null, true)); + } + + + public String generate() throws IOException { + for (ValueSet vs : valuesets) { + vs.setUserData("path", folder+"/"+getId()+"-vs-"+vs.getId()+".html"); + } + // ok, all compared; now produce the output + // first page we produce is simply the index + Map vars = new HashMap(); + vars.put("title", getTitle()); + vars.put("left", genPCLink(getLeftName(), getLeftLink(), getLeftPrefix())); + vars.put("right", genPCLink(getRightName(), getRightLink(), getRightPrefix())); + vars.put("table", genPCTable()); + vars.put("valuesets", genValueSets(folder+"/"+getId()+"-vs")); + producePage(summaryTemplate(), Utilities.path(folder, getId()+".html"), vars); + + // then we produce a comparison page for each pair + for (ProfileComparison cmp : getComparisons()) { + vars.clear(); + vars.put("title", getTitle()); + vars.put("left", genPCLink(getLeftName(), getLeftLink(), getLeftPrefix())); + vars.put("right", genPCLink(getRightName(), getRightLink(), getRightPrefix())); + vars.put("messages", genCmpMessages(cmp)); + vars.put("subset", genCompModel(cmp.getSubset(), "intersection", getId()+"."+cmp.getId(), "", folder)); + vars.put("superset", genCompModel(cmp.getSuperset(), "union", getId()+"."+cmp.getId(), "", folder)); + producePage(singleTemplate(), Utilities.path(folder, getId()+"."+cmp.getId()+".html"), vars); + } +// // and also individual pages for each pair outcome +// // then we produce value set pages for each value set +// +// // TODO Auto-generated method stub + return Utilities.path(folder, getId()+".html"); + } + + + private void producePage(String src, String path, Map vars) throws IOException { + while (src.contains("[%")) + { + int i1 = src.indexOf("[%"); + int i2 = src.substring(i1).indexOf("%]")+i1; + String s1 = src.substring(0, i1); + String s2 = src.substring(i1 + 2, i2).trim(); + String s3 = src.substring(i2+2); + String v = vars.containsKey(s2) ? vars.get(s2) : "?pp??"; + src = s1+v+s3; + } + TextFile.stringToFile(src, path); + } + + private String summaryTemplate() throws IOException { + return TextFile.fileToString(Utilities.path(folder, "template-comparison-set.html")); + } + + private String singleTemplate() throws IOException { + return TextFile.fileToString(Utilities.path(folder, "template-comparison.html")); + } + + private String cachedFetch(String id, String source) throws IOException { + String tmpDir = System.getProperty("java.io.tmpdir"); + String local = Utilities.path(tmpDir, id); + File f = new File(local); + if (f.exists()) + return TextFile.fileToString(f); + URL url = new URL(source); + URLConnection c = url.openConnection(); + String result = TextFile.streamToString(c.getInputStream()); + TextFile.stringToFile(result, f); + return result; + } + + @Override + public boolean isDatatype(String typeSimple) { + throw new Error("Not done yet"); + } + + @Override + public boolean isResource(String typeSimple) { + throw new Error("Not done yet"); + } + + @Override + public boolean hasLinkFor(String name) { + StructureDefinition sd = context.fetchTypeDefinition(name); + return sd != null && sd.hasUserData("path"); + } + + @Override + public String getLinkFor(String corePath, String name) { + StructureDefinition sd = context.fetchTypeDefinition(name); + return sd == null ? null : sd.getUserString("path"); + } + + @Override + public BindingResolution resolveBinding(StructureDefinition def, ElementDefinitionBindingComponent binding, String path) throws FHIRException { + return resolveBindingInt(def, binding.getValueSet(), binding.getDescription()); + } + + @Override + public BindingResolution resolveBinding(StructureDefinition def, String url, String path) throws FHIRException { + return resolveBindingInt(def, url, url); + } + + public BindingResolution resolveBindingInt(StructureDefinition def, String url, String desc) throws FHIRException { + ValueSet vs = null; + if (url != null && url.startsWith("#")) { + for (ValueSet t : valuesets) { + if (("#"+t.getId()).equals(url)) { + vs = t; + break; + } + } + } + if (url != null && vs == null) + context.fetchResource(ValueSet.class, url); + BindingResolution br = new BindingResolution(); + if (vs != null) { + br.display = vs.present(); + br.url = vs.getUserString("path"); + } else { + br.display = desc; + } + return br; + } + + @Override + public String getLinkForProfile(StructureDefinition profile, String url) { + StructureDefinition sd = context.fetchResource(StructureDefinition.class, url); + return sd == null ? null : sd.getUserString("path")+"|"+sd.present(); + } + + @Override + public boolean prependLinks() { + return false; + } + + @Override + public String getLinkForUrl(String corePath, String s) { + return null; + } + + public int getErrCount() { + int res = 0; + for (ProfileComparison pc : comparisons) { + res = res + pc.getErrorCount(); + } + return res; + + } + + + + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ProfileComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ProfileComparer.java new file mode 100644 index 000000000..e650fa2dd --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ProfileComparer.java @@ -0,0 +1,814 @@ +package org.hl7.fhir.r5.comparison; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.comparison.CanonicalResourceComparer.CanonicalResourceComparison; +import org.hl7.fhir.r5.comparison.CodeSystemComparer.CodeSystemComparison; +import org.hl7.fhir.r5.comparison.OldProfileComparer.ProfileComparison; +import org.hl7.fhir.r5.comparison.ValueSetComparer.ValueSetComparison; +import org.hl7.fhir.r5.conformance.ProfileUtilities; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.CodeSystem; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.DataType; +import org.hl7.fhir.r5.model.ElementDefinition; +import org.hl7.fhir.r5.model.IntegerType; +import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r5.model.ElementDefinition.DiscriminatorType; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionConstraintComponent; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionMappingComponent; +import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingComponent; +import org.hl7.fhir.r5.model.ElementDefinition.SlicingRules; +import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; +import org.hl7.fhir.r5.model.Enumerations.BindingStrength; +import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; +import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; +import org.hl7.fhir.r5.utils.DefinitionNavigator; +import org.hl7.fhir.r5.model.ValueSet; + +public class ProfileComparer extends CanonicalResourceComparer { + + private static final int BOTH_NULL = 0; + private static final int EITHER_NULL = 1; + + public class ProfileComparison extends CanonicalResourceComparison { + + private StructuralMatch combined; + + public ProfileComparison(StructureDefinition left, StructureDefinition right) { + super(left, right); + combined = new StructuralMatch(); // base + } + + public StructuralMatch getCombined() { + return combined; + } + } + + public ProfileComparer(IWorkerContext context) { + super(context); + } + + @Override + protected String fhirType() { + return "StructureDefinition"; + } + + public ProfileComparison compare(StructureDefinition left, StructureDefinition right) throws DefinitionException, FHIRFormatError, IOException { + check(left, "left"); + check(right, "right"); + + ProfileComparison res = new ProfileComparison(left, right); + StructureDefinition sd = new StructureDefinition(); + res.setUnion(sd); + sd.setId(UUID.randomUUID().toString().toLowerCase()); + sd.setUrl("urn:uuid:"+sd.getId()); + sd.setName("Union"+left.getName()+"And"+right.getName()); + sd.setTitle("Union of "+left.getTitle()+" And "+right.getTitle()); + sd.setStatus(left.getStatus()); + sd.setDate(new Date()); + + StructureDefinition sd1 = new StructureDefinition(); + res.setIntersection(sd1); + sd1.setId(UUID.randomUUID().toString().toLowerCase()); + sd1.setUrl("urn:uuid:"+sd1.getId()); + sd1.setName("Intersection"+left.getName()+"And"+right.getName()); + sd1.setTitle("Intersection of "+left.getTitle()+" And "+right.getTitle()); + sd1.setStatus(left.getStatus()); + sd1.setDate(new Date()); + + compareMetadata(left, right, res.getMetadata(), res); + comparePrimitives("fhirVersion", left.getFhirVersionElement(), right.getFhirVersionElement(), res.getMetadata(), IssueSeverity.WARNING, res); + comparePrimitives("kind", left.getKindElement(), right.getKindElement(), res.getMetadata(), IssueSeverity.WARNING, res); + comparePrimitives("abstract", left.getAbstractElement(), right.getAbstractElement(), res.getMetadata(), IssueSeverity.WARNING, res); + comparePrimitives("type", left.getTypeElement(), right.getTypeElement(), res.getMetadata(), IssueSeverity.ERROR, res); + comparePrimitives("baseDefinition", left.getBaseDefinitionElement(), right.getBaseDefinitionElement(), res.getMetadata(), IssueSeverity.ERROR, res); + + if (left.getType().equals(right.getType())) { + DefinitionNavigator ln = new DefinitionNavigator(context, left); + DefinitionNavigator rn = new DefinitionNavigator(context, right); + // StructuralMatch res = new StructuralMatch(left.current(), right.current()); + // compareElements(res, res.getCombined(), ln.path(), null, ln, rn, sd, sd1); + } + return res; + } + + private void check(StructureDefinition sd, String name) { + if (sd == null) + throw new DefinitionException("No StructureDefinition provided ("+name+": "+sd.getName()+")"); + if (sd.getType().equals("Extension")) { + throw new DefinitionException("StructureDefinition is for an extension - use ExtensionComparer instead ("+name+": "+sd.getName()+")"); + } + if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { + throw new DefinitionException("StructureDefinition is not for an profile - can't be compared ("+name+": "+sd.getName()+")"); + } + if (sd.getSnapshot().getElement().isEmpty()) + throw new DefinitionException("StructureDefinition snapshot is empty ("+name+": "+sd.getName()+")"); + } + + private void compareElements(ProfileComparison comp, StructuralMatch res, String path, String sliceName, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, FHIRFormatError, IOException { + assert(path != null); + assert(left != null); + assert(right != null); + assert(left.path().equals(right.path())); + + // not allowed to be different: + ruleCompares(comp, res, left.current().getDefaultValue(), right.current().getDefaultValue(), path+".defaultValue[x]", BOTH_NULL); + ruleEqual(comp, res, path, left.current().getMeaningWhenMissing(), right.current().getMeaningWhenMissing(), "meaningWhenMissing Must be the same", true); + ruleEqual(comp, res, left.current().getIsModifier(), right.current().getIsModifier(), path, "isModifier"); + ruleEqual(comp, res, left.current().getIsSummary(), right.current().getIsSummary(), path, "isSummary"); + + // we ignore slicing right now - we're going to clone the root one anyway, and then think about clones + // simple stuff + ElementDefinition subset = new ElementDefinition(); + subset.setPath(left.path()); + if (sliceName != null) + subset.setSliceName(sliceName); + + + subset.getRepresentation().addAll(left.current().getRepresentation()); // can't be bothered even testing this one + subset.setDefaultValue(left.current().getDefaultValue()); + subset.setMeaningWhenMissing(left.current().getMeaningWhenMissing()); + subset.setIsModifier(left.current().getIsModifier()); + subset.setIsSummary(left.current().getIsSummary()); + + // descriptive properties from ElementDefinition - merge them: + subset.setLabel(mergeText(comp, res, path, "label", left.current().getLabel(), right.current().getLabel())); + subset.setShort(mergeText(comp, res, path, "short", left.current().getShort(), right.current().getShort())); + subset.setDefinition(mergeText(comp, res, path, "definition", left.current().getDefinition(), right.current().getDefinition())); + subset.setComment(mergeText(comp, res, path, "comments", left.current().getComment(), right.current().getComment())); + subset.setRequirements(mergeText(comp, res, path, "requirements", left.current().getRequirements(), right.current().getRequirements())); + subset.getCode().addAll(mergeCodings(left.current().getCode(), right.current().getCode())); + subset.getAlias().addAll(mergeStrings(left.current().getAlias(), right.current().getAlias())); + subset.getMapping().addAll(mergeMappings(left.current().getMapping(), right.current().getMapping())); + // left will win for example + subset.setExample(left.current().hasExample() ? left.current().getExample() : right.current().getExample()); + + subset.setMustSupport(left.current().getMustSupport() || right.current().getMustSupport()); + ElementDefinition superset = subset.copy(); + + + // compare and intersect + superset.setMin(unionMin(left.current().getMin(), right.current().getMin())); + superset.setMax(unionMax(left.current().getMax(), right.current().getMax())); + subset.setMin(intersectMin(left.current().getMin(), right.current().getMin())); + subset.setMax(intersectMax(left.current().getMax(), right.current().getMax())); + rule(comp, res, subset.getMax().equals("*") || Integer.parseInt(subset.getMax()) >= subset.getMin(), path, "Cardinality Mismatch: "+card(left)+"/"+card(right)); + + superset.getType().addAll(unionTypes(path, left.current().getType(), right.current().getType())); + subset.getType().addAll(intersectTypes(comp, res, subset, path, left.current().getType(), right.current().getType())); + rule(comp, res, !subset.getType().isEmpty() || (!left.current().hasType() && !right.current().hasType()), path, "Type Mismatch:\r\n "+typeCode(left)+"\r\n "+typeCode(right)); + // + // + superset.setMaxLengthElement(unionMaxLength(left.current().getMaxLength(), right.current().getMaxLength())); + subset.setMaxLengthElement(intersectMaxLength(left.current().getMaxLength(), right.current().getMaxLength())); + if (left.current().hasBinding() || right.current().hasBinding()) { + compareBindings(comp, res, subset, superset, path, left.current(), right.current()); + } + // note these are backwards + superset.getConstraint().addAll(intersectConstraints(path, left.current().getConstraint(), right.current().getConstraint())); + subset.getConstraint().addAll(unionConstraints(comp, res, path, left.current().getConstraint(), right.current().getConstraint())); + comp.getIntersection().getSnapshot().getElement().add(subset); + comp.getUnion().getSnapshot().getElement().add(superset); + + // add the children + compareChildren(comp, res, path, left, right); +// +// // now process the slices +// if (left.current().hasSlicing() || right.current().hasSlicing()) { +// assert sliceName == null; +// if (isExtension(left.path())) +// return compareExtensions(outcome, path, superset, subset, left, right); +// // return true; +// else { +// ElementDefinitionSlicingComponent slicingL = left.current().getSlicing(); +// ElementDefinitionSlicingComponent slicingR = right.current().getSlicing(); +// // well, this is tricky. If one is sliced, and the other is not, then in general, the union just ignores the slices, and the intersection is the slices. +// if (left.current().hasSlicing() && !right.current().hasSlicing()) { +// // the super set is done. Any restrictions in the slices are irrelevant to what the super set says, except that we're going sum up the value sets if we can (for documentation purposes) (todo) +// // the minimum set is the slicing specified in the slicer +// subset.setSlicing(slicingL); +// // stick everything from the right to do with the slices to the subset +// copySlices(outcome.subset.getSnapshot().getElement(), left.getStructure().getSnapshot().getElement(), left.slices()); +// } else if (!left.current().hasSlicing() && right.current().hasSlicing()) { +// // the super set is done. Any restrictions in the slices are irrelevant to what the super set says, except that we're going sum up the value sets if we can (for documentation purposes) (todo) +// // the minimum set is the slicing specified in the slicer +// subset.setSlicing(slicingR); +// // stick everything from the right to do with the slices to the subset +// copySlices(outcome.subset.getSnapshot().getElement(), right.getStructure().getSnapshot().getElement(), right.slices()); +// } else if (isTypeSlicing(slicingL) || isTypeSlicing(slicingR)) { +// superset.getSlicing().setRules(SlicingRules.OPEN).setOrdered(false).addDiscriminator().setType(DiscriminatorType.TYPE).setPath("$this"); +// subset.getSlicing().setRules(slicingL.getRules() == SlicingRules.CLOSED || slicingR.getRules() == SlicingRules.CLOSED ? SlicingRules.OPEN : SlicingRules.CLOSED).setOrdered(false).addDiscriminator().setType(DiscriminatorType.TYPE).setPath("$this"); +// +// // the superset is the union of the types +// // the subset is the intersection of them +// List handled = new ArrayList<>(); +// for (DefinitionNavigator t : left.slices()) { +// DefinitionNavigator r = findMatchingSlice(right.slices(), t); +// if (r == null) { +// copySlice(outcome.superset.getSnapshot().getElement(), left.getStructure().getSnapshot().getElement(), t); +// } else { +// handled.add(r); +// ret = compareElements(outcome, path+":"+t.current().getSliceName(), t, r, t.current().getSliceName()) && ret; +// } +// } +// for (DefinitionNavigator t : right.slices()) { +// if (!handled.contains(t)) { +// copySlice(outcome.superset.getSnapshot().getElement(), right.getStructure().getSnapshot().getElement(), t); +// } +// } +// } else if (slicingMatches(slicingL, slicingR)) { +// // if it's the same, we can try matching the slices - though we might have to give up without getting matches correct +// // there amy be implied consistency we can't reason about +// throw new DefinitionException("Slicing matches but is not handled yet at "+left.current().getId()+": ("+ProfileUtilities.summarizeSlicing(slicingL)+")"); +// } else { +// // if the slicing is different, we can't compare them - or can we? +// throw new DefinitionException("Slicing doesn't match at "+left.current().getId()+": ("+ProfileUtilities.summarizeSlicing(slicingL)+" / "+ProfileUtilities.summarizeSlicing(slicingR)+")"); +// } +// } +// // todo: name +// } +// return ret; +// +// // TODO Auto-generated method stub +// return null; + } + + private void compareChildren(ProfileComparison comp, StructuralMatch res, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError { + List lc = left.children(); + List rc = right.children(); + // it's possible that one of these profiles walks into a data type and the other doesn't + // if it does, we have to load the children for that data into the profile that doesn't + // walk into it + if (lc.isEmpty() && !rc.isEmpty() && right.current().getType().size() == 1 && left.hasTypeChildren(right.current().getType().get(0))) + lc = left.childrenFromType(right.current().getType().get(0)); + if (rc.isEmpty() && !lc.isEmpty() && left.current().getType().size() == 1 && right.hasTypeChildren(left.current().getType().get(0))) + rc = right.childrenFromType(left.current().getType().get(0)); + + List matchR = new ArrayList<>(); + for (DefinitionNavigator l : lc) { + DefinitionNavigator r = findInList(rc, l); + if (r == null) { + comp.getUnion().getSnapshot().getElement().add(l.current().copy()); + res.getChildren().add(new StructuralMatch(l.current(), vm(IssueSeverity.INFORMATION, "Removed this element", path))); + } else { + matchR.add(r); + StructuralMatch sm = new StructuralMatch(l.current(), r.current()); + res.getChildren().add(sm); + compareElements(comp, sm, l.path(), null, left, right); + } + } + for (DefinitionNavigator r : rc) { + if (!matchR.contains(r)) { + comp.getUnion().getSnapshot().getElement().add(r.current().copy()); + res.getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Added this element", path), r.current())); + } + } + } + + private DefinitionNavigator findInList(List rc, DefinitionNavigator l) { + // TODO: fix + return null; + } + + private boolean ruleCompares(ProfileComparison comp, StructuralMatch res, DataType vLeft, DataType vRight, String path, int nullStatus) throws IOException { + // TODO: fix +// if (vLeft == null && vRight == null && nullStatus == BOTH_NULL) +// return true; +// if (vLeft == null && vRight == null) { +// res.getMessages().add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Must be the same and not null (null/null)", ValidationMessage.IssueSeverity.ERROR)); +// status(ed, ProfileUtilities.STATUS_ERROR); +// } +// if (vLeft == null && nullStatus == EITHER_NULL) +// return true; +// if (vRight == null && nullStatus == EITHER_NULL) +// return true; +// if (vLeft == null || vRight == null || !Base.compareDeep(vLeft, vRight, false)) { +// res.getMessages().add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Must be the same ("+toString(vLeft)+"/"+toString(vRight)+")", ValidationMessage.IssueSeverity.ERROR)); +// status(ed, ProfileUtilities.STATUS_ERROR); +// } + return true; + } + + private boolean rule(ProfileComparison comp, StructuralMatch res, boolean test, String path, String message) { + // TODO: fix +// if (!test) { +// messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, message, ValidationMessage.IssueSeverity.ERROR)); +// status(ed, ProfileUtilities.STATUS_ERROR); +// } + return test; + } + + + private boolean ruleEqual(ProfileComparison comp, StructuralMatch res, boolean vLeft, boolean vRight, String path, String elementName) { + // TODO: fix +// if (vLeft != vRight) { +// res.getMessages().add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, elementName+" must be the same ("+vLeft+"/"+vRight+")", ValidationMessage.IssueSeverity.ERROR)); +// status(ed, ProfileUtilities.STATUS_ERROR); +// } + return true; + } + + private boolean ruleEqual(ProfileComparison comp, StructuralMatch res, String path, String vLeft, String vRight, String description, boolean nullOK) { + // TODO: fix +// if (vLeft == null && vRight == null && nullOK) +// return true; +// if (vLeft == null && vRight == null) { +// res.getMessages().add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, description+" and not null (null/null)", ValidationMessage.IssueSeverity.ERROR)); +// if (ed != null) +// status(ed, ProfileUtilities.STATUS_ERROR); +// } +// if (vLeft == null || !vLeft.equals(vRight)) { +// res.getMessages().add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, description+" ("+vLeft+"/"+vRight+")", ValidationMessage.IssueSeverity.ERROR)); +// if (ed != null) +// status(ed, ProfileUtilities.STATUS_ERROR); +// } + return true; + } + + private String mergeText(ProfileComparison outcome, StructuralMatch sm, String path, String name, String left, String right) { + // TODO: fix +// if (left == null && right == null) +// return null; +// if (left == null) +// return right; +// if (right == null) +// return left; +// left = stripLinks(left); +// right = stripLinks(right); +// if (left.equalsIgnoreCase(right)) +// return left; +// if (path != null) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, path, "Elements differ in definition for "+name+":\r\n \""+left+"\"\r\n \""+right+"\"", +// "Elements differ in definition for "+name+":
\""+Utilities.escapeXml(left)+"\"
\""+Utilities.escapeXml(right)+"\"", ValidationMessage.IssueSeverity.INFORMATION)); +// status(ed, ProfileUtilities.STATUS_HINT); +// } + return "left: "+left+"; right: "+right; + } + + + private List mergeCodings(List left, List right) { + List result = new ArrayList(); + result.addAll(left); + for (Coding c : right) { + boolean found = false; + for (Coding ct : left) + if (Utilities.equals(c.getSystem(), ct.getSystem()) && Utilities.equals(c.getCode(), ct.getCode())) + found = true; + if (!found) + result.add(c); + } + return result; + } + + + private List mergeStrings(List left, List right) { + List result = new ArrayList(); + result.addAll(left); + for (StringType c : right) { + boolean found = false; + for (StringType ct : left) + if (Utilities.equals(c.getValue(), ct.getValue())) + found = true; + if (!found) + result.add(c); + } + return result; + } + + private List mergeMappings(List left, List right) { + List result = new ArrayList(); + result.addAll(left); + for (ElementDefinitionMappingComponent c : right) { + boolean found = false; + for (ElementDefinitionMappingComponent ct : left) + if (Utilities.equals(c.getIdentity(), ct.getIdentity()) && Utilities.equals(c.getLanguage(), ct.getLanguage()) && Utilities.equals(c.getMap(), ct.getMap())) + found = true; + if (!found) + result.add(c); + } + return result; + } + + private int intersectMin(int left, int right) { + if (left > right) + return left; + else + return right; + } + + private int unionMin(int left, int right) { + if (left > right) + return right; + else + return left; + } + + private String intersectMax(String left, String right) { + int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left); + int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right); + if (l < r) + return left; + else + return right; + } + + private String unionMax(String left, String right) { + int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left); + int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right); + if (l < r) + return right; + else + return left; + } + + private IntegerType intersectMaxLength(int left, int right) { + if (left == 0) + left = Integer.MAX_VALUE; + if (right == 0) + right = Integer.MAX_VALUE; + if (left < right) + return left == Integer.MAX_VALUE ? null : new IntegerType(left); + else + return right == Integer.MAX_VALUE ? null : new IntegerType(right); + } + + private IntegerType unionMaxLength(int left, int right) { + if (left == 0) + left = Integer.MAX_VALUE; + if (right == 0) + right = Integer.MAX_VALUE; + if (left < right) + return right == Integer.MAX_VALUE ? null : new IntegerType(right); + else + return left == Integer.MAX_VALUE ? null : new IntegerType(left); + } + + private String card(DefinitionNavigator defn) { + return Integer.toString(defn.current().getMin())+".."+defn.current().getMax(); + } + + + private Collection unionTypes(String path, List left, List right) throws DefinitionException, IOException, FHIRFormatError { + List result = new ArrayList(); + for (TypeRefComponent l : left) + checkAddTypeUnion(path, result, l); + for (TypeRefComponent r : right) + checkAddTypeUnion(path, result, r); + return result; + } + + private void checkAddTypeUnion(String path, List results, TypeRefComponent nw) throws DefinitionException, IOException, FHIRFormatError { + // TODO: fix +// boolean pfound = false; +// boolean tfound = false; +// nw = nw.copy(); +// if (nw.hasAggregation()) +// throw new DefinitionException("Aggregation not supported: "+path); +// for (TypeRefComponent ex : results) { +// if (Utilities.equals(ex.getWorkingCode(), nw.getWorkingCode())) { +// if (!ex.hasProfile() && !nw.hasProfile()) +// pfound = true; +// else if (!ex.hasProfile()) { +// pfound = true; +// } else if (!nw.hasProfile()) { +// pfound = true; +// ex.setProfile(null); +// } else { +// // both have profiles. Is one derived from the other? +// StructureDefinition sdex = context.fetchResource(StructureDefinition.class, ex.getProfile().get(0).getValue()); +// StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, nw.getProfile().get(0).getValue()); +// if (sdex != null && sdnw != null) { +// if (sdex == sdnw) { +// pfound = true; +// } else if (derivesFrom(sdex, sdnw)) { +// ex.setProfile(nw.getProfile()); +// pfound = true; +// } else if (derivesFrom(sdnw, sdex)) { +// pfound = true; +// } else if (sdnw.getSnapshot().getElement().get(0).getPath().equals(sdex.getSnapshot().getElement().get(0).getPath())) { +// ProfileComparison comp = compareProfiles(sdex, sdnw); +// if (comp.getSuperset() != null) { +// pfound = true; +// ex.addProfile("#"+comp.id); +// } +// } +// } +// } +// if (!ex.hasTargetProfile() && !nw.hasTargetProfile()) +// tfound = true; +// else if (!ex.hasTargetProfile()) { +// tfound = true; +// } else if (!nw.hasTargetProfile()) { +// tfound = true; +// ex.setTargetProfile(null); +// } else { +// // both have profiles. Is one derived from the other? +// StructureDefinition sdex = context.fetchResource(StructureDefinition.class, ex.getTargetProfile().get(0).getValue()); +// StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, nw.getTargetProfile().get(0).getValue()); +// if (sdex != null && sdnw != null) { +// if (sdex == sdnw) { +// tfound = true; +// } else if (derivesFrom(sdex, sdnw)) { +// ex.setTargetProfile(nw.getTargetProfile()); +// tfound = true; +// } else if (derivesFrom(sdnw, sdex)) { +// tfound = true; +// } else if (sdnw.getSnapshot().getElement().get(0).getPath().equals(sdex.getSnapshot().getElement().get(0).getPath())) { +// ProfileComparison comp = compareProfiles(sdex, sdnw); +// if (comp.getSuperset() != null) { +// tfound = true; +// ex.addTargetProfile("#"+comp.id); +// } +// } +// } +// } +// } +// } +// if (!tfound || !pfound) +// results.add(nw); + } + + + private boolean derivesFrom(StructureDefinition left, StructureDefinition right) { + // left derives from right if it's base is the same as right + // todo: recursive... + return left.hasBaseDefinition() && left.getBaseDefinition().equals(right.getUrl()); + } + + + private Collection intersectTypes(ProfileComparison comp, StructuralMatch res, ElementDefinition ed, String path, List left, List right) throws DefinitionException, IOException, FHIRFormatError { + // TODO: fix + List result = new ArrayList(); +// for (TypeRefComponent l : left) { +// if (l.hasAggregation()) +// throw new DefinitionException("Aggregation not supported: "+path); +// boolean pfound = false; +// boolean tfound = false; +// TypeRefComponent c = l.copy(); +// for (TypeRefComponent r : right) { +// if (r.hasAggregation()) +// throw new DefinitionException("Aggregation not supported: "+path); +// if (!l.hasProfile() && !r.hasProfile()) { +// pfound = true; +// } else if (!r.hasProfile()) { +// pfound = true; +// } else if (!l.hasProfile()) { +// pfound = true; +// c.setProfile(r.getProfile()); +// } else { +// StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), outcome.leftName()); +// StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), outcome.rightName()); +// if (sdl != null && sdr != null) { +// if (sdl == sdr) { +// pfound = true; +// } else if (derivesFrom(sdl, sdr)) { +// pfound = true; +// } else if (derivesFrom(sdr, sdl)) { +// c.setProfile(r.getProfile()); +// pfound = true; +// } else if (sdl.getType().equals(sdr.getType())) { +// ProfileComparison comp = compareProfiles(sdl, sdr); +// if (comp.getSubset() != null) { +// pfound = true; +// c.addProfile("#"+comp.id); +// } +// } +// } +// } +// if (!l.hasTargetProfile() && !r.hasTargetProfile()) { +// tfound = true; +// } else if (!r.hasTargetProfile()) { +// tfound = true; +// } else if (!l.hasTargetProfile()) { +// tfound = true; +// c.setTargetProfile(r.getTargetProfile()); +// } else { +// StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getTargetProfile().get(0).getValue(), outcome.leftName()); +// StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getTargetProfile().get(0).getValue(), outcome.rightName()); +// if (sdl != null && sdr != null) { +// if (sdl == sdr) { +// tfound = true; +// } else if (derivesFrom(sdl, sdr)) { +// tfound = true; +// } else if (derivesFrom(sdr, sdl)) { +// c.setTargetProfile(r.getTargetProfile()); +// tfound = true; +// } else if (sdl.getType().equals(sdr.getType())) { +// ProfileComparison comp = compareProfiles(sdl, sdr); +// if (comp.getSubset() != null) { +// tfound = true; +// c.addTargetProfile("#"+comp.id); +// } +// } +// } +// } +// } +// if (pfound && tfound) +// result.add(c); +// } + return result; + } + + private String typeCode(DefinitionNavigator defn) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (TypeRefComponent t : defn.current().getType()) + b.append(t.getWorkingCode()+(t.hasProfile() ? "("+t.getProfile()+")" : "")+(t.hasTargetProfile() ? "("+t.getTargetProfile()+")" : "")); // todo: other properties + return b.toString(); + } + + + private boolean compareBindings(ProfileComparison comp, StructuralMatch res, ElementDefinition subset, ElementDefinition superset, String path, ElementDefinition lDef, ElementDefinition rDef) throws FHIRFormatError { + // TODO: fix +// assert(lDef.hasBinding() || rDef.hasBinding()); +// if (!lDef.hasBinding()) { +// subset.setBinding(rDef.getBinding()); +// // technically, the super set is unbound, but that's not very useful - so we use the provided on as an example +// superset.setBinding(rDef.getBinding().copy()); +// superset.getBinding().setStrength(BindingStrength.EXAMPLE); +// return true; +// } +// if (!rDef.hasBinding()) { +// subset.setBinding(lDef.getBinding()); +// superset.setBinding(lDef.getBinding().copy()); +// superset.getBinding().setStrength(BindingStrength.EXAMPLE); +// return true; +// } +// ElementDefinitionBindingComponent left = lDef.getBinding(); +// ElementDefinitionBindingComponent right = rDef.getBinding(); +// if (Base.compareDeep(left, right, false)) { +// subset.setBinding(left); +// superset.setBinding(right); +// } +// +// // if they're both examples/preferred then: +// // subset: left wins if they're both the same +// // superset: +// if (isPreferredOrExample(left) && isPreferredOrExample(right)) { +// if (right.getStrength() == BindingStrength.PREFERRED && left.getStrength() == BindingStrength.EXAMPLE && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Example/preferred bindings differ at "+path+" using binding from "+outcome.rightName(), ValidationMessage.IssueSeverity.INFORMATION)); +// status(subset, ProfileUtilities.STATUS_HINT); +// subset.setBinding(right); +// superset.setBinding(unionBindings(superset, outcome, path, left, right)); +// } else { +// if ((right.getStrength() != BindingStrength.EXAMPLE || left.getStrength() != BindingStrength.EXAMPLE) && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false) ) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Example/preferred bindings differ at "+path+" using binding from "+outcome.leftName(), ValidationMessage.IssueSeverity.INFORMATION)); +// status(subset, ProfileUtilities.STATUS_HINT); +// } +// subset.setBinding(left); +// superset.setBinding(unionBindings(superset, outcome, path, left, right)); +// } +// return true; +// } +// // if either of them are extensible/required, then it wins +// if (isPreferredOrExample(left)) { +// subset.setBinding(right); +// superset.setBinding(unionBindings(superset, outcome, path, left, right)); +// return true; +// } +// if (isPreferredOrExample(right)) { +// subset.setBinding(left); +// superset.setBinding(unionBindings(superset, outcome, path, left, right)); +// return true; +// } +// +// // ok, both are extensible or required. +// ElementDefinitionBindingComponent subBinding = new ElementDefinitionBindingComponent(); +// subset.setBinding(subBinding); +// ElementDefinitionBindingComponent superBinding = new ElementDefinitionBindingComponent(); +// superset.setBinding(superBinding); +// subBinding.setDescription(mergeText(subset, outcome, path, "description", left.getDescription(), right.getDescription())); +// superBinding.setDescription(mergeText(subset, outcome, null, "description", left.getDescription(), right.getDescription())); +// if (left.getStrength() == BindingStrength.REQUIRED || right.getStrength() == BindingStrength.REQUIRED) +// subBinding.setStrength(BindingStrength.REQUIRED); +// else +// subBinding.setStrength(BindingStrength.EXTENSIBLE); +// if (left.getStrength() == BindingStrength.EXTENSIBLE || right.getStrength() == BindingStrength.EXTENSIBLE) +// superBinding.setStrength(BindingStrength.EXTENSIBLE); +// else +// superBinding.setStrength(BindingStrength.REQUIRED); +// +// if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { +// subBinding.setValueSet(left.getValueSet()); +// superBinding.setValueSet(left.getValueSet()); +// return true; +// } else if (!left.hasValueSet()) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "No left Value set at "+path, ValidationMessage.IssueSeverity.ERROR)); +// return true; +// } else if (!right.hasValueSet()) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "No right Value set at "+path, ValidationMessage.IssueSeverity.ERROR)); +// return true; +// } else { +// // ok, now we compare the value sets. This may be unresolvable. +// ValueSet lvs = resolveVS(outcome.left, left.getValueSet()); +// ValueSet rvs = resolveVS(outcome.right, right.getValueSet()); +// if (lvs == null) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to resolve left value set "+left.getValueSet().toString()+" at "+path, ValidationMessage.IssueSeverity.ERROR)); +// return true; +// } else if (rvs == null) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to resolve right value set "+right.getValueSet().toString()+" at "+path, ValidationMessage.IssueSeverity.ERROR)); +// return true; +// } else { +// // first, we'll try to do it by definition +// ValueSet cvs = intersectByDefinition(lvs, rvs); +// if(cvs == null) { +// // if that didn't work, we'll do it by expansion +// ValueSetExpansionOutcome le; +// ValueSetExpansionOutcome re; +// try { +// le = context.expandVS(lvs, true, false); +// re = context.expandVS(rvs, true, false); +// if (le.getError() != null) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value set "+lvs.getUrl()+" could not be expanded", ValidationMessage.IssueSeverity.ERROR)); +// } else if (re.getError() != null) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value set "+rvs.getUrl()+" could not be expanded", ValidationMessage.IssueSeverity.ERROR)); +// } else if (!closed(le.getValueset())) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value set "+lvs.getUrl()+" is not closed, so can't be compased", ValidationMessage.IssueSeverity.ERROR)); +// } else if (!closed(re.getValueset())) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value set "+rvs.getUrl()+" is not closed, so can't be compased", ValidationMessage.IssueSeverity.ERROR)); +// } else { +// cvs = intersectByExpansion(path, le.getValueset(), re.getValueset()); +// if (!cvs.getCompose().hasInclude()) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "The value sets "+lvs.getUrl()+" and "+rvs.getUrl()+" do not intersect", ValidationMessage.IssueSeverity.ERROR)); +// status(subset, ProfileUtilities.STATUS_ERROR); +// return false; +// } +// } +// } catch (Exception e){ +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Unable to expand or process value sets "+lvs.getUrl()+" and "+rvs.getUrl()+": "+e.getMessage(), ValidationMessage.IssueSeverity.ERROR)); +// status(subset, ProfileUtilities.STATUS_ERROR); +// e.printStackTrace(); +// return false; +// } +// } +// if (cvs != null) { +// subBinding.setValueSet("#"+addValueSet(cvs)); +// superBinding.setValueSet("#"+addValueSet(unite(superset, outcome, path, lvs, rvs))); +// } +// } +// } + return false; + } + + private List intersectConstraints(String path, List left, List right) { + List result = new ArrayList(); + for (ElementDefinitionConstraintComponent l : left) { + boolean found = false; + for (ElementDefinitionConstraintComponent r : right) + if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) + found = true; + if (found) + result.add(l); + } + return result; + } + + // we can't really know about constraints. We create warnings, and collate them + private List unionConstraints(ProfileComparison comp, StructuralMatch res, String path, List left, List right) { + // TODO: fix + List result = new ArrayList(); +// for (ElementDefinitionConstraintComponent l : left) { +// boolean found = false; +// for (ElementDefinitionConstraintComponent r : right) +// if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) +// found = true; +// if (!found) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "StructureDefinition "+outcome.leftName()+" has a constraint that is not found in "+outcome.rightName()+" and it is uncertain whether they are compatible ("+l.getXpath()+")", ValidationMessage.IssueSeverity.INFORMATION)); +// status(ed, ProfileUtilities.STATUS_WARNING); +// } +// result.add(l); +// } +// for (ElementDefinitionConstraintComponent r : right) { +// boolean found = false; +// for (ElementDefinitionConstraintComponent l : left) +// if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity())) +// found = true; +// if (!found) { +// outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "StructureDefinition "+outcome.rightName()+" has a constraint that is not found in "+outcome.leftName()+" and it is uncertain whether they are compatible ("+r.getXpath()+")", ValidationMessage.IssueSeverity.INFORMATION)); +// status(ed, ProfileUtilities.STATUS_WARNING); +// result.add(r); +// } +// } + return result; + } + + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ResourceComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ResourceComparer.java new file mode 100644 index 000000000..7b2c163c5 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ResourceComparer.java @@ -0,0 +1,112 @@ +package org.hl7.fhir.r5.comparison; + +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.r5.comparison.CodeSystemComparer.CodeSystemComparison; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; +import org.hl7.fhir.utilities.xhtml.NodeType; +import org.hl7.fhir.utilities.xhtml.XhtmlDocument; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece; + +public class ResourceComparer { + + public class ResourceCmparison { + protected List messages = new ArrayList<>(); + + public List getMessages() { + return messages; + } + } + + public final static String COLOR_NO_ROW_LEFT = "#ffffb3"; + public final static String COLOR_NO_CELL_LEFT = "#ffff4d"; + public final static String COLOR_NO_ROW_RIGHT = "#ffecb3"; + public final static String COLOR_NO_CELL_RIGHT = "#ffcc33"; + public final static String COLOR_DIFFERENT = "#f0b3ff"; + public final static String COLOR_ISSUE = "#ffad99"; + + protected IWorkerContext context; + + public ResourceComparer(IWorkerContext context) { + super(); + this.context = context; + } + + public Cell missingCell(HierarchicalTableGenerator gen) { + Cell c = gen.new Cell(null, null, "", null, null); + return c; + } + + public Cell missingCell(HierarchicalTableGenerator gen, String color) { + Cell c = gen.new Cell(null, null, "", null, null); + if (color != null) { + c.setStyle("background-color: "+color); + } + return c; + } + + public XhtmlNode renderErrors(ResourceCmparison csc) { + XhtmlNode div = new XhtmlNode(NodeType.Element, "div"); + XhtmlNode tbl = div.table("grid"); + for (ValidationMessage vm : csc.messages) { + XhtmlNode tr = tbl.tr(); + tr.td().tx(vm.getLocation()); + tr.td().tx(vm.getMessage()); + tr.td().tx(vm.getLevel().getDisplay()); + } + return div; + } + + + protected ValidationMessage vm(IssueSeverity level, String message, String path) { + return new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, message, level == IssueSeverity.NULL ? IssueSeverity.INFORMATION : level); + } + + private String colorForLevel(IssueSeverity level) { + switch (level) { + case ERROR: + return "#ffcccc"; + case FATAL: + return "#ff9999"; + case WARNING: + return "#ffebcc"; + default: // INFORMATION: + return "#ffffe6"; + } + } + + private String halfColorForLevel(IssueSeverity level) { + switch (level) { + case ERROR: + return "#ffeeee"; + case FATAL: + return "#ffcccc"; + case WARNING: + return "#fff4ee"; + default: // INFORMATION: + return "#fffff2"; + } + } + + protected Cell cellForMessages(HierarchicalTableGenerator gen, List messages) { + Cell cell = gen.new Cell(); + Piece piece = gen.new Piece("ul"); + cell.addPiece(piece); + for (ValidationMessage msg : messages) { + XhtmlNode li = new XhtmlNode(NodeType.Element, "li"); + piece.getChildren().add(li); + li.style("background-color: "+halfColorForLevel(msg.getLevel())); + li.tx(msg.getMessage()); + } + return cell; + } + +} 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 new file mode 100644 index 000000000..a37e374e1 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/StructuralMatch.java @@ -0,0 +1,81 @@ +package org.hl7.fhir.r5.comparison; + +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.utilities.validation.ValidationMessage; + +public class StructuralMatch { + + private T left; + private T right; + private List messages = new ArrayList<>(); + private List> children = new ArrayList<>(); + + public StructuralMatch() { + // root, just a place holder... + } + + public StructuralMatch(T left, T right) { + super(); + this.left = left; + this.right = right; + } + + public StructuralMatch(T left, T right, ValidationMessage msg) { + super(); + this.left = left; + this.right = right; + if (msg != null) { + this.messages.add(msg); + } + } + + public StructuralMatch(ValidationMessage msg, T right) { + super(); + this.messages.add(msg); + this.right = right; + } + + public StructuralMatch(T left, ValidationMessage msg) { + super(); + this.left = left; + this.messages.add(msg); + } + + public T getLeft() { + return left; + } + public T getRight() { + return right; + } + + public List> getChildren() { + return children; + } + + /** + * return left if it exists, or return right (which might be null) + * + * This is used when accessing whatever makes the items common + * + * @return + */ + public T either() { + return left != null ? left : right; + } + + public boolean hasLeft() { + return left != null; + } + + public boolean hasRight() { + return right != null; + } + + public List getMessages() { + return messages; + } + + +} \ 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 new file mode 100644 index 000000000..6bc7e68ab --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java @@ -0,0 +1,820 @@ +package org.hl7.fhir.r5.comparison; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.comparison.CodeSystemComparer.CodeSystemComparison; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.BackboneElement; +import org.hl7.fhir.r5.model.CanonicalType; +import org.hl7.fhir.r5.model.CodeSystem; +import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; +import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent; +import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent; +import org.hl7.fhir.r5.model.Element; +import org.hl7.fhir.r5.model.UriType; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; +import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; +import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; +import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent; +import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; +import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; +import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.validation.ValidationMessage.Source; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; +import org.hl7.fhir.utilities.xhtml.XhtmlDocument; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Title; +import org.hl7.fhir.utilities.xhtml.NodeType; + +public class ValueSetComparer extends CanonicalResourceComparer { + + public class ValueSetComparison extends CanonicalResourceComparison { + + public ValueSetComparison(ValueSet left, ValueSet right) { + super(left, right); + } + + private StructuralMatch includes = new StructuralMatch<>(); + private StructuralMatch excludes = new StructuralMatch<>(); + private StructuralMatch expansion; + + public StructuralMatch getIncludes() { + return includes; + } + + public StructuralMatch getExcludes() { + return excludes; + } + + public StructuralMatch getExpansion() { + return expansion; + } + + public StructuralMatch forceExpansion() { + if (expansion == null) { + expansion = new StructuralMatch<>(); + } + return expansion; + } + } + + public ValueSetComparer(IWorkerContext context) { + super(context); + this.context = context; + } + + public ValueSetComparison compare(ValueSet left, ValueSet right) { + if (left == null) + throw new DefinitionException("No ValueSet provided (left)"); + if (right == null) + throw new DefinitionException("No ValueSet provided (right)"); + + ValueSetComparison res = new ValueSetComparison(left, right); + ValueSet vs = new ValueSet(); + res.setUnion(vs); + vs.setId(UUID.randomUUID().toString().toLowerCase()); + vs.setUrl("urn:uuid:"+vs.getId()); + vs.setName("Union"+left.getName()+"And"+right.getName()); + vs.setTitle("Union of "+left.getTitle()+" And "+right.getTitle()); + vs.setStatus(left.getStatus()); + vs.setDate(new Date()); + + ValueSet vs1 = new ValueSet(); + res.setIntersection(vs1); + vs1.setId(UUID.randomUUID().toString().toLowerCase()); + vs1.setUrl("urn:uuid:"+vs1.getId()); + vs1.setName("Intersection"+left.getName()+"And"+right.getName()); + vs1.setTitle("Intersection of "+left.getTitle()+" And "+right.getTitle()); + 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); + 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); + } + + compareCompose(left.getCompose(), right.getCompose(), res, res.getUnion().getCompose(), res.getIntersection().getCompose()); + compareExpansions(left, right, res); + return res; + } + + + + private void compareCompose(ValueSetComposeComponent left, ValueSetComposeComponent right, ValueSetComparison res, ValueSetComposeComponent union, ValueSetComposeComponent intersection) { + // 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.getIncludes().getChildren().add(new StructuralMatch(l, vm(IssueSeverity.INFORMATION, "Removed Include", "ValueSet.compose.include"))); + } else { + matchR.add(r); + ConceptSetComponent csM = new ConceptSetComponent(); + ConceptSetComponent csI = new ConceptSetComponent(); + union.getInclude().add(csM); + intersection.getInclude().add(csI); + StructuralMatch sm = new StructuralMatch(l, r); + res.getIncludes().getChildren().add(sm); + compareDefinitions(l, r, sm, csM, csI); + } + } + for (ConceptSetComponent r : right.getInclude()) { + if (!matchR.contains(r)) { + union.getInclude().add(r); + res.getIncludes().getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Added Include", "ValueSet.compose.include"), r)); + } + } + + // now. the excludes + matchR.clear(); + for (ConceptSetComponent l : left.getExclude()) { + ConceptSetComponent r = findInList(right.getExclude(), l, left.getExclude()); + if (r == null) { + union.getExclude().add(l); + res.getExcludes().getChildren().add(new StructuralMatch(l, vm(IssueSeverity.INFORMATION, "Removed Exclude", "ValueSet.compose.exclude"))); + } else { + matchR.add(r); + ConceptSetComponent csM = new ConceptSetComponent(); + ConceptSetComponent csI = new ConceptSetComponent(); + union.getExclude().add(csM); + intersection.getExclude().add(csI); + StructuralMatch sm = new StructuralMatch(l, r); + res.getExcludes().getChildren().add(sm); + compareDefinitions(l, r, sm, csM, csI); + } + } + for (ConceptSetComponent r : right.getExclude()) { + if (!matchR.contains(r)) { + union.getExclude().add(r); + res.getExcludes().getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Added Exclude", "ValueSet.compose.exclude"), r)); + } + } + } + + private ConceptSetComponent findInList(List matches, ConceptSetComponent item, List source) { + if (matches.size() == 1 && source.size() == 1) { + return matches.get(0); + } + int matchCount = countMatchesBySystem(matches, item); + int sourceCount = countMatchesBySystem(source, item); + + if (matchCount == 1 && sourceCount == 1) { + for (ConceptSetComponent t : matches) { + if (t.getSystem().equals(item.getSystem())) { + return t; + } + } + } + // if there's more than one candidate match by system, then we look for a full match + for (ConceptSetComponent t : matches) { + if (t.equalsDeep(item)) { + return t; + } + } + return null; + } + + private int countMatchesBySystem(List list, ConceptSetComponent item) { + int c = 0; + for (ConceptSetComponent t : list) { + if (t.getSystem().equals(item.getSystem())) { + c++; + } + } + return c; + } + + + private void compareDefinitions(ConceptSetComponent left, ConceptSetComponent right, StructuralMatch combined, ConceptSetComponent union, ConceptSetComponent intersection) { + // 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); + combined.getChildren().add(new StructuralMatch(l, vm(IssueSeverity.INFORMATION, "Removed ValueSet", "ValueSet.compose.include.valueSet"))); + } else { + matchVSR.add(r); + if (l.getValue().equals(r.getValue())) { + union.getValueSet().add(l); + intersection.getValueSet().add(l); + StructuralMatch sm = new StructuralMatch(l, r, null); + combined.getChildren().add(sm); + } else { + union.getValueSet().add(l); + union.getValueSet().add(r); + StructuralMatch sm = new StructuralMatch(l, r, vm(IssueSeverity.INFORMATION, "Values are different", "ValueSet.compose.include.valueSet")); + combined.getChildren().add(sm); + } + } + } + for (CanonicalType r : right.getValueSet()) { + if (!matchVSR.contains(r)) { + union.getValueSet().add(r); + combined.getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Add ValueSet", "ValueSet.compose.include.valueSet"), r)); + } + } + + List matchCR = new ArrayList<>(); + for (ConceptReferenceComponent l : left.getConcept()) { + ConceptReferenceComponent r = findInList(right.getConcept(), l, left.getConcept()); + if (r == null) { + union.getConcept().add(l); + combined.getChildren().add(new StructuralMatch(l, vm(IssueSeverity.INFORMATION, "Removed this Concept", "ValueSet.compose.include.concept"))); + } else { + matchCR.add(r); + if (l.getCode().equals(r.getCode())) { + ConceptReferenceComponent cu = new ConceptReferenceComponent(); + ConceptReferenceComponent ci = new ConceptReferenceComponent(); + union.getConcept().add(cu); + intersection.getConcept().add(ci); + StructuralMatch sm = new StructuralMatch(l, r); + combined.getChildren().add(sm); + compareConcepts(l, r, sm, cu, ci); + } else { + union.getConcept().add(l); + union.getConcept().add(r); + StructuralMatch sm = new StructuralMatch(l, r, vm(IssueSeverity.INFORMATION, "Concepts are different", "ValueSet.compose.include.concept")); + combined.getChildren().add(sm); + compareConcepts(l, r, sm, null, null); + } + } + } + for (ConceptReferenceComponent r : right.getConcept()) { + if (!matchCR.contains(r)) { + union.getConcept().add(r); + combined.getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Added this Concept", "ValueSet.compose.include.concept"), r)); + } + } + + List matchFR = new ArrayList<>(); + for (ConceptSetFilterComponent l : left.getFilter()) { + ConceptSetFilterComponent r = findInList(right.getFilter(), l, left.getFilter()); + if (r == null) { + union.getFilter().add(l); + combined.getChildren().add(new StructuralMatch(l, vm(IssueSeverity.INFORMATION, "Removed this item", "ValueSet.compose.include.filter"))); + } else { + matchFR.add(r); + if (l.getProperty().equals(r.getProperty()) && l.getOp().equals(r.getOp())) { + ConceptSetFilterComponent cu = new ConceptSetFilterComponent(); + ConceptSetFilterComponent ci = new ConceptSetFilterComponent(); + union.getFilter().add(cu); + intersection.getFilter().add(ci); + StructuralMatch sm = new StructuralMatch(l, r); + combined.getChildren().add(sm); + compareFilters(l, r, sm, cu, ci); + } else { + union.getFilter().add(l); + union.getFilter().add(r); + StructuralMatch sm = new StructuralMatch(l, r, vm(IssueSeverity.INFORMATION, "Codes are different", "ValueSet.compose.include.filter")); + combined.getChildren().add(sm); + compareFilters(l, r, sm, null, null); + } + } + } + for (ConceptSetFilterComponent r : right.getFilter()) { + if (!matchFR.contains(r)) { + union.getFilter().add(r); + combined.getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Added this item", "ValueSet.compose.include.filter"), r)); + } + } + } + + private void compareConcepts(ConceptReferenceComponent l, ConceptReferenceComponent r, StructuralMatch sm, ConceptReferenceComponent cu, ConceptReferenceComponent ci) { + sm.getChildren().add(new StructuralMatch(l.getCodeElement(), r.getCodeElement(), l.getCode().equals(r.getCode()) ? null : vm(IssueSeverity.INFORMATION, "Codes do not match", "ValueSet.compose.include.concept"))); + if (ci != null) { + ci.setCode(l.getCode()); + cu.setCode(l.getCode()); + } + if (l.hasDisplay() && r.hasDisplay()) { + sm.getChildren().add(new StructuralMatch(l.getDisplayElement(), r.getDisplayElement(), l.getDisplay().equals(r.getDisplay()) ? null : vm(IssueSeverity.INFORMATION, "Displays do not match", "ValueSet.compose.include.concept"))); + if (ci != null) { + ci.setDisplay(r.getDisplay()); + cu.setDisplay(r.getDisplay()); + } + } else if (l.hasDisplay()) { + sm.getChildren().add(new StructuralMatch(l.getDisplayElement(), null, vm(IssueSeverity.INFORMATION, "Display Removed", "ValueSet.compose.include.concept"))); + if (ci != null) { + ci.setDisplay(l.getDisplay()); + cu.setDisplay(l.getDisplay()); + } + } else if (r.hasDisplay()) { + sm.getChildren().add(new StructuralMatch(null, r.getDisplayElement(), vm(IssueSeverity.INFORMATION, "Display added", "ValueSet.compose.include.concept"))); + if (ci != null) { + ci.setDisplay(r.getDisplay()); + cu.setDisplay(r.getDisplay()); + } + } else { + sm.getChildren().add(new StructuralMatch(null, null, vm(IssueSeverity.INFORMATION, "No Display", "ValueSet.compose.include.concept"))); + } + } + + private void 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 : vm(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 : vm(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 : vm(IssueSeverity.INFORMATION, "Values do not match", "ValueSet.compose.include.concept"))); + if (ci != null) { + ci.setProperty(l.getProperty()); + ci.setOp(l.getOp()); + ci.setValue(l.getValue()); + cu.setProperty(l.getProperty()); + cu.setOp(l.getOp()); + cu.setValue(l.getValue()); + } + } + + private CanonicalType findInList(List matches, CanonicalType item, List source) { + if (matches.size() == 1 && source.size() == 1) { + return matches.get(0); + } + for (CanonicalType t : matches) { + if (t.getValue().equals(item.getValue())) { + return t; + } + } + return null; + } + + private ConceptReferenceComponent findInList(List matches, ConceptReferenceComponent item, List source) { + if (matches.size() == 1 && source.size() == 1) { + return matches.get(0); + } + for (ConceptReferenceComponent t : matches) { + if (t.getCode().equals(item.getCode())) { + return t; + } + } + return null; + } + + private ConceptSetFilterComponent findInList(List matches, ConceptSetFilterComponent item, List source) { + if (matches.size() == 1 && source.size() == 1) { + return matches.get(0); + } + for (ConceptSetFilterComponent t : matches) { + if (t.getProperty().equals(item.getProperty()) && t.getOp().equals(item.getOp()) ) { + return t; + } + } + return null; + } + + private void compareExpansions(ValueSet left, ValueSet right, ValueSetComparison res) { + ValueSet expL = left.hasExpansion() ? left : expand(left, res, "left"); + ValueSet expR = left.hasExpansion() ? left : expand(right, res, "right"); + if (expL != null && expR != null) { + // ignore the parameters for now + compareConcepts(expL.getExpansion().getContains(), expR.getExpansion().getContains(), res.forceExpansion(), res.getUnion().getExpansion().getContains(), res.getIntersection().getExpansion().getContains(), "ValueSet.expansion.contains", res); + } + } + + private ValueSet expand(ValueSet vs, ValueSetComparison res, String name) { + ValueSetExpansionOutcome vse = context.expandVS(vs, true, false); + if (vse.getValueset() != null) { + return vse.getValueset(); + } else { + res.getMessages().add(new ValidationMessage(Source.TerminologyEngine, IssueType.EXCEPTION, "ValueSet", "Error Expanding "+name+":"+vse.getError(), IssueSeverity.ERROR)); + return null; + } + } + + private void compareConcepts(List left, List right, StructuralMatch combined, List union, List intersection, String path, ValueSetComparison res) { + List matchR = new ArrayList<>(); + for (ValueSetExpansionContainsComponent l : left) { + ValueSetExpansionContainsComponent r = findInList(right, l); + if (r == null) { + union.add(l); + combined.getChildren().add(new StructuralMatch(l, vm(IssueSeverity.INFORMATION, "Removed from expansion", path))); + } else { + matchR.add(r); + ValueSetExpansionContainsComponent ccU = merge(l, r); + ValueSetExpansionContainsComponent ccI = intersect(l, r); + union.add(ccU); + intersection.add(ccI); + StructuralMatch sm = new StructuralMatch(l, r); + compareItem(sm.getMessages(), path, l, r, res); + combined.getChildren().add(sm); + compareConcepts(l.getContains(), r.getContains(), sm, ccU.getContains(), ccI.getContains(), path+".where(code = '"+l.getCode()+"').contains", res); + } + } + for (ValueSetExpansionContainsComponent r : right) { + if (!matchR.contains(r)) { + union.add(r); + combined.getChildren().add(new StructuralMatch(vm(IssueSeverity.INFORMATION, "Added to expansion", path), r)); + } + } + } + + private void compareItem(List msgs, String path, ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r, ValueSetComparison res) { + compareStrings(path, msgs, l.getDisplay(), r.getDisplay(), "display", IssueSeverity.WARNING, res); + } + + private void compareStrings(String path, List msgs, String left, String right, String name, IssueSeverity level, ValueSetComparison res) { + if (!Utilities.noString(right)) { + if (Utilities.noString(left)) { + msgs.add(vm(level, "Value for "+name+" added", path)); + } 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(vm(level, name+" changed from left to right", path)); + } + } else if (!Utilities.noString(left)) { + msgs.add(vm(level, "Value for "+name+" removed", path)); + } + } + + private ValueSetExpansionContainsComponent findInList(List list, ValueSetExpansionContainsComponent item) { + for (ValueSetExpansionContainsComponent t : list) { + if (t.getSystem().equals(item.getSystem()) && t.getCode().equals(item.getCode())) { + return t; + } + } + return null; + } + + private ValueSetExpansionContainsComponent intersect(ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r) { + ValueSetExpansionContainsComponent res = new ValueSetExpansionContainsComponent(); + if (l.hasAbstract() && r.hasAbstract()) { + res.setAbstract(l.getAbstract()); + } + if (l.hasCode() && r.hasCode()) { + res.setCode(l.getCode()); + } + if (l.hasSystem() && r.hasSystem()) { + res.setSystem(l.getSystem()); + } + if (l.hasVersion() && r.hasVersion()) { + res.setVersion(l.getVersion()); + } + if (l.hasDisplay() && r.hasDisplay()) { + res.setDisplay(l.getDisplay()); + } + return res; + } + + private ValueSetExpansionContainsComponent merge(ValueSetExpansionContainsComponent l, ValueSetExpansionContainsComponent r) { + ValueSetExpansionContainsComponent res = new ValueSetExpansionContainsComponent(); + if (l.hasAbstract()) { + res.setAbstract(l.getAbstract()); + } else if (r.hasAbstract()) { + res.setAbstract(r.getAbstract()); + } + if (l.hasCode()) { + res.setCode(l.getCode()); + } else if (r.hasCode()) { + res.setCode(r.getCode()); + } + if (l.hasSystem()) { + res.setSystem(l.getSystem()); + } else if (r.hasSystem()) { + res.setSystem(r.getSystem()); + } + if (l.hasVersion()) { + res.setVersion(l.getVersion()); + } else if (r.hasVersion()) { + res.setVersion(r.getVersion()); + } + if (l.hasDisplay()) { + res.setDisplay(l.getDisplay()); + } else if (r.hasDisplay()) { + res.setDisplay(r.getDisplay()); + } + return res; + } + + @Override + protected String fhirType() { + return "ValueSet"; + } + + public XhtmlNode renderCompose(ValueSetComparison csc, String id, String prefix) throws FHIRException, IOException { + HierarchicalTableGenerator gen = new HierarchicalTableGenerator(Utilities.path("[tmp]", "comparison"), false); + TableModel model = gen.new TableModel(id, true); + model.setAlternating(true); + model.getTitles().add(gen.new Title(null, null, "Item", "The type of item being compared", null, 100)); + model.getTitles().add(gen.new Title(null, null, "Property", "The system for the concept", null, 100, 2)); + model.getTitles().add(gen.new Title(null, null, "Value", "The display for the concept", null, 200, 2)); + model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200)); + for (StructuralMatch t : csc.getIncludes().getChildren()) { + addComposeRow(gen, model.getRows(), t, "include"); + } + for (StructuralMatch t : csc.getExcludes().getChildren()) { + addComposeRow(gen, model.getRows(), t, "exclude"); + } + return gen.generate(model, prefix, 0, null); + } + + private void addComposeRow(HierarchicalTableGenerator gen, List rows, StructuralMatch t, String name) { + Row r = gen.new Row(); + rows.add(r); + r.getCells().add(gen.new Cell(null, null, name, null, null)); + if (t.hasLeft() && t.hasRight()) { + ConceptSetComponent csL = (ConceptSetComponent) t.getLeft(); + ConceptSetComponent csR = (ConceptSetComponent) t.getRight(); + // we assume both have systems + if (csL.getSystem().equals(csR.getSystem())) { + r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).span(2).center()); + } else { + r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + r.getCells().add(gen.new Cell(null, null, csR.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + } + + if (csL.hasVersion() && csR.hasVersion()) { + if (csL.getVersion().equals(csR.getVersion())) { + r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).span(2).center()); + } else { + r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + } + } else if (csL.hasVersion()) { + r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null)); + r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); + } else if (csR.hasVersion()) { + r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); + r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null)); + } else { + r.getCells().add(missingCell(gen).span(2).center()); + } + + } else if (t.hasLeft()) { + r.setColor(COLOR_NO_ROW_RIGHT); + ConceptSetComponent cs = (ConceptSetComponent) t.getLeft(); + r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null)); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null)); + r.getCells().add(missingCell(gen)); + } else { + r.setColor(COLOR_NO_ROW_LEFT); + ConceptSetComponent cs = (ConceptSetComponent) t.getRight(); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null)); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null)); + } + r.getCells().add(cellForMessages(gen, t.getMessages())); + for (StructuralMatch c : t.getChildren()) { + if (c.either() instanceof ConceptReferenceComponent) { + addSetConceptRow(gen, r.getSubRows(), c); + } else { + addSetFilterRow(gen, r.getSubRows(), c); + } + } + } + + private void addSetConceptRow(HierarchicalTableGenerator gen, List rows, StructuralMatch t) { + Row r = gen.new Row(); + rows.add(r); + r.getCells().add(gen.new Cell(null, null, "Concept", null, null)); + if (t.hasLeft() && t.hasRight()) { + ConceptReferenceComponent csL = (ConceptReferenceComponent) t.getLeft(); + ConceptReferenceComponent csR = (ConceptReferenceComponent) t.getRight(); + // we assume both have codes + if (csL.getCode().equals(csR.getCode())) { + r.getCells().add(gen.new Cell(null, null, csL.getCode(), null, null).span(2).center()); + } else { + r.getCells().add(gen.new Cell(null, null, csL.getCode(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + r.getCells().add(gen.new Cell(null, null, csR.getCode(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + } + + if (csL.hasDisplay() && csR.hasDisplay()) { + if (csL.getDisplay().equals(csR.getDisplay())) { + r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null).span(2).center()); + } else { + r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + r.getCells().add(gen.new Cell(null, null, csR.getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + } + } else if (csL.hasDisplay()) { + r.getCells().add(gen.new Cell(null, null, csL.getDisplay(), null, null)); + r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); + } else if (csR.hasDisplay()) { + r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); + r.getCells().add(gen.new Cell(null, null, csR.getDisplay(), null, null)); + } else { + r.getCells().add(missingCell(gen).span(2).center()); + } + + } else if (t.hasLeft()) { + r.setColor(COLOR_NO_ROW_RIGHT); + ConceptReferenceComponent cs = (ConceptReferenceComponent) t.getLeft(); + r.getCells().add(gen.new Cell(null, null, cs.getCode(), null, null)); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, cs.hasDisplay() ? "Version: "+cs.getDisplay() : "", null, null)); + r.getCells().add(missingCell(gen)); + } else { + r.setColor(COLOR_NO_ROW_LEFT); + ConceptReferenceComponent cs = (ConceptReferenceComponent) t.getRight(); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, cs.getCode(), null, null)); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, cs.hasDisplay() ? "Version: "+cs.getDisplay() : "", null, null)); + } + r.getCells().add(cellForMessages(gen, t.getMessages())); + + } + + private void addSetFilterRow(HierarchicalTableGenerator gen, List rows, StructuralMatch t) { +// Row r = gen.new Row(); +// rows.add(r); +// r.getCells().add(gen.new Cell(null, null, "Filter", null, null)); +// if (t.hasLeft() && t.hasRight()) { +// ConceptSetComponent csL = (ConceptSetComponent) t.getLeft(); +// ConceptSetComponent csR = (ConceptSetComponent) t.getRight(); +// // we assume both have systems +// if (csL.getSystem().equals(csR.getSystem())) { +// r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).span(2).center()); +// } else { +// r.getCells().add(gen.new Cell(null, null, csL.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); +// r.getCells().add(gen.new Cell(null, null, csR.getSystem(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); +// } +// +// if (csL.hasVersion() && csR.hasVersion()) { +// if (csL.getVersion().equals(csR.getVersion())) { +// r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).span(2).center()); +// } else { +// r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); +// r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); +// } +// } else if (csL.hasVersion()) { +// r.getCells().add(gen.new Cell(null, null, csL.getVersion(), null, null)); +// r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); +// } else if (csR.hasVersion()) { +// r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); +// r.getCells().add(gen.new Cell(null, null, csR.getVersion(), null, null)); +// } else { +// r.getCells().add(missingCell(gen).span(2).center()); +// } +// +// } else if (t.hasLeft()) { +// r.setColor(COLOR_NO_ROW_RIGHT); +// ConceptSetComponent cs = (ConceptSetComponent) t.getLeft(); +// r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null)); +// r.getCells().add(missingCell(gen)); +// r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null)); +// r.getCells().add(missingCell(gen)); +// } else { +// r.setColor(COLOR_NO_ROW_LEFT); +// ConceptSetComponent cs = (ConceptSetComponent) t.getRight(); +// r.getCells().add(missingCell(gen)); +// r.getCells().add(gen.new Cell(null, null, cs.getSystem(), null, null)); +// r.getCells().add(missingCell(gen)); +// r.getCells().add(gen.new Cell(null, null, cs.hasVersion() ? "Version: "+cs.getVersion() : "", null, null)); +// } +// r.getCells().add(gen.new Cell(null, null, t.getError(), null, null)); + + } + + public XhtmlNode renderExpansion(ValueSetComparison csc, String id, String prefix) throws IOException { + if (csc.getExpansion() == null) { + XhtmlNode p = new XhtmlNode(NodeType.Element, "p"); + p.tx("Unable to generate expansion - see errors"); + return p; + } + // columns: code(+system), version, display , abstract, inactive, + boolean hasSystem = csc.getExpansion().getChildren().isEmpty() ? false : getSystemVaries(csc.getExpansion(), csc.getExpansion().getChildren().get(0).either().getSystem()); + boolean hasVersion = findVersion(csc.getExpansion()); + boolean hasAbstract = findAbstract(csc.getExpansion()); + boolean hasInactive = findInactive(csc.getExpansion()); + + HierarchicalTableGenerator gen = new HierarchicalTableGenerator(Utilities.path("[tmp]", "comparison"), false); + TableModel model = gen.new TableModel(id, true); + model.setAlternating(true); + if (hasSystem) { + model.getTitles().add(gen.new Title(null, null, "System", "The code for the concept", null, 100)); + } + model.getTitles().add(gen.new Title(null, null, "Code", "The system for the concept", null, 100)); + model.getTitles().add(gen.new Title(null, null, "Display", "The display for the concept", null, 200, 2)); +// if (hasVersion) { +// model.getTitles().add(gen.new Title(null, null, "Version", "The version for the concept", null, 200, 2)); +// } +// if (hasAbstract) { +// model.getTitles().add(gen.new Title(null, null, "Abstract", "The abstract flag for the concept", null, 200, 2)); +// } +// if (hasInactive) { +// model.getTitles().add(gen.new Title(null, null, "Inactive", "The inactive flag for the concept", null, 200, 2)); +// } + model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200)); + for (StructuralMatch t : csc.getExpansion().getChildren()) { + addExpansionRow(gen, model.getRows(), t, hasSystem, hasVersion, hasAbstract, hasInactive); + } + return gen.generate(model, prefix, 0, null); + } + + private void addExpansionRow(HierarchicalTableGenerator gen, List rows, StructuralMatch t, boolean hasSystem, boolean hasVersion, boolean hasAbstract, boolean hasInactive) { + Row r = gen.new Row(); + rows.add(r); + if (hasSystem) { + r.getCells().add(gen.new Cell(null, null, t.either().getSystem(), null, null)); + } + r.getCells().add(gen.new Cell(null, null, t.either().getCode(), null, null)); + if (t.hasLeft() && t.hasRight()) { + if (t.getLeft().hasDisplay() && t.getRight().hasDisplay()) { + if (t.getLeft().getDisplay().equals(t.getRight().getDisplay())) { + r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2).center()); + } else { + r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT)); + } + } else if (t.getLeft().hasDisplay()) { + r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null)); + r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT)); + } else if (t.getRight().hasDisplay()) { + r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT)); + r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null)); + } else { + r.getCells().add(missingCell(gen).span(2).center()); + } + + } else if (t.hasLeft()) { + r.setColor(COLOR_NO_ROW_RIGHT); + r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null)); + r.getCells().add(missingCell(gen)); + } else { + r.setColor(COLOR_NO_ROW_LEFT); + r.getCells().add(missingCell(gen)); + r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null)); + } + r.getCells().add(cellForMessages(gen, t.getMessages())); + for (StructuralMatch c : t.getChildren()) { + addExpansionRow(gen, r.getSubRows(), c, hasSystem, hasVersion, hasAbstract, hasInactive); + } + } + + private boolean getSystemVaries(StructuralMatch list, String system) { + for (StructuralMatch t : list.getChildren()) { + if (t.hasLeft() && !system.equals(t.getLeft().getSystem())) { + return true; + } + if (t.hasRight() && !system.equals(t.getRight().getSystem())) { + return true; + } + if (getSystemVaries(t, system)) { + return true; + } + } + return false; + } + + private boolean findInactive(StructuralMatch list) { + for (StructuralMatch t : list.getChildren()) { + if (t.hasLeft() && t.getLeft().getInactive()) { + return true; + } + if (t.hasRight() && t.getRight().getInactive()) { + return true; + } + if (findInactive(t)) { + return true; + } + } + return false; + } + + private boolean findAbstract(StructuralMatch list) { + for (StructuralMatch t : list.getChildren()) { + if (t.hasLeft() && t.getLeft().getAbstract()) { + return true; + } + if (t.hasRight() && t.getRight().getAbstract()) { + return true; + } + if (findAbstract(t)) { + return true; + } + } + return false; + } + + private boolean findVersion(StructuralMatch list) { + for (StructuralMatch t : list.getChildren()) { + if (t.hasLeft() && t.getLeft().hasVersion()) { + return true; + } + if (t.hasRight() && t.getRight().hasVersion()) { + return true; + } + if (findVersion(t)) { + return true; + } + } + return false; + } + +} diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/TextFile.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/TextFile.java index ac712b19a..cbd7b20fd 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/TextFile.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/TextFile.java @@ -245,5 +245,8 @@ public class TextFile { return streamToString(new ByteArrayInputStream(bs)); } - + public static void streamToFile(InputStream stream, String filename) throws IOException { + byte[] cnt = streamToBytes(stream); + bytesToFile(cnt, filename); + } } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java index 0a7bfdafc..89a37c13d 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/Utilities.java @@ -1265,6 +1265,18 @@ public class Utilities { } } + public static boolean startsWithInList(String s, String... list) { + if (s == null) { + return false; + } + for (String l : list) { + if (s.startsWith(l)) { + return true; + } + } + return false; + } + } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/PackageCacheManager.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/PackageCacheManager.java index 6dab7e051..26e738343 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/PackageCacheManager.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/PackageCacheManager.java @@ -204,9 +204,12 @@ public class PackageCacheManager { } public void loadFromFolder(String packagesFolder) throws IOException { - for (File f : new File(packagesFolder).listFiles()) { - if (f.getName().endsWith(".tgz")) { - temporaryPackages.add(NpmPackage.fromPackage(new FileInputStream(f))); + File[] files = new File(packagesFolder).listFiles(); + if (files != null) { + for (File f : files) { + if (f.getName().endsWith(".tgz")) { + temporaryPackages.add(NpmPackage.fromPackage(new FileInputStream(f))); + } } } } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/HierarchicalTableGenerator.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/HierarchicalTableGenerator.java index e345d682f..c411640aa 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/HierarchicalTableGenerator.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/HierarchicalTableGenerator.java @@ -100,6 +100,10 @@ public class HierarchicalTableGenerator extends TranslatingUtilities { private static final String BACKGROUND_ALT_COLOR = "#F7F7F7"; public static boolean ACTIVE_TABLES = false; + public enum TextAlignment { + LEFT, CENTER, RIGHT; + } + private static Map files = new HashMap(); private class Counter { @@ -201,6 +205,7 @@ public class HierarchicalTableGenerator extends TranslatingUtilities { private List pieces = new ArrayList(); private String cellStyle; protected int span = 1; + private TextAlignment alignment = TextAlignment.LEFT; public Cell() { @@ -373,6 +378,10 @@ public class HierarchicalTableGenerator extends TranslatingUtilities { span = value; return this; } + public Cell center() { + alignment = TextAlignment.CENTER; + return this; + } } diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java new file mode 100644 index 000000000..77f1abc7a --- /dev/null +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java @@ -0,0 +1,245 @@ +package org.hl7.fhir.comparison.tests; + +import com.google.common.base.Charsets; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.NotImplementedException; +import org.hl7.fhir.convertors.VersionConvertor_10_50; +import org.hl7.fhir.convertors.VersionConvertor_14_50; +import org.hl7.fhir.convertors.VersionConvertor_30_50; +import org.hl7.fhir.convertors.VersionConvertor_40_50; +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.exceptions.PathEngineException; +import org.hl7.fhir.r5.comparison.CodeSystemComparer; +import org.hl7.fhir.r5.comparison.CodeSystemComparer.CodeSystemComparison; +import org.hl7.fhir.r5.comparison.ValueSetComparer; +import org.hl7.fhir.r5.comparison.ValueSetComparer.ValueSetComparison; +import org.hl7.fhir.r5.conformance.ProfileUtilities; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.context.SimpleWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.elementmodel.Manager; +import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; +import org.hl7.fhir.r5.elementmodel.ObjectConverter; +import org.hl7.fhir.r5.formats.IParser.OutputStyle; +import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.formats.XmlParser; +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.CanonicalResource; +import org.hl7.fhir.r5.model.CodeSystem; +import org.hl7.fhir.r5.model.Constants; +import org.hl7.fhir.r5.model.FhirPublication; +import org.hl7.fhir.r5.model.Patient; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.TypeDetails; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.test.utils.TestingUtilities; +import org.hl7.fhir.r5.utils.FHIRPathEngine; +import org.hl7.fhir.r5.utils.FHIRPathEngine.IEvaluationContext; +import org.hl7.fhir.r5.utils.FHIRPathEngine.IEvaluationContext.FunctionDetails; +import org.hl7.fhir.r5.utils.IResourceValidator; +import org.hl7.fhir.r5.utils.IResourceValidator.IValidatorResourceFetcher; +import org.hl7.fhir.r5.utils.IResourceValidator.ReferenceValidationPolicy; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.cache.NpmPackage; +import org.hl7.fhir.utilities.cache.PackageCacheManager; +import org.hl7.fhir.utilities.cache.ToolsVersion; +import org.hl7.fhir.utilities.validation.ValidationMessage; +import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; +import org.hl7.fhir.utilities.xhtml.XhtmlComposer; +import org.hl7.fhir.validation.ValidationEngine; +import org.hl7.fhir.validation.instance.InstanceValidator; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.thymeleaf.util.IWritableCharSequence; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Stream; + +public class ComparisonTests { + + public final static boolean PRINT_OUTPUT_TO_CONSOLE = true; + + public static Stream data() throws IOException { + String contents = TestingUtilities.loadTestResource("comparison", "manifest.json"); + + Map examples = new HashMap(); + manifest = (JsonObject) new com.google.gson.JsonParser().parse(contents); + for (Entry e : manifest.getAsJsonObject("test-cases").entrySet()) { + examples.put(e.getKey(), e.getValue().getAsJsonObject()); + } + + List names = new ArrayList(examples.size()); + names.addAll(examples.keySet()); + Collections.sort(names); + + List objects = new ArrayList<>(); + for (String id : names) { + objects.add(Arguments.of(id, examples.get(id))); + } + return objects.stream(); + } + + private static JsonObject manifest; + private static IWorkerContext context; + private JsonObject content; + + private static final String DEF_TX = "http://tx.fhir.org"; + private static final String HEADER = ""; + private static final String BREAK = "
"; + private static final String FOOTER = ""; + + @ParameterizedTest(name = "{index}: id {0}") + @MethodSource("data") + public void test(String name, JsonObject content) throws Exception { + this.content = content; + + if (content.has("use-test") && !content.get("use-test").getAsBoolean()) + return; + + if (context == null) { + System.out.println("---- Load R5 ----------------------------------------------------------------"); + context = TestingUtilities.context(); + } + + if (!new File(Utilities.path("[tmp]", "comparison")).exists()) { + System.out.println("---- Set up Output ----------------------------------------------------------"); + Utilities.createDirectory(Utilities.path("[tmp]", "comparison")); + PackageCacheManager pcm = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION); + NpmPackage npm = pcm.loadPackage("hl7.fhir.pubpack", "0.0.4"); + for (String f : npm.list("other")) { + TextFile.streamToFile(npm.load("other", f), Utilities.path("[tmp]", "comparison", f)); + } + } + System.out.println("---- " + name + " ----------------------------------------------------------------"); + CanonicalResource left = load("left"); + CanonicalResource right = load("right"); + + if (left instanceof CodeSystem && right instanceof CodeSystem) { + CodeSystemComparer cs = new CodeSystemComparer(context); + CodeSystemComparison csc = cs.compare((CodeSystem) left, (CodeSystem) right); + Assertions.assertTrue(csc.getUnion().getConcept().size() > csc.getIntersection().getConcept().size()); + new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path("[tmp]", "comparison", name+"-union.json")), csc.getUnion()); + new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path("[tmp]", "comparison", name+"-intersection.json")), csc.getIntersection()); + + String xmle = new XhtmlComposer(true).compose(cs.renderErrors(csc)); + String xml1 = new XhtmlComposer(true).compose(cs.renderMetadata(csc, "", "")); + String xml2 = new XhtmlComposer(true).compose(cs.renderConcepts(csc, "", "")); + TextFile.stringToFile(HEADER+hd("Messages")+xmle+BREAK+hd("Metadata")+xml1+BREAK+hd("Concepts")+xml2+FOOTER, Utilities.path("[tmp]", "comparison", name+".html")); + checkOutcomes(csc.getMessages(), content); + } else if (left instanceof ValueSet && right instanceof ValueSet) { + ValueSetComparer cs = new ValueSetComparer(context); + ValueSetComparison csc = cs.compare((ValueSet) left, (ValueSet) right); + new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path("[tmp]", "comparison", name+"-union.json")), csc.getUnion()); + new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path("[tmp]", "comparison", name+"-intersection.json")), csc.getIntersection()); + + String xmle = new XhtmlComposer(true).compose(cs.renderErrors(csc)); + String xml1 = new XhtmlComposer(true).compose(cs.renderMetadata(csc, "", "")); + String xml2 = new XhtmlComposer(true).compose(cs.renderCompose(csc, "", "")); + String xml3 = new XhtmlComposer(true).compose(cs.renderExpansion(csc, "", "")); + TextFile.stringToFile(HEADER+hd("Messages")+xmle+BREAK+hd("Metadata")+xml1+BREAK+hd("Definition")+xml2+BREAK+hd("Expansion")+xml3+FOOTER, Utilities.path("[tmp]", "comparison", name+".html")); + checkOutcomes(csc.getMessages(), content); + } else { + throw new FHIRException("Can't compare "+left.fhirType()+" to "+right.fhirType()); + } + } + + private String hd(String text) { + return "

"+text+"

\r\n"; + } + + private CanonicalResource load(String name) throws IOException { + JsonObject details = content.getAsJsonObject(name); + String src = TestingUtilities.loadTestResource("comparison", details.get("source").getAsString()); + return (CanonicalResource) loadResource(details.get("source").getAsString(), src, details.get("version").getAsString()); + } + + public Resource loadResource(String filename, String contents, String ver) throws IOException, FHIRFormatError, FileNotFoundException, FHIRException, DefinitionException { + try (InputStream inputStream = IOUtils.toInputStream(contents, Charsets.UTF_8)) { + if (filename.contains(".json")) { + if (Constants.VERSION.equals(ver) || "5.0".equals(ver)) + return new JsonParser().parse(inputStream); + else if (VersionUtilities.isR3Ver(ver)) + return VersionConvertor_30_50.convertResource(new org.hl7.fhir.dstu3.formats.JsonParser().parse(inputStream), false); + else if (VersionUtilities.isR2BVer(ver)) + return VersionConvertor_14_50.convertResource(new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(inputStream)); + else if (VersionUtilities.isR2Ver(ver)) + return VersionConvertor_10_50.convertResource(new org.hl7.fhir.dstu2.formats.JsonParser().parse(inputStream)); + else if (VersionUtilities.isR4Ver(ver)) + return VersionConvertor_40_50.convertResource(new org.hl7.fhir.r4.formats.JsonParser().parse(inputStream)); + else + throw new FHIRException("unknown version " + ver); + } else { + if (Constants.VERSION.equals(ver) || "5.0".equals(ver)) + return new XmlParser().parse(inputStream); + else if (VersionUtilities.isR3Ver(ver)) + return VersionConvertor_30_50.convertResource(new org.hl7.fhir.dstu3.formats.XmlParser().parse(inputStream), false); + else if (VersionUtilities.isR2BVer(ver)) + return VersionConvertor_14_50.convertResource(new org.hl7.fhir.dstu2016may.formats.XmlParser().parse(inputStream)); + else if (VersionUtilities.isR2Ver(ver)) + return VersionConvertor_10_50.convertResource(new org.hl7.fhir.dstu2.formats.XmlParser().parse(inputStream)); + else if (VersionUtilities.isR4Ver(ver)) + return VersionConvertor_40_50.convertResource(new org.hl7.fhir.r4.formats.XmlParser().parse(inputStream)); + else + throw new FHIRException("unknown version " + ver); + } + } + } + + private void checkOutcomes(List errors, JsonObject focus) { + JsonObject output = focus.getAsJsonObject("output"); + int ec = 0; + int wc = 0; + int hc = 0; + List errLocs = new ArrayList<>(); + for (ValidationMessage vm : errors) { + if (vm.getLevel() == IssueSeverity.FATAL || vm.getLevel() == IssueSeverity.ERROR) { + ec++; + if (PRINT_OUTPUT_TO_CONSOLE) { + System.out.println(vm.getDisplay()); + } + errLocs.add(vm.getLocation()); + } + if (vm.getLevel() == IssueSeverity.WARNING) { + wc++; + if (PRINT_OUTPUT_TO_CONSOLE) { + System.out.println(vm.getDisplay()); + } + } + if (vm.getLevel() == IssueSeverity.INFORMATION) { + hc++; + if (PRINT_OUTPUT_TO_CONSOLE) { + System.out.println(vm.getDisplay()); + } + } + } + Assertions.assertEquals(output.get("errorCount").getAsInt(), ec, "Expected " + Integer.toString(output.get("errorCount").getAsInt()) + " errors, but found " + Integer.toString(ec) + "."); + if (output.has("warningCount")) + Assertions.assertEquals(output.get("warningCount").getAsInt(), wc, "Expected " + Integer.toString(output.get("warningCount").getAsInt()) + " warnings, but found " + Integer.toString(wc) + "."); + if (output.has("infoCount")) + Assertions.assertEquals(output.get("infoCount").getAsInt(), hc, "Expected " + Integer.toString(output.get("infoCount").getAsInt()) + " hints, but found " + Integer.toString(hc) + "."); + } + +}