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 (isPrimitive() && (hash == "value".hashCode())) {
300      this.value = castToString(value).asStringValue();
301      return this;
302    }
303    if ("xhtml".equals(getType()) && (hash == "value".hashCode())) {
304      this.xhtml = castToXhtml(value);
305      this.value =  castToXhtmlString(value);
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    if (childForValue == null)
337      for (Property p : property.getChildProperties(this.name, type)) {
338        if (p.getName().equals(name) || p.getName().equals(name+"[x]")) {
339          Element ne = new Element(name, p);
340          children.add(ne);
341          childForValue = ne;
342          break;
343        }
344      }
345    
346    if (childForValue == null)
347      throw new Error("Cannot set property "+name+" on "+this.name);
348    else if (value.isPrimitive()) {
349      if (childForValue.property.getName().endsWith("[x]"))
350        childForValue.name = name+Utilities.capitalize(value.fhirType());
351      childForValue.setValue(value.primitiveValue());
352    } else {
353      Element ve = (Element) value;
354      childForValue.type = ve.getType();
355      if (childForValue.property.getName().endsWith("[x]"))
356        childForValue.name = name+Utilities.capitalize(childForValue.type);
357      else if (value.isResource()) {
358        if (childForValue.elementProperty == null)
359          childForValue.elementProperty = childForValue.property;
360        childForValue.property = ve.property;
361        childForValue.special = SpecialElement.BUNDLE_ENTRY;
362      }
363      if (ve.children != null) {
364        if (childForValue.children == null)
365          childForValue.children = new ArrayList<Element>();
366        else 
367          childForValue.children.clear();
368        childForValue.children.addAll(ve.children);
369      }
370    }
371    return childForValue;
372  }
373
374  private Base convertToElement(Property prop, Base v) throws FHIRException {
375    return new ObjectConverter(property.getContext()).convert(prop, (Type) v);
376  }
377
378  private boolean isDataType(Base v) {
379    return v instanceof Type &&  property.getContext().getTypeNames().contains(v.fhirType());
380  }
381
382  @Override
383  public Base makeProperty(int hash, String name) throws FHIRException {
384    if (isPrimitive() && (hash == "value".hashCode())) {
385      return new StringType(value);
386    }
387
388    if (children == null)
389      children = new ArrayList<Element>();
390    
391    // look through existing children
392    for (Element child : children) {
393      if (child.getName().equals(name)) {
394        if (!child.isList()) {
395          return child;
396        } else {
397          Element ne = new Element(child);
398          children.add(ne);
399          numberChildren();
400          return ne;
401        }
402      }
403    }
404
405    for (Property p : property.getChildProperties(this.name, type)) {
406      if (p.getName().equals(name)) {
407        Element ne = new Element(name, p);
408        children.add(ne);
409        return ne;
410      }
411    }
412      
413    throw new Error("Unrecognised name "+name+" on "+this.name); 
414  }
415  
416        private int maxToInt(String max) {
417    if (max.equals("*"))
418      return Integer.MAX_VALUE;
419    else
420      return Integer.parseInt(max);
421        }
422
423        @Override
424        public boolean isPrimitive() {
425                return type != null ? property.isPrimitive(type) : property.isPrimitive(property.getType(name));
426        }
427        
428  @Override
429  public boolean isBooleanPrimitive() {
430    return isPrimitive() && ("boolean".equals(type) || "boolean".equals(property.getType(name)));
431  }
432 
433  @Override
434  public boolean isResource() {
435    return property.isResource();
436  }
437  
438
439        @Override
440        public boolean hasPrimitiveValue() {
441                return property.isPrimitiveName(name) || property.IsLogicalAndHasPrimitiveValue(name);
442        }
443        
444
445        @Override
446        public String primitiveValue() {
447                if (isPrimitive())
448                  return value;
449                else {
450                        if (hasPrimitiveValue() && children != null) {
451                                for (Element c : children) {
452                                        if (c.getName().equals("value"))
453                                                return c.primitiveValue();
454                                }
455                        }
456                        return null;
457                }
458        }
459        
460        // for the validator
461  public int line() {
462    return line;
463  }
464
465  public int col() {
466    return col;
467  }
468
469        public Element markLocation(int line, int col) {
470                this.line = line;
471                this.col = col; 
472                return this;
473        }
474
475        public void clearDecorations() {
476          clearUserData("fhir.decorations");
477          for (Element e : children)
478            e.clearDecorations();         
479        }
480        
481        public void markValidation(StructureDefinition profile, ElementDefinition definition) {
482          @SuppressWarnings("unchecked")
483    List<ElementDecoration> decorations = (List<ElementDecoration>) getUserData("fhir.decorations");
484          if (decorations == null) {
485            decorations = new ArrayList<ElementDecoration>();
486            setUserData("fhir.decorations", decorations);
487          }
488          decorations.add(new ElementDecoration(DecorationType.TYPE, profile.getUserString("path"), definition.getPath()));
489          if (tail(definition.getId()).contains(":")) {
490            String[] details = tail(definition.getId()).split("\\:");
491            decorations.add(new ElementDecoration(DecorationType.SLICE, null, details[1]));
492          }
493        }
494        
495  private String tail(String id) {
496    return id.contains(".") ? id.substring(id.lastIndexOf(".")+1) : id;
497  }
498
499  public Element getNamedChild(String name) {
500          if (children == null)
501                return null;
502          Element result = null;
503          for (Element child : children) {
504                if (child.getName().equals(name)) {
505                        if (result == null)
506                                result = child;
507                        else 
508                                throw new Error("Attempt to read a single element when there is more than one present ("+name+")");
509                }
510          }
511          return result;
512        }
513
514  public void getNamedChildren(String name, List<Element> list) {
515        if (children != null)
516                for (Element child : children) 
517                        if (child.getName().equals(name))
518                                list.add(child);
519  }
520
521  public String getNamedChildValue(String name) {
522        Element child = getNamedChild(name);
523        return child == null ? null : child.value;
524  }
525
526  public void getNamedChildrenWithWildcard(String string, List<Element> values) {
527          Validate.isTrue(string.endsWith("[x]"));
528          
529          String start = string.substring(0, string.length() - 3);
530                if (children != null) {
531                        for (Element child : children) { 
532                                if (child.getName().startsWith(start)) {
533                                        values.add(child);
534                                }
535                        }
536                }
537  }
538
539  
540        public XhtmlNode getXhtml() {
541                return xhtml;
542        }
543
544        public Element setXhtml(XhtmlNode xhtml) {
545                this.xhtml = xhtml;
546                return this;
547        }
548
549        @Override
550        public boolean isEmpty() {
551                if (isNotBlank(value)) {
552                        return false;
553                }
554                for (Element next : getChildren()) {
555                        if (!next.isEmpty()) {
556                                return false;
557                        }
558                }
559                return true;
560        }
561
562  public Property getElementProperty() {
563    return elementProperty;
564  }
565
566  public boolean hasElementProperty() {
567    return elementProperty != null;
568  }
569
570  public boolean hasChild(String name) {
571    return getNamedChild(name) != null;
572  }
573
574  @Override
575  public String toString() {
576    return name+"="+fhirType() + "["+(children == null || hasValue() ? value : Integer.toString(children.size())+" children")+"]";
577  }
578
579  @Override
580  public String getIdBase() {
581    return getChildValue("id");
582  }
583
584  @Override
585  public void setIdBase(String value) {
586    setChildValue("id", value);
587  }
588
589
590  @Override
591  public boolean equalsDeep(Base other) {
592    if (!super.equalsDeep(other))
593      return false;
594    if (isPrimitive() && other.isPrimitive())
595      return primitiveValue().equals(other.primitiveValue());
596    if (isPrimitive() || other.isPrimitive())
597      return false;
598    Set<String> processed  = new HashSet<String>();
599    for (org.hl7.fhir.r4.model.Property p : children()) {
600      String name = p.getName();
601      processed.add(name);
602      org.hl7.fhir.r4.model.Property o = other.getChildByName(name);
603      if (!equalsDeep(p, o))
604        return false;
605    }
606    for (org.hl7.fhir.r4.model.Property p : children()) {
607      String name = p.getName();
608      if (!processed.contains(name)) {
609        org.hl7.fhir.r4.model.Property o = other.getChildByName(name);
610        if (!equalsDeep(p, o))
611          return false;
612      }
613    }
614    return true;
615  }
616
617  private boolean equalsDeep(org.hl7.fhir.r4.model.Property p, org.hl7.fhir.r4.model.Property o) {
618    if (o == null || p == null)
619      return false;
620    if (p.getValues().size() != o.getValues().size())
621      return false;
622    for (int i = 0; i < p.getValues().size(); i++)
623      if (!Base.compareDeep(p.getValues().get(i), o.getValues().get(i), true))
624        return false;
625    return true;
626  }
627
628  @Override
629  public boolean equalsShallow(Base other) {
630    if (!super.equalsShallow(other))
631      return false;
632    if (isPrimitive() && other.isPrimitive())
633      return primitiveValue().equals(other.primitiveValue());
634    if (isPrimitive() || other.isPrimitive())
635      return false;
636    return true; //?
637  }
638
639  public Type asType() throws FHIRException {
640    return new ObjectConverter(property.getContext()).convertToType(this);
641  }
642
643  @Override
644  public boolean isMetadataBased() {
645    return true;
646  }
647
648  public boolean isList() {
649    if (elementProperty != null)
650      return elementProperty.isList();
651    else
652      return property.isList();
653  }
654  
655  @Override
656  public String[] getTypesForProperty(int hash, String name) throws FHIRException {
657    Property p = property.getChildSimpleName(this.name, name);
658    if (p != null) {
659      Set<String> types = new HashSet<String>();
660      for (TypeRefComponent tr : p.getDefinition().getType()) {
661        types.add(tr.getCode());
662      }
663      return types.toArray(new String[]{});
664    }
665    return super.getTypesForProperty(hash, name);
666
667  }
668
669  public void sort() {
670    if (children != null) {
671      List<Element> remove = new ArrayList<Element>();
672      for (Element child : children) {
673        child.sort();
674        if (child.isEmpty())
675          remove.add(child);
676      }
677      children.removeAll(remove);
678      Collections.sort(children, new ElementSortComparator(this, this.property));
679    }
680  }
681
682  public class ElementSortComparator implements Comparator<Element> {
683    private List<ElementDefinition> children;
684    public ElementSortComparator(Element e, Property property) {
685      String tn = e.getType();
686      StructureDefinition sd = property.getContext().fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(tn, property.getContext().getOverrideVersionNs()));
687      if (sd != null && !sd.getAbstract())
688        children = sd.getSnapshot().getElement();
689      else
690        children = property.getStructure().getSnapshot().getElement();
691    }
692    
693    @Override
694    public int compare(Element e0, Element e1) {
695      int i0 = find(e0);
696      int i1 = find(e1);
697      return Integer.compare(i0, i1);
698    }
699    private int find(Element e0) {
700      int i =  e0.elementProperty != null ? children.indexOf(e0.elementProperty.getDefinition()) :  children.indexOf(e0.property.getDefinition());
701      return i; 
702    }
703
704  }
705
706  public class ICodingImpl implements ICoding {
707    private String system;
708    private String version;
709    private String code;
710    private String display;
711    private boolean doesSystem;
712    private boolean doesVersion;
713    private boolean doesCode;
714    private boolean doesDisplay;
715    public ICodingImpl(boolean doesCode, boolean doesSystem, boolean doesVersion, boolean doesDisplay) {
716      super();
717      this.doesCode = doesCode;
718      this.doesSystem = doesSystem;
719      this.doesVersion = doesVersion;
720      this.doesDisplay = doesDisplay;
721    }
722    public String getSystem() {
723      return system;
724    }
725    public String getVersion() {
726      return version;
727    }
728    public String getCode() {
729      return code;
730    }
731    public String getDisplay() {
732      return display;
733    }
734    public boolean hasSystem() {
735      return !Utilities.noString(system); 
736    }
737    public boolean hasVersion() {
738      return !Utilities.noString(version);
739    }
740    public boolean hasCode() {
741      return !Utilities.noString(code);
742    }
743    public boolean hasDisplay() {
744      return !Utilities.noString(display);
745    }
746    public boolean supportsSystem() {
747      return doesSystem;
748    }
749    public boolean supportsVersion() {
750      return doesVersion;
751    }
752    public boolean supportsCode() {
753      return doesCode;
754    }
755    public boolean supportsDisplay() {
756      return doesDisplay;
757    }    
758  }
759
760  public ICoding getAsICoding() throws FHIRException {
761    if ("code".equals(fhirType())) {
762      if (property.getDefinition().getBinding().getStrength() != BindingStrength.REQUIRED)
763        return null;
764      ICodingImpl c = new ICodingImpl(true, true, false, false);
765      c.code = primitiveValue();
766      ValueSetExpansionOutcome vse = property.getContext().expandVS(property.getDefinition().getBinding(), true, false);
767      if (vse.getValueset() == null)
768        return null;
769      for (ValueSetExpansionContainsComponent cc : vse.getValueset().getExpansion().getContains()) {
770        if (cc.getCode().equals(c.code)) {
771          c.system = cc.getSystem();
772          if (cc.hasVersion()) {
773            c.doesVersion = true;
774            c.version = cc.getVersion();
775          }
776          if (cc.hasDisplay()) {
777            c.doesDisplay = true;
778            c.display = cc.getDisplay();
779          }
780        }
781      }
782      if (c.system == null)
783        return null;
784      return c;   
785    } else if ("Coding".equals(fhirType())) {
786      ICodingImpl c = new ICodingImpl(true, true, true, true);
787      c.system = getNamedChildValue("system");
788      c.code = getNamedChildValue("code");
789      c.display = getNamedChildValue("display");
790      c.version = getNamedChildValue("version");
791      return c;
792    } else if ("Quantity".equals(fhirType())) {
793      ICodingImpl c = new ICodingImpl(true, true, false, false);
794      c.system = getNamedChildValue("system");
795      c.code = getNamedChildValue("code");
796      return c;
797    } else 
798      return null;
799  }
800
801  
802}