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