001package org.hl7.fhir.r5.context;
002
003import java.util.ArrayList;
004import java.util.Collection;
005import java.util.Collections;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Map;
010import java.util.Set;
011
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.StructureDefinition.StructureDefinitionKind;
026import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
027import org.hl7.fhir.r5.model.StructureMap;
028import org.hl7.fhir.r5.utils.ToolingExtensions;
029import org.hl7.fhir.r5.utils.XVerExtensionManager;
030import org.hl7.fhir.r5.model.Identifier;
031import org.hl7.fhir.r5.model.NamingSystem;
032import org.hl7.fhir.r5.model.StructureDefinition;
033import org.hl7.fhir.utilities.OIDUtils;
034import org.hl7.fhir.utilities.Utilities;
035import org.hl7.fhir.utilities.VersionUtilities;
036import org.hl7.fhir.utilities.i18n.I18nConstants;
037import org.hl7.fhir.utilities.validation.ValidationMessage;
038import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
039import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
040
041public class ContextUtilities implements ProfileKnowledgeProvider {
042
043  private IWorkerContext context;
044  private boolean suppressDebugMessages;
045  private boolean ignoreProfileErrors;
046  private XVerExtensionManager xverManager;
047  private Map<String, String> oidCache = new HashMap<>();
048  private List<StructureDefinition> allStructuresList = new ArrayList<StructureDefinition>();
049  private List<String> canonicalResourceNames;
050  private List<String> concreteResourceNames;
051  
052  public ContextUtilities(IWorkerContext context) {
053    super();
054    this.context = context;
055  }
056
057  public boolean isSuppressDebugMessages() {
058    return suppressDebugMessages;
059  }
060
061  public void setSuppressDebugMessages(boolean suppressDebugMessages) {
062    this.suppressDebugMessages = suppressDebugMessages;
063  }
064  public boolean isIgnoreProfileErrors() {
065    return ignoreProfileErrors;
066  }
067
068  public void setIgnoreProfileErrors(boolean ignoreProfileErrors) {
069    this.ignoreProfileErrors = ignoreProfileErrors;
070  }
071
072  public String oid2Uri(String oid) {
073    if (oid != null && oid.startsWith("urn:oid:")) {
074      oid = oid.substring(8);
075    }
076    if (oidCache.containsKey(oid)) {
077      return oidCache.get(oid);
078    }
079
080    String uri = OIDUtils.getUriForOid(oid);
081    if (uri != null) {
082      oidCache.put(oid, uri);
083      return uri;
084    }
085    CodeSystem cs = context.fetchCodeSystem("http://terminology.hl7.org/CodeSystem/v2-tables");
086    if (cs != null) {
087      for (ConceptDefinitionComponent cc : cs.getConcept()) {
088        for (ConceptPropertyComponent cp : cc.getProperty()) {
089          if (Utilities.existsInList(cp.getCode(), "v2-table-oid", "v2-cs-oid") && oid.equals(cp.getValue().primitiveValue())) {
090            for (ConceptPropertyComponent cp2 : cc.getProperty()) {
091              if ("v2-cs-uri".equals(cp2.getCode())) {
092                oidCache.put(oid, cp2.getValue().primitiveValue());
093                return cp2.getValue().primitiveValue();                  
094              }
095            }              
096          }
097        }
098      }
099    }
100    for (CodeSystem css : context.fetchResourcesByType(CodeSystem.class)) {
101      if (("urn:oid:"+oid).equals(css.getUrl())) {
102        oidCache.put(oid, css.getUrl());
103        return css.getUrl();
104      }
105      for (Identifier id : css.getIdentifier()) {
106        if ("urn:ietf:rfc:3986".equals(id.getSystem()) && ("urn:oid:"+oid).equals(id.getValue())) {
107          oidCache.put(oid, css.getUrl());
108          return css.getUrl();
109        }
110      }
111    }
112    for (NamingSystem ns : context.fetchResourcesByType(NamingSystem.class)) {
113      if (hasOid(ns, oid)) {
114        uri = getUri(ns);
115        if (uri != null) {
116          oidCache.put(oid, null);
117          return null;
118        }
119      }
120    }
121    oidCache.put(oid, null);
122    return null;
123  }    
124
125  private String getUri(NamingSystem ns) {
126    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
127      if (id.getType() == NamingSystemIdentifierType.URI)
128        return id.getValue();
129    }
130    return null;
131  }
132
133  private boolean hasOid(NamingSystem ns, String oid) {
134    for (NamingSystemUniqueIdComponent id : ns.getUniqueId()) {
135      if (id.getType() == NamingSystemIdentifierType.OID && id.getValue().equals(oid))
136        return true;
137    }
138    return false;
139  }
140
141  /**
142   * @return a list of the resource and type names defined for this version
143   */
144  public List<String> getTypeNames() {
145    Set<String> result = new HashSet<String>();
146    for (StructureDefinition sd : context.fetchResourcesByType(StructureDefinition.class)) {
147      if (sd.getKind() != StructureDefinitionKind.LOGICAL && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION)
148        result.add(sd.getName());
149    }
150    return Utilities.sorted(result);
151  }
152
153
154  /**
155   * @return a set of the resource and type names defined for this version
156   */
157  public Set<String> getTypeNameSet() {
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          VersionUtilities.versionsCompatible(context.getVersion(), sd.getFhirVersion().toCode())) {
162        result.add(sd.getName());
163      }
164    }
165    return result;
166  }
167
168  public String getLinkForUrl(String corePath, String url) {
169    if (url == null) {
170      return null;
171    }
172    
173    if (context.hasResource(CanonicalResource.class, url)) {
174      CanonicalResource  cr = context.fetchResource(CanonicalResource.class, url);
175      return cr.getWebPath();
176    }
177    return null;
178  }
179  
180
181  protected String tail(String url) {
182    if (Utilities.noString(url)) {
183      return "noname";
184    }
185    if (url.contains("/")) {
186      return url.substring(url.lastIndexOf("/")+1);
187    }
188    return url;
189  }
190  
191  private boolean hasUrlProperty(StructureDefinition sd) {
192    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
193      if (ed.getPath().equals(sd.getType()+".url")) {
194        return true;
195      }
196    }
197    return false;
198  }
199  
200  // -- profile services ---------------------------------------------------------
201  
202
203  /**
204   * @return a list of the resource names that are canonical resources defined for this version
205   */
206  public List<String> getCanonicalResourceNames() {
207    if (canonicalResourceNames == null) {
208      canonicalResourceNames =  new ArrayList<>();
209      Set<String> names = new HashSet<>();
210      for (StructureDefinition sd : allStructures()) {
211        if (sd.getKind() == StructureDefinitionKind.RESOURCE && !sd.getAbstract() && hasUrlProperty(sd)) {
212          names.add(sd.getType());
213        }
214      }
215      canonicalResourceNames.addAll(Utilities.sorted(names));
216    }
217    return canonicalResourceNames;
218  }
219
220  /**
221   * @return a list of all structure definitions, with snapshots generated (if possible)
222   */
223  public List<StructureDefinition> allStructures(){
224    if (allStructuresList.isEmpty()) {
225      Set<StructureDefinition> set = new HashSet<StructureDefinition>();
226      for (StructureDefinition sd : getStructures()) {
227        if (!set.contains(sd)) {
228          try {
229            generateSnapshot(sd);
230            // new XmlParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path("[tmp]", "snapshot", tail(sd.getUrl())+".xml")), sd);
231          } catch (Exception e) {
232            if (!isSuppressDebugMessages()) {
233              System.out.println("Unable to generate snapshot @2 for "+tail(sd.getUrl()) +" from "+tail(sd.getBaseDefinition())+" because "+e.getMessage());
234              if (context.getLogger().isDebugLogging()) {
235                e.printStackTrace();
236              }
237            }
238          }
239          allStructuresList.add(sd);
240          set.add(sd);
241        }
242      }
243    }
244    return allStructuresList;
245  }
246
247  /**
248   * @return a list of all structure definitions, without trying to generate snapshots
249   */
250  public List<StructureDefinition> getStructures() {
251    return context.fetchResourcesByType(StructureDefinition.class);
252  }
253    
254  /**
255   * Given a structure definition, generate a snapshot (or regenerate it)
256   * @param p
257   * @throws DefinitionException
258   * @throws FHIRException
259   */
260  public void generateSnapshot(StructureDefinition p) throws DefinitionException, FHIRException {
261    generateSnapshot(p, false);
262  }
263  
264  public void generateSnapshot(StructureDefinition p, boolean ifLogical) {
265    if ((!p.hasSnapshot() || isProfileNeedsRegenerate(p) ) && (ifLogical || p.getKind() != StructureDefinitionKind.LOGICAL)) {
266      if (!p.hasBaseDefinition())
267        throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE___HAS_NO_BASE_AND_NO_SNAPSHOT, p.getName(), p.getUrl()));
268      StructureDefinition sd = context.fetchResource(StructureDefinition.class, p.getBaseDefinition(), p);
269      if (sd == null && "http://hl7.org/fhir/StructureDefinition/Base".equals(p.getBaseDefinition())) {
270        sd = ProfileUtilities.makeBaseDefinition(p.getFhirVersion());
271      }
272      if (sd == null) {
273        throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE___BASE__COULD_NOT_BE_RESOLVED, p.getName(), p.getUrl(), p.getBaseDefinition()));
274      }
275      List<ValidationMessage> msgs = new ArrayList<ValidationMessage>();
276      List<String> errors = new ArrayList<String>();
277      ProfileUtilities pu = new ProfileUtilities(context, msgs, this);
278      pu.setAutoFixSliceNames(true);
279      pu.setThrowException(false);
280      pu.setForPublication(context.isForPublication());
281      if (xverManager == null) {
282        xverManager = new XVerExtensionManager(context);
283      }
284      pu.setXver(xverManager);
285      if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) {
286        pu.sortDifferential(sd, p, p.getUrl(), errors, true);
287      }
288      pu.setDebug(false);
289      for (String err : errors) {
290        msgs.add(new ValidationMessage(Source.ProfileValidator, IssueType.EXCEPTION, p.getWebPath(), "Error sorting Differential: "+err, ValidationMessage.IssueSeverity.ERROR));
291      }
292      pu.generateSnapshot(sd, p, p.getUrl(), sd.getUserString("webroot"), p.getName());
293      for (ValidationMessage msg : msgs) {
294        if ((!ignoreProfileErrors && msg.getLevel() == ValidationMessage.IssueSeverity.ERROR) || msg.getLevel() == ValidationMessage.IssueSeverity.FATAL) {
295          if (!msg.isIgnorableError()) {
296            throw new DefinitionException(context.formatMessage(I18nConstants.PROFILE___ELEMENT__ERROR_GENERATING_SNAPSHOT_, p.getName(), p.getUrl(), msg.getLocation(), msg.getMessage()));
297          } else {
298            System.err.println(msg.getMessage());
299          }
300        }
301      }
302      if (!p.hasSnapshot())
303        throw new FHIRException(context.formatMessage(I18nConstants.PROFILE___ERROR_GENERATING_SNAPSHOT, p.getName(), p.getUrl()));
304      pu = null;
305    }
306    p.setGeneratedSnapshot(true);
307  }
308  
309
310  // work around the fact that some Implementation guides were published with old snapshot generators that left invalid snapshots behind.
311  private boolean isProfileNeedsRegenerate(StructureDefinition p) {
312    boolean needs = !p.hasUserData("hack.regnerated") && Utilities.existsInList(p.getUrl(), "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse");
313    if (needs) {
314      p.setUserData("hack.regnerated", "yes");
315    }
316    return needs;
317  }
318
319  @Override
320  public boolean isPrimitiveType(String type) {
321    StructureDefinition sd = context.fetchTypeDefinition(type);
322    return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
323  }
324
325  @Override
326  public boolean isDatatype(String type) {
327    StructureDefinition sd = context.fetchTypeDefinition(type);
328    return sd != null && (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE || sd.getKind() == StructureDefinitionKind.COMPLEXTYPE) && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION;
329  }
330
331  @Override
332  public boolean isResource(String t) {
333    StructureDefinition sd;
334    try {
335      sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+t);
336    } catch (Exception e) {
337      return false;
338    }
339    if (sd == null)
340      return false;
341    if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT)
342      return false;
343    return sd.getKind() == StructureDefinitionKind.RESOURCE;
344  }
345
346  @Override
347  public boolean hasLinkFor(String typeSimple) {
348    return false;
349  }
350
351  @Override
352  public String getLinkFor(String corePath, String typeSimple) {
353    return null;
354  }
355
356  @Override
357  public BindingResolution resolveBinding(StructureDefinition profile, ElementDefinitionBindingComponent binding, String path) {
358    return null;
359  }
360
361  @Override
362  public BindingResolution resolveBinding(StructureDefinition profile, String url, String path) {
363    return null;
364  }
365
366  @Override
367  public String getLinkForProfile(StructureDefinition profile, String url) {
368    return null;
369  }
370  @Override
371  public boolean prependLinks() {
372    return false;
373  }
374
375  public boolean isPrimitiveDatatype(String type) {
376    StructureDefinition sd = context.fetchTypeDefinition(type);
377    return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
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 && ed != null && ed.hasExtension(ToolingExtensions.EXT_JSON_NAME) && 
384          key.equals(ToolingExtensions.readStringExtension(ed, ToolingExtensions.EXT_JSON_NAME))) {
385        return sd;
386      }
387    }
388    return null;
389  }
390
391  public List<String>  getConcreteResources() {
392    if (concreteResourceNames == null) {
393      concreteResourceNames =  new ArrayList<>();
394      Set<String> names = new HashSet<>();
395      for (StructureDefinition sd : allStructures()) {
396        if (sd.getKind() == StructureDefinitionKind.RESOURCE && !sd.getAbstract()) {
397          names.add(sd.getType());
398        }
399      }
400      concreteResourceNames.addAll(Utilities.sorted(names));
401    }
402    return concreteResourceNames;
403  }
404
405  public List<StructureMap> listMaps(String url) {
406    List<StructureMap> res = new ArrayList<>();
407    String start = url.substring(0, url.indexOf("*"));
408    String end = url.substring(url.indexOf("*")+1);
409    for (StructureMap map : context.fetchResourcesByType(StructureMap.class)) {
410      String u = map.getUrl();
411      if (u.startsWith(start) && u.endsWith(end)) {
412        res.add(map);
413      }
414    }
415    return res;
416  }
417
418  public List<String> fetchCodeSystemVersions(String system) {
419    List<String> res = new ArrayList<>();
420    for (CodeSystem cs : context.fetchResourcesByType(CodeSystem.class)) {
421      if (system.equals(cs.getUrl()) && cs.hasVersion()) {
422        res.add(cs.getVersion());
423      }
424    }
425    return res;
426  }
427
428  public StructureDefinition findType(String typeName) {
429    StructureDefinition t = context.fetchTypeDefinition(typeName);
430    if (t != null) {
431      return t;
432    }
433    List<StructureDefinition> candidates = new ArrayList<>();
434    for (StructureDefinition sd : getStructures()) {
435      if (sd.getType().equals(typeName)) {
436        candidates.add(sd);
437      }
438    }
439    if (candidates.size() == 1) {
440      return candidates.get(0);
441    }
442    return null;
443  }
444
445}
446