001package org.hl7.fhir.r4.profilemodel;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, \
008  are permitted provided that the following conditions are met:
009  
010   * Redistributions of source code must retain the above copyright notice, this \
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, \
013     this list of conditions and the following disclaimer in the documentation \
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND \
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED \
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. \
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, \
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT \
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR \
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) \
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE \
028  POSSIBILITY OF SUCH DAMAGE.
029  */
030
031import java.util.ArrayList;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035
036import org.apache.xmlbeans.impl.xb.xsdschema.All;
037import org.hl7.fhir.exceptions.DefinitionException;
038import org.hl7.fhir.r4.model.Base;
039import org.hl7.fhir.r4.model.ElementDefinition;
040import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
041import org.hl7.fhir.r4.model.StructureDefinition;
042import org.hl7.fhir.r4.model.Type;
043import org.hl7.fhir.r4.model.ValueSet;
044import org.hl7.fhir.r4.profilemodel.PEDefinition.PEDefinitionElementMode;
045import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
046import org.hl7.fhir.utilities.Utilities;
047
048public abstract class PEDefinition {
049
050  public enum PEDefinitionElementMode {
051    Resource, Element, DataType, Extension
052  }
053
054  protected PEBuilder builder;
055  protected String name;
056  protected String path;
057  protected StructureDefinition profile;
058  protected ElementDefinition definition;
059  protected List<PEType> types;
060  protected Map<String, List<PEDefinition>> children = new HashMap<>();
061  private boolean recursing;
062  private boolean mustHaveValue;
063  private boolean inFixedValue;
064  private boolean isSlicer;
065  private List<PEDefinition> slices; // if there are some...
066  
067//  /**
068//   * Don't create one of these directly - always use the public methods on ProfiledElementBuilder
069//   *  
070//   * @param builder
071//   * @param baseElement
072//   * @param profiledElement
073//   * @param data
074//   */
075//  protected PEDefinition(PEBuilder builder, String name, 
076//      ElementDefinition definition, Base data) {
077//    super();
078//    this.builder = builder;
079//    this.name = name;
080//    this.definition = definition;
081////    this.data = data;
082//  }
083
084  protected PEDefinition(PEBuilder builder, String name, StructureDefinition profile, ElementDefinition definition, String ppath) {
085    this.builder = builder;
086    this.name = name;
087    this.profile = profile;
088    this.definition = definition;
089    this.path = ppath == null ? name : ppath+"."+name;
090  }
091
092
093  /** 
094   * @return The name of the element or slice in the profile (always unique amongst children)
095   */
096  public String name() {
097    return name;
098  }
099
100  /** 
101   * @return The path of the element or slice in the profile (name.name.name...)
102   */
103  public String path() {
104    return path;
105  }
106
107  /**
108   * @return The name of the element in the resource (may be different to the slice name)
109   */
110  public String schemaName() {
111    String n = definition.getName();
112    return n;
113  }
114  
115  /**
116   * @return The name of the element in the resource (may be different to the slice name)
117   */
118  public String schemaNameWithType() {
119    String n = definition.getName();
120    if (n.endsWith("[x]") && types().size() == 1) {
121      n = n.replace("[x]", Utilities.capitalize(types.get(0).getType()));
122    }
123    return n;
124  }
125  
126  /**
127   * @return a list of types. There is usually at least one type; it might be Element, Type, BackboneElement or BackboneType
128   * 
129   * The following elements don't have types (true primitives): Element.id. Extension.url, PrimitiveType.value
130   */
131  public List<PEType> types() {
132    if (types == null) {
133      List<PEType> ltypes = new ArrayList<>();
134      listTypes(ltypes);
135      types = ltypes;
136    }
137    return types;
138  }
139  
140  protected abstract void listTypes(List<PEType> types);
141  
142  /**
143   * @return The minimum number of repeats allowed
144   */
145  public int min() {
146    return mustHaveValue ? 1 : definition.getMin();
147  }
148  
149  /**
150   * @return the maximum number of repeats allowed
151   */
152  public int max() {
153    return definition.getMax() == null || "*".equals(definition.getMax()) ? Integer.MAX_VALUE : Integer.parseInt(definition.getMax());
154  }
155  
156  /**
157   * @return the definition of the element in the profile (fully populated)
158   * 
159   * Note that the profile definition might be the same as a base definition, when the tree runs off the end of what's profiled
160   */
161  public ElementDefinition definition() {
162    return definition;
163  }
164  
165  /**
166   * @return the definition of the element in the base specification
167   * 
168   * Note that the profile definition might be the same as a base definition, when the tree runs off the end of what's profiled
169   */
170  public ElementDefinition baseDefinition() {
171    String type = definition.getBase().getPath();
172    if (type.contains(".")) {
173      type= type.substring(0, type.indexOf("."));
174    }
175    StructureDefinition sd = builder.getContext().fetchTypeDefinition(type);
176    return sd.getSnapshot().getElementByPath(definition.getBase().getPath());
177  }
178  
179  /**
180   * @return the short documentation of the definition (shown in the profile table view)
181   */
182  public String shortDocumentation() {
183    return definition.getShort();
184  }
185  
186  /**
187   * @return the full definition of the element (markdown syntax)
188   */
189  public String documentation() {
190    return definition.getDefinition();
191  }
192  
193//  /**
194//   * @return if the profiled definition has a value set, get the expansion 
195//   */
196//  public ValueSet expansion() {
197//    throw new NotImplementedException("Not done yet");
198//  }
199//  
200  /**
201   * @param typeUrl - the url of one of the types listed in types()
202   * @return - the list of children for the nominated type
203   * 
204   * Warning: profiles and resources can be recursive; you can't iterate this tree until you get 
205   * to the leaves because you will never get to a child that doesn't have children (extensions have extensions etc)
206   * 
207   */
208  public List<PEDefinition> children(String typeUrl) {
209    return children(typeUrl, false);
210  }
211  
212  public List<PEDefinition> children(String typeUrl, boolean allFixed) {
213    if (children.containsKey(typeUrl+"$"+allFixed)) {
214      return children.get(typeUrl+"$"+allFixed);      
215    } 
216    List<PEDefinition> res = new ArrayList<>();
217    makeChildren(typeUrl, res, allFixed);
218    children.put(typeUrl+"$"+allFixed, res);
219    return res;    
220  }
221  
222  public List<PEDefinition> children() {
223    if (types().size() == 1) {
224      return children(types.get(0).getUrl(), false);
225    } else {
226      throw new DefinitionException("Attempt to get children for an element that doesn't have a single type (element = "+path+", types = "+types()+")");
227    }
228  }
229  
230  public List<PEDefinition> children(boolean allFixed) {
231    if (types().size() == 1) {
232      return children(types.get(0).getUrl(), allFixed);
233    } else {
234      throw new DefinitionException("Attempt to get children for an element that doesn't have a single type (element = "+path+", types = "+types()+")");
235    }
236  }
237  
238  /**
239   * @return True if the element has a fixed value. This will always be false if fixedProps = false when the builder is created
240   */
241  public boolean hasFixedValue() {
242    return definition.hasFixed() || definition.hasPattern();
243  }
244
245  public Type getFixedValue() {
246    return definition.hasFixed() ? definition.getFixed() : definition.getPattern();
247  }
248  
249
250  protected abstract void makeChildren(String typeUrl, List<PEDefinition> children, boolean allFixed);
251
252  @Override
253  public String toString() {
254    return name+"("+schemaName()+"):"+types().toString()+" ["+min()+":"+(max() == Integer.MAX_VALUE ? "*" : max() )+"] \""+shortDocumentation()+"\"";
255  }
256
257  /**
258   * @return true if the builder observes that this element is recursing (extensions have extensions)
259   * 
260   * Note that this is unreliable and may be withdrawn if it can't be fixed
261   */
262  public boolean isRecursing() {
263    return recursing;
264  }
265
266  protected void setRecursing(boolean recursing) {
267    this.recursing = recursing;
268  }
269  
270  protected boolean isMustHaveValue() {
271    return mustHaveValue;
272  }
273
274  protected void setMustHaveValue(boolean mustHaveValue) {
275    this.mustHaveValue = mustHaveValue;
276  }
277
278  /**
279   * @return true if this property is inside an element that has an assigned fixed value
280   */
281  public boolean isInFixedValue() {
282    return inFixedValue;
283  }
284
285
286  protected void setInFixedValue(boolean inFixedValue) {
287    this.inFixedValue = inFixedValue;
288  }
289
290
291  /** 
292   * This is public to support unit testing - there's no reason to use it otherwise
293   * 
294   * @return used in the instance processor to differentiate slices
295   */
296  public abstract String fhirpath();
297
298
299  public boolean isList() {
300    return "*".equals(definition.getMax()) || (Utilities.parseInt(definition.getMax(), 2) > 1);
301  }
302
303
304  public boolean repeats() {
305    return max() > 1;
306  }
307  
308  public PEDefinitionElementMode mode() {
309    if (builder.isResource(definition.getBase().getPath())) {
310      return PEDefinitionElementMode.Resource;
311    }
312    for (TypeRefComponent tr : definition.getType()) {
313      if ("Extension".equals(tr.getWorkingCode())) {
314        return PEDefinitionElementMode.Extension;
315      }
316      if (!Utilities.existsInList(tr.getWorkingCode(), "Element", "BackboneElement")) {
317        return PEDefinitionElementMode.DataType;
318      }
319    }
320    return PEDefinitionElementMode.Element;
321  }
322
323  /**
324   * @return true if this element is profiled one way or another
325   */
326  public boolean isProfiled() {
327    return !profile.getUrl().startsWith("http://hl7.org/fhir/StructureDefinition");
328  }
329
330
331  public boolean isSlicer() {
332    return isSlicer;
333  }
334
335
336  public void setSlicer(boolean isSlicer) {
337    this.isSlicer = isSlicer;
338  }
339
340
341  public boolean isBaseList() {
342    return !"1".equals(definition.getBase().getMax());
343  }
344
345
346  public StructureDefinition getProfile() {
347    return profile;
348  }
349
350
351  public boolean isKeyElement() {
352    boolean selfKey = definition.getMustSupport() || definition.getMustHaveValue() || min() > 0 || definition.hasCondition();
353    if (isProfiled() && !selfKey) {
354      if (types() != null && types().size() > 0) {
355        for (PEDefinition child : children()) {
356          if (child.isKeyElement()) {
357            return true;
358          }
359        }
360      }
361    }
362    return selfKey;
363  }
364
365
366  public boolean isPrimitive() {
367    return types().size() == 1 && builder.getContext().isPrimitiveType(types.get(0).getName());
368  }
369
370
371  public boolean isBasePrimitive() {
372    ElementDefinition ed = baseDefinition();
373    return ed != null && ed.getType().size() == 1 && builder.getContext().isPrimitiveType(ed.getType().get(0).getWorkingCode());
374  }
375
376
377  // extensions do something different here 
378  public List<PEDefinition> directChildren(boolean allFixed) {
379    return children(allFixed);
380  }
381
382
383  public List<PEDefinition> getSlices() {
384    return slices;
385  }
386
387
388  public void setSlices(List<PEDefinition> slices) {
389    this.slices = slices;
390  }
391
392
393  public boolean isExtension() {
394    return false;
395  }
396
397  public String getExtensionUrl() {
398    return null;
399  }
400
401  public ValueSet valueSet() {
402    if (definition.getBinding().hasValueSet()) {
403      return builder.getContext().fetchResource(ValueSet.class, definition.getBinding().getValueSet());
404    }
405    return null;
406  }
407
408
409  public PEBuilder getBuilder() {
410    return builder;
411  }
412
413  public String typeSummary() {
414    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
415    for (PEType t : types()) {
416      b.append(t.getName());
417    }       
418    return b.toString();
419  }
420
421
422  public boolean isSlice() {
423    return definition.hasSliceName();
424  }
425}
426
427