
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}