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                                        throw new Error("logic error, gettype when types > 1");
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          return TypesUtilities.isPrimitive(code);
253         // was this... but this can be very inefficient compared to hard coding the list
254//              StructureDefinition sd = context.fetchTypeDefinition(code);
255//      return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
256        }
257
258        public boolean isPrimitive() {
259          return isPrimitive(getType());
260        }
261        private String lowFirst(String t) {
262                return t.substring(0, 1).toLowerCase()+t.substring(1);
263        }
264
265        public boolean isResource() {
266          if (type != null) {
267            String tc = type.getCode();
268      return (("Resource".equals(tc) || "DomainResource".equals(tc)) ||  Utilities.existsInList(tc, context.getResourceNames()));
269          } else if (definition.getType().size() > 0) {
270      String tc = definition.getType().get(0).getCode();
271      return definition.getType().size() == 1 && (("Resource".equals(tc) || "DomainResource".equals(tc)) ||  Utilities.existsInList(tc, context.getResourceNames()));
272    }
273          else
274            return !definition.getPath().contains(".") && (structure.getKind() == StructureDefinitionKind.RESOURCE);
275        }
276
277  public boolean isList() {
278    return !"1".equals(definition.getMax());
279  }
280
281  public boolean isBaseList() {
282    return !"1".equals(definition.getBase().getMax());
283  }
284
285  public String getScopedPropertyName() {
286    return definition.getBase().getPath();
287  }
288
289  private boolean isElementWithOnlyExtension(final ElementDefinition ed, final List<ElementDefinition> children) {
290    boolean result = false;
291    if (!ed.getType().isEmpty()) {
292      result = true;
293      for (final ElementDefinition ele : children) {
294        if (!ele.getPath().contains("extension")) {
295          result = false;
296          break;
297        }
298      }
299    }
300    return result;
301  }
302  
303        public boolean IsLogicalAndHasPrimitiveValue(String name) {
304//              if (canBePrimitive!= null)
305//                      return canBePrimitive;
306                
307        if (structure.getKind() != StructureDefinitionKind.LOGICAL)
308                return false;
309        if (!hasType(name))
310                return false;
311        StructureDefinition sd = context.fetchResource(StructureDefinition.class, structure.getUrl().substring(0, structure.getUrl().lastIndexOf("/")+1)+getType(name));
312        if (sd == null)
313          sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(getType(name), null));
314    if (sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE)
315      return true;
316        if (sd == null || sd.getKind() != StructureDefinitionKind.LOGICAL)
317                return false;
318        for (ElementDefinition ed : sd.getSnapshot().getElement()) {
319                if (ed.getPath().equals(sd.getId()+".value") && ed.getType().size() == 1 && isPrimitive(ed.getType().get(0).getCode())) {
320                        return true;
321                }
322        }
323        return false;
324        }
325
326  public boolean isChoice() {
327    if (type != null) {
328      return true;
329    }
330    if (definition.getType().size() <= 1)
331      return false;
332    String tn = definition.getType().get(0).getCode();
333    for (int i = 1; i < definition.getType().size(); i++) 
334      if (!definition.getType().get(i).getCode().equals(tn))
335        return true;
336    return false;
337  }
338
339
340  public List<Property> getChildProperties(String elementName, String statedType) throws FHIRException {
341    ElementDefinition ed = definition;
342    StructureDefinition sd = structure;
343    SourcedChildDefinitions children = profileUtilities.getChildMap(sd, ed);
344    String url = null;
345    if (children.getList().isEmpty() || isElementWithOnlyExtension(ed, children.getList())) {
346      // ok, find the right definitions
347      String t = null;
348      if (ed.getType().size() == 1)
349        t = ed.getType().get(0).getWorkingCode();
350      else if (ed.getType().size() == 0)
351        throw new Error("types == 0, and no children found on "+getDefinition().getPath());
352      else {
353        t = ed.getType().get(0).getWorkingCode();
354        boolean all = true;
355        for (TypeRefComponent tr : ed.getType()) {
356          if (!tr.getWorkingCode().equals(t)) {
357            all = false;
358            break;
359          }
360        }
361        if (!all) {
362          // ok, it's polymorphic
363          if (ed.hasRepresentation(PropertyRepresentation.TYPEATTR)) {
364            t = statedType;
365            if (t == null && ToolingExtensions.hasExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype"))
366              t = ToolingExtensions.readStringExtension(ed, "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype");
367            boolean ok = false;
368            for (TypeRefComponent tr : ed.getType()) { 
369              if (tr.getWorkingCode().equals(t)) 
370                ok = true;
371              if (Utilities.isAbsoluteUrl(tr.getWorkingCode())) {
372                StructureDefinition sdt = context.fetchResource(StructureDefinition.class, tr.getWorkingCode());
373                if (sdt != null && sdt.getType().equals(t)) {
374                  url = tr.getWorkingCode();
375                  ok = true;
376                }
377              }
378              if (ok)
379                break;
380            }
381             if (!ok)
382               throw new DefinitionException("Type '"+t+"' is not an acceptable type for '"+elementName+"' on property "+definition.getPath());
383            
384          } else {
385            t = elementName.substring(tail(ed.getPath()).length() - 3);
386            if (isPrimitive(lowFirst(t)))
387              t = lowFirst(t);
388          }
389        }
390      }
391      if (!"xhtml".equals(t)) {
392        for (TypeRefComponent aType: ed.getType()) {
393          if (aType.getWorkingCode().equals(t)) {
394            if (aType.hasProfile()) {
395              assert aType.getProfile().size() == 1; 
396              url = aType.getProfile().get(0).getValue();
397            } else {
398              url = ProfileUtilities.sdNs(t, null);
399            }
400            break;
401          }
402        }
403        if (url==null)
404          throw new FHIRException("Unable to find type " + t + " for element " + elementName + " with path " + ed.getPath());
405        sd = context.fetchResource(StructureDefinition.class, url);        
406        if (sd == null)
407          throw new DefinitionException("Unable to find type '"+t+"' for name '"+elementName+"' on property "+definition.getPath());
408        children = profileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0));
409      }
410    }
411    List<Property> properties = new ArrayList<Property>();
412    for (ElementDefinition child : children.getList()) {
413      properties.add(new Property(context, child, sd, this.profileUtilities));
414    }
415    return properties;
416  }
417
418  protected List<Property> getChildProperties(TypeDetails type) throws DefinitionException {
419    ElementDefinition ed = definition;
420    StructureDefinition sd = structure;
421    SourcedChildDefinitions children = profileUtilities.getChildMap(sd, ed);
422    if (children.getList().isEmpty()) {
423      // ok, find the right definitions
424      String t = null;
425      if (ed.getType().size() == 1)
426        t = ed.getType().get(0).getCode();
427      else if (ed.getType().size() == 0)
428        throw new Error("types == 0, and no children found");
429      else {
430        t = ed.getType().get(0).getCode();
431        boolean all = true;
432        for (TypeRefComponent tr : ed.getType()) {
433          if (!tr.getCode().equals(t)) {
434            all = false;
435            break;
436          }
437        }
438        if (!all) {
439          // ok, it's polymorphic
440          t = type.getType();
441        }
442      }
443      if (!"xhtml".equals(t)) {
444        sd = context.fetchResource(StructureDefinition.class, t);
445        if (sd == null)
446          throw new DefinitionException("Unable to find class '"+t+"' for name '"+ed.getPath()+"' on property "+definition.getPath());
447        children = profileUtilities.getChildMap(sd, sd.getSnapshot().getElement().get(0));
448      }
449    }
450    List<Property> properties = new ArrayList<Property>();
451    for (ElementDefinition child : children.getList()) {
452      properties.add(new Property(context, child, sd, this.profileUtilities));
453    }
454    return properties;
455  }
456
457  private String tail(String path) {
458    return path.contains(".") ? path.substring(path.lastIndexOf(".")+1) : path;
459  }
460
461  public Property getChild(String elementName, String childName) throws FHIRException {
462    List<Property> children = getChildProperties(elementName, null);
463    for (Property p : children) {
464      if (p.getName().equals(childName)) {
465        return p;
466      }
467    }
468    return null;
469  }
470
471  public Property getChild(String name, TypeDetails type) throws DefinitionException {
472    List<Property> children = getChildProperties(type);
473    for (Property p : children) {
474      if (p.getName().equals(name) || p.getName().equals(name+"[x]")) {
475        return p;
476      }
477    }
478    return null;
479  }
480
481  public Property getChild(String name) throws FHIRException {
482    List<Property> children = getChildProperties(name, null);
483    for (Property p : children) {
484      if (p.getName().equals(name)) {
485        return p;
486      }
487    }
488    return null;
489  }
490
491  public Property getChildSimpleName(String elementName, String name) throws FHIRException {
492    List<Property> children = getChildProperties(elementName, null);
493    for (Property p : children) {
494      if (p.getName().equals(name) || p.getName().equals(name+"[x]")) {
495        return p;
496      }
497    }
498    return null;
499  }
500
501  public IWorkerContext getContext() {
502    return context;
503  }
504
505  @Override
506  public String toString() {
507    return definition.getPath();
508  }
509
510
511  public boolean isJsonKeyArray() {
512    return definition.hasExtension(ToolingExtensions.EXT_JSON_PROP_KEY);
513  }
514
515
516  public String getJsonKeyProperty() {
517    return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_PROP_KEY);
518  }
519
520
521  public boolean hasTypeSpecifier() {
522    return definition.hasExtension(ToolingExtensions.EXT_TYPE_SPEC);
523  }
524
525
526  public List<StringPair> getTypeSpecifiers() {
527    List<StringPair> res = new ArrayList<>();
528    for (Extension e : definition.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_SPEC)) {
529      res.add(new StringPair(ToolingExtensions.readStringExtension(e,  "condition"), ToolingExtensions.readStringExtension(e,  "type")));
530    }
531    return res;
532  }
533
534
535  public Property cloneToType(StructureDefinition sd) {
536    Property res = new Property(context, definition.copy(), sd);
537    res.definition.getType().clear();
538    res.definition.getType().add(new TypeRefComponent(sd.getUrl()));
539    return res;
540  }
541
542
543  public boolean hasImpliedPrefix() {
544    return definition.hasExtension(ToolingExtensions.EXT_IMPLIED_PREFIX);
545  }
546
547
548  public String getImpliedPrefix() {
549    return ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_IMPLIED_PREFIX);
550  }
551
552
553  public boolean isNullable() {    
554    return ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_JSON_NULLABLE);
555  }
556
557
558  public String summary() {
559    return structure.getUrl()+"#"+definition.getId();
560  }
561
562
563  public boolean canBeEmpty() {
564    if (definition.hasExtension(ToolingExtensions.EXT_JSON_EMPTY)) {
565      return !"absent".equals(ToolingExtensions.readStringExtension(definition, ToolingExtensions.EXT_JSON_EMPTY));
566    } else {
567      return false;
568    }
569  }
570
571
572  public boolean isLogical() {
573    return structure.getKind() == StructureDefinitionKind.LOGICAL;
574  }
575
576
577  public ProfileUtilities getUtils() {
578    return profileUtilities;
579  }
580
581  public boolean isJsonPrimitiveChoice() {
582    return ToolingExtensions.readBoolExtension(definition, ToolingExtensions.EXT_JSON_PRIMITIVE_CHOICE);
583  }
584
585  public Object typeSummary() {
586    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(" | ");
587    for (TypeRefComponent t : definition.getType()) {
588      b.append(t.getCode());
589    }
590    return b.toString();
591  }
592
593
594  public boolean hasJsonName() {
595    return definition.hasExtension(ToolingExtensions.EXT_JSON_NAME);
596  }
597
598  
599}