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