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