diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/CapabilityStatementUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/CapabilityStatementUtilities.java index 8f6796227..540cbed27 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/CapabilityStatementUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/CapabilityStatementUtilities.java @@ -381,6 +381,7 @@ public class CapabilityStatementUtilities { pc.setRightName(otherName+": "+sdR.present()); pc.setRightLink(sdR.getUserString("path")); pc.compareProfiles(sdL, sdR); + System.out.println("Generate Comparison between "+pc.getLeftName()+" and "+pc.getRightName()); pc.generate(folder); td.ah(pc.getId()+".html").tx("Comparison..."); td.tx(pc.getErrCount()+" "+Utilities.pluralize("error", pc.getErrCount())); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileComparer.java index c441ba1e8..9eab58f06 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileComparer.java @@ -42,10 +42,13 @@ 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; @@ -324,7 +327,7 @@ public class ProfileComparer implements ProfileKnowledgeProvider { outcome.superset = new StructureDefinition(); outcome.subset = new StructureDefinition(); if (outcome.ruleEqual(ln.path(), null,ln.path(), rn.path(), "Base Type is not compatible", false)) { - if (compareElements(outcome, ln.path(), ln, rn)) { + 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()); @@ -359,9 +362,10 @@ public class ProfileComparer implements ProfileKnowledgeProvider { * @throws IOException * @throws FHIRFormatError */ - private boolean compareElements(ProfileComparison outcome, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError { + private boolean compareElements(ProfileComparison outcome, String path, DefinitionNavigator left, DefinitionNavigator right, String sliceName) throws DefinitionException, IOException, FHIRFormatError { +// System.out.println(path); // preconditions: - assert(path != null); + assert(path != null); assert(left != null); assert(right != null); assert(left.path().equals(right.path())); @@ -370,6 +374,8 @@ public class ProfileComparer implements ProfileKnowledgeProvider { // 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 @@ -424,23 +430,114 @@ public class ProfileComparer implements ProfileKnowledgeProvider { 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(); - throw new DefinitionException("Slicing is not handled yet"); + // 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; + } - // add the children - outcome.subset.getSnapshot().getElement().add(subset); - outcome.superset.getSnapshot().getElement().add(superset); - return compareChildren(subset, outcome, path, left, right); + + 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 { @@ -527,7 +624,7 @@ public class ProfileComparer implements ProfileKnowledgeProvider { 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)) + 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)); @@ -978,6 +1075,8 @@ public class ProfileComparer implements ProfileKnowledgeProvider { return right; if (right == null) return left; + left = stripLinks(left); + right = stripLinks(right); if (left.equalsIgnoreCase(right)) return left; if (path != null) { @@ -988,6 +1087,19 @@ public class ProfileComparer implements ProfileKnowledgeProvider { 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); @@ -1206,6 +1318,7 @@ public class ProfileComparer implements ProfileKnowledgeProvider { 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"); @@ -1217,6 +1330,7 @@ public class ProfileComparer implements ProfileKnowledgeProvider { 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(); @@ -1319,6 +1433,7 @@ public class ProfileComparer implements ProfileKnowledgeProvider { vars.put("valuesets", genValueSets(dest+"/"+getId()+"-vs")); producePage(summaryTemplate(), Utilities.path(dest, getId()+".html"), vars); + System.out.println(" .. profiles"); // then we produce a comparison page for each pair for (ProfileComparison cmp : getComparisons()) { vars.clear(); @@ -1442,7 +1557,7 @@ public class ProfileComparer implements ProfileKnowledgeProvider { @Override public String getLinkForUrl(String corePath, String s) { - throw new Error("Not done yet"); + return null; } public int getErrCount() { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java index a3ec7a33b..a8d4df962 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ProfileUtilities.java @@ -1333,7 +1333,7 @@ public class ProfileUtilities extends TranslatingUtilities { } - private String summarizeSlicing(ElementDefinitionSlicingComponent slice) { + public static String summarizeSlicing(ElementDefinitionSlicingComponent slice) { StringBuilder b = new StringBuilder(); boolean first = true; for (ElementDefinitionSlicingDiscriminatorComponent d : slice.getDiscriminator()) { @@ -1341,11 +1341,11 @@ public class ProfileUtilities extends TranslatingUtilities { first = false; else b.append(", "); - b.append(d); + b.append(d.getType().toCode()+":"+d.getPath()); } - b.append("("); + b.append(" ("); if (slice.hasOrdered()) - b.append(slice.getOrderedElement().asStringValue()); + b.append(slice.getOrdered() ? "ordered" : "unordered"); b.append("/"); if (slice.hasRules()) b.append(slice.getRules().toCode()); @@ -3121,7 +3121,11 @@ public class ProfileUtilities extends TranslatingUtilities { private void genFixedValue(HierarchicalTableGenerator gen, Row erow, Type value, boolean snapshot, boolean pattern, String corePath) { String ref = pkp.getLinkFor(corePath, value.fhirType()); - ref = ref.substring(0, ref.indexOf(".html"))+"-definitions.html#"; + if (ref != null) { + ref = ref.substring(0, ref.indexOf(".html"))+"-definitions.html#"; + } else { + ref = "??"; + } StructureDefinition sd = context.fetchTypeDefinition(value.fhirType()); for (org.hl7.fhir.r5.model.Property t : value.children()) {