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