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 seamless 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                  pe.setSlicer(true);
334                }
335                i++;
336                while (i < list.size() && list.get(i).getPath().equals(defn.getPath())) {
337                  StructureDefinition ext = getExtensionDefinition(list.get(i));
338                  if (ext != null) {
339                    res.add(new PEDefinitionExtension(this, list.get(i).getSliceName(), profile, list.get(i), defn, ext, parent.path()));
340                  } else if (isTypeSlicing(defn)) {
341                    res.add(new PEDefinitionTypeSlice(this, list.get(i).getSliceName(), profile, list.get(i), defn, parent.path()));
342                  } else {
343                    res.add(new PEDefinitionSlice(this, list.get(i).getSliceName(), profile, list.get(i), defn, parent.path()));
344                  }
345                  i++;
346                }
347              } else {
348                res.add(pe);
349                i++;
350              }
351            } else {
352              i++;
353            } 
354          } else {
355            i++;
356          }
357        }
358      }
359      return res;
360    } else if (list.isEmpty()) {
361      throw new DefinitionException("not done yet!");
362    } else {
363      throw new DefinitionException("not done yet");
364    }
365  }
366
367  private boolean passElementPropsCheck(ElementDefinition bdefn) {
368    switch (elementProps) {
369    case EXTENSION:
370      return !Utilities.existsInList(bdefn.getBase().getPath(), "Element.id");
371    case NONE:
372      return !Utilities.existsInList(bdefn.getBase().getPath(), "Element.id", "Element.extension");
373    case EXTENSION_ID:
374    default:
375      return true;
376    }
377  }
378
379  private boolean isTypeSlicing(ElementDefinition defn) {
380    ElementDefinitionSlicingComponent sl = defn.getSlicing();
381    return sl.getRules() == SlicingRules.CLOSED && sl.getDiscriminator().size() == 1 &&
382        sl.getDiscriminatorFirstRep().getType() == DiscriminatorType.TYPE && "$this".equals(sl.getDiscriminatorFirstRep().getPath());
383  }
384
385  private boolean include(ElementDefinition defn) {
386    if (fixedPropsDefault) { 
387      return true;
388    } else { 
389      return !(defn.hasFixed() || defn.hasPattern());
390    }
391  }
392
393  protected List<PEDefinition> listSlices(StructureDefinition profileStructure, ElementDefinition definition, PEDefinition parent) {
394    List<ElementDefinition> list = pu.getSliceList(profileStructure, definition);
395    List<PEDefinition> res = new ArrayList<>();
396    for (ElementDefinition ed : list) {
397      if (profileStructure.getDerivation() == TypeDerivationRule.CONSTRAINT && profileStructure.getType().equals("Extension")) {
398        res.add(new PEDefinitionSubExtension(this, profileStructure, ed, parent.path()));
399      } else {
400        PEDefinitionElement pe = new PEDefinitionElement(this, profileStructure, ed, parent.path());
401        pe.setRecursing(definition == ed || (profileStructure.getDerivation() == TypeDerivationRule.SPECIALIZATION && profileStructure.getType().equals("Extension")));
402        res.add(pe);
403      }
404    }
405    return res;
406  }
407
408
409  private boolean checkType(ElementDefinition defn, String url) {
410    for (TypeRefComponent t : defn.getType()) {
411      if (("http://hl7.org/fhir/StructureDefinition/"+t.getWorkingCode()).equals(url)) {
412        return true;
413      }
414      for (CanonicalType u : t.getProfile()) {
415        if (url.equals(u.getValue())) {
416          return true;
417        }
418      }
419    }
420    return false;
421  }
422
423
424  private StructureDefinition getExtensionDefinition(ElementDefinition ed) {
425    if ("Extension".equals(ed.getTypeFirstRep().getWorkingCode()) && ed.getTypeFirstRep().getProfile().size() == 1) {
426      return context.fetchResource(StructureDefinition.class, ed.getTypeFirstRep().getProfile().get(0).asStringValue());
427    } else {
428      return null;
429    }
430  }
431
432
433  private ElementDefinition getByName(List<ElementDefinition> blist, String name) {
434    for (ElementDefinition ed : blist) {
435      if (name.equals(ed.getName())) {
436        return ed;
437      }
438    }
439    return null;
440  }
441
442
443  protected PEType makeType(TypeRefComponent t) {
444    if (t.hasProfile()) {
445      StructureDefinition sd = context.fetchResource(StructureDefinition.class, t.getProfile().get(0).getValue());
446      if (sd == null) {
447        return new PEType(tail(t.getProfile().get(0).getValue()), t.getWorkingCode(), t.getProfile().get(0).getValue());
448      } else {
449        return new PEType(sd.getName(), t.getWorkingCode(), t.getProfile().get(0).getValue());
450      }
451    } else {
452      return makeType(t.getWorkingCode());
453    } 
454  }
455
456  protected PEType makeType(TypeRefComponent t, CanonicalType u) {
457    StructureDefinition sd = context.fetchResource(StructureDefinition.class, u.getValue());
458    if (sd == null) {
459      return new PEType(tail(u.getValue()), t.getWorkingCode(), u.getValue());
460    } else {
461      return new PEType(sd.getName(), t.getWorkingCode(), u.getValue());
462    }
463  }
464
465
466  protected PEType makeType(String tn) {
467    return new PEType(tn, tn, "http://hl7.org/fhir/StructureDefinition/"+ tn);
468  }
469
470  private String tail(String value) {
471    return value.contains("/") ? value.substring(value.lastIndexOf("/")+1) : value;
472  }
473
474  protected List<ElementDefinition> getChildren(StructureDefinition profileStructure, ElementDefinition definition) {
475    return pu.getChildList(profileStructure, definition);
476  }
477
478  private PEInstance loadInstance(PEDefinition defn, Resource resource) {
479    return new PEInstance(this, defn, resource, resource, defn.name());
480  }
481
482  public IWorkerContext getContext() {
483    return context;
484  }
485
486  protected void populateByProfile(Base base, PEDefinition definition) {
487    for (PEDefinition pe : definition.children(true)) {
488      System.out.println("PopulateByProfile for "+pe.path);
489      if (pe.fixedValue()) {
490        if (pe.definition().hasPattern()) {
491          base.setProperty(pe.schemaName(), pe.definition().getPattern());
492        } else { 
493          base.setProperty(pe.schemaName(), pe.definition().getFixed());
494        }
495      } else if (!pe.isSlicer()) {
496          for (int i = 0; i < pe.min(); i++) {
497            Base b = null;
498            if (pe.schemaName().endsWith("[x]")) {
499              if (pe.types().size() == 1) {
500                b = base.addChild(pe.schemaName().replace("[x]", Utilities.capitalize(pe.types().get(0).getType())));
501              }
502            } else if (!pe.isBaseList()) {
503              b = base.makeProperty(pe.schemaName().hashCode(), pe.schemaName());
504            } else {
505              b = base.addChild(pe.schemaName());
506            }
507            if (b != null) {
508              populateByProfile(b, pe);
509            }
510        }
511      }
512    }
513  }
514
515  public String makeSliceExpression(StructureDefinition profile, ElementDefinitionSlicingComponent slicing, ElementDefinition definition) {
516    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" and ");
517    for (ElementDefinitionSlicingDiscriminatorComponent d : slicing.getDiscriminator()) {
518      switch (d.getType()) {
519      case EXISTS:
520        throw new DefinitionException("The discriminator type 'exists' is not supported by the PEBuilder");
521      case PATTERN:
522        throw new DefinitionException("The discriminator type 'pattern' is not supported by the PEBuilder");
523      case POSITION:
524        throw new DefinitionException("The discriminator type 'position' is not supported by the PEBuilder");
525      case PROFILE:
526        throw new DefinitionException("The discriminator type 'profile' is not supported by the PEBuilder");
527      case TYPE:
528        throw new DefinitionException("The discriminator type 'type' is not supported by the PEBuilder");
529      case VALUE:
530        String path = d.getPath();
531        if (path.contains(".")) {
532          throw new DefinitionException("The discriminator path '"+path+"' is not supported by the PEBuilder");          
533        }
534        ElementDefinition ed = getChildElement(profile, definition, path);
535        if (ed == null) {
536          throw new DefinitionException("The discriminator path '"+path+"' could not be resolved by the PEBuilder");          
537        }
538        if (!ed.hasFixed()) {
539          throw new DefinitionException("The discriminator path '"+path+"' has no fixed value - this is not supported by the PEBuilder");          
540        }
541        if (!ed.getFixed().isPrimitive()) {
542          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");          
543        }
544        b.append(path+" = '"+ed.getFixed().primitiveValue()+"'");
545        break;
546      case NULL:
547        throw new DefinitionException("The discriminator type 'null' is not supported by the PEBuilder");
548      default:
549        throw new DefinitionException("The discriminator type '??' is not supported by the PEBuilder"); 
550      }
551    }
552    return b.toString();
553  }
554
555  private ElementDefinition getChildElement(StructureDefinition profile, ElementDefinition definition, String path) {
556    List<ElementDefinition> elements = pu.getChildList(profile, definition);
557    if (elements.size() == 0) {
558      profile = definition.getTypeFirstRep().hasProfile() ? context.fetchResource(StructureDefinition.class, definition.getTypeFirstRep().getProfile().get(0).asStringValue()) :
559        context.fetchTypeDefinition(definition.getTypeFirstRep().getWorkingCode());
560      elements = pu.getChildList(profile, profile.getSnapshot().getElementFirstRep());
561    }
562    return getByName(elements, path);
563  }
564
565  public List<Base> exec(Resource resource, Base data, String fhirpath) {
566    return fpe.evaluate(this, resource, resource, data, fhirpath);
567  }
568
569  public boolean isResource(String name) {
570    return cu.isResource(name);
571  }
572}