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