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