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