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