001package org.hl7.fhir.convertors;
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
033import com.google.gson.JsonArray;
034import com.google.gson.JsonObject;
035import com.google.gson.JsonPrimitive;
036import org.hl7.fhir.convertors.conv40_50.resources40_50.StructureDefinition40_50;
037import org.hl7.fhir.convertors.conv40_50.resources40_50.ValueSet40_50;
038import org.hl7.fhir.exceptions.FHIRException;
039import org.hl7.fhir.exceptions.FHIRFormatError;
040import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
041import org.hl7.fhir.r5.formats.IParser.OutputStyle;
042import org.hl7.fhir.r5.formats.JsonParser;
043import org.hl7.fhir.r5.formats.XmlParser;
044import org.hl7.fhir.r5.model.*;
045import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent;
046import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent;
047import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
048import org.hl7.fhir.r5.model.Enumerations.BindingStrength;
049import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
050import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
051import org.hl7.fhir.r5.utils.ToolingExtensions;
052import org.hl7.fhir.utilities.*;
053import org.hl7.fhir.utilities.xhtml.NodeType;
054import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
055import org.hl7.fhir.utilities.xhtml.XhtmlNode;
056import org.w3c.dom.Document;
057import org.w3c.dom.Element;
058import org.w3c.dom.Node;
059
060import java.io.ByteArrayOutputStream;
061import java.io.FileInputStream;
062import java.io.IOException;
063import java.util.*;
064
065public class SpecDifferenceEvaluator {
066
067  private final SpecPackage original = new SpecPackage();
068  private final SpecPackage revision = new SpecPackage();
069  private final Map<String, String> renames = new HashMap<String, String>();
070  private final List<String> moves = new ArrayList<String>();
071  private XhtmlNode tbl;
072  private TypeLinkProvider linker;
073
074  public static void main(String[] args) throws Exception {
075    System.out.println("gen diff");
076    SpecDifferenceEvaluator self = new SpecDifferenceEvaluator();
077    self.loadFromIni(new IniFile("C:\\work\\org.hl7.fhir\\build\\source\\fhir.ini"));
078//    loadVS2(self.original.valuesets, "C:\\work\\org.hl7.fhir.dstu2.original\\build\\publish\\valuesets.xml");
079//    loadVS(self.revision.valuesets, "C:\\work\\org.hl7.fhir.dstu2.original\\build\\publish\\valuesets.xml");
080
081    loadSD4(self.original.getTypes(), "C:\\work\\org.hl7.fhir\\build\\source\\release4\\profiles-types.xml");
082    loadSD(self.revision.getTypes(), "C:\\work\\org.hl7.fhir\\build\\publish\\profiles-types.xml");
083    loadSD4(self.original.getResources(), "C:\\work\\org.hl7.fhir\\build\\source\\release4\\profiles-resources.xml");
084    loadSD(self.revision.getResources(), "C:\\work\\org.hl7.fhir\\build\\publish\\profiles-resources.xml");
085    loadVS4(self.original.getExpansions(), "C:\\work\\org.hl7.fhir\\build\\source\\release4\\expansions.xml");
086    loadVS(self.revision.getExpansions(), "C:\\work\\org.hl7.fhir\\build\\publish\\expansions.xml");
087    loadVS4(self.original.getValuesets(), "C:\\work\\org.hl7.fhir\\build\\source\\release4\\valuesets.xml");
088    loadVS(self.revision.getValuesets(), "C:\\work\\org.hl7.fhir\\build\\publish\\valuesets.xml");
089    StringBuilder b = new StringBuilder();
090    b.append("<html>\r\n");
091    b.append("<head>\r\n");
092    b.append("<link href=\"fhir.css\" rel=\"stylesheet\"/>\r\n");
093    b.append("</head>\r\n");
094    b.append("<body>\r\n");
095    b.append(self.getDiffAsHtml(null));
096    b.append("</body>\r\n");
097    b.append("</html>\r\n");
098    TextFile.stringToFile(b.toString(), "c:\\temp\\diff.html");
099    System.out.println("done");
100  }
101
102  private static void loadSD4(Map<String, StructureDefinition> map, String fn) throws FHIRException, IOException {
103    org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) new org.hl7.fhir.r4.formats.XmlParser().parse(new FileInputStream(fn));
104    for (org.hl7.fhir.r4.model.Bundle.BundleEntryComponent be : bundle.getEntry()) {
105      if (be.getResource() instanceof org.hl7.fhir.r4.model.StructureDefinition) {
106        org.hl7.fhir.r4.model.StructureDefinition sd = (org.hl7.fhir.r4.model.StructureDefinition) be.getResource();
107        map.put(sd.getName(), StructureDefinition40_50.convertStructureDefinition(sd));
108      }
109    }
110
111  }
112
113  private static void loadSD(Map<String, StructureDefinition> map, String fn) throws FHIRFormatError, IOException {
114    Bundle bundle = (Bundle) new XmlParser().parse(new FileInputStream(fn));
115    for (BundleEntryComponent be : bundle.getEntry()) {
116      if (be.getResource() instanceof StructureDefinition) {
117        StructureDefinition sd = (StructureDefinition) be.getResource();
118        map.put(sd.getName(), sd);
119      }
120    }
121  }
122
123  private static void loadVS4(Map<String, ValueSet> map, String fn) throws FHIRException, IOException {
124    org.hl7.fhir.r4.model.Bundle bundle = (org.hl7.fhir.r4.model.Bundle) new org.hl7.fhir.r4.formats.XmlParser().parse(new FileInputStream(fn));
125    for (org.hl7.fhir.r4.model.Bundle.BundleEntryComponent be : bundle.getEntry()) {
126      if (be.getResource() instanceof org.hl7.fhir.r4.model.ValueSet) {
127        org.hl7.fhir.r4.model.ValueSet sd = (org.hl7.fhir.r4.model.ValueSet) be.getResource();
128        map.put(sd.getName(), ValueSet40_50.convertValueSet(sd));
129      }
130    }
131  }
132
133  private static void loadVS(Map<String, ValueSet> map, String fn) throws FHIRFormatError, IOException {
134    Bundle bundle = (Bundle) new XmlParser().parse(new FileInputStream(fn));
135    for (BundleEntryComponent be : bundle.getEntry()) {
136      if (be.getResource() instanceof ValueSet) {
137        ValueSet sd = (ValueSet) be.getResource();
138        map.put(sd.getName(), sd);
139      }
140    }
141  }
142
143  public void loadFromIni(IniFile ini) {
144    String[] names = ini.getPropertyNames("r5-renames");
145    if (names != null)
146      for (String n : names)
147        // note reverse of order
148        renames.put(ini.getStringProperty("r5-renames", n), n);
149  }
150
151  public SpecPackage getOriginal() {
152    return original;
153  }
154
155  public SpecPackage getRevision() {
156    return revision;
157  }
158
159  public void getDiffAsJson(JsonObject json, StructureDefinition rev) throws IOException {
160    this.linker = null;
161    StructureDefinition orig = original.getResources().get(checkRename(rev.getName()));
162    if (orig == null)
163      orig = original.getTypes().get(checkRename(rev.getName()));
164    JsonArray types = new JsonArray();
165    json.add("types", types);
166    types.add(new JsonPrimitive(rev.getName()));
167    JsonObject type = new JsonObject();
168    json.add(rev.getName(), type);
169    if (orig == null)
170      type.addProperty("status", "new");
171    else {
172      start();
173      compareJson(type, orig, rev);
174    }
175  }
176
177  public void getDiffAsXml(Document doc, Element xml, StructureDefinition rev) throws IOException {
178    this.linker = null;
179    StructureDefinition orig = original.getResources().get(checkRename(rev.getName()));
180    if (orig == null)
181      orig = original.getTypes().get(checkRename(rev.getName()));
182    Element type = doc.createElement("type");
183    type.setAttribute("name", rev.getName());
184    xml.appendChild(type);
185    if (orig == null)
186      type.setAttribute("status", "new");
187    else {
188      start();
189      compareXml(doc, type, orig, rev);
190    }
191  }
192
193  public void getDiffAsJson(JsonObject json) throws IOException {
194    this.linker = null;
195    JsonArray types = new JsonArray();
196    json.add("types", types);
197
198    for (String s : sorted(revision.getTypes().keySet())) {
199      StructureDefinition orig = original.getTypes().get(s);
200      StructureDefinition rev = revision.getTypes().get(s);
201      types.add(new JsonPrimitive(rev.getName()));
202      JsonObject type = new JsonObject();
203      json.add(rev.getName(), type);
204      if (orig == null) {
205        type.addProperty("status", "new");
206      } else if (rev.getKind() == StructureDefinitionKind.PRIMITIVETYPE) {
207        type.addProperty("status", "no-change");
208      } else if (rev.hasDerivation() && orig.hasDerivation() && rev.getDerivation() != orig.getDerivation()) {
209        type.addProperty("status", "status-change");
210        type.addProperty("past-status", orig.getDerivation().toCode());
211        type.addProperty("current-status", rev.getDerivation().toCode());
212      } else {
213        compareJson(type, orig, rev);
214      }
215    }
216    for (String s : sorted(original.getTypes().keySet())) {
217      StructureDefinition orig = original.getTypes().get(s);
218      StructureDefinition rev = revision.getTypes().get(s);
219      if (rev == null) {
220        types.add(new JsonPrimitive(orig.getName()));
221        JsonObject type = new JsonObject();
222        json.add(orig.getName(), type);
223        type.addProperty("status", "deleted");
224      }
225    }
226
227    for (String s : sorted(revision.getResources().keySet())) {
228      StructureDefinition orig = original.getResources().get(checkRename(s));
229      StructureDefinition rev = revision.getResources().get(s);
230      types.add(new JsonPrimitive(rev.getName()));
231      JsonObject type = new JsonObject();
232      json.add(rev.getName(), type);
233      if (orig == null) {
234        type.addProperty("status", "new");
235      } else {
236        compareJson(type, orig, rev);
237      }
238    }
239    for (String s : sorted(original.getResources().keySet())) {
240      StructureDefinition orig = original.getResources().get(s);
241      StructureDefinition rev = revision.getResources().get(s);
242      if (rev == null) {
243        types.add(new JsonPrimitive(orig.getName()));
244        JsonObject type = new JsonObject();
245        json.add(orig.getName(), type);
246        type.addProperty("status", "deleted");
247      }
248    }
249  }
250
251  public void getDiffAsXml(Document doc, Element xml) throws IOException {
252    this.linker = null;
253
254    for (String s : sorted(revision.getTypes().keySet())) {
255      StructureDefinition orig = original.getTypes().get(s);
256      StructureDefinition rev = revision.getTypes().get(s);
257      Element type = doc.createElement("type");
258      type.setAttribute("name", rev.getName());
259      xml.appendChild(type);
260      if (orig == null) {
261        type.setAttribute("status", "new");
262      } else if (rev.getKind() == StructureDefinitionKind.PRIMITIVETYPE) {
263        type.setAttribute("status", "no-change");
264      } else if (rev.hasDerivation() && orig.hasDerivation() && rev.getDerivation() != orig.getDerivation()) {
265        type.setAttribute("status", "status-change");
266        type.setAttribute("past-status", orig.getDerivation().toCode());
267        type.setAttribute("current-status", rev.getDerivation().toCode());
268      } else {
269        compareXml(doc, type, orig, rev);
270      }
271    }
272    for (String s : sorted(original.getTypes().keySet())) {
273      StructureDefinition orig = original.getTypes().get(s);
274      StructureDefinition rev = revision.getTypes().get(s);
275      if (rev == null) {
276        Element type = doc.createElement("type");
277        type.setAttribute("name", orig.getName());
278        xml.appendChild(type);
279        type.setAttribute("status", "deleted");
280      }
281    }
282
283    for (String s : sorted(revision.getResources().keySet())) {
284      StructureDefinition orig = original.getResources().get(checkRename(s));
285      StructureDefinition rev = revision.getResources().get(s);
286      Element type = doc.createElement("type");
287      type.setAttribute("name", rev.getName());
288      xml.appendChild(type);
289      if (orig == null) {
290        type.setAttribute("status", "new");
291      } else {
292        compareXml(doc, type, orig, rev);
293      }
294    }
295    for (String s : sorted(original.getResources().keySet())) {
296      StructureDefinition orig = original.getResources().get(s);
297      StructureDefinition rev = revision.getResources().get(s);
298      if (rev == null) {
299        Element type = doc.createElement("type");
300        type.setAttribute("name", orig.getName());
301        xml.appendChild(type);
302        type.setAttribute("status", "deleted");
303      }
304    }
305  }
306
307  public String getDiffAsHtml(TypeLinkProvider linker, StructureDefinition rev) throws IOException {
308    this.linker = linker;
309
310    StructureDefinition orig = original.getResources().get(checkRename(rev.getName()));
311    if (orig == null)
312      orig = original.getTypes().get(checkRename(rev.getName()));
313    if (orig == null)
314      return "<p>This " + rev.getKind().toCode() + " did not exist in Release 2</p>";
315    else {
316      start();
317      compare(orig, rev);
318      return new XhtmlComposer(false, true).compose(tbl) + "\r\n<p>See the <a href=\"diff.html\">Full Difference</a> for further information</p>\r\n";
319    }
320  }
321
322  public String getDiffAsHtml(TypeLinkProvider linker) throws IOException {
323    this.linker = linker;
324    start();
325
326    header("Types");
327    for (String s : sorted(revision.getTypes().keySet())) {
328      StructureDefinition orig = original.getTypes().get(s);
329      StructureDefinition rev = revision.getTypes().get(s);
330      if (orig == null) {
331        markNew(rev.getName(), true, false, false);
332      } else if (rev.getKind() == StructureDefinitionKind.PRIMITIVETYPE) {
333        markNoChanges(rev.getName(), true);
334      } else if (rev.hasDerivation() && orig.hasDerivation() && rev.getDerivation() != orig.getDerivation()) {
335        markChanged(rev.getName(), "Changed from a " + orig.getDerivation().toCode() + " to a " + rev.getDerivation().toCode(), true);
336      } else {
337        compare(orig, rev);
338      }
339    }
340    for (String s : sorted(original.getTypes().keySet())) {
341      StructureDefinition orig = original.getTypes().get(s);
342      StructureDefinition rev = revision.getTypes().get(s);
343      if (rev == null)
344        markDeleted(orig.getName(), true);
345    }
346
347    header("Resources");
348    for (String s : sorted(revision.getResources().keySet())) {
349      StructureDefinition orig = original.getResources().get(checkRename(s));
350      StructureDefinition rev = revision.getResources().get(s);
351      if (orig == null) {
352        markNew(rev.getName(), true, true, false);
353      } else {
354        compare(orig, rev);
355      }
356    }
357    for (String s : sorted(original.getResources().keySet())) {
358      StructureDefinition orig = original.getResources().get(s);
359      StructureDefinition rev = revision.getResources().get(s);
360      if (rev == null)
361        markDeleted(orig.getName(), true);
362    }
363
364    return new XhtmlComposer(false, true).compose(tbl);
365  }
366
367  private Object checkRename(String s) {
368    if (renames.containsKey(s))
369      return renames.get(s);
370    else
371      return s;
372  }
373
374  private List<String> sorted(Set<String> keys) {
375    List<String> list = new ArrayList<String>();
376    list.addAll(keys);
377    Collections.sort(list);
378    return list;
379  }
380
381  private void header(String title) {
382    tbl.addTag("tr").setAttribute("class", "diff-title").addTag("td").setAttribute("colspan", "2").addText(title);
383  }
384
385  private void start() {
386    tbl = new XhtmlNode(NodeType.Element, "table");
387    tbl.setAttribute("class", "grid");
388
389  }
390
391  private void markNoChanges(String name, boolean item) {
392    XhtmlNode tr = tbl.addTag("tr").setAttribute("class", item ? "diff-item" : "diff-entry");
393    XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left");
394    XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right");
395    String link = linker == null ? null : linker.getLink(name);
396    if (link != null)
397      left.addTag("a").setAttribute("href", link).addText(name);
398    else
399      left.addText(name);
400    right.span("opacity: 0.5", null).addText("(No Changes)");
401  }
402
403  private void markChanged(String name, String change, boolean item) {
404    XhtmlNode tr = tbl.addTag("tr").setAttribute("class", item ? "diff-item" : "diff-entry");
405    XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left");
406    XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right");
407    String link = linker == null ? null : linker.getLink(name);
408    if (link != null)
409      left.addTag("a").setAttribute("href", link).addText(name);
410    else
411      left.addText(name);
412    right.ul().li().addText(change);
413  }
414
415  private void markDeleted(String name, boolean item) {
416    XhtmlNode tr = tbl.addTag("tr").setAttribute("class", item ? "diff-del-item" : "diff-del");
417    XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left");
418    XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right");
419    left.addText(name);
420    right.ul().li().addText("deleted");
421  }
422
423  private void markNew(String name, boolean item, boolean res, boolean mand) {
424    XhtmlNode tr = tbl.addTag("tr").setAttribute("class", item ? "diff-new-item" : "diff-new");
425    XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left");
426    XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right");
427    String link = linker == null ? null : linker.getLink(name);
428    if (link != null)
429      left.addTag("a").setAttribute("href", link).addText(name);
430    else
431      left.addText(name);
432    if (!res && mand)
433      right.ul().li().b().addText("Added Mandatory Element");
434    else
435      right.ul().li().addText(res ? "Added Resource" : !name.contains(".") ? "Added Type" : mand ? "Added Mandatory Element " : "Added Element");
436  }
437
438  private void compare(StructureDefinition orig, StructureDefinition rev) {
439    moves.clear();
440    XhtmlNode tr = tbl.addTag("tr").setAttribute("class", "diff-item");
441    XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left");
442    String link = linker == null ? null : linker.getLink(rev.getName());
443    if (link != null)
444      left.addTag("a").setAttribute("href", link).addText(rev.getName());
445    else
446      left.addText(rev.getName());
447    XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right");
448
449    // first, we must match revision elements to old elements
450    boolean changed = false;
451    if (!orig.getName().equals(rev.getName())) {
452      changed = true;
453      right.ul().li().addText("Name Changed from " + orig.getName() + " to " + rev.getName());
454    }
455    for (ElementDefinition ed : rev.getDifferential().getElement()) {
456      ElementDefinition oed = getMatchingElement(rev.getName(), orig.getDifferential().getElement(), ed);
457      if (oed != null) {
458        ed.setUserData("match", oed);
459        oed.setUserData("match", ed);
460      }
461    }
462
463    for (ElementDefinition ed : rev.getDifferential().getElement()) {
464      ElementDefinition oed = (ElementDefinition) ed.getUserData("match");
465      if (oed == null) {
466        changed = true;
467        markNew(ed.getPath(), false, false, ed.getMin() > 0);
468      } else
469        changed = compareElement(ed, oed) || changed;
470    }
471
472    List<String> dels = new ArrayList<String>();
473
474    for (ElementDefinition ed : orig.getDifferential().getElement()) {
475      if (ed.getUserData("match") == null) {
476        changed = true;
477        boolean marked = false;
478        for (String s : dels)
479          if (ed.getPath().startsWith(s + "."))
480            marked = true;
481        if (!marked) {
482          dels.add(ed.getPath());
483          markDeleted(ed.getPath(), false);
484        }
485      }
486    }
487
488    if (!changed)
489      right.ul().li().addText("No Changes");
490
491    for (ElementDefinition ed : rev.getDifferential().getElement())
492      ed.clearUserData("match");
493    for (ElementDefinition ed : orig.getDifferential().getElement())
494      ed.clearUserData("match");
495
496  }
497
498  private ElementDefinition getMatchingElement(String tn, List<ElementDefinition> list, ElementDefinition target) {
499    // now, look for matches by name (ignoring slicing for now)
500    String tp = mapPath(tn, target.getPath());
501    if (tp.endsWith("[x]"))
502      tp = tp.substring(0, tp.length() - 4);
503    for (ElementDefinition ed : list) {
504      String p = ed.getPath();
505      if (p.endsWith("[x]"))
506        p = p.substring(0, p.length() - 4);
507      if (p.equals(tp))
508        return ed;
509    }
510    return null;
511  }
512
513  /**
514   * change from rev to original. TODO: make this a config file somewhere?
515   *
516   * @param tn
517   * @return
518   */
519  private String mapPath(String tn, String path) {
520    if (renames.containsKey(path))
521      return renames.get(path);
522    for (String r : renames.keySet()) {
523      if (path.startsWith(r + "."))
524        return renames.get(r) + "." + path.substring(r.length() + 1);
525    }
526    return path;
527  }
528
529  private boolean compareElement(ElementDefinition rev, ElementDefinition orig) {
530    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("\r\n");
531    String rn = tail(rev.getPath());
532    String on = tail(orig.getPath());
533    String rp = head(rev.getPath());
534    String op = head(orig.getPath());
535    boolean renamed = false;
536    if (!rn.equals(on) && rev.getPath().contains(".")) {
537      if (rp.equals(op))
538        b.append("Renamed from " + on + " to " + rn);
539      else
540        b.append("Moved from " + orig.getPath() + " to " + rn);
541      renamed = true;
542    } else if (!rev.getPath().equals(orig.getPath())) {
543      if (!moveAlreadyNoted(rev.getPath(), orig.getPath())) {
544        noteMove(rev.getPath(), orig.getPath());
545        b.append("Moved from " + head(orig.getPath()) + " to " + head(rev.getPath()));
546        renamed = true;
547      }
548    }
549
550    if (rev.getMin() != orig.getMin())
551      b.append("Min Cardinality changed from " + orig.getMin() + " to " + rev.getMin());
552
553    if (!rev.getMax().equals(orig.getMax()))
554      b.append("Max Cardinality changed from " + orig.getMax() + " to " + rev.getMax());
555
556    analyseTypes(b, rev, orig);
557
558    if (hasBindingToNote(rev) || hasBindingToNote(orig)) {
559      String s = compareBindings(rev, orig);
560      if (!Utilities.noString(s))
561        b.append(s);
562    }
563
564    if (rev.hasDefaultValue() || orig.hasDefaultValue()) {
565      if (!rev.hasDefaultValue())
566        b.append("Default Value " + describeValue(orig.getDefaultValue()) + " removed");
567      else if (!orig.hasDefaultValue())
568        b.append("Default Value " + describeValue(rev.getDefaultValue()) + " added");
569      else {
570        // do not use Base.compare here, because it is subject to type differences
571        String s1 = describeValue(orig.getDefaultValue());
572        String s2 = describeValue(rev.getDefaultValue());
573        if (!s1.equals(s2))
574          b.append("Default Value changed from " + s1 + " to " + s2);
575      }
576    }
577
578    if (rev.getIsModifier() != orig.getIsModifier()) {
579      if (rev.getIsModifier())
580        b.append("Now marked as Modifier");
581      else
582        b.append("No longer marked as Modifier");
583    }
584
585    if (b.length() > 0) {
586      XhtmlNode tr = tbl.addTag("tr").setAttribute("class", renamed ? "diff-changed-item" : "diff-entry");
587      XhtmlNode left = tr.addTag("td").setAttribute("class", "diff-left");
588      left.addText(rev.getPath());
589      XhtmlNode right = tr.addTag("td").setAttribute("class", "diff-right");
590      XhtmlNode ul = null;
591      for (String s : b.toString().split("\\r?\\n")) {
592        if (!Utilities.noString(s)) {
593          if (ul == null)
594            ul = right.addTag("ul");
595          XhtmlNode li = ul.addTag("li").notPretty();
596          if (s.contains("`")) {
597            String[] p = s.split("\\`");
598            boolean code = true;
599            li.addText(p[0]);
600            for (int i = 1; i < p.length; i++) {
601              if (code)
602                li.addTag("code").addText(p[i]);
603              else
604                li.addText(p[i]);
605              code = !code;
606            }
607          } else
608            li.addText(s);
609        }
610      }
611    }
612    return b.length() > 0;
613  }
614
615  private void noteMove(String revpath, String origpath) {
616    moves.add(revpath + "=" + origpath);
617  }
618
619  private boolean moveAlreadyNoted(String revpath, String origpath) {
620    if (moves.contains(revpath + "=" + origpath))
621      return true;
622    if (!revpath.contains(".") || !origpath.contains("."))
623      return false;
624    return moveAlreadyNoted(head(revpath), head(origpath));
625  }
626
627  @SuppressWarnings("rawtypes")
628  private String describeValue(DataType v) {
629    if (v instanceof PrimitiveType) {
630      return "\"" + ((PrimitiveType) v).asStringValue() + "\"";
631    }
632    return "{complex}";
633  }
634
635  private String compareBindings(ElementDefinition rev, ElementDefinition orig) {
636    if (!hasBindingToNote(rev)) {
637      return "Remove Binding " + describeBinding(orig);
638    } else if (!hasBindingToNote(orig)) {
639      return "Add Binding " + describeBinding(rev);
640    } else {
641      return compareBindings(rev.getBinding(), orig.getBinding());
642    }
643  }
644
645  private String compareBindings(ElementDefinitionBindingComponent rev, ElementDefinitionBindingComponent orig) {
646    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("\r\n");
647    if (rev.getStrength() != orig.getStrength())
648      b.append("Change binding strength from " + orig.getStrength().toCode() + " to " + rev.getStrength().toCode());
649    if (!Base.compareDeep(rev.getValueSet(), orig.getValueSet(), false))
650      b.append("Change value set from " + describeReference(orig.getValueSet()) + " to " + describeReference(rev.getValueSet()));
651    if (!maxValueSetsMatch(rev, orig))
652      b.append("Change max value set from " + describeMax(orig) + " to " + describeMax(rev));
653    if (rev.getStrength() == BindingStrength.REQUIRED && orig.getStrength() == BindingStrength.REQUIRED) {
654      ValueSet vrev = getValueSet(rev.getValueSet(), revision.getExpansions());
655      ValueSet vorig = getValueSet(rev.getValueSet(), original.getExpansions());
656      CommaSeparatedStringBuilder br = new CommaSeparatedStringBuilder();
657      int ir = 0;
658      CommaSeparatedStringBuilder bo = new CommaSeparatedStringBuilder();
659      int io = 0;
660      if (vrev != null && vorig != null) {
661        for (ValueSetExpansionContainsComponent cc : vorig.getExpansion().getContains()) {
662          if (!hasCode(vrev, cc)) {
663            io++;
664            bo.append("`" + Utilities.escapeXml(cc.getCode()) + "`");
665          }
666        }
667        for (ValueSetExpansionContainsComponent cc : vrev.getExpansion().getContains()) {
668          if (!hasCode(vorig, cc)) {
669            ir++;
670            br.append("`" + Utilities.escapeXml(cc.getCode()) + "`");
671          }
672        }
673      }
674      if (io > 0)
675        b.append("Remove " + Utilities.pluralize("Code", io) + " " + bo);
676      if (ir > 0)
677        b.append("Add " + Utilities.pluralize("Code", ir) + "  " + br);
678
679    }
680    if (rev.getStrength() == BindingStrength.EXTENSIBLE && orig.getStrength() == BindingStrength.EXTENSIBLE) {
681      ValueSet vrev = getValueSet(rev.getValueSet(), revision.getValuesets());
682      ValueSet vorig = getValueSet(orig.getValueSet(), original.getValuesets());
683      if (vrev != null && vrev.hasCompose() && vrev.getCompose().getInclude().size() == 1 && vrev.getCompose().getIncludeFirstRep().hasSystem() &&
684        vorig != null && vorig.hasCompose() && vorig.getCompose().getInclude().size() == 1 && vorig.getCompose().getIncludeFirstRep().hasSystem()) {
685        if (!vorig.getCompose().getIncludeFirstRep().getSystem().equals(vrev.getCompose().getIncludeFirstRep().getSystem())) {
686          b.append("Change code system for extensibly bound codes from \"" + vorig.getCompose().getIncludeFirstRep().getSystem() + "\" to \"" + vrev.getCompose().getIncludeFirstRep().getSystem() + "\"");
687        }
688      }
689    }
690
691    return b.toString();
692  }
693
694  private String describeMax(ElementDefinitionBindingComponent orig) {
695    if (!orig.hasExtension(ToolingExtensions.EXT_MAX_VALUESET))
696      return "n/a";
697    return "`" + ToolingExtensions.readStringExtension(orig, ToolingExtensions.EXT_MAX_VALUESET) + "`";
698  }
699
700  private boolean maxValueSetsMatch(ElementDefinitionBindingComponent rev, ElementDefinitionBindingComponent orig) {
701    if (!rev.hasExtension(ToolingExtensions.EXT_MAX_VALUESET) && !orig.hasExtension(ToolingExtensions.EXT_MAX_VALUESET))
702      return true;
703    if (rev.hasExtension(ToolingExtensions.EXT_MAX_VALUESET) != orig.hasExtension(ToolingExtensions.EXT_MAX_VALUESET))
704      return false;
705    return ToolingExtensions.readStringExtension(rev, ToolingExtensions.EXT_MAX_VALUESET).equals(ToolingExtensions.readStringExtension(orig, ToolingExtensions.EXT_MAX_VALUESET));
706  }
707
708//  "Remove code "+
709//  "add code "+
710
711  private String describeBinding(ElementDefinition orig) {
712    if (orig.getBinding().hasExtension(ToolingExtensions.EXT_MAX_VALUESET))
713      return "`" + orig.getBinding().getValueSet() + "` (" + orig.getBinding().getStrength().toCode() + "), max =`" + ToolingExtensions.readStringExtension(orig.getBinding(), ToolingExtensions.EXT_MAX_VALUESET) + "`";
714    else
715      return "`" + orig.getBinding().getValueSet() + "` (" + orig.getBinding().getStrength().toCode() + ")";
716  }
717
718  private void describeBinding(JsonObject element, String name, ElementDefinition orig) {
719    JsonObject binding = new JsonObject();
720    element.add(name, binding);
721    binding.addProperty("reference", describeReference(orig.getBinding().getValueSet()));
722    binding.addProperty("strength", orig.getBinding().getStrength().toCode());
723    if (orig.getBinding().hasExtension(ToolingExtensions.EXT_MAX_VALUESET))
724      binding.addProperty("max", ToolingExtensions.readStringExtension(orig.getBinding(), ToolingExtensions.EXT_MAX_VALUESET));
725  }
726
727  private void describeBinding(Document doc, Element element, String name, ElementDefinition orig) {
728    Element binding = doc.createElement(name);
729    element.appendChild(binding);
730    binding.setAttribute("reference", describeReference(orig.getBinding().getValueSet()));
731    binding.setAttribute("strength", orig.getBinding().getStrength().toCode());
732    if (orig.getBinding().hasExtension(ToolingExtensions.EXT_MAX_VALUESET))
733      binding.setAttribute("max", ToolingExtensions.readStringExtension(orig.getBinding(), ToolingExtensions.EXT_MAX_VALUESET));
734  }
735
736  private String describeReference(String ref) {
737    return ref;
738  }
739
740  private ValueSet getValueSet(String ref, Map<String, ValueSet> expansions) {
741    if (ref != null) {
742      if (Utilities.isAbsoluteUrl(ref)) {
743        for (ValueSet ve : expansions.values()) {
744          if (ref.equals(ve.getUrl()))
745            return ve;
746        }
747      } else if (ref.startsWith("ValueSet/")) {
748        ref = ref.substring(9);
749        for (ValueSet ve : expansions.values()) {
750          if (ve.getId().equals(ref))
751            return ve;
752        }
753      }
754    }
755    return null;
756  }
757
758  private String listCodes(ValueSet vs) {
759    if (vs.getExpansion().getContains().size() > 15)
760      return ">15 codes";
761    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" | ");
762    for (ValueSetExpansionContainsComponent ce : vs.getExpansion().getContains()) {
763      if (ce.hasCode())
764        b.append(ce.getCode());
765    }
766    return b.toString();
767  }
768
769  private boolean hasBindingToNote(ElementDefinition ed) {
770    return ed.hasBinding() &&
771      (ed.getBinding().getStrength() == BindingStrength.EXTENSIBLE || ed.getBinding().getStrength() == BindingStrength.REQUIRED || ed.getBinding().hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) &&
772      ed.getBinding().hasValueSet();
773  }
774
775  private String tail(String path) {
776    return path.contains(".") ? path.substring(path.lastIndexOf(".") + 1) : path;
777  }
778
779  private String head(String path) {
780    return path.contains(".") ? path.substring(0, path.lastIndexOf(".")) : path;
781  }
782
783  private void analyseTypes(CommaSeparatedStringBuilder bp, ElementDefinition rev, ElementDefinition orig) {
784    if (rev.getType().size() == 1 && orig.getType().size() == 1) {
785      String r = describeType(rev.getType().get(0));
786      if (Utilities.noString(r) && Utilities.existsInList(rev.getId(), "Element.id"))
787        r = "string";
788      if (Utilities.noString(r) && Utilities.existsInList(rev.getId(), "Extension.url"))
789        r = "uri";
790      String o = describeType(orig.getType().get(0));
791      if (r == null && o == null)
792        System.out.println("null @ " + rev.getPath());
793      if (r.contains("(") && o.contains("(") && r.startsWith(o.substring(0, o.indexOf("(") + 1))) {
794        compareParameters(bp, rev.getType().get(0), orig.getType().get(0));
795      } else if (!r.equals(o))
796        bp.append("Type changed from " + o + " to " + r);
797    } else {
798      CommaSeparatedStringBuilder removed = new CommaSeparatedStringBuilder();
799      CommaSeparatedStringBuilder added = new CommaSeparatedStringBuilder();
800      CommaSeparatedStringBuilder retargetted = new CommaSeparatedStringBuilder();
801      for (TypeRefComponent tr : orig.getType()) {
802        if (!hasType(rev.getType(), tr))
803          removed.append(describeType(tr));
804      }
805      for (TypeRefComponent tr : rev.getType()) {
806        if (!hasType(orig.getType(), tr) && !isAbstractType(tr.getWorkingCode()))
807          added.append(describeType(tr));
808      }
809      for (TypeRefComponent tr : rev.getType()) {
810        TypeRefComponent tm = getType(rev.getType(), tr);
811        if (tm != null) {
812          compareParameters(bp, tr, tm);
813        }
814      }
815      if (added.length() > 0)
816        bp.append("Add " + Utilities.pluralize("Type", added.count()) + " " + added);
817      if (removed.length() > 0)
818        bp.append("Remove " + Utilities.pluralize("Type", removed.count()) + " " + removed);
819      if (retargetted.length() > 0)
820        bp.append(retargetted.toString());
821    }
822  }
823
824  private void compareParameters(CommaSeparatedStringBuilder bp, TypeRefComponent tr, TypeRefComponent tm) {
825    List<String> added = new ArrayList<>();
826    List<String> removed = new ArrayList<>();
827
828    for (CanonicalType p : tr.getTargetProfile()) {
829      if (!hasParam(tm, p.asStringValue())) {
830        added.add(trimNS(p.asStringValue()));
831      }
832    }
833
834    for (CanonicalType p : tm.getTargetProfile()) {
835      if (!hasParam(tr, p.asStringValue())) {
836        removed.add(trimNS(p.asStringValue()));
837      }
838    }
839
840    if (!added.isEmpty())
841      bp.append("Type " + tr.getWorkingCode() + ": Added Target " + Utilities.pluralize("Type", added.size()) + " " + csv(added));
842    if (!removed.isEmpty())
843      bp.append("Type " + tr.getWorkingCode() + ": Removed Target " + Utilities.pluralize("Type", removed.size()) + " " + csv(removed));
844  }
845
846  private String trimNS(String v) {
847    if (v.startsWith("http://hl7.org/fhir/StructureDefinition/"))
848      return v.substring(40);
849    return v;
850  }
851
852  private String csv(List<String> list) {
853    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
854    for (String s : list)
855      b.append(s);
856    return b.toString();
857  }
858
859  private boolean hasParam(TypeRefComponent tm, String s) {
860    for (CanonicalType t : tm.getTargetProfile())
861      if (s.equals(t.asStringValue()))
862        return true;
863    return false;
864  }
865
866  private boolean isAbstractType(String code) {
867    return Utilities.existsInList(code, "Element", "BackboneElement");
868  }
869
870  private boolean hasType(List<TypeRefComponent> types, TypeRefComponent tr) {
871    for (TypeRefComponent t : types) {
872      if (t.getWorkingCode().equals(tr.getWorkingCode())) {
873        if (((!t.hasProfile() && !tr.hasProfile()) || (t.getProfile().equals(tr.getProfile()))))
874          return true;
875      }
876    }
877    return false;
878  }
879
880  private TypeRefComponent getType(List<TypeRefComponent> types, TypeRefComponent tr) {
881    for (TypeRefComponent t : types) {
882      if (t.getWorkingCode().equals(tr.getWorkingCode())) {
883        return t;
884      }
885    }
886    return null;
887  }
888
889  private String describeType(TypeRefComponent tr) {
890    if (!tr.hasProfile() && !tr.hasTargetProfile())
891      return tr.getWorkingCode();
892    else if (Utilities.existsInList(tr.getWorkingCode(), "Reference", "canonical")) {
893      StringBuilder b = new StringBuilder(tr.getWorkingCode());
894      b.append("(");
895      boolean first = true;
896      for (UriType u : tr.getTargetProfile()) {
897        if (first)
898          first = false;
899        else
900          b.append(" | ");
901        if (u.getValue().startsWith("http://hl7.org/fhir/StructureDefinition/"))
902          b.append(u.getValue().substring(40));
903        else
904          b.append(u.getValue());
905      }
906      b.append(")");
907      return b.toString();
908    } else {
909      StringBuilder b = new StringBuilder(tr.getWorkingCode());
910      if (tr.getProfile().size() > 0) {
911        b.append("(");
912        boolean first = true;
913        for (UriType u : tr.getTargetProfile()) {
914          if (first)
915            first = false;
916          else
917            b.append(" | ");
918          b.append(u.getValue());
919        }
920        b.append(")");
921      }
922      return b.toString();
923    }
924  }
925
926  public void saveR4AsR5(ZipGenerator zip, FhirFormat fmt) throws IOException {
927    for (StructureDefinition t : original.getTypes().values())
928      saveResource(zip, t, fmt);
929    for (StructureDefinition t : original.getResources().values())
930      saveResource(zip, t, fmt);
931    for (StructureDefinition t : original.getProfiles().values())
932      saveResource(zip, t, fmt);
933    for (StructureDefinition t : original.getExtensions().values())
934      saveResource(zip, t, fmt);
935    for (ValueSet t : original.getValuesets().values())
936      saveResource(zip, t, fmt);
937    for (ValueSet t : original.getExpansions().values())
938      saveResource(zip, t, fmt);
939  }
940
941  private void saveResource(ZipGenerator zip, Resource t, FhirFormat fmt) throws IOException {
942    ByteArrayOutputStream bs = new ByteArrayOutputStream();
943    if (fmt == FhirFormat.JSON)
944      new JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(bs, t);
945    else
946      new XmlParser().setOutputStyle(OutputStyle.PRETTY).compose(bs, t);
947    zip.addBytes(t.fhirType() + "-" + t.getId() + "." + fmt.getExtension(), bs.toByteArray(), true);
948  }
949
950  private void compareJson(JsonObject type, StructureDefinition orig, StructureDefinition rev) {
951    JsonObject elements = new JsonObject();
952    // first, we must match revision elements to old elements
953    boolean changed = false;
954    if (!orig.getName().equals(rev.getName())) {
955      changed = true;
956      type.addProperty("old-name", orig.getName());
957    }
958    for (ElementDefinition ed : rev.getDifferential().getElement()) {
959      ElementDefinition oed = getMatchingElement(rev.getName(), orig.getDifferential().getElement(), ed);
960      if (oed != null) {
961        ed.setUserData("match", oed);
962        oed.setUserData("match", ed);
963      }
964    }
965
966    for (ElementDefinition ed : rev.getDifferential().getElement()) {
967      ElementDefinition oed = (ElementDefinition) ed.getUserData("match");
968      if (oed == null) {
969        changed = true;
970        JsonObject element = new JsonObject();
971        elements.add(ed.getPath(), element);
972        element.addProperty("status", "new");
973      } else
974        changed = compareElementJson(elements, ed, oed) || changed;
975    }
976
977    List<String> dels = new ArrayList<String>();
978
979    for (ElementDefinition ed : orig.getDifferential().getElement()) {
980      if (ed.getUserData("match") == null) {
981        changed = true;
982        boolean marked = false;
983        for (String s : dels)
984          if (ed.getPath().startsWith(s + "."))
985            marked = true;
986        if (!marked) {
987          dels.add(ed.getPath());
988          JsonObject element = new JsonObject();
989          elements.add(ed.getPath(), element);
990          element.addProperty("status", "deleted");
991        }
992      }
993    }
994
995    if (elements.entrySet().size() > 0)
996      type.add("elements", elements);
997
998    if (changed)
999      type.addProperty("status", "changed");
1000    else
1001      type.addProperty("status", "no-change");
1002
1003    for (ElementDefinition ed : rev.getDifferential().getElement())
1004      ed.clearUserData("match");
1005    for (ElementDefinition ed : orig.getDifferential().getElement())
1006      ed.clearUserData("match");
1007
1008  }
1009
1010  private void compareXml(Document doc, Element type, StructureDefinition orig, StructureDefinition rev) {
1011    // first, we must match revision elements to old elements
1012    boolean changed = false;
1013    if (!orig.getName().equals(rev.getName())) {
1014      changed = true;
1015      type.setAttribute("old-name", orig.getName());
1016    }
1017    for (ElementDefinition ed : rev.getDifferential().getElement()) {
1018      ElementDefinition oed = getMatchingElement(rev.getName(), orig.getDifferential().getElement(), ed);
1019      if (oed != null) {
1020        ed.setUserData("match", oed);
1021        oed.setUserData("match", ed);
1022      }
1023    }
1024
1025    for (ElementDefinition ed : rev.getDifferential().getElement()) {
1026      ElementDefinition oed = (ElementDefinition) ed.getUserData("match");
1027      if (oed == null) {
1028        changed = true;
1029        Element element = doc.createElement("element");
1030        element.setAttribute("path", ed.getPath());
1031        type.appendChild(element);
1032        element.setAttribute("status", "new");
1033      } else
1034        changed = compareElementXml(doc, type, ed, oed) || changed;
1035    }
1036
1037    List<String> dels = new ArrayList<String>();
1038
1039    for (ElementDefinition ed : orig.getDifferential().getElement()) {
1040      if (ed.getUserData("match") == null) {
1041        changed = true;
1042        boolean marked = false;
1043        for (String s : dels)
1044          if (ed.getPath().startsWith(s + "."))
1045            marked = true;
1046        if (!marked) {
1047          dels.add(ed.getPath());
1048          Element element = doc.createElement("element");
1049          element.setAttribute("path", ed.getPath());
1050          type.appendChild(element);
1051          element.setAttribute("status", "deleted");
1052        }
1053      }
1054    }
1055
1056    if (changed)
1057      type.setAttribute("status", "changed");
1058    else
1059      type.setAttribute("status", "no-change");
1060
1061    for (ElementDefinition ed : rev.getDifferential().getElement())
1062      ed.clearUserData("match");
1063    for (ElementDefinition ed : orig.getDifferential().getElement())
1064      ed.clearUserData("match");
1065
1066  }
1067
1068  private boolean compareElementJson(JsonObject elements, ElementDefinition rev, ElementDefinition orig) {
1069    JsonObject element = new JsonObject();
1070
1071    String rn = tail(rev.getPath());
1072    String on = tail(orig.getPath());
1073
1074    if (!rn.equals(on) && rev.getPath().contains("."))
1075      element.addProperty("old-name", on);
1076
1077    if (rev.getMin() != orig.getMin()) {
1078      element.addProperty("old-min", orig.getMin());
1079      element.addProperty("new-min", rev.getMin());
1080    }
1081
1082    if (!rev.getMax().equals(orig.getMax())) {
1083      element.addProperty("old-max", orig.getMax());
1084      element.addProperty("new-max", rev.getMax());
1085    }
1086
1087    analyseTypes(element, rev, orig);
1088
1089    if (hasBindingToNote(rev) || hasBindingToNote(orig)) {
1090      compareBindings(element, rev, orig);
1091    }
1092
1093    if (rev.hasDefaultValue() || orig.hasDefaultValue()) {
1094      boolean changed = true;
1095      if (!rev.hasDefaultValue())
1096        element.addProperty("default", "removed");
1097      else if (!orig.hasDefaultValue())
1098        element.addProperty("default", "added");
1099      else {
1100        String s1 = describeValue(orig.getDefaultValue());
1101        String s2 = describeValue(rev.getDefaultValue());
1102        if (!s1.equals(s2))
1103          element.addProperty("default", "changed");
1104        else
1105          changed = false;
1106      }
1107      if (changed) {
1108        if (orig.hasDefaultValue())
1109          element.addProperty("old-default", describeValue(orig.getDefaultValue()));
1110        if (rev.hasDefaultValue())
1111          element.addProperty("new-default", describeValue(rev.getDefaultValue()));
1112      }
1113    }
1114
1115    if (rev.getIsModifier() != orig.getIsModifier()) {
1116      if (rev.getIsModifier())
1117        element.addProperty("modifier", "added");
1118      else
1119        element.addProperty("modifier", "removed");
1120    }
1121
1122    if (element.entrySet().isEmpty())
1123      return false;
1124    else {
1125      elements.add(rev.getPath(), element);
1126      return true;
1127    }
1128  }
1129
1130  private boolean compareElementXml(Document doc, Element type, ElementDefinition rev, ElementDefinition orig) {
1131    Element element = doc.createElement("element");
1132
1133    String rn = tail(rev.getPath());
1134    String on = tail(orig.getPath());
1135
1136    if (!rn.equals(on) && rev.getPath().contains("."))
1137      element.setAttribute("old-name", on);
1138
1139    if (rev.getMin() != orig.getMin()) {
1140      element.setAttribute("old-min", Integer.toString(orig.getMin()));
1141      element.setAttribute("new-min", Integer.toString(rev.getMin()));
1142    }
1143
1144    if (!rev.getMax().equals(orig.getMax())) {
1145      element.setAttribute("old-max", orig.getMax());
1146      element.setAttribute("new-max", rev.getMax());
1147    }
1148
1149    analyseTypes(doc, element, rev, orig);
1150
1151    if (hasBindingToNote(rev) || hasBindingToNote(orig)) {
1152      compareBindings(doc, element, rev, orig);
1153    }
1154
1155    if (rev.hasDefaultValue() || orig.hasDefaultValue()) {
1156      boolean changed = true;
1157      if (!rev.hasDefaultValue())
1158        element.setAttribute("default", "removed");
1159      else if (!orig.hasDefaultValue())
1160        element.setAttribute("default", "added");
1161      else {
1162        String s1 = describeValue(orig.getDefaultValue());
1163        String s2 = describeValue(rev.getDefaultValue());
1164        if (!s1.equals(s2))
1165          element.setAttribute("default", "changed");
1166        else
1167          changed = false;
1168      }
1169      if (changed) {
1170        if (orig.hasDefaultValue())
1171          element.setAttribute("old-default", describeValue(orig.getDefaultValue()));
1172        if (rev.hasDefaultValue())
1173          element.setAttribute("new-default", describeValue(rev.getDefaultValue()));
1174      }
1175    }
1176
1177    if (rev.getIsModifier() != orig.getIsModifier()) {
1178      if (rev.getIsModifier())
1179        element.setAttribute("modifier", "added");
1180      else
1181        element.setAttribute("modifier", "removed");
1182    }
1183
1184    if (element.getAttributes().getLength() == 0 && element.getChildNodes().getLength() == 0)
1185      return false;
1186    else {
1187      element.setAttribute("path", rev.getPath());
1188      type.appendChild(element);
1189      return true;
1190    }
1191  }
1192
1193  private void analyseTypes(JsonObject element, ElementDefinition rev, ElementDefinition orig) {
1194    JsonArray oa = new JsonArray();
1195    JsonArray ra = new JsonArray();
1196
1197    if (rev.getType().size() == 1 && orig.getType().size() == 1) {
1198      String r = describeType(rev.getType().get(0));
1199      if (Utilities.noString(r) && Utilities.existsInList(rev.getId(), "Element.id", "Extension.url"))
1200        r = "string";
1201      String o = describeType(orig.getType().get(0));
1202      if (Utilities.noString(o) && Utilities.existsInList(orig.getId(), "Element.id", "Extension.url"))
1203        o = "string";
1204      if (!o.equals(r)) {
1205        oa.add(new JsonPrimitive(o));
1206        ra.add(new JsonPrimitive(r));
1207      }
1208    } else {
1209      for (TypeRefComponent tr : orig.getType()) {
1210        if (!hasType(rev.getType(), tr))
1211          oa.add(new JsonPrimitive(describeType(tr)));
1212      }
1213      for (TypeRefComponent tr : rev.getType()) {
1214        if (!hasType(orig.getType(), tr) && !isAbstractType(tr.getWorkingCode()))
1215          ra.add(new JsonPrimitive(describeType(tr)));
1216      }
1217      for (TypeRefComponent tr : rev.getType()) {
1218        TypeRefComponent tm = getType(rev.getType(), tr);
1219        if (tm != null) {
1220          compareParameters(element, tr, tm);
1221        }
1222      }
1223
1224    }
1225    if (oa.size() > 0)
1226      element.add("removed-types", oa);
1227    if (ra.size() > 0)
1228      element.add("added-types", ra);
1229  }
1230
1231  private void compareParameters(JsonObject element, TypeRefComponent tr, TypeRefComponent tm) {
1232    JsonArray added = new JsonArray();
1233    JsonArray removed = new JsonArray();
1234
1235    for (CanonicalType p : tr.getTargetProfile()) {
1236      if (!hasParam(tm, p.asStringValue())) {
1237        added.add(new JsonPrimitive(p.asStringValue()));
1238      }
1239    }
1240
1241    for (CanonicalType p : tm.getTargetProfile()) {
1242      if (!hasParam(tr, p.asStringValue())) {
1243        removed.add(new JsonPrimitive(p.asStringValue()));
1244      }
1245    }
1246
1247    if (added.size() > 0)
1248      element.add(tr.getWorkingCode() + "-target-added", added);
1249    if (removed.size() > 0)
1250      element.add(tr.getWorkingCode() + "-target-removed", removed);
1251  }
1252
1253  private void analyseTypes(Document doc, Element element, ElementDefinition rev, ElementDefinition orig) {
1254    if (rev.getType().size() == 1 && orig.getType().size() == 1) {
1255      String r = describeType(rev.getType().get(0));
1256      if (Utilities.noString(r) && Utilities.existsInList(rev.getId(), "Element.id", "Extension.url"))
1257        r = "string";
1258      String o = describeType(orig.getType().get(0));
1259      if (Utilities.noString(o) && Utilities.existsInList(orig.getId(), "Element.id", "Extension.url"))
1260        o = "string";
1261      if (!o.equals(r)) {
1262        element.appendChild(makeElementWithAttribute(doc, "removed-type", "name", o));
1263        element.appendChild(makeElementWithAttribute(doc, "added-type", "name", r));
1264      }
1265    } else {
1266      for (TypeRefComponent tr : orig.getType()) {
1267        if (!hasType(rev.getType(), tr))
1268          element.appendChild(makeElementWithAttribute(doc, "removed-type", "name", describeType(tr)));
1269      }
1270      for (TypeRefComponent tr : rev.getType()) {
1271        if (!hasType(orig.getType(), tr) && !isAbstractType(tr.getWorkingCode()))
1272          element.appendChild(makeElementWithAttribute(doc, "added-type", "name", describeType(tr)));
1273      }
1274      for (TypeRefComponent tr : rev.getType()) {
1275        TypeRefComponent tm = getType(rev.getType(), tr);
1276        if (tm != null) {
1277          compareParameters(doc, element, tr, tm);
1278        }
1279      }
1280    }
1281  }
1282
1283  private void compareParameters(Document doc, Element element, TypeRefComponent tr, TypeRefComponent tm) {
1284
1285    for (CanonicalType p : tr.getTargetProfile()) {
1286      if (!hasParam(tm, p.asStringValue())) {
1287        element.appendChild(makeElementWithAttribute(doc, tr.getWorkingCode() + "-target-added", "name", p.asStringValue()));
1288      }
1289    }
1290
1291    for (CanonicalType p : tm.getTargetProfile()) {
1292      if (!hasParam(tr, p.asStringValue())) {
1293        element.appendChild(makeElementWithAttribute(doc, tr.getWorkingCode() + "-target-removed", "name", p.asStringValue()));
1294      }
1295    }
1296  }
1297
1298  private Node makeElementWithAttribute(Document doc, String name, String aname, String content) {
1299    Element e = doc.createElement(name);
1300    e.setAttribute(aname, content);
1301    return e;
1302  }
1303
1304  private void compareBindings(JsonObject element, ElementDefinition rev, ElementDefinition orig) {
1305    if (!hasBindingToNote(rev)) {
1306      element.addProperty("binding-status", "removed");
1307      describeBinding(element, "old-binding", orig);
1308    } else if (!hasBindingToNote(orig)) {
1309      element.addProperty("binding-status", "added");
1310      describeBinding(element, "new-binding", rev);
1311    } else if (compareBindings(element, rev.getBinding(), orig.getBinding())) {
1312      element.addProperty("binding-status", "changed");
1313      describeBinding(element, "old-binding", orig);
1314      describeBinding(element, "new-binding", rev);
1315    }
1316  }
1317
1318  private boolean compareBindings(JsonObject element, ElementDefinitionBindingComponent rev, ElementDefinitionBindingComponent orig) {
1319    boolean res = false;
1320    if (rev.getStrength() != orig.getStrength()) {
1321      element.addProperty("binding-strength-changed", true);
1322      res = true;
1323    }
1324    if (!Base.compareDeep(rev.getValueSet(), orig.getValueSet(), false)) {
1325      element.addProperty("binding-valueset-changed", true);
1326      res = true;
1327    }
1328    if (!maxValueSetsMatch(rev, orig)) {
1329      element.addProperty("max-valueset-changed", true);
1330      res = true;
1331    }
1332
1333    if (rev.getStrength() == BindingStrength.REQUIRED && orig.getStrength() == BindingStrength.REQUIRED) {
1334      JsonArray oa = new JsonArray();
1335      JsonArray ra = new JsonArray();
1336      ValueSet vrev = getValueSet(rev.getValueSet(), revision.getExpansions());
1337      ValueSet vorig = getValueSet(rev.getValueSet(), original.getExpansions());
1338      if (vrev != null && vorig != null) {
1339        for (ValueSetExpansionContainsComponent cc : vorig.getExpansion().getContains()) {
1340          if (!hasCode(vrev, cc))
1341            oa.add(new JsonPrimitive(cc.getCode()));
1342        }
1343        for (ValueSetExpansionContainsComponent cc : vrev.getExpansion().getContains()) {
1344          if (!hasCode(vorig, cc))
1345            ra.add(new JsonPrimitive(cc.getCode()));
1346        }
1347      }
1348      if (oa.size() > 0 || ra.size() > 0) {
1349        element.addProperty("binding-codes-changed", true);
1350        res = true;
1351      }
1352      if (oa.size() > 0)
1353        element.add("removed-codes", oa);
1354      if (ra.size() > 0)
1355        element.add("added-codes", ra);
1356    }
1357    return res;
1358  }
1359
1360  private boolean hasCode(ValueSet vs, ValueSetExpansionContainsComponent cc) {
1361    for (ValueSetExpansionContainsComponent ct : vs.getExpansion().getContains()) {
1362      if (ct.getSystem().equals(cc.getSystem()) && ct.getCode().equals(cc.getCode()))
1363        return true;
1364    }
1365    return false;
1366  }
1367
1368  private void compareBindings(Document doc, Element element, ElementDefinition rev, ElementDefinition orig) {
1369    if (!hasBindingToNote(rev)) {
1370      element.setAttribute("binding-status", "removed");
1371      describeBinding(doc, element, "old-binding", orig);
1372    } else if (!hasBindingToNote(orig)) {
1373      element.setAttribute("binding-status", "added");
1374      describeBinding(doc, element, "new-binding", rev);
1375    } else if (compareBindings(doc, element, rev.getBinding(), orig.getBinding())) {
1376      element.setAttribute("binding-status", "changed");
1377      describeBinding(doc, element, "old-binding", orig);
1378      describeBinding(doc, element, "new-binding", rev);
1379    }
1380  }
1381
1382  private boolean compareBindings(Document doc, Element element, ElementDefinitionBindingComponent rev, ElementDefinitionBindingComponent orig) {
1383    boolean res = false;
1384    if (rev.getStrength() != orig.getStrength()) {
1385      element.setAttribute("binding-strength-changed", "true");
1386      res = true;
1387    }
1388    if (!Base.compareDeep(rev.getValueSet(), orig.getValueSet(), false)) {
1389      element.setAttribute("binding-valueset-changed", "true");
1390      res = true;
1391    }
1392    if (!maxValueSetsMatch(rev, orig)) {
1393      element.setAttribute("max-valueset-changed", "true");
1394      res = true;
1395    }
1396    if (rev.getStrength() == BindingStrength.REQUIRED && orig.getStrength() == BindingStrength.REQUIRED) {
1397      ValueSet vrev = getValueSet(rev.getValueSet(), revision.getExpansions());
1398      ValueSet vorig = getValueSet(rev.getValueSet(), original.getExpansions());
1399      boolean changed = false;
1400      if (vrev != null && vorig != null) {
1401        for (ValueSetExpansionContainsComponent cc : vorig.getExpansion().getContains()) {
1402          if (!hasCode(vrev, cc)) {
1403            element.appendChild(makeElementWithAttribute(doc, "removed-code", "code", cc.getCode()));
1404            changed = true;
1405          }
1406        }
1407        for (ValueSetExpansionContainsComponent cc : vrev.getExpansion().getContains()) {
1408          if (!hasCode(vorig, cc)) {
1409            element.appendChild(makeElementWithAttribute(doc, "added-code", "code", cc.getCode()));
1410            changed = true;
1411          }
1412        }
1413      }
1414      if (changed) {
1415        element.setAttribute("binding-codes-changed", "true");
1416        res = true;
1417      }
1418    }
1419    return res;
1420  }
1421}