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