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