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