
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}