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