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