001package org.hl7.fhir.r5.context;
002
003import java.util.ArrayList;
004import java.util.HashMap;
005import java.util.HashSet;
006import java.util.List;
007import java.util.Map;
008import java.util.Set;
009
010import lombok.Getter;import lombok.Setter;import lombok.extern.slf4j.Slf4j;
011import org.apache.commons.lang3.exception.ExceptionUtils;
012import org.hl7.fhir.exceptions.DefinitionException;
013import org.hl7.fhir.exceptions.FHIRException;
014import org.hl7.fhir.r5.conformance.profile.BindingResolution;
015import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider;
016import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
017import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
018import org.hl7.fhir.r5.extensions.ExtensionUtilities;
019import org.hl7.fhir.r5.model.CanonicalResource;
020import org.hl7.fhir.r5.model.CodeSystem;
021import org.hl7.fhir.r5.model.ElementDefinition;
022import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
023import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
024import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent;
025import org.hl7.fhir.r5.model.NamingSystem.NamingSystemIdentifierType;
026import org.hl7.fhir.r5.model.NamingSystem.NamingSystemUniqueIdComponent;
027import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
028import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
029import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
030import org.hl7.fhir.r5.model.StructureMap;
031
032import org.hl7.fhir.r5.utils.UserDataNames;
033import org.hl7.fhir.r5.utils.XVerExtensionManager;
034import org.hl7.fhir.r5.model.Identifier;
035import org.hl7.fhir.r5.model.NamingSystem;
036import org.hl7.fhir.r5.model.Parameters;
037import org.hl7.fhir.r5.model.Resource;
038import org.hl7.fhir.r5.model.StructureDefinition;
039import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
040import org.hl7.fhir.utilities.OIDUtilities;
041import org.hl7.fhir.utilities.Utilities;
042import org.hl7.fhir.utilities.VersionUtilities;
043import org.hl7.fhir.utilities.i18n.I18nConstants;
044import org.hl7.fhir.utilities.validation.ValidationMessage;
045import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
046import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
047
048@MarkedToMoveToAdjunctPackage
049@Slf4j
050public class ContextUtilities implements ProfileKnowledgeProvider {
051
052  private IWorkerContext context;
053  private XVerExtensionManager xverManager;
054  private Map<String, String> oidCache = new HashMap<>();
055  private List<StructureDefinition> allStructuresList = new ArrayList<StructureDefinition>();
056  private List<String> canonicalResourceNames;
057  private List<String> concreteResourceNames;
058  private Set<String> concreteResourceNameSet;
059  @Setter private List<String> suppressedMappings;
060  @Getter@Setter private Set<String> localFileNames;
061  @Getter@Setter private Set<String> masterSourceNames;
062
063  public ContextUtilities(IWorkerContext context) {
064    super();
065    this.context = context;
066    this.suppressedMappings = new ArrayList<String>();
067  }
068
069  public ContextUtilities(IWorkerContext context, List<String> suppressedMappings) {
070    super();
071    this.context = context;
072    this.suppressedMappings = suppressedMappings;
073  }
074
075  @Deprecated
076  public boolean isSuppressDebugMessages() {
077    return false;
078  }
079
080  @Deprecated
081  public void setSuppressDebugMessages(boolean suppressDebugMessages) {
082    //DO NOTHING
083  }
084  
085  public String oid2Uri(String oid) {
086    if (oid != null && oid.startsWith("urn:oid:")) {
087      oid = oid.substring(8);
088    }
089    if (oidCache.containsKey(oid)) {
090      return oidCache.get(oid);
091    }
092
093    String uri = OIDUtilities.getUriForOid(oid);
094    if (uri != null) {
095      oidCache.put(oid, uri);
096      return uri;
097    }
098    CodeSystem cs = context.fetchCodeSystem("http://terminology.hl7.org/CodeSystem/v2-tables");
099    if (cs != null) {
100      for (ConceptDefinitionComponent cc : cs.getConcept()) {
101        for (ConceptPropertyComponent cp : cc.getProperty()) {
102          if (Utilities.existsInList(cp.getCode(), "v2-table-oid", "v2-cs-oid") && oid.equals(cp.getValue().primitiveValue())) {
103            for (ConceptPropertyComponent cp2 : cc.getProperty()) {
104              if ("v2-cs-uri".equals(cp2.getCode())) {
105                oidCache.put(oid, cp2.getValue().primitiveValue());
106                return cp2.getValue().primitiveValue();                  
107              }
108            }              
109          }
110        }
111      }
112    }
113    for (CodeSystem css : context.fetchResourcesByType(CodeSystem.class)) {
114      if (("urn:oid:"+oid).equals(css.getUrl())) {
115        oidCache.put(oid, css.getUrl());
116        return css.getUrl();
117      }
118      for (Identifier id : css.getIdentifier()) {
119        if ("urn:ietf:rfc:3986".equals(id.getSystem()) && ("urn:oid:"+oid).equals(id.getValue())) {
120          oidCache.put(oid, css.getUrl());
121          return css.getUrl();
122        }
123      }
124    }
125    for (NamingSystem ns : context.fetchResourcesByType(NamingSystem.class)) {
126      if (hasOid(ns, oid)) {
127        uri = getUri(ns);
128        if (uri != null) {
129          oidCache.put(oid, null);
130          return null;
131        }
132      }
133    }
134    oidCache.put(oid, null);
135    return null;
136  }    
137
138  private String getUri(NamingSystem ns) {
139    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
140      if (id.getType() == NamingSystemIdentifierType.URI)
141        return id.getValue();
142    }
143    return null;
144  }
145
146  private boolean hasOid(NamingSystem ns, String oid) {
147    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
148      if (id.getType() == NamingSystemIdentifierType.OID && id.getValue().equals(oid))
149        return true;
150    }
151    return false;
152  }
153
154  /**
155   * @return a list of the resource and type names defined for this version
156   */
157  public List<String> getTypeNames() {
158    Set<String> result = new HashSet<String>();
159    for (StructureDefinition sd : context.fetchResourcesByType(StructureDefinition.class)) {
160      if (sd.getKind() != StructureDefinitionKind.LOGICAL && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION)
161        result.add(sd.getName());
162    }
163    return Utilities.sorted(result);
164  }
165
166
167  /**
168   * @return a set of the resource and type names defined for this version
169   */
170  public Set<String> getTypeNameSet() {
171    Set<String> result = new HashSet<String>();
172    for (StructureDefinition sd : context.fetchResourcesByType(StructureDefinition.class)) {
173      if (sd.getKind() != StructureDefinitionKind.LOGICAL && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION && 
174          VersionUtilities.versionsCompatible(context.getVersion(), sd.getFhirVersion().toCode())) {
175        result.add(sd.getName());
176      }
177    }
178    return result;
179  }
180
181  public String getLinkForUrl(String corePath, String url) {
182    if (url == null) {
183      return null;
184    }
185    
186    if (context.hasResource(CanonicalResource.class, url)) {
187      CanonicalResource  cr = context.fetchResource(CanonicalResource.class, url);
188      return cr.getWebPath();
189    }
190    return null;
191  }
192  
193
194  protected String tail(String url) {
195    if (Utilities.noString(url)) {
196      return "noname";
197    }
198    if (url.contains("/")) {
199      return url.substring(url.lastIndexOf("/")+1);
200    }
201    return url;
202  }
203  
204  private boolean hasUrlProperty(StructureDefinition sd) {
205    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
206      if (ed.getPath().equals(sd.getType()+".url")) {
207        return true;
208      }
209    }
210    return false;
211  }
212  
213  // -- profile services ---------------------------------------------------------
214  
215
216  /**
217   * @return a list of the resource names that are canonical resources defined for this version
218   */
219  public List<String> getCanonicalResourceNames() {
220    if (canonicalResourceNames == null) {
221      canonicalResourceNames =  new ArrayList<>();
222      Set<String> names = new HashSet<>();
223      for (StructureDefinition sd : allStructures()) {
224        if (sd.getKind() == StructureDefinitionKind.RESOURCE && !sd.getAbstract() && hasUrlProperty(sd)) {
225          names.add(sd.getType());
226        }
227      }
228      canonicalResourceNames.addAll(Utilities.sorted(names));
229    }
230    return canonicalResourceNames;
231  }
232
233  /**
234   * @return a list of all structure definitions, with snapshots generated (if possible)
235   */
236  public List<StructureDefinition> allStructures(){
237    if (allStructuresList.isEmpty()) {
238      Set<StructureDefinition> set = new HashSet<StructureDefinition>();
239      for (StructureDefinition sd : getStructures()) {
240        if (!set.contains(sd)) {
241          try {
242            generateSnapshot(sd);
243            // new XmlParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path("[tmp]", "snapshot", tail(sd.getUrl())+".xml")), sd);
244          } catch (Exception e) {
245            log.debug("Unable to generate snapshot @2 for "+tail(sd.getUrl()) +" from "+tail(sd.getBaseDefinition())+" because "+e.getMessage());
246            context.getLogger().logDebugMessage(ILoggingService.LogCategory.GENERATE, ExceptionUtils.getStackTrace(e));
247          }
248          allStructuresList.add(sd);
249          set.add(sd);
250        }
251      }
252    }
253    return allStructuresList;
254  }
255
256  /**
257   * @return a list of all structure definitions, without trying to generate snapshots
258   */
259  public List<StructureDefinition> getStructures() {
260    return context.fetchResourcesByType(StructureDefinition.class);
261  }
262    
263  /**
264   * Given a structure definition, generate a snapshot (or regenerate it)
265   * @param p
266   * @throws DefinitionException
267   * @throws FHIRException
268   */
269  public void generateSnapshot(StructureDefinition p) throws DefinitionException, FHIRException {
270    if ((!p.hasSnapshot() || isProfileNeedsRegenerate(p))) {
271      if (!p.hasBaseDefinition())
272        throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE___HAS_NO_BASE_AND_NO_SNAPSHOT, p.getName(), p.getUrl()));
273      StructureDefinition sd = context.fetchResource(StructureDefinition.class, p.getBaseDefinition(), p);
274      if (sd == null && "http://hl7.org/fhir/StructureDefinition/Base".equals(p.getBaseDefinition())) {
275        sd = ProfileUtilities.makeBaseDefinition(p.getFhirVersion());
276      }
277      if (sd == null) {
278        throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE___BASE__COULD_NOT_BE_RESOLVED, p.getName(), p.getUrl(), p.getBaseDefinition()));
279      }
280      List<ValidationMessage> msgs = new ArrayList<ValidationMessage>();
281      List<String> errors = new ArrayList<String>();
282      ProfileUtilities pu = new ProfileUtilities(context, msgs, this);
283      pu.setAutoFixSliceNames(true);
284      pu.setThrowException(false);
285      pu.setForPublication(context.isForPublication());
286      pu.setSuppressedMappings(suppressedMappings);
287      pu.setLocalFileNames(localFileNames);
288      pu.setMasterSourceFileNames(masterSourceNames);
289      if (xverManager == null) {
290        xverManager = new XVerExtensionManager(context);
291      }
292      pu.setXver(xverManager);
293      if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
294        pu.sortDifferential(sd, p, p.getUrl(), errors, true);
295      }
296      pu.setDebug(false);
297      for (String err : errors) {
298        msgs.add(new ValidationMessage(Source.ProfileValidator, IssueType.EXCEPTION, p.getWebPath(), "Error sorting Differential: "+err, ValidationMessage.IssueSeverity.ERROR));
299      }
300      pu.generateSnapshot(sd, p, p.getUrl(), sd.getUserString(UserDataNames.render_webroot), p.getName());
301      for (ValidationMessage msg : msgs) {
302        if ((!ProfileUtilities.isSuppressIgnorableExceptions() && msg.getLevel() == ValidationMessage.IssueSeverity.ERROR) || msg.getLevel() == ValidationMessage.IssueSeverity.FATAL) {
303          if (!msg.isIgnorableError()) {
304            throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE___ELEMENT__ERROR_GENERATING_SNAPSHOT_, p.getName(), p.getUrl(), msg.getLocation(), msg.getMessage()));
305          } else {
306            log.error(msg.getMessage());
307          }
308        }
309      }
310      if (!p.hasSnapshot())
311        throw new FHIRException(context.formatMessage(I18nConstants.PROFILE___ERROR_GENERATING_SNAPSHOT, p.getName(), p.getUrl()));
312      pu = null;
313    }
314    p.setGeneratedSnapshot(true);
315  }
316  
317
318  // work around the fact that some Implementation guides were published with old snapshot generators that left invalid snapshots behind.
319  private boolean isProfileNeedsRegenerate(StructureDefinition p) {
320    boolean needs = !p.hasUserData(UserDataNames.SNAPSHOT_regeneration_tracker) && Utilities.existsInList(p.getUrl(), "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse");
321    if (needs) {
322      p.setUserData(UserDataNames.SNAPSHOT_regeneration_tracker, "yes");
323    }
324    return needs;
325  }
326
327  @Override
328  public boolean isPrimitiveType(String type) {
329    return context.isPrimitiveType(type);
330  }
331
332  @Override
333  public boolean isDatatype(String type) {
334    StructureDefinition sd = context.fetchTypeDefinition(type);
335    return sd != null && (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE || sd.getKind() == StructureDefinitionKind.COMPLEXTYPE) && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION;
336  }
337
338  @Override
339  public boolean isResource(String t) {
340    if (getConcreteResourceSet().contains(t)) {
341      return true;
342    }
343    StructureDefinition sd;
344    try {
345      sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+t);
346    } catch (Exception e) {
347      return false;
348    }
349    if (sd == null)
350      return false;
351    if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT)
352      return false;
353    return sd.getKind() == StructureDefinitionKind.RESOURCE;
354  }
355
356  @Override
357  public boolean hasLinkFor(String typeSimple) {
358    return false;
359  }
360
361  @Override
362  public String getLinkFor(String corePath, String typeSimple) {
363    return null;
364  }
365
366  @Override
367  public BindingResolution resolveBinding(StructureDefinition profile, ElementDefinitionBindingComponent binding, String path) {
368    return null;
369  }
370
371  @Override
372  public BindingResolution resolveBinding(StructureDefinition profile, String url, String path) {
373    return null;
374  }
375
376  @Override
377  public String getLinkForProfile(StructureDefinition profile, String url) {
378    return null;
379  }
380  @Override
381  public boolean prependLinks() {
382    return false;
383  }
384
385  public StructureDefinition fetchByJsonName(String key) {
386    for (StructureDefinition sd : context.fetchResourcesByType(StructureDefinition.class)) {
387      ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
388      if (/*sd.getKind() == StructureDefinitionKind.LOGICAL && */ 
389          // this is turned off because it's valid to use a FHIR type directly in
390          // an extension of this kind, and that can't be a logical model. Any profile on
391          // a type is acceptable as long as it has the json name on it  
392          ed != null && ed.hasExtension(ExtensionDefinitions.EXT_JSON_NAME, ExtensionDefinitions.EXT_JSON_NAME_DEPRECATED) &&
393          key.equals(ExtensionUtilities.readStringExtension(ed, ExtensionDefinitions.EXT_JSON_NAME, ExtensionDefinitions.EXT_JSON_NAME_DEPRECATED))) {
394        return sd;
395      }
396    }
397    return null;
398  }
399
400  public Set<String> getConcreteResourceSet() {
401    if (concreteResourceNameSet == null) {
402      concreteResourceNameSet =  new HashSet<>();
403      for (StructureDefinition sd : getStructures()) {
404        if (sd.getKind() == StructureDefinitionKind.RESOURCE && !sd.getAbstract() && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) {
405          concreteResourceNameSet.add(sd.getType());
406        }
407      }
408    }
409    return concreteResourceNameSet;
410  }
411
412  public List<String> getConcreteResources() {
413    if (concreteResourceNames == null) {
414      concreteResourceNames =  new ArrayList<>();
415      concreteResourceNames.addAll(Utilities.sorted(getConcreteResourceSet()));
416    }
417    return concreteResourceNames;
418  }
419
420  public List<StructureMap> listMaps(String url) {
421    List<StructureMap> res = new ArrayList<>();
422    String start = url.substring(0, url.indexOf("*"));
423    String end = url.substring(url.indexOf("*")+1);
424    for (StructureMap map : context.fetchResourcesByType(StructureMap.class)) {
425      String u = map.getUrl();
426      if (u.startsWith(start) && u.endsWith(end)) {
427        res.add(map);
428      }
429    }
430    return res;
431  }
432
433  public List<String> fetchCodeSystemVersions(String system) {
434    List<String> res = new ArrayList<>();
435    for (CodeSystem cs : context.fetchResourceVersionsByTypeAndUrl(CodeSystem.class, system)) {
436      if (cs.hasVersion()) {
437        res.add(cs.getVersion());
438      }
439    }
440    return res;
441  }
442
443  public StructureDefinition findType(String typeName) {
444    StructureDefinition t = context.fetchTypeDefinition(typeName);
445    if (t != null) {
446      return t;
447    }
448    List<StructureDefinition> candidates = new ArrayList<>();
449    for (StructureDefinition sd : getStructures()) {
450      if (sd.getType().equals(typeName)) {
451        candidates.add(sd);
452      }
453    }
454    if (candidates.size() == 1) {
455      return candidates.get(0);
456    }
457    return null;
458  }
459
460  public StructureDefinition fetchProfileByIdentifier(String tid) {
461    for (StructureDefinition sd : context.fetchResourcesByType(StructureDefinition.class)) {
462      for (Identifier ii : sd.getIdentifier()) {
463        if (tid.equals(ii.getValue())) {
464          return sd;
465        }
466      }
467    }
468    return null;
469  }
470
471  public boolean isAbstractType(String typeName) {
472    StructureDefinition sd = context.fetchTypeDefinition(typeName);
473    if (sd != null) {
474      return sd.getAbstract();
475    }
476    return false;
477  }
478
479  public boolean isDomainResource(String typeName) {
480    StructureDefinition sd = context.fetchTypeDefinition(typeName);
481    while (sd != null) {
482      if ("DomainResource".equals(sd.getType())) {
483        return true;
484      }
485      sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());  
486    }
487    return false;
488  }
489
490  public IWorkerContext getWorker() {
491    return context;     
492  }
493
494  @Override
495  public String getCanonicalForDefaultContext() {
496    // TODO Auto-generated method stub
497    return null;
498  }
499
500  public String pinValueSet(String valueSet) {
501    return pinValueSet(valueSet, context.getExpansionParameters());
502  }
503
504  public String pinValueSet(String value, Parameters expParams) {
505    if (value.contains("|")) {
506      return value;
507    }
508    for (ParametersParameterComponent p : expParams.getParameter()) {
509      if ("default-valueset-version".equals(p.getName())) {
510        String s = p.getValue().primitiveValue();
511        if (s.startsWith(value+"|")) {
512          return s;
513        }
514      }
515    }
516    return value;
517  }
518
519  public List<StructureDefinition> allBaseStructures() {
520    List<StructureDefinition> res = new ArrayList<>();
521    for (StructureDefinition sd : allStructures()) {
522      if (sd.getDerivation() == TypeDerivationRule.SPECIALIZATION && sd.getKind() != StructureDefinitionKind.LOGICAL) {
523        res.add(sd);
524      }
525    }
526    return res;
527  }
528
529  public <T extends Resource> List<T> fetchByIdentifier(Class<T> class_, String system) {
530    List<T> list = new ArrayList<>();
531    for (T t : context.fetchResourcesByType(class_)) {
532      if (t instanceof CanonicalResource) {
533        CanonicalResource cr = (CanonicalResource) t;
534        for (Identifier id : cr.getIdentifier()) {
535          if (system.equals(id.getValue())) {
536            list.add(t);
537          }
538        }
539      }
540    }
541    return list;
542  }
543
544  public StructureDefinition fetchStructureByName(String name) {
545    StructureDefinition sd = null;
546    for (StructureDefinition t : context.fetchResourcesByType(StructureDefinition.class)) {
547      if (name.equals(t.getName())) {
548        if (sd == null) {
549          sd = t;
550        } else {
551          throw new FHIRException("Duplicate Structure name "+name+": found both "+t.getVersionedUrl()+" and "+sd.getVersionedUrl());
552        }
553      }
554    }
555    return sd;
556  }
557
558  @Override
559  public String getDefinitionsName(Resource r) {
560    // TODO Auto-generated method stub
561    return null;
562  }
563
564  }
565