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.context.TerminologyCache.CacheToken;
019import org.hl7.fhir.r4.formats.JsonParser;
020import org.hl7.fhir.r4.model.BooleanType;
021import org.hl7.fhir.r4.model.Bundle;
022import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
023import org.hl7.fhir.r4.model.CodeSystem;
024import org.hl7.fhir.r4.model.CodeSystem.CodeSystemContentMode;
025import org.hl7.fhir.r4.model.CodeSystem.CodeSystemHierarchyMeaning;
026import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionComponent;
027import org.hl7.fhir.r4.model.CodeSystem.ConceptDefinitionDesignationComponent;
028import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionBindingComponent;
029import org.hl7.fhir.r4.model.NamingSystem.NamingSystemIdentifierType;
030import org.hl7.fhir.r4.model.NamingSystem.NamingSystemUniqueIdComponent;
031import org.hl7.fhir.r4.model.CodeableConcept;
032import org.hl7.fhir.r4.model.Coding;
033import org.hl7.fhir.r4.model.ConceptMap;
034import org.hl7.fhir.r4.model.Constants;
035import org.hl7.fhir.r4.model.ImplementationGuide;
036import org.hl7.fhir.r4.model.MetadataResource;
037import org.hl7.fhir.r4.model.NamingSystem;
038import org.hl7.fhir.r4.model.OperationDefinition;
039import org.hl7.fhir.r4.model.OperationOutcome;
040import org.hl7.fhir.r4.model.Parameters;
041import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
042import org.hl7.fhir.r4.model.PlanDefinition;
043import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind;
044import org.hl7.fhir.r4.model.PrimitiveType;
045import org.hl7.fhir.r4.model.Questionnaire;
046import org.hl7.fhir.r4.model.Reference;
047import org.hl7.fhir.r4.model.Resource;
048import org.hl7.fhir.r4.model.SearchParameter;
049import org.hl7.fhir.r4.model.StringType;
050import org.hl7.fhir.r4.model.StructureDefinition;
051import org.hl7.fhir.r4.model.StructureMap;
052import org.hl7.fhir.r4.model.UriType;
053import org.hl7.fhir.r4.model.ValueSet;
054import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent;
055import org.hl7.fhir.r4.model.ValueSet.ConceptSetFilterComponent;
056import org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent;
057import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent;
058import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
059import org.hl7.fhir.r4.terminologies.ValueSetCheckerSimple;
060import org.hl7.fhir.r4.terminologies.ValueSetExpander.ETooCostly;
061import org.hl7.fhir.r4.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
062import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
063import org.hl7.fhir.r4.terminologies.ValueSetExpanderFactory;
064import org.hl7.fhir.r4.terminologies.ValueSetExpanderSimple;
065import org.hl7.fhir.r4.terminologies.ValueSetExpansionCache;
066import org.hl7.fhir.r4.utils.ToolingExtensions;
067import org.hl7.fhir.r4.utils.client.FHIRToolingClient;
068import org.hl7.fhir.exceptions.DefinitionException;
069import org.hl7.fhir.exceptions.FHIRException;
070import org.hl7.fhir.exceptions.NoTerminologyServiceException;
071import org.hl7.fhir.exceptions.TerminologyServiceException;
072import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
073import org.hl7.fhir.utilities.MimeType;
074import org.hl7.fhir.utilities.OIDUtils;
075import org.hl7.fhir.utilities.TextFile;
076import org.hl7.fhir.utilities.TranslationServices;
077import org.hl7.fhir.utilities.Utilities;
078import org.hl7.fhir.utilities.validation.ValidationMessage;
079import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
080import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
081import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
082
083import com.google.gson.JsonObject;
084import com.google.gson.JsonSyntaxException;
085
086import ca.uhn.fhir.rest.annotation.Metadata;
087
088public abstract class BaseWorkerContext implements IWorkerContext {
089
090  private Object lock = new Object(); // used as a lock for the data that follows
091  
092  private Map<String, Map<String, Resource>> allResourcesById = new HashMap<String, Map<String, Resource>>();
093  // all maps are to the full URI
094  private Map<String, CodeSystem> codeSystems = new HashMap<String, CodeSystem>();
095  private Set<String> nonSupportedCodeSystems = new HashSet<String>();
096  private Map<String, ValueSet> valueSets = new HashMap<String, ValueSet>();
097  private Map<String, ConceptMap> maps = new HashMap<String, ConceptMap>();
098  private Map<String, StructureMap> transforms = new HashMap<String, StructureMap>();
099  private Map<String, StructureDefinition> structures = new HashMap<String, StructureDefinition>();
100  private Map<String, ImplementationGuide> guides = new HashMap<String, ImplementationGuide>();
101  private Map<String, SearchParameter> searchParameters = new HashMap<String, SearchParameter>();
102  private Map<String, Questionnaire> questionnaires = new HashMap<String, Questionnaire>();
103  private Map<String, OperationDefinition> operations = new HashMap<String, OperationDefinition>();
104  private Map<String, PlanDefinition> plans = new HashMap<String, PlanDefinition>();
105  private List<NamingSystem> systems = new ArrayList<NamingSystem>();
106  
107  protected Map<String, Map<String, ValidationResult>> validationCache = new HashMap<String, Map<String,ValidationResult>>();
108  protected String tsServer;
109  protected String name;
110  private boolean allowLoadingDuplicates;
111
112  protected FHIRToolingClient txServer;
113  private Bundle bndCodeSystems;
114  private boolean canRunWithoutTerminology;
115  protected boolean noTerminologyServer;
116  private int expandCodesLimit = 1000;
117  protected ILoggingService logger;
118  protected Parameters expParameters;
119  private TranslationServices translator = new NullTranslator();
120  protected TerminologyCache txCache;
121  
122  public BaseWorkerContext() throws FileNotFoundException, IOException, FHIRException {
123    super();
124    txCache = new TerminologyCache(lock, null);
125  }
126
127  public BaseWorkerContext(Map<String, CodeSystem> codeSystems, Map<String, ValueSet> valueSets, Map<String, ConceptMap> maps, Map<String, StructureDefinition> profiles, Map<String, ImplementationGuide> guides) throws FileNotFoundException, IOException, FHIRException {
128    super();
129    this.codeSystems = codeSystems;
130    this.valueSets = valueSets;
131    this.maps = maps;
132    this.structures = profiles;
133    this.guides = guides;
134    txCache = new TerminologyCache(lock, null);
135  }
136
137  protected void copy(BaseWorkerContext other) {
138    synchronized (other.lock) { // tricky, because you need to lock this as well, but it's really not in use yet 
139      allResourcesById.putAll(other.allResourcesById);
140      translator = other.translator;
141      codeSystems.putAll(other.codeSystems);
142      nonSupportedCodeSystems.addAll(other.nonSupportedCodeSystems);
143      valueSets.putAll(other.valueSets);
144      maps.putAll(other.maps);
145      transforms.putAll(other.transforms);
146      structures.putAll(other.structures);
147      searchParameters.putAll(other.searchParameters);
148      plans.putAll(other.plans);
149      questionnaires.putAll(other.questionnaires);
150      operations.putAll(other.operations);
151      systems.addAll(other.systems);
152      guides.putAll(other.guides);
153
154      allowLoadingDuplicates = other.allowLoadingDuplicates;
155      tsServer = other.tsServer;
156      name = other.name;
157      txServer = other.txServer;
158      bndCodeSystems = other.bndCodeSystems;
159      canRunWithoutTerminology = other.canRunWithoutTerminology;
160      noTerminologyServer = other.noTerminologyServer;
161      if (other.txCache != null)
162        txCache = other.txCache.copy();
163      expandCodesLimit = other.expandCodesLimit;
164      logger = other.logger;
165      expParameters = other.expParameters;
166    }
167  }
168  
169  public void cacheResource(Resource r) throws FHIRException {
170    synchronized (lock) {
171      Map<String, Resource> map = allResourcesById.get(r.fhirType());
172      if (map == null) {
173        map = new HashMap<String, Resource>();
174        allResourcesById.put(r.fhirType(), map);
175      }
176      map.put(r.getId(), r);
177
178      if (r instanceof MetadataResource) {
179        MetadataResource m = (MetadataResource) r;
180        String url = m.getUrl();
181        if (!allowLoadingDuplicates && hasResource(r.getClass(), url))
182          throw new DefinitionException("Duplicate Resource " + url);
183        if (r instanceof StructureDefinition)
184          seeMetadataResource((StructureDefinition) m, structures, false);
185        else if (r instanceof ValueSet)
186          seeMetadataResource((ValueSet) m, valueSets, false);
187        else if (r instanceof CodeSystem)
188          seeMetadataResource((CodeSystem) m, codeSystems, false);
189        else if (r instanceof ImplementationGuide)
190          seeMetadataResource((ImplementationGuide) m, guides, false);
191        else if (r instanceof SearchParameter)
192          seeMetadataResource((SearchParameter) m, searchParameters, false);
193        else if (r instanceof PlanDefinition)
194          seeMetadataResource((PlanDefinition) m, plans, false);
195        else if (r instanceof OperationDefinition)
196          seeMetadataResource((OperationDefinition) m, operations, false);
197        else if (r instanceof Questionnaire)
198          seeMetadataResource((Questionnaire) m, questionnaires, true);
199        else if (r instanceof ConceptMap)
200          seeMetadataResource((ConceptMap) m, maps, false);
201        else if (r instanceof StructureMap)
202          seeMetadataResource((StructureMap) m, transforms, false);
203        else if (r instanceof NamingSystem)
204          systems.add((NamingSystem) r);
205      }
206    }
207  }
208
209  /*
210   *  Compare business versions, returning "true" if the candidate newer version is in fact newer than the oldVersion
211   *  Comparison will work for strictly numeric versions as well as multi-level versions separated by ., -, _, : or space
212   *  Failing that, it will do unicode-based character ordering.
213   *  E.g. 1.5.3 < 1.14.3
214   *       2017-3-10 < 2017-12-7
215   *       A3 < T2
216   */
217  private boolean laterVersion(String newVersion, String oldVersion) {
218    // Compare business versions, retur
219    newVersion = newVersion.trim();
220    oldVersion = oldVersion.trim();
221    if (StringUtils.isNumeric(newVersion) && StringUtils.isNumeric(oldVersion))
222      return Double.parseDouble(newVersion) > Double.parseDouble(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 if (hasDelimiter(newVersion, oldVersion, "_"))
228      return laterDelimitedVersion(newVersion, oldVersion, "\\_");
229    else if (hasDelimiter(newVersion, oldVersion, ":"))
230      return laterDelimitedVersion(newVersion, oldVersion, "\\:");
231    else if (hasDelimiter(newVersion, oldVersion, " "))
232      return laterDelimitedVersion(newVersion, oldVersion, "\\ ");
233    else {
234      return newVersion.compareTo(oldVersion) > 0;
235    }
236  }
237  
238  /*
239   * Returns true if both strings include the delimiter and have the same number of occurrences of it
240   */
241  private boolean hasDelimiter(String s1, String s2, String delimiter) {
242    return s1.contains(delimiter) && s2.contains(delimiter) && s1.split(delimiter).length == s2.split(delimiter).length;
243  }
244
245  private boolean laterDelimitedVersion(String newVersion, String oldVersion, String delimiter) {
246    String[] newParts = newVersion.split(delimiter);
247    String[] oldParts = oldVersion.split(delimiter);
248    for (int i = 0; i < newParts.length; i++) {
249      if (!newParts[i].equals(oldParts[i]))
250        return laterVersion(newParts[i], oldParts[i]);
251    }
252    // This should never happen
253    throw new Error("Delimited versions have exact match for delimiter '"+delimiter+"' : "+ Arrays.asList(newParts)+" vs "+Arrays.asList(oldParts));
254  }
255  
256  protected <T extends MetadataResource> void seeMetadataResource(T r, Map<String, T> map, boolean addId) throws FHIRException {
257    if (addId)
258      map.put(r.getId(), r); // todo: why?
259    if (!map.containsKey(r.getUrl()))
260      map.put(r.getUrl(), r);
261    else {
262      // 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
263      MetadataResource existingResource = (MetadataResource)map.get(r.getUrl());
264      if (r.hasVersion() && existingResource.hasVersion() && !r.getVersion().equals(existingResource.getVersion())) {
265        if (laterVersion(r.getVersion(), existingResource.getVersion())) {
266          map.remove(r.getUrl());
267          map.put(r.getUrl(), r);
268        }
269      } else
270        map.remove(r.getUrl());
271        map.put(r.getUrl(), r);
272//        throw new FHIRException("Multiple declarations of resource with same canonical URL (" + r.getUrl() + ") and version (" + (r.hasVersion() ? r.getVersion() : "" ) + ")");
273    }
274    if (r.hasVersion())
275      map.put(r.getUrl()+"|"+r.getVersion(), r);
276  }  
277
278  @Override
279  public CodeSystem fetchCodeSystem(String system) {
280    synchronized (lock) {
281      return codeSystems.get(system);
282    }
283  } 
284
285  @Override
286  public boolean supportsSystem(String system) throws TerminologyServiceException {
287    synchronized (lock) {
288      if (codeSystems.containsKey(system))
289        return true;
290      else if (nonSupportedCodeSystems.contains(system))
291        return false;
292      else if (system.startsWith("http://example.org") || system.startsWith("http://acme.com") || system.startsWith("http://hl7.org/fhir/valueset-") || system.startsWith("urn:oid:"))
293        return false;
294      else {
295        if (noTerminologyServer)
296          return false;
297        if (bndCodeSystems == null) {
298          try {
299            log("Terminology server: Check for supported code systems for "+system);
300            bndCodeSystems = txServer.fetchFeed(txServer.getAddress()+"/CodeSystem?content-mode=not-present&_summary=true&_count=1000");
301          } catch (Exception e) {
302            if (canRunWithoutTerminology) {
303              noTerminologyServer = true;
304              log("==============!! Running without terminology server !! ==============");
305              if (txServer!=null) {
306                log("txServer = "+txServer.getAddress());
307                log("Error = "+e.getMessage()+"");
308              }
309              log("=====================================================================");
310              return false;
311            } else
312              throw new TerminologyServiceException(e);
313          }
314        }
315        if (bndCodeSystems != null) {
316          for (BundleEntryComponent be : bndCodeSystems.getEntry()) {
317            CodeSystem cs = (CodeSystem) be.getResource();
318//            if (cs == null)
319//              System.out.println("no resource for "+be.getFullUrl());
320            if (cs != null && !codeSystems.containsKey(cs.getUrl())) {
321              codeSystems.put(cs.getUrl(), null);
322            }
323          }
324        }
325        if (codeSystems.containsKey(system))
326          return true;
327      }
328      nonSupportedCodeSystems.add(system);
329      return false;
330    }
331  }
332
333  private void log(String message) {
334    if (logger != null)
335      logger.logMessage(message);
336    else
337      System.out.println(message);
338  }
339
340
341  protected void tlog(String msg) {
342    System.out.println("-tx cache miss: "+msg);
343  }
344
345  // --- expansion support ------------------------------------------------------------------------------------------------------------
346
347  public int getExpandCodesLimit() {
348    return expandCodesLimit;
349  }
350
351  public void setExpandCodesLimit(int expandCodesLimit) {
352    this.expandCodesLimit = expandCodesLimit;
353  }
354
355  @Override
356  public ValueSetExpansionOutcome expandVS(ElementDefinitionBindingComponent binding, boolean cacheOk, boolean heirarchical) throws FHIRException {
357    ValueSet vs = null;
358    vs = fetchResource(ValueSet.class, binding.getValueSet());
359    if (vs == null)
360      throw new FHIRException("Unable to resolve value Set "+binding.getValueSet());
361    return expandVS(vs, cacheOk, heirarchical);
362  }
363  
364  @Override
365  public ValueSetExpansionOutcome expandVS(ConceptSetComponent inc, boolean heirachical) throws TerminologyServiceException {
366    ValueSet vs = new ValueSet();
367    vs.setCompose(new ValueSetComposeComponent());
368    vs.getCompose().getInclude().add(inc);
369    CacheToken cacheToken = txCache.generateExpandToken(vs, heirachical);
370    ValueSetExpansionOutcome res;
371    res = txCache.getExpansion(cacheToken);
372    if (res != null)
373      return res;
374    Parameters p = expParameters.copy(); 
375    p.setParameter("includeDefinition", false);
376    p.setParameter("excludeNested", !heirachical);
377    
378    if (noTerminologyServer)
379      return new ValueSetExpansionOutcome("Error expanding ValueSet: running without terminology services", TerminologyServiceErrorClass.NOSERVICE);
380    Map<String, String> params = new HashMap<String, String>();
381    params.put("_limit", Integer.toString(expandCodesLimit ));
382    params.put("_incomplete", "true");
383    tlog("$expand on "+txCache.summary(vs));
384    try {
385      ValueSet result = txServer.expandValueset(vs, p, params);
386      res = new ValueSetExpansionOutcome(result);  
387    } catch (Exception e) {
388      res = new ValueSetExpansionOutcome(e.getMessage() == null ? e.getClass().getName() : e.getMessage(), TerminologyServiceErrorClass.UNKNOWN);
389    }
390    txCache.cacheExpansion(cacheToken, res, TerminologyCache.PERMANENT);
391    return res;
392
393
394
395  }
396
397  @Override
398  public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean heirarchical) {
399    if (expParameters == null)
400      throw new Error("No Expansion Parameters provided");
401    Parameters p = expParameters.copy(); 
402    return expandVS(vs, cacheOk, heirarchical, p);
403  }
404  
405  public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean heirarchical, Parameters p)  {
406    if (p == null)
407      throw new Error("No Parameters provided to expandVS");
408    if (vs.hasExpansion()) {
409      return new ValueSetExpansionOutcome(vs.copy());
410    }
411    if (!vs.hasUrl())
412      throw new Error("no value set");
413    
414      CacheToken cacheToken = txCache.generateExpandToken(vs, heirarchical);
415      ValueSetExpansionOutcome res;
416      if (cacheOk) {
417        res = txCache.getExpansion(cacheToken);
418        if (res != null)
419          return res;
420      }
421      p.setParameter("includeDefinition", false);
422      p.setParameter("excludeNested", !heirarchical);
423      
424      // ok, first we try to expand locally
425      try {
426        ValueSetExpanderSimple vse = new ValueSetExpanderSimple(this);
427        res = vse.doExpand(vs, p);
428        if (!res.getValueset().hasUrl())
429          throw new Error("no url in expand value set");
430        txCache.cacheExpansion(cacheToken, res, TerminologyCache.TRANSIENT);
431        return res;
432      } catch (Exception e) {
433      }
434      
435      // if that failed, we try to expand on the server
436      if (noTerminologyServer)
437        return new ValueSetExpansionOutcome("Error expanding ValueSet: running without terminology services", TerminologyServiceErrorClass.NOSERVICE);
438      Map<String, String> params = new HashMap<String, String>();
439      params.put("_limit", Integer.toString(expandCodesLimit ));
440      params.put("_incomplete", "true");
441      tlog("$expand on "+txCache.summary(vs));
442      try {
443        ValueSet result = txServer.expandValueset(vs, p, params);
444        if (!result.hasUrl())
445          result.setUrl(vs.getUrl());
446        if (!result.hasUrl())
447          throw new Error("no url in expand value set 2");
448        res = new ValueSetExpansionOutcome(result);  
449      } catch (Exception e) {
450        res = new ValueSetExpansionOutcome(e.getMessage() == null ? e.getClass().getName() : e.getMessage(), TerminologyServiceErrorClass.UNKNOWN);
451      }
452      txCache.cacheExpansion(cacheToken, res, TerminologyCache.PERMANENT);
453      return res;
454  }
455
456
457  private boolean hasTooCostlyExpansion(ValueSet valueset) {
458    return valueset != null && valueset.hasExpansion() && ToolingExtensions.hasExtension(valueset.getExpansion(), "http://hl7.org/fhir/StructureDefinition/valueset-toocostly");
459  }
460  // --- validate code -------------------------------------------------------------------------------
461  
462  @Override
463  public ValidationResult validateCode(String system, String code, String display) {
464    Coding c = new Coding(system, code, display);
465    return validateCode(c, null);
466  }
467
468  @Override
469  public ValidationResult validateCode(String system, String code, String display, ValueSet vs) {
470    Coding c = new Coding(system, code, display);
471    return validateCode(c, vs);
472  }
473
474  @Override
475  public ValidationResult validateCode(String code, ValueSet vs) {
476    Coding c = new Coding(null, code, null);
477    return doValidateCode(c, vs, true);
478  }
479
480  @Override
481  public ValidationResult validateCode(String system, String code, String display, ConceptSetComponent vsi) {
482    Coding c = new Coding(system, code, display);
483    ValueSet vs = new ValueSet();
484    vs.setUrl(Utilities.makeUuidUrn());
485    vs.getCompose().addInclude(vsi);
486    return validateCode(c, vs);
487  }
488
489  @Override
490  public ValidationResult validateCode(Coding code, ValueSet vs) {
491    return doValidateCode(code, vs, false);
492  }
493  
494  public ValidationResult doValidateCode(Coding code, ValueSet vs, boolean implySystem) {
495    CacheToken cacheToken = txCache.generateValidationToken(code, vs);
496    ValidationResult res = txCache.getValidation(cacheToken);
497    if (res != null)
498      return res;
499
500    // ok, first we try to validate locally
501    try {
502      ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(vs, this); 
503      res = vsc.validateCode(code);
504      txCache.cacheValidation(cacheToken, res, TerminologyCache.TRANSIENT);
505      return res;
506    } catch (Exception e) {
507    }
508
509    // if that failed, we try to validate on the server
510    if (noTerminologyServer)
511      return new ValidationResult(IssueSeverity.ERROR,  "Error validating code: running without terminology services", TerminologyServiceErrorClass.NOSERVICE);
512    String csumm = txCache.summary(code);
513    tlog("$validate "+csumm+" for "+ txCache.summary(vs));
514    try {
515      Parameters pIn = new Parameters();
516      pIn.addParameter().setName("coding").setValue(code);
517      if (implySystem)
518        pIn.addParameter().setName("implySystem").setValue(new BooleanType(true));
519      res = validateOnServer(vs, pIn);
520    } catch (Exception e) {
521      res = new ValidationResult(IssueSeverity.ERROR, e.getMessage() == null ? e.getClass().getName() : e.getMessage());
522    }
523    txCache.cacheValidation(cacheToken, res, TerminologyCache.PERMANENT);
524    return res;
525  }
526
527  @Override
528  public ValidationResult validateCode(CodeableConcept code, ValueSet vs) {
529    CacheToken cacheToken = txCache.generateValidationToken(code, vs);
530    ValidationResult res = txCache.getValidation(cacheToken);
531    if (res != null)
532      return res;
533
534    // ok, first we try to validate locally
535    try {
536      ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(vs, this); 
537      res = vsc.validateCode(code);
538      txCache.cacheValidation(cacheToken, res, TerminologyCache.TRANSIENT);
539      return res;
540    } catch (Exception e) {
541    }
542
543    // if that failed, we try to validate on the server
544    if (noTerminologyServer)
545      return new ValidationResult(IssueSeverity.ERROR, "Error validating code: running without terminology services", TerminologyServiceErrorClass.NOSERVICE);
546    tlog("$validate "+txCache.summary(code)+" for "+ txCache.summary(vs));
547    try {
548      Parameters pIn = new Parameters();
549      pIn.addParameter().setName("codeableConcept").setValue(code);
550      res = validateOnServer(vs, pIn);
551    } catch (Exception e) {
552      res = new ValidationResult(IssueSeverity.ERROR, e.getMessage() == null ? e.getClass().getName() : e.getMessage());
553    }
554    txCache.cacheValidation(cacheToken, res, TerminologyCache.PERMANENT);
555    return res;
556  }
557
558  private ValidationResult validateOnServer(ValueSet vs, Parameters pin) {
559    if (vs != null)
560      pin.addParameter().setName("valueSet").setResource(vs);
561    for (ParametersParameterComponent pp : pin.getParameter())
562      if (pp.getName().equals("profile"))
563        throw new Error("Can only specify profile in the context");
564    if (expParameters == null)
565      throw new Error("No ExpansionProfile provided");
566    pin.addParameter().setName("profile").setResource(expParameters);
567    Parameters pOut;
568    if (vs == null)
569      pOut = txServer.operateType(CodeSystem.class, "validate-code", pin);
570    else
571      pOut = txServer.operateType(ValueSet.class, "validate-code", pin);
572    boolean ok = false;
573    String message = "No Message returned";
574    String display = null;
575    TerminologyServiceErrorClass err = TerminologyServiceErrorClass.UNKNOWN;
576    for (ParametersParameterComponent p : pOut.getParameter()) {
577      if (p.getName().equals("result"))
578        ok = ((BooleanType) p.getValue()).getValue().booleanValue();
579      else if (p.getName().equals("message"))
580        message = ((StringType) p.getValue()).getValue();
581      else if (p.getName().equals("display"))
582        display = ((StringType) p.getValue()).getValue();
583      else if (p.getName().equals("cause")) {
584        try {
585          IssueType it = IssueType.fromCode(((StringType) p.getValue()).getValue());
586          if (it == IssueType.UNKNOWN)
587            err = TerminologyServiceErrorClass.UNKNOWN;
588          else if (it == IssueType.NOTSUPPORTED)
589            err = TerminologyServiceErrorClass.VALUESET_UNSUPPORTED;
590        } catch (FHIRException e) {
591        }
592      }
593    }
594    if (!ok)
595      return new ValidationResult(IssueSeverity.ERROR, message, err);
596    else if (message != null && !message.equals("No Message returned")) 
597      return new ValidationResult(IssueSeverity.WARNING, message, new ConceptDefinitionComponent().setDisplay(display));
598    else if (display != null)
599      return new ValidationResult(new ConceptDefinitionComponent().setDisplay(display));
600    else
601      return new ValidationResult(new ConceptDefinitionComponent());
602  }
603
604  // --------------------------------------------------------------------------------------------------------------------------------------------------------
605  
606  public void initTS(String cachePath) throws Exception {
607    txCache = new TerminologyCache(lock, cachePath);
608  }
609
610  @Override
611  public List<ConceptMap> findMapsForSource(String url) throws FHIRException {
612    synchronized (lock) {
613      List<ConceptMap> res = new ArrayList<ConceptMap>();
614      for (ConceptMap map : maps.values())
615        if (((Reference) map.getSource()).getReference().equals(url)) 
616          res.add(map);
617      return res;
618    }
619  }
620
621  public boolean isCanRunWithoutTerminology() {
622    return canRunWithoutTerminology;
623  }
624
625  public void setCanRunWithoutTerminology(boolean canRunWithoutTerminology) {
626    this.canRunWithoutTerminology = canRunWithoutTerminology;
627  }
628
629  public void setLogger(ILoggingService logger) {
630    this.logger = logger;
631  }
632
633  public Parameters getExpansionParameters() {
634    return expParameters;
635  }
636
637  public void setExpansionProfile(Parameters expParameters) {
638    this.expParameters = expParameters;
639  }
640
641  @Override
642  public boolean isNoTerminologyServer() {
643    return noTerminologyServer;
644  }
645
646  public String getName() {
647    return name;
648  }
649
650  public void setName(String name) {
651    this.name = name;
652  }
653
654  @Override
655  public Set<String> getResourceNamesAsSet() {
656    Set<String> res = new HashSet<String>();
657    res.addAll(getResourceNames());
658    return res;
659  }
660
661  public boolean isAllowLoadingDuplicates() {
662    return allowLoadingDuplicates;
663  }
664
665  public void setAllowLoadingDuplicates(boolean allowLoadingDuplicates) {
666    this.allowLoadingDuplicates = allowLoadingDuplicates;
667  }
668
669  @SuppressWarnings("unchecked")
670  @Override
671  public <T extends Resource> T fetchResourceWithException(Class<T> class_, String uri) throws FHIRException {
672       if (class_ == StructureDefinition.class)
673      uri = ProfileUtilities.sdNs(uri, getOverrideVersionNs());
674    synchronized (lock) {
675
676      if (uri.startsWith("http:") || uri.startsWith("https:")) {
677        String version = null;
678        if (uri.contains("#"))
679          uri = uri.substring(0, uri.indexOf("#"));
680        if (class_ == Resource.class || class_ == null) {
681          if (structures.containsKey(uri))
682            return (T) structures.get(uri);
683          if (guides.containsKey(uri))
684            return (T) guides.get(uri);
685          if (valueSets.containsKey(uri))
686            return (T) valueSets.get(uri);
687          if (codeSystems.containsKey(uri))
688            return (T) codeSystems.get(uri);
689          if (operations.containsKey(uri))
690            return (T) operations.get(uri);
691          if (searchParameters.containsKey(uri))
692            return (T) searchParameters.get(uri);
693          if (plans.containsKey(uri))
694            return (T) plans.get(uri);
695          if (maps.containsKey(uri))
696            return (T) maps.get(uri);
697          if (transforms.containsKey(uri))
698            return (T) transforms.get(uri);
699          if (questionnaires.containsKey(uri))
700            return (T) questionnaires.get(uri);
701          return null;      
702        } else if (class_ == ImplementationGuide.class) {
703          return (T) guides.get(uri);
704        } else if (class_ == StructureDefinition.class) {
705          return (T) structures.get(uri);
706        } else if (class_ == ValueSet.class) {
707          return (T) valueSets.get(uri);
708        } else if (class_ == CodeSystem.class) {
709          return (T) codeSystems.get(uri);
710        } else if (class_ == ConceptMap.class) {
711          return (T) maps.get(uri);
712        } else if (class_ == PlanDefinition.class) {
713          return (T) plans.get(uri);
714        } else if (class_ == OperationDefinition.class) {
715          OperationDefinition od = operations.get(uri);
716          return (T) od;
717        } else if (class_ == SearchParameter.class) {
718          SearchParameter res = searchParameters.get(uri);
719          if (res == null) {
720            StringBuilder b = new StringBuilder();
721            for (String s : searchParameters.keySet()) {
722              b.append(s);
723              b.append("\r\n");
724            }
725          }
726          if (res != null)
727            return (T) res;
728        }
729      }
730      if (class_ == Questionnaire.class)
731        return (T) questionnaires.get(uri);
732      if (class_ == null) {
733        if (uri.matches(Constants.URI_REGEX) && !uri.contains("ValueSet"))
734          return null;
735
736        // it might be a special URL.
737        if (Utilities.isAbsoluteUrl(uri) || uri.startsWith("ValueSet/")) {
738          Resource res = null; // findTxValueSet(uri);
739          if (res != null)
740            return (T) res;
741        }
742        return null;      
743      }      
744      throw new FHIRException("not done yet: can't fetch "+uri);
745    }
746  }
747
748  private Set<String> notCanonical = new HashSet<String>();
749
750  private String overrideVersionNs;
751  
752//  private MetadataResource findTxValueSet(String uri) {
753//    MetadataResource res = expansionCache.getStoredResource(uri);
754//    if (res != null)
755//      return res;
756//    synchronized (lock) {
757//      if (notCanonical.contains(uri))
758//        return null;
759//    }
760//    try {
761//      tlog("get canonical "+uri);
762//      res = txServer.getCanonical(ValueSet.class, uri);
763//    } catch (Exception e) {
764//      synchronized (lock) {
765//        notCanonical.add(uri);
766//      }
767//      return null;
768//    }
769//    if (res != null)
770//      try {
771//        expansionCache.storeResource(res);
772//      } catch (IOException e) {
773//      }
774//    return res;
775//  }
776
777  @Override
778  public Resource fetchResourceById(String type, String uri) {
779    synchronized (lock) {
780      String[] parts = uri.split("\\/");
781      if (!Utilities.noString(type) && parts.length == 1)
782        return allResourcesById.get(type).get(parts[0]);
783      if (parts.length >= 2) {
784        if (!Utilities.noString(type))
785          if (!type.equals(parts[parts.length-2])) 
786            throw new Error("Resource type mismatch for "+type+" / "+uri);
787        return allResourcesById.get(parts[parts.length-2]).get(parts[parts.length-1]);
788      } else
789        throw new Error("Unable to process request for resource for "+type+" / "+uri);
790    }
791  }
792
793  public <T extends Resource> T fetchResource(Class<T> class_, String uri) {
794    try {
795      return fetchResourceWithException(class_, uri);
796    } catch (FHIRException e) {
797      throw new Error(e);
798    }
799  }
800  
801  @Override
802  public <T extends Resource> boolean hasResource(Class<T> class_, String uri) {
803    try {
804      return fetchResourceWithException(class_, uri) != null;
805    } catch (Exception e) {
806      return false;
807    }
808  }
809
810
811  public TranslationServices translator() {
812    return translator;
813  }
814
815  public void setTranslator(TranslationServices translator) {
816    this.translator = translator;
817  }
818  
819  public class NullTranslator implements TranslationServices {
820
821    @Override
822    public String translate(String context, String value, String targetLang) {
823      return value;
824    }
825
826    @Override
827    public String translate(String context, String value) {
828      return value;
829    }
830
831    @Override
832    public String toStr(float value) {
833      return null;
834    }
835
836    @Override
837    public String toStr(Date value) {
838      return null;
839    }
840
841    @Override
842    public String translateAndFormat(String contest, String lang, String value, Object... args) {
843      return String.format(value, args);
844    }
845
846    @Override
847    public Map<String, String> translations(String value) {
848      // TODO Auto-generated method stub
849      return null;
850    }
851
852    @Override
853    public Set<String> listTranslations(String category) {
854      // TODO Auto-generated method stub
855      return null;
856    }
857
858  }
859  
860  public void reportStatus(JsonObject json) {
861    synchronized (lock) {
862      json.addProperty("codeystem-count", codeSystems.size());
863      json.addProperty("valueset-count", valueSets.size());
864      json.addProperty("conceptmap-count", maps.size());
865      json.addProperty("transforms-count", transforms.size());
866      json.addProperty("structures-count", structures.size());
867      json.addProperty("guides-count", guides.size());
868    }
869  }
870
871
872  public void dropResource(Resource r) throws FHIRException {
873    dropResource(r.fhirType(), r.getId());   
874  }
875
876  public void dropResource(String fhirType, String id) {
877    synchronized (lock) {
878
879      Map<String, Resource> map = allResourcesById.get(fhirType);
880      if (map == null) {
881        map = new HashMap<String, Resource>();
882        allResourcesById.put(fhirType, map);
883      }
884      if (map.containsKey(id))
885        map.remove(id);
886
887      if (fhirType.equals("StructureDefinition"))
888        dropMetadataResource(structures, id);
889      else if (fhirType.equals("ImplementationGuide"))
890        dropMetadataResource(guides, id);
891      else if (fhirType.equals("ValueSet"))
892        dropMetadataResource(valueSets, id);
893      else if (fhirType.equals("CodeSystem"))
894        dropMetadataResource(codeSystems, id);
895      else if (fhirType.equals("OperationDefinition"))
896        dropMetadataResource(operations, id);
897      else if (fhirType.equals("Questionnaire"))
898        dropMetadataResource(questionnaires, id);
899      else if (fhirType.equals("ConceptMap"))
900        dropMetadataResource(maps, id);
901      else if (fhirType.equals("StructureMap"))
902        dropMetadataResource(transforms, id);
903      else if (fhirType.equals("NamingSystem"))
904        for (int i = systems.size()-1; i >= 0; i--) {
905          if (systems.get(i).getId().equals(id))
906            systems.remove(i);
907        }
908    }
909  }
910
911  private <T extends MetadataResource> void dropMetadataResource(Map<String, T> map, String id) {
912    T res = map.get(id);
913    if (res != null) {
914      map.remove(id);
915      if (map.containsKey(res.getUrl()))
916        map.remove(res.getUrl());
917      if (res.getVersion() != null)
918        if (map.containsKey(res.getUrl()+"|"+res.getVersion()))
919          map.remove(res.getUrl()+"|"+res.getVersion());
920    }
921  }
922
923  @Override
924  public List<MetadataResource> allConformanceResources() {
925    synchronized (lock) {
926      List<MetadataResource> result = new ArrayList<MetadataResource>();
927      result.addAll(structures.values());
928      result.addAll(guides.values());
929      result.addAll(codeSystems.values());
930      result.addAll(valueSets.values());
931      result.addAll(maps.values());
932      result.addAll(transforms.values());
933      result.addAll(plans.values());
934      return result;
935    }
936  }
937  
938  public void addNonSupportedCodeSystems(String s) {
939    synchronized (lock) {
940      nonSupportedCodeSystems.add(s);
941    }   
942   }
943
944  public String listNonSupportedSystems() {
945    synchronized (lock) {
946      String sl = null;
947      for (String s : nonSupportedCodeSystems)
948        sl = sl == null ? s : sl + "\r\n" + s;
949      return sl;
950    }
951  }
952
953
954  public int totalCount() {
955    synchronized (lock) {
956      return valueSets.size() +  maps.size() + structures.size() + transforms.size();
957    }
958  }
959  
960  public List<ConceptMap> listMaps() {
961    List<ConceptMap> m = new ArrayList<ConceptMap>();
962    synchronized (lock) {
963      m.addAll(maps.values());    
964    }
965    return m;
966  }
967  
968  public List<StructureMap> listTransforms() {
969    List<StructureMap> m = new ArrayList<StructureMap>();
970    synchronized (lock) {
971      m.addAll(transforms.values());    
972    }
973    return m;
974  }
975  
976  public StructureMap getTransform(String code) {
977    synchronized (lock) {
978      return transforms.get(code);
979    }
980  }
981
982  public List<StructureDefinition> listStructures() {
983    List<StructureDefinition> m = new ArrayList<StructureDefinition>();
984    synchronized (lock) {
985      m.addAll(structures.values());    
986    }
987    return m;
988  }
989
990  public StructureDefinition getStructure(String code) {
991    synchronized (lock) {
992      return structures.get(code);
993    }
994  }
995
996  @Override
997  public String oid2Uri(String oid) {
998    synchronized (lock) {
999      String uri = OIDUtils.getUriForOid(oid);
1000      if (uri != null)
1001        return uri;
1002      for (NamingSystem ns : systems) {
1003        if (hasOid(ns, oid)) {
1004          uri = getUri(ns);
1005          if (uri != null)
1006            return null;
1007        }
1008      }
1009    }
1010    return null;
1011  }
1012  
1013
1014  private String getUri(NamingSystem ns) {
1015    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
1016      if (id.getType() == NamingSystemIdentifierType.URI)
1017        return id.getValue();
1018    }
1019    return null;
1020  }
1021
1022  private boolean hasOid(NamingSystem ns, String oid) {
1023    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
1024      if (id.getType() == NamingSystemIdentifierType.OID && id.getValue().equals(oid))
1025        return true;
1026    }
1027    return false;
1028  }
1029
1030  public void cacheVS(JsonObject json, Map<String, ValidationResult> t) {
1031    synchronized (lock) {
1032      validationCache.put(json.get("url").getAsString(), t);
1033    }
1034  }
1035
1036  public SearchParameter getSearchParameter(String code) {
1037    synchronized (lock) {
1038      return searchParameters.get(code);
1039    }
1040  }
1041
1042  @Override
1043  public String getOverrideVersionNs() {
1044    return overrideVersionNs;
1045  }
1046
1047  @Override
1048  public void setOverrideVersionNs(String value) {
1049    overrideVersionNs = value;
1050  }
1051
1052  @Override
1053  public ILoggingService getLogger() {
1054    return logger;
1055  }
1056
1057  @Override
1058  public StructureDefinition fetchTypeDefinition(String typeName) {
1059    if (Utilities.isAbsoluteUrl(typeName))
1060      return fetchResource(StructureDefinition.class, typeName);
1061    else
1062      return fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+typeName);
1063  }
1064
1065  
1066}