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