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