001package org.hl7.fhir.r4.elementmodel;
002
003import static org.apache.commons.lang3.StringUtils.isNotBlank;
004
005import java.io.IOException;
006import java.util.*;
007
008import org.apache.commons.lang3.Validate;
009import org.hl7.fhir.r4.conformance.ProfileUtilities;
010import org.hl7.fhir.r4.elementmodel.Element.ElementSortComparator;
011import org.hl7.fhir.r4.elementmodel.Element.ICodingImpl;
012import org.hl7.fhir.r4.model.Base;
013import org.hl7.fhir.r4.model.Coding;
014import org.hl7.fhir.r4.model.ElementDefinition;
015import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
016import org.hl7.fhir.r4.model.Enumerations.BindingStrength;
017import org.hl7.fhir.r4.model.ICoding;
018import org.hl7.fhir.r4.model.StringType;
019import org.hl7.fhir.r4.model.StructureDefinition;
020import org.hl7.fhir.r4.model.Type;
021import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
022import org.hl7.fhir.r4.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
023import org.hl7.fhir.exceptions.FHIRException;
024import org.hl7.fhir.utilities.ElementDecoration;
025import org.hl7.fhir.utilities.ElementDecoration.DecorationType;
026import org.hl7.fhir.utilities.Utilities;
027import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
028import org.hl7.fhir.utilities.xhtml.XhtmlNode;
029
030/**
031 * This class represents the underlying reference model of FHIR
032 * 
033 * A resource is nothing but a set of elements, where every element has a 
034 * name, maybe a stated type, maybe an id, and either a value or child elements 
035 * (one or the other, but not both or neither)
036 * 
037 * @author Grahame Grieve
038 *
039 */
040public class Element extends Base {
041
042
043  public enum SpecialElement {
044                CONTAINED, BUNDLE_ENTRY, BUNDLE_OUTCOME, PARAMETER;
045
046    public static SpecialElement fromProperty(Property property) {
047      if (property.getStructure().getIdElement().getIdPart().equals("Parameters"))
048        return PARAMETER;
049      if (property.getStructure().getIdElement().getIdPart().equals("Bundle") && property.getName().equals("resource"))
050        return BUNDLE_ENTRY;
051      if (property.getStructure().getIdElement().getIdPart().equals("Bundle") && property.getName().equals("outcome"))
052        return BUNDLE_OUTCOME;
053      if (property.getName().equals("contained")) 
054        return CONTAINED;
055      throw new Error("Unknown resource containing a native resource: "+property.getDefinition().getId());
056    }
057        }
058
059        private List<String> comments;// not relevant for production, but useful in documentation
060        private String name;
061        private String type;
062        private String value;
063        private int index = -1;
064        private List<Element> children;
065        private Property property;
066  private Property elementProperty; // this is used when special is set to true - it tracks the underlying element property which is used in a few places
067        private int line;
068        private int col;
069        private SpecialElement special;
070        private XhtmlNode xhtml; // if this is populated, then value will also hold the string representation
071
072        public Element(String name) {
073                super();
074                this.name = name;
075        }
076
077  public Element(Element other) {
078    super();
079    name = other.name;
080    type = other.type;
081    property = other.property;
082    elementProperty = other.elementProperty;
083    special = other.special;
084  }
085  
086  public Element(String name, Property property) {
087                super();
088                this.name = name;
089                this.property = property;
090        }
091
092        public Element(String name, Property property, String type, String value) {
093                super();
094                this.name = name;
095                this.property = property;
096                this.type = type;
097                this.value = value;
098        }
099
100        public void updateProperty(Property property, SpecialElement special, Property elementProperty) {
101                this.property = property;
102    this.elementProperty = elementProperty;
103                this.special = special;
104        }
105
106        public SpecialElement getSpecial() {
107                return special;
108        }
109
110        public String getName() {
111                return name;
112        }
113
114        public String getType() {
115                if (type == null)
116                        return property.getType(name);
117                else
118                  return type;
119        }
120
121        public String getValue() {
122                return value;
123        }
124
125        public boolean hasChildren() {
126                return !(children == null || children.isEmpty());
127        }
128
129        public List<Element> getChildren() {
130                if (children == null)
131                        children = new ArrayList<Element>();
132                return children;
133        }
134
135        public boolean hasComments() {
136                return !(comments == null || comments.isEmpty());
137        }
138
139        public List<String> getComments() {
140                if (comments == null)
141                        comments = new ArrayList<String>();
142                return comments;
143        }
144
145        public Property getProperty() {
146                return property;
147        }
148
149        public void setValue(String value) {
150                this.value = value;
151        }
152
153        public void setType(String type) {
154                this.type = type;
155
156        }
157
158        public boolean hasValue() {
159                return value != null;
160        }
161
162        public List<Element> getChildrenByName(String name) {
163                List<Element> res = new ArrayList<Element>();
164                if (hasChildren()) {
165                        for (Element child : children)
166                                if (name.equals(child.getName()))
167                                        res.add(child);
168                }
169                return res;
170        }
171
172        public void numberChildren() {
173                if (children == null)
174                        return;
175                
176                String last = "";
177                int index = 0;
178                for (Element child : children) {
179                        if (child.getProperty().isList()) {
180                          if (last.equals(child.getName())) {
181                                index++;
182                          } else {
183                                last = child.getName();
184                                index = 0;
185                          }
186                        child.index = index;
187                        } else {
188                                child.index = -1;
189                        }
190                        child.numberChildren();
191                }       
192        }
193
194        public int getIndex() {
195                return index;
196        }
197
198        public boolean hasIndex() {
199                return index > -1;
200        }
201
202        public void setIndex(int index) {
203                this.index = index;
204        }
205
206        public String getChildValue(String name) {
207                if (children == null)
208                        return null;
209                for (Element child : children) {
210                        if (name.equals(child.getName()))
211                                return child.getValue();
212                }
213        return null;
214        }
215
216  public void setChildValue(String name, String value) {
217    if (children == null)
218      children = new ArrayList<Element>();
219    for (Element child : children) {
220      if (name.equals(child.getName())) {
221        if (!child.isPrimitive())
222          throw new Error("Cannot set a value of a non-primitive type ("+name+" on "+this.getName()+")");
223        child.setValue(value);
224      }
225    }
226    try {
227      setProperty(name.hashCode(), name, new StringType(value));
228    } catch (FHIRException e) {
229      throw new Error(e);
230    }
231  }
232
233        public List<Element> getChildren(String name) {
234                List<Element> res = new ArrayList<Element>(); 
235                if (children != null)
236                for (Element child : children) {
237                        if (name.equals(child.getName()))
238                                res.add(child);
239                }
240                return res;
241        }
242
243  public boolean hasType() {
244    if (type == null)
245      return property.hasType(name);
246    else
247      return true;
248  }
249
250  @Override
251  public String fhirType() {
252    return getType();
253  }
254
255  @Override
256        public Base[] getProperty(int hash, String name, boolean checkValid) throws FHIRException {
257        if (isPrimitive() && (hash == "value".hashCode()) && !Utilities.noString(value)) {
258//              String tn = getType();
259//              throw new Error(tn+" not done yet");
260          Base[] b = new Base[1];
261          b[0] = new StringType(value);
262          return b;
263        }
264                
265        List<Base> result = new ArrayList<Base>();
266        if (children != null) {
267        for (Element child : children) {
268                if (child.getName().equals(name))
269                        result.add(child);
270                if (child.getName().startsWith(name) && child.getProperty().isChoice() && child.getProperty().getName().equals(name+"[x]"))
271                        result.add(child);
272        }
273        }
274        if (result.isEmpty() && checkValid) {
275//              throw new FHIRException("not determined yet");
276        }
277        return result.toArray(new Base[result.size()]);
278        }
279
280        @Override
281        protected void listChildren(List<org.hl7.fhir.r4.model.Property> childProps) {
282          if (children != null) {
283            Map<String, org.hl7.fhir.r4.model.Property> map = new HashMap<String, org.hl7.fhir.r4.model.Property>();
284            for (Element c : children) {
285              org.hl7.fhir.r4.model.Property p = map.get(c.getName());
286              if (p == null) {
287              p = new org.hl7.fhir.r4.model.Property(c.getName(), c.fhirType(), c.getProperty().getDefinition().getDefinition(), c.getProperty().getDefinition().getMin(), maxToInt(c.getProperty().getDefinition().getMax()), c);
288          childProps.add(p);
289          map.put(c.getName(), p);
290              
291              } else
292                p.getValues().add(c);
293            }
294          }
295        }
296        
297  @Override
298  public Base setProperty(int hash, String name, Base value) throws FHIRException {
299    if ("xhtml".equals(getType()) && (hash == "value".hashCode())) {
300      this.xhtml = castToXhtml(value);
301      this.value =  castToXhtmlString(value);
302      return this;
303    }
304    if (isPrimitive() && (hash == "value".hashCode())) {
305      this.value = castToString(value).asStringValue();
306      return this;
307    }
308    
309    if (!value.isPrimitive() && !(value instanceof Element)) {
310      if (isDataType(value)) 
311        value = convertToElement(property.getChild(name), value);
312      else
313        throw new FHIRException("Cannot set property "+name+" on "+this.name+" - value is not a primitive type ("+value.fhirType()+") or an ElementModel type");
314    }
315    
316    if (children == null)
317      children = new ArrayList<Element>();
318    Element childForValue = null;
319    
320    // look through existing children
321    for (Element child : children) {
322      if (child.getName().equals(name)) {
323        if (!child.isList()) {
324          childForValue = child;
325          break;
326        } else {
327          Element ne = new Element(child);
328          children.add(ne);
329          numberChildren();
330          childForValue = ne;
331          break;
332        }
333      }
334    }
335
336    int i = 0;
337    if (childForValue == null)
338      for (Property p : property.getChildProperties(this.name, type)) {
339        int t = -1;
340        for (int c =0; c < children.size(); c++) {
341          Element e = children.get(c);
342          if (p.getName().equals(e.getName()))
343            t = c;
344        }
345        if (t > i)
346          i = t;
347        if (p.getName().equals(name) || p.getName().equals(name+"[x]")) {
348          Element ne = new Element(name, p);
349          children.add(i, ne);
350          childForValue = ne;
351          break;
352        }
353      }
354    
355    if (childForValue == null)
356      throw new Error("Cannot set property "+name+" on "+this.name);
357    else if (value.isPrimitive()) {
358      if (childForValue.property.getName().endsWith("[x]"))
359        childForValue.name = name+Utilities.capitalize(value.fhirType());
360      childForValue.setValue(value.primitiveValue());
361    } else {
362      Element ve = (Element) value;
363      childForValue.type = ve.getType();
364      if (childForValue.property.getName().endsWith("[x]"))
365        childForValue.name = name+Utilities.capitalize(childForValue.type);
366      else if (value.isResource()) {
367        if (childForValue.elementProperty == null)
368          childForValue.elementProperty = childForValue.property;
369        childForValue.property = ve.property;
370        childForValue.special = SpecialElement.BUNDLE_ENTRY;
371      }
372      if (ve.children != null) {
373        if (childForValue.children == null)
374          childForValue.children = new ArrayList<Element>();
375        else 
376          childForValue.children.clear();
377        childForValue.children.addAll(ve.children);
378      }
379    }
380    return childForValue;
381  }
382
383  private Base convertToElement(Property prop, Base v) throws FHIRException {
384    return new ObjectConverter(property.getContext()).convert(prop, (Type) v);
385  }
386
387  private boolean isDataType(Base v) {
388    return v instanceof Type &&  property.getContext().getTypeNames().contains(v.fhirType());
389  }
390
391  @Override
392  public Base makeProperty(int hash, String name) throws FHIRException {
393    if (isPrimitive() && (hash == "value".hashCode())) {
394      return new StringType(value);
395    }
396
397    if (children == null)
398      children = new ArrayList<Element>();
399    
400    // look through existing children
401    for (Element child : children) {
402      if (child.getName().equals(name)) {
403        if (!child.isList()) {
404          return child;
405        } else {
406          Element ne = new Element(child);
407          children.add(ne);
408          numberChildren();
409          return ne;
410        }
411      }
412    }
413
414    for (Property p : property.getChildProperties(this.name, type)) {
415      if (p.getName().equals(name)) {
416        Element ne = new Element(name, p);
417        children.add(ne);
418        return ne;
419      }
420    }
421      
422    throw new Error("Unrecognised name "+name+" on "+this.name); 
423  }
424  
425        private int maxToInt(String max) {
426    if (max.equals("*"))
427      return Integer.MAX_VALUE;
428    else
429      return Integer.parseInt(max);
430        }
431
432        @Override
433        public boolean isPrimitive() {
434                return type != null ? property.isPrimitive(type) : property.isPrimitive(property.getType(name));
435        }
436        
437  @Override
438  public boolean isBooleanPrimitive() {
439    return isPrimitive() && ("boolean".equals(type) || "boolean".equals(property.getType(name)));
440  }
441 
442  @Override
443  public boolean isResource() {
444    return property.isResource();
445  }
446  
447
448        @Override
449        public boolean hasPrimitiveValue() {
450                return property.isPrimitiveName(name) || property.IsLogicalAndHasPrimitiveValue(name);
451        }
452        
453
454        @Override
455        public String primitiveValue() {
456                if (isPrimitive())
457                  return value;
458                else {
459                        if (hasPrimitiveValue() && children != null) {
460                                for (Element c : children) {
461                                        if (c.getName().equals("value"))
462                                                return c.primitiveValue();
463                                }
464                        }
465                        return null;
466                }
467        }
468        
469        // for the validator
470  public int line() {
471    return line;
472  }
473
474  public int col() {
475    return col;
476  }
477
478        public Element markLocation(int line, int col) {
479                this.line = line;
480                this.col = col; 
481                return this;
482        }
483
484        public void clearDecorations() {
485          clearUserData("fhir.decorations");
486          for (Element e : children)
487            e.clearDecorations();         
488        }
489        
490        public void markValidation(StructureDefinition profile, ElementDefinition definition) {
491          @SuppressWarnings("unchecked")
492    List<ElementDecoration> decorations = (List<ElementDecoration>) getUserData("fhir.decorations");
493          if (decorations == null) {
494            decorations = new ArrayList<ElementDecoration>();
495            setUserData("fhir.decorations", decorations);
496          }
497          decorations.add(new ElementDecoration(DecorationType.TYPE, profile.getUserString("path"), definition.getPath()));
498          if (definition.getId() != null && tail(definition.getId()).contains(":")) {
499            String[] details = tail(definition.getId()).split("\\:");
500            decorations.add(new ElementDecoration(DecorationType.SLICE, null, details[1]));
501          }
502        }
503        
504  private String tail(String id) {
505    return id.contains(".") ? id.substring(id.lastIndexOf(".")+1) : id;
506  }
507
508  public Element getNamedChild(String name) {
509          if (children == null)
510                return null;
511          Element result = null;
512          for (Element child : children) {
513                if (child.getName().equals(name)) {
514                        if (result == null)
515                                result = child;
516                        else 
517                                throw new Error("Attempt to read a single element when there is more than one present ("+name+")");
518                }
519          }
520          return result;
521        }
522
523  public void getNamedChildren(String name, List<Element> list) {
524        if (children != null)
525                for (Element child : children) 
526                        if (child.getName().equals(name))
527                                list.add(child);
528  }
529
530  public String getNamedChildValue(String name) {
531        Element child = getNamedChild(name);
532        return child == null ? null : child.value;
533  }
534
535  public void getNamedChildrenWithWildcard(String string, List<Element> values) {
536          Validate.isTrue(string.endsWith("[x]"));
537          
538          String start = string.substring(0, string.length() - 3);
539                if (children != null) {
540                        for (Element child : children) { 
541                                if (child.getName().startsWith(start)) {
542                                        values.add(child);
543                                }
544                        }
545                }
546  }
547
548  
549        public XhtmlNode getXhtml() {
550                return xhtml;
551        }
552
553        public Element setXhtml(XhtmlNode xhtml) {
554                this.xhtml = xhtml;
555                return this;
556        }
557
558        @Override
559        public boolean isEmpty() {
560                if (value != null && !"".equals(value)) {
561                        return false;
562                }
563                for (Element next : getChildren()) {
564                        if (!next.isEmpty()) {
565                                return false;
566                        }
567                }
568                return true;
569        }
570
571  public Property getElementProperty() {
572    return elementProperty;
573  }
574
575  public boolean hasElementProperty() {
576    return elementProperty != null;
577  }
578
579  public boolean hasChild(String name) {
580    return getNamedChild(name) != null;
581  }
582
583  @Override
584  public String toString() {
585    return name+"="+fhirType() + "["+(children == null || hasValue() ? value : Integer.toString(children.size())+" children")+"]";
586  }
587
588  @Override
589  public String getIdBase() {
590    return getChildValue("id");
591  }
592
593  @Override
594  public void setIdBase(String value) {
595    setChildValue("id", value);
596  }
597
598
599  @Override
600  public boolean equalsDeep(Base other) {
601    if (!super.equalsDeep(other))
602      return false;
603    if (isPrimitive() && other.isPrimitive())
604      return primitiveValue().equals(other.primitiveValue());
605    if (isPrimitive() || other.isPrimitive())
606      return false;
607    Set<String> processed  = new HashSet<String>();
608    for (org.hl7.fhir.r4.model.Property p : children()) {
609      String name = p.getName();
610      processed.add(name);
611      org.hl7.fhir.r4.model.Property o = other.getChildByName(name);
612      if (!equalsDeep(p, o))
613        return false;
614    }
615    for (org.hl7.fhir.r4.model.Property p : children()) {
616      String name = p.getName();
617      if (!processed.contains(name)) {
618        org.hl7.fhir.r4.model.Property o = other.getChildByName(name);
619        if (!equalsDeep(p, o))
620          return false;
621      }
622    }
623    return true;
624  }
625
626  private boolean equalsDeep(org.hl7.fhir.r4.model.Property p, org.hl7.fhir.r4.model.Property o) {
627    if (o == null || p == null)
628      return false;
629    if (p.getValues().size() != o.getValues().size())
630      return false;
631    for (int i = 0; i < p.getValues().size(); i++)
632      if (!Base.compareDeep(p.getValues().get(i), o.getValues().get(i), true))
633        return false;
634    return true;
635  }
636
637  @Override
638  public boolean equalsShallow(Base other) {
639    if (!super.equalsShallow(other))
640      return false;
641    if (isPrimitive() && other.isPrimitive())
642      return primitiveValue().equals(other.primitiveValue());
643    if (isPrimitive() || other.isPrimitive())
644      return false;
645    return true; //?
646  }
647
648  public Type asType() throws FHIRException {
649    return new ObjectConverter(property.getContext()).convertToType(this);
650  }
651
652  @Override
653  public boolean isMetadataBased() {
654    return true;
655  }
656
657  public boolean isList() {
658    if (elementProperty != null)
659      return elementProperty.isList();
660    else
661      return property.isList();
662  }
663  
664  @Override
665  public String[] getTypesForProperty(int hash, String name) throws FHIRException {
666    Property p = property.getChildSimpleName(this.name, name);
667    if (p != null) {
668      Set<String> types = new HashSet<String>();
669      for (TypeRefComponent tr : p.getDefinition().getType()) {
670        types.add(tr.getCode());
671      }
672      return types.toArray(new String[]{});
673    }
674    return super.getTypesForProperty(hash, name);
675
676  }
677
678  public void sort() {
679    if (children != null) {
680      List<Element> remove = new ArrayList<Element>();
681      for (Element child : children) {
682        child.sort();
683        if (child.isEmpty())
684          remove.add(child);
685      }
686      children.removeAll(remove);
687      Collections.sort(children, new ElementSortComparator(this, this.property));
688    }
689  }
690
691  public class ElementSortComparator implements Comparator<Element> {
692    private List<ElementDefinition> children;
693    public ElementSortComparator(Element e, Property property) {
694      String tn = e.getType();
695      StructureDefinition sd = property.getContext().fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(tn, property.getContext().getOverrideVersionNs()));
696      if (sd != null && !sd.getAbstract())
697        children = sd.getSnapshot().getElement();
698      else
699        children = property.getStructure().getSnapshot().getElement();
700    }
701    
702    @Override
703    public int compare(Element e0, Element e1) {
704      int i0 = find(e0);
705      int i1 = find(e1);
706      return Integer.compare(i0, i1);
707    }
708    private int find(Element e0) {
709      int i =  e0.elementProperty != null ? children.indexOf(e0.elementProperty.getDefinition()) :  children.indexOf(e0.property.getDefinition());
710      return i; 
711    }
712
713  }
714
715  public class ICodingImpl implements ICoding {
716    private String system;
717    private String version;
718    private String code;
719    private String display;
720    private boolean doesSystem;
721    private boolean doesVersion;
722    private boolean doesCode;
723    private boolean doesDisplay;
724    public ICodingImpl(boolean doesCode, boolean doesSystem, boolean doesVersion, boolean doesDisplay) {
725      super();
726      this.doesCode = doesCode;
727      this.doesSystem = doesSystem;
728      this.doesVersion = doesVersion;
729      this.doesDisplay = doesDisplay;
730    }
731    public String getSystem() {
732      return system;
733    }
734    public String getVersion() {
735      return version;
736    }
737    public String getCode() {
738      return code;
739    }
740    public String getDisplay() {
741      return display;
742    }
743    public boolean hasSystem() {
744      return !Utilities.noString(system); 
745    }
746    public boolean hasVersion() {
747      return !Utilities.noString(version);
748    }
749    public boolean hasCode() {
750      return !Utilities.noString(code);
751    }
752    public boolean hasDisplay() {
753      return !Utilities.noString(display);
754    }
755    public boolean supportsSystem() {
756      return doesSystem;
757    }
758    public boolean supportsVersion() {
759      return doesVersion;
760    }
761    public boolean supportsCode() {
762      return doesCode;
763    }
764    public boolean supportsDisplay() {
765      return doesDisplay;
766    }    
767  }
768
769  public ICoding getAsICoding() throws FHIRException {
770    if ("code".equals(fhirType())) {
771      if (property.getDefinition().getBinding().getStrength() != BindingStrength.REQUIRED)
772        return null;
773      ICodingImpl c = new ICodingImpl(true, true, false, false);
774      c.code = primitiveValue();
775      ValueSetExpansionOutcome vse = property.getContext().expandVS(property.getDefinition().getBinding(), true, false);
776      if (vse.getValueset() == null)
777        return null;
778      for (ValueSetExpansionContainsComponent cc : vse.getValueset().getExpansion().getContains()) {
779        if (cc.getCode().equals(c.code)) {
780          c.system = cc.getSystem();
781          if (cc.hasVersion()) {
782            c.doesVersion = true;
783            c.version = cc.getVersion();
784          }
785          if (cc.hasDisplay()) {
786            c.doesDisplay = true;
787            c.display = cc.getDisplay();
788          }
789        }
790      }
791      if (c.system == null)
792        return null;
793      return c;   
794    } else if ("Coding".equals(fhirType())) {
795      ICodingImpl c = new ICodingImpl(true, true, true, true);
796      c.system = getNamedChildValue("system");
797      c.code = getNamedChildValue("code");
798      c.display = getNamedChildValue("display");
799      c.version = getNamedChildValue("version");
800      return c;
801    } else if ("Quantity".equals(fhirType())) {
802      ICodingImpl c = new ICodingImpl(true, true, false, false);
803      c.system = getNamedChildValue("system");
804      c.code = getNamedChildValue("code");
805      return c;
806    } else 
807      return null;
808  }
809
810  
811}