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