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