001package org.hl7.fhir.r5.elementmodel;
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 */
031
032
033
034import java.util.ArrayList;
035import java.util.List;
036
037import org.hl7.fhir.exceptions.DefinitionException;
038import org.hl7.fhir.exceptions.FHIRException;
039import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
040import org.hl7.fhir.r5.conformance.profile.ProfileUtilities.SourcedChildDefinitions;
041import org.hl7.fhir.r5.context.IWorkerContext;
042import org.hl7.fhir.r5.formats.FormatUtilities;
043import org.hl7.fhir.r5.model.ElementDefinition;
044import org.hl7.fhir.r5.model.ElementDefinition.PropertyRepresentation;
045import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
046import org.hl7.fhir.r5.model.Extension;
047import org.hl7.fhir.r5.model.StructureDefinition;
048import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
049import org.hl7.fhir.r5.model.TypeDetails;
050import org.hl7.fhir.r5.utils.ToolingExtensions;
051import org.hl7.fhir.r5.utils.TypesUtilities;
052import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
053import org.hl7.fhir.utilities.StringPair;
054import org.hl7.fhir.utilities.Utilities;
055
056public class Property {
057
058        private IWorkerContext context;
059        private ElementDefinition definition;
060        private StructureDefinition structure;
061  private ProfileUtilities profileUtilities;
062  private TypeRefComponent type;
063
064  public Property(IWorkerContext context, ElementDefinition definition, StructureDefinition structure, ProfileUtilities profileUtilities) {
065                this.context = context;
066                this.definition = definition;
067                this.structure = structure;
068    this.profileUtilities = profileUtilities;
069        }
070
071
072  public Property(IWorkerContext context, ElementDefinition definition, StructureDefinition structure, ProfileUtilities profileUtilities, String type) {
073    this.context = context;
074    this.definition = definition;
075    this.structure = structure;
076    this.profileUtilities = profileUtilities;
077    for (TypeRefComponent tr : definition.getType()) {
078      if (tr.getWorkingCode().equals(type)) {
079        this.type = tr;
080      }
081    }
082  }
083  
084        public Property(IWorkerContext context, ElementDefinition definition, StructureDefinition structure) {
085    this(context, definition, structure, new ProfileUtilities(context, null, null));
086        }
087
088        public String getName() {
089                return definition.getPath().substring(definition.getPath().lastIndexOf(".")+1);
090        }
091
092  public String getJsonName() {
093    if (definition.hasExtension(ToolingExtensions.EXT_JSON_NAME)) {
094      return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_NAME);
095    } else {
096      return getName();
097    }
098  }
099
100  public String getXmlName() {
101    if (definition.hasExtension(ToolingExtensions.EXT_XML_NAME)) {
102      return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_XML_NAME);
103    } else {
104      return getName();
105    }
106  }
107
108  public String getXmlNamespace() {
109    if (ToolingExtensions.hasExtension(definition, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace")) {
110      return ToolingExtensions.readStringExtension(definition, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace");
111    } else if (ToolingExtensions.hasExtension(structure, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace")) {
112      return ToolingExtensions.readStringExtension(structure, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace");
113    } else {
114      return FormatUtilities.FHIR_NS;
115    }
116  }
117        
118        public ElementDefinition getDefinition() {
119                return definition;
120        }
121
122        public String getType() {
123          if (type != null) {
124            return type.getWorkingCode();
125          } else  if (definition.getType().size() == 0)
126                        return null;
127                else if (definition.getType().size() > 1) {
128                        String tn = definition.getType().get(0).getWorkingCode();
129                        for (int i = 1; i < definition.getType().size(); i++) {
130                                if (!tn.equals(definition.getType().get(i).getWorkingCode()))
131                                        return null; // though really, we shouldn't get here - type != null when definition.getType.size() > 1, or it should be
132                        }
133                        return tn;
134                } else
135                        return definition.getType().get(0).getWorkingCode();
136        }
137
138        public String getType(String elementName) {
139          if (type != null) {
140      return type.getWorkingCode();
141    } 
142          if (!definition.getPath().contains("."))
143      return definition.getPath();
144    ElementDefinition ed = definition;
145    if (definition.hasContentReference()) {
146      String url = null;
147      String path = definition.getContentReference();
148      if (!path.startsWith("#")) {
149        if (path.contains("#")) {
150          url = path.substring(0, path.indexOf("#"));
151          path = path.substring(path.indexOf("#")+1);
152        } else {
153          throw new Error("Illegal content reference '"+path+"'");
154        }
155      } else {
156        path = path.substring(1);
157      }
158      StructureDefinition sd = (url == null || url.equals(structure.getUrl())) ? structure : context.fetchResource(StructureDefinition.class, url, structure);
159      if (sd == null) {
160        throw new Error("Unknown Type in content reference '"+path+"'");        
161      }
162      boolean found = false;
163      for (ElementDefinition d : sd.getSnapshot().getElement()) {
164        if (d.hasId() && d.getId().equals(path)) {
165          found = true;
166          ed = d;
167        }
168      }
169      if (!found)
170        throw new Error("Unable to resolve "+definition.getContentReference()+" at "+definition.getPath()+" on "+sd.getUrl());
171    }
172    if (ed.getType().size() == 0)
173                        return null;
174    else if (ed.getType().size() > 1) {
175      String t = ed.getType().get(0).getCode();
176                        boolean all = true;
177      for (TypeRefComponent tr : ed.getType()) {
178                                if (!t.equals(tr.getCode()))
179                                        all = false;
180                        }
181                        if (all)
182                                return t;
183      String tail = ed.getPath().substring(ed.getPath().lastIndexOf(".")+1);
184      if (tail.endsWith("[x]") && elementName != null && elementName.startsWith(tail.substring(0, tail.length()-3))) {
185                                String name = elementName.substring(tail.length()-3);
186        return isPrimitive(lowFirst(name)) ? lowFirst(name) : name;        
187                        } else {
188              if (ToolingExtensions.hasExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype"))
189                return ToolingExtensions.readStringExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype");
190        throw new Error("logic error, gettype when types > 1, name mismatch for "+elementName+" on at "+ed.getPath());
191                        }
192    } else if (ed.getType().get(0).getCode() == null) {
193      if (Utilities.existsInList(ed.getId(), "Element.id", "Extension.url"))
194        return "string";
195      else
196        return structure.getId();
197                } else
198      return ed.getType().get(0).getWorkingCode();
199        }
200
201  public boolean hasType(String elementName) {
202    if (type != null) {
203      return false; // ?
204    } else if (definition.getType().size() == 0) {
205      return false;
206    } else if (isJsonPrimitiveChoice()) { 
207      for (TypeRefComponent tr : definition.getType()) {
208        if (elementName.equals(tr.getWorkingCode())) {
209          return true;
210        }
211      }
212      return false;
213    } else if (definition.getType().size() > 1) {
214      String t = definition.getType().get(0).getCode();
215      boolean all = true;
216      for (TypeRefComponent tr : definition.getType()) {
217        if (!t.equals(tr.getCode()))
218          all = false;
219      }
220      if (all)
221        return true;
222      String tail = definition.getPath().substring(definition.getPath().lastIndexOf(".")+1);
223      if (tail.endsWith("[x]") && elementName.startsWith(tail.substring(0, tail.length()-3))) {
224//        String name = elementName.substring(tail.length()-3);
225        return true;        
226      } else
227        return false;
228    } else
229      return true;
230  }
231
232        public StructureDefinition getStructure() {
233                return structure;
234        }
235
236        /**
237         * Is the given name a primitive
238         * 
239         * @param E.g. "Observation.status"
240         */
241        public boolean isPrimitiveName(String name) {
242          String code = getType(name);
243      return isPrimitive(code);
244        }
245
246        /**
247         * Is the given type a primitive
248         * 
249         * @param E.g. "integer"
250         */
251        public boolean isPrimitive(String code) {
252          if (Utilities.isAbsoluteUrl(code)) {
253            StructureDefinition sd = context.fetchTypeDefinition(code);
254            return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
255          } else {
256            return TypesUtilities.isPrimitive(code);
257          }
258        }
259
260        public boolean isPrimitive() {
261          return isPrimitive(getType());
262        }
263        private String lowFirst(String t) {
264                return t.substring(0, 1).toLowerCase()+t.substring(1);
265        }
266
267        public boolean isResource() {
268          if (type != null) {
269            String tc = type.getCode();
270      return (("Resource".equals(tc) || "DomainResource".equals(tc)) ||  Utilities.existsInList(tc, context.getResourceNames()));
271          } else if (definition.getType().size() > 0) {
272      String tc = definition.getType().get(0).getCode();
273      return definition.getType().size() == 1 && (("Resource".equals(tc) || "DomainResource".equals(tc)) ||  Utilities.existsInList(tc, context.getResourceNames()));
274    }
275          else {
276            return !definition.getPath().contains(".") && (structure.getKind() == StructureDefinitionKind.RESOURCE);
277          }
278        }
279
280  public boolean isList() {
281    return !"1".equals(definition.getMax());
282  }
283
284  public boolean isBaseList() {
285    return !"1".equals(definition.getBase().getMax());
286  }
287
288  public String getScopedPropertyName() {
289    return definition.getBase().getPath();
290  }
291
292  private boolean isElementWithOnlyExtension(final ElementDefinition ed, final List<ElementDefinition> children) {
293    boolean result = false;
294    if (!ed.getType().isEmpty()) {
295      result = true;
296      for (final ElementDefinition ele : children) {
297        if (!ele.getPath().contains("extension")) {
298          result = false;
299          break;
300        }
301      }
302    }
303    return result;
304  }
305  
306        public boolean IsLogicalAndHasPrimitiveValue(String name) {
307//              if (canBePrimitive!= null)
308//                      return canBePrimitive;
309                
310        if (structure.getKind() != StructureDefinitionKind.LOGICAL)
311                return false;
312        if (!hasType(name))
313                return false;
314        StructureDefinition sd = context.fetchResource(StructureDefinition.class, structure.getUrl().substring(0, structure.getUrl().lastIndexOf("/")+1)+getType(name));
315        if (sd == null)
316          sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(getType(name), null));
317    if (sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE)
318      return true;
319        if (sd == null || sd.getKind() != StructureDefinitionKind.LOGICAL)
320                return false;
321        for (ElementDefinition ed : sd.getSnapshot().getElement()) {
322                if (ed.getPath().equals(sd.getId()+".value") && ed.getType().size() == 1 && isPrimitive(ed.getType().get(0).getCode())) {
323                        return true;
324                }
325        }
326        return false;
327        }
328
329  public boolean isChoice() {
330    if (type != null) {
331      return true;
332    }
333    if (definition.getType().size() <= 1)
334      return false;
335    String tn = definition.getType().get(0).getCode();
336    for (int i = 1; i < definition.getType().size(); i++) 
337      if (!definition.getType().get(i).getCode().equals(tn))
338        return true;
339    return false;
340  }
341
342
343  public List<Property> getChildProperties(String elementName, String statedType) throws FHIRException {
344    ElementDefinition ed = definition;
345    StructureDefinition sd = structure;
346    SourcedChildDefinitions children = profileUtilities.getChildMap(sd, ed);
347    String url = null;
348    if (children.getList().isEmpty() || isElementWithOnlyExtension(ed, children.getList())) {
349      // ok, find the right definitions
350      String t = null;
351      if (ed.getType().size() == 1)
352        t = ed.getType().get(0).getWorkingCode();
353      else if (ed.getType().size() == 0)
354        throw new Error("types == 0, and no children found on "+getDefinition().getPath());
355      else {
356        t = ed.getType().get(0).getWorkingCode();
357        boolean all = true;
358        for (TypeRefComponent tr : ed.getType()) {
359          if (!tr.getWorkingCode().equals(t)) {
360            all = false;
361            break;
362          }
363        }
364        if (!all) {
365          // ok, it's polymorphic
366          if (ed.hasRepresentation(PropertyRepresentation.TYPEATTR)) {
367            t = statedType;
368            if (t == null && ToolingExtensions.hasExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype"))
369              t = ToolingExtensions.readStringExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype");
370            boolean ok = false;
371            for (TypeRefComponent tr : ed.getType()) { 
372              if (tr.getWorkingCode().equals(t)) 
373                ok = true;
374              if (Utilities.isAbsoluteUrl(tr.getWorkingCode())) {
375                StructureDefinition sdt = context.fetchResource(StructureDefinition.class, tr.getWorkingCode());
376                if (sdt != null && sdt.getTypeTail().equals(t)) {
377                  url = tr.getWorkingCode();
378                  ok = true;
379                }
380              }
381              if (ok)
382                break;
383            }
384             if (!ok)
385               throw new DefinitionException("Type '"+t+"' is not an acceptable type for '"+elementName+"' on property "+definition.getPath());
386            
387          } else {
388            t = elementName.substring(tail(ed.getPath()).length() - 3);
389            if (isPrimitive(lowFirst(t)))
390              t = lowFirst(t);
391          }
392        }
393      }
394      if (!"xhtml".equals(t)) {
395        for (TypeRefComponent aType: ed.getType()) {
396          if (aType.getWorkingCode().equals(t)) {
397            if (aType.hasProfile()) {
398              assert aType.getProfile().size() == 1; 
399              url = aType.getProfile().get(0).getValue();
400            } else {
401              url = ProfileUtilities.sdNs(t, null);
402            }
403            break;
404          }
405        }
406        if (url==null)
407          throw new FHIRException("Unable to find type " + t + " for element " + elementName + " with path " + ed.getPath());
408        sd = context.fetchResource(StructureDefinition.class, url);        
409        if (sd == null)
410          throw new DefinitionException("Unable to find type '"+t+"' for name '"+elementName+"' on property "+definition.getPath());
411        children = profileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0));
412      }
413    }
414    List<Property> properties = new ArrayList<Property>();
415    for (ElementDefinition child : children.getList()) {
416      properties.add(new Property(context, child, sd, this.profileUtilities));
417    }
418    return properties;
419  }
420
421  protected List<Property> getChildProperties(TypeDetails type) throws DefinitionException {
422    ElementDefinition ed = definition;
423    StructureDefinition sd = structure;
424    SourcedChildDefinitions children = profileUtilities.getChildMap(sd, ed);
425    if (children.getList().isEmpty()) {
426      // ok, find the right definitions
427      String t = null;
428      if (ed.getType().size() == 1)
429        t = ed.getType().get(0).getCode();
430      else if (ed.getType().size() == 0)
431        throw new Error("types == 0, and no children found");
432      else {
433        t = ed.getType().get(0).getCode();
434        boolean all = true;
435        for (TypeRefComponent tr : ed.getType()) {
436          if (!tr.getCode().equals(t)) {
437            all = false;
438            break;
439          }
440        }
441        if (!all) {
442          // ok, it's polymorphic
443          t = type.getType();
444        }
445      }
446      if (!"xhtml".equals(t)) {
447        sd = context.fetchResource(StructureDefinition.class, t);
448        if (sd == null)
449          throw new DefinitionException("Unable to find class '"+t+"' for name '"+ed.getPath()+"' on property "+definition.getPath());
450        children = profileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0));
451      }
452    }
453    List<Property> properties = new ArrayList<Property>();
454    for (ElementDefinition child : children.getList()) {
455      properties.add(new Property(context, child, sd, this.profileUtilities));
456    }
457    return properties;
458  }
459
460  private String tail(String path) {
461    return path.contains(".") ? path.substring(path.lastIndexOf(".")+1) : path;
462  }
463
464  public Property getChild(String elementName, String childName) throws FHIRException {
465    List<Property> children = getChildProperties(elementName, null);
466    for (Property p : children) {
467      if (p.getName().equals(childName)) {
468        return p;
469      }
470    }
471    return null;
472  }
473
474  public Property getChild(String name, TypeDetails type) throws DefinitionException {
475    List<Property> children = getChildProperties(type);
476    for (Property p : children) {
477      if (p.getName().equals(name) || p.getName().equals(name+"[x]")) {
478        return p;
479      }
480    }
481    return null;
482  }
483
484  public Property getChild(String name) throws FHIRException {
485    List<Property> children = getChildProperties(name, null);
486    for (Property p : children) {
487      if (p.getName().equals(name)) {
488        return p;
489      }
490    }
491    return null;
492  }
493
494  public Property getChildSimpleName(String elementName, String name) throws FHIRException {
495    List<Property> children = getChildProperties(elementName, null);
496    for (Property p : children) {
497      if (p.getName().equals(name) || p.getName().equals(name+"[x]")) {
498        return p;
499      }
500    }
501    return null;
502  }
503
504  public IWorkerContext getContext() {
505    return context;
506  }
507
508  @Override
509  public String toString() {
510    return definition.getPath();
511  }
512
513
514  public boolean isJsonKeyArray() {
515    return definition.hasExtension(ToolingExtensions.EXT_JSON_PROP_KEY);
516  }
517
518
519  public String getJsonKeyProperty() {
520    return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_PROP_KEY);
521  }
522
523
524  public boolean hasTypeSpecifier() {
525    return definition.hasExtension(ToolingExtensions.EXT_TYPE_SPEC);
526  }
527
528
529  public List<StringPair> getTypeSpecifiers() {
530    List<StringPair> res = new ArrayList<>();
531    for (Extension e : definition.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_SPEC)) {
532      res.add(new StringPair(ToolingExtensions.readStringExtension(e,  "condition"), ToolingExtensions.readStringExtension(e,  "type")));
533    }
534    return res;
535  }
536
537
538  public Property cloneToType(StructureDefinition sd) {
539    Property res = new Property(context, definition.copy(), sd);
540    res.definition.getType().clear();
541    res.definition.getType().add(new TypeRefComponent(sd.getUrl()));
542    return res;
543  }
544
545
546  public boolean hasImpliedPrefix() {
547    return definition.hasExtension(ToolingExtensions.EXT_IMPLIED_PREFIX);
548  }
549
550
551  public String getImpliedPrefix() {
552    return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_IMPLIED_PREFIX);
553  }
554
555
556  public boolean isNullable() {    
557    return ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_JSON_NULLABLE);
558  }
559
560
561  public String summary() {
562    return structure.getUrl()+"#"+definition.getId();
563  }
564
565
566  public boolean canBeEmpty() {
567    if (definition.hasExtension(ToolingExtensions.EXT_JSON_EMPTY)) {
568      return !"absent".equals(ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_EMPTY));
569    } else {
570      return false;
571    }
572  }
573
574
575  public boolean isLogical() {
576    return structure.getKind() == StructureDefinitionKind.LOGICAL;
577  }
578
579
580  public ProfileUtilities getUtils() {
581    return profileUtilities;
582  }
583
584  public boolean isJsonPrimitiveChoice() {
585    return ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_JSON_PRIMITIVE_CHOICE);
586  }
587
588  public Object typeSummary() {
589    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" | ");
590    for (TypeRefComponent t : definition.getType()) {
591      b.append(t.getCode());
592    }
593    return b.toString();
594  }
595
596
597  public boolean hasJsonName() {
598    return definition.hasExtension(ToolingExtensions.EXT_JSON_NAME);
599  }
600
601
602  public boolean isTranslatable() {
603    boolean ok = ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_TRANSLATABLE);
604    if (!ok && !Utilities.existsInList(definition.getBase().getPath(), "Reference.reference", "Coding.version", "Identifier.value", "SampledData.offsets", "SampledData.data", "ContactPoint.value")) {
605      String t = getType();
606      ok = Utilities.existsInList(t, "string", "markdown");
607    }
608    return ok;
609  }
610
611  
612}