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