001package org.hl7.fhir.convertors.misc;
002
003import java.io.FileNotFoundException;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.HashSet;
007import java.util.List;
008import java.util.Set;
009
010import org.hl7.fhir.exceptions.DefinitionException;
011import org.hl7.fhir.exceptions.FHIRException;
012import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
013import org.hl7.fhir.r5.context.ContextUtilities;
014import org.hl7.fhir.r5.context.IWorkerContext;
015import org.hl7.fhir.r5.context.SimpleWorkerContext;
016import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
017import org.hl7.fhir.r5.extensions.ExtensionUtilities;
018import org.hl7.fhir.r5.model.CanonicalType;
019import org.hl7.fhir.r5.model.ElementDefinition;
020import org.hl7.fhir.r5.model.StructureDefinition;
021import org.hl7.fhir.r5.model.ElementDefinition.DiscriminatorType;
022import org.hl7.fhir.r5.model.ElementDefinition.SlicingRules;
023import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
024import org.hl7.fhir.r5.model.Enumerations.FHIRVersion;
025import org.hl7.fhir.r5.model.SearchParameter;
026import org.hl7.fhir.r5.model.StringType;
027import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionContextComponent;
028import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
029import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
030import org.hl7.fhir.r5.model.UriType;
031import org.hl7.fhir.utilities.StandardsStatus;
032import org.hl7.fhir.utilities.Utilities;
033import org.hl7.fhir.utilities.VersionUtilities;
034
035public class ProfileVersionAdaptor {
036  public enum ConversionMessageStatus {
037    ERROR, WARNING, NOTE
038  }
039
040  public static class ConversionMessage {
041    private String message;
042    private ConversionMessageStatus status;
043    public ConversionMessage(String message, ConversionMessageStatus status) {
044      super();
045      this.message = message;
046      this.status = status;
047    }
048    public String getMessage() {
049      return message;
050    }
051    public ConversionMessageStatus getStatus() {
052      return status;
053    } 
054  }
055
056  private IWorkerContext sCtxt;
057  private IWorkerContext tCtxt;
058  private ProfileUtilities tpu;
059  private ContextUtilities tcu;
060  private List<StructureDefinition> snapshotQueue = new ArrayList<>();
061
062  public ProfileVersionAdaptor(IWorkerContext sourceContext, IWorkerContext targetContext) {
063    super();
064    this.sCtxt = sourceContext;
065    this.tCtxt = targetContext;
066    if (VersionUtilities.versionMatches(sourceContext.getVersion(), targetContext.getVersion())) {
067      throw new DefinitionException("Cannot convert profile from "+sourceContext.getVersion()+" to "+targetContext.getVersion());
068    } else if (VersionUtilities.compareVersions(sourceContext.getVersion(), targetContext.getVersion()) < 1) {
069      throw new DefinitionException("Only converts backwards - cannot do "+sourceContext.getVersion()+" to "+targetContext.getVersion());
070    }
071    tcu = new ContextUtilities(tCtxt);
072    tpu = new ProfileUtilities(tCtxt, null, tcu);
073
074  }
075
076  public StructureDefinition convert(StructureDefinition sd, List<ConversionMessage> log) throws FileNotFoundException, IOException {
077    if (sd.getKind() == StructureDefinitionKind.LOGICAL) {
078      return convertLogical(sd, log);
079    }
080    if (sd.getDerivation() != TypeDerivationRule.CONSTRAINT || !"Extension".equals(sd.getType())) {
081      return null; // nothing to say right now
082    }
083    sd = sd.copy();
084    convertContext(sd, log);
085    if (sd.getContext().isEmpty()) {
086      log.clear();
087      log.add(new ConversionMessage("There are no valid contexts for this extension", ConversionMessageStatus.WARNING));
088      return null; // didn't convert successfully
089    }
090    sd.setFhirVersion(FHIRVersion.fromCode(tCtxt.getVersion()));
091    sd.setSnapshot(null);
092
093    // first pass, targetProfiles
094    for (ElementDefinition ed : sd.getDifferential().getElement()) {
095      for (TypeRefComponent td : ed.getType()) {
096        List<CanonicalType> toRemove = new ArrayList<CanonicalType>();
097        for (CanonicalType c : td.getTargetProfile()) {
098          String tp = getCorrectedProfile(c);
099          if (tp == null) {
100            log.add(new ConversionMessage("Remove the target profile " + c.getValue() + " from the element " + ed.getIdOrPath(), ConversionMessageStatus.WARNING));
101            toRemove.add(c);
102          } else if (!tp.equals(c.getValue())) {
103            log.add(new ConversionMessage("Change the target profile " + c.getValue() + " to " + tp + " on the element " + ed.getIdOrPath(), ConversionMessageStatus.WARNING));
104            c.setValue(tp);
105          }
106        }
107        td.getTargetProfile().removeAll(toRemove);
108      }
109    }
110    // second pass, unsupported primitive data types
111    for (ElementDefinition ed : sd.getDifferential().getElement()) {
112      for (TypeRefComponent tr : ed.getType()) {
113        String mappedDT = getMappedDT(tr.getCode());
114        if (mappedDT != null) {
115          log.add(new ConversionMessage("Map the type " + tr.getCode() + " to " + mappedDT + " on the element " + ed.getIdOrPath(), ConversionMessageStatus.WARNING));
116          tr.setCode(mappedDT);
117        }
118      }
119    }
120
121    // third pass, unsupported complex data types
122    ElementDefinition lastExt = null;
123    ElementDefinition group = null;
124    for (int i = 0; i < sd.getDifferential().getElement().size(); i++) {
125      ElementDefinition ed = sd.getDifferential().getElement().get(i);
126      if (ed.getPath().contains(".value")) {
127        if (ed.getType().size() > 1) {
128          if (ed.getType().removeIf(tr -> !tcu.isDatatype(tr.getWorkingCode()))) {
129            log.add(new ConversionMessage("Remove types from the element " + ed.getIdOrPath(), ConversionMessageStatus.WARNING));
130          }
131        } else if (ed.getType().size() == 1) {
132          TypeRefComponent tr = ed.getTypeFirstRep();
133          if (!tcu.isDatatype(tr.getWorkingCode()) || !isValidExtensionType(tr.getWorkingCode())) {
134            if (ed.hasBinding()) {
135              if (!"CodeableReference".equals(tr.getWorkingCode())) {
136                throw new DefinitionException("not handled: Unknown type " + tr.getWorkingCode() + " has a binding");
137              }
138            }
139            ed.getType().clear();
140            ed.setMin(0);
141            ed.setMax("0");
142            lastExt.setDefinition(ed.getDefinition());
143            lastExt.setShort(ed.getShort());
144            lastExt.setMax("*");
145            lastExt.getSlicing().setRules(SlicingRules.OPEN).setOrdered(false).addDiscriminator().setType(DiscriminatorType.VALUE).setPath("url");
146            StructureDefinition type = sCtxt.fetchTypeDefinition(tr.getCode());
147            if (type == null) {
148              throw new DefinitionException("unable to find definition for " + tr.getCode());
149            }
150            log.add(new ConversionMessage("Replace the type " + tr.getCode() + " with a set of extensions for the content of the type along with the _datatype extension", ConversionMessageStatus.WARNING));
151            int insPoint = sd.getDifferential().getElement().indexOf(lastExt);
152            int offset = 1;
153
154            // a slice extension for _datatype
155            offset = addDatatypeSlice(sd, offset, insPoint, lastExt, tr.getCode());
156
157            // now, a slice extension for each thing in the data type differential
158            for (ElementDefinition ted : type.getDifferential().getElement()) {
159              if (ted.getPath().contains(".")) { // skip the root
160                ElementDefinition base = lastExt;
161                int bo = 0;
162                int cc = Utilities.charCount(ted.getPath(), '.');
163                if (cc > 2) {
164                  throw new DefinitionException("type is deeper than 2?");
165                } else if (cc == 2) {
166                  base = group;
167                  bo = 2;
168                } else {
169                  // nothing
170                }
171                ElementDefinition ned = new ElementDefinition(base.getPath());
172                ned.setSliceName(ted.getName());
173                ned.setShort(ted.getShort());
174                ned.setDefinition(ted.getDefinition());
175                ned.setComment(ted.getComment());
176                ned.setMin(ted.getMin());
177                ned.setMax(ted.getMax());
178                offset = addDiffElement(sd, insPoint - bo, offset, ned);
179                // set the extensions to 0
180                ElementDefinition need = new ElementDefinition(base.getPath() + ".extension");
181                need.setMax("0");
182                offset = addDiffElement(sd, insPoint - bo, offset, need);
183                // fix the url 
184                ned = new ElementDefinition(base.getPath() + ".url");
185                ned.setFixed(new UriType(ted.getName()));
186                offset = addDiffElement(sd, insPoint - bo, offset, ned);
187                // set the value 
188                ned = new ElementDefinition(base.getPath() + ".value[x]");
189                ned.setMin(1);
190                offset = addDiffElement(sd, insPoint - bo, offset, ned);
191                if (ted.getType().size() == 1 && Utilities.existsInList(ted.getTypeFirstRep().getWorkingCode(), "Element", "BackboneElement")) {
192                  need.setMax("*");
193                  ned.setMin(0);
194                  ned.setMax("0");
195                  ned.getType().clear();
196                  group = need;
197                  group.getSlicing().setRules(SlicingRules.OPEN).setOrdered(false).addDiscriminator().setType(DiscriminatorType.VALUE).setPath("url");
198                } else {
199                  Set<String> types = new HashSet<>();
200                  for (TypeRefComponent ttr : ted.getType()) {
201                    TypeRefComponent ntr = checkTypeReference(ttr, types);
202                    if (ntr != null) {
203                      types.add(ntr.getWorkingCode());
204                      ned.addType(ntr);
205                    }
206                  }
207                  if (ned.getType().isEmpty()) {
208                    throw new DefinitionException("No types?");
209                  }
210                  if (ed.hasBinding() && "concept".equals(ted.getName())) {
211                    ned.setBinding(ed.getBinding());
212                  } else {
213                    ned.setBinding(ted.getBinding());
214                  }
215                }
216              }
217            }
218          }
219        }
220      }
221      if (ed.getPath().endsWith(".extension")) {
222        lastExt = ed;
223      }
224    }
225    if (!log.isEmpty()) {
226      if (!sd.hasExtension(ExtensionDefinitions.EXT_FMM_LEVEL) || ExtensionUtilities.readIntegerExtension(sd, ExtensionDefinitions.EXT_FMM_LEVEL, 0) > 2) {
227        ExtensionUtilities.setCodeExtension(sd, ExtensionDefinitions.EXT_FMM_LEVEL, "2");
228      }
229      StandardsStatus code = ExtensionUtilities.getStandardsStatus(sd);
230      if (code == StandardsStatus.TRIAL_USE) {
231        ExtensionUtilities.setCodeExtension(sd, ExtensionDefinitions.EXT_STANDARDS_STATUS, "draft");
232        ExtensionUtilities.setCodeExtension(sd, ExtensionDefinitions.EXT_STANDARDS_STATUS_REASON, "Extensions that have been modified for " + VersionUtilities.getNameForVersion(tCtxt.getVersion()) + " are still draft while real-world experience is collected");
233        log.add(new ConversionMessage("Note: Extensions that have been modified for " + VersionUtilities.getNameForVersion(tCtxt.getVersion()) + " are still draft while real-world experience is collected", ConversionMessageStatus.NOTE));
234      }
235    }
236    snapshotQueue.add(sd);
237    tCtxt.cacheResource(sd);
238    return sd;
239  }
240
241  public void generateSnapshots(List<ConversionMessage> log) {
242    for (StructureDefinition sd : snapshotQueue) {
243      StructureDefinition base = tCtxt.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
244      if (base == null) {
245        log.add(new ConversionMessage("Unable to create "+VersionUtilities.getNameForVersion(tCtxt.getVersion())+" version of "+sd.getVersionedUrl()+" because cannot find StructureDefinition for "+sd.getBaseDefinition()+" for version "+tCtxt.getVersion(), ConversionMessageStatus.WARNING));
246      } else {
247        tpu.generateSnapshot(base, sd, sd.getUrl(), "http://hl7.org/" + VersionUtilities.getNameForVersion(tCtxt.getVersion()) + "/", sd.getName());
248      }
249    }
250  }
251
252  private StructureDefinition convertLogical(StructureDefinition sdSrc, List<ConversionMessage> log) {
253    StructureDefinition sd = sdSrc.copy();
254    sd.setFhirVersion(FHIRVersion.fromCode(tCtxt.getVersion()));
255    sd.setSnapshot(null);
256
257    // first pass, targetProfiles
258    for (ElementDefinition ed : sd.getDifferential().getElement()) {
259      for (TypeRefComponent td : ed.getType()) {
260        List<CanonicalType> toRemove = new ArrayList<CanonicalType>();
261        for (CanonicalType c : td.getTargetProfile()) {
262          String tp = getCorrectedProfile(c);
263          if (tp == null) {
264            log.add(new ConversionMessage("Remove the target profile "+c.getValue()+" from the element "+ed.getIdOrPath(), ConversionMessageStatus.WARNING));
265            toRemove.add(c);
266          } else if (!tp.equals(c.getValue())) {
267            log.add(new ConversionMessage("Change the target profile "+c.getValue()+" to "+tp+" on the element "+ed.getIdOrPath(), ConversionMessageStatus.WARNING));
268            c.setValue(tp);
269          }
270        }
271        td.getTargetProfile().removeAll(toRemove);
272      }
273    }
274    // second pass, unsupported primitive data types
275    for (ElementDefinition ed : sd.getDifferential().getElement()) {
276      for (TypeRefComponent tr : ed.getType()) {
277        String mappedDT = getMappedDT(tr.getCode());
278        if (mappedDT != null) {
279          log.add(new ConversionMessage("Map the type "+tr.getCode()+" to "+mappedDT+" on the element "+ed.getIdOrPath(), ConversionMessageStatus.WARNING));
280          tr.setCode(mappedDT);
281        }
282      }
283    }
284
285    // third pass, unsupported complex data types
286    for (int i = 0; i < sd.getDifferential().getElement().size(); i++) {
287      ElementDefinition ed = sd.getDifferential().getElement().get(i);
288      if (ed.getType().size() > 1) {
289        if (ed.getType().removeIf(tr -> !tcu.isDatatype(tr.getWorkingCode()))) {
290          log.add(new ConversionMessage("Remove types from the element " + ed.getIdOrPath(), ConversionMessageStatus.WARNING));
291        }
292      } else if (ed.getType().size() == 1) {
293        TypeRefComponent tr = ed.getTypeFirstRep();
294        if (!tcu.isDatatype(tr.getWorkingCode()) && !isValidLogicalType(tr.getWorkingCode())) {
295          log.add(new ConversionMessage("Illegal type "+tr.getWorkingCode(), ConversionMessageStatus.ERROR));
296          return null;
297        }
298      }
299    }
300
301    if (!log.isEmpty()) {
302      if (!sd.hasExtension(ExtensionDefinitions.EXT_FMM_LEVEL) || ExtensionUtilities.readIntegerExtension(sd, ExtensionDefinitions.EXT_FMM_LEVEL, 0) > 2) {
303        ExtensionUtilities.setCodeExtension(sd, ExtensionDefinitions.EXT_FMM_LEVEL, "2");
304      }
305      ExtensionUtilities.setCodeExtension(sd, ExtensionDefinitions.EXT_STANDARDS_STATUS, "draft");
306      ExtensionUtilities.setCodeExtension(sd, ExtensionDefinitions.EXT_STANDARDS_STATUS_REASON, "Logical Models that have been modified for "+VersionUtilities.getNameForVersion(tCtxt.getVersion())+" are still draft while real-world experience is collected");
307      log.add(new ConversionMessage("Note: Logical Models that have been modified for "+VersionUtilities.getNameForVersion(tCtxt.getVersion())+" are still draft while real-world experience is collected", ConversionMessageStatus.NOTE));
308    }
309
310    StructureDefinition base = tCtxt.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
311    if (base == null) {
312      base = sCtxt.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
313    }
314    if (base == null) {
315      throw new FHIRException("Unable to find base for Logical Model from "+sd.getBaseDefinition());
316    }
317    snapshotQueue.add(sd);
318    tCtxt.cacheResource(sd);
319    return sd;
320  }
321
322  private boolean isValidLogicalType(String code) {
323    StructureDefinition sd = tCtxt.fetchTypeDefinition(code);
324    if (sd != null) {
325      return true;
326    }
327    sd = sCtxt.fetchTypeDefinition(code);
328    if (sd != null && !sd.getSourcePackage().isCore()) {
329      return true;
330    }
331    return false;
332  }
333
334  private int addDatatypeSlice(StructureDefinition sd, int offset, int insPoint, ElementDefinition base, String type) {
335    ElementDefinition ned = new ElementDefinition(base.getPath());
336    ned.setSliceName("_datatype");
337    ned.setShort("DataType name '"+type+"' from "+VersionUtilities.getNameForVersion(sCtxt.getVersion()));
338    ned.setDefinition(ned.getShort());
339    ned.setMin(1);
340    ned.setMax("1");
341    ned.addType().setCode("Extension").addProfile("http://hl7.org/fhir/StructureDefinition/_datatype");
342    offset = addDiffElement(sd, insPoint, offset, ned);
343    //    // set the extensions to 0
344    //    ElementDefinition need = new ElementDefinition(base.getPath()+".extension");
345    //    need.setMax("0");
346    //    offset = addDiffElement(sd, insPoint, offset, need);
347    //    // fix the url 
348    //    ned = new ElementDefinition(base.getPath()+".url");
349    //    ned.setFixed(new UriType("http://hl7.org/fhir/StructureDefinition/_datatype"));
350    //    offset = addDiffElement(sd, insPoint, offset, ned);
351    // set the value 
352    ned = new ElementDefinition(base.getPath()+".value[x]");
353    ned.setMin(1);
354    offset = addDiffElement(sd, insPoint, offset, ned);
355    ned.addType().setCode("string");
356    ned.setFixed(new StringType(type));
357    return offset;
358  }
359
360  private int addDiffElement(StructureDefinition sd, int insPoint, int offset, ElementDefinition ned) {
361    sd.getDifferential().getElement().add(insPoint+offset, ned);
362    offset++;
363    return offset;
364  }
365
366  private boolean isValidExtensionType(String type) {
367    StructureDefinition extDef = tCtxt.fetchTypeDefinition("Extension");
368    ElementDefinition ed = extDef.getSnapshot().getElementByPath("Extension.value");
369    for (TypeRefComponent tr : ed.getType()) {
370      if (type.equals(tr.getCode())) {
371        return true;
372      }
373    }
374    return false;
375  }
376
377  private TypeRefComponent checkTypeReference(TypeRefComponent tr, Set<String> types) {
378    String dt = getMappedDT(tr.getCode());
379    if (dt != null) {
380      if (types.contains(dt)) {
381        return null;
382      } else {
383        return tr.copy().setCode(dt);
384      }
385    } else if (tcu.isDatatype(tr.getWorkingCode())) {
386      return tr.copy();
387    } else {
388      return null;
389    }
390  }
391
392  private String getCorrectedProfile(CanonicalType c) {
393    StructureDefinition sd = tCtxt.fetchResource(StructureDefinition.class, c.getValue());
394    if (sd != null) {
395      return c.getValue();
396    }
397    // or it might be something defined in the IG or it's dependencies
398    sd = sCtxt.fetchResource(StructureDefinition.class, c.getValue());
399    if (sd != null && !sd.getSourcePackage().isCore()) {
400      return c.getValue();
401    }
402    return null;
403  }
404
405  private String getMappedDT(String code) {
406    if (VersionUtilities.isR5Plus(tCtxt.getVersion())) {
407      return code;
408    }
409    if (VersionUtilities.isR4Plus(tCtxt.getVersion())) {
410      switch (code) {
411      case "integer64" : return "version";
412      default:
413        return null;
414      }
415    }
416    if (VersionUtilities.isR3Ver(tCtxt.getVersion())) {
417      switch (code) {
418      case "integer64" : return "string";
419      case "canonical" : return "uri";
420      case "url" : return "uri";
421      default:
422        return null;
423      }
424    }
425    return null;
426  }
427
428  public void convertContext(StructureDefinition sd, List<ConversionMessage> log) {
429    List<StructureDefinitionContextComponent> toRemove = new ArrayList<>();
430    for (StructureDefinitionContextComponent ctxt : sd.getContext()) {
431      if (ctxt.getType() != null) {
432        switch (ctxt.getType()) {
433        case ELEMENT:
434          String newPath = adaptPath(ctxt.getExpression());
435          if (newPath == null) {
436            log.add(new ConversionMessage("Remove the extension context "+ctxt.getExpression(), ConversionMessageStatus.WARNING));
437            toRemove.add(ctxt);
438          } else if (!newPath.equals(ctxt.getExpression())) {
439            log.add(new ConversionMessage("Adjust the extension context "+ctxt.getExpression()+" to "+newPath, ConversionMessageStatus.WARNING));
440            ctxt.setExpression(newPath);
441          }
442          break;
443        case EXTENSION:
444          // nothing. for now
445          break;
446        case FHIRPATH:
447          // nothing. for now ?
448          break;
449        case NULL:
450          break;
451        default:
452          break;
453        }
454      }
455    }
456    sd.getContext().removeAll(toRemove);
457  }
458
459  /**
460   * WIP: change a context for an older version, or delete it (= return null)
461   *
462   * ToDo: Use the Cross-Version infrastructure to make this more intelligent
463   *
464   * @param path
465   * @return
466   */
467  private String adaptPath(String path) {
468    String base = path.contains(".") ? path.substring(0, path.indexOf(".")) : path;
469    StructureDefinition sd = tCtxt.fetchTypeDefinition(base);
470    if (sd == null) {
471      StructureDefinition ssd = sCtxt.fetchTypeDefinition(base);
472      if (ssd != null && ssd.getKind() == StructureDefinitionKind.RESOURCE) {
473        if ("CanonicalResource".equals(base)) {
474          return "CanonicalResource";
475        } else {
476          return "Basic";
477        }
478      } else if (ssd != null && ssd.getKind() == StructureDefinitionKind.COMPLEXTYPE) {
479        return null;
480      } else {
481        return null;
482      }
483    } else {
484      ElementDefinition ed = sd.getSnapshot().getElementByPath(base);
485      if (ed == null) {
486        return null;
487      } else {
488        return path;
489      }
490    }
491  }
492
493  public SearchParameter convert(SearchParameter resource, List<ConversionMessage> log) {
494    SearchParameter res = resource.copy();
495    // todo: translate resource types
496    res.getBase().removeIf(t -> { 
497      String rt = t.asStringValue();
498      boolean r = !tcu.isResource(rt);
499      if (r) {
500        log.add(new ConversionMessage("Remove search base "+rt, ConversionMessageStatus.WARNING));
501      }
502      return r;
503    }
504        );
505    return res;
506  }
507
508}