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