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