001package org.hl7.fhir.r4b.renderers;
002
003import java.io.BufferedWriter;
004import java.io.FileWriter;
005import java.io.IOException;
006import java.text.ParseException;
007import java.text.SimpleDateFormat;
008import java.util.ArrayList;
009import java.util.Collections;
010import java.util.Date;
011import java.util.HashMap;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Map;
015import java.util.Set;
016
017import org.hl7.fhir.exceptions.DefinitionException;
018import org.hl7.fhir.exceptions.FHIRException;
019import org.hl7.fhir.exceptions.FHIRFormatError;
020import org.hl7.fhir.exceptions.TerminologyServiceException;
021import org.hl7.fhir.r4b.context.IWorkerContext.CodingValidationRequest;
022import org.hl7.fhir.r4b.context.IWorkerContext.ValidationResult;
023import org.hl7.fhir.r4b.model.BooleanType;
024import org.hl7.fhir.r4b.model.CanonicalResource;
025import org.hl7.fhir.r4b.model.CodeSystem;
026import org.hl7.fhir.r4b.model.CodeSystem.ConceptDefinitionComponent;
027import org.hl7.fhir.r4b.model.Coding;
028import org.hl7.fhir.r4b.model.ConceptMap;
029import org.hl7.fhir.r4b.model.DataType;
030import org.hl7.fhir.r4b.model.DomainResource;
031import org.hl7.fhir.r4b.model.Enumerations.FilterOperator;
032import org.hl7.fhir.r4b.model.Extension;
033import org.hl7.fhir.r4b.model.ExtensionHelper;
034import org.hl7.fhir.r4b.model.PrimitiveType;
035import org.hl7.fhir.r4b.model.Resource;
036import org.hl7.fhir.r4b.model.UriType;
037import org.hl7.fhir.r4b.model.ValueSet;
038import org.hl7.fhir.r4b.model.ValueSet.ConceptReferenceComponent;
039import org.hl7.fhir.r4b.model.ValueSet.ConceptReferenceDesignationComponent;
040import org.hl7.fhir.r4b.model.ValueSet.ConceptSetComponent;
041import org.hl7.fhir.r4b.model.ValueSet.ConceptSetFilterComponent;
042import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionComponent;
043import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent;
044import org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionParameterComponent;
045import org.hl7.fhir.r4b.renderers.utils.RenderingContext;
046import org.hl7.fhir.r4b.renderers.utils.Resolver.ResourceContext;
047import org.hl7.fhir.r4b.terminologies.CodeSystemUtilities;
048import org.hl7.fhir.r4b.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
049import org.hl7.fhir.r4b.utils.ToolingExtensions;
050import org.hl7.fhir.utilities.Utilities;
051import org.hl7.fhir.utilities.xhtml.XhtmlNode;
052
053import com.google.common.collect.HashMultimap;
054import com.google.common.collect.Multimap;
055
056public class ValueSetRenderer extends TerminologyRenderer {
057
058  public ValueSetRenderer(RenderingContext context) {
059    super(context);
060  }
061
062  public ValueSetRenderer(RenderingContext context, ResourceContext rcontext) {
063    super(context, rcontext);
064  }
065
066  private static final String ABSTRACT_CODE_HINT = "This code is not selectable ('Abstract')";
067
068  private static final int MAX_LANGS_IN_LINE = 5;
069
070  private List<ConceptMapRenderInstructions> renderingMaps = new ArrayList<ConceptMapRenderInstructions>();
071
072  public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException {
073    return render(x, (ValueSet) dr, false);
074  }
075  
076  public boolean render(XhtmlNode x, ValueSet vs, boolean header) throws FHIRFormatError, DefinitionException, IOException {
077   List<UsedConceptMap> maps = findReleventMaps(vs);
078    
079    boolean hasExtensions;
080    if (vs.hasExpansion()) {
081      // for now, we just accept an expansion if there is one
082      hasExtensions = generateExpansion(x, vs, header, maps);
083    } else {
084      hasExtensions = generateComposition(x, vs, header, maps);
085    }
086    return hasExtensions;
087  }
088
089  public void describe(XhtmlNode x, ValueSet vs) {
090    x.tx(display(vs));
091  }
092
093  public String display(ValueSet vs) {
094    return vs.present();
095  }
096
097  
098  private List<UsedConceptMap> findReleventMaps(ValueSet vs) throws FHIRException {
099    List<UsedConceptMap> res = new ArrayList<UsedConceptMap>();
100    for (CanonicalResource md : getContext().getWorker().allConformanceResources()) {
101      if (md instanceof ConceptMap) {
102        ConceptMap cm = (ConceptMap) md;
103        if (isSource(vs, cm.getSource())) {
104          ConceptMapRenderInstructions re = findByTarget(cm.getTarget());
105          if (re != null) {
106            ValueSet vst = cm.hasTarget() ? getContext().getWorker().fetchResource(ValueSet.class, cm.hasTargetCanonicalType() ? cm.getTargetCanonicalType().getValue() : cm.getTargetUriType().asStringValue()) : null;
107            res.add(new UsedConceptMap(re, vst == null ? cm.getUserString("path") : vst.getUserString("path"), cm));
108          }
109        }
110      }
111    }
112    return res;
113//    Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>();
114//  for (ConceptMap a : context.getWorker().findMapsForSource(vs.getUrl())) {
115//    String url = "";
116//    ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference());
117//    if (vsr != null)
118//      url = (String) vsr.getUserData("filename");
119//    mymaps.put(a, url);
120//  }
121//    Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>();
122//  for (ConceptMap a : context.getWorker().findMapsForSource(cs.getValueSet())) {
123//    String url = "";
124//    ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference());
125//    if (vsr != null)
126//      url = (String) vsr.getUserData("filename");
127//    mymaps.put(a, url);
128//  }
129    // also, look in the contained resources for a concept map
130//    for (Resource r : cs.getContained()) {
131//      if (r instanceof ConceptMap) {
132//        ConceptMap cm = (ConceptMap) r;
133//        if (((Reference) cm.getSource()).getReference().equals(cs.getValueSet())) {
134//          String url = "";
135//          ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) cm.getTarget()).getReference());
136//          if (vsr != null)
137//              url = (String) vsr.getUserData("filename");
138//        mymaps.put(cm, url);
139//        }
140//      }
141//    }
142  }  
143  
144  private boolean isSource(ValueSet vs, DataType source) {
145    return vs.hasUrl() && source != null && vs.getUrl().equals(source.primitiveValue());
146  }  
147  
148  private boolean generateExpansion(XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRFormatError, DefinitionException, IOException {
149    boolean hasExtensions = false;
150    List<String> langs = new ArrayList<String>();
151
152
153    if (header) {
154      XhtmlNode h = x.addTag(getHeader());
155      h.tx("Value Set Contents");
156      if (IsNotFixedExpansion(vs))
157        addMarkdown(x, vs.getDescription());
158      if (vs.hasCopyright())
159        generateCopyright(x, vs);
160    }
161    if (ToolingExtensions.hasExtension(vs.getExpansion(), ToolingExtensions.EXT_EXP_TOOCOSTLY)) {
162      List<Extension> exl = vs.getExpansion().getExtensionsByUrl(ToolingExtensions.EXT_EXP_TOOCOSTLY);
163      boolean other = false;
164      for (Extension ex : exl) {
165        if (ex.getValue() instanceof BooleanType) {
166          x.para().style("border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? getContext().getTooCostlyNoteEmpty() : getContext().getTooCostlyNoteNotEmpty());
167        } else if (!other) {
168          x.para().style("border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? getContext().getTooCostlyNoteEmptyDependent() : getContext().getTooCostlyNoteNotEmptyDependent());
169          other = true;
170        }
171      }
172    } else {
173      Integer count = countMembership(vs);
174      if (count == null)
175        x.para().tx("This value set does not contain a fixed number of concepts");
176      else
177        x.para().tx("This value set contains "+count.toString()+" concepts");
178    }
179    
180    generateContentModeNotices(x, vs.getExpansion());
181    generateVersionNotice(x, vs.getExpansion());
182
183    CodeSystem allCS = null;
184    boolean doLevel = false;
185    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
186      if (cc.hasContains()) {
187        doLevel = true;
188        break;
189      }
190    }
191    
192    boolean doSystem = true; // checkDoSystem(vs, src);
193    boolean doDefinition = checkDoDefinition(vs.getExpansion().getContains());
194    if (doSystem && allFromOneSystem(vs)) {
195      doSystem = false;
196      XhtmlNode p = x.para();
197      p.tx("All codes in this table are from the system ");
198      allCS = getContext().getWorker().fetchCodeSystem(vs.getExpansion().getContains().get(0).getSystem());
199      String ref = null;
200      if (allCS != null)
201        ref = getCsRef(allCS);
202      if (ref == null)
203        p.code(vs.getExpansion().getContains().get(0).getSystem());
204      else
205        p.ah(context.fixReference(ref)).code(vs.getExpansion().getContains().get(0).getSystem());
206    }
207    XhtmlNode t = x.table( "codes");
208    XhtmlNode tr = t.tr();
209    if (doLevel)
210      tr.td().b().tx("Lvl");
211    tr.td().attribute("style", "white-space:nowrap").b().tx("Code");
212    if (doSystem)
213      tr.td().b().tx("System");
214    XhtmlNode tdDisp = tr.td();
215    tdDisp.b().tx("Display");
216    boolean doLangs = false;
217    for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) {
218      scanForLangs(c, langs);
219    }
220    if (doDefinition) {
221      tr.td().b().tx("Definition");
222      doLangs = false;
223    } else {
224      // if we're not doing definitions and we don't have too many languages, we'll do them in line
225      if (langs.size() < MAX_LANGS_IN_LINE) {
226        doLangs = true;
227        if (vs.hasLanguage()) {
228          tdDisp.tx(" - "+describeLang(vs.getLanguage()));
229        }
230        for (String lang : langs) {
231          tr.td().b().addText(describeLang(lang));
232        }
233      }
234    }
235
236    
237    addMapHeaders(tr, maps);
238    for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) {
239      addExpansionRowToTable(t, c, 0, doLevel, doSystem, doDefinition, maps, allCS, langs, doLangs);
240    }
241
242    // now, build observed languages
243
244    if (!doLangs && langs.size() > 0) {
245      Collections.sort(langs);
246      x.para().b().tx("Additional Language Displays");
247      t = x.table( "codes");
248      tr = t.tr();
249      tr.td().b().tx("Code");
250      for (String lang : langs) {
251        tr.td().b().addText(describeLang(lang));
252      }
253      for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) {
254        addLanguageRow(c, t, langs);
255      }
256    }
257
258    return hasExtensions;
259  }
260
261  private void generateContentModeNotices(XhtmlNode x, ValueSetExpansionComponent expansion) {
262    generateContentModeNotice(x, expansion, "example", "Expansion based on example code system"); 
263    generateContentModeNotice(x, expansion, "fragment", "Expansion based on code system fragment"); 
264  }
265  
266  private void generateContentModeNotice(XhtmlNode x, ValueSetExpansionComponent expansion, String mode, String text) {
267    Multimap<String, String> versions = HashMultimap.create();
268    for (ValueSetExpansionParameterComponent p : expansion.getParameter()) {
269      if (p.getName().equals(mode)) {
270        String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|");
271        if (parts.length == 2)
272          versions.put(parts[0], parts[1]);
273      }
274    }
275    if (versions.size() > 0) {
276      XhtmlNode div = null;
277      XhtmlNode ul = null;
278      boolean first = true;
279      for (String s : versions.keySet()) {
280        if (versions.size() == 1 && versions.get(s).size() == 1) {
281          for (String v : versions.get(s)) { // though there'll only be one
282            XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #ffcccc; padding: 8px; margin-bottom: 8px");
283            p.tx(text+" ");
284            expRef(p, s, v);
285          }
286        } else {
287          for (String v : versions.get(s)) {
288            if (first) {
289              div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px");
290              div.para().tx(text+"s: ");
291              ul = div.ul();
292              first = false;
293            }
294            expRef(ul.li(), s, v);
295          }
296        }
297      }
298    }
299  }
300
301  private boolean checkDoSystem(ValueSet vs, ValueSet src) {
302    if (src != null)
303      vs = src;
304    return vs.hasCompose();
305  }
306
307  private boolean IsNotFixedExpansion(ValueSet vs) {
308    if (vs.hasCompose())
309      return false;
310
311
312    // it's not fixed if it has any includes that are not version fixed
313    for (ConceptSetComponent cc : vs.getCompose().getInclude()) {
314      if (cc.hasValueSet())
315        return true;
316      if (!cc.hasVersion())
317        return true;
318    }
319    return false;
320  }
321
322
323 
324  
325  private ConceptMapRenderInstructions findByTarget(DataType source) {
326    if (source == null) {
327      return null;
328    }
329    String src = source.primitiveValue();
330    if (src != null)
331      for (ConceptMapRenderInstructions t : renderingMaps) {
332        if (src.equals(t.getUrl()))
333          return t;
334      }
335    return null;
336  }
337
338
339  private Integer countMembership(ValueSet vs) {
340    int count = 0;
341    if (vs.hasExpansion())
342      count = count + conceptCount(vs.getExpansion().getContains());
343    else {
344      if (vs.hasCompose()) {
345        if (vs.getCompose().hasExclude()) {
346          try {
347            ValueSetExpansionOutcome vse = getContext().getWorker().expandVS(vs, true, false);
348            count = 0;
349            count += conceptCount(vse.getValueset().getExpansion().getContains());
350            return count;
351          } catch (Exception e) {
352            return null;
353          }
354        }
355        for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
356          if (inc.hasFilter())
357            return null;
358          if (!inc.hasConcept())
359            return null;
360          count = count + inc.getConcept().size();
361        }
362      }
363    }
364    return count;
365  }
366
367  private int conceptCount(List<ValueSetExpansionContainsComponent> list) {
368    int count = 0;
369    for (ValueSetExpansionContainsComponent c : list) {
370      if (!c.getAbstract())
371        count++;
372      count = count + conceptCount(c.getContains());
373    }
374    return count;
375  }
376
377  private void addCSRef(XhtmlNode x, String url) {
378    CodeSystem cs = getContext().getWorker().fetchCodeSystem(url);
379    if (cs == null) {
380      x.code(url);
381    } else if (cs.hasUserData("path")) {
382      x.ah(cs.getUserString("path")).tx(cs.present());
383    } else {
384      x.code(url);
385      x.tx(" ("+cs.present()+")");
386    }
387  }
388
389  @SuppressWarnings("rawtypes")
390  private void generateVersionNotice(XhtmlNode x, ValueSetExpansionComponent expansion) {
391    Multimap<String, String> versions = HashMultimap.create();
392    for (ValueSetExpansionParameterComponent p : expansion.getParameter()) {
393      if (p.getName().equals("version")) {
394        String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|");
395        if (parts.length == 2)
396          versions.put(parts[0], parts[1]);
397      }
398    }
399    if (versions.size() > 0) {
400      XhtmlNode div = null;
401      XhtmlNode ul = null;
402      boolean first = true;
403      for (String s : versions.keySet()) {
404        if (versions.size() == 1 && versions.get(s).size() == 1) {
405          for (String v : versions.get(s)) { // though there'll only be one
406            XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px");
407            p.tx("Expansion based on ");
408            expRef(p, s, v);
409          }
410        } else {
411          for (String v : versions.get(s)) {
412            if (first) {
413              div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px");
414              div.para().tx("Expansion based on: ");
415              ul = div.ul();
416              first = false;
417            }
418            expRef(ul.li(), s, v);
419          }
420        }
421      }
422    }
423  }
424
425  private void expRef(XhtmlNode x, String u, String v) {
426    // TODO Auto-generated method stub
427    if (u.equals("http://snomed.info/sct")) {
428      String[] parts = v.split("\\/");
429      if (parts.length >= 5) {
430        String m = describeModule(parts[4]);
431        if (parts.length == 7) {
432          x.tx("SNOMED CT "+m+" edition "+formatSCTDate(parts[6]));
433        } else {
434          x.tx("SNOMED CT "+m+" edition");
435        }
436      } else {
437        x.tx(describeSystem(u)+" version "+v);
438      }
439    } else if (u.equals("http://loinc.org")) {
440      String vd = describeLoincVer(v);
441      if (vd != null) {
442        x.tx("Loinc v"+v+" ("+vd+")");
443      } else {
444        x.tx("Loinc v"+v);        
445      }
446    } else {
447      CanonicalResource cr = (CanonicalResource) getContext().getWorker().fetchResource(Resource.class, u+"|"+v);
448      if (cr != null) {
449        if (cr.hasUserData("path")) {
450          x.ah(cr.getUserString("path")).tx(cr.present()+" v"+v+" ("+cr.fhirType()+")");          
451        } else {
452          x.tx(describeSystem(u)+" v"+v+" ("+cr.fhirType()+")");
453        }
454      } else {
455        x.tx(describeSystem(u)+" version "+v);
456      }
457    }
458  }
459
460  private String describeLoincVer(String v) {
461    if ("2.67".equals(v))  return "Dec 2019";
462    if ("2.66".equals(v))  return "Jun 2019";
463    if ("2.65".equals(v))  return "Dec 2018";
464    if ("2.64".equals(v))  return "Jun 2018";
465    if ("2.63".equals(v))  return "Dec 2017";
466    if ("2.61".equals(v))  return "Jun 2017";
467    if ("2.59".equals(v))  return "Feb 2017";
468    if ("2.58".equals(v))  return "Dec 2016";
469    if ("2.56".equals(v))  return "Jun 2016";
470    if ("2.54".equals(v))  return "Dec 2015";
471    if ("2.52".equals(v))  return "Jun 2015";
472    if ("2.50".equals(v))  return "Dec 2014";
473    if ("2.48".equals(v))  return "Jun 2014";
474    if ("2.46".equals(v))  return "Dec 2013";
475    if ("2.44".equals(v))  return "Jun 2013";
476    if ("2.42".equals(v))  return "Dec 2012";
477    if ("2.40".equals(v))  return "Jun 2012";
478    if ("2.38".equals(v))  return "Dec 2011";
479    if ("2.36".equals(v))  return "Jun 2011";
480    if ("2.34".equals(v))  return "Dec 2010";
481    if ("2.32".equals(v))  return "Jun 2010";
482    if ("2.30".equals(v))  return "Feb 2010";
483    if ("2.29".equals(v))  return "Dec 2009";
484    if ("2.27".equals(v))  return "Jul 2009";
485    if ("2.26".equals(v))  return "Jan 2009";
486    if ("2.24".equals(v))  return "Jul 2008";
487    if ("2.22".equals(v))  return "Dec 2007";
488    if ("2.21".equals(v))  return "Jun 2007";
489    if ("2.19".equals(v))  return "Dec 2006";
490    if ("2.17".equals(v))  return "Jun 2006";
491    if ("2.16".equals(v))  return "Dec 2005";
492    if ("2.15".equals(v))  return "Jun 2005";
493    if ("2.14".equals(v))  return "Dec 2004";
494    if ("2.13".equals(v))  return "Aug 2004";
495    if ("2.12".equals(v))  return "Feb 2004";
496    if ("2.10".equals(v))  return "Oct 2003";
497    if ("2.09".equals(v))  return "May 2003";
498    if ("2.08 ".equals(v)) return "Sep 2002";
499    if ("2.07".equals(v))  return "Aug 2002";
500    if ("2.05".equals(v))  return "Feb 2002";
501    if ("2.04".equals(v))  return "Jan 2002";
502    if ("2.03".equals(v))  return "Jul 2001";
503    if ("2.02".equals(v))  return "May 2001";
504    if ("2.01".equals(v))  return "Jan 2001";
505    if ("2.00".equals(v))  return "Jan 2001";
506    if ("1.0n".equals(v))  return "Feb 2000";
507    if ("1.0ma".equals(v)) return "Aug 1999";
508    if ("1.0m".equals(v))  return "Jul 1999";
509    if ("1.0l".equals(v))  return "Jan 1998";
510    if ("1.0ja".equals(v)) return "Oct 1997";
511    return null;
512  }
513
514  private String formatSCTDate(String ds) {
515    SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
516    Date date;
517    try {
518      date = format.parse(ds);
519    } catch (ParseException e) {
520      return ds;
521    }
522    return new SimpleDateFormat("dd-MMM yyyy").format(date);
523  }
524
525  private String describeModule(String module) {
526    if ("900000000000207008".equals(module))
527      return "International";
528    if ("731000124108".equals(module))
529      return "United States";
530    if ("32506021000036107".equals(module))
531      return "Australian";
532    if ("449081005".equals(module))
533      return "Spanish";
534    if ("554471000005108".equals(module))
535      return "Danish";
536    if ("11000146104".equals(module))
537      return "Dutch";
538    if ("45991000052106".equals(module))
539      return "Swedish";
540    if ("999000041000000102".equals(module))
541      return "United Kingdon";
542    return module;
543  }
544
545  private boolean hasVersionParameter(ValueSetExpansionComponent expansion) {
546    for (ValueSetExpansionParameterComponent p : expansion.getParameter()) {
547      if (p.getName().equals("version"))
548        return true;
549    }
550    return false;
551  }
552
553  private void addLanguageRow(ValueSetExpansionContainsComponent c, XhtmlNode t, List<String> langs) {
554    XhtmlNode tr = t.tr();
555    tr.td().addText(c.getCode());
556    addLangaugesToRow(c, langs, tr);
557    for (ValueSetExpansionContainsComponent cc : c.getContains()) {
558      addLanguageRow(cc, t, langs);
559    }
560  }
561
562  public void addLangaugesToRow(ValueSetExpansionContainsComponent c, List<String> langs, XhtmlNode tr) {
563    for (String lang : langs) {
564      String d = null;
565      for (Extension ext : c.getExtension()) {
566        if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
567          String l = ToolingExtensions.readStringExtension(ext, "lang");
568          if (lang.equals(l)) {
569            d = ToolingExtensions.readStringExtension(ext, "content");
570          }
571        }
572      }
573      if (d == null) {
574        for (ConceptReferenceDesignationComponent dd : c.getDesignation()) {
575          String l = dd.getLanguage();
576          if (lang.equals(l)) {
577            d = dd.getValue();
578          }
579        }
580      }
581      tr.td().addText(d == null ? "" : d);
582    }
583  }
584
585  
586  private boolean checkDoDefinition(List<ValueSetExpansionContainsComponent> contains) {
587    for (ValueSetExpansionContainsComponent c : contains) {
588      CodeSystem cs = getContext().getWorker().fetchCodeSystem(c.getSystem());
589      if (cs != null)
590        return true;
591      if (checkDoDefinition(c.getContains()))
592        return true;
593    }
594    return false;
595  }
596
597
598  private boolean allFromOneSystem(ValueSet vs) {
599    if (vs.getExpansion().getContains().isEmpty())
600      return false;
601    String system = vs.getExpansion().getContains().get(0).getSystem();
602    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
603      if (!checkSystemMatches(system, cc))
604        return false;
605    }
606    return true;
607  }
608
609  private String getCsRef(String system) {
610    CodeSystem cs = getContext().getWorker().fetchCodeSystem(system);
611    return getCsRef(cs);
612  }
613
614  private  <T extends Resource> String getCsRef(T cs) {
615    String ref = (String) cs.getUserData("filename");
616    if (ref == null)
617      ref = (String) cs.getUserData("path");
618    if (ref == null)
619      return "?ngen-14?.html";
620    if (!ref.contains(".html"))
621      ref = ref + ".html";
622    return ref.replace("\\", "/");
623  }
624
625  private void scanForLangs(ValueSetExpansionContainsComponent c, List<String> langs) {
626    for (Extension ext : c.getExtension()) {
627      if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
628        String lang = ToolingExtensions.readStringExtension(ext,  "lang");
629        if (!Utilities.noString(lang) && !langs.contains(lang)) {
630          langs.add(lang);
631        }
632      }
633    }
634    for (ConceptReferenceDesignationComponent d : c.getDesignation()) {
635      String lang = d.getLanguage();
636      if (!Utilities.noString(lang) && !langs.contains(lang)) {
637        langs.add(lang);
638      }
639    }
640    for (ValueSetExpansionContainsComponent cc : c.getContains()) {
641      scanForLangs(cc, langs);
642    }    
643  }
644  
645  private void addExpansionRowToTable(XhtmlNode t, ValueSetExpansionContainsComponent c, int i, boolean doLevel, boolean doSystem, boolean doDefinition, List<UsedConceptMap> maps, CodeSystem allCS, List<String> langs, boolean doLangs) {
646    XhtmlNode tr = t.tr();
647    XhtmlNode td = tr.td();
648
649    String tgt = makeAnchor(c.getSystem(), c.getCode());
650    td.an(tgt);
651
652    if (doLevel) {
653      td.addText(Integer.toString(i));
654      td = tr.td();
655    }
656    String s = Utilities.padLeft("", '\u00A0', i*2);
657    td.attribute("style", "white-space:nowrap").addText(s);
658    addCodeToTable(c.getAbstract(), c.getSystem(), c.getCode(), c.getDisplay(), td);
659    if (doSystem) {
660      td = tr.td();
661      td.addText(c.getSystem());
662    }
663    td = tr.td();
664    if (c.hasDisplayElement())
665      td.addText(c.getDisplay());
666
667    if (doDefinition) {
668      CodeSystem cs = allCS;
669      if (cs == null)
670        cs = getContext().getWorker().fetchCodeSystem(c.getSystem());
671      td = tr.td();
672      if (cs != null)
673        td.addText(CodeSystemUtilities.getCodeDefinition(cs, c.getCode()));
674    }
675    for (UsedConceptMap m : maps) {
676      td = tr.td();
677      List<TargetElementComponentWrapper> mappings = findMappingsForCode(c.getCode(), m.getMap());
678      boolean first = true;
679      for (TargetElementComponentWrapper mapping : mappings) {
680        if (!first)
681            td.br();
682        first = false;
683        XhtmlNode span = td.span(null, mapping.comp.getEquivalence().toString());
684        span.addText(getCharForRelationship(mapping.comp));
685        addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); 
686        if (!Utilities.noString(mapping.comp.getComment()))
687          td.i().tx("("+mapping.comp.getComment()+")");
688      }
689    }
690    if (doLangs) {
691      addLangaugesToRow(c, langs, tr);
692    }
693    for (ValueSetExpansionContainsComponent cc : c.getContains()) {
694      addExpansionRowToTable(t, cc, i+1, doLevel, doSystem, doDefinition, maps, allCS, langs, doLangs);
695    }
696  }
697
698
699
700
701
702  private boolean checkSystemMatches(String system, ValueSetExpansionContainsComponent cc) {
703    if (!system.equals(cc.getSystem()))
704      return false;
705    for (ValueSetExpansionContainsComponent cc1 : cc.getContains()) {
706      if (!checkSystemMatches(system, cc1))
707        return false;
708    }
709     return true;
710  }
711
712  private void addCodeToTable(boolean isAbstract, String system, String code, String display, XhtmlNode td) {
713    CodeSystem e = getContext().getWorker().fetchCodeSystem(system);
714    if (e == null || e.getContent() != org.hl7.fhir.r4b.model.CodeSystem.CodeSystemContentMode.COMPLETE) {
715      if (isAbstract)
716        td.i().setAttribute("title", ABSTRACT_CODE_HINT).addText(code);
717      else if ("http://snomed.info/sct".equals(system)) {
718        td.ah(sctLink(code)).addText(code);
719      } else if ("http://loinc.org".equals(system)) {
720          td.ah("http://details.loinc.org/LOINC/"+code+".html").addText(code);
721      } else        
722        td.addText(code);
723    } else {
724      String href = context.fixReference(getCsRef(e));
725      if (href.contains("#"))
726        href = href + "-"+Utilities.nmtokenize(code);
727      else
728        href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(code);
729      if (isAbstract)
730        td.ah(href).setAttribute("title", ABSTRACT_CODE_HINT).i().addText(code);
731      else
732        td.ah(href).addText(code);
733    }
734  }
735
736
737  public String sctLink(String code) {
738//    if (snomedEdition != null)
739//      http://browser.ihtsdotools.org/?perspective=full&conceptId1=428041000124106&edition=us-edition&release=v20180301&server=https://prod-browser-exten.ihtsdotools.org/api/snomed&langRefset=900000000000509007
740    return "http://snomed.info/id/"+code;
741  }
742
743  private void addRefToCode(XhtmlNode td, String target, String vslink, String code) {
744    CodeSystem cs = getContext().getWorker().fetchCodeSystem(target);
745    String cslink = getCsRef(cs);
746    XhtmlNode a = null;
747    if (cslink != null) 
748      a = td.ah(getContext().getSpecificationLink()+cslink+"#"+cs.getId()+"-"+code);
749    else
750      a = td.ah(getContext().getSpecificationLink()+vslink+"#"+code);
751    a.addText(code);
752  }
753
754  private boolean generateComposition(XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRException, IOException {
755    boolean hasExtensions = false;
756    List<String> langs = new ArrayList<String>();
757    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
758      scanForLangs(inc, langs);
759    }
760    for (ConceptSetComponent inc : vs.getCompose().getExclude()) {
761      scanForLangs(inc, langs);
762    }
763    boolean doLangs = langs.size() < MAX_LANGS_IN_LINE;
764    
765    if (header) {
766      XhtmlNode h = x.h2();
767      h.addText(vs.present());
768      addMarkdown(x, vs.getDescription());
769      if (vs.hasCopyrightElement())
770        generateCopyright(x, vs);
771    }
772    if (vs.getCompose().getInclude().size() == 1 && vs.getCompose().getExclude().size() == 0) {
773      hasExtensions = genInclude(x.ul(), vs.getCompose().getInclude().get(0), "Include", langs, doLangs, maps) || hasExtensions;
774    } else {
775      XhtmlNode p = x.para();
776      p.tx("This value set includes codes based on the following rules:");
777      XhtmlNode ul = x.ul();
778      for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
779        hasExtensions = genInclude(ul, inc, "Include", langs, doLangs, maps) || hasExtensions;
780      }
781      if (vs.getCompose().hasExclude()) {
782        p = x.para();
783        p.tx("This value set excludes codes based on the following rules:");
784        ul = x.ul();
785        for (ConceptSetComponent exc : vs.getCompose().getExclude()) {
786          hasExtensions = genInclude(ul, exc, "Exclude", langs, doLangs, maps) || hasExtensions;
787        }
788      }
789    }
790    
791    // now, build observed languages
792
793    if (!doLangs && langs.size() > 0) {
794      Collections.sort(langs);
795      x.para().b().tx("Additional Language Displays");
796      XhtmlNode t = x.table( "codes");
797      XhtmlNode tr = t.tr();
798      tr.td().b().tx("Code");
799      for (String lang : langs)
800        tr.td().b().addText(describeLang(lang));
801      for (ConceptSetComponent c : vs.getCompose().getInclude()) {
802        for (ConceptReferenceComponent cc : c.getConcept()) {
803          addLanguageRow(cc, t, langs);
804        }
805      }
806    }
807
808    return hasExtensions;
809  }
810
811  private void scanForLangs(ConceptSetComponent inc, List<String> langs) {
812    for (ConceptReferenceComponent cc : inc.getConcept()) {
813      for (Extension ext : cc.getExtension()) {
814        if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
815          String lang = ToolingExtensions.readStringExtension(ext,  "lang");
816          if (!Utilities.noString(lang) && !langs.contains(lang)) {
817            langs.add(lang);
818          }
819        }
820      }
821      for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
822        String lang = d.getLanguage();
823        if (!Utilities.noString(lang) && !langs.contains(lang)) {
824          langs.add(lang);
825        }
826      }
827    }
828  }
829
830  private boolean genInclude(XhtmlNode ul, ConceptSetComponent inc, String type, List<String> langs, boolean doLangs, List<UsedConceptMap> maps) throws FHIRException, IOException {
831    boolean hasExtensions = false;
832    XhtmlNode li;
833    li = ul.li();
834    CodeSystem e = getContext().getWorker().fetchCodeSystem(inc.getSystem());
835
836    if (inc.hasSystem()) {
837      if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) {
838        li.addText(type+" all codes defined in ");
839        addCsRef(inc, li, e);
840      } else {
841        if (inc.getConcept().size() > 0) {
842          li.addText(type+" these codes as defined in ");
843          addCsRef(inc, li, e);
844          if (inc.hasVersion()) {
845            li.addText(" version ");
846            li.code(inc.getVersion()); 
847          }
848
849          // for performance reasons, we do all the fetching in one batch
850          Map<String, ConceptDefinitionComponent> definitions = getConceptsForCodes(e, inc);
851          
852          XhtmlNode t = li.table("none");
853          boolean hasComments = false;
854          boolean hasDefinition = false;
855          for (ConceptReferenceComponent c : inc.getConcept()) {
856            hasComments = hasComments || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT);
857            ConceptDefinitionComponent cc = definitions.get(c.getCode()); 
858            hasDefinition = hasDefinition || ((cc != null && cc.hasDefinition()) || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION));
859          }
860          if (hasComments || hasDefinition)
861            hasExtensions = true;
862          addMapHeaders(addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false, false, null, langs, doLangs), maps);
863          for (ConceptReferenceComponent c : inc.getConcept()) {
864            XhtmlNode tr = t.tr();
865            XhtmlNode td = tr.td();
866            ConceptDefinitionComponent cc = definitions.get(c.getCode()); 
867            addCodeToTable(false, inc.getSystem(), c.getCode(), c.hasDisplay()? c.getDisplay() : cc != null ? cc.getDisplay() : "", td);
868
869            td = tr.td();
870            if (!Utilities.noString(c.getDisplay()))
871              td.addText(c.getDisplay());
872            else if (cc != null && !Utilities.noString(cc.getDisplay()))
873              td.addText(cc.getDisplay());
874
875            if (hasDefinition) {
876              td = tr.td();
877              if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)) {
878                smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION));
879              } else if (cc != null && !Utilities.noString(cc.getDefinition())) {
880                smartAddText(td, cc.getDefinition());
881              }
882            }
883            if (hasComments) {
884              td = tr.td();
885              if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT)) {
886                smartAddText(td, "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT));
887              }
888            }
889            if (doLangs) {
890              addLangaugesToRow(c, langs, tr);
891            }
892          }
893        }
894        if (inc.getFilter().size() > 0) {
895          li.addText(type+" codes from ");
896          addCsRef(inc, li, e);
897          li.tx(" where ");
898          for (int i = 0; i < inc.getFilter().size(); i++) {
899            ConceptSetFilterComponent f = inc.getFilter().get(i);
900            if (i > 0) {
901              if (i == inc.getFilter().size()-1) {
902                li.tx(" and ");
903              } else {
904                li.tx(", ");
905              }
906            }
907            if (f.getOp() == FilterOperator.EXISTS) {
908              if (f.getValue().equals("true")) {
909                li.tx(f.getProperty()+" exists");
910              } else {
911                li.tx(f.getProperty()+" doesn't exist");
912              }
913            } else {
914              li.tx(f.getProperty()+" "+describe(f.getOp())+" ");
915              if (e != null && codeExistsInValueSet(e, f.getValue())) {
916                String href = getContext().fixReference(getCsRef(e));
917                if (href.contains("#"))
918                  href = href + "-"+Utilities.nmtokenize(f.getValue());
919                else
920                  href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(f.getValue());
921                li.ah(href).addText(f.getValue());
922              } else if ("concept".equals(f.getProperty()) && inc.hasSystem()) {
923                li.addText(f.getValue());
924                ValidationResult vr = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions(), inc.getSystem(), inc.getVersion(), f.getValue(), null);
925                if (vr.isOk()) {
926                  li.tx(" ("+vr.getDisplay()+")");
927                }
928              }
929              else
930                li.addText(f.getValue());
931              String disp = ToolingExtensions.getDisplayHint(f);
932              if (disp != null)
933                li.tx(" ("+disp+")");
934            }
935          }
936        }
937      }
938      if (inc.hasValueSet()) {
939        li.tx(", where the codes are contained in ");
940        boolean first = true;
941        for (UriType vs : inc.getValueSet()) {
942          if (first)
943            first = false;
944          else
945            li.tx(", ");
946          AddVsRef(vs.asStringValue(), li);
947        }
948      }
949    } else {
950      li.tx("Import all the codes that are contained in ");
951      if (inc.getValueSet().size() < 4) {
952        boolean first = true;
953        for (UriType vs : inc.getValueSet()) {
954          if (first)
955            first = false;
956          else
957            li.tx(", ");
958          AddVsRef(vs.asStringValue(), li);
959        }
960      } else {
961        XhtmlNode xul = li.ul();
962        for (UriType vs : inc.getValueSet()) {
963          AddVsRef(vs.asStringValue(), xul.li());
964        }
965        
966      }
967    }
968    return hasExtensions;
969  }
970
971  public void addLangaugesToRow(ConceptReferenceComponent c, List<String> langs, XhtmlNode tr) {
972    for (String lang : langs) {
973      String d = null;
974      for (Extension ext : c.getExtension()) {
975        if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
976          String l = ToolingExtensions.readStringExtension(ext, "lang");
977          if (lang.equals(l)) {
978            d = ToolingExtensions.readStringExtension(ext, "content");
979          }
980        }
981      }
982      if (d == null) {
983        for (ConceptReferenceDesignationComponent dd : c.getDesignation()) {
984          String l = dd.getLanguage();
985          if (lang.equals(l)) {
986            d = dd.getValue();
987          }
988        }
989      }
990      tr.td().addText(d == null ? "" : d);
991    }
992  }
993
994
995  private Map<String, ConceptDefinitionComponent> getConceptsForCodes(CodeSystem e, ConceptSetComponent inc) {
996    if (e == null) {
997      e = getContext().getWorker().fetchCodeSystem(inc.getSystem());
998    }
999    
1000    ValueSetExpansionComponent vse = null;
1001    if (!context.isNoSlowLookup() && !getContext().getWorker().hasCache()) {
1002      try {
1003        ValueSetExpansionOutcome vso = getContext().getWorker().expandVS(inc, false);   
1004        ValueSet valueset = vso.getValueset();
1005        if (valueset == null)
1006          throw new TerminologyServiceException("Error Expanding ValueSet: "+vso.getError());
1007        vse = valueset.getExpansion();        
1008
1009      } catch (TerminologyServiceException e1) {
1010        return null;
1011      }
1012    }
1013    
1014    Map<String, ConceptDefinitionComponent> results = new HashMap<>();
1015    List<CodingValidationRequest> serverList = new ArrayList<>();
1016    
1017    // 1st pass, anything we can resolve internally
1018    for (ConceptReferenceComponent cc : inc.getConcept()) {
1019      String code = cc.getCode();
1020      ConceptDefinitionComponent v = null;
1021      if (e != null) {
1022        v = getConceptForCode(e.getConcept(), code);
1023      }
1024      if (v == null && vse != null) {
1025        v = getConceptForCodeFromExpansion(vse.getContains(), code);
1026      }
1027      if (v != null) {
1028        results.put(code, v);
1029      } else {
1030        serverList.add(new CodingValidationRequest(new Coding(inc.getSystem(), code, null)));
1031      }
1032    }
1033    if (!context.isNoSlowLookup() && !serverList.isEmpty()) {
1034      getContext().getWorker().validateCodeBatch(getContext().getTerminologyServiceOptions(), serverList, null);
1035      for (CodingValidationRequest vr : serverList) {
1036        ConceptDefinitionComponent v = vr.getResult().asConceptDefinition();
1037        if (v != null) {
1038          results.put(vr.getCoding().getCode(), v);
1039        }
1040      }
1041    }
1042    return results;
1043  }
1044  
1045  private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> list, String code) {
1046    for (ConceptDefinitionComponent c : list) {
1047    if (code.equals(c.getCode()))
1048      return c;
1049      ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code);
1050      if (v != null)
1051        return v;
1052    }
1053    return null;
1054  }
1055
1056  private ConceptDefinitionComponent getConceptForCodeFromExpansion(List<ValueSetExpansionContainsComponent> list, String code) {
1057    for (ValueSetExpansionContainsComponent c : list) {
1058      if (code.equals(c.getCode())) {
1059        ConceptDefinitionComponent res = new ConceptDefinitionComponent();
1060        res.setCode(c.getCode());
1061        res.setDisplay(c.getDisplay());
1062        return res;
1063      }
1064      ConceptDefinitionComponent v = getConceptForCodeFromExpansion(c.getContains(), code);
1065      if (v != null)
1066        return v;
1067    }
1068    return null;
1069  }
1070
1071 
1072  private boolean codeExistsInValueSet(CodeSystem cs, String code) {
1073    for (ConceptDefinitionComponent c : cs.getConcept()) {
1074      if (inConcept(code, c))
1075        return true;
1076    }
1077    return false;
1078  }
1079  
1080
1081  private void addLanguageRow(ConceptReferenceComponent c, XhtmlNode t, List<String> langs) {
1082    XhtmlNode tr = t.tr();
1083    tr.td().addText(c.getCode());
1084    for (String lang : langs) {
1085      String d = null;
1086      for (ConceptReferenceDesignationComponent cd : c.getDesignation()) {
1087        String l = cd.getLanguage();
1088        if (lang.equals(l))
1089          d = cd.getValue();
1090      }
1091      tr.td().addText(d == null ? "" : d);
1092    }
1093  }
1094
1095
1096  private String describe(FilterOperator op) {
1097    if (op == null)
1098      return " null ";
1099    switch (op) {
1100    case EQUAL: return " = ";
1101    case ISA: return " is-a ";
1102    case ISNOTA: return " is-not-a ";
1103    case REGEX: return " matches (by regex) ";
1104    case NULL: return " ?ngen-13? ";
1105    case IN: return " in ";
1106    case NOTIN: return " not in ";
1107    case DESCENDENTOF: return " descends from ";
1108    case EXISTS: return " exists ";
1109    case GENERALIZES: return " generalizes ";
1110    }
1111    return null;
1112  }
1113
1114
1115
1116 
1117
1118  private boolean inConcept(String code, ConceptDefinitionComponent c) {
1119    if (c.hasCodeElement() && c.getCode().equals(code))
1120      return true;
1121    for (ConceptDefinitionComponent g : c.getConcept()) {
1122      if (inConcept(code, g))
1123        return true;
1124    }
1125    return false;
1126  }
1127
1128
1129}