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      for (String s : options.getLanguages()) {
538        pIn.addParameter("displayLanguage", s);
539      }
540    }
541  }
542
543  @Override
544  public ValidationResult validateCode(ValidationOptions options, CodeableConcept code, ValueSet vs) {
545    CacheToken cacheToken = txCache.generateValidationToken(options, code, vs);
546    ValidationResult res = txCache.getValidation(cacheToken);
547    if (res != null)
548      return res;
549
550    // ok, first we try to validate locally
551    try {
552      ValueSetCheckerSimple vsc = new ValueSetCheckerSimple(options, vs, this); 
553      res = vsc.validateCode(code);
554      txCache.cacheValidation(cacheToken, res, TerminologyCache.TRANSIENT);
555      return res;
556    } catch (Exception e) {
557    }
558
559    // if that failed, we try to validate on the server
560    if (noTerminologyServer)
561      return new ValidationResult(IssueSeverity.ERROR, "Error validating code: running without terminology services", TerminologyServiceErrorClass.NOSERVICE);
562    tlog("$validate "+txCache.summary(code)+" for "+ txCache.summary(vs));
563    try {
564      Parameters pIn = new Parameters();
565      pIn.addParameter().setName("codeableConcept").setValue(code);
566      if (options != null)
567        setTerminologyOptions(options, pIn);
568      res = validateOnServer(vs, pIn);
569    } catch (Exception e) {
570      res = new ValidationResult(IssueSeverity.ERROR, e.getMessage() == null ? e.getClass().getName() : e.getMessage()).setTxLink(txLog.getLastId());
571    }
572    txCache.cacheValidation(cacheToken, res, TerminologyCache.PERMANENT);
573    return res;
574  }
575
576  private ValidationResult validateOnServer(ValueSet vs, Parameters pin) throws FHIRException {
577    if (vs != null)
578      pin.addParameter().setName("valueSet").setResource(vs);
579    for (ParametersParameterComponent pp : pin.getParameter())
580      if (pp.getName().equals("profile"))
581        throw new Error("Can only specify profile in the context");
582    if (expParameters == null)
583      throw new Error("No ExpansionProfile provided");
584    pin.addParameter().setName("profile").setResource(expParameters);
585    txLog.clearLastId();
586    Parameters pOut;
587    if (vs == null)
588      pOut = txClient.validateCS(pin);
589    else
590      pOut = txClient.validateVS(pin);
591    boolean ok = false;
592    String message = "No Message returned";
593    String display = null;
594    TerminologyServiceErrorClass err = TerminologyServiceErrorClass.UNKNOWN;
595    for (ParametersParameterComponent p : pOut.getParameter()) {
596      if (p.getName().equals("result"))
597        ok = ((BooleanType) p.getValue()).getValue().booleanValue();
598      else if (p.getName().equals("message"))
599        message = ((StringType) p.getValue()).getValue();
600      else if (p.getName().equals("display"))
601        display = ((StringType) p.getValue()).getValue();
602      else if (p.getName().equals("cause")) {
603        try {
604          IssueType it = IssueType.fromCode(((StringType) p.getValue()).getValue());
605          if (it == IssueType.UNKNOWN)
606            err = TerminologyServiceErrorClass.UNKNOWN;
607          else if (it == IssueType.NOTSUPPORTED)
608            err = TerminologyServiceErrorClass.VALUESET_UNSUPPORTED;
609        } catch (FHIRException e) {
610        }
611      }
612    }
613    if (!ok)
614      return new ValidationResult(IssueSeverity.ERROR, message, err).setTxLink(txLog.getLastId()).setTxLink(txLog.getLastId());
615    else if (message != null && !message.equals("No Message returned")) 
616      return new ValidationResult(IssueSeverity.WARNING, message, new ConceptDefinitionComponent().setDisplay(display)).setTxLink(txLog.getLastId()).setTxLink(txLog.getLastId());
617    else if (display != null)
618      return new ValidationResult(new ConceptDefinitionComponent().setDisplay(display)).setTxLink(txLog.getLastId()).setTxLink(txLog.getLastId());
619    else
620      return new ValidationResult(new ConceptDefinitionComponent()).setTxLink(txLog.getLastId()).setTxLink(txLog.getLastId());
621  }
622
623  // --------------------------------------------------------------------------------------------------------------------------------------------------------
624  
625  public void initTS(String cachePath) throws Exception {
626    txCache = new TerminologyCache(lock, cachePath);
627  }
628
629  @Override
630  public List<ConceptMap> findMapsForSource(String url) throws FHIRException {
631    synchronized (lock) {
632      List<ConceptMap> res = new ArrayList<ConceptMap>();
633      for (ConceptMap map : maps.values())
634        if (((Reference) map.getSource()).getReference().equals(url)) 
635          res.add(map);
636      return res;
637    }
638  }
639
640  public boolean isCanRunWithoutTerminology() {
641    return canRunWithoutTerminology;
642  }
643
644  public void setCanRunWithoutTerminology(boolean canRunWithoutTerminology) {
645    this.canRunWithoutTerminology = canRunWithoutTerminology;
646  }
647
648  public void setLogger(ILoggingService logger) {
649    this.logger = logger;
650  }
651
652  public Parameters getExpansionParameters() {
653    return expParameters;
654  }
655
656  public void setExpansionProfile(Parameters expParameters) {
657    this.expParameters = expParameters;
658  }
659
660  @Override
661  public boolean isNoTerminologyServer() {
662    return noTerminologyServer;
663  }
664
665  public String getName() {
666    return name;
667  }
668
669  public void setName(String name) {
670    this.name = name;
671  }
672
673  @Override
674  public Set<String> getResourceNamesAsSet() {
675    Set<String> res = new HashSet<String>();
676    res.addAll(getResourceNames());
677    return res;
678  }
679
680  public boolean isAllowLoadingDuplicates() {
681    return allowLoadingDuplicates;
682  }
683
684  public void setAllowLoadingDuplicates(boolean allowLoadingDuplicates) {
685    this.allowLoadingDuplicates = allowLoadingDuplicates;
686  }
687
688  @SuppressWarnings("unchecked")
689  @Override
690  public <T extends Resource> T fetchResourceWithException(Class<T> class_, String uri) throws FHIRException {
691       if (class_ == StructureDefinition.class)
692      uri = ProfileUtilities.sdNs(uri, getOverrideVersionNs());
693    synchronized (lock) {
694
695      if (uri.startsWith("http:") || uri.startsWith("https:")) {
696        String version = null;
697        if (uri.contains("|")) {
698          version = uri.substring(uri.lastIndexOf("|")+1);
699          uri = uri.substring(0, uri.lastIndexOf("|"));
700        }
701        if (uri.contains("#"))
702          uri = uri.substring(0, uri.indexOf("#"));
703        if (class_ == Resource.class || class_ == null) {
704          if (structures.containsKey(uri))
705            return (T) structures.get(uri);
706          if (guides.containsKey(uri))
707            return (T) guides.get(uri);
708          if (capstmts.containsKey(uri))
709            return (T) capstmts.get(uri);
710          if (valueSets.containsKey(uri))
711            return (T) valueSets.get(uri);
712          if (codeSystems.containsKey(uri))
713            return (T) codeSystems.get(uri);
714          if (operations.containsKey(uri))
715            return (T) operations.get(uri);
716          if (searchParameters.containsKey(uri))
717            return (T) searchParameters.get(uri);
718          if (plans.containsKey(uri))
719            return (T) plans.get(uri);
720          if (maps.containsKey(uri))
721            return (T) maps.get(uri);
722          if (transforms.containsKey(uri))
723            return (T) transforms.get(uri);
724          if (questionnaires.containsKey(uri))
725            return (T) questionnaires.get(uri);
726          for (Map<String, Resource> rt : allResourcesById.values()) {
727            for (Resource r : rt.values()) {
728              if (r instanceof MetadataResource) {
729                MetadataResource mr = (MetadataResource) r;
730                if (uri.equals(mr.getUrl()))
731                  return (T) mr;
732              }
733            }            
734          }
735          return null;      
736        } else if (class_ == ImplementationGuide.class) {
737          return (T) guides.get(uri);
738        } else if (class_ == CapabilityStatement.class) {
739          return (T) capstmts.get(uri);
740        } else if (class_ == StructureDefinition.class) {
741          return (T) structures.get(uri);
742        } else if (class_ == StructureMap.class) {
743          return (T) transforms.get(uri);
744        } else if (class_ == ValueSet.class) {
745          if (valueSets.containsKey(uri+"|"+version))
746            return (T) valueSets.get(uri+"|"+version);
747          else
748          return (T) valueSets.get(uri);
749        } else if (class_ == CodeSystem.class) {
750          if (codeSystems.containsKey(uri+"|"+version))
751            return (T) codeSystems.get(uri+"|"+version);
752          else
753          return (T) codeSystems.get(uri);
754        } else if (class_ == ConceptMap.class) {
755          return (T) maps.get(uri);
756        } else if (class_ == PlanDefinition.class) {
757          return (T) plans.get(uri);
758        } else if (class_ == OperationDefinition.class) {
759          OperationDefinition od = operations.get(uri);
760          return (T) od;
761        } else if (class_ == SearchParameter.class) {
762          SearchParameter res = searchParameters.get(uri);
763          if (res == null) {
764            StringBuilder b = new StringBuilder();
765            for (String s : searchParameters.keySet()) {
766              b.append(s);
767              b.append("\r\n");
768            }
769          }
770            return (T) res;
771        }
772      }
773      if (class_ == CodeSystem.class && codeSystems.containsKey(uri))
774        return (T) codeSystems.get(uri);
775      
776      if (class_ == Questionnaire.class)
777        return (T) questionnaires.get(uri);
778      if (class_ == null) {
779        if (uri.matches(Constants.URI_REGEX) && !uri.contains("ValueSet"))
780          return null;
781
782        // it might be a special URL.
783        if (Utilities.isAbsoluteUrl(uri) || uri.startsWith("ValueSet/")) {
784          Resource res = null; // findTxValueSet(uri);
785          if (res != null)
786            return (T) res;
787        }
788        return null;      
789      }    
790      if (supportedCodeSystems.contains(uri))
791        return null;
792      throw new FHIRException("not done yet: can't fetch "+uri);
793    }
794  }
795
796  private Set<String> notCanonical = new HashSet<String>();
797
798  private String overrideVersionNs;
799  
800//  private MetadataResource findTxValueSet(String uri) {
801//    MetadataResource res = expansionCache.getStoredResource(uri);
802//    if (res != null)
803//      return res;
804//    synchronized (lock) {
805//      if (notCanonical.contains(uri))
806//        return null;
807//    }
808//    try {
809//      tlog("get canonical "+uri);
810//      res = txServer.getCanonical(ValueSet.class, uri);
811//    } catch (Exception e) {
812//      synchronized (lock) {
813//        notCanonical.add(uri);
814//      }
815//      return null;
816//    }
817//    if (res != null)
818//      try {
819//        expansionCache.storeResource(res);
820//      } catch (IOException e) {
821//      }
822//    return res;
823//  }
824
825  @Override
826  public Resource fetchResourceById(String type, String uri) {
827    synchronized (lock) {
828      String[] parts = uri.split("\\/");
829      if (!Utilities.noString(type) && parts.length == 1) {
830        if (allResourcesById.containsKey(type))
831        return allResourcesById.get(type).get(parts[0]);
832        else
833          return null;
834      }
835      if (parts.length >= 2) {
836        if (!Utilities.noString(type))
837          if (!type.equals(parts[parts.length-2])) 
838            throw new Error("Resource type mismatch for "+type+" / "+uri);
839        return allResourcesById.get(parts[parts.length-2]).get(parts[parts.length-1]);
840      } else
841        throw new Error("Unable to process request for resource for "+type+" / "+uri);
842    }
843  }
844
845  public <T extends Resource> T fetchResource(Class<T> class_, String uri) {
846    try {
847      return fetchResourceWithException(class_, uri);
848    } catch (FHIRException e) {
849      throw new Error(e);
850    }
851  }
852  
853  @Override
854  public <T extends Resource> boolean hasResource(Class<T> class_, String uri) {
855    try {
856      return fetchResourceWithException(class_, uri) != null;
857    } catch (Exception e) {
858      return false;
859    }
860  }
861
862
863  public TranslationServices translator() {
864    return translator;
865  }
866
867  public void setTranslator(TranslationServices translator) {
868    this.translator = translator;
869  }
870  
871  public class NullTranslator implements TranslationServices {
872
873    @Override
874    public String translate(String context, String value, String targetLang) {
875      return value;
876    }
877
878    @Override
879    public String translate(String context, String value) {
880      return value;
881    }
882
883    @Override
884    public String toStr(float value) {
885      return null;
886    }
887
888    @Override
889    public String toStr(Date value) {
890      return null;
891    }
892
893    @Override
894    public String translateAndFormat(String contest, String lang, String value, Object... args) {
895      return String.format(value, args);
896    }
897
898    @Override
899    public Map<String, String> translations(String value) {
900      // TODO Auto-generated method stub
901      return null;
902    }
903
904    @Override
905    public Set<String> listTranslations(String category) {
906      // TODO Auto-generated method stub
907      return null;
908    }
909
910  }
911  
912  public void reportStatus(JsonObject json) {
913    synchronized (lock) {
914      json.addProperty("codeystem-count", codeSystems.size());
915      json.addProperty("valueset-count", valueSets.size());
916      json.addProperty("conceptmap-count", maps.size());
917      json.addProperty("transforms-count", transforms.size());
918      json.addProperty("structures-count", structures.size());
919      json.addProperty("guides-count", guides.size());
920      json.addProperty("statements-count", capstmts.size());
921    }
922  }
923
924
925  public void dropResource(Resource r) throws FHIRException {
926    dropResource(r.fhirType(), r.getId());   
927  }
928
929  public void dropResource(String fhirType, String id) {
930    synchronized (lock) {
931
932      Map<String, Resource> map = allResourcesById.get(fhirType);
933      if (map == null) {
934        map = new HashMap<String, Resource>();
935        allResourcesById.put(fhirType, map);
936      }
937      if (map.containsKey(id))
938        map.remove(id);
939
940      if (fhirType.equals("StructureDefinition"))
941        dropMetadataResource(structures, id);
942      else if (fhirType.equals("ImplementationGuide"))
943        dropMetadataResource(guides, id);
944      else if (fhirType.equals("CapabilityStatement"))
945        dropMetadataResource(capstmts, id);
946      else if (fhirType.equals("ValueSet"))
947        dropMetadataResource(valueSets, id);
948      else if (fhirType.equals("CodeSystem"))
949        dropMetadataResource(codeSystems, id);
950      else if (fhirType.equals("OperationDefinition"))
951        dropMetadataResource(operations, id);
952      else if (fhirType.equals("Questionnaire"))
953        dropMetadataResource(questionnaires, id);
954      else if (fhirType.equals("ConceptMap"))
955        dropMetadataResource(maps, id);
956      else if (fhirType.equals("StructureMap"))
957        dropMetadataResource(transforms, id);
958      else if (fhirType.equals("NamingSystem"))
959        for (int i = systems.size()-1; i >= 0; i--) {
960          if (systems.get(i).getId().equals(id))
961            systems.remove(i);
962        }
963    }
964  }
965
966  private <T extends MetadataResource> void dropMetadataResource(Map<String, T> map, String id) {
967    T res = map.get(id);
968    if (res != null) {
969      map.remove(id);
970      if (map.containsKey(res.getUrl()))
971        map.remove(res.getUrl());
972      if (res.getVersion() != null)
973        if (map.containsKey(res.getUrl()+"|"+res.getVersion()))
974          map.remove(res.getUrl()+"|"+res.getVersion());
975    }
976  }
977
978  @Override
979  public List<MetadataResource> allConformanceResources() {
980    synchronized (lock) {
981      List<MetadataResource> result = new ArrayList<MetadataResource>();
982      result.addAll(structures.values());
983      result.addAll(guides.values());
984      result.addAll(capstmts.values());
985      result.addAll(codeSystems.values());
986      result.addAll(valueSets.values());
987      result.addAll(maps.values());
988      result.addAll(transforms.values());
989      result.addAll(plans.values());
990      result.addAll(questionnaires.values());
991      return result;
992    }
993  }
994  
995  public String listSupportedSystems() {
996    synchronized (lock) {
997      String sl = null;
998      for (String s : supportedCodeSystems)
999        sl = sl == null ? s : sl + "\r\n" + s;
1000      return sl;
1001    }
1002  }
1003
1004
1005  public int totalCount() {
1006    synchronized (lock) {
1007      return valueSets.size() +  maps.size() + structures.size() + transforms.size();
1008    }
1009  }
1010  
1011  public List<ConceptMap> listMaps() {
1012    List<ConceptMap> m = new ArrayList<ConceptMap>();
1013    synchronized (lock) {
1014      m.addAll(maps.values());    
1015    }
1016    return m;
1017  }
1018  
1019  public List<StructureMap> listTransforms() {
1020    List<StructureMap> m = new ArrayList<StructureMap>();
1021    synchronized (lock) {
1022      m.addAll(transforms.values());    
1023    }
1024    return m;
1025  }
1026  
1027  public StructureMap getTransform(String code) {
1028    synchronized (lock) {
1029      return transforms.get(code);
1030    }
1031  }
1032
1033  public List<StructureDefinition> listStructures() {
1034    List<StructureDefinition> m = new ArrayList<StructureDefinition>();
1035    synchronized (lock) {
1036      m.addAll(structures.values());    
1037    }
1038    return m;
1039  }
1040
1041  public StructureDefinition getStructure(String code) {
1042    synchronized (lock) {
1043      return structures.get(code);
1044    }
1045  }
1046
1047  @Override
1048  public String oid2Uri(String oid) {
1049    synchronized (lock) {
1050      String uri = OIDUtils.getUriForOid(oid);
1051      if (uri != null)
1052        return uri;
1053      for (NamingSystem ns : systems) {
1054        if (hasOid(ns, oid)) {
1055          uri = getUri(ns);
1056          if (uri != null)
1057            return null;
1058        }
1059      }
1060    }
1061    return null;
1062  }
1063  
1064
1065  private String getUri(NamingSystem ns) {
1066    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
1067      if (id.getType() == NamingSystemIdentifierType.URI)
1068        return id.getValue();
1069    }
1070    return null;
1071  }
1072
1073  private boolean hasOid(NamingSystem ns, String oid) {
1074    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
1075      if (id.getType() == NamingSystemIdentifierType.OID && id.getValue().equals(oid))
1076        return true;
1077    }
1078    return false;
1079  }
1080
1081  public void cacheVS(JsonObject json, Map<String, ValidationResult> t) {
1082    synchronized (lock) {
1083      validationCache.put(json.get("url").getAsString(), t);
1084    }
1085  }
1086
1087  public SearchParameter getSearchParameter(String code) {
1088    synchronized (lock) {
1089      return searchParameters.get(code);
1090    }
1091  }
1092
1093  @Override
1094  public String getOverrideVersionNs() {
1095    return overrideVersionNs;
1096  }
1097
1098  @Override
1099  public void setOverrideVersionNs(String value) {
1100    overrideVersionNs = value;
1101  }
1102
1103  @Override
1104  public ILoggingService getLogger() {
1105    return logger;
1106  }
1107
1108  @Override
1109  public StructureDefinition fetchTypeDefinition(String typeName) {
1110    if (Utilities.isAbsoluteUrl(typeName))
1111      return fetchResource(StructureDefinition.class, typeName);
1112    else
1113      return fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+typeName);
1114  }
1115
1116  public boolean isTlogging() {
1117    return tlogging;
1118  }
1119
1120  public void setTlogging(boolean tlogging) {
1121    this.tlogging = tlogging;
1122  }
1123
1124  public UcumService getUcumService() {
1125    return ucumService;
1126  }
1127
1128  public void setUcumService(UcumService ucumService) {
1129    this.ucumService = ucumService;
1130  }
1131
1132  @Override
1133  public List<StructureDefinition> getStructures() {
1134    List<StructureDefinition> res = new ArrayList<>();
1135    synchronized (lock) { // tricky, because you need to lock this as well, but it's really not in use yet
1136      res.addAll(structures.values());
1137    }
1138    return res;
1139  }
1140  
1141  public String getLinkForUrl(String corePath, String url) {
1142    for (CodeSystem r : codeSystems.values())
1143      if (url.equals(r.getUrl()))
1144        return r.getUserString("path");
1145
1146    for (ValueSet r : valueSets.values())
1147      if (url.equals(r.getUrl()))
1148        return r.getUserString("path");
1149    
1150    for (ConceptMap r : maps.values())
1151      if (url.equals(r.getUrl()))
1152        return r.getUserString("path");
1153    
1154    for (StructureMap r : transforms.values())
1155      if (url.equals(r.getUrl()))
1156        return r.getUserString("path");
1157    
1158    for (StructureDefinition r : structures.values())
1159      if (url.equals(r.getUrl()))
1160        return r.getUserString("path");
1161    
1162    for (ImplementationGuide r : guides.values())
1163      if (url.equals(r.getUrl()))
1164        return r.getUserString("path");
1165    
1166    for (CapabilityStatement r : capstmts.values())
1167      if (url.equals(r.getUrl()))
1168        return r.getUserString("path");
1169    
1170    for (SearchParameter r : searchParameters.values())
1171      if (url.equals(r.getUrl()))
1172        return r.getUserString("path");
1173    
1174    for (Questionnaire r : questionnaires.values())
1175      if (url.equals(r.getUrl()))
1176        return r.getUserString("path");
1177    
1178    for (OperationDefinition r : operations.values())
1179      if (url.equals(r.getUrl()))
1180        return r.getUserString("path");
1181    
1182    for (PlanDefinition r : plans.values())
1183      if (url.equals(r.getUrl()))
1184        return r.getUserString("path");
1185
1186    if (url.equals("http://loinc.org"))
1187      return corePath+"loinc.html";
1188    if (url.equals("http://unitsofmeasure.org"))
1189      return corePath+"ucum.html";
1190    if (url.equals("http://snomed.info/sct"))
1191      return corePath+"snomed.html";
1192    return null;
1193  }
1194}