001package org.hl7.fhir.r5.comparison;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.HashMap;
007import java.util.List;
008import java.util.Map;
009import java.util.Set;
010
011import org.hl7.fhir.exceptions.FHIRException;
012import org.hl7.fhir.r5.comparison.ResourceComparer.MessageCounts;
013import org.hl7.fhir.r5.model.CanonicalResource;
014import org.hl7.fhir.r5.model.CanonicalType;
015import org.hl7.fhir.r5.model.CapabilityStatement;
016import org.hl7.fhir.r5.model.CodeType;
017import org.hl7.fhir.r5.model.CodeableConcept;
018import org.hl7.fhir.r5.model.Coding;
019import org.hl7.fhir.r5.model.PrimitiveType;
020import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
021import org.hl7.fhir.utilities.Utilities;
022import org.hl7.fhir.utilities.validation.ValidationMessage;
023import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
024import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
025import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
026import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
027import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
028import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
029import org.hl7.fhir.utilities.xhtml.XhtmlNode;
030
031public abstract class CanonicalResourceComparer extends ResourceComparer {
032
033
034  public abstract class CanonicalResourceComparison<T extends CanonicalResource> extends ResourceComparison {
035    protected T left;
036    protected T right;
037    protected T union;
038    protected T intersection;
039    protected Map<String, StructuralMatch<String>> metadata = new HashMap<>();                                             
040
041    public CanonicalResourceComparison(T left, T right) {
042      super(left.getId(), right.getId());
043      this.left = left;
044      this.right = right;
045    }
046
047    public T getLeft() {
048      return left;
049    }
050
051    public T getRight() {
052      return right;
053    }
054
055    public T getUnion() {
056      return union;
057    }
058
059    public T getIntersection() {
060      return intersection;
061    }
062
063    public Map<String, StructuralMatch<String>> getMetadata() {
064      return metadata;
065    }
066
067    public void setLeft(T left) {
068      this.left = left;
069    }
070
071    public void setRight(T right) {
072      this.right = right;
073    }
074
075    public void setUnion(T union) {
076      this.union = union;
077    }
078
079    public void setIntersection(T intersection) {
080      this.intersection = intersection;
081    }
082
083    @Override
084    protected String toTable() {
085      String s = "";
086      s = s + refCell(left);
087      s = s + refCell(right);
088      s = s + "<td><a href=\""+getId()+".html\">Comparison</a></td>";
089      s = s + "<td>"+outcomeSummary()+"</td>";
090      return "<tr style=\"background-color: "+color()+"\">"+s+"</tr>\r\n";
091    }
092
093    @Override
094    protected void countMessages(MessageCounts cnts) {
095      for (StructuralMatch<String> sm : metadata.values()) {
096        sm.countMessages(cnts);
097      }
098    }
099  }
100
101  public CanonicalResourceComparer(ComparisonSession session) {
102    super(session);
103  }
104
105  protected void compareMetadata(CanonicalResource left, CanonicalResource right, Map<String, StructuralMatch<String>> comp, CanonicalResourceComparison<? extends CanonicalResource> res) {
106    comparePrimitives("url", left.getUrlElement(), right.getUrlElement(), comp, IssueSeverity.ERROR, res);
107    comparePrimitives("version", left.getVersionElement(), right.getVersionElement(), comp, IssueSeverity.ERROR, res);
108    comparePrimitives("name", left.getNameElement(), right.getNameElement(), comp, IssueSeverity.INFORMATION, res);
109    comparePrimitives("title", left.getTitleElement(), right.getTitleElement(), comp, IssueSeverity.INFORMATION, res);
110    comparePrimitives("status", left.getStatusElement(), right.getStatusElement(), comp, IssueSeverity.INFORMATION, res);
111    comparePrimitives("experimental", left.getExperimentalElement(), right.getExperimentalElement(), comp, IssueSeverity.WARNING, res);
112    comparePrimitives("date", left.getDateElement(), right.getDateElement(), comp, IssueSeverity.INFORMATION, res);
113    comparePrimitives("publisher", left.getPublisherElement(), right.getPublisherElement(), comp, IssueSeverity.INFORMATION, res);
114    comparePrimitives("description", left.getDescriptionElement(), right.getDescriptionElement(), comp, IssueSeverity.NULL, res);
115    comparePrimitives("purpose", left.getPurposeElement(), right.getPurposeElement(), comp, IssueSeverity.NULL, res);
116    comparePrimitives("copyright", left.getCopyrightElement(), right.getCopyrightElement(), comp, IssueSeverity.INFORMATION, res);
117    compareCodeableConceptList("jurisdiction", left.getJurisdiction(), right.getJurisdiction(), comp, IssueSeverity.INFORMATION, res, res.getUnion().getJurisdiction(), res.getIntersection().getJurisdiction());
118  }
119
120  protected void compareCodeableConceptList(String name, List<CodeableConcept> left, List<CodeableConcept> right, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, List<CodeableConcept> union, List<CodeableConcept> intersection ) {
121    List<CodeableConcept> matchR = new ArrayList<>();
122    StructuralMatch<String> combined = new StructuralMatch<String>();
123    for (CodeableConcept l : left) {
124      CodeableConcept r = findCodeableConceptInList(right, l);
125      if (r == null) {
126        union.add(l);
127        combined.getChildren().add(new StructuralMatch<String>(gen(l), vm(IssueSeverity.INFORMATION, "Removed the item '"+gen(l)+"'", fhirType()+"."+name, res.getMessages())));
128      } else {
129        matchR.add(r);
130        union.add(r);
131        intersection.add(r);
132        StructuralMatch<String> sm = new StructuralMatch<String>(gen(l), gen(r));
133        combined.getChildren().add(sm);
134      }
135    }
136    for (CodeableConcept r : right) {
137      if (!matchR.contains(r)) {
138        union.add(r);
139        combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+gen(r)+"'", fhirType()+"."+name, res.getMessages()), gen(r)));        
140      }
141    }    
142    comp.put(name, combined);    
143  }
144  
145
146  private CodeableConcept findCodeableConceptInList(List<CodeableConcept> list, CodeableConcept item) {
147    for (CodeableConcept t : list) {
148      if (t.matches(item)) {
149        return t;
150      }
151    }
152    return null;
153  }
154  
155  protected String gen(CodeableConcept cc) {
156    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
157    for (Coding c : cc.getCoding()) {
158      b.append(gen(c));
159    }
160    return b.toString();
161  }
162
163  protected String gen(Coding c) {
164    return c.getSystem()+(c.hasVersion() ? "|"+c.getVersion() : "")+"#"+c.getCode();
165  }
166
167  protected void compareCanonicalList(String name, List<CanonicalType> left, List<CanonicalType> right, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, List<CanonicalType> union, List<CanonicalType> intersection ) {
168    List<CanonicalType> matchR = new ArrayList<>();
169    StructuralMatch<String> combined = new StructuralMatch<String>();
170    for (CanonicalType l : left) {
171      CanonicalType r = findCanonicalInList(right, l);
172      if (r == null) {
173        union.add(l);
174        combined.getChildren().add(new StructuralMatch<String>(l.getValue(), vm(IssueSeverity.INFORMATION, "Removed the item '"+l.getValue()+"'", fhirType()+"."+name, res.getMessages())));
175      } else {
176        matchR.add(r);
177        union.add(r);
178        intersection.add(r);
179        StructuralMatch<String> sm = new StructuralMatch<String>(l.getValue(), r.getValue());
180        combined.getChildren().add(sm);
181      }
182    }
183    for (CanonicalType r : right) {
184      if (!matchR.contains(r)) {
185        union.add(r);
186        combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+r.getValue()+"'", fhirType()+"."+name, res.getMessages()), r.getValue()));        
187      }
188    }    
189    comp.put(name, combined);    
190  }
191  
192  private CanonicalType findCanonicalInList(List<CanonicalType> list, CanonicalType item) {
193    for (CanonicalType t : list) {
194      if (t.getValue().equals(item.getValue())) {
195        return t;
196      }
197    }
198    return null;
199  }
200
201  protected void compareCodeList(String name, List<CodeType> left, List<CodeType> right, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res, List<CodeType> union, List<CodeType> intersection ) {
202    List<CodeType> matchR = new ArrayList<>();
203    StructuralMatch<String> combined = new StructuralMatch<String>();
204    for (CodeType l : left) {
205      CodeType r = findCodeInList(right, l);
206      if (r == null) {
207        union.add(l);
208        combined.getChildren().add(new StructuralMatch<String>(l.getValue(), vm(IssueSeverity.INFORMATION, "Removed the item '"+l.getValue()+"'", fhirType()+"."+name, res.getMessages())));
209      } else {
210        matchR.add(r);
211        union.add(r);
212        intersection.add(r);
213        StructuralMatch<String> sm = new StructuralMatch<String>(l.getValue(), r.getValue());
214        combined.getChildren().add(sm);
215      }
216    }
217    for (CodeType r : right) {
218      if (!matchR.contains(r)) {
219        union.add(r);
220        combined.getChildren().add(new StructuralMatch<String>(vm(IssueSeverity.INFORMATION, "Added the item '"+r.getValue()+"'", fhirType()+"."+name, res.getMessages()), r.getValue()));        
221      }
222    }    
223    comp.put(name, combined);    
224  }
225  
226  private CodeType findCodeInList(List<CodeType> list, CodeType item) {
227    for (CodeType t : list) {
228      if (t.getValue().equals(item.getValue())) {
229        return t;
230      }
231    }
232    return null;
233  }
234
235  @SuppressWarnings("rawtypes")
236  protected void comparePrimitives(String name, PrimitiveType l, PrimitiveType r, Map<String, StructuralMatch<String>> comp, IssueSeverity level, CanonicalResourceComparison<? extends CanonicalResource> res) {
237    StructuralMatch<String> match = null;
238    if (l.isEmpty() && r.isEmpty()) {
239      match = new StructuralMatch<>(null, null, null);
240    } else if (l.isEmpty()) {
241      match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "Added the item '"+r.primitiveValue()+"'", fhirType()+"."+name));
242    } else if (r.isEmpty()) {
243      match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "Removed the item '"+l.primitiveValue()+"'", fhirType()+"."+name));
244    } else if (!l.hasValue() && !r.hasValue()) {
245      match = new StructuralMatch<>(null, null, vmI(IssueSeverity.INFORMATION, "No Value", fhirType()+"."+name));
246    } else if (!l.hasValue()) {
247      match = new StructuralMatch<>(null, r.primitiveValue(), vmI(IssueSeverity.INFORMATION, "No Value on Left", fhirType()+"."+name));
248    } else if (!r.hasValue()) {
249      match = new StructuralMatch<>(l.primitiveValue(), null, vmI(IssueSeverity.INFORMATION, "No Value on Right", fhirType()+"."+name));
250    } else if (l.getValue().equals(r.getValue())) {
251      match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), null);
252    } else {
253      match = new StructuralMatch<>(l.primitiveValue(), r.primitiveValue(), vmI(level, "Values Differ", fhirType()+"."+name));
254      if (level != IssueSeverity.NULL) {
255        res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, fhirType()+"."+name, "Values for "+name+" differ: '"+l.primitiveValue()+"' vs '"+r.primitiveValue()+"'", level));
256      }
257    } 
258    comp.put(name, match);    
259  }
260
261  protected abstract String fhirType();
262
263  public XhtmlNode renderMetadata(CanonicalResourceComparison<? extends CanonicalResource> comparison, String id, String prefix) throws FHIRException, IOException {
264    // columns: code, display (left|right), properties (left|right)
265    HierarchicalTableGenerator gen = new HierarchicalTableGenerator(Utilities.path("[tmp]", "compare"), false);
266    TableModel model = gen.new TableModel(id, true);
267    model.setAlternating(true);
268    model.getTitles().add(gen.new Title(null, null, "Name", "Property Name", null, 100));
269    model.getTitles().add(gen.new Title(null, null, "Value", "The value of the property", null, 200, 2));
270    model.getTitles().add(gen.new Title(null, null, "Comments", "Additional information about the comparison", null, 200));
271
272    for (String n : sorted(comparison.getMetadata().keySet())) {
273      StructuralMatch<String> t = comparison.getMetadata().get(n);
274      addRow(gen, model.getRows(), n, t);
275    }
276    return gen.generate(model, prefix, 0, null);
277  }
278
279  private void addRow(HierarchicalTableGenerator gen, List<Row> rows, String name, StructuralMatch<String> t) {
280    Row r = gen.new Row();
281    rows.add(r);
282    r.getCells().add(gen.new Cell(null, null, name, null, null));
283    if (t.hasLeft() && t.hasRight()) {
284      if (t.getLeft().equals(t.getRight())) {
285        r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null).span(2));        
286      } else {
287        r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));        
288        r.getCells().add(gen.new Cell(null, null, t.getRight(), null, null).setStyle("background-color: "+COLOR_DIFFERENT));
289      }
290    } else if (t.hasLeft()) {
291      r.setColor(COLOR_NO_ROW_RIGHT);
292      r.getCells().add(gen.new Cell(null, null, t.getLeft(), null, null));        
293      r.getCells().add(missingCell(gen));        
294    } else if (t.hasRight()) {        
295      r.setColor(COLOR_NO_ROW_LEFT);
296      r.getCells().add(missingCell(gen));        
297      r.getCells().add(gen.new Cell(null, null, t.getRight(), null, null));        
298    } else {
299      r.getCells().add(missingCell(gen).span(2));
300    }
301    r.getCells().add(cellForMessages(gen, t.getMessages()));
302    int i = 0;
303    for (StructuralMatch<String> c : t.getChildren()) {
304      addRow(gen, r.getSubRows(), name+"["+i+"]", c);
305      i++;
306    }
307  }
308
309
310  private List<String> sorted(Set<String> keys) {
311    List<String> res = new ArrayList<>();
312    res.addAll(keys);
313    Collections.sort(res);
314    return res;
315  }
316
317
318}