001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.text.ParseException;
005import java.text.SimpleDateFormat;
006import java.util.ArrayList;
007import java.util.Collections;
008import java.util.Date;
009import java.util.HashMap;
010import java.util.List;
011import java.util.Map;
012
013import org.hl7.fhir.exceptions.DefinitionException;
014import org.hl7.fhir.exceptions.FHIRException;
015import org.hl7.fhir.exceptions.FHIRFormatError;
016import org.hl7.fhir.exceptions.TerminologyServiceException;
017import org.hl7.fhir.r5.context.IWorkerContext.CodingValidationRequest;
018import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
019import org.hl7.fhir.r5.model.BooleanType;
020import org.hl7.fhir.r5.model.CanonicalResource;
021import org.hl7.fhir.r5.model.CodeSystem;
022import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
023import org.hl7.fhir.r5.model.Coding;
024import org.hl7.fhir.r5.model.ConceptMap;
025import org.hl7.fhir.r5.model.DataType;
026import org.hl7.fhir.r5.model.Enumerations.FilterOperator;
027import org.hl7.fhir.r5.model.Extension;
028import org.hl7.fhir.r5.model.ExtensionHelper;
029import org.hl7.fhir.r5.model.PrimitiveType;
030import org.hl7.fhir.r5.model.Resource;
031import org.hl7.fhir.r5.model.UriType;
032import org.hl7.fhir.r5.model.ValueSet;
033import org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent;
034import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
035import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
036import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent;
037import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent;
038import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent;
039import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
040import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent;
041import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionPropertyComponent;
042import org.hl7.fhir.r5.renderers.utils.RenderingContext;
043import org.hl7.fhir.r5.renderers.utils.RenderingContext.GenerationRules;
044import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext;
045import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
046import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
047import org.hl7.fhir.r5.terminologies.ValueSetUtilities;
048import org.hl7.fhir.r5.utils.ToolingExtensions;
049import org.hl7.fhir.utilities.LoincLinker;
050import org.hl7.fhir.utilities.Utilities;
051import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
052import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
053import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
054import org.hl7.fhir.utilities.xhtml.XhtmlNode;
055
056import com.google.common.collect.HashMultimap;
057import com.google.common.collect.Multimap;
058
059public class ValueSetRenderer extends TerminologyRenderer {
060
061  public ValueSetRenderer(RenderingContext context) {
062    super(context);
063  }
064
065  public ValueSetRenderer(RenderingContext context, ResourceContext rcontext) {
066    super(context, rcontext);
067  }
068
069  private static final String ABSTRACT_CODE_HINT = "This code is not selectable ('Abstract')";
070
071  private static final int MAX_DESIGNATIONS_IN_LINE = 5;
072
073  private List<ConceptMapRenderInstructions> renderingMaps = new ArrayList<ConceptMapRenderInstructions>();
074
075  public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException {
076    return render(x, (ValueSet) dr, false);
077  }
078  
079  public boolean render(XhtmlNode x, ValueSet vs, boolean header) throws FHIRFormatError, DefinitionException, IOException {
080    List<UsedConceptMap> maps = findReleventMaps(vs);
081    
082    boolean hasExtensions;
083    if (vs.hasExpansion()) {
084      // for now, we just accept an expansion if there is one
085      hasExtensions = generateExpansion(x, vs, header, maps);
086    } else {
087      hasExtensions = generateComposition(x, vs, header, maps);
088    }
089    return hasExtensions;
090  }
091
092  public void describe(XhtmlNode x, ValueSet vs) {
093    x.tx(display(vs));
094  }
095
096  public String display(ValueSet vs) {
097    return vs.present();
098  }
099
100  
101  private List<UsedConceptMap> findReleventMaps(ValueSet vs) throws FHIRException {
102    List<UsedConceptMap> res = new ArrayList<UsedConceptMap>();
103    for (ConceptMap cm : getContext().getWorker().fetchResourcesByType(ConceptMap.class)) {
104      if (isSource(vs, cm.getSourceScope())) {
105        ConceptMapRenderInstructions re = findByTarget(cm.getTargetScope());
106        if (re == null) {
107          re = new ConceptMapRenderInstructions(cm.present(), cm.getUrl(), false);
108        }
109        if (re != null) {
110          ValueSet vst = cm.hasTargetScope() ? getContext().getWorker().fetchResource(ValueSet.class, cm.hasTargetScopeCanonicalType() ? cm.getTargetScopeCanonicalType().getValue() : cm.getTargetScopeUriType().asStringValue(), cm) : null;
111          res.add(new UsedConceptMap(re, vst == null ? cm.getUserString("path") : vst.getUserString("path"), cm));
112        }
113      }
114    }
115    return res;
116
117//    @Override
118//    public List<ConceptMap> findMapsForSource(String url) throws FHIRException {
119//      synchronized (lock) {
120//        List<ConceptMap> res = new ArrayList<ConceptMap>();
121//        for (ConceptMap map : maps.getList()) {
122//          if (((Reference) map.getSourceScope()).getReference().equals(url)) { 
123//            res.add(map);
124//          } 
125//        } 
126//        return res;
127//      }
128//    }
129
130//    Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>();
131//  for (ConceptMap a : context.getWorker().findMapsForSource(vs.getUrl())) {
132//    String url = "";
133//    ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference());
134//    if (vsr != null)
135//      url = (String) vsr.getUserData("filename");
136//    mymaps.put(a, url);
137//  }
138//    Map<ConceptMap, String> mymaps = new HashMap<ConceptMap, String>();
139//  for (ConceptMap a : context.getWorker().findMapsForSource(cs.getValueSet())) {
140//    String url = "";
141//    ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) a.getTarget()).getReference());
142//    if (vsr != null)
143//      url = (String) vsr.getUserData("filename");
144//    mymaps.put(a, url);
145//  }
146    // also, look in the contained resources for a concept map
147//    for (Resource r : cs.getContained()) {
148//      if (r instanceof ConceptMap) {
149//        ConceptMap cm = (ConceptMap) r;
150//        if (((Reference) cm.getSource()).getReference().equals(cs.getValueSet())) {
151//          String url = "";
152//          ValueSet vsr = context.getWorker().fetchResource(ValueSet.class, ((Reference) cm.getTarget()).getReference());
153//          if (vsr != null)
154//              url = (String) vsr.getUserData("filename");
155//        mymaps.put(cm, url);
156//        }
157//      }
158//    }
159  }  
160  
161  private boolean isSource(ValueSet vs, DataType source) {
162    return vs.hasUrl() && source != null && vs.getUrl().equals(source.primitiveValue());
163  }  
164  
165  private boolean generateExpansion(XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRFormatError, DefinitionException, IOException {
166    boolean hasExtensions = false;
167    List<String> langs = new ArrayList<String>();
168    Map<String, String> designations = new HashMap<>(); //  map of url = description, where url is the designation code. Designations that are for languages won't make it into this list
169    Map<String, String> properties = new HashMap<>(); //  map of url = description, where url is the designation code. Designations that are for languages won't make it into this list
170
171    if (header) {
172      XhtmlNode h = x.addTag(getHeader());
173      h.tx("Value Set Contents");
174      if (IsNotFixedExpansion(vs))
175        addMarkdown(x, vs.getDescription());
176      if (vs.hasCopyright())
177        generateCopyright(x, vs);
178    }
179    if (ToolingExtensions.hasExtension(vs.getExpansion(), ToolingExtensions.EXT_EXP_TOOCOSTLY)) {
180      List<Extension> exl = vs.getExpansion().getExtensionsByUrl(ToolingExtensions.EXT_EXP_TOOCOSTLY);
181      boolean other = false;
182      for (Extension ex : exl) {
183        if (ex.getValue() instanceof BooleanType) {
184          x.para().style("border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? getContext().getTooCostlyNoteEmpty() : getContext().getTooCostlyNoteNotEmpty());
185        } else if (!other) {
186          x.para().style("border: maroon 1px solid; background-color: #FFCCCC; font-weight: bold; padding: 8px").addText(vs.getExpansion().getContains().isEmpty() ? getContext().getTooCostlyNoteEmptyDependent() : getContext().getTooCostlyNoteNotEmptyDependent());
187          other = true;
188        }
189      }
190    } else {
191      Integer count = countMembership(vs);
192      if (count == null)
193        x.para().tx("This value set does not contain a fixed number of concepts");
194      else
195        x.para().tx("This value set contains "+count.toString()+" concepts");
196    }
197    
198    generateContentModeNotices(x, vs.getExpansion(), vs);
199    generateVersionNotice(x, vs.getExpansion(), vs);
200
201    CodeSystem allCS = null;
202    boolean doLevel = false;
203    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
204      if (cc.hasContains()) {
205        doLevel = true;
206        break;
207      }
208    }
209    
210    boolean doDefinition = checkDoDefinition(vs.getExpansion().getContains());
211    XhtmlNode t = x.table( "codes");
212    XhtmlNode tr = t.tr();
213    if (doLevel)
214      tr.td().b().tx("Level");
215    tr.td().attribute("style", "white-space:nowrap").b().tx("Code");
216    tr.td().b().tx("System");
217    XhtmlNode tdDisp = tr.td();
218    tdDisp.b().tx("Display");
219    boolean doDesignations = false;
220    for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) {
221      scanForDesignations(c, langs, designations);
222    }
223    scanForProperties(vs.getExpansion(), langs, properties);
224    if (doDefinition) {
225      tr.td().b().tx("Definition");
226      doDesignations = false;
227      for (String n : Utilities.sorted(properties.keySet())) {
228        tr.td().b().ah(properties.get(n)).addText(n);        
229      }
230    } else {
231      for (String n : Utilities.sorted(properties.keySet())) {
232        tr.td().b().ah(properties.get(n)).addText(n);        
233      }
234      // if we're not doing definitions and we don't have too many languages, we'll do them in line
235      doDesignations = langs.size() + properties.size() + designations.size() < MAX_DESIGNATIONS_IN_LINE;
236
237      if (doDesignations) {
238        if (vs.hasLanguage()) {
239          tdDisp.tx(" - "+describeLang(vs.getLanguage()));
240        }
241        for (String url : designations.keySet()) {
242          tr.td().b().addText(designations.get(url));
243        }
244        for (String lang : langs) {
245          tr.td().b().addText(describeLang(lang));
246        }
247      }
248    }
249
250    
251    addMapHeaders(tr, maps);
252    for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) {
253      addExpansionRowToTable(t, vs, c, 1, doLevel, true, doDefinition, maps, allCS, langs, designations, doDesignations, properties);
254    }
255
256    // now, build observed languages
257
258    if (!doDesignations && langs.size() + designations.size() > 0) {
259      Collections.sort(langs);
260      if (designations.size() == 0) {
261        x.para().b().tx("Additional Language Displays");
262      } else if (langs.size() == 0) {
263        x.para().b().tx("Additional Designations");
264      } else {
265        x.para().b().tx("Additional Designations and Language Displays");
266      }
267      t = x.table("codes");
268      tr = t.tr();
269      tr.td().b().tx("Code");
270      for (String url : designations.keySet()) {
271        tr.td().b().addText(designations.get(url));
272      }
273      for (String lang : langs) {
274        tr.td().b().addText(describeLang(lang));
275      }
276      for (ValueSetExpansionContainsComponent c : vs.getExpansion().getContains()) {
277        addDesignationRow(c, t, langs, designations);
278      }
279    }
280
281    return hasExtensions;
282  }
283
284
285  private void scanForProperties(ValueSetExpansionComponent exp, List<String> langs, Map<String, String> properties) {
286    properties.clear();
287    for (ValueSetExpansionPropertyComponent pp : exp.getProperty()) {
288      if (pp.hasCode() && pp.hasUri() && anyActualproperties(exp.getContains(), pp.getCode())) {
289        properties.put(pp.getCode(), pp.getUri());
290      }
291    }
292  }
293
294  private boolean anyActualproperties(List<ValueSetExpansionContainsComponent> contains, String pp) {
295    for (ValueSetExpansionContainsComponent c : contains) {
296      for (ConceptPropertyComponent cp : c.getProperty()) {
297        if (pp.equals(cp.getCode())) {
298          return true;
299        }
300      }
301      if (anyActualproperties(c.getContains(), pp)) {
302        return true;
303      }
304    }
305    return false;
306  }
307
308  private void generateContentModeNotices(XhtmlNode x, ValueSetExpansionComponent expansion, Resource vs) {
309    generateContentModeNotice(x, expansion, "example", "Expansion based on example code system", vs); 
310    generateContentModeNotice(x, expansion, "fragment", "Expansion based on code system fragment", vs); 
311  }
312  
313  private void generateContentModeNotice(XhtmlNode x, ValueSetExpansionComponent expansion, String mode, String text, Resource vs) {
314    Multimap<String, String> versions = HashMultimap.create();
315    for (ValueSetExpansionParameterComponent p : expansion.getParameter()) {
316      if (p.getName().equals(mode)) {
317        String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|");
318        if (parts.length == 2)
319          versions.put(parts[0], parts[1]);
320      }
321    }
322    if (versions.size() > 0) {
323      XhtmlNode div = null;
324      XhtmlNode ul = null;
325      boolean first = true;
326      for (String s : versions.keySet()) {
327        if (versions.size() == 1 && versions.get(s).size() == 1) {
328          for (String v : versions.get(s)) { // though there'll only be one
329            XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #ffcccc; padding: 8px; margin-bottom: 8px");
330            p.tx(text+" ");
331            expRef(p, s, v, vs);
332          }
333        } else {
334          for (String v : versions.get(s)) {
335            if (first) {
336              div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px");
337              div.para().tx(text+"s: ");
338              ul = div.ul();
339              first = false;
340            }
341            expRef(ul.li(), s, v, vs);
342          }
343        }
344      }
345    }
346  }
347
348  private boolean checkDoSystem(ValueSet vs, ValueSet src) {
349    if (src != null)
350      vs = src;
351    return vs.hasCompose();
352  }
353
354  private boolean IsNotFixedExpansion(ValueSet vs) {
355    if (vs.hasCompose())
356      return false;
357
358
359    // it's not fixed if it has any includes that are not version fixed
360    for (ConceptSetComponent cc : vs.getCompose().getInclude()) {
361      if (cc.hasValueSet())
362        return true;
363      if (!cc.hasVersion())
364        return true;
365    }
366    return false;
367  }
368
369
370 
371  
372  private ConceptMapRenderInstructions findByTarget(DataType source) {
373    if (source == null) {
374      return null;
375    }
376    String src = source.primitiveValue();
377    if (src == null) {
378      return null;
379    }
380    for (ConceptMapRenderInstructions t : renderingMaps) {
381      if (src.equals(t.getUrl()))
382        return t;
383    }
384    return null;    
385  }
386
387
388  private Integer countMembership(ValueSet vs) {
389    int count = 0;
390    if (vs.hasExpansion())
391      count = count + conceptCount(vs.getExpansion().getContains());
392    else {
393      if (vs.hasCompose()) {
394        if (vs.getCompose().hasExclude()) {
395          try {
396            ValueSetExpansionOutcome vse = getContext().getWorker().expandVS(vs, true, false);
397            count = 0;
398            count += conceptCount(vse.getValueset().getExpansion().getContains());
399            return count;
400          } catch (Exception e) {
401            return null;
402          }
403        }
404        for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
405          if (inc.hasFilter())
406            return null;
407          if (!inc.hasConcept())
408            return null;
409          count = count + inc.getConcept().size();
410        }
411      }
412    }
413    return count;
414  }
415
416  private int conceptCount(List<ValueSetExpansionContainsComponent> list) {
417    int count = 0;
418    for (ValueSetExpansionContainsComponent c : list) {
419      if (!c.getAbstract())
420        count++;
421      count = count + conceptCount(c.getContains());
422    }
423    return count;
424  }
425
426  private void addCSRef(XhtmlNode x, String url) {
427    CodeSystem cs = getContext().getWorker().fetchCodeSystem(url);
428    if (cs == null) {
429      x.code(url);
430    } else if (cs.hasUserData("path")) {
431      x.ah(cs.getUserString("path")).tx(cs.present());
432    } else {
433      x.code(url);
434      x.tx(" ("+cs.present()+")");
435    }
436  }
437
438  @SuppressWarnings("rawtypes")
439  private void generateVersionNotice(XhtmlNode x, ValueSetExpansionComponent expansion, Resource vs) {
440    Multimap<String, String> versions = HashMultimap.create();
441    for (ValueSetExpansionParameterComponent p : expansion.getParameter()) {
442      if (p.getName().equals("version")) {
443        String[] parts = ((PrimitiveType) p.getValue()).asStringValue().split("\\|");
444        if (parts.length == 2)
445          versions.put(parts[0], parts[1]);
446      }
447    }
448    if (versions.size() > 0) {
449      XhtmlNode div = null;
450      XhtmlNode ul = null;
451      boolean first = true;
452      for (String s : versions.keySet()) {
453        if (versions.size() == 1 && versions.get(s).size() == 1) {
454          for (String v : versions.get(s)) { // though there'll only be one
455            XhtmlNode p = x.para().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px");
456            p.tx("Expansion based on ");
457            expRef(p, s, v, vs);
458          }
459        } else {
460          for (String v : versions.get(s)) {
461            if (first) {
462              div = x.div().style("border: black 1px dotted; background-color: #EEEEEE; padding: 8px; margin-bottom: 8px");
463              div.para().tx("Expansion based on: ");
464              ul = div.ul();
465              first = false;
466            }
467            expRef(ul.li(), s, v, vs);
468          }
469        }
470      }
471    }
472  }
473
474  private void expRef(XhtmlNode x, String u, String v, Resource source) {
475    // TODO Auto-generated method stub
476    if (u.equals("http://snomed.info/sct")) {
477      String[] parts = v.split("\\/");
478      if (parts.length >= 5) {
479        String m = describeModule(parts[4]);
480        if (parts.length == 7) {
481          x.tx("SNOMED CT "+m+" edition "+formatSCTDate(parts[6]));
482        } else {
483          x.tx("SNOMED CT "+m+" edition");
484        }
485      } else {
486        x.tx(describeSystem(u)+" version "+v);
487      }
488    } else if (u.equals("http://loinc.org")) {
489      String vd = describeLoincVer(v);
490      if (vd != null) {
491        x.tx("Loinc v"+v+" ("+vd+")");
492      } else {
493        x.tx("Loinc v"+v);        
494      }
495    } else {
496      CanonicalResource cr = (CanonicalResource) getContext().getWorker().fetchResource(Resource.class, u+"|"+v, source);
497      if (cr != null) {
498        if (cr.hasUserData("path")) {
499          x.ah(cr.getUserString("path")).tx(cr.present()+" v"+v+" ("+cr.fhirType()+")");          
500        } else {
501          x.tx(describeSystem(u)+" v"+v+" ("+cr.fhirType()+")");
502        }
503      } else {
504        x.tx(describeSystem(u)+" version "+v);
505      }
506    }
507  }
508
509  private String describeLoincVer(String v) {
510    if ("2.67".equals(v))  return "Dec 2019";
511    if ("2.66".equals(v))  return "Jun 2019";
512    if ("2.65".equals(v))  return "Dec 2018";
513    if ("2.64".equals(v))  return "Jun 2018";
514    if ("2.63".equals(v))  return "Dec 2017";
515    if ("2.61".equals(v))  return "Jun 2017";
516    if ("2.59".equals(v))  return "Feb 2017";
517    if ("2.58".equals(v))  return "Dec 2016";
518    if ("2.56".equals(v))  return "Jun 2016";
519    if ("2.54".equals(v))  return "Dec 2015";
520    if ("2.52".equals(v))  return "Jun 2015";
521    if ("2.50".equals(v))  return "Dec 2014";
522    if ("2.48".equals(v))  return "Jun 2014";
523    if ("2.46".equals(v))  return "Dec 2013";
524    if ("2.44".equals(v))  return "Jun 2013";
525    if ("2.42".equals(v))  return "Dec 2012";
526    if ("2.40".equals(v))  return "Jun 2012";
527    if ("2.38".equals(v))  return "Dec 2011";
528    if ("2.36".equals(v))  return "Jun 2011";
529    if ("2.34".equals(v))  return "Dec 2010";
530    if ("2.32".equals(v))  return "Jun 2010";
531    if ("2.30".equals(v))  return "Feb 2010";
532    if ("2.29".equals(v))  return "Dec 2009";
533    if ("2.27".equals(v))  return "Jul 2009";
534    if ("2.26".equals(v))  return "Jan 2009";
535    if ("2.24".equals(v))  return "Jul 2008";
536    if ("2.22".equals(v))  return "Dec 2007";
537    if ("2.21".equals(v))  return "Jun 2007";
538    if ("2.19".equals(v))  return "Dec 2006";
539    if ("2.17".equals(v))  return "Jun 2006";
540    if ("2.16".equals(v))  return "Dec 2005";
541    if ("2.15".equals(v))  return "Jun 2005";
542    if ("2.14".equals(v))  return "Dec 2004";
543    if ("2.13".equals(v))  return "Aug 2004";
544    if ("2.12".equals(v))  return "Feb 2004";
545    if ("2.10".equals(v))  return "Oct 2003";
546    if ("2.09".equals(v))  return "May 2003";
547    if ("2.08 ".equals(v)) return "Sep 2002";
548    if ("2.07".equals(v))  return "Aug 2002";
549    if ("2.05".equals(v))  return "Feb 2002";
550    if ("2.04".equals(v))  return "Jan 2002";
551    if ("2.03".equals(v))  return "Jul 2001";
552    if ("2.02".equals(v))  return "May 2001";
553    if ("2.01".equals(v))  return "Jan 2001";
554    if ("2.00".equals(v))  return "Jan 2001";
555    if ("1.0n".equals(v))  return "Feb 2000";
556    if ("1.0ma".equals(v)) return "Aug 1999";
557    if ("1.0m".equals(v))  return "Jul 1999";
558    if ("1.0l".equals(v))  return "Jan 1998";
559    if ("1.0ja".equals(v)) return "Oct 1997";
560    return null;
561  }
562
563  private String formatSCTDate(String ds) {
564    SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
565    Date date;
566    try {
567      date = format.parse(ds);
568    } catch (ParseException e) {
569      return ds;
570    }
571    return new SimpleDateFormat("dd-MMM yyyy").format(date);
572  }
573
574  private String describeModule(String module) {
575    if ("900000000000207008".equals(module))
576      return "International";
577    if ("731000124108".equals(module))
578      return "United States";
579    if ("32506021000036107".equals(module))
580      return "Australian";
581    if ("449081005".equals(module))
582      return "Spanish";
583    if ("554471000005108".equals(module))
584      return "Danish";
585    if ("11000146104".equals(module))
586      return "Dutch";
587    if ("45991000052106".equals(module))
588      return "Swedish";
589    if ("999000041000000102".equals(module))
590      return "United Kingdon";
591    return module;
592  }
593
594  private boolean hasVersionParameter(ValueSetExpansionComponent expansion) {
595    for (ValueSetExpansionParameterComponent p : expansion.getParameter()) {
596      if (p.getName().equals("version"))
597        return true;
598    }
599    return false;
600  }
601
602  private void addDesignationRow(ValueSetExpansionContainsComponent c, XhtmlNode t, List<String> langs, Map<String, String> designations) {
603    XhtmlNode tr = t.tr();
604    tr.td().addText(c.getCode());
605    addDesignationsToRow(c, designations, tr);
606    addLangaugesToRow(c, langs, tr);
607    for (ValueSetExpansionContainsComponent cc : c.getContains()) {
608      addDesignationRow(cc, t, langs, designations);
609    }
610  }
611
612  public void addDesignationsToRow(ValueSetExpansionContainsComponent c, Map<String, String> designations, XhtmlNode tr) {
613    for (String url : designations.keySet()) {
614      String d = null;
615      if (d == null) {
616        for (ConceptReferenceDesignationComponent dd : c.getDesignation()) {
617          if (url.equals(getUrlForDesignation(dd))) {
618            d = dd.getValue();
619          }
620        }
621      }
622      tr.td().addText(d == null ? "" : d);
623    }
624  }
625
626  public void addLangaugesToRow(ValueSetExpansionContainsComponent c, List<String> langs, XhtmlNode tr) {
627    for (String lang : langs) {
628      String d = null;
629      for (Extension ext : c.getExtension()) {
630        if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
631          String l = ToolingExtensions.readStringExtension(ext, "lang");
632          if (lang.equals(l)) {
633            d = ToolingExtensions.readStringExtension(ext, "content");
634          }
635        }
636      }
637      if (d == null) {
638        for (ConceptReferenceDesignationComponent dd : c.getDesignation()) {
639          String l = dd.getLanguage();
640          if (lang.equals(l)) {
641            d = dd.getValue();
642          }
643        }
644      }
645      tr.td().addText(d == null ? "" : d);
646    }
647  }
648
649  
650  private boolean checkDoDefinition(List<ValueSetExpansionContainsComponent> contains) {
651    for (ValueSetExpansionContainsComponent c : contains) {
652      CodeSystem cs = getContext().getWorker().fetchCodeSystem(c.getSystem());
653      if (cs != null) {
654        ConceptDefinitionComponent cd = CodeSystemUtilities.getCode(cs, c.getCode());
655        if (cd != null && cd.hasDefinition()) {
656          return true;
657        }
658      }
659      if (checkDoDefinition(c.getContains()))
660        return true;
661    }
662    return false;
663  }
664
665
666  private boolean allFromOneSystem(ValueSet vs) {
667    if (vs.getExpansion().getContains().isEmpty())
668      return false;
669    String system = vs.getExpansion().getContains().get(0).getSystem();
670    for (ValueSetExpansionContainsComponent cc : vs.getExpansion().getContains()) {
671      if (!checkSystemMatches(system, cc))
672        return false;
673    }
674    return true;
675  }
676
677  private String getCsRef(String system) {
678    CodeSystem cs = getContext().getWorker().fetchCodeSystem(system);
679    return getCsRef(cs);
680  }
681
682  private  <T extends Resource> String getCsRef(T cs) {
683    if (cs == null) {
684      return "?cs-n?";
685    }
686    String ref = (String) cs.getUserData("filename");
687    if (ref == null)
688      ref = (String) cs.getUserData("path");
689    if (ref == null)
690      return "?ngen-14?.html";
691    if (!ref.contains(".html"))
692      ref = ref + ".html";
693    return ref.replace("\\", "/");
694  }
695
696  private void scanForDesignations(ValueSetExpansionContainsComponent c, List<String> langs, Map<String, String> designations) {
697    for (Extension ext : c.getExtension()) {
698      if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
699        String lang = ToolingExtensions.readStringExtension(ext,  "lang");
700        if (!Utilities.noString(lang) && !langs.contains(lang)) {
701          langs.add(lang);
702        }
703      }
704    }
705    for (ConceptReferenceDesignationComponent d : c.getDesignation()) {
706      String lang = d.getLanguage();
707      if (!Utilities.noString(lang) && !langs.contains(lang)) {
708        langs.add(lang);
709      } else {
710        // can we present this as a designation that we know?
711        String disp = getDisplayForDesignation(d);
712        String url = getUrlForDesignation(d);
713        if (disp == null) {
714          disp = getDisplayForUrl(url);
715        }
716        if (disp != null && !designations.containsKey(url) && url != null) {
717          designations.put(url, disp);
718        }
719      }
720    }
721    for (ValueSetExpansionContainsComponent cc : c.getContains()) {
722      scanForDesignations(cc, langs, designations);
723    }
724  }
725
726  private void scanForLangs(ValueSetExpansionContainsComponent c, List<String> langs) {
727    for (Extension ext : c.getExtension()) {
728      if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
729        String lang = ToolingExtensions.readStringExtension(ext,  "lang");
730        if (!Utilities.noString(lang) && !langs.contains(lang)) {
731          langs.add(lang);
732        }
733      }
734    }
735    for (ConceptReferenceDesignationComponent d : c.getDesignation()) {
736      String lang = d.getLanguage();
737      if (!Utilities.noString(lang) && !langs.contains(lang)) {
738        langs.add(lang);
739      }
740    }
741    for (ValueSetExpansionContainsComponent cc : c.getContains()) {
742      scanForLangs(cc, langs);
743    }    
744  }
745
746  private void addExpansionRowToTable(XhtmlNode t, ValueSet vs, ValueSetExpansionContainsComponent c, int i, boolean doLevel, boolean doSystem, boolean doDefinition, List<UsedConceptMap> maps, CodeSystem allCS, List<String> langs, Map<String, String> designations, boolean doDesignations, Map<String, String> properties) throws FHIRFormatError, DefinitionException, IOException {
747    XhtmlNode tr = t.tr();
748    if (ValueSetUtilities.isDeprecated(vs, c)) {
749      tr.setAttribute("style", "background-color: #ffeeee");
750    }
751      
752    XhtmlNode td = tr.td();
753
754    String tgt = makeAnchor(c.getSystem(), c.getCode());
755    td.an(tgt);
756
757    if (doLevel) {
758      td.addText(Integer.toString(i));
759      td = tr.td();
760    }
761    String s = Utilities.padLeft("", '\u00A0', i*2);
762    td.attribute("style", "white-space:nowrap").addText(s);
763    addCodeToTable(c.getAbstract(), c.getSystem(), c.getCode(), c.getDisplay(), td);
764    if (doSystem) {
765      td = tr.td();
766      td.addText(c.getSystem());
767    }
768    td = tr.td();
769    if (c.hasDisplayElement())
770      td.addText(c.getDisplay());
771
772    if (doDefinition) {
773      CodeSystem cs = allCS;
774      if (cs == null)
775        cs = getContext().getWorker().fetchCodeSystem(c.getSystem());
776      td = tr.td();
777      if (cs != null) {
778        String defn = CodeSystemUtilities.getCodeDefinition(cs, c.getCode());
779        addMarkdown(td, defn, cs.getUserString("path"));
780      }
781    }
782    for (String n  : Utilities.sorted(properties.keySet())) {
783      td = tr.td();
784      String ps = getPropertyValue(c, n); 
785      if (!Utilities.noString(ps)) {  
786        td.addText(ps);        
787      }
788    }
789    for (UsedConceptMap m : maps) {
790      td = tr.td();
791      List<TargetElementComponentWrapper> mappings = findMappingsForCode(c.getCode(), m.getMap());
792      boolean first = true;
793      for (TargetElementComponentWrapper mapping : mappings) {
794        if (!first)
795            td.br();
796        first = false;
797        XhtmlNode span = td.span(null, mapping.comp.getRelationship().toString());
798        span.addText(getCharForRelationship(mapping.comp));
799        addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); 
800        if (!Utilities.noString(mapping.comp.getComment()))
801          td.i().tx("("+mapping.comp.getComment()+")");
802      }
803    }
804    if (doDesignations) {
805      addDesignationsToRow(c, designations, tr);
806      addLangaugesToRow(c, langs, tr);
807    }
808    for (ValueSetExpansionContainsComponent cc : c.getContains()) {
809      addExpansionRowToTable(t, vs, cc, i+1, doLevel, doSystem, doDefinition, maps, allCS, langs, designations, doDesignations, properties);
810    }
811  }
812
813
814
815
816
817  private String getPropertyValue(ValueSetExpansionContainsComponent c, String n) {
818    for (ConceptPropertyComponent  cp : c.getProperty()) {
819      if (n.equals(cp.getCode())) {
820        return cp.getValue().primitiveValue();
821      }
822    }
823    return null;
824  }
825
826  private boolean checkSystemMatches(String system, ValueSetExpansionContainsComponent cc) {
827    if (!system.equals(cc.getSystem()))
828      return false;
829    for (ValueSetExpansionContainsComponent cc1 : cc.getContains()) {
830      if (!checkSystemMatches(system, cc1))
831        return false;
832    }
833     return true;
834  }
835
836  private void addCodeToTable(boolean isAbstract, String system, String code, String display, XhtmlNode td) {
837    CodeSystem e = getContext().getWorker().fetchCodeSystem(system);
838    if (e == null || (e.getContent() != org.hl7.fhir.r5.model.CodeSystem.CodeSystemContentMode.COMPLETE && e.getContent() != org.hl7.fhir.r5.model.CodeSystem.CodeSystemContentMode.FRAGMENT)) {
839      if (isAbstract)
840        td.i().setAttribute("title", ABSTRACT_CODE_HINT).addText(code);
841      else if ("http://snomed.info/sct".equals(system)) {
842        td.ah(sctLink(code)).addText(code);
843      } else if ("http://loinc.org".equals(system)) {
844          td.ah(LoincLinker.getLinkForCode(code)).addText(code);
845      } else        
846        td.addText(code);
847    } else {
848      String href = context.fixReference(getCsRef(e));
849      if (href.contains("#"))
850        href = href + "-"+Utilities.nmtokenize(code);
851      else
852        href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(code);
853      if (isAbstract)
854        td.ah(href).setAttribute("title", ABSTRACT_CODE_HINT).i().addText(code);
855      else
856        td.ah(href).addText(code);
857    }
858  }
859
860
861  public String sctLink(String code) {
862//    if (snomedEdition != null)
863//      http://browser.ihtsdotools.org/?perspective=full&conceptId1=428041000124106&edition=us-edition&release=v20180301&server=https://prod-browser-exten.ihtsdotools.org/api/snomed&langRefset=900000000000509007
864    return "http://snomed.info/id/"+code;
865  }
866
867  private void addRefToCode(XhtmlNode td, String target, String vslink, String code) {
868    addCodeToTable(false, target, code, null, td);
869//    CodeSystem cs = getContext().getWorker().fetchCodeSystem(target);
870//    String cslink = getCsRef(cs);
871//    String link = cslink != null ? cslink+"#"+cs.getId()+"-"+code : vslink+"#"+code;
872//    if (!Utilities.isAbsoluteUrl(link)) {
873//      link = getContext().getSpecificationLink()+link;
874//    }
875//    XhtmlNode a = td.ah(link);
876//    a.addText(code);
877  }
878
879  private boolean generateComposition(XhtmlNode x, ValueSet vs, boolean header, List<UsedConceptMap> maps) throws FHIRException, IOException {
880    boolean hasExtensions = false;
881    List<String> langs = new ArrayList<String>();
882    Map<String, String> designations = new HashMap<>(); //  map of url = description, where url is the designation code. Designations that are for languages won't make it into this list 
883    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
884      scanDesignations(inc, langs, designations);
885    }
886    for (ConceptSetComponent inc : vs.getCompose().getExclude()) {
887      scanDesignations(inc, langs, designations);
888    }
889    boolean doDesignations = langs.size() + designations.size() < MAX_DESIGNATIONS_IN_LINE;
890    
891    if (header) {
892      XhtmlNode h = x.h2();
893      h.addText(vs.present());
894      addMarkdown(x, vs.getDescription());
895      if (vs.hasCopyrightElement())
896        generateCopyright(x, vs);
897    }
898    int index = 0;
899    if (vs.getCompose().getInclude().size() == 1 && vs.getCompose().getExclude().size() == 0) {
900      hasExtensions = genInclude(x.ul(), vs.getCompose().getInclude().get(0), "Include", langs, doDesignations, maps, designations, index, vs) || hasExtensions;
901    } else {
902      XhtmlNode p = x.para();
903      p.tx("This value set includes codes based on the following rules:");
904      XhtmlNode ul = x.ul();
905      for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
906        hasExtensions = genInclude(ul, inc, "Include", langs, doDesignations, maps, designations, index, vs) || hasExtensions;
907        index++;
908      }
909      if (vs.getCompose().hasExclude()) {
910        p = x.para();
911        p.tx("This value set excludes codes based on the following rules:");
912        ul = x.ul();
913        for (ConceptSetComponent exc : vs.getCompose().getExclude()) {
914          hasExtensions = genInclude(ul, exc, "Exclude", langs, doDesignations, maps, designations, index, vs) || hasExtensions;
915          index++;
916        }
917      }
918    }
919    
920    // now, build observed languages
921
922    if (!doDesignations && langs.size() + designations.size() > 0) {
923      Collections.sort(langs);
924      if (designations.size() == 0) {
925        x.para().b().tx("Additional Language Displays");        
926      } else if (langs.size() == 0) {
927        x.para().b().tx("Additional Designations");       
928      } else {
929        x.para().b().tx("Additional Designations and Language Displays");
930      }
931      XhtmlNode t = x.table("codes");
932      XhtmlNode tr = t.tr();
933      tr.td().b().tx("Code");
934      for (String url : designations.keySet()) {
935        tr.td().b().addText(designations.get(url));
936      }
937      for (String lang : langs) {
938        tr.td().b().addText(describeLang(lang));
939      }
940      for (ConceptSetComponent c : vs.getCompose().getInclude()) {
941        for (ConceptReferenceComponent cc : c.getConcept()) {
942          addDesignationRow(cc, t, langs, designations);
943        }
944      }
945    }
946
947  
948    return hasExtensions;
949  }
950
951  private void renderExpansionRules(XhtmlNode x, ConceptSetComponent inc, int index, Map<String, ConceptDefinitionComponent> definitions) throws FHIRException, IOException {
952    String s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface, but the rules are not properly defined";
953    if (inc.hasExtension(ToolingExtensions.EXT_EXPAND_RULES)) {
954      String rule = inc.getExtensionString(ToolingExtensions.EXT_EXPAND_RULES);
955      if (rule != null) {
956        switch (rule) {
957        case "all-codes": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains all the codes, and also this structure:"; 
958        case "ungrouped": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains this structure, and any codes not found in the structure:";
959        case "groups-only": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains this structure:";
960        }
961      }
962    }
963    x.br();
964    x.tx(s);
965    HierarchicalTableGenerator gen = new HierarchicalTableGenerator(context.getDestDir(), context.isInlineGraphics(), true);
966    TableModel model = gen.new TableModel("exp.h="+index, context.getRules() == GenerationRules.IG_PUBLISHER);    
967    model.setAlternating(true);
968    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("vs.exp.header", "Code"), translate("vs.exp.hint", "The code for the item"), null, 0));
969    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("vs.exp.header", "Display"), translate("vs.exp.hint", "The display for the item"), null, 0));
970
971    for (Extension ext : inc.getExtensionsByUrl(ToolingExtensions.EXT_EXPAND_GROUP)) {
972      renderExpandGroup(gen, model, ext, inc, definitions);
973    }
974    x.br();
975    x.tx("table"); 
976    XhtmlNode xn = gen.generate(model, context.getLocalPrefix(), 1, null);
977    x.getChildNodes().add(xn);
978  }
979
980  private void renderExpandGroup(HierarchicalTableGenerator gen, TableModel model, Extension ext, ConceptSetComponent inc, Map<String, ConceptDefinitionComponent> definitions) {
981    Row row = gen.new Row(); 
982    model.getRows().add(row);
983    row.setIcon("icon_entry_blue.png", "entry");
984    String code = ext.getExtensionString("code");
985    if (code != null) {
986      row.getCells().add(gen.new Cell(null, null, code, null, null));
987      row.getCells().add(gen.new Cell(null, null, getDisplayForCode(inc, code, definitions), null, null));
988    } else if (ext.hasId()) {      
989      row.getCells().add(gen.new Cell(null, null, "(#"+ext.getId()+")", null, null));      
990      row.getCells().add(gen.new Cell(null, null, ext.getExtensionString("display"), null, null));
991    } else {
992      row.getCells().add(gen.new Cell(null, null, null, null, null));      
993      row.getCells().add(gen.new Cell(null, null, ext.getExtensionString("display"), null, null));
994    }
995    for (Extension member : ext.getExtensionsByUrl("member")) {
996      Row subRow = gen.new Row(); 
997      row.getSubRows().add(subRow);
998      subRow.setIcon("icon_entry_blue.png", "entry");
999      String mc = member.getValue().primitiveValue();
1000      // mc might be a reference to another expansion group - we check that first, or to a code in the compose
1001      if (mc.startsWith("#")) {
1002        // it's a reference by id
1003        subRow.getCells().add(gen.new Cell(null, null, "("+mc+")", null, null));      
1004        subRow.getCells().add(gen.new Cell(null, null, "group reference by id", null, null));
1005      } else {
1006        Extension tgt = findTargetByCode(inc, mc);
1007        if (tgt != null) {
1008          subRow.getCells().add(gen.new Cell(null, null, mc, null, null));      
1009          subRow.getCells().add(gen.new Cell(null, null, "group reference by code", null, null));                    
1010        } else {
1011          subRow.getCells().add(gen.new Cell(null, null, mc, null, null));      
1012          subRow.getCells().add(gen.new Cell(null, null, getDisplayForCode(inc, mc, definitions), null, null));          
1013        }
1014      }
1015    }
1016  }
1017
1018  private Extension findTargetByCode(ConceptSetComponent inc, String mc) {
1019    for (Extension ext : inc.getExtensionsByUrl(ToolingExtensions.EXT_EXPAND_GROUP)) {
1020      String code = ext.getExtensionString("code");
1021      if (mc.equals(code)) {
1022        return ext;
1023      }
1024    }
1025    return null;
1026  }
1027
1028  private String getDisplayForCode(ConceptSetComponent inc, String code, Map<String, ConceptDefinitionComponent> definitions) {
1029    for (ConceptReferenceComponent cc : inc.getConcept()) {
1030      if (code.equals(cc.getCode())) {
1031        if (cc.hasDisplay()) {
1032          return cc.getDisplay();
1033        }
1034      }
1035    }
1036    if (definitions.containsKey(code)) {
1037      return definitions.get(code).getDisplay();
1038    }
1039    return null;
1040  }
1041
1042  private void scanDesignations(ConceptSetComponent inc, List<String> langs, Map<String, String> designations) {
1043    for (ConceptReferenceComponent cc : inc.getConcept()) {
1044      for (Extension ext : cc.getExtension()) {
1045        if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
1046          String lang = ToolingExtensions.readStringExtension(ext,  "lang");
1047          if (!Utilities.noString(lang) && !langs.contains(lang)) {
1048            langs.add(lang);
1049          }
1050        }
1051      }
1052      for (ConceptReferenceDesignationComponent d : cc.getDesignation()) {
1053        String lang = d.getLanguage();
1054        if (!Utilities.noString(lang) && !langs.contains(lang)) {
1055          langs.add(lang);
1056        } else {
1057          // can we present this as a designation that we know?
1058          String disp = getDisplayForDesignation(d);
1059          String url = getUrlForDesignation(d);
1060          if (disp == null) {
1061            disp = getDisplayForUrl(url);
1062          }
1063          if (disp != null && !designations.containsKey(url)) {
1064            designations.put(url, disp);            
1065          }
1066        }
1067      }
1068    }
1069  }
1070
1071  private String getDisplayForUrl(String url) {
1072    if (url == null) {
1073      return null;
1074    }
1075    switch (url) {
1076    case "http://snomed.info/sct#900000000000003001":
1077      return "Fully specified name";
1078    case "http://snomed.info/sct#900000000000013009":
1079      return "Synonym";
1080    default:
1081      // As specified in http://www.hl7.org/fhir/valueset-definitions.html#ValueSet.compose.include.concept.designation.use and in http://www.hl7.org/fhir/codesystem-definitions.html#CodeSystem.concept.designation.use the terminology binding is extensible.
1082      return url;
1083    }
1084  }
1085
1086  private String getUrlForDesignation(ConceptReferenceDesignationComponent d) {
1087    if (d.hasUse() && d.getUse().hasSystem() && d.getUse().hasCode()) {
1088      return d.getUse().getSystem()+"#"+d.getUse().getCode();
1089    } else {
1090      return null;
1091    }
1092  }
1093
1094  private String getDisplayForDesignation(ConceptReferenceDesignationComponent d) {
1095    if (d.hasUse() && d.getUse().hasDisplay()) {
1096      return d.getUse().getDisplay();
1097    } else {
1098      return null;
1099    }
1100  }
1101
1102  private boolean genInclude(XhtmlNode ul, ConceptSetComponent inc, String type, List<String> langs, boolean doDesignations, List<UsedConceptMap> maps, Map<String, String> designations, int index, Resource vsRes) throws FHIRException, IOException {
1103    boolean hasExtensions = false;
1104    XhtmlNode li;
1105    li = ul.li();
1106    CodeSystem e = getContext().getWorker().fetchCodeSystem(inc.getSystem());
1107    Map<String, ConceptDefinitionComponent> definitions = new HashMap<>();
1108    
1109    if (inc.hasSystem()) {
1110      if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) {
1111        li.addText(type+" all codes defined in ");
1112        addCsRef(inc, li, e);
1113      } else {
1114        if (inc.getConcept().size() > 0) {
1115          li.addText(type+" these codes as defined in ");
1116          addCsRef(inc, li, e);
1117          if (inc.hasVersion()) {
1118            li.addText(" version ");
1119            li.code(inc.getVersion()); 
1120          }
1121
1122          // for performance reasons, we do all the fetching in one batch
1123          definitions = getConceptsForCodes(e, inc);
1124
1125          
1126          XhtmlNode t = li.table("none");
1127          boolean hasComments = false;
1128          boolean hasDefinition = false;
1129          for (ConceptReferenceComponent c : inc.getConcept()) {
1130            hasComments = hasComments || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT);
1131            ConceptDefinitionComponent cc = definitions == null ? null : definitions.get(c.getCode()); 
1132            hasDefinition = hasDefinition || ((cc != null && cc.hasDefinition()) || ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION));
1133          }
1134          if (hasComments || hasDefinition)
1135            hasExtensions = true;
1136          addMapHeaders(addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false, false, null, langs, designations, doDesignations), maps);
1137          for (ConceptReferenceComponent c : inc.getConcept()) {
1138            XhtmlNode tr = t.tr();
1139            XhtmlNode td = tr.td();
1140            ConceptDefinitionComponent cc = definitions == null ? null : definitions.get(c.getCode()); 
1141            addCodeToTable(false, inc.getSystem(), c.getCode(), c.hasDisplay()? c.getDisplay() : cc != null ? cc.getDisplay() : "", td);
1142
1143            td = tr.td();
1144            if (!Utilities.noString(c.getDisplay()))
1145              td.addText(c.getDisplay());
1146            else if (cc != null && !Utilities.noString(cc.getDisplay()))
1147              td.addText(cc.getDisplay());
1148
1149            if (hasDefinition) {
1150              td = tr.td();
1151              if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)) {
1152                smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION));
1153              } else if (cc != null && !Utilities.noString(cc.getDefinition())) {
1154                smartAddText(td, cc.getDefinition());
1155              }
1156            }
1157            if (hasComments) {
1158              td = tr.td();
1159              if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT)) {
1160                smartAddText(td, "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT));
1161              }
1162            }
1163            if (doDesignations) {
1164              addDesignationsToRow(c, designations, tr);
1165              addLangaugesToRow(c, langs, tr);
1166            }
1167            for (UsedConceptMap m : maps) {
1168              td = tr.td();
1169              List<TargetElementComponentWrapper> mappings = findMappingsForCode(c.getCode(), m.getMap());
1170              boolean first = true;
1171              for (TargetElementComponentWrapper mapping : mappings) {
1172                if (!first)
1173                    td.br();
1174                first = false;
1175                XhtmlNode span = td.span(null, mapping.comp.getRelationship().toString());
1176                span.addText(getCharForRelationship(mapping.comp));
1177                addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); 
1178                if (!Utilities.noString(mapping.comp.getComment()))
1179                  td.i().tx("("+mapping.comp.getComment()+")");
1180              }
1181            }
1182          }
1183        }
1184        if (inc.getFilter().size() > 0) {
1185          li.addText(type+" codes from ");
1186          addCsRef(inc, li, e);
1187          li.tx(" where ");
1188          for (int i = 0; i < inc.getFilter().size(); i++) {
1189            ConceptSetFilterComponent f = inc.getFilter().get(i);
1190            if (i > 0) {
1191              if (i == inc.getFilter().size()-1) {
1192                li.tx(" and ");
1193              } else {
1194                li.tx(", ");
1195              }
1196            }
1197            if (f.getOp() == FilterOperator.EXISTS) {
1198              if (f.getValue().equals("true")) {
1199                li.tx(f.getProperty()+" exists");
1200              } else {
1201                li.tx(f.getProperty()+" doesn't exist");
1202              }
1203            } else {
1204              li.tx(f.getProperty()+" "+describe(f.getOp())+" ");
1205              if (e != null && codeExistsInValueSet(e, f.getValue())) {
1206                String href = getContext().fixReference(getCsRef(e));
1207                if (href.contains("#"))
1208                  href = href + "-"+Utilities.nmtokenize(f.getValue());
1209                else
1210                  href = href + "#"+e.getId()+"-"+Utilities.nmtokenize(f.getValue());
1211                li.ah(href).addText(f.getValue());
1212              } else if ("concept".equals(f.getProperty()) && inc.hasSystem()) {
1213                li.addText(f.getValue());
1214                ValidationResult vr = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions(), inc.getSystem(), inc.getVersion(), f.getValue(), null);
1215                if (vr.isOk()) {
1216                  li.tx(" ("+vr.getDisplay()+")");
1217                }
1218              }
1219              else
1220                li.addText(f.getValue());
1221              String disp = ToolingExtensions.getDisplayHint(f);
1222              if (disp != null)
1223                li.tx(" ("+disp+")");
1224            }
1225          }
1226        }
1227      }
1228      if (inc.hasValueSet()) {
1229        li.tx(", where the codes are contained in ");
1230        boolean first = true;
1231        for (UriType vs : inc.getValueSet()) {
1232          if (first)
1233            first = false;
1234          else
1235            li.tx(", ");
1236          AddVsRef(vs.asStringValue(), li, vsRes);
1237        }
1238      }
1239      if (inc.hasExtension(ToolingExtensions.EXT_EXPAND_RULES) || inc.hasExtension(ToolingExtensions.EXT_EXPAND_GROUP)) {
1240        hasExtensions = true;
1241        renderExpansionRules(li, inc, index, definitions);
1242      }
1243    } else {
1244      li.tx("Import all the codes that are contained in ");
1245      if (inc.getValueSet().size() < 4) {
1246        boolean first = true;
1247        for (UriType vs : inc.getValueSet()) {
1248          if (first)
1249            first = false;
1250          else
1251            li.tx(", ");
1252          AddVsRef(vs.asStringValue(), li, vsRes);
1253        }
1254      } else {
1255        XhtmlNode xul = li.ul();
1256        for (UriType vs : inc.getValueSet()) {
1257          AddVsRef(vs.asStringValue(), xul.li(), vsRes);
1258        }
1259        
1260      }
1261    }
1262    return hasExtensions;
1263  }
1264
1265  public void addDesignationsToRow(ConceptReferenceComponent c, Map<String, String> designations, XhtmlNode tr) {
1266    for (String url : designations.keySet()) {
1267      String d = null;
1268      if (d == null) {
1269        for (ConceptReferenceDesignationComponent dd : c.getDesignation()) {
1270          if (url.equals(getUrlForDesignation(dd))) {
1271            d = dd.getValue();
1272          }
1273        }
1274      }
1275      tr.td().addText(d == null ? "" : d);
1276    }
1277  }
1278
1279  public void addLangaugesToRow(ConceptReferenceComponent c, List<String> langs, XhtmlNode tr) {
1280    for (String lang : langs) {
1281      String d = null;
1282      for (Extension ext : c.getExtension()) {
1283        if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) {
1284          String l = ToolingExtensions.readStringExtension(ext, "lang");
1285          if (lang.equals(l)) {
1286            d = ToolingExtensions.readStringExtension(ext, "content");
1287          }
1288        }
1289      }
1290      if (d == null) {
1291        for (ConceptReferenceDesignationComponent dd : c.getDesignation()) {
1292          String l = dd.getLanguage();
1293          if (lang.equals(l)) {
1294            d = dd.getValue();
1295          }
1296        }
1297      }
1298      tr.td().addText(d == null ? "" : d);
1299    }
1300  }
1301
1302
1303  private Map<String, ConceptDefinitionComponent> getConceptsForCodes(CodeSystem e, ConceptSetComponent inc) {
1304    if (e == null) {
1305      e = getContext().getWorker().fetchCodeSystem(inc.getSystem());
1306    }
1307    
1308    ValueSetExpansionComponent vse = null;
1309    if (!context.isNoSlowLookup()) { // && !getContext().getWorker().hasCache()) { removed GG 20220107 like what is this trying to do?
1310      try {
1311        ValueSetExpansionOutcome vso = getContext().getWorker().expandVS(inc, false, false);   
1312        ValueSet valueset = vso.getValueset();
1313        if (valueset == null)
1314          throw new TerminologyServiceException("Error Expanding ValueSet: "+vso.getError());
1315        vse = valueset.getExpansion();        
1316
1317      } catch (TerminologyServiceException e1) {
1318        return null;
1319      }
1320    }
1321    
1322    Map<String, ConceptDefinitionComponent> results = new HashMap<>();
1323    List<CodingValidationRequest> serverList = new ArrayList<>();
1324    
1325    // 1st pass, anything we can resolve internally
1326    for (ConceptReferenceComponent cc : inc.getConcept()) {
1327      String code = cc.getCode();
1328      ConceptDefinitionComponent v = null;
1329      if (e != null && code != null) {
1330        v = getConceptForCode(e.getConcept(), code);
1331      }
1332      if (v == null && vse != null) {
1333        v = getConceptForCodeFromExpansion(vse.getContains(), code);
1334      }
1335      if (v != null) {
1336        results.put(code, v);
1337      } else {
1338        serverList.add(new CodingValidationRequest(new Coding(inc.getSystem(), code, null)));
1339      }
1340    }
1341    if (!context.isNoSlowLookup() && !serverList.isEmpty()) {
1342      getContext().getWorker().validateCodeBatch(getContext().getTerminologyServiceOptions(), serverList, null);
1343      for (CodingValidationRequest vr : serverList) {
1344        ConceptDefinitionComponent v = vr.getResult().asConceptDefinition();
1345        if (v != null) {
1346          results.put(vr.getCoding().getCode(), v);
1347        }
1348      }
1349    }
1350    return results;
1351  }
1352  
1353  private ConceptDefinitionComponent getConceptForCode(List<ConceptDefinitionComponent> list, String code) {
1354    for (ConceptDefinitionComponent c : list) {
1355    if (code.equals(c.getCode()))
1356      return c;
1357      ConceptDefinitionComponent v = getConceptForCode(c.getConcept(), code);
1358      if (v != null)
1359        return v;
1360    }
1361    return null;
1362  }
1363
1364  private ConceptDefinitionComponent getConceptForCodeFromExpansion(List<ValueSetExpansionContainsComponent> list, String code) {
1365    for (ValueSetExpansionContainsComponent c : list) {
1366      if (code.equals(c.getCode())) {
1367        ConceptDefinitionComponent res = new ConceptDefinitionComponent();
1368        res.setCode(c.getCode());
1369        res.setDisplay(c.getDisplay());
1370        return res;
1371      }
1372      ConceptDefinitionComponent v = getConceptForCodeFromExpansion(c.getContains(), code);
1373      if (v != null)
1374        return v;
1375    }
1376    return null;
1377  }
1378
1379 
1380  private boolean codeExistsInValueSet(CodeSystem cs, String code) {
1381    for (ConceptDefinitionComponent c : cs.getConcept()) {
1382      if (inConcept(code, c))
1383        return true;
1384    }
1385    return false;
1386  }
1387  
1388
1389
1390  private void addDesignationRow(ConceptReferenceComponent c, XhtmlNode t, List<String> langs, Map<String, String> designations) {
1391    XhtmlNode tr = t.tr();
1392    tr.td().addText(c.getCode());
1393    addDesignationsToRow(c, designations, tr);
1394    addLangaugesToRow(c, langs, tr);
1395  }
1396
1397
1398  private String describe(FilterOperator op) {
1399    if (op == null)
1400      return " null ";
1401    switch (op) {
1402    case EQUAL: return " = ";
1403    case ISA: return " is-a ";
1404    case ISNOTA: return " is-not-a ";
1405    case REGEX: return " matches (by regex) ";
1406    case NULL: return " ?ngen-13? ";
1407    case IN: return " in ";
1408    case NOTIN: return " not in ";
1409    case DESCENDENTOF: return " descends from ";
1410    case EXISTS: return " exists ";
1411    case GENERALIZES: return " generalizes ";
1412    }
1413    return null;
1414  }
1415
1416
1417
1418 
1419
1420  private boolean inConcept(String code, ConceptDefinitionComponent c) {
1421    if (c.hasCodeElement() && c.getCode().equals(code))
1422      return true;
1423    for (ConceptDefinitionComponent g : c.getConcept()) {
1424      if (inConcept(code, g))
1425        return true;
1426    }
1427    return false;
1428  }
1429
1430
1431}