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