001package org.hl7.fhir.r4.context;
002
003import java.io.ByteArrayOutputStream;
004import java.io.File;
005import java.io.FileInputStream;
006import java.io.FileNotFoundException;
007import java.io.FileOutputStream;
008import java.io.IOException;
009import java.util.*;
010
011import org.apache.commons.codec.Charsets;
012import org.apache.commons.lang3.StringUtils;
013import org.hl7.fhir.r4.formats.IParser.OutputStyle;
014import org.hl7.fhir.r4.conformance.ProfileUtilities;
015import org.hl7.fhir.r4.context.BaseWorkerContext.NullTranslator;
016import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult;
017import org.hl7.fhir.r4.context.IWorkerContext.ILoggingService.LogCategory;
018import org.hl7.fhir.r4.formats.JsonParser;
019import org.hl7.fhir.r4.model.BooleanType;
020import org.hl7.fhir.r4.model.Bundle;
021import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
022import org.hl7.fhir.r4.model.CodeSystem;
023import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode;
024import org.hl7.fhir.r4.model.CodeSystem.CodeSystemHierarchyMeaning;
025import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
026import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent;
027import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent;
028import org.hl7.fhir.r4.model.NamingSystem.NamingSystemIdentifierType;
029import org.hl7.fhir.r4.model.NamingSystem.NamingSystemUniqueIdComponent;
030import org.hl7.fhir.r4.model.CodeableConcept;
031import org.hl7.fhir.r4.model.Coding;
032import org.hl7.fhir.r4.model.ConceptMap;
033import org.hl7.fhir.r4.model.Constants;
034import org.hl7.fhir.r4.model.ExpansionProfile;
035import org.hl7.fhir.r4.model.MetadataResource;
036import org.hl7.fhir.r4.model.NamingSystem;
037import org.hl7.fhir.r4.model.OperationDefinition;
038import org.hl7.fhir.r4.model.OperationOutcome;
039import org.hl7.fhir.r4.model.Parameters;
040import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
041import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind;
042import org.hl7.fhir.r4.model.PrimitiveType;
043import org.hl7.fhir.r4.model.Questionnaire;
044import org.hl7.fhir.r4.model.Reference;
045import org.hl7.fhir.r4.model.Resource;
046import org.hl7.fhir.r4.model.SearchParameter;
047import org.hl7.fhir.r4.model.StringType;
048import org.hl7.fhir.r4.model.StructureDefinition;
049import org.hl7.fhir.r4.model.StructureMap;
050import org.hl7.fhir.r4.model.UriType;
051import org.hl7.fhir.r4.model.ValueSet;
052import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
053import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent;
054import org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent;
055import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent;
056import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
057import org.hl7.fhir.r4.terminologies.ValueSetExpander.ETooCostly;
058import org.hl7.fhir.r4.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
059import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
060import org.hl7.fhir.r4.terminologies.ValueSetExpanderFactory;
061import org.hl7.fhir.r4.terminologies.ValueSetExpansionCache;
062import org.hl7.fhir.r4.utils.ToolingExtensions;
063import org.hl7.fhir.r4.utils.client.FHIRToolingClient;
064import org.hl7.fhir.exceptions.DefinitionException;
065import org.hl7.fhir.exceptions.FHIRException;
066import org.hl7.fhir.exceptions.NoTerminologyServiceException;
067import org.hl7.fhir.exceptions.TerminologyServiceException;
068import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
069import org.hl7.fhir.utilities.OIDUtils;
070import org.hl7.fhir.utilities.TextFile;
071import org.hl7.fhir.utilities.TranslationServices;
072import org.hl7.fhir.utilities.Utilities;
073import org.hl7.fhir.utilities.validation.ValidationMessage;
074import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
075import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
076import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
077
078import com.google.gson.JsonObject;
079import com.google.gson.JsonSyntaxException;
080
081import ca.uhn.fhir.rest.annotation.Metadata;
082
083public abstract class BaseWorkerContext implements IWorkerContext {
084
085  private Object lock = new Object(); // used as a lock for the data that follows
086  
087  private Map<String, Map<String, Resource>> allResourcesById = new HashMap<String, Map<String, Resource>>();
088  // all maps are to the full URI
089  private Map<String, CodeSystem> codeSystems = new HashMap<String, CodeSystem>();
090  private Set<String> nonSupportedCodeSystems = new HashSet<String>();
091  private Map<String, ValueSet> valueSets = new HashMap<String, ValueSet>();
092  private Map<String, ConceptMap> maps = new HashMap<String, ConceptMap>();
093  private Map<String, StructureMap> transforms = new HashMap<String, StructureMap>();
094//  private Map<String, StructureDefinition> profiles = new HashMap<String, StructureDefinition>();
095  private Map<String, StructureDefinition> structures = new HashMap<String, StructureDefinition>();
096//  private Map<String, StructureDefinition> extensionDefinitions = new HashMap<String, StructureDefinition>();
097  private Map<String, SearchParameter> searchParameters = new HashMap<String, SearchParameter>();
098  private Map<String, Questionnaire> questionnaires = new HashMap<String, Questionnaire>();
099  private Map<String, OperationDefinition> operations = new HashMap<String, OperationDefinition>();
100  private List<NamingSystem> systems = new ArrayList<NamingSystem>();
101
102  
103  private ValueSetExpansionCache expansionCache = new ValueSetExpansionCache(this, lock);
104  protected boolean cacheValidation; // if true, do an expansion and cache the expansion
105  private Set<String> failed = new HashSet<String>(); // value sets for which we don't try to do expansion, since the first attempt to get a comprehensive expansion was not successful
106  protected Map<String, Map<String, ValidationResult>> validationCache = new HashMap<String, Map<String,ValidationResult>>();
107  protected String tsServer;
108  protected String validationCachePath;
109  protected String name;
110  private boolean allowLoadingDuplicates;
111
112  // private ValueSetExpansionCache expansionCache; //   
113
114  protected FHIRToolingClient txServer;
115  private Bundle bndCodeSystems;
116  private boolean canRunWithoutTerminology;
117  protected boolean noTerminologyServer;
118  protected String cache;
119  private int expandCodesLimit = 1000;
120  protected ILoggingService logger;
121  protected ExpansionProfile expProfile;
122  private TranslationServices translator = new NullTranslator();
123
124  public BaseWorkerContext() {
125    super();
126  }
127
128  public BaseWorkerContext(Map<String, CodeSystem> codeSystems, Map<String, ValueSet> valueSets, Map<String, ConceptMap> maps,  Map<String, StructureDefinition> profiles) {
129    super();
130    this.codeSystems = codeSystems;
131    this.valueSets = valueSets;
132    this.maps = maps;
133    this.structures = profiles;
134  }
135
136  protected void copy(BaseWorkerContext other) {
137    synchronized (other.lock) { // tricky, because you need to lock this as well, but it's really not in use yet 
138      allResourcesById.putAll(other.allResourcesById);
139      translator = other.translator;
140      codeSystems.putAll(other.codeSystems);
141      nonSupportedCodeSystems.addAll(other.nonSupportedCodeSystems);
142      valueSets.putAll(other.valueSets);
143      maps.putAll(other.maps);
144      transforms.putAll(other.transforms);
145      structures.putAll(other.structures);
146      searchParameters.putAll(other.searchParameters);
147      questionnaires.putAll(other.questionnaires);
148      operations.putAll(other.operations);
149      systems.addAll(other.systems);
150
151      allowLoadingDuplicates = other.allowLoadingDuplicates;
152      cacheValidation = other.cacheValidation;
153      tsServer = other.tsServer;
154      validationCachePath = other.validationCachePath;
155      name = other.name;
156      txServer = other.txServer;
157      bndCodeSystems = other.bndCodeSystems;
158      canRunWithoutTerminology = other.canRunWithoutTerminology;
159      noTerminologyServer = other.noTerminologyServer;
160      cache = other.cache;
161      expandCodesLimit = other.expandCodesLimit;
162      logger = other.logger;
163      expProfile = other.expProfile;
164    }
165  }
166  
167  public void cacheResource(Resource r) throws FHIRException {
168    synchronized (lock) {
169      Map<String, Resource> map = allResourcesById.get(r.fhirType());
170      if (map == null) {
171        map = new HashMap<String, Resource>();
172        allResourcesById.put(r.fhirType(), map);
173      }
174      map.put(r.getId(), r);
175
176      if (r instanceof MetadataResource) {
177        MetadataResource m = (MetadataResource) r;
178        String url = m.getUrl();
179        if (!allowLoadingDuplicates && hasResource(r.getClass(), url))
180          throw new DefinitionException("Duplicate Resource " + url);
181        if (r instanceof StructureDefinition)
182          seeMetadataResource((StructureDefinition) m, structures, false);
183        else if (r instanceof ValueSet)
184          seeMetadataResource((ValueSet) m, valueSets, false);
185        else if (r instanceof CodeSystem)
186          seeMetadataResource((CodeSystem) m, codeSystems, false);
187        else if (r instanceof SearchParameter)
188          seeMetadataResource((SearchParameter) m, searchParameters, false);
189        else if (r instanceof OperationDefinition)
190          seeMetadataResource((OperationDefinition) m, operations, false);
191        else if (r instanceof Questionnaire)
192          seeMetadataResource((Questionnaire) m, questionnaires, true);
193        else if (r instanceof ConceptMap)
194          seeMetadataResource((ConceptMap) m, maps, false);
195        else if (r instanceof StructureMap)
196          seeMetadataResource((StructureMap) m, transforms, false);
197        else if (r instanceof NamingSystem)
198          systems.add((NamingSystem) r);
199      }
200    }
201  }
202
203  /*
204   *  Compare business versions, returning "true" if the candidate newer version is in fact newer than the oldVersion
205   *  Comparison will work for strictly numeric versions as well as multi-level versions separated by ., -, _, : or space
206   *  Failing that, it will do unicode-based character ordering.
207   *  E.g. 1.5.3 < 1.14.3
208   *       2017-3-10 < 2017-12-7
209   *       A3 < T2
210   */
211  private boolean laterVersion(String newVersion, String oldVersion) {
212    // Compare business versions, retur
213    newVersion = newVersion.trim();
214    oldVersion = oldVersion.trim();
215    if (StringUtils.isNumeric(newVersion) && StringUtils.isNumeric(oldVersion))
216      return Double.parseDouble(newVersion) > Double.parseDouble(oldVersion);
217    else if (hasDelimiter(newVersion, oldVersion, "."))
218      return laterDelimitedVersion(newVersion, oldVersion, "\\.");
219    else if (hasDelimiter(newVersion, oldVersion, "-"))
220      return laterDelimitedVersion(newVersion, oldVersion, "\\-");
221    else if (hasDelimiter(newVersion, oldVersion, "_"))
222      return laterDelimitedVersion(newVersion, oldVersion, "\\_");
223    else if (hasDelimiter(newVersion, oldVersion, ":"))
224      return laterDelimitedVersion(newVersion, oldVersion, "\\:");
225    else if (hasDelimiter(newVersion, oldVersion, " "))
226      return laterDelimitedVersion(newVersion, oldVersion, "\\ ");
227    else {
228      return newVersion.compareTo(oldVersion) > 0;
229    }
230  }
231  
232  /*
233   * Returns true if both strings include the delimiter and have the same number of occurrences of it
234   */
235  private boolean hasDelimiter(String s1, String s2, String delimiter) {
236    return s1.contains(delimiter) && s2.contains(delimiter) && s1.split(delimiter).length == s2.split(delimiter).length;
237  }
238
239  private boolean laterDelimitedVersion(String newVersion, String oldVersion, String delimiter) {
240    String[] newParts = newVersion.split(delimiter);
241    String[] oldParts = oldVersion.split(delimiter);
242    for (int i = 0; i < newParts.length; i++) {
243      if (!newParts[i].equals(oldParts[i]))
244        return laterVersion(newParts[i], oldParts[i]);
245    }
246    // This should never happen
247    throw new Error("Delimited versions have exact match for delimiter '"+delimiter+"' : "+ Arrays.asList(newParts)+" vs "+Arrays.asList(oldParts));
248  }
249  
250  protected <T extends MetadataResource> void seeMetadataResource(T r, Map<String, T> map, boolean addId) throws FHIRException {
251    if (addId)
252      map.put(r.getId(), r); // todo: why?
253    if (!map.containsKey(r.getUrl()))
254      map.put(r.getUrl(), r);
255    else {
256      // If this resource already exists, see if it's the newest business version.  The default resource to return if not qualified is the most recent business version
257      MetadataResource existingResource = (MetadataResource)map.get(r.getUrl());
258      if (r.hasVersion() && existingResource.hasVersion() && !r.getVersion().equals(existingResource.getVersion())) {
259        if (laterVersion(r.getVersion(), existingResource.getVersion())) {
260          map.remove(r.getUrl());
261          map.put(r.getUrl(), r);
262        }
263      } else
264        map.remove(r.getUrl());
265        map.put(r.getUrl(), r);
266//        throw new FHIRException("Multiple declarations of resource with same canonical URL (" + r.getUrl() + ") and version (" + (r.hasVersion() ? r.getVersion() : "" ) + ")");
267    }
268    if (r.hasVersion())
269      map.put(r.getUrl()+"|"+r.getVersion(), r);
270  }  
271
272  @Override
273  public CodeSystem fetchCodeSystem(String system) {
274    synchronized (lock) {
275      return codeSystems.get(system);
276    }
277  } 
278
279  @Override
280  public boolean supportsSystem(String system) throws TerminologyServiceException {
281    synchronized (lock) {
282      if (codeSystems.containsKey(system))
283        return true;
284      else if (nonSupportedCodeSystems.contains(system))
285        return false;
286      else if (system.startsWith("http://example.org") || system.startsWith("http://acme.com") || system.startsWith("http://hl7.org/fhir/valueset-") || system.startsWith("urn:oid:"))
287        return false;
288      else {
289        if (noTerminologyServer)
290          return false;
291        if (bndCodeSystems == null) {
292          try {
293            tlog("Terminology server: Check for supported code systems for "+system);
294            bndCodeSystems = txServer.fetchFeed(txServer.getAddress()+"/CodeSystem?content-mode=not-present&_summary=true&_count=1000");
295          } catch (Exception e) {
296            if (canRunWithoutTerminology) {
297              noTerminologyServer = true;
298              log("==============!! Running without terminology server !! ==============");
299              log("txServer = "+txServer.getAddress());
300              log("Error = "+e.getMessage()+"");
301              log("=====================================================================");
302              return false;
303            } else
304              throw new TerminologyServiceException(e);
305          }
306        }
307        if (bndCodeSystems != null) {
308          for (BundleEntryComponent be : bndCodeSystems.getEntry()) {
309            CodeSystem cs = (CodeSystem) be.getResource();
310            if (!codeSystems.containsKey(cs.getUrl())) {
311              codeSystems.put(cs.getUrl(), null);
312            }
313          }
314        }
315        if (codeSystems.containsKey(system))
316          return true;
317      }
318      nonSupportedCodeSystems.add(system);
319      return false;
320    }
321  }
322
323  private void log(String message) {
324    if (logger != null)
325      logger.logMessage(message);
326    else
327      System.out.println(message);
328  }
329
330  @Override
331  public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean heirarchical) {
332    try {
333      if (vs.hasExpansion()) {
334        return new ValueSetExpansionOutcome(vs.copy());
335      }
336      String cacheFn = null;
337      if (cache != null) {
338        cacheFn = Utilities.path(cache, determineCacheId(vs, heirarchical)+".json");
339        if (new File(cacheFn).exists())
340          return loadFromCache(vs.copy(), cacheFn);
341      }
342      if (cacheOk && vs.hasUrl()) {
343        if (expProfile == null)
344          throw new Exception("No ExpansionProfile provided");
345        ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs, expProfile.setExcludeNested(!heirarchical));
346        if (vse.getValueset() != null) {
347          if (cache != null) {
348            FileOutputStream s = new FileOutputStream(cacheFn);
349            newJsonParser().compose(new FileOutputStream(cacheFn), vse.getValueset());
350            s.close();
351          }
352        }
353        return vse;
354      } else {
355        ValueSetExpansionOutcome res = expandOnServer(vs, cacheFn);
356        if (cacheFn != null) {
357          if (res.getValueset() != null) {
358            saveToCache(res.getValueset(), cacheFn);
359          } else { 
360            OperationOutcome oo = new OperationOutcome();
361            oo.addIssue().getDetails().setText(res.getError());
362            saveToCache(oo, cacheFn);
363          }
364        }
365        return res;
366      }
367    } catch (NoTerminologyServiceException e) {
368      return new ValueSetExpansionOutcome(e.getMessage() == null ? e.getClass().getName() : e.getMessage(), TerminologyServiceErrorClass.NOSERVICE);
369    } catch (Exception e) {
370      return new ValueSetExpansionOutcome(e.getMessage() == null ? e.getClass().getName() : e.getMessage(), TerminologyServiceErrorClass.UNKNOWN);
371    }
372  }
373
374  private ValueSetExpansionOutcome loadFromCache(ValueSet vs, String cacheFn) throws FileNotFoundException, Exception {
375    JsonParser parser = new JsonParser();
376    Resource r = parser.parse(new FileInputStream(cacheFn));
377    if (r instanceof OperationOutcome)
378      return new ValueSetExpansionOutcome(((OperationOutcome) r).getIssue().get(0).getDetails().getText(), TerminologyServiceErrorClass.UNKNOWN);
379    else {
380      vs.setExpansion(((ValueSet) r).getExpansion()); // because what is cached might be from a different value set
381      return new ValueSetExpansionOutcome(vs);
382    }
383  }
384
385  private void saveToCache(Resource res, String cacheFn) throws FileNotFoundException, Exception {
386    JsonParser parser = new JsonParser();
387    parser.compose(new FileOutputStream(cacheFn), res);
388  }
389
390  private String determineCacheId(ValueSet vs, boolean heirarchical) throws Exception {
391    // just the content logical definition is hashed
392    ValueSet vsid = new ValueSet();
393    vsid.setCompose(vs.getCompose());
394    JsonParser parser = new JsonParser();
395    parser.setOutputStyle(OutputStyle.NORMAL);
396    ByteArrayOutputStream b = new  ByteArrayOutputStream();
397    parser.compose(b, vsid);
398    b.close();
399    String s = new String(b.toByteArray(), Charsets.UTF_8);
400    // any code systems we can find, we add these too. 
401    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
402      CodeSystem cs = fetchCodeSystem(inc.getSystem());
403      if (cs != null) {
404        String css = cacheValue(cs);
405        s = s + css;
406      }
407    }
408    s = s + "-"+Boolean.toString(heirarchical);
409    String r = Integer.toString(s.hashCode());
410    //    TextFile.stringToFile(s, Utilities.path(cache, r+".id.json"));
411    return r;
412  }
413
414
415  private String cacheValue(CodeSystem cs) throws IOException {
416    CodeSystem csid = new CodeSystem();
417    csid.setId(cs.getId());
418    csid.setVersion(cs.getVersion());
419    csid.setContent(cs.getContent());
420    csid.setHierarchyMeaning(CodeSystemHierarchyMeaning.GROUPEDBY);
421    for (ConceptDefinitionComponent cc : cs.getConcept()) 
422      csid.getConcept().add(processCSConcept(cc));
423    JsonParser parser = new JsonParser();
424    parser.setOutputStyle(OutputStyle.NORMAL);
425    ByteArrayOutputStream b = new  ByteArrayOutputStream();
426    parser.compose(b, csid);
427    b.close();
428    return new String(b.toByteArray(), Charsets.UTF_8);
429  }
430
431
432  private ConceptDefinitionComponent processCSConcept(ConceptDefinitionComponent cc) {
433    ConceptDefinitionComponent ccid = new ConceptDefinitionComponent();
434    ccid.setCode(cc.getCode());
435    ccid.setDisplay(cc.getDisplay());
436    for (ConceptDefinitionComponent cci : cc.getConcept()) 
437      ccid.getConcept().add(processCSConcept(cci));
438    return ccid;
439  }
440
441  public ValueSetExpansionOutcome expandOnServer(ValueSet vs, String fn) throws Exception {
442    if (noTerminologyServer)
443      return new ValueSetExpansionOutcome("Error expanding ValueSet: running without terminology services", TerminologyServiceErrorClass.NOSERVICE);
444    if (expProfile == null)
445      throw new Exception("No ExpansionProfile provided");
446
447    try {
448      Map<String, String> params = new HashMap<String, String>();
449      params.put("_limit", Integer.toString(expandCodesLimit ));
450      params.put("_incomplete", "true");
451      tlog("Terminology Server: $expand on "+getVSSummary(vs));
452      ValueSet result = txServer.expandValueset(vs, expProfile.setIncludeDefinition(false), params);
453      return new ValueSetExpansionOutcome(result);  
454    } catch (Exception e) {
455      return new ValueSetExpansionOutcome("Error expanding ValueSet \""+vs.getUrl()+": "+e.getMessage(), TerminologyServiceErrorClass.UNKNOWN);
456    }
457  }
458
459  private String getVSSummary(ValueSet vs) {
460    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
461    for (ConceptSetComponent cc : vs.getCompose().getInclude())
462      b.append("Include "+getIncSummary(cc));
463    for (ConceptSetComponent cc : vs.getCompose().getExclude())
464      b.append("Exclude "+getIncSummary(cc));
465    return b.toString();
466  }
467
468  private String getIncSummary(ConceptSetComponent cc) {
469    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
470    for (UriType vs : cc.getValueSet())
471      b.append(vs.asStringValue());
472    String vsd = b.length() > 0 ? " where the codes are in the value sets ("+b.toString()+")" : "";
473    String system = cc.getSystem();
474    if (cc.hasConcept())
475      return Integer.toString(cc.getConcept().size())+" codes from "+system+vsd;
476    if (cc.hasFilter()) {
477      String s = "";
478      for (ConceptSetFilterComponent f : cc.getFilter()) {
479        if (!Utilities.noString(s))
480          s = s + " & ";
481        s = s + f.getProperty()+" "+f.getOp().toCode()+" "+f.getValue();
482      }
483      return "from "+system+" where "+s+vsd;
484    }
485    return "All codes from "+system+vsd;
486  }
487
488  private ValidationResult handleByCache(ValueSet vs, Coding coding, boolean tryCache) {
489    String cacheId = cacheId(coding);
490    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
491    if (cache == null) {
492      cache = new HashMap<String, IWorkerContext.ValidationResult>();
493      validationCache.put(vs.getUrl(), cache);
494    }
495    if (cache.containsKey(cacheId))
496      return cache.get(cacheId);
497    if (!tryCache)
498      return null;
499    if (!cacheValidation)
500      return null;
501    if (failed.contains(vs.getUrl()))
502      return null;
503    ValueSetExpansionOutcome vse = expandVS(vs, true, false);
504    if (vse.getValueset() == null || notcomplete(vse.getValueset())) {
505      failed.add(vs.getUrl());
506      return null;
507    }
508
509    ValidationResult res = validateCode(coding, vse.getValueset());
510    cache.put(cacheId, res);
511    return res;
512  }
513
514  private boolean notcomplete(ValueSet vs) {
515    if (!vs.hasExpansion())
516      return true;
517    if (!vs.getExpansion().getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-unclosed").isEmpty())
518      return true;
519    if (!vs.getExpansion().getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-toocostly").isEmpty())
520      return true;
521    return false;
522  }
523
524  private ValidationResult handleByCache(ValueSet vs, CodeableConcept concept, boolean tryCache) {
525    String cacheId = cacheId(concept);
526    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
527    if (cache == null) {
528      cache = new HashMap<String, IWorkerContext.ValidationResult>();
529      validationCache.put(vs.getUrl(), cache);
530    }
531    if (cache.containsKey(cacheId))
532      return cache.get(cacheId);
533
534    if (validationCache.containsKey(vs.getUrl()) && validationCache.get(vs.getUrl()).containsKey(cacheId))
535      return validationCache.get(vs.getUrl()).get(cacheId);
536    if (!tryCache)
537      return null;
538    if (!cacheValidation)
539      return null;
540    if (failed.contains(vs.getUrl()))
541      return null;
542    ValueSetExpansionOutcome vse = expandVS(vs, true, false);
543    if (vse.getValueset() == null || notcomplete(vse.getValueset())) {
544      failed.add(vs.getUrl());
545      return null;
546    }
547    ValidationResult res = validateCode(concept, vse.getValueset());
548    cache.put(cacheId, res);
549    return res;
550  }
551
552  private String cacheId(Coding coding) {
553    return "|"+coding.getSystem()+"|"+coding.getVersion()+"|"+coding.getCode()+"|"+coding.getDisplay();
554  }
555
556  private String cacheId(CodeableConcept cc) {
557    StringBuilder b = new StringBuilder();
558    for (Coding c : cc.getCoding()) {
559      b.append("#");
560      b.append(cacheId(c));
561    }    
562    return b.toString();
563  }
564
565  private ValidationResult verifyCodeExternal(ValueSet vs, Coding coding, boolean tryCache) throws Exception {
566    ValidationResult res = vs == null ? null : handleByCache(vs, coding, tryCache);
567    if (res != null)
568      return res;
569    Parameters pin = new Parameters();
570    pin.addParameter().setName("coding").setValue(coding);
571    if (vs != null)
572      pin.addParameter().setName("valueSet").setResource(vs);
573    res = serverValidateCode(pin, vs == null);
574    if (vs != null) {
575      Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
576      cache.put(cacheId(coding), res);
577    }
578    return res;
579  }
580
581  private ValidationResult verifyCodeExternal(ValueSet vs, CodeableConcept cc, boolean tryCache) throws Exception {
582    ValidationResult res = handleByCache(vs, cc, tryCache);
583    if (res != null)
584      return res;
585    Parameters pin = new Parameters();
586    pin.addParameter().setName("codeableConcept").setValue(cc);
587    pin.addParameter().setName("valueSet").setResource(vs);
588    res = serverValidateCode(pin, tryCache);
589    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
590    cache.put(cacheId(cc), res);
591    return res;
592  }
593
594  private ValidationResult serverValidateCode(Parameters pin, boolean doCache) throws Exception {
595    if (noTerminologyServer)
596      return new ValidationResult(null, null, TerminologyServiceErrorClass.NOSERVICE);
597    String cacheName = doCache ? generateCacheName(pin) : null;
598    ValidationResult res = loadFromCache(cacheName);
599    if (res != null)
600      return res;
601    tlog("Terminology Server: $validate-code "+describeValidationParameters(pin));
602    for (ParametersParameterComponent pp : pin.getParameter())
603      if (pp.getName().equals("profile"))
604        throw new Error("Can only specify profile in the context");
605    if (expProfile == null)
606      throw new Exception("No ExpansionProfile provided");
607    pin.addParameter().setName("profile").setResource(expProfile);
608
609    Parameters pout = txServer.operateType(ValueSet.class, "validate-code", pin);
610    boolean ok = false;
611    String message = "No Message returned";
612    String display = null;
613    TerminologyServiceErrorClass err = TerminologyServiceErrorClass.UNKNOWN;
614    for (ParametersParameterComponent p : pout.getParameter()) {
615      if (p.getName().equals("result"))
616        ok = ((BooleanType) p.getValue()).getValue().booleanValue();
617      else if (p.getName().equals("message"))
618        message = ((StringType) p.getValue()).getValue();
619      else if (p.getName().equals("display"))
620        display = ((StringType) p.getValue()).getValue();
621      else if (p.getName().equals("cause")) {
622        try {
623          IssueType it = IssueType.fromCode(((StringType) p.getValue()).getValue());
624          if (it == IssueType.UNKNOWN)
625            err = TerminologyServiceErrorClass.UNKNOWN;
626          else if (it == IssueType.NOTSUPPORTED)
627            err = TerminologyServiceErrorClass.VALUESET_UNSUPPORTED;
628        } catch (FHIRException e) {
629        }
630      }
631    }
632    if (!ok)
633      res = new ValidationResult(IssueSeverity.ERROR, message, err);
634    else if (message != null) 
635      res = new ValidationResult(IssueSeverity.WARNING, message, new ConceptDefinitionComponent().setDisplay(display));
636    else if (display != null)
637      res = new ValidationResult(new ConceptDefinitionComponent().setDisplay(display));
638    else
639      res = new ValidationResult(new ConceptDefinitionComponent());
640    saveToCache(res, cacheName);
641    return res;
642  }
643
644
645  protected void tlog(String msg) {
646    System.out.println("-tx: "+msg);
647  }
648
649  @SuppressWarnings("rawtypes")
650  private String describeValidationParameters(Parameters pin) {
651    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
652    for (ParametersParameterComponent p : pin.getParameter()) {
653      if (p.hasValue() && p.getValue() instanceof PrimitiveType) {
654        b.append(p.getName()+"="+((PrimitiveType) p.getValue()).asStringValue());
655      } else if (p.hasValue() && p.getValue() instanceof Coding) {
656        b.append("system="+((Coding) p.getValue()).getSystem());
657        b.append("code="+((Coding) p.getValue()).getCode());
658        b.append("display="+((Coding) p.getValue()).getDisplay());
659      } else if (p.hasValue() && p.getValue() instanceof CodeableConcept) {
660        if (((CodeableConcept) p.getValue()).hasCoding()) {
661          Coding c = ((CodeableConcept) p.getValue()).getCodingFirstRep();
662          b.append("system="+c.getSystem());
663          b.append("code="+c.getCode());
664          b.append("display="+c.getDisplay());
665        } else if (((CodeableConcept) p.getValue()).hasText()) {
666          b.append("text="+((CodeableConcept) p.getValue()).getText());
667        }
668      } else if (p.hasResource() && (p.getResource() instanceof ValueSet)) {
669        b.append("valueset="+getVSSummary((ValueSet) p.getResource()));
670      } 
671    }
672    return b.toString();
673  }
674
675  private ValidationResult loadFromCache(String fn) throws FileNotFoundException, IOException {
676    if (fn == null)
677      return null;
678    if (!(new File(fn).exists()))
679      return null;
680    String cnt = TextFile.fileToString(fn);
681    if (cnt.startsWith("!error: "))
682      return new ValidationResult(IssueSeverity.ERROR, cnt.substring(8));
683    else if (cnt.startsWith("!warning: "))
684      return new ValidationResult(IssueSeverity.ERROR, cnt.substring(10));
685    else
686      return new ValidationResult(new ConceptDefinitionComponent().setDisplay(cnt));
687  }
688
689  private void saveToCache(ValidationResult res, String cacheName) throws IOException {
690    if (cacheName == null)
691      return;
692    if (res.getDisplay() != null)
693      TextFile.stringToFile(res.getDisplay(), cacheName);
694    else if (res.getMessage() != null) {
695      if (res.getSeverity() == IssueSeverity.WARNING)
696        TextFile.stringToFile("!warning: "+res.getMessage(), cacheName);
697      else 
698        TextFile.stringToFile("!error: "+res.getMessage(), cacheName);
699    }
700  }
701
702  private String generateCacheName(Parameters pin) throws IOException {
703    if (cache == null)
704      return null;
705    String json = new JsonParser().composeString(pin);
706    return Utilities.path(cache, "vc"+Integer.toString(json.hashCode())+".json");
707  }
708
709  @Override
710  public ValueSetExpansionComponent expandVS(ConceptSetComponent inc, boolean heirachical) throws TerminologyServiceException {
711    ValueSet vs = new ValueSet();
712    vs.setCompose(new ValueSetComposeComponent());
713    vs.getCompose().getInclude().add(inc);
714    ValueSetExpansionOutcome vse = expandVS(vs, true, heirachical);
715    ValueSet valueset = vse.getValueset();
716    if (valueset == null)
717      throw new TerminologyServiceException("Error Expanding ValueSet: "+vse.getError());
718    return valueset.getExpansion();
719  }
720
721  @Override
722  public ValidationResult validateCode(String system, String code, String display) {
723    try {
724      CodeSystem cs = null;
725      synchronized (lock) {
726        cs = codeSystems.get(system);
727      }
728      
729      if (cs != null && cs.getContent() == CodeSystemContentMode.COMPLETE)
730        return verifyCodeInCodeSystem(cs, system, code, display);
731      else 
732        return verifyCodeExternal(null, new Coding().setSystem(system).setCode(code).setDisplay(display), false);
733    } catch (Exception e) {
734      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage());
735    }
736  }
737
738
739  @Override
740  public ValidationResult validateCode(Coding code, ValueSet vs) {
741    CodeSystem cs = null;
742    synchronized (lock) {
743      cs = codeSystems.get(code.getSystem());
744    }
745    if (cs != null) 
746      try {
747        return verifyCodeInCodeSystem(cs, code.getSystem(), code.getCode(), code.getDisplay());
748      } catch (Exception e) {
749        return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+code.getSystem()+"\": "+e.getMessage());
750      }
751    else if (vs != null && vs.hasExpansion()) 
752      try {
753        return verifyCodeInternal(vs, code.getSystem(), code.getCode(), code.getDisplay());
754      } catch (Exception e) {
755        return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+code.getSystem()+"\": "+e.getMessage());
756      }
757    else 
758      try {
759        return verifyCodeExternal(vs, code, true);
760      } catch (Exception e) {
761        return new ValidationResult(IssueSeverity.WARNING, "Error validating code \""+code+"\" in system \""+code.getSystem()+"\": "+e.getMessage());
762      }
763  }
764
765  @Override
766  public ValidationResult validateCode(CodeableConcept code, ValueSet vs) {
767    try {
768      if (vs.hasExpansion()) 
769        return verifyCodeInternal(vs, code);
770      else {
771        // we'll try expanding first; if that doesn't work, then we'll just pass it to the server to validate 
772        // ... could be a problem if the server doesn't have the code systems we have locally, so we try not to depend on the server
773        try {
774          ValueSetExpansionOutcome vse = expandVS(vs, true, false);
775          if (vse.getValueset() != null && !hasTooCostlyExpansion(vse.getValueset()))
776            return verifyCodeInternal(vse.getValueset(), code);
777        } catch (Exception e) {
778          // failed? we'll just try the server
779        }        
780        return verifyCodeExternal(vs, code, true);
781      }
782    } catch (Exception e) {
783      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code.toString()+"\": "+e.getMessage(), TerminologyServiceErrorClass.SERVER_ERROR);
784    }
785  }
786
787
788  private boolean hasTooCostlyExpansion(ValueSet valueset) {
789    return valueset != null && valueset.hasExpansion() && ToolingExtensions.hasExtension(valueset.getExpansion(), "http://hl7.org/fhir/StructureDefinition/valueset-toocostly");
790  }
791
792  @Override
793  public ValidationResult validateCode(String system, String code, String display, ValueSet vs) {
794    try {
795      if (system == null && display == null)
796        return verifyCodeInternal(vs, code);
797      CodeSystem cs = null;
798      synchronized (lock) {
799        cs = codeSystems.get(system);
800      }
801      
802      if (cs != null || vs.hasExpansion()) 
803        return verifyCodeInternal(vs, system, code, display);
804      else 
805        return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), true);
806    } catch (Exception e) {
807      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage(), TerminologyServiceErrorClass.SERVER_ERROR);
808    }
809  }
810
811  @Override
812  public ValidationResult validateCode(String system, String code, String display, ConceptSetComponent vsi) {
813    try {
814      ValueSet vs = new ValueSet();
815      vs.setUrl(Utilities.makeUuidUrn());
816      vs.getCompose().addInclude(vsi);
817      return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), true);
818    } catch (Exception e) {
819      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage());
820    }
821  }
822
823  public void initTS(String cachePath) throws Exception {
824    cache = cachePath;
825    expansionCache = new ValueSetExpansionCache(this, null, lock);
826    validationCachePath = Utilities.path(cachePath, "validation.cache");
827    try {
828      loadValidationCache();
829    } catch (Exception e) {
830      e.printStackTrace();
831    }
832  }
833
834  protected void loadValidationCache() throws JsonSyntaxException, Exception {
835  }
836
837  @Override
838  public List<ConceptMap> findMapsForSource(String url) throws FHIRException {
839    synchronized (lock) {
840      List<ConceptMap> res = new ArrayList<ConceptMap>();
841      for (ConceptMap map : maps.values())
842        if (((Reference) map.getSource()).getReference().equals(url)) 
843          res.add(map);
844      return res;
845    }
846  }
847
848  private ValidationResult verifyCodeInternal(ValueSet vs, CodeableConcept code) throws Exception {
849    for (Coding c : code.getCoding()) {
850      ValidationResult res = verifyCodeInternal(vs, c.getSystem(), c.getCode(), c.getDisplay());
851      if (res.isOk())
852        return res;
853    }
854    if (code.getCoding().isEmpty())
855      return new ValidationResult(IssueSeverity.ERROR, "None code provided");
856    else
857      return new ValidationResult(IssueSeverity.ERROR, "None of the codes are in the specified value set");
858  }
859
860  private ValidationResult verifyCodeInternal(ValueSet vs, String system, String code, String display) throws Exception {
861    if (vs.hasExpansion())
862      return verifyCodeInExpansion(vs, system, code, display);
863    else {
864      ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs, null);
865      if (vse.getValueset() != null) 
866        return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), false);
867      else
868        return verifyCodeInExpansion(vse.getValueset(), system, code, display);
869    }
870  }
871
872  private ValidationResult verifyCodeInternal(ValueSet vs, String code) throws FileNotFoundException, ETooCostly, IOException, FHIRException {
873    if (vs.hasExpansion())
874      return verifyCodeInExpansion(vs, code);
875    else {
876      ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs, null);
877      if (vse.getValueset() == null)
878        return new ValidationResult(IssueSeverity.ERROR, vse.getError(), vse.getErrorClass());
879      else
880        return verifyCodeInExpansion(vse.getValueset(), code);
881    }
882  }
883
884  private ValidationResult verifyCodeInCodeSystem(CodeSystem cs, String system, String code, String display) throws Exception {
885    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
886    if (cc == null)
887      if (cs.getContent().equals(CodeSystem.CodeSystemContentMode.COMPLETE))
888        return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+cs.getUrl());
889      else if (!cs.getContent().equals(CodeSystem.CodeSystemContentMode.NOTPRESENT))
890        return new ValidationResult(IssueSeverity.WARNING, "Unknown Code "+code+" in partial code list of "+cs.getUrl());
891      else 
892        return verifyCodeExternal(null, new Coding().setSystem(system).setCode(code).setDisplay(display), false);
893    //
894    //        return new ValidationResult(IssueSeverity.WARNING, "A definition was found for "+cs.getUrl()+", but it has no codes in the definition");
895    //      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+cs.getUrl());
896    if (display == null)
897      return new ValidationResult(cc);
898    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
899    if (cc.hasDisplay()) {
900      b.append(cc.getDisplay());
901      if (display.equalsIgnoreCase(cc.getDisplay()))
902        return new ValidationResult(cc);
903    }
904    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
905      b.append(ds.getValue());
906      if (display.equalsIgnoreCase(ds.getValue()))
907        return new ValidationResult(cc);
908    }
909    return new ValidationResult(IssueSeverity.WARNING, "Display Name for "+code+" must be one of '"+b.toString()+"'", cc);
910  }
911
912
913  private ValidationResult verifyCodeInExpansion(ValueSet vs, String system,String code, String display) {
914    ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code, system);
915    if (cc == null)
916      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+vs.getUrl());
917    if (display == null)
918      return new ValidationResult(new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay()));
919    if (cc.hasDisplay()) {
920      if (display.equalsIgnoreCase(cc.getDisplay()))
921        return new ValidationResult(new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay()));
922      return new ValidationResult(IssueSeverity.WARNING, "Display Name for "+code+" must be '"+cc.getDisplay()+"'", new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay()));
923    }
924    return null;
925  }
926
927  private ValidationResult verifyCodeInExpansion(ValueSet vs, String code) throws FHIRException {
928    if (vs.getExpansion().hasExtension("http://hl7.org/fhir/StructureDefinition/valueset-toocostly")) {
929      throw new FHIRException("Unable to validate core - value set is too costly to expand"); 
930    } else {
931      ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code, null);
932      if (cc == null)
933        return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+vs.getUrl());
934      return null;
935    }
936  }
937
938  private ValueSetExpansionContainsComponent findCode(List<ValueSetExpansionContainsComponent> contains, String code, String system) {
939    for (ValueSetExpansionContainsComponent cc : contains) {
940      if (code.equals(cc.getCode()) && (system == null || cc.getSystem().equals(system)))
941        return cc;
942      ValueSetExpansionContainsComponent c = findCode(cc.getContains(), code, system);
943      if (c != null)
944        return c;
945    }
946    return null;
947  }
948
949  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) {
950    for (ConceptDefinitionComponent cc : concept) {
951      if (code.equals(cc.getCode()))
952        return cc;
953      ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code);
954      if (c != null)
955        return c;
956    }
957    return null;
958  }
959
960  public boolean isCanRunWithoutTerminology() {
961    return canRunWithoutTerminology;
962  }
963
964  public void setCanRunWithoutTerminology(boolean canRunWithoutTerminology) {
965    this.canRunWithoutTerminology = canRunWithoutTerminology;
966  }
967
968  public int getExpandCodesLimit() {
969    return expandCodesLimit;
970  }
971
972  public void setExpandCodesLimit(int expandCodesLimit) {
973    this.expandCodesLimit = expandCodesLimit;
974  }
975
976  public void setLogger(ILoggingService logger) {
977    this.logger = logger;
978  }
979
980  public ExpansionProfile getExpansionProfile() {
981    return expProfile;
982  }
983
984  public void setExpansionProfile(ExpansionProfile expProfile) {
985    this.expProfile = expProfile;
986  }
987
988  @Override
989  public boolean isNoTerminologyServer() {
990    return noTerminologyServer;
991  }
992
993  public String getName() {
994    return name;
995  }
996
997  public void setName(String name) {
998    this.name = name;
999  }
1000
1001  @Override
1002  public Set<String> getResourceNamesAsSet() {
1003    Set<String> res = new HashSet<String>();
1004    res.addAll(getResourceNames());
1005    return res;
1006  }
1007
1008  public boolean isAllowLoadingDuplicates() {
1009    return allowLoadingDuplicates;
1010  }
1011
1012  public void setAllowLoadingDuplicates(boolean allowLoadingDuplicates) {
1013    this.allowLoadingDuplicates = allowLoadingDuplicates;
1014  }
1015
1016  @SuppressWarnings("unchecked")
1017  @Override
1018  public <T extends Resource> T fetchResourceWithException(Class<T> class_, String uri) throws FHIRException {
1019       if (class_ == StructureDefinition.class)
1020      uri = ProfileUtilities.sdNs(uri);
1021    synchronized (lock) {
1022
1023      if (uri.startsWith("http:") || uri.startsWith("https:")) {
1024        String version = null;
1025        if (uri.contains("#"))
1026          uri = uri.substring(0, uri.indexOf("#"));
1027        if (class_ == Resource.class || class_ == null) {
1028          if (structures.containsKey(uri))
1029            return (T) structures.get(uri);
1030          if (valueSets.containsKey(uri))
1031            return (T) valueSets.get(uri);
1032          if (codeSystems.containsKey(uri))
1033            return (T) codeSystems.get(uri);
1034          if (operations.containsKey(uri))
1035            return (T) operations.get(uri);
1036          if (searchParameters.containsKey(uri))
1037            return (T) searchParameters.get(uri);
1038          if (maps.containsKey(uri))
1039            return (T) maps.get(uri);
1040          if (transforms.containsKey(uri))
1041            return (T) transforms.get(uri);
1042          if (questionnaires.containsKey(uri))
1043            return (T) questionnaires.get(uri);
1044          return null;      
1045        } else if (class_ == StructureDefinition.class) {
1046          return (T) structures.get(uri);
1047        } else if (class_ == ValueSet.class) {
1048          return (T) valueSets.get(uri);
1049        } else if (class_ == CodeSystem.class) {
1050          return (T) codeSystems.get(uri);
1051        } else if (class_ == ConceptMap.class) {
1052          return (T) maps.get(uri);
1053        } else if (class_ == OperationDefinition.class) {
1054          OperationDefinition od = operations.get(uri);
1055          return (T) od;
1056        } else if (class_ == SearchParameter.class) {
1057          SearchParameter res = searchParameters.get(uri);
1058          if (res == null) {
1059            StringBuilder b = new StringBuilder();
1060            for (String s : searchParameters.keySet()) {
1061              b.append(s);
1062              b.append("\r\n");
1063            }
1064          }
1065          if (res != null)
1066            return (T) res;
1067        }
1068      }
1069      if (class_ == Questionnaire.class)
1070        return (T) questionnaires.get(uri);
1071      if (class_ == null) {
1072        if (uri.matches(Constants.URI_REGEX) && !uri.contains("ValueSet"))
1073          return null;
1074
1075        // it might be a special URL.
1076        if (Utilities.isAbsoluteUrl(uri) || uri.startsWith("ValueSet/")) {
1077          Resource res = findTxValueSet(uri);
1078          if (res != null)
1079            return (T) res;
1080        }
1081        return null;      
1082      }      
1083      throw new FHIRException("not done yet: can't fetch "+uri);
1084    }
1085  }
1086
1087  private Set<String> notCanonical = new HashSet<String>();
1088  
1089  private MetadataResource findTxValueSet(String uri) {
1090    MetadataResource res = expansionCache.getStoredResource(uri);
1091    if (res != null)
1092      return res;
1093    synchronized (lock) {
1094      if (notCanonical.contains(uri))
1095        return null;
1096    }
1097    try {
1098      tlog("get canonical "+uri);
1099      res = txServer.getCanonical(ValueSet.class, uri);
1100    } catch (Exception e) {
1101      synchronized (lock) {
1102        notCanonical.add(uri);
1103      }
1104      return null;
1105    }
1106    if (res != null)
1107      try {
1108        expansionCache.storeResource(res);
1109      } catch (IOException e) {
1110      }
1111    return res;
1112  }
1113
1114  @Override
1115  public Resource fetchResourceById(String type, String uri) {
1116    synchronized (lock) {
1117      String[] parts = uri.split("\\/");
1118      if (!Utilities.noString(type) && parts.length == 1)
1119        return allResourcesById.get(type).get(parts[0]);
1120      if (parts.length >= 2) {
1121        if (!Utilities.noString(type))
1122          if (!type.equals(parts[parts.length-2])) 
1123            throw new Error("Resource type mismatch for "+type+" / "+uri);
1124        return allResourcesById.get(parts[parts.length-2]).get(parts[parts.length-1]);
1125      } else
1126        throw new Error("Unable to process request for resource for "+type+" / "+uri);
1127    }
1128  }
1129
1130  public <T extends Resource> T fetchResource(Class<T> class_, String uri) {
1131    try {
1132      return fetchResourceWithException(class_, uri);
1133    } catch (FHIRException e) {
1134      throw new Error(e);
1135    }
1136  }
1137  
1138  @Override
1139  public <T extends Resource> boolean hasResource(Class<T> class_, String uri) {
1140    try {
1141      return fetchResourceWithException(class_, uri) != null;
1142    } catch (Exception e) {
1143      return false;
1144    }
1145  }
1146
1147  @Override
1148  public ValueSetExpansionOutcome expandVS(ElementDefinitionBindingComponent binding, boolean cacheOk, boolean heirarchical) throws FHIRException {
1149    ValueSet vs = null;
1150    if (binding.hasValueSetCanonicalType()) {
1151      vs = fetchResource(ValueSet.class, binding.getValueSetCanonicalType().getValue());
1152      if (vs == null)
1153        throw new FHIRException("Unable to resolve value Set "+binding.getValueSetCanonicalType().getValue());
1154    } else {
1155      vs = fetchResource(ValueSet.class, binding.getValueSetUriType().asStringValue());
1156      if (vs == null)
1157        throw new FHIRException("Unable to resolve value Set "+binding.getValueSetUriType().asStringValue());
1158    }
1159    return expandVS(vs, cacheOk, heirarchical);
1160  }
1161  
1162
1163  public TranslationServices translator() {
1164    return translator;
1165  }
1166
1167  public void setTranslator(TranslationServices translator) {
1168    this.translator = translator;
1169  }
1170  
1171  public class NullTranslator implements TranslationServices {
1172
1173    @Override
1174    public String translate(String context, String value, String targetLang) {
1175      return value;
1176    }
1177
1178    @Override
1179    public String translate(String context, String value) {
1180      return value;
1181    }
1182
1183    @Override
1184    public String toStr(float value) {
1185      return null;
1186    }
1187
1188    @Override
1189    public String toStr(Date value) {
1190      return null;
1191    }
1192
1193    @Override
1194    public String translateAndFormat(String contest, String lang, String value, Object... args) {
1195      return String.format(value, args);
1196    }
1197
1198    @Override
1199    public Map<String, String> translations(String value) {
1200      // TODO Auto-generated method stub
1201      return null;
1202    }
1203
1204    @Override
1205    public Set<String> listTranslations(String category) {
1206      // TODO Auto-generated method stub
1207      return null;
1208    }
1209
1210  }
1211  
1212  public void reportStatus(JsonObject json) {
1213    synchronized (lock) {
1214      json.addProperty("codeystem-count", codeSystems.size());
1215      json.addProperty("valueset-count", valueSets.size());
1216      json.addProperty("conceptmap-count", maps.size());
1217      json.addProperty("transforms-count", transforms.size());
1218      json.addProperty("structures-count", structures.size());
1219    }
1220  }
1221
1222
1223  public void dropResource(Resource r) throws FHIRException {
1224    dropResource(r.fhirType(), r.getId());   
1225  }
1226
1227  public void dropResource(String fhirType, String id) {
1228    synchronized (lock) {
1229
1230      Map<String, Resource> map = allResourcesById.get(fhirType);
1231      if (map == null) {
1232        map = new HashMap<String, Resource>();
1233        allResourcesById.put(fhirType, map);
1234      }
1235      if (map.containsKey(id))
1236        map.remove(id);
1237
1238      if (fhirType.equals("StructureDefinition"))
1239        dropMetadataResource(structures, id);
1240      else if (fhirType.equals("ValueSet"))
1241        dropMetadataResource(valueSets, id);
1242      else if (fhirType.equals("CodeSystem"))
1243        dropMetadataResource(codeSystems, id);
1244      else if (fhirType.equals("OperationDefinition"))
1245        dropMetadataResource(operations, id);
1246      else if (fhirType.equals("Questionnaire"))
1247        dropMetadataResource(questionnaires, id);
1248      else if (fhirType.equals("ConceptMap"))
1249        dropMetadataResource(maps, id);
1250      else if (fhirType.equals("StructureMap"))
1251        dropMetadataResource(transforms, id);
1252      else if (fhirType.equals("NamingSystem"))
1253        for (int i = systems.size()-1; i >= 0; i--) {
1254          if (systems.get(i).getId().equals(id))
1255            systems.remove(i);
1256        }
1257    }
1258  }
1259
1260  private <T extends MetadataResource> void dropMetadataResource(Map<String, T> map, String id) {
1261    T res = map.get(id);
1262    if (res != null) {
1263      map.remove(id);
1264      if (map.containsKey(res.getUrl()))
1265        map.remove(res.getUrl());
1266      if (res.getVersion() != null)
1267        if (map.containsKey(res.getUrl()+"|"+res.getVersion()))
1268          map.remove(res.getUrl()+"|"+res.getVersion());
1269    }
1270  }
1271
1272  @Override
1273  public List<MetadataResource> allConformanceResources() {
1274    synchronized (lock) {
1275      List<MetadataResource> result = new ArrayList<MetadataResource>();
1276      result.addAll(structures.values());
1277      result.addAll(codeSystems.values());
1278      result.addAll(valueSets.values());
1279      result.addAll(maps.values());
1280      result.addAll(transforms.values());
1281      return result;
1282    }
1283  }
1284  
1285  public void addNonSupportedCodeSystems(String s) {
1286    synchronized (lock) {
1287      nonSupportedCodeSystems.add(s);
1288    }   
1289   }
1290
1291  public String listNonSupportedSystems() {
1292    synchronized (lock) {
1293      String sl = null;
1294      for (String s : nonSupportedCodeSystems)
1295        sl = sl == null ? s : sl + "\r\n" + s;
1296      return sl;
1297    }
1298  }
1299
1300
1301  public int totalCount() {
1302    synchronized (lock) {
1303      return valueSets.size() +  maps.size() + structures.size() + transforms.size();
1304    }
1305  }
1306  
1307  public List<ConceptMap> listMaps() {
1308    List<ConceptMap> m = new ArrayList<ConceptMap>();
1309    synchronized (lock) {
1310      m.addAll(maps.values());    
1311    }
1312    return m;
1313  }
1314  
1315  public List<StructureMap> listTransforms() {
1316    List<StructureMap> m = new ArrayList<StructureMap>();
1317    synchronized (lock) {
1318      m.addAll(transforms.values());    
1319    }
1320    return m;
1321  }
1322  
1323  public StructureMap getTransform(String code) {
1324    synchronized (lock) {
1325      return transforms.get(code);
1326    }
1327  }
1328
1329  public List<StructureDefinition> listStructures() {
1330    List<StructureDefinition> m = new ArrayList<StructureDefinition>();
1331    synchronized (lock) {
1332      m.addAll(structures.values());    
1333    }
1334    return m;
1335  }
1336
1337  public StructureDefinition getStructure(String code) {
1338    synchronized (lock) {
1339      return structures.get(code);
1340    }
1341  }
1342
1343  public void setCache(ValueSetExpansionCache cache) {
1344    synchronized (lock) {
1345      this.expansionCache = cache;
1346    }
1347  }
1348
1349  @Override
1350  public String oid2Uri(String oid) {
1351    synchronized (lock) {
1352      String uri = OIDUtils.getUriForOid(oid);
1353      if (uri != null)
1354        return uri;
1355      for (NamingSystem ns : systems) {
1356        if (hasOid(ns, oid)) {
1357          uri = getUri(ns);
1358          if (uri != null)
1359            return null;
1360        }
1361      }
1362    }
1363    return null;
1364  }
1365  
1366
1367  private String getUri(NamingSystem ns) {
1368    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
1369      if (id.getType() == NamingSystemIdentifierType.URI)
1370        return id.getValue();
1371    }
1372    return null;
1373  }
1374
1375  private boolean hasOid(NamingSystem ns, String oid) {
1376    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
1377      if (id.getType() == NamingSystemIdentifierType.OID && id.getValue().equals(oid))
1378        return true;
1379    }
1380    return false;
1381  }
1382
1383  public void cacheVS(JsonObject json, Map<String, ValidationResult> t) {
1384    synchronized (lock) {
1385      validationCache.put(json.get("url").getAsString(), t);
1386    }
1387  }
1388
1389  public SearchParameter getSearchParameter(String code) {
1390    synchronized (lock) {
1391      return searchParameters.get(code);
1392    }
1393  }
1394
1395  
1396}