001package org.hl7.fhir.r5.utils;
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.Collections;
036import java.util.HashMap;
037import java.util.List;
038import java.util.Map;
039
040import org.hl7.fhir.exceptions.DefinitionException;
041import org.hl7.fhir.r5.context.IWorkerContext;
042import org.hl7.fhir.r5.model.Base;
043import org.hl7.fhir.r5.model.ElementDefinition;
044import org.hl7.fhir.r5.model.ElementDefinition.DiscriminatorType;
045import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingComponent;
046import org.hl7.fhir.r5.model.ElementDefinition.SlicingRules;
047import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
048import org.hl7.fhir.r5.model.Resource;
049import org.hl7.fhir.r5.model.StructureDefinition;
050import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
051import org.hl7.fhir.utilities.Utilities;
052import org.hl7.fhir.utilities.i18n.I18nConstants;
053
054@MarkedToMoveToAdjunctPackage
055public class DefinitionNavigator {
056
057  private IWorkerContext context;
058  private StructureDefinition structure;
059  private int index;
060  private boolean indexMatches; // 
061  private List<DefinitionNavigator> children;
062  private List<DefinitionNavigator> typeChildren;
063  private List<DefinitionNavigator> slices;
064  private List<String> names = new ArrayList<String>();
065  private TypeRefComponent typeOfChildren;
066  private String path;
067  private boolean diff;
068  private boolean followTypes;
069  private boolean inlineChildren;
070  private ElementDefinitionSlicingComponent manualSlice;
071  private TypeRefComponent manualType;
072  
073  public DefinitionNavigator(IWorkerContext context, StructureDefinition structure, boolean diff, boolean followTypes) throws DefinitionException {
074    if (!diff && !structure.hasSnapshot())
075      throw new DefinitionException("Snapshot required");
076    this.context = context;
077    this.structure = structure;
078    this.index = 0;
079    this.diff = diff;
080    this.followTypes = followTypes;
081    if (diff) {
082      this.path = structure.getType(); // fragile?
083      indexMatches = this.path.equals(list().get(0).getPath());
084    } else {
085      indexMatches = true;
086      this.path = current().getPath(); // first element
087    }
088    names.add(nameTail());
089  }
090  
091  private DefinitionNavigator(IWorkerContext context, StructureDefinition structure, boolean diff, boolean followTypes, int index, String path, List<String> names, String type) {
092    this.path = path;
093    this.context = context;
094    this.structure = structure;
095    this.diff = diff;
096    this.followTypes = followTypes;
097    this.index = index;
098    this.indexMatches = true;
099    if (type == null)
100      for (String name : names)
101        this.names.add(name+"."+nameTail());
102    else {
103      this.names.addAll(names);
104      this.names.add(type);
105    }
106  }
107  
108  /**
109   * Special case - changing the value of followTypes, for special use case
110   * @param other
111   * @param followTypes
112   */
113  public DefinitionNavigator(DefinitionNavigator other, boolean followTypes) {
114    this.context = other.context;
115    this.structure =  other.structure;
116    this.index =  other.index;
117    this.diff =  other.diff;
118    this.followTypes = followTypes;
119    this.path = other.path;
120    this.indexMatches = other.indexMatches;
121    this.typeOfChildren = other.typeOfChildren;
122    this.inlineChildren = other.inlineChildren;
123    this.manualSlice = other.manualSlice;
124    this.manualType = other.manualType;
125  }
126  
127  /**
128   * When you walk a tree, and you walk into a typed structure, an element can simultaineously 
129   * be covered by multiple types at once. Take, for example, the string label for an identifer value.
130   * It has the following paths:
131   *   Patient.identifier.value.value
132   *   Identifier.value.value
133   *   String.value
134   *   value
135   * If you started in a bundle, the list might be even longer and deeper
136   *   
137   * Any of these names might be relevant. This function returns the names in an ordered list
138   * in the order above  
139   * @return
140   */
141  public List<String> getNames() {
142    return names;
143  }
144  
145  private List<ElementDefinition> list() {
146    if (diff) {
147      return structure.getDifferential().getElement();      
148    } else {
149      return structure.getSnapshot().getElement();
150    }    
151  }
152  public ElementDefinition current() {
153   return indexMatches ? list().get(index) : null;      
154  }
155  
156  public List<DefinitionNavigator> slices() throws DefinitionException {
157    if (children == null) {
158      loadChildren();
159    }
160    // never return null: if no slices were found, give back an empty list
161    return slices != null ? slices : Collections.emptyList();
162  }
163  
164  public List<DefinitionNavigator> children() throws DefinitionException {
165    if (children == null) {
166      loadChildren();
167    }
168    return children;
169  }
170
171  private void loadChildren() throws DefinitionException {
172    children = new ArrayList<DefinitionNavigator>();
173    String prefix = path+".";
174    Map<String, DefinitionNavigator> nameMap = new HashMap<String, DefinitionNavigator>();
175
176    DefinitionNavigator last = null;
177    String polymorphicRoot = null;
178    DefinitionNavigator polymorphicDN = null;
179    for (int i = indexMatches ? index + 1 : index; i < list().size(); i++) {
180      String path = list().get(i).getPath();
181      if (path.startsWith(prefix)) {
182        if (!path.substring(prefix.length()).contains(".")) {
183          // immediate child
184          DefinitionNavigator dn = new DefinitionNavigator(context, structure, diff, followTypes, i, this.path+"."+tail(path), names, null);
185          last = dn;
186
187          if (nameMap.containsKey(path)) {
188            DefinitionNavigator master = nameMap.get(path);
189            ElementDefinition cm = master.current();
190            // Skip missing slicing error for extensions: they are implicitly sliced by url
191            if (!cm.hasSlicing()) {
192              String cmPath = cm.getPath();
193              boolean isExtension = cmPath.endsWith(".extension")
194                                 || cmPath.endsWith(".modifierExtension");
195              if (!isExtension) {
196                throw new DefinitionException("Found slices with no slicing details at " + dn.current().getPath());
197              }
198            }
199            if (master.slices == null) {
200              master.slices = new ArrayList<DefinitionNavigator>();
201            }
202            master.slices.add(dn);
203          
204          } else if (polymorphicRoot != null && path.startsWith(polymorphicRoot) && !path.substring(polymorphicRoot.length()).contains(".")) {
205            if (polymorphicDN.slices == null) {
206              polymorphicDN.slices = new ArrayList<DefinitionNavigator>();
207              polymorphicDN.manualSlice = new ElementDefinitionSlicingComponent();
208              polymorphicDN.manualSlice.setUserData(UserDataNames.DN_TRANSIENT, "true");
209              polymorphicDN.manualSlice.setRules(SlicingRules.CLOSED);
210              polymorphicDN.manualSlice.setOrdered(false);
211              polymorphicDN.manualSlice.addDiscriminator().setType(DiscriminatorType.TYPE).setPath("$this");
212            }
213            polymorphicDN.slices.add(dn);
214            if (!dn.current().hasType()) {
215              String t = path.substring(polymorphicRoot.length());
216              StructureDefinition sd = context.fetchTypeDefinition(t);
217              if (sd == null) {
218                sd = context.fetchTypeDefinition(Utilities.uncapitalize(t));
219              }
220              if (sd != null) {
221                dn.manualType = new TypeRefComponent(sd.getType());
222              }
223            }
224          } else if (dn.current().hasSliceName()) {
225            // this is an error unless we're dealing with extensions, which are auto-sliced (for legacy reasons)
226            if (diff && "extension".equals(dn.current().getName())) {
227              StructureDefinition vsd = new StructureDefinition(); // fake wrapper for placeholder element
228              vsd.getDifferential().getElement().add(makeExtensionDefinitionElement(path));
229              DefinitionNavigator master = new DefinitionNavigator(context, vsd, diff, followTypes, 0, this.path+"."+tail(path), names, null);
230              nameMap.put(path, master);
231              children.add(master);
232              master.slices = new ArrayList<DefinitionNavigator>();
233              master.slices.add(dn);
234            } else {
235              throw new DefinitionException(context.formatMessage(I18nConstants.DN_SLICE_NO_DEFINITION, path));
236            }
237          } else {
238            nameMap.put(path, dn);
239            children.add(dn);
240            if (diff && path.endsWith("[x]")) { 
241              // we're going to infer slicing if we need to
242              polymorphicRoot = path.substring(0, path.length() -3);
243              polymorphicDN = dn;
244            } else {
245              polymorphicRoot = null;
246              polymorphicDN = null;
247              
248            }
249          }
250        } else if (last == null || !path.startsWith(last.path)) {
251          // implied child
252          DefinitionNavigator dn = new DefinitionNavigator(context, structure, diff, followTypes, i, this.path+"."+tail(path), names, null);
253          nameMap.put(path, dn);
254          children.add(dn);
255        }
256      } else if (path.length() < prefix.length()) {
257        break;
258      }
259    }
260    inlineChildren = !children.isEmpty();
261    if (children.isEmpty() && followTypes) {
262      ElementDefinition ed = current();
263      if (ed.getType().size() != 1) {
264        // well, we can't walk into it 
265        return; // what to do?
266      }
267      TypeRefComponent tr = ed.getTypeFirstRep();
268      StructureDefinition sdt = null;
269      if (tr.getProfile().size() > 1) {
270        return;
271      } else if (tr.getProfile().size() == 1) {
272        sdt = context.fetchResource(StructureDefinition.class, tr.getProfile().get(0).asStringValue());        
273      } else {
274        sdt = context.fetchTypeDefinition(ed.getTypeFirstRep().getWorkingCode());
275      }
276      if (sdt == null) {
277        return;
278      }
279      List<ElementDefinition> list = diff ? sdt.getDifferential().getElement() : sdt.getSnapshot().getElement();
280      for (int i = 0; i < list.size(); i++) {
281        ElementDefinition edt = list.get(i);
282        if (Utilities.charCount(edt.getPath(), '.') == 1) {
283          DefinitionNavigator dn = new DefinitionNavigator(context, sdt, diff, followTypes, i, ed.getPath(), names, null);
284          children.add(dn);
285        }
286      }
287    }
288  }
289
290  private ElementDefinition makeExtensionDefinitionElement(String path) {
291    ElementDefinition ed = new ElementDefinition(path);
292    ed.setUserData(UserDataNames.DN_TRANSIENT, "true");
293    ed.getSlicing().setRules(SlicingRules.OPEN).setOrdered(false).addDiscriminator().setType(DiscriminatorType.VALUE).setPath("url");
294    return ed;
295  }
296
297  public String path() {
298    return path;
299  }
300  
301  private String tail(String p) {
302    if (p.contains("."))
303      return p.substring(p.lastIndexOf('.')+1);
304    else
305      return p;
306  }
307
308  public String nameTail() {
309    return tail(path);
310  }
311
312  /**
313   * if you have a typed element, the tree might end at that point.
314   * And you may or may not want to walk into the tree of that type
315   * It depends what you are doing. So this is a choice. You can 
316   * ask for the children, and then, if you get no children, you 
317   * can see if there are children defined for the type, and then 
318   * get them
319   * 
320   * you have to provide a type if there's more than one type 
321   * for current() since this library doesn't know how to choose
322   * @param res 
323   * @throws DefinitionException 
324   * @
325   */
326  public boolean hasTypeChildren(TypeRefComponent type, Resource res) throws DefinitionException {
327    if (typeChildren == null || typeOfChildren != type) {
328      loadTypedChildren(type, res);
329    }
330    return !typeChildren.isEmpty();
331  }
332
333  private void loadTypedChildren(TypeRefComponent type, Resource src) throws DefinitionException {
334    typeOfChildren = null;
335    StructureDefinition sd = context.fetchResource(StructureDefinition.class, /* GF#13465 : this somehow needs to be revisited type.hasProfile() ? type.getProfile() : */ type.getWorkingCode(), src);
336    if (sd != null) {
337      DefinitionNavigator dn = new DefinitionNavigator(context, sd, diff, followTypes, 0, path, names, sd.getType());
338      typeChildren = dn.children();
339    } else
340      throw new DefinitionException("Unable to find definition for "+type.getWorkingCode()+(type.hasProfile() ? "("+type.getProfile()+")" : ""));
341    typeOfChildren = type;
342  }
343
344  /**
345   * 
346   * @param res 
347   * @return
348   * @throws DefinitionException 
349   * @
350   */
351  public List<DefinitionNavigator> childrenFromType(TypeRefComponent type, Resource res) throws DefinitionException {
352    if (typeChildren == null || typeOfChildren != type) {
353      loadTypedChildren(type, res);
354    }
355    return typeChildren;
356  }
357
358  public StructureDefinition getStructure() {
359    return structure;
360  }
361
362  @Override
363  public String toString() {
364    return getId();
365  }
366
367  public String getId() {
368    return current() == null ? path : current().hasSliceName() ? current().getPath()+":"+current().getSliceName() : current().getPath();
369  }
370
371  public Base parent() {
372    // TODO Auto-generated method stub
373    return null;
374  }
375
376  public boolean sliced() {
377    if (manualSlice != null) {
378      return true;
379    } else if (current() == null) {
380      return false;
381    } else {
382      return current().hasSlicing();
383    }
384  }
385
386  public DefinitionNavigator childByPath(String path) {
387    for (DefinitionNavigator child : children()) {
388      if (child.path().equals(path)) {
389        return child;
390      }
391    }
392    return null;
393  }
394
395  public boolean hasChildren() {
396    return !children().isEmpty();
397  }
398
399  public boolean hasSlices() {
400     return sliced() && slices != null && !slices.isEmpty();
401  }
402
403  public boolean hasInlineChildren() {
404    if (children == null) {
405      loadChildren();
406    }
407    return inlineChildren;
408  }
409
410  public DefinitionNavigator childByName(String name) {
411    for (DefinitionNavigator child : children()) {
412      if (child.current().getName().equals(name)) {
413        return child;
414      }
415      if (child.current().getName().startsWith(name+"[x]")) {
416        return child;
417      }
418    }
419    return null;
420  }
421
422  public boolean isManualSliced() {
423    return manualSlice != null;
424  }
425
426  public ElementDefinitionSlicingComponent getSlicing() {
427    return manualSlice != null ? manualSlice : current().getSlicing();
428  }
429
430  public void setManualSliced(ElementDefinitionSlicingComponent manualSliced) {
431    this.manualSlice = manualSliced;
432  }
433
434  public TypeRefComponent getManualType() {
435    return manualType;
436  }
437  
438
439}