001package org.hl7.fhir.r4.conformance;
002
003import java.io.File;
004import java.io.IOException;
005import java.net.URL;
006import java.net.URLConnection;
007import java.util.ArrayList;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013
014import org.hl7.fhir.exceptions.DefinitionException;
015import org.hl7.fhir.exceptions.FHIRFormatError;
016import org.hl7.fhir.r4.conformance.ProfileComparer.ProfileComparison;
017import org.hl7.fhir.r4.context.IWorkerContext;
018import org.hl7.fhir.r4.formats.IParser;
019import org.hl7.fhir.r4.model.Base;
020import org.hl7.fhir.r4.model.Coding;
021import org.hl7.fhir.r4.model.ElementDefinition;
022import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent;
023import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionConstraintComponent;
024import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionMappingComponent;
025import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionSlicingComponent;
026import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
027import org.hl7.fhir.r4.model.Enumerations.BindingStrength;
028import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
029import org.hl7.fhir.r4.model.IntegerType;
030import org.hl7.fhir.r4.model.PrimitiveType;
031import org.hl7.fhir.r4.model.Reference;
032import org.hl7.fhir.r4.model.StringType;
033import org.hl7.fhir.r4.model.StructureDefinition;
034import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule;
035import org.hl7.fhir.r4.model.Type;
036import org.hl7.fhir.r4.model.UriType;
037import org.hl7.fhir.r4.model.ValueSet;
038import org.hl7.fhir.r4.model.ValueSet.ConceptReferenceComponent;
039import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
040import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
041import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
042import org.hl7.fhir.r4.utils.DefinitionNavigator;
043import org.hl7.fhir.r4.utils.ToolingExtensions;
044import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
045import org.hl7.fhir.utilities.TextFile;
046import org.hl7.fhir.utilities.Utilities;
047import org.hl7.fhir.utilities.Logger.LogMessageType;
048import org.hl7.fhir.utilities.validation.ValidationMessage;
049import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
050
051import com.google.gson.JsonObject;
052
053/**
054 * A engine that generates difference analysis between two sets of structure 
055 * definitions, typically from 2 different implementation guides. 
056 * 
057 * How this class works is that you create it with access to a bunch of underying
058 * resources that includes all the structure definitions from both implementation 
059 * guides 
060 * 
061 * Once the class is created, you repeatedly pass pairs of structure definitions,
062 * one from each IG, building up a web of difference analyses. This class will
063 * automatically process any internal comparisons that it encounters
064 * 
065 * When all the comparisons have been performed, you can then generate a variety
066 * of output formats
067 * 
068 * @author Grahame Grieve
069 *
070 */
071public class ProfileComparer {
072
073  private IWorkerContext context;
074  
075  public ProfileComparer(IWorkerContext context) {
076    super();
077    this.context = context;
078  }
079
080  private static final int BOTH_NULL = 0;
081  private static final int EITHER_NULL = 1;
082
083  public class ProfileComparison {
084    private String id;
085    /**
086     * the first of two structures that were compared to generate this comparison
087     * 
088     *   In a few cases - selection of example content and value sets - left gets 
089     *   preference over right
090     */
091    private StructureDefinition left;
092
093    /**
094     * the second of two structures that were compared to generate this comparison
095     * 
096     *   In a few cases - selection of example content and value sets - left gets 
097     *   preference over right
098     */
099    private StructureDefinition right;
100
101    
102    public String getId() {
103      return id;
104    }
105    private String leftName() {
106      return left.getName();
107    }
108    private String rightName() {
109      return right.getName();
110    }
111
112    /**
113     * messages generated during the comparison. There are 4 grades of messages:
114     *   information - a list of differences between structures
115     *   warnings - notifies that the comparer is unable to fully compare the structures (constraints differ, open value sets)
116     *   errors - where the structures are incompatible
117     *   fatal errors - some error that prevented full analysis 
118     * 
119     * @return
120     */
121    private List<ValidationMessage> messages = new ArrayList<ValidationMessage>();
122
123    /**
124     * The structure that describes all instances that will conform to both structures 
125     */
126    private StructureDefinition subset;
127
128    /**
129     * The structure that describes all instances that will conform to either structures 
130     */
131    private StructureDefinition superset;
132
133    public StructureDefinition getLeft() {
134      return left;
135    }
136
137    public StructureDefinition getRight() {
138      return right;
139    }
140
141    public List<ValidationMessage> getMessages() {
142      return messages;
143    }
144
145    public StructureDefinition getSubset() {
146      return subset;
147    }
148
149    public StructureDefinition getSuperset() {
150      return superset;
151    }
152    
153    private boolean ruleEqual(String path, ElementDefinition ed, String vLeft, String vRight, String description, boolean nullOK) {
154      if (vLeft == null && vRight == null && nullOK)
155        return true;
156      if (vLeft == null && vRight == null) {
157        messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, description+" and not null (null/null)", ValidationMessage.IssueSeverity.ERROR));
158        if (ed != null)
159          status(ed, ProfileUtilities.STATUS_ERROR);
160      }
161      if (vLeft == null || !vLeft.equals(vRight)) {
162        messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, description+" ("+vLeft+"/"+vRight+")", ValidationMessage.IssueSeverity.ERROR));
163        if (ed != null)
164          status(ed, ProfileUtilities.STATUS_ERROR);
165      }
166      return true;
167    }
168    
169    private boolean ruleCompares(ElementDefinition ed, Type vLeft, Type vRight, String path, int nullStatus) throws IOException {
170      if (vLeft == null && vRight == null && nullStatus == BOTH_NULL)
171        return true;
172      if (vLeft == null && vRight == null) {
173        messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Must be the same and not null (null/null)", ValidationMessage.IssueSeverity.ERROR));
174        status(ed, ProfileUtilities.STATUS_ERROR);
175      }
176      if (vLeft == null && nullStatus == EITHER_NULL)
177        return true;
178      if (vRight == null && nullStatus == EITHER_NULL)
179        return true;
180      if (vLeft == null || vRight == null || !Base.compareDeep(vLeft, vRight, false)) {
181        messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "Must be the same ("+toString(vLeft)+"/"+toString(vRight)+")", ValidationMessage.IssueSeverity.ERROR));
182        status(ed, ProfileUtilities.STATUS_ERROR);
183      }
184      return true;
185    }
186
187    private boolean rule(ElementDefinition ed, boolean test, String path, String message) {
188      if (!test)  {
189        messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, message, ValidationMessage.IssueSeverity.ERROR));
190        status(ed, ProfileUtilities.STATUS_ERROR);
191      }
192      return test;
193    }
194
195    private boolean ruleEqual(ElementDefinition ed, boolean vLeft, boolean vRight, String path, String elementName) {
196      if (vLeft != vRight) {
197        messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, elementName+" must be the same ("+vLeft+"/"+vRight+")", ValidationMessage.IssueSeverity.ERROR));
198        status(ed, ProfileUtilities.STATUS_ERROR);
199      }
200      return true;
201    }
202
203    private String toString(Type val) throws IOException {
204      if (val instanceof PrimitiveType) 
205        return "\"" + ((PrimitiveType) val).getValueAsString()+"\"";
206      
207      IParser jp = context.newJsonParser();
208      return jp.composeString(val, "value");
209    }
210    
211    public String getErrorCount() {
212      int c = 0;
213      for (ValidationMessage vm : messages)
214        if (vm.getLevel() == ValidationMessage.IssueSeverity.ERROR)
215          c++;
216      return Integer.toString(c);
217    }
218
219    public String getWarningCount() {
220      int c = 0;
221      for (ValidationMessage vm : messages)
222        if (vm.getLevel() == ValidationMessage.IssueSeverity.WARNING)
223          c++;
224      return Integer.toString(c);
225    }
226    
227    public String getHintCount() {
228      int c = 0;
229      for (ValidationMessage vm : messages)
230        if (vm.getLevel() == ValidationMessage.IssueSeverity.INFORMATION)
231          c++;
232      return Integer.toString(c);
233    }
234  }
235  
236  /**
237   * Value sets used in the subset and superset
238   */
239  private List<ValueSet> valuesets = new ArrayList<ValueSet>();
240  private List<ProfileComparison> comparisons = new ArrayList<ProfileComparison>();
241  private String id; 
242  private String title;
243  private String leftLink;
244  private String leftName;
245  private String rightLink;
246  private String rightName;
247  
248  
249  public List<ValueSet> getValuesets() {
250    return valuesets;
251  }
252
253  public void status(ElementDefinition ed, int value) {
254    ed.setUserData(ProfileUtilities.UD_ERROR_STATUS, Math.max(value, ed.getUserInt("error-status")));
255  }
256
257  public List<ProfileComparison> getComparisons() {
258    return comparisons;
259  }
260
261  /**
262   * Compare left and right structure definitions to see whether they are consistent or not
263   * 
264   * Note that left and right are arbitrary choices. In one respect, left 
265   * is 'preferred' - the left's example value and data sets will be selected 
266   * over the right ones in the common structure definition
267   * @throws DefinitionException 
268   * @throws IOException 
269   * @throws FHIRFormatError 
270   *  
271   * @
272   */
273  public ProfileComparison compareProfiles(StructureDefinition left, StructureDefinition right) throws DefinitionException, IOException, FHIRFormatError {
274    ProfileComparison outcome = new ProfileComparison();
275    outcome.left = left;
276    outcome.right = right;
277    
278    if (left == null)
279      throw new DefinitionException("No StructureDefinition provided (left)");
280    if (right == null)
281      throw new DefinitionException("No StructureDefinition provided (right)");
282    if (!left.hasSnapshot())
283      throw new DefinitionException("StructureDefinition has no snapshot (left: "+outcome.leftName()+")");
284    if (!right.hasSnapshot())
285      throw new DefinitionException("StructureDefinition has no snapshot (right: "+outcome.rightName()+")");
286    if (left.getSnapshot().getElement().isEmpty())
287      throw new DefinitionException("StructureDefinition snapshot is empty (left: "+outcome.leftName()+")");
288    if (right.getSnapshot().getElement().isEmpty())
289      throw new DefinitionException("StructureDefinition snapshot is empty (right: "+outcome.rightName()+")");
290
291    for (ProfileComparison pc : comparisons) 
292      if (pc.left.getUrl().equals(left.getUrl()) && pc.right.getUrl().equals(right.getUrl()))
293        return pc;
294
295    outcome.id = Integer.toString(comparisons.size()+1);
296    comparisons.add(outcome);
297    
298    DefinitionNavigator ln = new DefinitionNavigator(context, left);
299    DefinitionNavigator rn = new DefinitionNavigator(context, right);
300    
301    // from here on in, any issues go in messages
302    outcome.superset = new StructureDefinition();
303    outcome.subset = new StructureDefinition();
304    if (outcome.ruleEqual(ln.path(), null,ln.path(), rn.path(), "Base Type is not compatible", false)) {
305      if (compareElements(outcome, ln.path(), ln, rn)) {
306        outcome.subset.setName("intersection of "+outcome.leftName()+" and "+outcome.rightName());
307        outcome.subset.setStatus(PublicationStatus.DRAFT);
308        outcome.subset.setKind(outcome.left.getKind());
309        outcome.subset.setType(outcome.left.getType());
310        outcome.subset.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/"+outcome.subset.getType());
311        outcome.subset.setDerivation(TypeDerivationRule.CONSTRAINT);
312        outcome.subset.setAbstract(false);
313        outcome.superset.setName("union of "+outcome.leftName()+" and "+outcome.rightName());
314        outcome.superset.setStatus(PublicationStatus.DRAFT);
315        outcome.superset.setKind(outcome.left.getKind());
316        outcome.superset.setType(outcome.left.getType());
317        outcome.superset.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/"+outcome.subset.getType());
318        outcome.superset.setAbstract(false);
319        outcome.superset.setDerivation(TypeDerivationRule.CONSTRAINT);
320      } else {
321        outcome.subset = null;
322        outcome.superset = null;
323      }
324    }
325    return outcome;
326  }
327
328  /**
329   * left and right refer to the same element. Are they compatible?   
330   * @param outcome 
331   * @param outcome
332   * @param path
333   * @param left
334   * @param right
335   * @- if there's a problem that needs fixing in this code
336   * @throws DefinitionException 
337   * @throws IOException 
338   * @throws FHIRFormatError 
339   */
340  private boolean compareElements(ProfileComparison outcome, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError {
341//    preconditions:
342    assert(path != null);
343    assert(left != null);
344    assert(right != null);
345    assert(left.path().equals(right.path()));
346    
347    // we ignore slicing right now - we're going to clone the root one anyway, and then think about clones 
348    // simple stuff
349    ElementDefinition subset = new ElementDefinition();
350    subset.setPath(left.path());
351    
352    // not allowed to be different: 
353    subset.getRepresentation().addAll(left.current().getRepresentation()); // can't be bothered even testing this one
354    if (!outcome.ruleCompares(subset, left.current().getDefaultValue(), right.current().getDefaultValue(), path+".defaultValue[x]", BOTH_NULL))
355      return false;
356    subset.setDefaultValue(left.current().getDefaultValue());
357    if (!outcome.ruleEqual(path, subset, left.current().getMeaningWhenMissing(), right.current().getMeaningWhenMissing(), "meaningWhenMissing Must be the same", true))
358      return false;
359    subset.setMeaningWhenMissing(left.current().getMeaningWhenMissing());
360    if (!outcome.ruleEqual(subset, left.current().getIsModifier(), right.current().getIsModifier(), path, "isModifier"))
361      return false;
362    subset.setIsModifier(left.current().getIsModifier());
363    if (!outcome.ruleEqual(subset, left.current().getIsSummary(), right.current().getIsSummary(), path, "isSummary"))
364      return false;
365    subset.setIsSummary(left.current().getIsSummary());
366    
367    // descriptive properties from ElementDefinition - merge them:
368    subset.setLabel(mergeText(subset, outcome, path, "label", left.current().getLabel(), right.current().getLabel()));
369    subset.setShort(mergeText(subset, outcome, path, "short", left.current().getShort(), right.current().getShort()));
370    subset.setDefinition(mergeText(subset, outcome, path, "definition", left.current().getDefinition(), right.current().getDefinition()));
371    subset.setComment(mergeText(subset, outcome, path, "comments", left.current().getComment(), right.current().getComment()));
372    subset.setRequirements(mergeText(subset, outcome, path, "requirements", left.current().getRequirements(), right.current().getRequirements()));
373    subset.getCode().addAll(mergeCodings(left.current().getCode(), right.current().getCode()));
374    subset.getAlias().addAll(mergeStrings(left.current().getAlias(), right.current().getAlias()));
375    subset.getMapping().addAll(mergeMappings(left.current().getMapping(), right.current().getMapping()));
376    // left will win for example
377    subset.setExample(left.current().hasExample() ? left.current().getExample() : right.current().getExample());
378
379    subset.setMustSupport(left.current().getMustSupport() || right.current().getMustSupport());
380    ElementDefinition superset = subset.copy();
381
382
383    // compare and intersect
384    superset.setMin(unionMin(left.current().getMin(), right.current().getMin()));
385    superset.setMax(unionMax(left.current().getMax(), right.current().getMax()));
386    subset.setMin(intersectMin(left.current().getMin(), right.current().getMin()));
387    subset.setMax(intersectMax(left.current().getMax(), right.current().getMax()));
388    outcome.rule(subset, subset.getMax().equals("*") || Integer.parseInt(subset.getMax()) >= subset.getMin(), path, "Cardinality Mismatch: "+card(left)+"/"+card(right));
389    
390    superset.getType().addAll(unionTypes(path, left.current().getType(), right.current().getType()));
391    subset.getType().addAll(intersectTypes(subset, outcome, path, left.current().getType(), right.current().getType()));
392    outcome.rule(subset, !subset.getType().isEmpty() || (!left.current().hasType() && !right.current().hasType()), path, "Type Mismatch:\r\n  "+typeCode(left)+"\r\n  "+typeCode(right));
393//    <fixed[x]><!-- ?? 0..1 * Value must be exactly this --></fixed[x]>
394//    <pattern[x]><!-- ?? 0..1 * Value must have at least these property values --></pattern[x]>
395    superset.setMaxLengthElement(unionMaxLength(left.current().getMaxLength(), right.current().getMaxLength()));
396    subset.setMaxLengthElement(intersectMaxLength(left.current().getMaxLength(), right.current().getMaxLength()));
397    if (left.current().hasBinding() || right.current().hasBinding()) {
398      compareBindings(outcome, subset, superset, path, left.current(), right.current());
399    }
400
401    // note these are backwards
402    superset.getConstraint().addAll(intersectConstraints(path, left.current().getConstraint(), right.current().getConstraint()));
403    subset.getConstraint().addAll(unionConstraints(subset, outcome, path, left.current().getConstraint(), right.current().getConstraint()));
404
405    // now process the slices
406    if (left.current().hasSlicing() || right.current().hasSlicing()) {
407      if (isExtension(left.path()))
408        return compareExtensions(outcome, path, superset, subset, left, right);
409//      return true;
410      else {
411        ElementDefinitionSlicingComponent slicingL = left.current().getSlicing();
412        ElementDefinitionSlicingComponent slicingR = right.current().getSlicing();
413        throw new DefinitionException("Slicing is not handled yet");
414      }
415    // todo: name 
416    }
417
418    // add the children
419    outcome.subset.getSnapshot().getElement().add(subset);
420    outcome.superset.getSnapshot().getElement().add(superset);
421    return compareChildren(subset, outcome, path, left, right);
422  }
423
424  private class ExtensionUsage {
425    private DefinitionNavigator defn;
426    private int minSuperset;
427    private int minSubset;
428    private String maxSuperset;
429    private String maxSubset;
430    private boolean both = false;
431    
432    public ExtensionUsage(DefinitionNavigator defn, int min, String max) {
433      super();
434      this.defn = defn;
435      this.minSubset = min;
436      this.minSuperset = min;
437      this.maxSubset = max;
438      this.maxSuperset = max;
439    }
440    
441  }
442  private boolean compareExtensions(ProfileComparison outcome, String path, ElementDefinition superset, ElementDefinition subset, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException {
443    // for now, we don't handle sealed (or ordered) extensions
444    
445    // for an extension the superset is all extensions, and the subset is.. all extensions - well, unless thay are sealed. 
446    // but it's not useful to report that. instead, we collate the defined ones, and just adjust the cardinalities
447    Map<String, ExtensionUsage> map = new HashMap<String, ExtensionUsage>();
448    
449    if (left.slices() != null)
450      for (DefinitionNavigator ex : left.slices()) {
451        String url = ex.current().getType().get(0).getProfile().get(0).getValue();
452        if (map.containsKey(url))
453          throw new DefinitionException("Duplicate Extension "+url+" at "+path);
454        else
455          map.put(url, new ExtensionUsage(ex, ex.current().getMin(), ex.current().getMax()));
456      }
457    if (right.slices() != null)
458      for (DefinitionNavigator ex : right.slices()) {
459        String url = ex.current().getType().get(0).getProfile().get(0).getValue();
460        if (map.containsKey(url)) {
461          ExtensionUsage exd = map.get(url);
462          exd.minSuperset = unionMin(exd.defn.current().getMin(), ex.current().getMin());
463          exd.maxSuperset = unionMax(exd.defn.current().getMax(), ex.current().getMax());
464          exd.minSubset = intersectMin(exd.defn.current().getMin(), ex.current().getMin());
465          exd.maxSubset = intersectMax(exd.defn.current().getMax(), ex.current().getMax());
466          exd.both = true;
467          outcome.rule(subset, exd.maxSubset.equals("*") || Integer.parseInt(exd.maxSubset) >= exd.minSubset, path, "Cardinality Mismatch on extension: "+card(exd.defn)+"/"+card(ex));
468        } else {
469          map.put(url, new ExtensionUsage(ex, ex.current().getMin(), ex.current().getMax()));
470        }
471      }
472    List<String> names = new ArrayList<String>();
473    names.addAll(map.keySet());
474    Collections.sort(names);
475    for (String name : names) {
476      ExtensionUsage exd = map.get(name);
477      if (exd.both)
478        outcome.subset.getSnapshot().getElement().add(exd.defn.current().copy().setMin(exd.minSubset).setMax(exd.maxSubset));
479      outcome.superset.getSnapshot().getElement().add(exd.defn.current().copy().setMin(exd.minSuperset).setMax(exd.maxSuperset));
480    }    
481    return true;
482  }
483
484  private boolean isExtension(String path) {
485    return path.endsWith(".extension") || path.endsWith(".modifierExtension");
486  }
487
488  private boolean compareChildren(ElementDefinition ed, ProfileComparison outcome, String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError {
489    List<DefinitionNavigator> lc = left.children();
490    List<DefinitionNavigator> rc = right.children();
491    // it's possible that one of these profiles walks into a data type and the other doesn't
492    // if it does, we have to load the children for that data into the profile that doesn't 
493    // walk into it
494    if (lc.isEmpty() && !rc.isEmpty() && right.current().getType().size() == 1 && left.hasTypeChildren(right.current().getType().get(0)))
495      lc = left.childrenFromType(right.current().getType().get(0));
496    if (rc.isEmpty() && !lc.isEmpty() && left.current().getType().size() == 1 && right.hasTypeChildren(left.current().getType().get(0)))
497      rc = right.childrenFromType(left.current().getType().get(0));
498    if (lc.size() != rc.size()) {
499      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));
500      status(ed, ProfileUtilities.STATUS_ERROR);
501      return false;      
502    } else {
503      for (int i = 0; i < lc.size(); i++) {
504        DefinitionNavigator l = lc.get(i);
505        DefinitionNavigator r = rc.get(i);
506        String cpath = comparePaths(l.path(), r.path(), path, l.nameTail(), r.nameTail());
507        if (cpath != null) {
508          if (!compareElements(outcome, cpath, l, r))
509            return false;
510        } else {
511          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));
512          status(ed, ProfileUtilities.STATUS_ERROR);
513          return false;
514        }
515      }
516    }
517    return true;
518  }
519
520  private String comparePaths(String path1, String path2, String path, String tail1, String tail2) {
521    if (tail1.equals(tail2)) {
522      return path+"."+tail1;
523    } else if (tail1.endsWith("[x]") && tail2.startsWith(tail1.substring(0, tail1.length()-3))) {
524      return path+"."+tail1;
525    } else if (tail2.endsWith("[x]") && tail1.startsWith(tail2.substring(0, tail2.length()-3))) {
526      return path+"."+tail2;
527    } else 
528      return null;
529  }
530
531  private boolean compareBindings(ProfileComparison outcome, ElementDefinition subset, ElementDefinition superset, String path, ElementDefinition lDef, ElementDefinition rDef) throws FHIRFormatError {
532    assert(lDef.hasBinding() || rDef.hasBinding());
533    if (!lDef.hasBinding()) {
534      subset.setBinding(rDef.getBinding());
535      // technically, the super set is unbound, but that's not very useful - so we use the provided on as an example
536      superset.setBinding(rDef.getBinding().copy());
537      superset.getBinding().setStrength(BindingStrength.EXAMPLE);
538      return true;
539    }
540    if (!rDef.hasBinding()) {
541      subset.setBinding(lDef.getBinding());
542      superset.setBinding(lDef.getBinding().copy());
543      superset.getBinding().setStrength(BindingStrength.EXAMPLE);
544      return true;
545    }
546    ElementDefinitionBindingComponent left = lDef.getBinding();
547    ElementDefinitionBindingComponent right = rDef.getBinding();
548    if (Base.compareDeep(left, right, false)) {
549      subset.setBinding(left);
550      superset.setBinding(right);      
551    }
552    
553    // if they're both examples/preferred then:
554    // subset: left wins if they're both the same
555    // superset: 
556    if (isPreferredOrExample(left) && isPreferredOrExample(right)) {
557      if (right.getStrength() == BindingStrength.PREFERRED && left.getStrength() == BindingStrength.EXAMPLE && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) { 
558        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));
559        status(subset, ProfileUtilities.STATUS_HINT);
560        subset.setBinding(right);
561        superset.setBinding(unionBindings(superset, outcome, path, left, right));
562      } else {
563        if ((right.getStrength() != BindingStrength.EXAMPLE || left.getStrength() != BindingStrength.EXAMPLE) && !Base.compareDeep(left.getValueSet(), right.getValueSet(), false) ) { 
564          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));
565          status(subset, ProfileUtilities.STATUS_HINT);
566        }
567        subset.setBinding(left);
568        superset.setBinding(unionBindings(superset, outcome, path, left, right));
569      }
570      return true;
571    }
572    // if either of them are extensible/required, then it wins
573    if (isPreferredOrExample(left)) {
574      subset.setBinding(right);
575      superset.setBinding(unionBindings(superset, outcome, path, left, right));
576      return true;
577    }
578    if (isPreferredOrExample(right)) {
579      subset.setBinding(left);
580      superset.setBinding(unionBindings(superset, outcome, path, left, right));
581      return true;
582    }
583    
584    // ok, both are extensible or required.
585    ElementDefinitionBindingComponent subBinding = new ElementDefinitionBindingComponent();
586    subset.setBinding(subBinding);
587    ElementDefinitionBindingComponent superBinding = new ElementDefinitionBindingComponent();
588    superset.setBinding(superBinding);
589    subBinding.setDescription(mergeText(subset, outcome, path, "description", left.getDescription(), right.getDescription()));
590    superBinding.setDescription(mergeText(subset, outcome, null, "description", left.getDescription(), right.getDescription()));
591    if (left.getStrength() == BindingStrength.REQUIRED || right.getStrength() == BindingStrength.REQUIRED)
592      subBinding.setStrength(BindingStrength.REQUIRED);
593    else
594      subBinding.setStrength(BindingStrength.EXTENSIBLE);
595    if (left.getStrength() == BindingStrength.EXTENSIBLE || right.getStrength() == BindingStrength.EXTENSIBLE)
596      superBinding.setStrength(BindingStrength.EXTENSIBLE);
597    else
598      superBinding.setStrength(BindingStrength.REQUIRED);
599    
600    if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false)) {
601      subBinding.setValueSet(left.getValueSet());
602      superBinding.setValueSet(left.getValueSet());
603      return true;
604    } else if (!left.hasValueSet()) {
605      outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "No left Value set at "+path, ValidationMessage.IssueSeverity.ERROR));
606      return true;      
607    } else if (!right.hasValueSet()) {
608      outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.STRUCTURE, path, "No right Value set at "+path, ValidationMessage.IssueSeverity.ERROR));
609      return true;      
610    } else {
611      // ok, now we compare the value sets. This may be unresolvable. 
612      ValueSet lvs = resolveVS(outcome.left, left.getValueSet());
613      ValueSet rvs = resolveVS(outcome.right, right.getValueSet());
614      if (lvs == null) {
615        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));
616        return true;
617      } else if (rvs == null) {
618        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));
619        return true;        
620      } else {
621        // first, we'll try to do it by definition
622        ValueSet cvs = intersectByDefinition(lvs, rvs);
623        if(cvs == null) {
624          // if that didn't work, we'll do it by expansion
625          ValueSetExpansionOutcome le;
626          ValueSetExpansionOutcome re;
627          try {
628            le = context.expandVS(lvs, true, false);
629            re = context.expandVS(rvs, true, false);
630            if (!closed(le.getValueset()) || !closed(re.getValueset())) 
631              throw new DefinitionException("unclosed value sets are not handled yet");
632            cvs = intersectByExpansion(lvs, rvs);
633            if (!cvs.getCompose().hasInclude()) {
634              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));
635              status(subset, ProfileUtilities.STATUS_ERROR);
636              return false;
637            }
638          } catch (Exception e){
639            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));
640            status(subset, ProfileUtilities.STATUS_ERROR);
641            return false;          
642          }
643        }
644        subBinding.setValueSet("#"+addValueSet(cvs));
645        superBinding.setValueSet("#"+addValueSet(unite(superset, outcome, path, lvs, rvs)));
646      }
647    }
648    return false;
649  }
650
651  private ElementDefinitionBindingComponent unionBindings(ElementDefinition ed, ProfileComparison outcome, String path, ElementDefinitionBindingComponent left, ElementDefinitionBindingComponent right) throws FHIRFormatError {
652    ElementDefinitionBindingComponent union = new ElementDefinitionBindingComponent();
653    if (left.getStrength().compareTo(right.getStrength()) < 0)
654      union.setStrength(left.getStrength());
655    else
656      union.setStrength(right.getStrength());
657    union.setDescription(mergeText(ed, outcome, path, "binding.description", left.getDescription(), right.getDescription()));
658    if (Base.compareDeep(left.getValueSet(), right.getValueSet(), false))
659      union.setValueSet(left.getValueSet());
660    else {
661      ValueSet lvs = resolveVS(outcome.left, left.getValueSet());
662      ValueSet rvs = resolveVS(outcome.left, right.getValueSet());
663      if (lvs != null && rvs != null)
664        union.setValueSet("#"+addValueSet(unite(ed, outcome, path, lvs, rvs)));
665      else if (lvs != null)
666        union.setValueSet("#"+addValueSet(lvs));
667      else if (rvs != null)
668        union.setValueSet("#"+addValueSet(rvs));
669    }
670    return union;
671  }
672
673  
674  private ValueSet unite(ElementDefinition ed, ProfileComparison outcome, String path, ValueSet lvs, ValueSet rvs) {
675    ValueSet vs = new ValueSet();
676    if (lvs.hasCompose()) {
677      for (ConceptSetComponent inc : lvs.getCompose().getInclude()) 
678        vs.getCompose().getInclude().add(inc);
679      if (lvs.getCompose().hasExclude()) {
680        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));
681        status(ed, ProfileUtilities.STATUS_ERROR);
682      }
683    }
684    if (rvs.hasCompose()) {
685      for (ConceptSetComponent inc : rvs.getCompose().getInclude())
686        if (!mergeIntoExisting(vs.getCompose().getInclude(), inc))
687          vs.getCompose().getInclude().add(inc);
688      if (rvs.getCompose().hasExclude()) {
689        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));
690        status(ed, ProfileUtilities.STATUS_ERROR);
691      }
692    }    
693    return vs;
694  }
695
696  private boolean mergeIntoExisting(List<ConceptSetComponent> include, ConceptSetComponent inc) {
697    for (ConceptSetComponent dst : include) {
698      if (Base.compareDeep(dst,  inc, false))
699        return true; // they're actually the same
700      if (dst.getSystem().equals(inc.getSystem())) {
701        if (inc.hasFilter() || dst.hasFilter()) {
702          return false; // just add the new one as a a parallel
703        } else if (inc.hasConcept() && dst.hasConcept()) {
704          for (ConceptReferenceComponent cc : inc.getConcept()) {
705            boolean found = false;
706            for (ConceptReferenceComponent dd : dst.getConcept()) {
707              if (dd.getCode().equals(cc.getCode()))
708                found = true;
709              if (found) {
710                if (cc.hasDisplay() && !dd.hasDisplay())
711                  dd.setDisplay(cc.getDisplay());
712                break;
713              }
714            }
715            if (!found)
716              dst.getConcept().add(cc.copy());
717          }
718        } else
719          dst.getConcept().clear(); // one of them includes the entire code system 
720      }
721    }
722    return false;
723  }
724
725  private ValueSet resolveVS(StructureDefinition ctxtLeft, String vsRef) {
726    if (vsRef == null)
727      return null;
728    return context.fetchResource(ValueSet.class, vsRef);
729  }
730
731  private ValueSet intersectByDefinition(ValueSet lvs, ValueSet rvs) {
732    // this is just a stub. The idea is that we try to avoid expanding big open value sets from SCT, RxNorm, LOINC.
733    // there's a bit of long hand logic coming here, but that's ok.
734    return null;
735  }
736
737  private ValueSet intersectByExpansion(ValueSet lvs, ValueSet rvs) {
738    // this is pretty straight forward - we intersect the lists, and build a compose out of the intersection
739    ValueSet vs = new ValueSet();
740    vs.setStatus(PublicationStatus.DRAFT);
741    
742    Map<String, ValueSetExpansionContainsComponent> left = new HashMap<String, ValueSetExpansionContainsComponent>();
743    scan(lvs.getExpansion().getContains(), left);
744    Map<String, ValueSetExpansionContainsComponent> right = new HashMap<String, ValueSetExpansionContainsComponent>();
745    scan(rvs.getExpansion().getContains(), right);
746    Map<String, ConceptSetComponent> inc = new HashMap<String, ConceptSetComponent>();
747    
748    for (String s : left.keySet()) {
749      if (right.containsKey(s)) {
750        ValueSetExpansionContainsComponent cc = left.get(s);
751        ConceptSetComponent c = inc.get(cc.getSystem());
752        if (c == null) {
753          c = vs.getCompose().addInclude().setSystem(cc.getSystem());
754          inc.put(cc.getSystem(), c);
755        }
756        c.addConcept().setCode(cc.getCode()).setDisplay(cc.getDisplay());
757      }
758    }
759    return vs;
760  }
761
762  private void scan(List<ValueSetExpansionContainsComponent> list, Map<String, ValueSetExpansionContainsComponent> map) {
763    for (ValueSetExpansionContainsComponent cc : list) {
764      if (cc.hasSystem() && cc.hasCode()) {
765        String s = cc.getSystem()+"::"+cc.getCode();
766        if (!map.containsKey(s))
767          map.put(s,  cc);
768      }
769      if (cc.hasContains())
770        scan(cc.getContains(), map);
771    }
772  }
773
774  private boolean closed(ValueSet vs) {
775    return !ToolingExtensions.findBooleanExtension(vs.getExpansion(), ToolingExtensions.EXT_UNCLOSED);
776  }
777
778  private boolean isPreferredOrExample(ElementDefinitionBindingComponent binding) {
779    return binding.getStrength() == BindingStrength.EXAMPLE || binding.getStrength() == BindingStrength.PREFERRED;
780  }
781
782  private Collection<? extends TypeRefComponent> intersectTypes(ElementDefinition ed, ProfileComparison outcome, String path, List<TypeRefComponent> left, List<TypeRefComponent> right) throws DefinitionException, IOException, FHIRFormatError {
783    List<TypeRefComponent> result = new ArrayList<TypeRefComponent>();
784    for (TypeRefComponent l : left) {
785      if (l.hasAggregation())
786        throw new DefinitionException("Aggregation not supported: "+path);
787      boolean pfound = false;
788      boolean tfound = false;
789      TypeRefComponent c = l.copy();
790      for (TypeRefComponent r : right) {
791        if (r.hasAggregation())
792          throw new DefinitionException("Aggregation not supported: "+path);
793        if (!l.hasProfile() && !r.hasProfile()) {
794          pfound = true;    
795        } else if (!r.hasProfile()) {
796          pfound = true; 
797        } else if (!l.hasProfile()) {
798          pfound = true;
799          c.setProfile(r.getProfile());
800        } else {
801          StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), outcome.leftName());
802          StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), outcome.rightName());
803          if (sdl != null && sdr != null) {
804            if (sdl == sdr) {
805              pfound = true;
806            } else if (derivesFrom(sdl, sdr)) {
807              pfound = true;
808            } else if (derivesFrom(sdr, sdl)) {
809              c.setProfile(r.getProfile());
810              pfound = true;
811            } else if (sdl.getType().equals(sdr.getType())) {
812              ProfileComparison comp = compareProfiles(sdl, sdr);
813              if (comp.getSubset() != null) {
814                pfound = true;
815                c.addProfile("#"+comp.id);
816              }
817            }
818          }
819        }
820        if (!l.hasTargetProfile() && !r.hasTargetProfile()) {
821          tfound = true;    
822        } else if (!r.hasTargetProfile()) {
823          tfound = true; 
824        } else if (!l.hasTargetProfile()) {
825          tfound = true;
826          c.setTargetProfile(r.getTargetProfile());
827        } else {
828          StructureDefinition sdl = resolveProfile(ed, outcome, path, l.getProfile().get(0).getValue(), outcome.leftName());
829          StructureDefinition sdr = resolveProfile(ed, outcome, path, r.getProfile().get(0).getValue(), outcome.rightName());
830          if (sdl != null && sdr != null) {
831            if (sdl == sdr) {
832              tfound = true;
833            } else if (derivesFrom(sdl, sdr)) {
834              tfound = true;
835            } else if (derivesFrom(sdr, sdl)) {
836              c.setTargetProfile(r.getTargetProfile());
837              tfound = true;
838            } else if (sdl.getType().equals(sdr.getType())) {
839              ProfileComparison comp = compareProfiles(sdl, sdr);
840              if (comp.getSubset() != null) {
841                tfound = true;
842                c.addTargetProfile("#"+comp.id);
843              }
844            }
845          }
846        }
847      }
848      if (pfound && tfound)
849        result.add(c);
850    }
851    return result;
852  }
853
854  private StructureDefinition resolveProfile(ElementDefinition ed, ProfileComparison outcome, String path, String url, String name) {
855    StructureDefinition res = context.fetchResource(StructureDefinition.class, url);
856    if (res == null) {
857      outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, path, "Unable to resolve profile "+url+" in profile "+name, ValidationMessage.IssueSeverity.WARNING));
858      status(ed, ProfileUtilities.STATUS_HINT);
859    }
860    return res;
861  }
862
863  private Collection<? extends TypeRefComponent> unionTypes(String path, List<TypeRefComponent> left, List<TypeRefComponent> right) throws DefinitionException, IOException, FHIRFormatError {
864    List<TypeRefComponent> result = new ArrayList<TypeRefComponent>();
865    for (TypeRefComponent l : left) 
866      checkAddTypeUnion(path, result, l);
867    for (TypeRefComponent r : right) 
868      checkAddTypeUnion(path, result, r);
869    return result;
870  }    
871    
872  private void checkAddTypeUnion(String path, List<TypeRefComponent> results, TypeRefComponent nw) throws DefinitionException, IOException, FHIRFormatError {
873    boolean pfound = false;
874    boolean tfound = false;
875    nw = nw.copy();
876    if (nw.hasAggregation())
877      throw new DefinitionException("Aggregation not supported: "+path);
878    for (TypeRefComponent ex : results) {
879      if (Utilities.equals(ex.getCode(), nw.getCode())) {
880        if (!ex.hasProfile() && !nw.hasProfile())
881          pfound = true;
882        else if (!ex.hasProfile()) {
883          pfound = true; 
884        } else if (!nw.hasProfile()) {
885          pfound = true;
886          ex.setProfile(null);
887        } else {
888          // both have profiles. Is one derived from the other? 
889          StructureDefinition sdex = context.fetchResource(StructureDefinition.class, ex.getProfile().get(0).getValue());
890          StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, nw.getProfile().get(0).getValue());
891          if (sdex != null && sdnw != null) {
892            if (sdex == sdnw) {
893              pfound = true;
894            } else if (derivesFrom(sdex, sdnw)) {
895              ex.setProfile(nw.getProfile());
896              pfound = true;
897            } else if (derivesFrom(sdnw, sdex)) {
898              pfound = true;
899            } else if (sdnw.getSnapshot().getElement().get(0).getPath().equals(sdex.getSnapshot().getElement().get(0).getPath())) {
900              ProfileComparison comp = compareProfiles(sdex, sdnw);
901              if (comp.getSuperset() != null) {
902                pfound = true;
903                ex.addProfile("#"+comp.id);
904              }
905            }
906          }
907        }        
908        if (!ex.hasTargetProfile() && !nw.hasTargetProfile())
909          tfound = true;
910        else if (!ex.hasTargetProfile()) {
911          tfound = true; 
912        } else if (!nw.hasTargetProfile()) {
913          tfound = true;
914          ex.setTargetProfile(null);
915        } else {
916          // both have profiles. Is one derived from the other? 
917          StructureDefinition sdex = context.fetchResource(StructureDefinition.class, ex.getTargetProfile().get(0).getValue());
918          StructureDefinition sdnw = context.fetchResource(StructureDefinition.class, nw.getTargetProfile().get(0).getValue());
919          if (sdex != null && sdnw != null) {
920            if (sdex == sdnw) {
921              tfound = true;
922            } else if (derivesFrom(sdex, sdnw)) {
923              ex.setTargetProfile(nw.getTargetProfile());
924              tfound = true;
925            } else if (derivesFrom(sdnw, sdex)) {
926              tfound = true;
927            } else if (sdnw.getSnapshot().getElement().get(0).getPath().equals(sdex.getSnapshot().getElement().get(0).getPath())) {
928              ProfileComparison comp = compareProfiles(sdex, sdnw);
929              if (comp.getSuperset() != null) {
930                tfound = true;
931                ex.addTargetProfile("#"+comp.id);
932              }
933            }
934          }
935        }        
936      }
937    }
938    if (!tfound || !pfound)
939      results.add(nw);      
940  }
941
942  
943  private boolean derivesFrom(StructureDefinition left, StructureDefinition right) {
944    // left derives from right if it's base is the same as right
945    // todo: recursive...
946    return left.hasBaseDefinition() && left.getBaseDefinition().equals(right.getUrl());
947  }
948
949
950  private String mergeText(ElementDefinition ed, ProfileComparison outcome, String path, String name, String left, String right) {
951    if (left == null && right == null)
952      return null;
953    if (left == null)
954      return right;
955    if (right == null)
956      return left;
957    if (left.equalsIgnoreCase(right))
958      return left;
959    if (path != null) {
960      outcome.messages.add(new ValidationMessage(Source.ProfileComparer, ValidationMessage.IssueType.INFORMATIONAL, path, "Elements differ in definition for "+name+":\r\n  \""+left+"\"\r\n  \""+right+"\"", 
961          "Elements differ in definition for "+name+":<br/>\""+Utilities.escapeXml(left)+"\"<br/>\""+Utilities.escapeXml(right)+"\"", ValidationMessage.IssueSeverity.INFORMATION));
962      status(ed, ProfileUtilities.STATUS_HINT);
963    }
964    return "left: "+left+"; right: "+right;
965  }
966
967  private List<Coding> mergeCodings(List<Coding> left, List<Coding> right) {
968    List<Coding> result = new ArrayList<Coding>();
969    result.addAll(left);
970    for (Coding c : right) {
971      boolean found = false;
972      for (Coding ct : left)
973        if (Utilities.equals(c.getSystem(), ct.getSystem()) && Utilities.equals(c.getCode(), ct.getCode()))
974          found = true;
975      if (!found)
976        result.add(c);
977    }
978    return result;
979  }
980
981  private List<StringType> mergeStrings(List<StringType> left, List<StringType> right) {
982    List<StringType> result = new ArrayList<StringType>();
983    result.addAll(left);
984    for (StringType c : right) {
985      boolean found = false;
986      for (StringType ct : left)
987        if (Utilities.equals(c.getValue(), ct.getValue()))
988          found = true;
989      if (!found)
990        result.add(c);
991    }
992    return result;
993  }
994
995  private List<ElementDefinitionMappingComponent> mergeMappings(List<ElementDefinitionMappingComponent> left, List<ElementDefinitionMappingComponent> right) {
996    List<ElementDefinitionMappingComponent> result = new ArrayList<ElementDefinitionMappingComponent>();
997    result.addAll(left);
998    for (ElementDefinitionMappingComponent c : right) {
999      boolean found = false;
1000      for (ElementDefinitionMappingComponent ct : left)
1001        if (Utilities.equals(c.getIdentity(), ct.getIdentity()) && Utilities.equals(c.getLanguage(), ct.getLanguage()) && Utilities.equals(c.getMap(), ct.getMap()))
1002          found = true;
1003      if (!found)
1004        result.add(c);
1005    }
1006    return result;
1007  }
1008
1009  // we can't really know about constraints. We create warnings, and collate them 
1010  private List<ElementDefinitionConstraintComponent> unionConstraints(ElementDefinition ed, ProfileComparison outcome, String path, List<ElementDefinitionConstraintComponent> left, List<ElementDefinitionConstraintComponent> right) {
1011    List<ElementDefinitionConstraintComponent> result = new ArrayList<ElementDefinitionConstraintComponent>();
1012    for (ElementDefinitionConstraintComponent l : left) {
1013      boolean found = false;
1014      for (ElementDefinitionConstraintComponent r : right)
1015        if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity()))
1016          found = true;
1017      if (!found) {
1018        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));
1019        status(ed, ProfileUtilities.STATUS_WARNING);
1020      }
1021      result.add(l);
1022    }
1023    for (ElementDefinitionConstraintComponent r : right) {
1024      boolean found = false;
1025      for (ElementDefinitionConstraintComponent l : left)
1026        if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity()))
1027          found = true;
1028      if (!found) {
1029        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));
1030        status(ed, ProfileUtilities.STATUS_WARNING);
1031        result.add(r);
1032      }
1033    }
1034    return result;
1035  }
1036
1037
1038  private List<ElementDefinitionConstraintComponent> intersectConstraints(String path, List<ElementDefinitionConstraintComponent> left, List<ElementDefinitionConstraintComponent> right) {
1039  List<ElementDefinitionConstraintComponent> result = new ArrayList<ElementDefinitionConstraintComponent>();
1040  for (ElementDefinitionConstraintComponent l : left) {
1041    boolean found = false;
1042    for (ElementDefinitionConstraintComponent r : right)
1043      if (Utilities.equals(r.getId(), l.getId()) || (Utilities.equals(r.getXpath(), l.getXpath()) && r.getSeverity() == l.getSeverity()))
1044        found = true;
1045    if (found)
1046      result.add(l);
1047  }
1048  return result;
1049}
1050
1051  private String card(DefinitionNavigator defn) {
1052    return Integer.toString(defn.current().getMin())+".."+defn.current().getMax();
1053  }
1054  
1055  private String typeCode(DefinitionNavigator defn) {
1056    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1057    for (TypeRefComponent t : defn.current().getType())
1058      b.append(t.getCode()+(t.hasProfile() ? "("+t.getProfile()+")" : "")+(t.hasTargetProfile() ? "("+t.getTargetProfile()+")" : "")); // todo: other properties
1059    return b.toString();
1060  }
1061
1062  private int intersectMin(int left, int right) {
1063    if (left > right)
1064      return left;
1065    else
1066      return right;
1067  }
1068
1069  private int unionMin(int left, int right) {
1070    if (left > right)
1071      return right;
1072    else
1073      return left;
1074  }
1075
1076  private String intersectMax(String left, String right) {
1077    int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left);
1078    int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right);
1079    if (l < r)
1080      return left;
1081    else
1082      return right;
1083  }
1084
1085  private String unionMax(String left, String right) {
1086    int l = "*".equals(left) ? Integer.MAX_VALUE : Integer.parseInt(left);
1087    int r = "*".equals(right) ? Integer.MAX_VALUE : Integer.parseInt(right);
1088    if (l < r)
1089      return right;
1090    else
1091      return left;
1092  }
1093
1094  private IntegerType intersectMaxLength(int left, int right) {
1095    if (left == 0) 
1096      left = Integer.MAX_VALUE;
1097    if (right == 0) 
1098      right = Integer.MAX_VALUE;
1099    if (left < right)
1100      return left == Integer.MAX_VALUE ? null : new IntegerType(left);
1101    else
1102      return right == Integer.MAX_VALUE ? null : new IntegerType(right);
1103  }
1104
1105  private IntegerType unionMaxLength(int left, int right) {
1106    if (left == 0) 
1107      left = Integer.MAX_VALUE;
1108    if (right == 0) 
1109      right = Integer.MAX_VALUE;
1110    if (left < right)
1111      return right == Integer.MAX_VALUE ? null : new IntegerType(right);
1112    else
1113      return left == Integer.MAX_VALUE ? null : new IntegerType(left);
1114  }
1115
1116  
1117  public String addValueSet(ValueSet cvs) {
1118    String id = Integer.toString(valuesets.size()+1);
1119    cvs.setId(id);
1120    valuesets.add(cvs);
1121    return id;
1122  }
1123
1124  
1125  
1126  public String getId() {
1127    return id;
1128  }
1129
1130  public void setId(String id) {
1131    this.id = id;
1132  }
1133
1134  public String getTitle() {
1135    return title;
1136  }
1137
1138  public void setTitle(String title) {
1139    this.title = title;
1140  }
1141
1142  public String getLeftLink() {
1143    return leftLink;
1144  }
1145
1146  public void setLeftLink(String leftLink) {
1147    this.leftLink = leftLink;
1148  }
1149
1150  public String getLeftName() {
1151    return leftName;
1152  }
1153
1154  public void setLeftName(String leftName) {
1155    this.leftName = leftName;
1156  }
1157
1158  public String getRightLink() {
1159    return rightLink;
1160  }
1161
1162  public void setRightLink(String rightLink) {
1163    this.rightLink = rightLink;
1164  }
1165
1166  public String getRightName() {
1167    return rightName;
1168  }
1169
1170  public void setRightName(String rightName) {
1171    this.rightName = rightName;
1172  }
1173
1174  private String genPCLink(String leftName, String leftLink) {
1175    return "<a href=\""+leftLink+"\">"+Utilities.escapeXml(leftName)+"</a>";
1176  }
1177  
1178  private String genPCTable() {
1179    StringBuilder b = new StringBuilder();
1180
1181    b.append("<table class=\"grid\">\r\n");
1182    b.append("<tr>");
1183    b.append(" <td><b>Left</b></td>");
1184    b.append(" <td><b>Right</b></td>");
1185    b.append(" <td><b>Comparison</b></td>");
1186    b.append(" <td><b>Error #</b></td>");
1187    b.append(" <td><b>Warning #</b></td>");
1188    b.append(" <td><b>Hint #</b></td>");
1189    b.append("</tr>");
1190
1191    for (ProfileComparison cmp : getComparisons()) {
1192      b.append("<tr>");
1193      b.append(" <td><a href=\""+cmp.getLeft().getUserString("path")+"\">"+Utilities.escapeXml(cmp.getLeft().getName())+"</a></td>");
1194      b.append(" <td><a href=\""+cmp.getRight().getUserString("path")+"\">"+Utilities.escapeXml(cmp.getRight().getName())+"</a></td>");
1195      b.append(" <td><a href=\""+getId()+"."+cmp.getId()+".html\">Click Here</a></td>");
1196      b.append(" <td>"+cmp.getErrorCount()+"</td>");
1197      b.append(" <td>"+cmp.getWarningCount()+"</td>");
1198      b.append(" <td>"+cmp.getHintCount()+"</td>");
1199      b.append("</tr>");
1200    }
1201    b.append("</table>\r\n");
1202
1203    return b.toString();
1204  }
1205
1206
1207  public String generate(String dest) throws IOException {
1208    // ok, all compared; now produce the output
1209    // first page we produce is simply the index
1210    Map<String, String> vars = new HashMap<String, String>();
1211    vars.put("title", getTitle());
1212    vars.put("left", genPCLink(getLeftName(), getLeftLink()));
1213    vars.put("right", genPCLink(getRightName(), getRightLink()));
1214    vars.put("table", genPCTable());
1215    producePage(summaryTemplate(), Utilities.path(dest, getId()+".html"), vars);
1216    
1217//    page.log("   ... generate", LogMessageType.Process);
1218//    String src = TextFile.fileToString(page.getFolders().srcDir + "template-comparison-set.html");
1219//    src = page.processPageIncludes(n+".html", src, "?type", null, "??path", null, null, "Comparison", pc, null, null, page.getDefinitions().getWorkgroups().get("fhir"));
1220//    TextFile.stringToFile(src, Utilities.path(page.getFolders().dstDir, n+".html"));
1221//    cachePage(n + ".html", src, "Comparison "+pc.getTitle(), false);
1222//
1223//    // then we produce a comparison page for each pair
1224//    for (ProfileComparison cmp : pc.getComparisons()) {
1225//      src = TextFile.fileToString(page.getFolders().srcDir + "template-comparison.html");
1226//      src = page.processPageIncludes(n+"."+cmp.getId()+".html", src, "?type", null, "??path", null, null, "Comparison", cmp, null, null, page.getDefinitions().getWorkgroups().get("fhir"));
1227//      TextFile.stringToFile(src, Utilities.path(page.getFolders().dstDir, n+"."+cmp.getId()+".html"));
1228//      cachePage(n +"."+cmp.getId()+".html", src, "Comparison "+pc.getTitle(), false);
1229//    }
1230//      //   and also individual pages for each pair outcome
1231//    // then we produce value set pages for each value set
1232//
1233//    // TODO Auto-generated method stub
1234    return null;
1235  }
1236
1237  private void producePage(String src, String path, Map<String, String> vars) throws IOException {
1238    while (src.contains("[%"))
1239    {
1240      int i1 = src.indexOf("[%");
1241      int i2 = src.substring(i1).indexOf("%]")+i1;
1242      String s1 = src.substring(0, i1);
1243      String s2 = src.substring(i1 + 2, i2).trim();
1244      String s3 = src.substring(i2+2);
1245      String v = vars.containsKey(s2) ? vars.get(s2) : "???";
1246      src = s1+v+s3;
1247    }
1248    TextFile.stringToFile(src, path);
1249  }
1250
1251  private String summaryTemplate() throws IOException {
1252    return cachedFetch("04a9d69a-47f2-4250-8645-bf5d880a8eaa-1.fhir-template", "http://build.fhir.org/template-comparison-set.html.template");
1253  }
1254
1255  private String cachedFetch(String id, String source) throws IOException {
1256    String tmpDir = System.getProperty("java.io.tmpdir");
1257    String local = Utilities.path(tmpDir, id);
1258    File f = new File(local);
1259    if (f.exists())
1260      return TextFile.fileToString(f);
1261    URL url = new URL(source);
1262    URLConnection c = url.openConnection();
1263    String result = TextFile.streamToString(c.getInputStream());
1264    TextFile.stringToFile(result, f);
1265    return result;
1266  }
1267
1268
1269  
1270  
1271}