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