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