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