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