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