001package org.hl7.fhir.r5.profilemodel;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import org.apache.commons.lang3.NotImplementedException;
007import org.hl7.fhir.exceptions.DefinitionException;
008import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
009import org.hl7.fhir.r5.context.ContextUtilities;
010import org.hl7.fhir.r5.context.IWorkerContext;
011import org.hl7.fhir.r5.model.Base;
012import org.hl7.fhir.r5.model.CanonicalType;
013import org.hl7.fhir.r5.model.ElementDefinition;
014import org.hl7.fhir.r5.model.ElementDefinition.DiscriminatorType;
015import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingComponent;
016import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent;
017import org.hl7.fhir.r5.model.ElementDefinition.SlicingRules;
018import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
019import org.hl7.fhir.r5.model.Resource;
020import org.hl7.fhir.r5.model.ResourceFactory;
021import org.hl7.fhir.r5.model.StructureDefinition;
022import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
023import org.hl7.fhir.r5.utils.FHIRPathEngine;
024import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
025import org.hl7.fhir.utilities.Utilities;
026
027/**
028 * Factory class for the ProfiledElement sub-system
029 * 
030 * *** NOTE: This sub-system is still under development ***
031 * 
032 * This subsystem takes a profile and creates a view of the profile that stitches
033 * all the parts together, and presents it as a seemless tree. There's two views:
034 * 
035 *  - definition: A logical view of the contents of the profile 
036 *  - instance: a logical view of a resource that conforms to the profile
037 *  
038 * The tree of elements in the profile model is different to the the base resource:
039 *  - some elements are removed (max = 0)
040 *  - extensions are turned into named elements 
041 *  - slices are turned into named elements 
042 *  - element properties - doco, cardinality, binding etc is updated for what the profile says
043 * 
044 * Definition
045 * ----------
046 * This presents a single view of the contents of a resource as specified by 
047 * the profile. It's suitable for use in any kind of tree view. 
048 * 
049 * Each node has a unique name amongst it's siblings, but this name may not be 
050 * the name in the instance, since slicing splits up a single named element into 
051 * different definitions.
052 * 
053 * Each node has:
054 *   - name (unique amongst siblings)
055 *   - schema name (the actual name in the instance)
056 *   - min cardinality 
057 *   - max cardinality 
058 *   - short documentation (for the tree view)
059 *   - full documentation (markdown source)
060 *   - profile definition - the full definition in the profile
061 *   - base definition - the full definition at the resource level
062 *   - types() - a list of possible types
063 *   - children(type) - a list of child nodes for the provided type 
064 *   - expansion - if there's a binding, the codes in the expansion based on the binding
065 *   
066 * Note that the tree may not have leaves; the trees recurse indefinitely because 
067 * extensions have extensions etc. So you can't do a depth-first search of the tree
068 * without some kind of decision to stop at a given point. 
069 * 
070 * Instance
071 * --------
072 * 
073 * todo
074 * 
075 * @author grahamegrieve
076 *
077 */
078public class PEBuilder {
079
080  public enum PEElementPropertiesPolicy {
081    NONE, EXTENSION, EXTENSION_ID
082  }
083
084  private IWorkerContext context;
085  private ProfileUtilities pu;
086  private ContextUtilities cu;
087  private PEElementPropertiesPolicy elementProps;
088  private boolean fixedPropsDefault;
089  private FHIRPathEngine fpe;
090
091  /**
092   * @param context - must be loaded with R5 definitions
093   * @param elementProps - whether to include Element.id and Element.extension in the tree. Recommended choice: Extension
094   */
095  public PEBuilder(IWorkerContext context, PEElementPropertiesPolicy elementProps, boolean fixedPropsDefault) {
096    super();
097    this.context = context;
098    this.elementProps = elementProps;
099    this.fixedPropsDefault = fixedPropsDefault;
100    pu = new ProfileUtilities(context, null, null);
101    cu = new ContextUtilities(context);
102    fpe = new FHIRPathEngine(context, pu);
103  }
104  
105  /**
106   * Given a profile, return a tree of the elements defined in the profile model. This builds the profile model
107   * for the provided version of the nominated profile
108   * 
109   * The tree of elements in the profile model is different to those defined in the base resource:
110   *  - some elements are removed (max = 0)
111   *  - extensions are turned into named elements 
112   *  - slices are turned into named elements 
113   *  - element properties - doco, cardinality, binding etc is updated for what the profile says
114   * 
115   * Warning: profiles and resources are recursive; you can't iterate this tree until it you get 
116   * to the leaves because there are nodes that don't terminate (extensions have extensions)
117   * 
118   */
119  public PEDefinition buildPEDefinition(StructureDefinition profile) {
120    if (!profile.hasSnapshot()) {
121      throw new DefinitionException("Profile '"+profile.getVersionedUrl()+"' does not have a snapshot");      
122    }
123    return new PEDefinitionResource(this, profile, profile.getName());
124  }
125  
126  /**
127   * Given a profile, return a tree of the elements defined in the profile model. This builds the profile model
128   * for the latest version of the nominated profile
129   * 
130   * The tree of elements in the profile model is different to those defined in the base resource:
131   *  - some elements are removed (max = 0)
132   *  - extensions are turned into named elements 
133   *  - slices are turned into named elements 
134   *  - element properties - doco, cardinality, binding etc is updated for what the profile says
135   * 
136   * Warning: profiles and resources are recursive; you can't iterate this tree until it you get 
137   * to the leaves because there are nodes that don't terminate (extensions have extensions)
138   * 
139   */
140  public PEDefinition buildPEDefinition(String url) {
141    StructureDefinition profile = getProfile(url);
142    if (profile == null) {
143      throw new DefinitionException("Unable to find profile for URL '"+url+"'");
144    }
145    if (!profile.hasSnapshot()) {
146      throw new DefinitionException("Profile '"+url+"' does not have a snapshot");      
147    }
148    return new PEDefinitionResource(this, profile, profile.getName());
149  }
150  
151  /**
152   * Given a profile, return a tree of the elements defined in the profile model. This builds the profile model
153   * for the nominated version of the nominated profile
154   * 
155   * The tree of elements in the profile model is different to the the base resource:
156   *  - some elements are removed (max = 0)
157   *  - extensions are turned into named elements 
158   *  - slices are turned into named elements 
159   *  - element properties - doco, cardinality, binding etc is updated for what the profile says
160   * 
161   * Warning: profiles and resources can be recursive; you can't iterate this tree until it you get 
162   * to the leaves because you will never get to a child that doesn't have children
163   * 
164   */
165  public PEDefinition buildPEDefinition(String url, String version) {
166    StructureDefinition profile = getProfile(url, version);
167    if (profile == null) {
168      throw new DefinitionException("Unable to find profile for URL '"+url+"'");
169    }
170    if (!profile.hasSnapshot()) {
171      throw new DefinitionException("Profile '"+url+"' does not have a snapshot");      
172    }
173    return new PEDefinitionResource(this, profile, profile.getName());
174  }
175  
176  /**
177   * Given a resource and a profile, return a tree of instance data as defined by the profile model 
178   * using the latest version of the profile
179   * 
180   * The tree is a facade to the underlying resource - all actual data is stored against the resource,
181   * and retrieved on the fly from the resource, so that applications can work at either level, as 
182   * convenient. 
183   * 
184   * Note that there's a risk that deleting something through the resource while holding 
185   * a handle to a PEInstance that is a facade on what is deleted leaves an orphan facade 
186   * that will continue to function, but is making changes to resource content that is no 
187   * longer part of the resource 
188   * 
189   */
190  public PEInstance buildPEInstance(String url, Resource resource) {
191    PEDefinition defn = buildPEDefinition(url);
192    return loadInstance(defn, resource);
193  }
194  
195  /**
196   * Given a resource and a profile, return a tree of instance data as defined by the profile model 
197   * using the provided version of the profile
198   * 
199   * The tree is a facade to the underlying resource - all actual data is stored against the resource,
200   * and retrieved on the fly from the resource, so that applications can work at either level, as 
201   * convenient. 
202   * 
203   * Note that there's a risk that deleting something through the resource while holding 
204   * a handle to a PEInstance that is a facade on what is deleted leaves an orphan facade 
205   * that will continue to function, but is making changes to resource content that is no 
206   * longer part of the resource 
207   * 
208   */
209  public PEInstance buildPEInstance(StructureDefinition profile, Resource resource) {
210    PEDefinition defn = buildPEDefinition(profile);
211    return loadInstance(defn, resource);
212  }
213  
214  /**
215   * Given a resource and a profile, return a tree of instance data as defined by the profile model 
216   * using the nominated version of the profile
217   * 
218   * The tree is a facade to the underlying resource - all actual data is stored against the resource,
219   * and retrieved on the fly from the resource, so that applications can work at either level, as 
220   * convenient. 
221   * 
222   * Note that there's a risk that deleting something through the resource while holding 
223   * a handle to a PEInstance that is a facade on what is deleted leaves an orphan facade 
224   * that will continue to function, but is making changes to resource content that is no 
225   * longer part of the resource 
226   */
227  public PEInstance buildPEInstance(String url, String version, Resource resource) {
228    PEDefinition defn = buildPEDefinition(url, version);
229    return loadInstance(defn, resource);
230  }
231  
232  /**
233   * For the current version of a profile, construct a resource and fill out any fixed or required elements
234   * 
235   * Note that fixed values are filled out irrespective of the value of fixedProps when the builder is created
236   * 
237   * @param url identifies the profile
238   * @param version identifies the version of the profile
239   * @param meta whether to mark the profile in Resource.meta.profile 
240   * @return constructed resource
241   */
242  public Resource createResource(String url, String version, boolean meta) {
243    PEDefinition definition = buildPEDefinition(url, version);
244    Resource res = ResourceFactory.createResource(definition.types().get(0).getType());
245    populateByProfile(res, definition);
246    if (meta) {
247      res.getMeta().addProfile(definition.profile.getUrl());
248    }
249    return res;
250  }
251
252  /**
253   * For the provided version of a profile, construct a resource and fill out any fixed or required elements
254   * 
255   * Note that fixed values are filled out irrespective of the value of fixedProps when the builder is created
256   * 
257   * @param profile  the profile
258   * @param meta whether to mark the profile in Resource.meta.profile 
259   * @return constructed resource
260   */
261  public Resource createResource(StructureDefinition profile, boolean meta) {
262    PEDefinition definition = buildPEDefinition(profile);
263    Resource res = ResourceFactory.createResource(definition.types().get(0).getType());
264    populateByProfile(res, definition);
265    if (meta) {
266      res.getMeta().addProfile(definition.profile.getUrl());
267    }
268    return res;
269  }
270
271  /**
272   * For the current version of a profile, construct a resource and fill out any fixed or required elements
273   * 
274   * Note that fixed values are filled out irrespective of the value of fixedProps when the builder is created
275   * 
276   * @param url identifies the profile
277   * @param meta whether to mark the profile in Resource.meta.profile 
278   * @return constructed resource
279   */
280  public Resource createResource(String url, boolean meta) {
281    PEDefinition definition = buildPEDefinition(url);
282    Resource res = ResourceFactory.createResource(definition.types().get(0).getType());
283    populateByProfile(res, definition);
284    if (meta) {
285      res.getMeta().addProfile(definition.profile.getUrl());
286    }
287    return res;
288  }
289
290
291
292  // -- methods below here are only used internally to the package
293
294  private StructureDefinition getProfile(String url) {
295    return context.fetchResource(StructureDefinition.class, url);
296  }
297
298
299  private StructureDefinition getProfile(String url, String version) {
300    return context.fetchResource(StructureDefinition.class, url, version);
301  }
302//
303//  protected List<PEDefinition> listChildren(boolean allFixed, StructureDefinition profileStructure, ElementDefinition definition, TypeRefComponent t, CanonicalType u) {
304//    // TODO Auto-generated method stub
305//    return null;
306//  }
307
308  protected List<PEDefinition> listChildren(boolean allFixed, PEDefinition parent, StructureDefinition profileStructure, ElementDefinition definition, String url, String... omitList) {
309    StructureDefinition profile = profileStructure;
310    List<ElementDefinition> list = pu.getChildList(profile, definition);
311    if (definition.getType().size() == 1 || (!definition.getPath().contains(".")) || list.isEmpty()) {
312      assert url == null || checkType(definition, url);
313      List<PEDefinition> res = new ArrayList<>();
314      if (list.size() == 0) {
315        profile = context.fetchResource(StructureDefinition.class, url);
316        list = pu.getChildList(profile, profile.getSnapshot().getElementFirstRep());
317      }
318      if (list.size() > 0) {
319        int i = 0;
320        while (i < list.size()) {
321          ElementDefinition defn = list.get(i);
322          if (!defn.getMax().equals("0") && (allFixed || include(defn))) {
323            if (passElementPropsCheck(defn) && !Utilities.existsInList(defn.getName(), omitList)) {
324              PEDefinitionElement pe = new PEDefinitionElement(this, profile, defn, parent.path());
325              pe.setRecursing(definition == defn || (profile.getDerivation() == TypeDerivationRule.SPECIALIZATION && profile.getType().equals("Extension")));
326              if (cu.isPrimitiveDatatype(definition.getTypeFirstRep().getWorkingCode()) && "value".equals(pe.name())) {
327                pe.setMustHaveValue(definition.getMustHaveValue());
328              }
329              pe.setInFixedValue(definition.hasFixed() || definition.hasPattern() || parent.isInFixedValue());
330              if (defn.hasSlicing()) {
331                if (defn.getSlicing().getRules() != SlicingRules.CLOSED) {
332                  res.add(pe);
333                }
334                i++;
335                while (i < list.size() && list.get(i).getPath().equals(defn.getPath())) {
336                  StructureDefinition ext = getExtensionDefinition(list.get(i));
337                  if (ext != null) {
338                    res.add(new PEDefinitionExtension(this, list.get(i).getSliceName(), profile, list.get(i), defn, ext, parent.path()));
339                  } else if (isTypeSlicing(defn)) {
340                    res.add(new PEDefinitionTypeSlice(this, list.get(i).getSliceName(), profile, list.get(i), defn, parent.path()));
341                  } else {
342                    res.add(new PEDefinitionSlice(this, list.get(i).getSliceName(), profile, list.get(i), defn, parent.path()));
343                  }
344                  i++;
345                }
346              } else {
347                res.add(pe);
348                i++;
349              }
350            } else {
351              i++;
352            } 
353          } else {
354            i++;
355          }
356        }
357      }
358      return res;
359    } else if (list.isEmpty()) {
360      throw new DefinitionException("not done yet!");
361    } else {
362      throw new DefinitionException("not done yet");
363    }
364  }
365
366  private boolean passElementPropsCheck(ElementDefinition bdefn) {
367    switch (elementProps) {
368    case EXTENSION:
369      return !Utilities.existsInList(bdefn.getBase().getPath(), "Element.id");
370    case NONE:
371      return !Utilities.existsInList(bdefn.getBase().getPath(), "Element.id", "Element.extension");
372    case EXTENSION_ID:
373    default:
374      return true;
375    }
376  }
377
378  private boolean isTypeSlicing(ElementDefinition defn) {
379    ElementDefinitionSlicingComponent sl = defn.getSlicing();
380    return sl.getRules() == SlicingRules.CLOSED && sl.getDiscriminator().size() == 1 &&
381        sl.getDiscriminatorFirstRep().getType() == DiscriminatorType.TYPE && "$this".equals(sl.getDiscriminatorFirstRep().getPath());
382  }
383
384  private boolean include(ElementDefinition defn) {
385    if (fixedPropsDefault) { 
386      return true;
387    } else { 
388      return !(defn.hasFixed() || defn.hasPattern());
389    }
390  }
391
392  protected List<PEDefinition> listSlices(StructureDefinition profileStructure, ElementDefinition definition, PEDefinition parent) {
393    List<ElementDefinition> list = pu.getSliceList(profileStructure, definition);
394    List<PEDefinition> res = new ArrayList<>();
395    for (ElementDefinition ed : list) {
396      if (profileStructure.getDerivation() == TypeDerivationRule.CONSTRAINT && profileStructure.getType().equals("Extension")) {
397        res.add(new PEDefinitionSubExtension(this, profileStructure, ed, parent.path()));
398      } else {
399        PEDefinitionElement pe = new PEDefinitionElement(this, profileStructure, ed, parent.path());
400        pe.setRecursing(definition == ed || (profileStructure.getDerivation() == TypeDerivationRule.SPECIALIZATION && profileStructure.getType().equals("Extension")));
401        res.add(pe);
402      }
403    }
404    return res;
405  }
406
407
408  private boolean checkType(ElementDefinition defn, String url) {
409    for (TypeRefComponent t : defn.getType()) {
410      if (("http://hl7.org/fhir/StructureDefinition/"+t.getWorkingCode()).equals(url)) {
411        return true;
412      }
413      for (CanonicalType u : t.getProfile()) {
414        if (url.equals(u.getValue())) {
415          return true;
416        }
417      }
418    }
419    return false;
420  }
421
422
423  private StructureDefinition getExtensionDefinition(ElementDefinition ed) {
424    if ("Extension".equals(ed.getTypeFirstRep().getWorkingCode()) && ed.getTypeFirstRep().getProfile().size() == 1) {
425      return context.fetchResource(StructureDefinition.class, ed.getTypeFirstRep().getProfile().get(0).asStringValue());
426    } else {
427      return null;
428    }
429  }
430
431
432  private ElementDefinition getByName(List<ElementDefinition> blist, String name) {
433    for (ElementDefinition ed : blist) {
434      if (name.equals(ed.getName())) {
435        return ed;
436      }
437    }
438    return null;
439  }
440
441
442  protected PEType makeType(TypeRefComponent t) {
443    if (t.hasProfile()) {
444      StructureDefinition sd = context.fetchResource(StructureDefinition.class, t.getProfile().get(0).getValue());
445      if (sd == null) {
446        return new PEType(tail(t.getProfile().get(0).getValue()), t.getWorkingCode(), t.getProfile().get(0).getValue());
447      } else {
448        return new PEType(sd.getName(), t.getWorkingCode(), t.getProfile().get(0).getValue());
449      }
450    } else {
451      return makeType(t.getWorkingCode());
452    } 
453  }
454
455  protected PEType makeType(TypeRefComponent t, CanonicalType u) {
456    StructureDefinition sd = context.fetchResource(StructureDefinition.class, u.getValue());
457    if (sd == null) {
458      return new PEType(tail(u.getValue()), t.getWorkingCode(), u.getValue());
459    } else {
460      return new PEType(sd.getName(), t.getWorkingCode(), u.getValue());
461    }
462  }
463
464
465  protected PEType makeType(String tn) {
466    return new PEType(tn, tn, "http://hl7.org/fhir/StructureDefinition/"+ tn);
467  }
468
469  private String tail(String value) {
470    return value.contains("/") ? value.substring(value.lastIndexOf("/")+1) : value;
471  }
472
473  protected List<ElementDefinition> getChildren(StructureDefinition profileStructure, ElementDefinition definition) {
474    return pu.getChildList(profileStructure, definition);
475  }
476
477  private PEInstance loadInstance(PEDefinition defn, Resource resource) {
478    return new PEInstance(this, defn, resource, resource, defn.name());
479  }
480
481  public IWorkerContext getContext() {
482    return context;
483  }
484
485  protected void populateByProfile(Base base, PEDefinition definition) {
486    for (PEDefinition pe : definition.children(true)) {
487      if (pe.fixedValue()) {
488        if (pe.definition().hasPattern()) {
489          base.setProperty(pe.schemaName(), pe.definition().getPattern());
490        } else { 
491          base.setProperty(pe.schemaName(), pe.definition().getFixed());
492        }
493      } else {
494          for (int i = 0; i < pe.min(); i++) {
495            Base b = null;
496            if (pe.schemaName().endsWith("[x]")) {
497              if (pe.types().size() == 1) {
498                b = base.addChild(pe.schemaName().replace("[x]", Utilities.capitalize(pe.types().get(0).getType())));
499              }
500            } else {
501              b = base.addChild(pe.schemaName());
502            }
503            if (b != null) {
504              populateByProfile(b, pe);
505            }
506        }
507      }
508    }
509  }
510
511  public String makeSliceExpression(StructureDefinition profile, ElementDefinitionSlicingComponent slicing, ElementDefinition definition) {
512    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" and ");
513    for (ElementDefinitionSlicingDiscriminatorComponent d : slicing.getDiscriminator()) {
514      switch (d.getType()) {
515      case EXISTS:
516        throw new DefinitionException("The discriminator type 'exists' is not supported by the PEBuilder");
517      case PATTERN:
518        throw new DefinitionException("The discriminator type 'pattern' is not supported by the PEBuilder");
519      case POSITION:
520        throw new DefinitionException("The discriminator type 'position' is not supported by the PEBuilder");
521      case PROFILE:
522        throw new DefinitionException("The discriminator type 'profile' is not supported by the PEBuilder");
523      case TYPE:
524        throw new DefinitionException("The discriminator type 'type' is not supported by the PEBuilder");
525      case VALUE:
526        String path = d.getPath();
527        if (path.contains(".")) {
528          throw new DefinitionException("The discriminator path '"+path+"' is not supported by the PEBuilder");          
529        }
530        ElementDefinition ed = getChildElement(profile, definition, path);
531        if (ed == null) {
532          throw new DefinitionException("The discriminator path '"+path+"' could not be resolved by the PEBuilder");          
533        }
534        if (!ed.hasFixed()) {
535          throw new DefinitionException("The discriminator path '"+path+"' has no fixed value - this is not supported by the PEBuilder");          
536        }
537        if (!ed.getFixed().isPrimitive()) {
538          throw new DefinitionException("The discriminator path '"+path+"' has a fixed value that is not a primitive ("+ed.getFixed().fhirType()+") - this is not supported by the PEBuilder");          
539        }
540        b.append(path+" = '"+ed.getFixed().primitiveValue()+"'");
541        break;
542      case NULL:
543        throw new DefinitionException("The discriminator type 'null' is not supported by the PEBuilder");
544      default:
545        throw new DefinitionException("The discriminator type '??' is not supported by the PEBuilder"); 
546      }
547    }
548    return b.toString();
549  }
550
551  private ElementDefinition getChildElement(StructureDefinition profile, ElementDefinition definition, String path) {
552    List<ElementDefinition> elements = pu.getChildList(profile, definition);
553    if (elements.size() == 0) {
554      profile = definition.getTypeFirstRep().hasProfile() ? context.fetchResource(StructureDefinition.class, definition.getTypeFirstRep().getProfile().get(0).asStringValue()) :
555        context.fetchTypeDefinition(definition.getTypeFirstRep().getWorkingCode());
556      elements = pu.getChildList(profile, profile.getSnapshot().getElementFirstRep());
557    }
558    return getByName(elements, path);
559  }
560
561  public List<Base> exec(Resource resource, Base data, String fhirpath) {
562    return fpe.evaluate(this, resource, resource, data, fhirpath);
563  }
564
565  public boolean isResource(String name) {
566    return cu.isResource(name);
567  }
568}