001package org.hl7.fhir.r5.conformance.profile;
002
003import java.util.ArrayList;
004import java.util.HashSet;
005import java.util.List;
006import java.util.Set;
007
008import org.hl7.fhir.exceptions.DefinitionException;
009import org.hl7.fhir.r5.conformance.profile.ProfileUtilities.SourcedChildDefinitions;
010import org.hl7.fhir.r5.context.IWorkerContext;
011import org.hl7.fhir.r5.model.ElementDefinition;
012import org.hl7.fhir.r5.model.ElementDefinition.DiscriminatorType;
013import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent;
014import org.hl7.fhir.r5.model.ElementDefinition.SlicingRules;
015import org.hl7.fhir.r5.model.StructureDefinition;
016import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionDifferentialComponent;
017import org.hl7.fhir.r5.utils.TypesUtilities;
018import org.hl7.fhir.r5.utils.UserDataNames;
019import org.hl7.fhir.utilities.Utilities;
020import org.hl7.fhir.utilities.i18n.I18nConstants;
021
022/**
023 * when a slice is encountered, it may have additional details defined after the slice that must be merged into 
024 * each of the slices. That's kind of multiple inheritance, and fiendishly complicated to add to the snapshot generator
025 * 
026 * This class pre-processes the differential, finding the slices that have these trailing properties, and 
027 * filling them out in the slices that follow
028 * 
029 * There's potential problems here, mostly around slicing extensions (other kind of slicing isn't allowed)
030 * and also the merging logic might need to be sophisticated.
031 * 
032 */
033public class SnapshotGenerationPreProcessor {
034
035
036  public class ElementAnalysis {
037    private StructureDefinition structure;
038    private ElementDefinition element;
039    private String type;
040    public SourcedChildDefinitions children;
041    protected ElementAnalysis(StructureDefinition structure, ElementDefinition element, String type) {
042      super();
043      this.structure = structure;
044      this.element = element;
045      this.type = type;
046    }
047    public StructureDefinition getStructure() {
048      return structure;
049    }
050    public ElementDefinition getElement() {
051      return element;
052    }
053    public SourcedChildDefinitions getChildren() {
054      return children;
055    }
056    public void setChildren(SourcedChildDefinitions children) {
057      this.children = children;
058    }
059    public String getType() {
060      return type;
061    }
062  }
063
064  public class SliceInfo {
065    SliceInfo parent;
066    String path;
067    boolean closed;
068    ElementDefinition slicer;
069    List<ElementDefinition> sliceStuff;
070    List<ElementDefinition> slices;
071
072    public SliceInfo(SliceInfo parent, ElementDefinition ed) {
073      this.parent = parent;
074      path = ed.getPath();
075      slicer = ed;
076      sliceStuff = new ArrayList<>();
077      if (parent != null) {
078        parent.add(ed);
079      }
080    }
081
082    public void newSlice(ElementDefinition ed) {
083      if (slices == null) {
084        slices = new ArrayList<ElementDefinition>();
085      }
086      slices.add(ed);
087      if (parent != null) {
088        parent.add(ed);
089      }
090    }
091    public void add(ElementDefinition ed) {
092      if (slices == null) {
093        sliceStuff.add(ed);
094      }      
095      if (parent != null) {
096        parent.add(ed);
097      }
098    }
099  }
100
101  private IWorkerContext context;
102  private ProfileUtilities utils;
103  Set<String> typeNames;
104  private List<SliceInfo> slicings = new ArrayList<>();
105
106  public SnapshotGenerationPreProcessor(ProfileUtilities utils) {
107    super();
108    this.utils = utils;
109    this.context = utils.getContext();
110  }
111
112  public void process(StructureDefinitionDifferentialComponent diff) {
113    // first pass, divide it up 
114    for (int cursor = 0; cursor < diff.getElement().size(); cursor++) {      
115      ElementDefinition ed = diff.getElement().get(cursor);
116
117      SliceInfo si = getSlicing(ed);
118      if (si == null) {
119        if (ed.hasSlicing() && !isExtensionSlicing(ed)) {
120          si = new SliceInfo(null, ed);
121          slicings.add(si);
122        } else {
123          // ignore this
124        }
125      } else {
126        if (ed.hasSliceName() && ed.getPath().equals(si.path)) {
127          si.newSlice(ed);
128        } else if (ed.hasSlicing() && !isExtensionSlicing(ed)) {
129          si = new SliceInfo(si, ed);
130          slicings.add(si);
131        } else {
132          si.add(ed);
133        }
134      }
135    }       
136
137    for (SliceInfo si : slicings) {
138      if (!si.sliceStuff.isEmpty() && si.slices != null) {
139        for (ElementDefinition ed : si.sliceStuff) {
140          if (ed.hasSlicing() && !isExtensionSlicing(ed)) {
141            String message = context.formatMessage(I18nConstants.UNSUPPORTED_SLICING_COMPLEXITY, si.slicer.getPath(), ed.getPath(), ed.getSlicing().summary());
142            System.out.println(message);
143            return;
144          }
145        }
146      }
147    }
148
149    // working backward
150    for (int i = slicings.size() - 1; i >= 0; i--) {
151      SliceInfo si = slicings.get(i);
152      if (!si.sliceStuff.isEmpty() && si.slices != null) {
153        // for each actual slice, we need to merge sliceStuff in
154        for (ElementDefinition slice : si.slices) {
155          mergeElements(diff.getElement(), si.sliceStuff, slice);
156        }
157      } else {
158        // we just ignore these - nothing to do
159      }
160    }
161
162  }
163
164  private void mergeElements(List<ElementDefinition> elements, List<ElementDefinition> allSlices, ElementDefinition slice) {
165    // we have
166    //   elements - the list of all the elements
167    //   allSlices which is the content defined for all the slices
168    //   slice -the anchor element for the slice
169
170    int sliceIndex = elements.indexOf(slice);
171    int startOfSlice = sliceIndex + 1;
172    int endOfSlice = findEndOfSlice(elements, slice);
173
174    Set<String> missing = new HashSet<>();
175    // the simple case is that all the stuff in allSlices exists between startOfSlice and endOfSlice
176    boolean allFound = true;
177    for (int i = 0; i < allSlices.size(); i++) {
178      boolean found = false;
179      for (int j = startOfSlice; j <= endOfSlice; j++) {
180        if (elements.get(j).getPath().equals(allSlices.get(i).getPath())) {
181          found = true;
182          break;
183        }
184      }
185      if (!found) {
186        missing.add(allSlices.get(i).getPath());
187        allFound = false;
188      }
189    }
190
191    if (allFound) {
192      // then we just merge it in
193      for (int j = startOfSlice; j <= endOfSlice; j++) {
194        for (int i = 0; i < allSlices.size(); i++) {
195          if (elements.get(j).getPath().equals(allSlices.get(i).getPath())) {
196            merge(elements.get(j), allSlices.get(i));
197          }
198        }
199      }
200    } else {
201      Set<ElementDefinition> handled = new HashSet<>();
202      
203      // merge the simple stuff
204      for (int j = startOfSlice; j <= endOfSlice; j++) {
205        for (int i = 0; i < allSlices.size(); i++) {
206          if (elements.get(j).getPath().equals(allSlices.get(i).getPath())) {
207            handled.add(allSlices.get(i));
208            merge(elements.get(j), allSlices.get(i));
209          }
210        }
211      }
212      
213      // we have a lot of work to do
214      // the challenge is that the things missing from startOfSlice..endOfSlice have to injected in the correct order 
215      // which means that we need to know the definitions
216      // and is extra tricky because we're sparse. so we just use the stated path
217      for (ElementDefinition ed : allSlices) {
218        if (!handled.contains(ed)) {
219          List<ElementAnalysis> edDef = analysePath(ed);
220          int index = determineInsertionPoint(elements, startOfSlice, endOfSlice, ed.getPath(), edDef);
221          ElementDefinition edc = ed.copy();
222          edc.setUserData(UserDataNames.SNAPSHOT_PREPROCESS_INJECTED, true);
223          edc.setId(null);
224          elements.add(index, edc);
225          endOfSlice++;
226        }
227      }
228    }    
229  }
230
231  private int determineInsertionPoint(List<ElementDefinition> elements, int startOfSlice, int endOfSlice, String path, List<ElementAnalysis> edDef) {
232    for (int i = startOfSlice; i <= endOfSlice; i++) {
233      // SNAPSHOT_PREPROCESS_INJECTED maintains the order in what is injected
234      if (!elements.get(i).hasUserData(UserDataNames.SNAPSHOT_PREPROCESS_INJECTED) && comesAfterThis(path, edDef, elements.get(i))) {
235        return i;
236      }
237    }
238    return endOfSlice+1;
239  }
240
241  private boolean comesAfterThis(String path, List<ElementAnalysis> edDef, ElementDefinition ed) {
242    String[] p1 = path.split("\\.");
243    String[] p2 = ed.getPath().split("\\.");
244    for (int i = 0; i < Integer.min(p1.length,  p2.length); i++) {
245      if (!p1[i].equals(p2[i])) {
246        ElementAnalysis sed = edDef.get(i-1);
247        int i1 = indexOfName(sed, p1[i]);
248        int i2 = indexOfName(sed, p2[i]);
249        return i1 < i2;
250      } else {
251        // well, we just go on
252      }
253    }
254    return p1.length < p2.length;
255  }
256
257  private int indexOfName(ElementAnalysis sed, String name) {
258    for (int i = 0; i < sed.getChildren().getList().size(); i++) {
259      if (name.equals(sed.getChildren().getList().get(i).getName())) {
260        return i;
261      }      
262    }
263    return -1;
264  }
265
266  private List<ElementAnalysis> analysePath(ElementDefinition ed) {
267    List<ElementAnalysis> res = new ArrayList<>();
268    for (String pn : ed.getPath().split("\\.")) {
269      if (res.isEmpty()) {
270        StructureDefinition sd = context.fetchTypeDefinition(pn);
271        if (sd == null) {
272          String message = context.formatMessage(I18nConstants.UNKNOWN_TYPE__AT_, pn, ed.getPath());
273          throw new DefinitionException(message);
274        }
275        res.add(new ElementAnalysis(sd, sd.getSnapshot().getElementFirstRep(), null));
276      } else {
277        ElementAnalysis sed = res.get(res.size()-1);
278        sed.setChildren(utils.getChildMap(sed.getStructure(), sed.getElement(), true, sed.getType()));
279        ElementDefinition t = null;
280        String type = null;
281        for (ElementDefinition child : sed.getChildren().getList()) {
282          if (pn.equals(child.getName())) {
283            t = child;
284            break;
285          }
286          if (child.getName().endsWith("[x]")) {
287            String rn = child.getName().substring(0, child.getName().length()-3);
288            if (pn.startsWith(rn)) {
289              t = child;
290              String tn = pn.substring(rn.length());
291              if (TypesUtilities.isPrimitive(Utilities.uncapitalize(tn))) {
292                type = Utilities.uncapitalize(tn);
293              } else {
294                type = tn;
295              }
296              break;
297            }
298          }
299        }
300        if (t == null) {
301          String message = context.formatMessage(I18nConstants.UNKNOWN_PROPERTY, pn, ed.getPath());
302          throw new DefinitionException("Unknown path "+pn+" in path "+ed.getPath());          
303        } else {
304          res.add(new ElementAnalysis(sed.getChildren().getSource(), t, type));
305        }
306      }
307    }
308    return res;
309  }
310
311  private int findEndOfSlice(List<ElementDefinition> elements, ElementDefinition slice) {
312    for (int i = elements.indexOf(slice); i < elements.size(); i++) {
313      ElementDefinition e = elements.get(i);
314      if (e.getPath().length() < slice.getPath().length() || (e.getPath().equals(slice.getPath()) && !slice.getSliceName().equals(e.getSliceName()))) {
315        return i-1;
316      }
317    }
318    return elements.size() - 1;
319  }
320
321  private void merge(ElementDefinition focus, ElementDefinition base) {
322    if (base.hasLabel() && !focus.hasLabel()) {
323      focus.setLabelElement(base.getLabelElement());
324    }    
325    if (base.hasCode() && !focus.hasCode()) {
326      focus.getCode().addAll(base.getCode());
327    }
328    if (base.hasShort() && !focus.hasShort()) {
329      focus.setShortElement(base.getShortElement());
330    }    
331    if (base.hasDefinition() && !focus.hasDefinition()) {
332      focus.setDefinitionElement(base.getDefinitionElement());
333    }    
334    if (base.hasComment() && !focus.hasComment()) {
335      focus.setCommentElement(base.getCommentElement());
336    }    
337    if (base.hasRequirements() && !focus.hasRequirements()) {
338      focus.setRequirementsElement(base.getRequirementsElement());
339    }    
340    if (base.hasAlias() && !focus.hasAlias()) {
341      focus.getAlias().addAll(base.getAlias());
342    }
343    if (base.hasMin() && !focus.hasMin()) {
344      focus.setMinElement(base.getMinElement());
345    }    
346    if (base.hasMax() && !focus.hasMax()) {
347      focus.setMaxElement(base.getMaxElement());
348    }    
349    if (base.hasType() && !focus.hasType()) {
350      focus.getType().addAll(base.getType());
351    }
352    if (base.hasDefaultValue() && !focus.hasDefaultValue()) {
353      focus.setDefaultValue(base.getDefaultValue());
354    }
355    if (base.hasMeaningWhenMissing() && !focus.hasMeaningWhenMissing()) {
356      focus.setMeaningWhenMissingElement(base.getMeaningWhenMissingElement());
357    }    
358    if (base.hasOrderMeaning() && !focus.hasOrderMeaning()) {
359      focus.setOrderMeaningElement(base.getOrderMeaningElement());
360    }    
361    if (base.hasFixed() && !focus.hasFixed()) {
362      focus.setFixed(base.getFixed());
363    }
364    if (base.hasPattern() && !focus.hasPattern()) {
365      focus.setPattern(base.getPattern());
366    }
367    if (base.hasExample() && !focus.hasExample()) {
368      focus.getExample().addAll(base.getExample());
369    }
370    if (base.hasMinValue() && !focus.hasMinValue()) {
371      focus.setMinValue(base.getMinValue());
372    }
373    if (base.hasMaxValue() && !focus.hasMaxValue()) {
374      focus.setMaxValue(base.getMaxValue());
375    }
376    if (base.hasMaxLength() && !focus.hasMaxLength()) {
377      focus.setMaxLengthElement(base.getMaxLengthElement());
378    }    
379    if (base.hasConstraint() && !focus.hasConstraint()) {
380      focus.getConstraint().addAll(base.getConstraint());
381    }
382    if (base.hasMustHaveValue() && !focus.hasMustHaveValue()) {
383      focus.setMustHaveValueElement(base.getMustHaveValueElement());
384    }    
385    if (base.hasValueAlternatives() && !focus.hasValueAlternatives()) {
386      focus.getValueAlternatives().addAll(base.getValueAlternatives());
387    }
388    if (base.hasMustSupport() && !focus.hasMustSupport()) {
389      focus.setMustSupportElement(base.getMustSupportElement());
390    }    
391    if (base.hasIsModifier() && !focus.hasIsModifier()) {
392      focus.setIsModifierElement(base.getIsModifierElement());
393    }    
394    if (base.hasIsModifierReason() && !focus.hasIsModifierReason()) {
395      focus.setIsModifierReasonElement(base.getIsModifierReasonElement());
396    }    
397    if (base.hasIsSummary() && !focus.hasIsSummary()) {
398      focus.setIsSummaryElement(base.getIsSummaryElement());
399    }    
400    if (base.hasBinding() && !focus.hasBinding()) {
401      focus.setBinding(base.getBinding());
402    }
403  }
404
405  private boolean isExtensionSlicing(ElementDefinition ed) {
406    if (!Utilities.existsInList(ed.getName(), "extension", "modiferExtension")) {
407      return false;
408    }
409    if (ed.getSlicing().getRules() != SlicingRules.OPEN || (!ed.getSlicing().hasOrdered() || ed.getSlicing().getOrdered()) || ed.getSlicing().getDiscriminator().size() != 1) {
410      return false;
411    }
412    ElementDefinitionSlicingDiscriminatorComponent d = ed.getSlicing().getDiscriminatorFirstRep();
413    return d.getType() == DiscriminatorType.VALUE && "url".equals(d.getPath());
414  }
415
416  private SliceInfo getSlicing(ElementDefinition ed) {
417    for (int i = slicings.size() - 1; i >= 0; i--) {
418      SliceInfo si = slicings.get(i);
419      if (!si.closed) {
420        if (si.path.length() > ed.getPath().length()) {
421          si.closed = true;
422        } else if (ed.getPath().startsWith(si.path)) {
423          return si;
424        }
425      }
426    }
427    return null;
428  }
429
430}