001package org.hl7.fhir.dstu3.context;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033
034import java.io.ByteArrayOutputStream;
035import java.io.FileNotFoundException;
036import java.io.FileOutputStream;
037import java.io.IOException;
038import java.util.ArrayList;
039import java.util.HashMap;
040import java.util.HashSet;
041import java.util.List;
042import java.util.Locale;
043import java.util.Map;
044import java.util.ResourceBundle;
045import java.util.Set;
046
047import lombok.extern.slf4j.Slf4j;
048import org.hl7.fhir.dstu3.formats.IParser.OutputStyle;
049import org.hl7.fhir.dstu3.formats.JsonParser;
050import org.hl7.fhir.dstu3.model.BooleanType;
051import org.hl7.fhir.dstu3.model.Bundle;
052import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
053import org.hl7.fhir.dstu3.model.CodeSystem;
054import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemHierarchyMeaning;
055import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent;
056import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionDesignationComponent;
057import org.hl7.fhir.dstu3.model.CodeableConcept;
058import org.hl7.fhir.dstu3.model.Coding;
059import org.hl7.fhir.dstu3.model.ConceptMap;
060import org.hl7.fhir.dstu3.model.DataElement;
061import org.hl7.fhir.dstu3.model.ExpansionProfile;
062import org.hl7.fhir.dstu3.model.IntegerType;
063import org.hl7.fhir.dstu3.model.OperationDefinition;
064import org.hl7.fhir.dstu3.model.OperationOutcome;
065import org.hl7.fhir.dstu3.model.Parameters;
066import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent;
067import org.hl7.fhir.dstu3.model.PrimitiveType;
068import org.hl7.fhir.dstu3.model.Questionnaire;
069import org.hl7.fhir.dstu3.model.Reference;
070import org.hl7.fhir.dstu3.model.Resource;
071import org.hl7.fhir.dstu3.model.SearchParameter;
072import org.hl7.fhir.dstu3.model.StringType;
073import org.hl7.fhir.dstu3.model.StructureDefinition;
074import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule;
075import org.hl7.fhir.dstu3.model.StructureMap;
076import org.hl7.fhir.dstu3.model.UriType;
077import org.hl7.fhir.dstu3.model.ValueSet;
078import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent;
079import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetFilterComponent;
080import org.hl7.fhir.dstu3.model.ValueSet.ValueSetComposeComponent;
081import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionComponent;
082import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent;
083import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.ETooCostly;
084import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.TerminologyServiceErrorClass;
085import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
086import org.hl7.fhir.dstu3.terminologies.ValueSetExpanderFactory;
087import org.hl7.fhir.dstu3.terminologies.ValueSetExpansionCache;
088import org.hl7.fhir.dstu3.utils.ToolingExtensions;
089import org.hl7.fhir.dstu3.utils.client.FHIRToolingClient;
090import org.hl7.fhir.exceptions.FHIRException;
091import org.hl7.fhir.exceptions.NoTerminologyServiceException;
092import org.hl7.fhir.exceptions.TerminologyServiceException;
093import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
094import org.hl7.fhir.utilities.FileUtilities;
095import org.hl7.fhir.utilities.UUIDUtilities;
096import org.hl7.fhir.utilities.Utilities;
097import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
098import org.hl7.fhir.utilities.i18n.I18nBase;
099import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
100import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
101
102import com.google.gson.JsonObject;
103import com.google.gson.JsonSyntaxException;
104
105import ca.uhn.fhir.rest.api.Constants;
106
107
108@Deprecated
109@Slf4j
110public abstract class BaseWorkerContext extends I18nBase implements IWorkerContext {
111
112  // all maps are to the full URI
113  protected Map<String, CodeSystem> codeSystems = new HashMap<String, CodeSystem>();
114  protected Set<String> nonSupportedCodeSystems = new HashSet<String>();
115  protected Map<String, ValueSet> valueSets = new HashMap<String, ValueSet>();
116  protected Map<String, ConceptMap> maps = new HashMap<String, ConceptMap>();
117  protected Map<String, StructureMap> transforms = new HashMap<String, StructureMap>();
118  protected Map<String, DataElement> dataElements = new HashMap<String, DataElement>();
119  protected Map<String, StructureDefinition> profiles = new HashMap<String, StructureDefinition>();
120  protected Map<String, SearchParameter> searchParameters = new HashMap<String, SearchParameter>();
121  protected Map<String, StructureDefinition> extensionDefinitions = new HashMap<String, StructureDefinition>();
122  protected Map<String, Questionnaire> questionnaires = new HashMap<String, Questionnaire>();
123  protected Map<String, OperationDefinition> operations = new HashMap<String, OperationDefinition>();
124
125  protected ValueSetExpanderFactory expansionCache = new ValueSetExpansionCache(this);
126  protected boolean cacheValidation; // if true, do an expansion and cache the expansion
127  private Set<String> failed = new HashSet<String>(); // value sets for which we don't try to do expansion, since the first attempt to get a comprehensive expansion was not successful
128  protected Map<String, Map<String, ValidationResult>> validationCache = new HashMap<String, Map<String, ValidationResult>>();
129  protected String tsServer;
130  protected String validationCachePath;
131  protected String name;
132
133  // private ValueSetExpansionCache expansionCache; //   
134
135  protected FHIRToolingClient txServer;
136  private Bundle bndCodeSystems;
137  private boolean canRunWithoutTerminology;
138  protected boolean allowLoadingDuplicates;
139  protected boolean noTerminologyServer;
140  protected String cache;
141  private int expandCodesLimit = 1000;
142
143  protected ExpansionProfile expProfile;
144  private Locale locale;
145  private ResourceBundle i18Nmessages;
146
147  public Map<String, CodeSystem> getCodeSystems() {
148    return codeSystems;
149  }
150
151  public Map<String, DataElement> getDataElements() {
152    return dataElements;
153  }
154
155  public Map<String, ValueSet> getValueSets() {
156    return valueSets;
157  }
158
159  public Map<String, ConceptMap> getMaps() {
160    return maps;
161  }
162
163  public Map<String, StructureDefinition> getProfiles() {
164    return profiles;
165  }
166
167  public Map<String, StructureDefinition> getExtensionDefinitions() {
168    return extensionDefinitions;
169  }
170
171  public Map<String, Questionnaire> getQuestionnaires() {
172    return questionnaires;
173  }
174
175  public Map<String, OperationDefinition> getOperations() {
176    return operations;
177  }
178
179  public void seeExtensionDefinition(String url, StructureDefinition ed) throws Exception {
180    if (extensionDefinitions.get(ed.getUrl()) != null) {
181      throw new Exception("duplicate extension definition: " + ed.getUrl());
182    }
183    extensionDefinitions.put(ed.getId(), ed);
184    extensionDefinitions.put(url, ed);
185    extensionDefinitions.put(ed.getUrl(), ed);
186  }
187
188  public void dropExtensionDefinition(String id) {
189    StructureDefinition sd = extensionDefinitions.get(id);
190    extensionDefinitions.remove(id);
191    if (sd != null) {
192      extensionDefinitions.remove(sd.getUrl());
193    }
194  }
195
196  public void seeQuestionnaire(String url, Questionnaire theQuestionnaire) throws Exception {
197    if (questionnaires.get(theQuestionnaire.getId()) != null) {
198      throw new Exception("duplicate extension definition: " + theQuestionnaire.getId());
199    }
200    questionnaires.put(theQuestionnaire.getId(), theQuestionnaire);
201    questionnaires.put(url, theQuestionnaire);
202  }
203
204  public void seeOperation(OperationDefinition opd) throws Exception {
205    if (operations.get(opd.getUrl()) != null) {
206      throw new Exception("duplicate extension definition: " + opd.getUrl());
207    }
208    operations.put(opd.getUrl(), opd);
209    operations.put(opd.getId(), opd);
210  }
211
212  public void seeValueSet(String url, ValueSet vs) throws Exception {
213    if (valueSets.containsKey(vs.getUrl()) && !allowLoadingDuplicates) {
214      throw new Exception("Duplicate value set " + vs.getUrl());
215    }
216    valueSets.put(vs.getId(), vs);
217    valueSets.put(url, vs);
218    valueSets.put(vs.getUrl(), vs);
219  }
220
221  public void dropValueSet(String id) {
222    ValueSet vs = valueSets.get(id);
223    valueSets.remove(id);
224    if (vs != null) {
225      valueSets.remove(vs.getUrl());
226    }
227  }
228
229  public void seeCodeSystem(String url, CodeSystem cs) throws FHIRException {
230    if (codeSystems.containsKey(cs.getUrl()) && !allowLoadingDuplicates) {
231      throw new FHIRException("Duplicate code system " + cs.getUrl());
232    }
233    codeSystems.put(cs.getId(), cs);
234    codeSystems.put(url, cs);
235    codeSystems.put(cs.getUrl(), cs);
236  }
237
238  public void dropCodeSystem(String id) {
239    CodeSystem cs = codeSystems.get(id);
240    codeSystems.remove(id);
241    if (cs != null) {
242      codeSystems.remove(cs.getUrl());
243    }
244  }
245
246  public void seeProfile(String url, StructureDefinition p) throws Exception {
247    if (profiles.containsKey(p.getUrl())) {
248      throw new Exception("Duplicate Profile " + p.getUrl());
249    }
250    profiles.put(p.getId(), p);
251    profiles.put(url, p);
252    profiles.put(p.getUrl(), p);
253  }
254
255  public void dropProfile(String id) {
256    StructureDefinition sd = profiles.get(id);
257    profiles.remove(id);
258    if (sd != null) {
259      profiles.remove(sd.getUrl());
260    }
261  }
262
263  @Override
264  public CodeSystem fetchCodeSystem(String system) {
265    return codeSystems.get(system);
266  }
267
268  @Override
269  public boolean supportsSystem(String system) throws TerminologyServiceException {
270    if (codeSystems.containsKey(system)) {
271      return true;
272    } else if (nonSupportedCodeSystems.contains(system)) {
273      return false;
274    } else if (system.startsWith("http://example.org") || system.startsWith("http://acme.com")
275      || system.startsWith("http://hl7.org/fhir/valueset-") || system.startsWith("urn:oid:")) {
276      return false;
277    } else {
278      if (noTerminologyServer) {
279        return false;
280      }
281      if (bndCodeSystems == null) {
282        try {
283          tlog("Terminology server: Check for supported code systems for " + system);
284          bndCodeSystems = txServer.fetchFeed(txServer.getAddress()
285            + "/CodeSystem?content-mode=not-present&_summary=true&_count=1000");
286        } catch (Exception e) {
287          if (canRunWithoutTerminology) {
288            noTerminologyServer = true;
289            log.info("==============!! Running without terminology server !!============== (" + e
290                      .getMessage() + ")");
291            return false;
292          } else {
293            throw new TerminologyServiceException(e);
294          }
295        }
296      }
297      if (bndCodeSystems != null) {
298        for (BundleEntryComponent be : bndCodeSystems.getEntry()) {
299          CodeSystem cs = (CodeSystem) be.getResource();
300          if (!codeSystems.containsKey(cs.getUrl())) {
301            codeSystems.put(cs.getUrl(), null);
302          }
303        }
304      }
305      if (codeSystems.containsKey(system)) {
306        return true;
307      }
308    }
309    nonSupportedCodeSystems.add(system);
310    return false;
311  }
312
313  @Override
314  public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk, boolean heirarchical) {
315    try {
316      if (vs.hasExpansion()) {
317        return new ValueSetExpansionOutcome(vs.copy());
318      }
319      String cacheFn = null;
320      if (cache != null) {
321        cacheFn = Utilities.path(cache, determineCacheId(vs, heirarchical) + ".json");
322        if (ManagedFileAccess.file(cacheFn).exists()) {
323          return loadFromCache(vs.copy(), cacheFn);
324        }
325      }
326      if (cacheOk && vs.hasUrl()) {
327        if (expProfile == null) {
328          throw new Exception("No ExpansionProfile provided");
329        }
330        ValueSetExpansionOutcome vse = expansionCache.getExpander()
331          .expand(vs, expProfile.setExcludeNested(!heirarchical));
332        if (vse.getValueset() != null) {
333          if (cache != null) {
334            FileOutputStream s = ManagedFileAccess.outStream(cacheFn);
335            newJsonParser().compose(ManagedFileAccess.outStream(cacheFn), vse.getValueset());
336            s.close();
337          }
338        }
339        return vse;
340      } else {
341        ValueSetExpansionOutcome res = expandOnServer(vs, cacheFn);
342        if (cacheFn != null) {
343          if (res.getValueset() != null) {
344            saveToCache(res.getValueset(), cacheFn);
345          } else {
346            OperationOutcome oo = new OperationOutcome();
347            oo.addIssue().getDetails().setText(res.getError());
348            saveToCache(oo, cacheFn);
349          }
350        }
351        return res;
352      }
353    } catch (NoTerminologyServiceException e) {
354      return new ValueSetExpansionOutcome(
355        e.getMessage() == null ? e.getClass().getName() : e.getMessage(),
356        TerminologyServiceErrorClass.NOSERVICE);
357    } catch (Exception e) {
358      return new ValueSetExpansionOutcome(
359        e.getMessage() == null ? e.getClass().getName() : e.getMessage(),
360        TerminologyServiceErrorClass.UNKNOWN);
361    }
362  }
363
364  private ValueSetExpansionOutcome loadFromCache(ValueSet vs, String cacheFn)
365    throws FileNotFoundException, Exception {
366    JsonParser parser = new JsonParser();
367    Resource r = parser.parse(ManagedFileAccess.inStream(cacheFn));
368    if (r instanceof OperationOutcome) {
369      return new ValueSetExpansionOutcome(
370        ((OperationOutcome) r).getIssue().get(0).getDetails().getText(),
371        TerminologyServiceErrorClass.UNKNOWN);
372    } else {
373      vs.setExpansion(((ValueSet) r)
374        .getExpansion()); // because what is cached might be from a different value set
375      return new ValueSetExpansionOutcome(vs);
376    }
377  }
378
379  private void saveToCache(Resource res, String cacheFn) throws FileNotFoundException, Exception {
380    JsonParser parser = new JsonParser();
381    parser.compose(ManagedFileAccess.outStream(cacheFn), res);
382  }
383
384  private String determineCacheId(ValueSet vs, boolean heirarchical) throws Exception {
385    // just the content logical definition is hashed
386    ValueSet vsid = new ValueSet();
387    vsid.setCompose(vs.getCompose());
388    JsonParser parser = new JsonParser();
389    parser.setOutputStyle(OutputStyle.NORMAL);
390    ByteArrayOutputStream b = new ByteArrayOutputStream();
391    parser.compose(b, vsid);
392    b.close();
393    String s = new String(b.toByteArray(), Constants.CHARSET_UTF8);
394    // any code systems we can find, we add these too. 
395    for (ConceptSetComponent inc : vs.getCompose().getInclude()) {
396      CodeSystem cs = fetchCodeSystem(inc.getSystem());
397      if (cs != null) {
398        String css = cacheValue(cs);
399        s = s + css;
400      }
401    }
402    s = s + "-" + Boolean.toString(heirarchical);
403    String r = Integer.toString(s.hashCode());
404    //    FileUtilities.stringToFile(s, Utilities.path(cache, r+".id.json"));
405    return r;
406  }
407
408
409  private String cacheValue(CodeSystem cs) throws IOException {
410    CodeSystem csid = new CodeSystem();
411    csid.setId(cs.getId());
412    csid.setVersion(cs.getVersion());
413    csid.setContent(cs.getContent());
414    csid.setHierarchyMeaning(CodeSystemHierarchyMeaning.GROUPEDBY);
415    for (ConceptDefinitionComponent cc : cs.getConcept()) {
416      csid.getConcept().add(processCSConcept(cc));
417    }
418    JsonParser parser = new JsonParser();
419    parser.setOutputStyle(OutputStyle.NORMAL);
420    ByteArrayOutputStream b = new ByteArrayOutputStream();
421    parser.compose(b, csid);
422    b.close();
423    return new String(b.toByteArray(), Constants.CHARSET_UTF8);
424  }
425
426
427  private ConceptDefinitionComponent processCSConcept(ConceptDefinitionComponent cc) {
428    ConceptDefinitionComponent ccid = new ConceptDefinitionComponent();
429    ccid.setCode(cc.getCode());
430    ccid.setDisplay(cc.getDisplay());
431    for (ConceptDefinitionComponent cci : cc.getConcept()) {
432      ccid.getConcept().add(processCSConcept(cci));
433    }
434    return ccid;
435  }
436
437  public ValueSetExpansionOutcome expandOnServer(ValueSet vs, String fn) throws Exception {
438    if (noTerminologyServer) {
439      return new ValueSetExpansionOutcome(
440        "Error expanding ValueSet: running without terminology services",
441        TerminologyServiceErrorClass.NOSERVICE);
442    }
443    if (expProfile == null) {
444      throw new Exception("No ExpansionProfile provided");
445    }
446
447    try {
448      Parameters params = new Parameters();
449      params.addParameter().setName("profile").setResource(expProfile.setIncludeDefinition(false));
450      params.addParameter().setName("_limit").setValue(new IntegerType(expandCodesLimit));
451      params.addParameter().setName("_incomplete").setValue(new BooleanType("true"));
452      tlog("Terminology Server: $expand on " + getVSSummary(vs));
453      ValueSet result = txServer.expandValueset(vs, params);
454      return new ValueSetExpansionOutcome(result);
455    } catch (Exception e) {
456      return new ValueSetExpansionOutcome(
457        "Error expanding ValueSet \"" + vs.getUrl() + ": " + e.getMessage(),
458        TerminologyServiceErrorClass.UNKNOWN);
459    }
460  }
461
462  private String getVSSummary(ValueSet vs) {
463    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
464    for (ConceptSetComponent cc : vs.getCompose().getInclude()) {
465      b.append("Include " + getIncSummary(cc));
466    }
467    for (ConceptSetComponent cc : vs.getCompose().getExclude()) {
468      b.append("Exclude " + getIncSummary(cc));
469    }
470    return b.toString();
471  }
472
473  private String getIncSummary(ConceptSetComponent cc) {
474    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
475    for (UriType vs : cc.getValueSet()) {
476      b.append(vs.asStringValue());
477    }
478    String vsd =
479      b.length() > 0 ? " where the codes are in the value sets (" + b.toString() + ")" : "";
480    String system = cc.getSystem();
481    if (cc.hasConcept()) {
482      return Integer.toString(cc.getConcept().size()) + " codes from " + system + vsd;
483    }
484    if (cc.hasFilter()) {
485      String s = "";
486      for (ConceptSetFilterComponent f : cc.getFilter()) {
487        if (!Utilities.noString(s)) {
488          s = s + " & ";
489        }
490        s = s + f.getProperty() + " " + f.getOp().toCode() + " " + f.getValue();
491      }
492      return "from " + system + " where " + s + vsd;
493    }
494    return "All codes from " + system + vsd;
495  }
496
497  private ValidationResult handleByCache(ValueSet vs, Coding coding, boolean tryCache) {
498    String cacheId = cacheId(coding);
499    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
500    if (cache == null) {
501      cache = new HashMap<String, IWorkerContext.ValidationResult>();
502      validationCache.put(vs.getUrl(), cache);
503    }
504    if (cache.containsKey(cacheId)) {
505      return cache.get(cacheId);
506    }
507    if (!tryCache) {
508      return null;
509    }
510    if (!cacheValidation) {
511      return null;
512    }
513    if (failed.contains(vs.getUrl())) {
514      return null;
515    }
516    ValueSetExpansionOutcome vse = expandVS(vs, true, false);
517    if (vse.getValueset() == null || notcomplete(vse.getValueset())) {
518      failed.add(vs.getUrl());
519      return null;
520    }
521
522    ValidationResult res = validateCode(coding, vse.getValueset());
523    cache.put(cacheId, res);
524    return res;
525  }
526
527  private boolean notcomplete(ValueSet vs) {
528    if (!vs.hasExpansion()) {
529      return true;
530    }
531    if (!vs.getExpansion()
532      .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-unclosed").isEmpty()) {
533      return true;
534    }
535    if (!vs.getExpansion()
536      .getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-toocostly").isEmpty()) {
537      return true;
538    }
539    return false;
540  }
541
542  private ValidationResult handleByCache(ValueSet vs, CodeableConcept concept, boolean tryCache) {
543    String cacheId = cacheId(concept);
544    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
545    if (cache == null) {
546      cache = new HashMap<String, IWorkerContext.ValidationResult>();
547      validationCache.put(vs.getUrl(), cache);
548    }
549    if (cache.containsKey(cacheId)) {
550      return cache.get(cacheId);
551    }
552
553    if (validationCache.containsKey(vs.getUrl()) && validationCache.get(vs.getUrl())
554      .containsKey(cacheId)) {
555      return validationCache.get(vs.getUrl()).get(cacheId);
556    }
557    if (!tryCache) {
558      return null;
559    }
560    if (!cacheValidation) {
561      return null;
562    }
563    if (failed.contains(vs.getUrl())) {
564      return null;
565    }
566    ValueSetExpansionOutcome vse = expandVS(vs, true, false);
567    if (vse.getValueset() == null || notcomplete(vse.getValueset())) {
568      failed.add(vs.getUrl());
569      return null;
570    }
571    ValidationResult res = validateCode(concept, vse.getValueset());
572    cache.put(cacheId, res);
573    return res;
574  }
575
576  private String cacheId(Coding coding) {
577    return "|" + coding.getSystem() + "|" + coding.getVersion() + "|" + coding.getCode() + "|"
578      + coding.getDisplay();
579  }
580
581  private String cacheId(CodeableConcept cc) {
582    StringBuilder b = new StringBuilder();
583    for (Coding c : cc.getCoding()) {
584      b.append("#");
585      b.append(cacheId(c));
586    }
587    return b.toString();
588  }
589
590  private ValidationResult verifyCodeExternal(ValueSet vs, Coding coding, boolean tryCache)
591    throws Exception {
592    ValidationResult res = vs == null ? null : handleByCache(vs, coding, tryCache);
593    if (res != null) {
594      return res;
595    }
596    Parameters pin = new Parameters();
597    pin.addParameter().setName("coding").setValue(coding);
598    if (vs != null) {
599      pin.addParameter().setName("valueSet").setResource(vs);
600    }
601    res = serverValidateCode(pin, vs == null);
602    if (vs != null) {
603      Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
604      cache.put(cacheId(coding), res);
605    }
606    return res;
607  }
608
609  private ValidationResult verifyCodeExternal(ValueSet vs, CodeableConcept cc, boolean tryCache)
610    throws Exception {
611    ValidationResult res = handleByCache(vs, cc, tryCache);
612    if (res != null) {
613      return res;
614    }
615    Parameters pin = new Parameters();
616    pin.addParameter().setName("codeableConcept").setValue(cc);
617    pin.addParameter().setName("valueSet").setResource(vs);
618    res = serverValidateCode(pin, false);
619    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
620    cache.put(cacheId(cc), res);
621    return res;
622  }
623
624  private ValidationResult serverValidateCode(Parameters pin, boolean doCache) throws Exception {
625    if (noTerminologyServer) {
626      return new ValidationResult(null, null, TerminologyServiceErrorClass.NOSERVICE);
627    }
628    String cacheName = doCache ? generateCacheName(pin) : null;
629    ValidationResult res = loadFromCache(cacheName);
630    if (res != null) {
631      return res;
632    }
633    tlog("Terminology Server: $validate-code " + describeValidationParameters(pin));
634    for (ParametersParameterComponent pp : pin.getParameter()) {
635      if (pp.getName().equals("profile")) {
636        throw new Error("Can only specify profile in the context");
637      }
638    }
639    if (expProfile == null) {
640      throw new Exception("No ExpansionProfile provided");
641    }
642    pin.addParameter().setName("profile").setResource(expProfile);
643
644    Parameters pout = txServer.operateType(ValueSet.class, "validate-code", pin);
645    boolean ok = false;
646    String message = "No Message returned";
647    String display = null;
648    TerminologyServiceErrorClass err = TerminologyServiceErrorClass.UNKNOWN;
649    for (ParametersParameterComponent p : pout.getParameter()) {
650      if (p.getName().equals("result")) {
651        ok = ((BooleanType) p.getValue()).getValue().booleanValue();
652      } else if (p.getName().equals("message")) {
653        message = ((StringType) p.getValue()).getValue();
654      } else if (p.getName().equals("display")) {
655        display = ((StringType) p.getValue()).getValue();
656      } else if (p.getName().equals("cause")) {
657        try {
658          IssueType it = IssueType.fromCode(((StringType) p.getValue()).getValue());
659          if (it == IssueType.UNKNOWN) {
660            err = TerminologyServiceErrorClass.UNKNOWN;
661          } else if (it == IssueType.NOTSUPPORTED) {
662            err = TerminologyServiceErrorClass.VALUESET_UNSUPPORTED;
663          }
664        } catch (FHIRException e) {
665        }
666      }
667    }
668    if (!ok) {
669      res = new ValidationResult(IssueSeverity.ERROR, message, err);
670    } else if (display != null) {
671      res = new ValidationResult(new ConceptDefinitionComponent().setDisplay(display));
672    } else {
673      res = new ValidationResult(new ConceptDefinitionComponent());
674    }
675    saveToCache(res, cacheName);
676    return res;
677  }
678
679
680  private void tlog(String msg) {
681    //    log(msg);
682  }
683
684  @SuppressWarnings("rawtypes")
685  private String describeValidationParameters(Parameters pin) {
686    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
687    for (ParametersParameterComponent p : pin.getParameter()) {
688      if (p.hasValue() && p.getValue() instanceof PrimitiveType) {
689        b.append(p.getName() + "=" + ((PrimitiveType) p.getValue()).asStringValue());
690      } else if (p.hasValue() && p.getValue() instanceof Coding) {
691        b.append("system=" + ((Coding) p.getValue()).getSystem());
692        b.append("code=" + ((Coding) p.getValue()).getCode());
693        b.append("display=" + ((Coding) p.getValue()).getDisplay());
694      } else if (p.hasValue() && p.getValue() instanceof CodeableConcept) {
695        if (((CodeableConcept) p.getValue()).hasCoding()) {
696          Coding c = ((CodeableConcept) p.getValue()).getCodingFirstRep();
697          b.append("system=" + c.getSystem());
698          b.append("code=" + c.getCode());
699          b.append("display=" + c.getDisplay());
700        } else if (((CodeableConcept) p.getValue()).hasText()) {
701          b.append("text=" + ((CodeableConcept) p.getValue()).getText());
702        }
703      } else if (p.hasResource() && (p.getResource() instanceof ValueSet)) {
704        b.append("valueset=" + getVSSummary((ValueSet) p.getResource()));
705      }
706    }
707    return b.toString();
708  }
709
710  private ValidationResult loadFromCache(String fn) throws FileNotFoundException, IOException {
711    if (fn == null) {
712      return null;
713    }
714    if (!(ManagedFileAccess.file(fn).exists())) {
715      return null;
716    }
717    String cnt = FileUtilities.fileToString(fn);
718    if (cnt.startsWith("!error: ")) {
719      return new ValidationResult(IssueSeverity.ERROR, cnt.substring(8));
720    } else if (cnt.startsWith("!warning: ")) {
721      return new ValidationResult(IssueSeverity.ERROR, cnt.substring(10));
722    } else {
723      return new ValidationResult(new ConceptDefinitionComponent().setDisplay(cnt));
724    }
725  }
726
727  private void saveToCache(ValidationResult res, String cacheName) throws IOException {
728    if (cacheName == null) {
729      return;
730    }
731    if (res.getDisplay() != null) {
732      FileUtilities.stringToFile(res.getDisplay(), cacheName);
733    } else if (res.getMessage() != null) {
734      if (res.getSeverity() == IssueSeverity.WARNING) {
735        FileUtilities.stringToFile("!warning: " + res.getMessage(), cacheName);
736      } else {
737        FileUtilities.stringToFile("!error: " + res.getMessage(), cacheName);
738      }
739    }
740  }
741
742  private String generateCacheName(Parameters pin) throws IOException {
743    if (cache == null) {
744      return null;
745    }
746    String json = new JsonParser().composeString(pin);
747    return Utilities.path(cache, "vc" + Integer.toString(json.hashCode()) + ".json");
748  }
749
750  @Override
751  public ValueSetExpansionComponent expandVS(ConceptSetComponent inc, boolean heirachical)
752    throws TerminologyServiceException {
753    ValueSet vs = new ValueSet();
754    vs.setCompose(new ValueSetComposeComponent());
755    vs.getCompose().getInclude().add(inc);
756    ValueSetExpansionOutcome vse = expandVS(vs, true, heirachical);
757    ValueSet valueset = vse.getValueset();
758    if (valueset == null) {
759      throw new TerminologyServiceException("Error Expanding ValueSet: " + vse.getError());
760    }
761    return valueset.getExpansion();
762  }
763
764  @Override
765  public ValidationResult validateCode(String system, String code, String display) {
766    try {
767      if (codeSystems.containsKey(system) && codeSystems.get(system) != null) {
768        return verifyCodeInCodeSystem(codeSystems.get(system), system, code, display);
769      } else {
770        return verifyCodeExternal(null,
771          new Coding().setSystem(system).setCode(code).setDisplay(display), false);
772      }
773    } catch (Exception e) {
774      return new ValidationResult(IssueSeverity.FATAL,
775        "Error validating code \"" + code + "\" in system \"" + system + "\": " + e.getMessage());
776    }
777  }
778
779
780  @Override
781  public ValidationResult validateCode(Coding code, ValueSet vs) {
782    if (codeSystems.containsKey(code.getSystem()) && codeSystems.get(code.getSystem()) != null) {
783      try {
784        return verifyCodeInCodeSystem(codeSystems.get(code.getSystem()), code.getSystem(),
785          code.getCode(), code.getDisplay());
786      } catch (Exception e) {
787        return new ValidationResult(IssueSeverity.FATAL,
788          "Error validating code \"" + code + "\" in system \"" + code.getSystem() + "\": " + e
789            .getMessage());
790      }
791    } else if (vs.hasExpansion()) {
792      try {
793        return verifyCodeInternal(vs, code.getSystem(), code.getCode(), code.getDisplay());
794      } catch (Exception e) {
795        return new ValidationResult(IssueSeverity.FATAL,
796          "Error validating code \"" + code + "\" in system \"" + code.getSystem() + "\": " + e
797            .getMessage());
798      }
799    } else {
800      try {
801        return verifyCodeExternal(vs, code, true);
802      } catch (Exception e) {
803        return new ValidationResult(IssueSeverity.WARNING,
804          "Error validating code \"" + code + "\" in system \"" + code.getSystem() + "\": " + e
805            .getMessage());
806      }
807    }
808  }
809
810  @Override
811  public ValidationResult validateCode(CodeableConcept code, ValueSet vs) {
812    try {
813      if (vs.hasExpansion()) {
814        return verifyCodeInternal(vs, code);
815      } else {
816        // we'll try expanding first; if that doesn't work, then we'll just pass it to the server to validate 
817        // ... could be a problem if the server doesn't have the code systems we have locally, so we try not to depend on the server
818        try {
819          ValueSetExpansionOutcome vse = expandVS(vs, true, false);
820          if (vse.getValueset() != null && !hasTooCostlyExpansion(vse.getValueset())) {
821            return verifyCodeInternal(vse.getValueset(), code);
822          }
823        } catch (Exception e) {
824          // failed? we'll just try the server
825        }
826        return verifyCodeExternal(vs, code, true);
827      }
828    } catch (Exception e) {
829      return new ValidationResult(IssueSeverity.FATAL,
830        "Error validating code \"" + code.toString() + "\": " + e.getMessage(),
831        TerminologyServiceErrorClass.SERVER_ERROR);
832    }
833  }
834
835
836  private boolean hasTooCostlyExpansion(ValueSet valueset) {
837    return valueset != null && valueset.hasExpansion() && ToolingExtensions
838      .hasExtension(valueset.getExpansion(),
839        "http://hl7.org/fhir/StructureDefinition/valueset-toocostly");
840  }
841
842  @Override
843  public ValidationResult validateCode(String system, String code, String display, ValueSet vs) {
844    try {
845      if (system == null && display == null) {
846        return verifyCodeInternal(vs, code);
847      }
848      if ((codeSystems.containsKey(system) && codeSystems.get(system) != null) || vs
849        .hasExpansion()) {
850        return verifyCodeInternal(vs, system, code, display);
851      } else {
852        return verifyCodeExternal(vs,
853          new Coding().setSystem(system).setCode(code).setDisplay(display), true);
854      }
855    } catch (Exception e) {
856      return new ValidationResult(IssueSeverity.FATAL,
857        "Error validating code \"" + code + "\" in system \"" + system + "\": " + e.getMessage(),
858        TerminologyServiceErrorClass.SERVER_ERROR);
859    }
860  }
861
862  @Override
863  public ValidationResult validateCode(String system, String code, String display,
864    ConceptSetComponent vsi) {
865    try {
866      ValueSet vs = new ValueSet();
867      vs.setUrl(UUIDUtilities.makeUuidUrn());
868      vs.getCompose().addInclude(vsi);
869      return verifyCodeExternal(vs,
870        new Coding().setSystem(system).setCode(code).setDisplay(display), true);
871    } catch (Exception e) {
872      return new ValidationResult(IssueSeverity.FATAL,
873        "Error validating code \"" + code + "\" in system \"" + system + "\": " + e.getMessage());
874    }
875  }
876
877  public void initTS(String cachePath, String tsServer) throws Exception {
878    cache = cachePath;
879    this.tsServer = tsServer;
880    expansionCache = new ValueSetExpansionCache(this, null);
881    validationCachePath = Utilities.path(cachePath, "validation.cache");
882    try {
883      loadValidationCache();
884    } catch (Exception e) {
885      e.printStackTrace();
886    }
887  }
888
889  protected void loadValidationCache() throws JsonSyntaxException, Exception {
890  }
891
892  @Override
893  public List<ConceptMap> findMapsForSource(String url) {
894    List<ConceptMap> res = new ArrayList<ConceptMap>();
895    for (ConceptMap map : maps.values()) {
896      if (((Reference) map.getSource()).getReference().equals(url)) {
897        res.add(map);
898      }
899    }
900    return res;
901  }
902
903  private ValidationResult verifyCodeInternal(ValueSet vs, CodeableConcept code) throws Exception {
904    for (Coding c : code.getCoding()) {
905      ValidationResult res = verifyCodeInternal(vs, c.getSystem(), c.getCode(), c.getDisplay());
906      if (res.isOk()) {
907        return res;
908      }
909    }
910    if (code.getCoding().isEmpty()) {
911      return new ValidationResult(IssueSeverity.ERROR, "None code provided");
912    } else {
913      return new ValidationResult(IssueSeverity.ERROR,
914        "None of the codes are in the specified value set");
915    }
916  }
917
918  private ValidationResult verifyCodeInternal(ValueSet vs, String system, String code,
919    String display) throws Exception {
920    if (vs.hasExpansion()) {
921      return verifyCodeInExpansion(vs, system, code, display);
922    } else {
923      ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs, null);
924      if (vse.getValueset() != null) {
925        return verifyCodeExternal(vs,
926          new Coding().setSystem(system).setCode(code).setDisplay(display), false);
927      } else {
928        return verifyCodeInExpansion(vse.getValueset(), system, code, display);
929      }
930    }
931  }
932
933  private ValidationResult verifyCodeInternal(ValueSet vs, String code)
934    throws FileNotFoundException, ETooCostly, IOException, FHIRException {
935    if (vs.hasExpansion()) {
936      return verifyCodeInExpansion(vs, code);
937    } else {
938      ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs, null);
939      if (vse.getValueset() == null) {
940        return new ValidationResult(IssueSeverity.ERROR, vse.getError(), vse.getErrorClass());
941      } else {
942        return verifyCodeInExpansion(vse.getValueset(), code);
943      }
944    }
945  }
946
947  private ValidationResult verifyCodeInCodeSystem(CodeSystem cs, String system, String code,
948    String display) throws Exception {
949    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
950    if (cc == null) {
951      if (cs.getContent().equals(CodeSystem.CodeSystemContentMode.COMPLETE)) {
952        return new ValidationResult(IssueSeverity.ERROR,
953          "Unknown Code " + code + " in " + cs.getUrl());
954      } else if (!cs.getContent().equals(CodeSystem.CodeSystemContentMode.NOTPRESENT)) {
955        return new ValidationResult(IssueSeverity.WARNING,
956          "Unknown Code " + code + " in partial code list of " + cs.getUrl());
957      } else {
958        return verifyCodeExternal(null,
959          new Coding().setSystem(system).setCode(code).setDisplay(display), false);
960      }
961    }
962    //
963    //        return new ValidationResult(IssueSeverity.WARNING, "A definition was found for "+cs.getUrl()+", but it has no codes in the definition");
964    //      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+cs.getUrl());
965    if (display == null) {
966      return new ValidationResult(cc);
967    }
968    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
969    if (cc.hasDisplay()) {
970      b.append(cc.getDisplay());
971      if (display.equalsIgnoreCase(cc.getDisplay())) {
972        return new ValidationResult(cc);
973      }
974    }
975    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
976      b.append(ds.getValue());
977      if (display.equalsIgnoreCase(ds.getValue())) {
978        return new ValidationResult(cc);
979      }
980    }
981    return new ValidationResult(IssueSeverity.WARNING,
982      "Display Name for " + code + " must be one of '" + b.toString() + "'", cc);
983  }
984
985
986  private ValidationResult verifyCodeInExpansion(ValueSet vs, String system, String code,
987    String display) {
988    ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code);
989    if (cc == null) {
990      return new ValidationResult(IssueSeverity.ERROR,
991        "Unknown Code " + code + " in " + vs.getUrl());
992    }
993    if (display == null) {
994      return new ValidationResult(
995        new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay()));
996    }
997    if (cc.hasDisplay()) {
998      if (display.equalsIgnoreCase(cc.getDisplay())) {
999        return new ValidationResult(
1000          new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay()));
1001      }
1002      return new ValidationResult(IssueSeverity.WARNING,
1003        "Display Name for " + code + " must be '" + cc.getDisplay() + "'",
1004        new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay()));
1005    }
1006    return null;
1007  }
1008
1009  private ValidationResult verifyCodeInExpansion(ValueSet vs, String code) throws FHIRException {
1010    if (vs.getExpansion()
1011      .hasExtension("http://hl7.org/fhir/StructureDefinition/valueset-toocostly")) {
1012      throw new FHIRException("Unable to validate core - value set is too costly to expand");
1013    } else {
1014      ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code);
1015      if (cc == null) {
1016        return new ValidationResult(IssueSeverity.ERROR,
1017          "Unknown Code " + code + " in " + vs.getUrl());
1018      }
1019      return null;
1020    }
1021  }
1022
1023  private ValueSetExpansionContainsComponent findCode(
1024    List<ValueSetExpansionContainsComponent> contains, String code) {
1025    for (ValueSetExpansionContainsComponent cc : contains) {
1026      if (code.equals(cc.getCode())) {
1027        return cc;
1028      }
1029      ValueSetExpansionContainsComponent c = findCode(cc.getContains(), code);
1030      if (c != null) {
1031        return c;
1032      }
1033    }
1034    return null;
1035  }
1036
1037  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept,
1038    String code) {
1039    for (ConceptDefinitionComponent cc : concept) {
1040      if (code.equals(cc.getCode())) {
1041        return cc;
1042      }
1043      ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code);
1044      if (c != null) {
1045        return c;
1046      }
1047    }
1048    return null;
1049  }
1050
1051  public Set<String> getNonSupportedCodeSystems() {
1052    return nonSupportedCodeSystems;
1053  }
1054
1055  public boolean isCanRunWithoutTerminology() {
1056    return canRunWithoutTerminology;
1057  }
1058
1059  public void setCanRunWithoutTerminology(boolean canRunWithoutTerminology) {
1060    this.canRunWithoutTerminology = canRunWithoutTerminology;
1061  }
1062
1063  public int getExpandCodesLimit() {
1064    return expandCodesLimit;
1065  }
1066
1067  public void setExpandCodesLimit(int expandCodesLimit) {
1068    this.expandCodesLimit = expandCodesLimit;
1069  }
1070
1071  @Deprecated
1072  public void setLogger(ILoggingService logger) {
1073
1074  }
1075
1076  public ExpansionProfile getExpansionProfile() {
1077    return expProfile;
1078  }
1079
1080  public void setExpansionProfile(ExpansionProfile expProfile) {
1081    this.expProfile = expProfile;
1082  }
1083
1084  @Override
1085  public boolean isNoTerminologyServer() {
1086    return noTerminologyServer;
1087  }
1088
1089  public String getName() {
1090    return name;
1091  }
1092
1093  public void setName(String name) {
1094    this.name = name;
1095  }
1096
1097  @Override
1098  public Set<String> getResourceNamesAsSet() {
1099    Set<String> res = new HashSet<String>();
1100    res.addAll(getResourceNames());
1101    return res;
1102  }
1103
1104  public void reportStatus(JsonObject json) {
1105    json.addProperty("codeystem-count", codeSystems.size());
1106    json.addProperty("valueset-count", valueSets.size());
1107    json.addProperty("conceptmap-count", maps.size());
1108    json.addProperty("transforms-count", transforms.size());
1109    json.addProperty("structures-count", profiles.size());
1110  }
1111
1112  public void cacheResource(Resource r) throws Exception {
1113    if (r instanceof ValueSet) {
1114      seeValueSet(((ValueSet) r).getUrl(), (ValueSet) r);
1115    } else if (r instanceof CodeSystem) {
1116      seeCodeSystem(((CodeSystem) r).getUrl(), (CodeSystem) r);
1117    } else if (r instanceof StructureDefinition) {
1118      StructureDefinition sd = (StructureDefinition) r;
1119      if ("http://hl7.org/fhir/StructureDefinition/Extension".equals(sd.getBaseDefinition())) {
1120        seeExtensionDefinition(sd.getUrl(), sd);
1121      } else if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
1122        seeProfile(sd.getUrl(), sd);
1123      }
1124    }
1125  }
1126
1127  public void dropResource(String type, String id) throws FHIRException {
1128    if (type.equals("ValueSet")) {
1129      dropValueSet(id);
1130    }
1131    if (type.equals("CodeSystem")) {
1132      dropCodeSystem(id);
1133    }
1134    if (type.equals("StructureDefinition")) {
1135      dropProfile(id);
1136      dropExtensionDefinition(id);
1137    }
1138  }
1139
1140  public boolean isAllowLoadingDuplicates() {
1141    return allowLoadingDuplicates;
1142  }
1143
1144  public void setAllowLoadingDuplicates(boolean allowLoadingDuplicates) {
1145    this.allowLoadingDuplicates = allowLoadingDuplicates;
1146  }
1147
1148  @Override
1149  public StructureDefinition fetchTypeDefinition(String typeName) {
1150    return fetchResource(StructureDefinition.class,
1151      "http://hl7.org/fhir/StructureDefinition/" + typeName);
1152  }
1153}