001package org.hl7.fhir.dstu2016may.utils;
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.FileNotFoundException;
035import java.io.IOException;
036import java.text.MessageFormat;
037import java.util.ArrayList;
038import java.util.HashMap;
039import java.util.HashSet;
040import java.util.List;
041import java.util.Locale;
042import java.util.Map;
043import java.util.Objects;
044import java.util.ResourceBundle;
045import java.util.Set;
046
047import org.hl7.fhir.dstu2016may.model.BooleanType;
048import org.hl7.fhir.dstu2016may.model.Bundle;
049import org.hl7.fhir.dstu2016may.model.Bundle.BundleEntryComponent;
050import org.hl7.fhir.dstu2016may.model.CodeSystem;
051import org.hl7.fhir.dstu2016may.model.CodeSystem.ConceptDefinitionComponent;
052import org.hl7.fhir.dstu2016may.model.CodeSystem.ConceptDefinitionDesignationComponent;
053import org.hl7.fhir.dstu2016may.model.CodeableConcept;
054import org.hl7.fhir.dstu2016may.model.Coding;
055import org.hl7.fhir.dstu2016may.model.ConceptMap;
056import org.hl7.fhir.dstu2016may.model.OperationOutcome.IssueSeverity;
057import org.hl7.fhir.dstu2016may.model.Parameters;
058import org.hl7.fhir.dstu2016may.model.Parameters.ParametersParameterComponent;
059import org.hl7.fhir.dstu2016may.model.Reference;
060import org.hl7.fhir.dstu2016may.model.StringType;
061import org.hl7.fhir.dstu2016may.model.StructureDefinition;
062import org.hl7.fhir.dstu2016may.model.ValueSet;
063import org.hl7.fhir.dstu2016may.model.ValueSet.ConceptSetComponent;
064import org.hl7.fhir.dstu2016may.model.ValueSet.ValueSetComposeComponent;
065import org.hl7.fhir.dstu2016may.model.ValueSet.ValueSetExpansionComponent;
066import org.hl7.fhir.dstu2016may.model.ValueSet.ValueSetExpansionContainsComponent;
067import org.hl7.fhir.dstu2016may.terminologies.ValueSetExpander.ETooCostly;
068import org.hl7.fhir.dstu2016may.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
069import org.hl7.fhir.dstu2016may.terminologies.ValueSetExpanderFactory;
070import org.hl7.fhir.dstu2016may.terminologies.ValueSetExpansionCache;
071import org.hl7.fhir.dstu2016may.utils.client.FHIRToolingClient;
072import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
073import org.hl7.fhir.utilities.Utilities;
074import org.hl7.fhir.utilities.i18n.I18nBase;
075
076public abstract class BaseWorkerContext extends I18nBase implements IWorkerContext {
077
078  // all maps are to the full URI
079  protected Map<String, CodeSystem> codeSystems = new HashMap<String, CodeSystem>();
080  protected Set<String> nonSupportedCodeSystems = new HashSet<String>();
081  protected Map<String, ValueSet> valueSets = new HashMap<String, ValueSet>();
082  protected Map<String, ConceptMap> maps = new HashMap<String, ConceptMap>();
083  
084  protected ValueSetExpanderFactory expansionCache = new ValueSetExpansionCache(this);
085  protected boolean cacheValidation; // if true, do an expansion and cache the expansion
086  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
087  protected Map<String, Map<String, ValidationResult>> validationCache = new HashMap<String, Map<String,ValidationResult>>();
088  
089  // private ValueSetExpansionCache expansionCache; //   
090
091  protected FHIRToolingClient txServer;
092  private Bundle bndCodeSystems;
093  private Locale locale;
094  private ResourceBundle i18Nmessages;
095
096  @Override
097  public CodeSystem fetchCodeSystem(String system) {
098    return codeSystems.get(system);
099  } 
100
101  @Override
102  public boolean supportsSystem(String system) {
103    if (codeSystems.containsKey(system))
104      return true;
105    else if (nonSupportedCodeSystems.contains(system))
106      return false;
107    else if (system.startsWith("http://example.org") || system.startsWith("http://acme.com") || system.startsWith("http://hl7.org/fhir/valueset-") || system.startsWith("urn:oid:"))
108      return false;
109    else {
110      System.out.println("check system "+system);
111      if (bndCodeSystems == null)
112        bndCodeSystems = txServer.fetchFeed(txServer.getAddress()+"/CodeSystem?content=not-present&_summary=true&_count=1000");
113      for (BundleEntryComponent be : bndCodeSystems.getEntry()) {
114        CodeSystem cs = (CodeSystem) be.getResource();
115        if (!codeSystems.containsKey(cs.getUrl())) {
116                codeSystems.put(cs.getUrl(), null);
117        }
118      }
119      for (BundleEntryComponent be : bndCodeSystems.getEntry()) {
120        CodeSystem cs = (CodeSystem) be.getResource();
121        if (system.equals(cs.getUrl())) {
122          return true;
123        }
124      }
125    }
126    nonSupportedCodeSystems.add(system);
127    return false;
128  }
129
130  @Override
131  public ValueSetExpansionOutcome expandVS(ValueSet vs, boolean cacheOk) {
132    try {
133      Map<String, String> params = new HashMap<String, String>();
134      params.put("_limit", "10000");
135      params.put("_incomplete", "true");
136      params.put("profile", "http://www.healthintersections.com.au/fhir/expansion/no-details");
137      ValueSet result = txServer.expandValueset(vs, null, params);
138      return new ValueSetExpansionOutcome(result);  
139    } catch (Exception e) {
140      return new ValueSetExpansionOutcome("Error expanding ValueSet \""+vs.getUrl()+": "+e.getMessage());
141    }
142  }
143
144  private ValidationResult handleByCache(ValueSet vs, Coding coding, boolean tryCache) {
145    String cacheId = cacheId(coding);
146    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
147    if (cache == null) {
148      cache = new HashMap<String, IWorkerContext.ValidationResult>();
149      validationCache.put(vs.getUrl(), cache);
150    }
151    if (cache.containsKey(cacheId))
152      return cache.get(cacheId);
153    if (!tryCache)
154      return null;
155    if (!cacheValidation)
156      return null;
157    if (failed.contains(vs.getUrl()))
158      return null;
159    ValueSetExpansionOutcome vse = expandVS(vs, true);
160    if (vse.getValueset() == null || notcomplete(vse.getValueset())) {
161      failed.add(vs.getUrl());
162      return null;
163    }
164    
165    ValidationResult res = validateCode(coding, vse.getValueset());
166    cache.put(cacheId, res);
167    return res;
168  }
169
170  private boolean notcomplete(ValueSet vs) {
171    if (!vs.hasExpansion())
172      return true;
173    if (!vs.getExpansion().getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-unclosed").isEmpty())
174      return true;
175    if (!vs.getExpansion().getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/valueset-toocostly").isEmpty())
176      return true;
177    return false;
178  }
179
180  private ValidationResult handleByCache(ValueSet vs, CodeableConcept concept, boolean tryCache) {
181    String cacheId = cacheId(concept);
182    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
183    if (cache == null) {
184      cache = new HashMap<String, IWorkerContext.ValidationResult>();
185      validationCache.put(vs.getUrl(), cache);
186    }
187    if (cache.containsKey(cacheId))
188      return cache.get(cacheId);
189    
190    if (validationCache.containsKey(vs.getUrl()) && validationCache.get(vs.getUrl()).containsKey(cacheId))
191      return validationCache.get(vs.getUrl()).get(cacheId);
192    if (!tryCache)
193      return null;
194    if (!cacheValidation)
195      return null;
196    if (failed.contains(vs.getUrl()))
197      return null;
198    ValueSetExpansionOutcome vse = expandVS(vs, true);
199    if (vse.getValueset() == null || notcomplete(vse.getValueset())) {
200      failed.add(vs.getUrl());
201      return null;
202    }
203    ValidationResult res = validateCode(concept, vse.getValueset());
204    cache.put(cacheId, res);
205    return res;
206  }
207
208  private String cacheId(Coding coding) {
209    return "|"+coding.getSystem()+"|"+coding.getVersion()+"|"+coding.getCode()+"|"+coding.getDisplay();
210  }
211  
212  private String cacheId(CodeableConcept cc) {
213    StringBuilder b = new StringBuilder();
214    for (Coding c : cc.getCoding()) {
215      b.append("#");
216      b.append(cacheId(c));
217    }    
218    return b.toString();
219  }
220  
221  private ValidationResult verifyCodeExternal(ValueSet vs, Coding coding, boolean tryCache) {
222    ValidationResult res = vs == null ? null : handleByCache(vs, coding, tryCache);
223    if (res != null)
224      return res;
225    Parameters pin = new Parameters();
226    pin.addParameter().setName("coding").setValue(coding);
227    if (vs != null)
228    pin.addParameter().setName("valueSet").setResource(vs);
229    res = serverValidateCode(pin);
230    if (vs != null) {
231    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
232    cache.put(cacheId(coding), res);
233    }
234    return res;
235  }
236  
237  private ValidationResult verifyCodeExternal(ValueSet vs, CodeableConcept cc, boolean tryCache) {
238    ValidationResult res = handleByCache(vs, cc, tryCache);
239    if (res != null)
240      return res;
241    Parameters pin = new Parameters();
242    pin.addParameter().setName("codeableConcept").setValue(cc);
243    pin.addParameter().setName("valueSet").setResource(vs);
244    res = serverValidateCode(pin);
245    Map<String, ValidationResult> cache = validationCache.get(vs.getUrl());
246    cache.put(cacheId(cc), res);
247    return res;
248  }
249
250  private ValidationResult serverValidateCode(Parameters pin) {
251  Parameters pout = txServer.operateType(ValueSet.class, "validate-code", pin);
252  boolean ok = false;
253  String message = "No Message returned";
254  String display = null;
255  for (ParametersParameterComponent p : pout.getParameter()) {
256    if (p.getName().equals("result"))
257      ok = ((BooleanType) p.getValue()).getValue().booleanValue();
258    else if (p.getName().equals("message"))
259      message = ((StringType) p.getValue()).getValue();
260    else if (p.getName().equals("display"))
261      display = ((StringType) p.getValue()).getValue();
262  }
263  if (!ok)
264    return new ValidationResult(IssueSeverity.ERROR, message);
265  else if (display != null)
266    return new ValidationResult(new ConceptDefinitionComponent().setDisplay(display));
267  else
268    return new ValidationResult(null);
269  }
270
271  
272  @Override
273  public ValueSetExpansionComponent expandVS(ConceptSetComponent inc) {
274    ValueSet vs = new ValueSet();
275    vs.setCompose(new ValueSetComposeComponent());
276    vs.getCompose().getInclude().add(inc);
277    ValueSetExpansionOutcome vse = expandVS(vs, true);
278    return vse.getValueset().getExpansion();
279  }
280
281  @Override
282  public ValidationResult validateCode(String system, String code, String display) {
283    try {
284      if (codeSystems.containsKey(system) && codeSystems.get(system) != null)
285        return verifyCodeInCodeSystem(codeSystems.get(system), system, code, display);
286      else 
287        return verifyCodeExternal(null, new Coding().setSystem(system).setCode(code).setDisplay(display), false);
288    } catch (Exception e) {
289      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage());
290    }
291  }
292
293  
294  @Override
295  public ValidationResult validateCode(Coding code, ValueSet vs) {
296    try {
297      if (codeSystems.containsKey(code.getSystem()) && codeSystems.get(code.getSystem()) != null) 
298        return verifyCodeInCodeSystem(codeSystems.get(code.getSystem()), code.getSystem(), code.getCode(), code.getDisplay());
299      else if (vs.hasExpansion()) 
300        return verifyCodeInternal(vs, code.getSystem(), code.getCode(), code.getDisplay());
301      else 
302        return verifyCodeExternal(vs, code, true);
303    } catch (Exception e) {
304      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+code.getSystem()+"\": "+e.getMessage());
305    }
306  }
307
308  @Override
309  public ValidationResult validateCode(CodeableConcept code, ValueSet vs) {
310    try {
311      if (vs.hasExpansion()) 
312        return verifyCodeInternal(vs, code);
313      else 
314        return verifyCodeExternal(vs, code, true);
315    } catch (Exception e) {
316      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code.toString()+"\": "+e.getMessage());
317    }
318  }
319
320
321  @Override
322  public ValidationResult validateCode(String system, String code, String display, ValueSet vs) {
323    try {
324      if (system == null && display == null)
325        return verifyCodeInternal(vs, code);
326      if ((codeSystems.containsKey(system)  && codeSystems.get(system) != null) || vs.hasExpansion()) 
327        return verifyCodeInternal(vs, system, code, display);
328      else 
329        return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), true);
330    } catch (Exception e) {
331      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage());
332    }
333  }
334
335  @Override
336  public ValidationResult validateCode(String system, String code, String display, ConceptSetComponent vsi) {
337    try {
338      ValueSet vs = new ValueSet().setUrl(Utilities.makeUuidUrn());
339      vs.getCompose().addInclude(vsi);
340      return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), true);
341    } catch (Exception e) {
342      return new ValidationResult(IssueSeverity.FATAL, "Error validating code \""+code+"\" in system \""+system+"\": "+e.getMessage());
343    }
344  }
345
346  @Override
347  public List<ConceptMap> findMapsForSource(String url) {
348    List<ConceptMap> res = new ArrayList<ConceptMap>();
349    for (ConceptMap map : maps.values())
350      if (((Reference) map.getSource()).getReference().equals(url)) 
351        res.add(map);
352    return res;
353  }
354
355  private ValidationResult verifyCodeInternal(ValueSet vs, CodeableConcept code) throws FileNotFoundException, ETooCostly, IOException {
356    for (Coding c : code.getCoding()) {
357      ValidationResult res = verifyCodeInternal(vs, c.getSystem(), c.getCode(), c.getDisplay());
358      if (res.isOk())
359        return res;
360    }
361    if (code.getCoding().isEmpty())
362      return new ValidationResult(IssueSeverity.ERROR, "None code provided");
363    else
364      return new ValidationResult(IssueSeverity.ERROR, "None of the codes are in the specified value set");
365  }
366
367  private ValidationResult verifyCodeInternal(ValueSet vs, String system, String code, String display) throws FileNotFoundException, ETooCostly, IOException {
368    if (vs.hasExpansion())
369      return verifyCodeInExpansion(vs, system, code, display);
370    else {
371      ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs);
372      if (vse.getValueset() != null) 
373        return verifyCodeExternal(vs, new Coding().setSystem(system).setCode(code).setDisplay(display), false);
374      else
375        return verifyCodeInExpansion(vse.getValueset(), system, code, display);
376    }
377  }
378
379  private ValidationResult verifyCodeInternal(ValueSet vs, String code) throws FileNotFoundException, ETooCostly, IOException {
380    if (vs.hasExpansion())
381      return verifyCodeInExpansion(vs, code);
382    else {
383      ValueSetExpansionOutcome vse = expansionCache.getExpander().expand(vs);
384      return verifyCodeInExpansion(vse.getValueset(), code);
385    }
386  }
387
388  private ValidationResult verifyCodeInCodeSystem(CodeSystem cs, String system, String code, String display) {
389    ConceptDefinitionComponent cc = findCodeInConcept(cs.getConcept(), code);
390    if (cc == null)
391      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+cs.getUrl());
392    if (display == null)
393      return new ValidationResult(cc);
394    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
395    if (cc.hasDisplay()) {
396      b.append(cc.getDisplay());
397      if (display.equalsIgnoreCase(cc.getDisplay()))
398        return new ValidationResult(cc);
399    }
400    for (ConceptDefinitionDesignationComponent ds : cc.getDesignation()) {
401      b.append(ds.getValue());
402      if (display.equalsIgnoreCase(ds.getValue()))
403        return new ValidationResult(cc);
404    }
405    return new ValidationResult(IssueSeverity.ERROR, "Display Name for "+code+" must be one of '"+b.toString()+"'");
406  }
407
408
409  private ValidationResult verifyCodeInExpansion(ValueSet vs, String system,String code, String display) {
410    ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code);
411    if (cc == null)
412      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+vs.getUrl());
413    if (display == null)
414      return new ValidationResult(new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay()));
415    if (cc.hasDisplay()) {
416      if (display.equalsIgnoreCase(cc.getDisplay()))
417        return new ValidationResult(new ConceptDefinitionComponent().setCode(code).setDisplay(cc.getDisplay()));
418      return new ValidationResult(IssueSeverity.ERROR, "Display Name for "+code+" must be '"+cc.getDisplay()+"'");
419    }
420    return null;
421  }
422
423  private ValidationResult verifyCodeInExpansion(ValueSet vs, String code) {
424    ValueSetExpansionContainsComponent cc = findCode(vs.getExpansion().getContains(), code);
425    if (cc == null)
426      return new ValidationResult(IssueSeverity.ERROR, "Unknown Code "+code+" in "+vs.getUrl());
427    return null;
428  }
429
430  private ValueSetExpansionContainsComponent findCode(List<ValueSetExpansionContainsComponent> contains, String code) {
431    for (ValueSetExpansionContainsComponent cc : contains) {
432      if (code.equals(cc.getCode()))
433        return cc;
434      ValueSetExpansionContainsComponent c = findCode(cc.getContains(), code);
435      if (c != null)
436        return c;
437    }
438    return null;
439  }
440
441  private ConceptDefinitionComponent findCodeInConcept(List<ConceptDefinitionComponent> concept, String code) {
442    for (ConceptDefinitionComponent cc : concept) {
443      if (code.equals(cc.getCode()))
444        return cc;
445      ConceptDefinitionComponent c = findCodeInConcept(cc.getConcept(), code);
446      if (c != null)
447        return c;
448    }
449    return null;
450  }
451
452  public Set<String> getNonSupportedCodeSystems() {
453    return nonSupportedCodeSystems;
454  }
455
456  @Override
457  public StructureDefinition fetchTypeDefinition(String typeName) {
458    return fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+typeName);
459  }
460}