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