
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}