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