001package org.hl7.fhir.r5.comparison;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.Date;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009
010import org.hl7.fhir.exceptions.DefinitionException;
011import org.hl7.fhir.exceptions.FHIRException;
012import org.hl7.fhir.r5.comparison.ResourceComparer.MessageCounts;
013import org.hl7.fhir.r5.model.CodeSystem;
014import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
015import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
016import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
017import org.hl7.fhir.r5.model.CodeSystem.PropertyComponent;
018import org.hl7.fhir.utilities.Utilities;
019import org.hl7.fhir.utilities.validation.ValidationMessage;
020import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
021import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
022import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
023import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
024import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell;
025import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
026import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
027import org.hl7.fhir.utilities.xhtml.XhtmlNode;
028
029public class CodeSystemComparer extends CanonicalResourceComparer {
030
031
032  public class CodeSystemComparison extends CanonicalResourceComparison<CodeSystem> {
033
034    private StructuralMatch<ConceptDefinitionComponent> combined;                                             
035    private Map<String, String> propMap = new HashMap<>(); // right to left; left retains it's name
036
037    public CodeSystemComparison(CodeSystem left, CodeSystem right) {
038      super(left, right);
039      combined = new StructuralMatch<CodeSystem.ConceptDefinitionComponent>(); // base
040    }
041
042    public Map<String, String> getPropMap() {
043      return propMap;
044    }
045
046    public StructuralMatch<ConceptDefinitionComponent> getCombined() {
047      return combined;
048    }
049
050    @Override
051    protected String abbreviation() {
052      return "cs";
053    }
054
055    @Override
056    protected String summary() {
057      return "CodeSystem: "+left.present()+" vs "+right.present();
058    }
059
060    @Override
061    protected String fhirType() {
062      return "CodeSystem";
063    }
064
065    @Override
066    protected void countMessages(MessageCounts cnts) {
067      super.countMessages(cnts);
068      combined.countMessages(cnts);
069    }
070
071  }
072
073  private CodeSystem right;
074
075  public CodeSystemComparer(ComparisonSession session) {
076    super(session);
077  }
078
079  public CodeSystemComparison compare(CodeSystem left, CodeSystem right) {    
080    if (left == null)
081      throw new DefinitionException("No CodeSystem provided (left)");
082    if (right == null)
083      throw new DefinitionException("No CodeSystem provided (right)");
084    
085    
086    CodeSystemComparison res = new CodeSystemComparison(left, right);
087    session.identify(res);
088    CodeSystem cs = new CodeSystem();
089    res.setUnion(cs);
090    session.identify(cs);
091    cs.setName("Union"+left.getName()+"And"+right.getName());
092    cs.setTitle("Union of "+left.getTitle()+" And "+right.getTitle());
093    cs.setStatus(left.getStatus());
094    cs.setDate(new Date());
095    for (PropertyComponent pL : left.getProperty()) {
096      cs.addProperty(pL.copy());
097    }
098    for (PropertyComponent pR : left.getProperty()) {
099      PropertyComponent pL = findProperty(left, pR);
100      if (pL == null) {
101        String code = getUniqued(pR.getCode(), cs.getProperty());
102        cs.addProperty(pR.copy().setCode(code));
103      } else {
104        res.getPropMap().put(pR.getCode(), pL.getCode());
105      }
106    }
107
108    CodeSystem cs1 = new CodeSystem();
109    res.setIntersection(cs1);
110    session.identify(cs1);
111    cs1.setName("Intersection"+left.getName()+"And"+right.getName());
112    cs1.setTitle("Intersection of "+left.getTitle()+" And "+right.getTitle());
113    cs1.setStatus(left.getStatus());
114    cs1.setDate(new Date());
115    cs1.getProperty().addAll(cs.getProperty());
116
117    compareMetadata(left, right, res.getMetadata(), res);
118    comparePrimitives("caseSensitive", left.getCaseSensitiveElement(), right.getCaseSensitiveElement(), res.getMetadata(), IssueSeverity.ERROR, res);
119    comparePrimitives("hierarchyMeaning", left.getHierarchyMeaningElement(), right.getHierarchyMeaningElement(), res.getMetadata(), IssueSeverity.ERROR, res);
120    comparePrimitives("compositional", left.getCompositionalElement(), right.getCompositionalElement(), res.getMetadata(), IssueSeverity.WARNING, res);
121    comparePrimitives("versionNeeded", left.getVersionNeededElement(), right.getVersionNeededElement(), res.getMetadata(), IssueSeverity.INFORMATION, res);
122    comparePrimitives("content", left.getContentElement(), right.getContentElement(), res.getMetadata(), IssueSeverity.WARNING, res);
123
124    compareConcepts(left.getConcept(), right.getConcept(), res.getCombined(), res.getUnion().getConcept(), res.getIntersection().getConcept(), res.getUnion(), res.getIntersection(), res, "CodeSystem.concept");
125    return res;
126  }
127
128  private String getUniqued(String code, List<PropertyComponent> list) {
129    int i = 0;
130    while (true) {
131      boolean ok = true;
132      String res = code+(i == 0 ? "" : i);
133      for (PropertyComponent t : list) {
134        if (res.equals(t.getCode())) {
135          ok = false;
136        }
137      }
138      if (ok) {
139        return res;
140      }
141    }
142  }
143
144  private PropertyComponent findProperty(CodeSystem left, PropertyComponent p) {
145    for (PropertyComponent t : left.getProperty()) {
146      if (p.hasUri() && t.hasUri() && p.getUri().equals(t.getUri())) {
147        return t;
148      } else if (!p.hasUri() && !t.hasUri() && p.getCode().equals(t.getCode())) {
149        return t;
150      }
151    }
152    return null;
153  }
154
155
156  private void compareConcepts(List<ConceptDefinitionComponent> left, List<ConceptDefinitionComponent> right, StructuralMatch<ConceptDefinitionComponent> combined,
157    List<ConceptDefinitionComponent> union, List<ConceptDefinitionComponent> intersection, CodeSystem csU, CodeSystem csI, CodeSystemComparison res, String path) {
158    List<ConceptDefinitionComponent> matchR = new ArrayList<>();
159    for (ConceptDefinitionComponent l : left) {
160      ConceptDefinitionComponent r = findInList(right, l);
161      if (r == null) {
162        union.add(l);
163        combined.getChildren().add(new StructuralMatch<CodeSystem.ConceptDefinitionComponent>(l, vmI(IssueSeverity.INFORMATION, "Removed this concept", path)));
164      } else {
165        matchR.add(r);
166        ConceptDefinitionComponent cdM = merge(l, r, csU.getProperty(), res);
167        ConceptDefinitionComponent cdI = intersect(l, r, res);
168        union.add(cdM);
169        intersection.add(cdI);
170        StructuralMatch<ConceptDefinitionComponent> sm = new StructuralMatch<CodeSystem.ConceptDefinitionComponent>(l, r);
171        compare(sm.getMessages(), l, r, path+".where(code='"+l.getCode()+"')", res);
172        combined.getChildren().add(sm);
173        compareConcepts(l.getConcept(), r.getConcept(), sm, cdM.getConcept(), cdI.getConcept(), csU, csI, res, path+".where(code='"+l.getCode()+"').concept");
174      }
175    }
176    for (ConceptDefinitionComponent r : right) {
177      if (!matchR.contains(r)) {
178        union.add(r);
179        combined.getChildren().add(new StructuralMatch<CodeSystem.ConceptDefinitionComponent>(vmI(IssueSeverity.INFORMATION, "Added this concept", path), r));        
180      }
181    }
182  }
183
184  private ConceptDefinitionComponent findInList(List<ConceptDefinitionComponent> list, ConceptDefinitionComponent item) {
185    for (ConceptDefinitionComponent t : list) {
186      if (t.getCode().equals(item.getCode())) {
187        return t;
188      }
189    }
190    return null;
191  }
192
193  private void compare(List<ValidationMessage> msgs, ConceptDefinitionComponent l, ConceptDefinitionComponent r, String path, CodeSystemComparison res) {
194    compareStrings(path, msgs, l.getDisplay(), r.getDisplay(), "display", IssueSeverity.WARNING, res);
195    compareStrings(path, msgs, l.getDefinition(), r.getDefinition(), "definition", IssueSeverity.INFORMATION, res);    
196  }
197
198  private void compareStrings(String path, List<ValidationMessage> msgs, String left, String right, String name, IssueSeverity level, CodeSystemComparison res) {
199    if (!Utilities.noString(right)) {
200      if (Utilities.noString(left)) {
201        msgs.add(vmI(level, "Value for "+name+" added", path));
202      } else if (!left.equals(right)) {
203        if (level != IssueSeverity.NULL) {
204          res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path+"."+name, "Changed value for "+name+": '"+left+"' vs '"+right+"'", level));
205        }
206        msgs.add(vmI(level, name+" changed from left to right", path));
207      }
208    } else if (!Utilities.noString(left)) {
209      msgs.add(vmI(level, "Value for "+name+" removed", path));
210    }
211  }
212
213  private ConceptDefinitionComponent merge(ConceptDefinitionComponent l, ConceptDefinitionComponent r, List<PropertyComponent> destProps, CodeSystemComparison res) {
214    ConceptDefinitionComponent cd = l.copy();
215    if (!l.hasDisplay() && r.hasDisplay()) {
216      cd.setDisplay(r.getDisplay());
217    }
218    if (!l.hasDefinition() && r.hasDefinition()) {
219      cd.setDefinition(r.getDefinition());
220    }
221    mergeProps(cd, l, r, destProps, res);
222    mergeDesignations(cd, l, r);
223    return cd;
224  }
225
226  private ConceptDefinitionComponent intersect(ConceptDefinitionComponent l, ConceptDefinitionComponent r, CodeSystemComparison res) {
227    ConceptDefinitionComponent cd = l.copy();
228    if (l.hasDisplay() && !r.hasDisplay()) {
229      cd.setDisplay(null);
230    }
231    if (l.hasDefinition() && !r.hasDefinition()) {
232      cd.setDefinition(null);
233    }
234    intersectProps(cd, l, r, res);
235    //    mergeDesignations(cd, l, r);
236    return cd;
237  }
238
239  private void mergeDesignations(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r) {
240    for (ConceptDefinitionDesignationComponent td : l.getDesignation()) {
241      if (hasDesignation(td, r.getDesignation())) {
242        cd.getDesignation().add(td);
243      }
244    }
245    for (ConceptDefinitionDesignationComponent td : r.getDesignation()) {
246      if (hasDesignation(td, l.getDesignation())) {
247        cd.getDesignation().add(td);
248      }
249    }
250  }
251
252  private boolean hasDesignation(ConceptDefinitionDesignationComponent td, List<ConceptDefinitionDesignationComponent> designation) {
253    for (ConceptDefinitionDesignationComponent t : designation) {
254      if (designationsMatch(td, t)) {
255        return true;
256      }
257    }
258    return false;
259  }
260
261  private boolean designationsMatch(ConceptDefinitionDesignationComponent l, ConceptDefinitionDesignationComponent r) {
262    if (l.hasUse() != r.hasUse()) {
263      return false;
264    }
265    if (l.hasLanguage() != r.hasLanguage()) {
266      return false;
267    }
268    if (l.hasValue() != r.hasValue()) {
269      return false;
270    }
271    if (l.hasUse()) {
272      if (l.getUse().equalsDeep(r.getUse())) {
273        return false;
274      }
275    }
276    if (l.hasLanguage()) {
277      if (l.getLanguageElement().equalsDeep(r.getLanguageElement())) {
278        return false;
279      }
280    }
281    if (l.hasValue()) {
282      if (l.getValueElement().equalsDeep(r.getValueElement())) {
283        return false;
284      }
285    }
286    return true;
287  }
288
289  private void mergeProps(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r, List<PropertyComponent> destProps, CodeSystemComparison res) {
290    List<ConceptPropertyComponent> matchR = new ArrayList<>();
291    for (ConceptPropertyComponent lp : l.getProperty()) {
292      ConceptPropertyComponent rp = findRightProp(r.getProperty(), lp, res);
293      if (rp == null) {
294        cd.getProperty().add(lp);
295      } else {
296        matchR.add(rp);
297        cd.getProperty().add(lp);
298        if (lp.getValue().equalsDeep(rp.getValue())) {
299          cd.getProperty().add(rp.setCode(res.getPropMap().get(rp.getCode())));
300        }
301      }
302    }
303    for (ConceptPropertyComponent rp : r.getProperty()) {
304      if (!matchR.contains(rp)) {
305        cd.getProperty().add(rp.setCode(res.getPropMap().get(rp.getCode())));        
306      }
307    }
308  }
309
310  private void intersectProps(ConceptDefinitionComponent cd, ConceptDefinitionComponent l, ConceptDefinitionComponent r, CodeSystemComparison res) {
311    for (ConceptPropertyComponent lp : l.getProperty()) {
312      ConceptPropertyComponent rp = findRightProp(r.getProperty(), lp, res);
313      if (rp != null) {
314        cd.getProperty().add(lp);
315      }
316    }
317  }
318
319  private ConceptPropertyComponent findRightProp(List<ConceptPropertyComponent> rightProperties, ConceptPropertyComponent lp, CodeSystemComparison res) {
320    for (ConceptPropertyComponent p : rightProperties) {
321      if (res.getPropMap().get(p.getCode()).equals(lp.getCode())) {
322        return p;
323      }
324    }
325    return null;
326  }
327
328  public XhtmlNode renderConcepts(CodeSystemComparison comparison, String id, String prefix) throws FHIRException, IOException {
329    // columns: code, display (left|right), properties (left|right)
330    HierarchicalTableGenerator gen = new HierarchicalTableGenerator(Utilities.path("[tmp]", "compare"), false);
331    TableModel model = gen.new TableModel(id, true);
332    model.setAlternating(true);
333    model.getTitles().add(gen.new Title(null, null, "Code", "The code for the concept", null, 100));
334    model.getTitles().add(gen.new Title(null, null, "Display", "The display for the concept", null, 200, 2));
335    for (PropertyComponent p : comparison.getUnion().getProperty()) {
336      model.getTitles().add(gen.new Title(null, null, p.getCode(), p.getDescription(), null, 100, 2));
337    }
338    model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200));
339    for (StructuralMatch<ConceptDefinitionComponent> t : comparison.getCombined().getChildren()) {
340      addRow(gen, model.getRows(), t, comparison);
341    }
342    return gen.generate(model, prefix, 0, null);
343  }
344
345  private void addRow(HierarchicalTableGenerator gen, List<Row> rows, StructuralMatch<ConceptDefinitionComponent> t, CodeSystemComparison comparison) {
346    Row r = gen.new Row();
347    rows.add(r);
348    r.getCells().add(gen.new Cell(null, null, t.either().getCode(), null, null));
349    if (t.hasLeft() && t.hasRight()) {
350      if (t.getLeft().hasDisplay() && t.getRight().hasDisplay()) {
351        if (t.getLeft().getDisplay().equals(t.getRight().getDisplay())) {
352          r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2));        
353        } else {
354          r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
355          r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
356        }
357      } else if (t.getLeft().hasDisplay()) {
358        r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null));        
359        r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT));        
360      } else if (t.getRight().hasDisplay()) {        
361        r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT));        
362        r.getCells().add(gen.new Cell(null, null, t.getRight().getDisplay(), null, null));        
363      } else {
364        r.getCells().add(missingCell(gen).span(2));
365      }
366      for (PropertyComponent p : comparison.getUnion().getProperty()) {
367        ConceptPropertyComponent lp = getProp(t.getLeft(), p, false, comparison);
368        ConceptPropertyComponent rp = getProp(t.getRight(), p, true, comparison);
369
370        if (lp != null && rp != null) {
371          if (lp.getValue().equals(rp.getValue())) {
372            r.getCells().add(gen.new Cell(null, null, t.getLeft().getDisplay(), null, null).span(2));        
373          } else {
374            r.getCells().add(gen.new Cell(null, null, lp.getValue().toString(), null, null));        
375            r.getCells().add(gen.new Cell(null, null, rp.getValue().toString(), null, null));
376          }
377        } else if (lp != null) {
378          r.getCells().add(gen.new Cell(null, null, lp.getValue().toString(), null, null));        
379          r.getCells().add(missingCell(gen, COLOR_NO_CELL_RIGHT));        
380        } else if (rp != null) {        
381          r.getCells().add(missingCell(gen, COLOR_NO_CELL_LEFT));        
382          r.getCells().add(gen.new Cell(null, null, rp.getValue().toString(), null, null));        
383        } else {
384          r.getCells().add(missingCell(gen).span(2));
385        }
386
387      }
388    } else if (t.hasLeft()) {
389      r.setColor(COLOR_NO_ROW_RIGHT);
390      r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null));
391      r.getCells().add(missingCell(gen));
392      for (PropertyComponent p : comparison.getUnion().getProperty()) {
393        r.getCells().add(propertyCell(gen, t.getLeft(), p, false, comparison));
394        r.getCells().add(missingCell(gen));
395      }
396    } else {
397      r.setColor(COLOR_NO_ROW_LEFT);
398      r.getCells().add(missingCell(gen));
399      r.getCells().add(gen.new Cell(null, null, t.either().getDisplay(), null, null));
400      for (PropertyComponent p : comparison.getUnion().getProperty()) {
401        r.getCells().add(missingCell(gen));
402        r.getCells().add(propertyCell(gen, t.getLeft(), p, true, comparison));
403      }
404    }
405    r.getCells().add(cellForMessages(gen, t.getMessages()));
406  }
407
408  private Cell propertyCell(HierarchicalTableGenerator gen, ConceptDefinitionComponent cd, PropertyComponent p, boolean right, CodeSystemComparison comp) {
409    ConceptPropertyComponent cp = getProp(cd, p, right, comp);
410    if (cp == null) {
411      return missingCell(gen, right ? COLOR_NO_CELL_RIGHT : COLOR_NO_CELL_LEFT);
412    } else {
413      return gen.new Cell(null, null, cp.getValue().toString(), null, null);
414    }
415  }
416
417  public ConceptPropertyComponent getProp(ConceptDefinitionComponent cd, PropertyComponent p, boolean right, CodeSystemComparison comp) {
418    String c = p.getCode();
419    if (right) {
420      c = comp.getPropMap().get(c);
421    }
422    ConceptPropertyComponent cp = null;
423    if (cd != null) {
424      for (ConceptPropertyComponent t : cd.getProperty()) {
425        if (t.getCode().equals(c)) {
426          cp = t;
427        }
428      }
429    }
430    return cp;
431  }
432
433  @Override
434  protected String fhirType() {
435    return "CodeSystem";
436  }
437
438}