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