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