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