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