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