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