001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.dao; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.FhirVersionEnum; 024import ca.uhn.fhir.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 028import ca.uhn.fhir.jpa.api.dao.IDao; 029import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; 030import ca.uhn.fhir.jpa.entity.PartitionEntity; 031import ca.uhn.fhir.jpa.entity.ResourceSearchView; 032import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceServiceRegistry; 033import ca.uhn.fhir.jpa.esr.IExternallyStoredResourceService; 034import ca.uhn.fhir.jpa.model.config.PartitionSettings; 035import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 036import ca.uhn.fhir.jpa.model.entity.BaseTag; 037import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity; 038import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 039import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; 040import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 041import ca.uhn.fhir.jpa.model.entity.ResourceTable; 042import ca.uhn.fhir.jpa.model.entity.ResourceTag; 043import ca.uhn.fhir.jpa.model.entity.TagDefinition; 044import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 045import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; 046import ca.uhn.fhir.model.api.IResource; 047import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 048import ca.uhn.fhir.model.api.Tag; 049import ca.uhn.fhir.model.api.TagList; 050import ca.uhn.fhir.model.base.composite.BaseCodingDt; 051import ca.uhn.fhir.model.primitive.IdDt; 052import ca.uhn.fhir.model.primitive.InstantDt; 053import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; 054import ca.uhn.fhir.parser.DataFormatException; 055import ca.uhn.fhir.parser.IParser; 056import ca.uhn.fhir.parser.LenientErrorHandler; 057import ca.uhn.fhir.rest.api.Constants; 058import ca.uhn.fhir.util.IMetaTagSorter; 059import ca.uhn.fhir.util.MetaUtil; 060import jakarta.annotation.Nullable; 061import org.apache.commons.collections4.CollectionUtils; 062import org.apache.commons.lang3.Validate; 063import org.hl7.fhir.instance.model.api.IAnyResource; 064import org.hl7.fhir.instance.model.api.IBaseCoding; 065import org.hl7.fhir.instance.model.api.IBaseMetaType; 066import org.hl7.fhir.instance.model.api.IBaseResource; 067import org.hl7.fhir.instance.model.api.IIdType; 068import org.slf4j.Logger; 069import org.slf4j.LoggerFactory; 070import org.springframework.beans.factory.annotation.Autowired; 071 072import java.util.ArrayList; 073import java.util.Collection; 074import java.util.Collections; 075import java.util.Date; 076import java.util.List; 077 078import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.decodeResource; 079import static java.util.Objects.nonNull; 080import static org.apache.commons.lang3.StringUtils.isNotBlank; 081 082public class JpaStorageResourceParser implements IJpaStorageResourceParser { 083 public static final LenientErrorHandler LENIENT_ERROR_HANDLER = new LenientErrorHandler(false).disableAllErrors(); 084 private static final Logger ourLog = LoggerFactory.getLogger(JpaStorageResourceParser.class); 085 086 @Autowired 087 private FhirContext myFhirContext; 088 089 @Autowired 090 private JpaStorageSettings myStorageSettings; 091 092 @Autowired 093 private IResourceHistoryTableDao myResourceHistoryTableDao; 094 095 @Autowired 096 private PartitionSettings myPartitionSettings; 097 098 @Autowired 099 private IPartitionLookupSvc myPartitionLookupSvc; 100 101 @Autowired 102 private ExternallyStoredResourceServiceRegistry myExternallyStoredResourceServiceRegistry; 103 104 @Autowired 105 IMetaTagSorter myMetaTagSorter; 106 107 @Override 108 public IBaseResource toResource(IBasePersistedResource theEntity, boolean theForHistoryOperation) { 109 RuntimeResourceDefinition type = myFhirContext.getResourceDefinition(theEntity.getResourceType()); 110 Class<? extends IBaseResource> resourceType = type.getImplementingClass(); 111 return toResource(resourceType, (IBaseResourceEntity) theEntity, null, theForHistoryOperation); 112 } 113 114 @Override 115 public <R extends IBaseResource> R toResource( 116 Class<R> theResourceType, 117 IBaseResourceEntity theEntity, 118 Collection<ResourceTag> theTagList, 119 boolean theForHistoryOperation) { 120 121 // 1. get resource, it's encoding and the tags if any 122 byte[] resourceBytes; 123 String resourceText; 124 ResourceEncodingEnum resourceEncoding; 125 @Nullable Collection<? extends BaseTag> tagList = Collections.emptyList(); 126 long version; 127 String provenanceSourceUri = null; 128 String provenanceRequestId = null; 129 130 if (theEntity instanceof ResourceHistoryTable) { 131 ResourceHistoryTable history = (ResourceHistoryTable) theEntity; 132 resourceBytes = history.getResource(); 133 resourceText = history.getResourceTextVc(); 134 resourceEncoding = history.getEncoding(); 135 switch (myStorageSettings.getTagStorageMode()) { 136 case VERSIONED: 137 default: 138 if (history.isHasTags()) { 139 tagList = history.getTags(); 140 } 141 break; 142 case NON_VERSIONED: 143 if (history.getResourceTable().isHasTags()) { 144 tagList = history.getResourceTable().getTags(); 145 } 146 break; 147 case INLINE: 148 tagList = null; 149 } 150 version = history.getVersion(); 151 if (history.getProvenance() != null) { 152 provenanceRequestId = history.getProvenance().getRequestId(); 153 provenanceSourceUri = history.getProvenance().getSourceUri(); 154 } 155 } else if (theEntity instanceof ResourceTable) { 156 ResourceTable resource = (ResourceTable) theEntity; 157 ResourceHistoryTable history; 158 if (resource.getCurrentVersionEntity() != null) { 159 history = resource.getCurrentVersionEntity(); 160 } else { 161 version = theEntity.getVersion(); 162 history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theEntity.getId(), version); 163 ((ResourceTable) theEntity).setCurrentVersionEntity(history); 164 165 while (history == null) { 166 if (version > 1L) { 167 version--; 168 history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance( 169 theEntity.getId(), version); 170 } else { 171 return null; 172 } 173 } 174 } 175 176 resourceBytes = history.getResource(); 177 resourceEncoding = history.getEncoding(); 178 resourceText = history.getResourceTextVc(); 179 switch (myStorageSettings.getTagStorageMode()) { 180 case VERSIONED: 181 case NON_VERSIONED: 182 if (resource.isHasTags()) { 183 tagList = resource.getTags(); 184 } 185 break; 186 case INLINE: 187 tagList = null; 188 break; 189 } 190 version = history.getVersion(); 191 if (history.getProvenance() != null) { 192 provenanceRequestId = history.getProvenance().getRequestId(); 193 provenanceSourceUri = history.getProvenance().getSourceUri(); 194 } 195 } else if (theEntity instanceof ResourceSearchView) { 196 // This is the search View 197 ResourceSearchView view = (ResourceSearchView) theEntity; 198 resourceBytes = view.getResource(); 199 resourceText = view.getResourceTextVc(); 200 resourceEncoding = view.getEncoding(); 201 version = view.getVersion(); 202 provenanceRequestId = view.getProvenanceRequestId(); 203 provenanceSourceUri = view.getProvenanceSourceUri(); 204 switch (myStorageSettings.getTagStorageMode()) { 205 case VERSIONED: 206 case NON_VERSIONED: 207 if (theTagList != null) { 208 tagList = theTagList; 209 } 210 break; 211 case INLINE: 212 tagList = null; 213 break; 214 } 215 } else { 216 // something wrong 217 return null; 218 } 219 220 // 2. get The text 221 String decodedResourceText = decodedResourceText(resourceBytes, resourceText, resourceEncoding); 222 223 // 3. Use the appropriate custom type if one is specified in the context 224 Class<R> resourceType = determineTypeToParse(theResourceType, tagList); 225 226 // 4. parse the text to FHIR 227 R retVal = parseResource(theEntity, resourceEncoding, decodedResourceText, resourceType); 228 229 // 5. fill MetaData 230 retVal = populateResourceMetadata(theEntity, theForHistoryOperation, tagList, version, retVal); 231 232 // 6. Handle source (provenance) 233 MetaUtil.populateResourceSource(myFhirContext, provenanceSourceUri, provenanceRequestId, retVal); 234 235 // 7. Add partition information 236 populateResourcePartitionInformation(theEntity, retVal); 237 238 // 8. sort tags, security labels and profiles 239 myMetaTagSorter.sort(retVal.getMeta()); 240 241 return retVal; 242 } 243 244 private <R extends IBaseResource> void populateResourcePartitionInformation( 245 IBaseResourceEntity theEntity, R retVal) { 246 if (myPartitionSettings.isPartitioningEnabled()) { 247 PartitionablePartitionId partitionId = theEntity.getPartitionId(); 248 if (partitionId != null && partitionId.getPartitionId() != null) { 249 PartitionEntity persistedPartition = 250 myPartitionLookupSvc.getPartitionById(partitionId.getPartitionId()); 251 retVal.setUserData(Constants.RESOURCE_PARTITION_ID, persistedPartition.toRequestPartitionId()); 252 } else { 253 retVal.setUserData(Constants.RESOURCE_PARTITION_ID, RequestPartitionId.defaultPartition()); 254 } 255 } 256 } 257 258 @SuppressWarnings("unchecked") 259 private <R extends IBaseResource> R parseResource( 260 IBaseResourceEntity theEntity, 261 ResourceEncodingEnum theResourceEncoding, 262 String theDecodedResourceText, 263 Class<R> theResourceType) { 264 R retVal; 265 if (theResourceEncoding == ResourceEncodingEnum.ESR) { 266 267 int colonIndex = theDecodedResourceText.indexOf(':'); 268 Validate.isTrue(colonIndex > 0, "Invalid ESR address: %s", theDecodedResourceText); 269 String providerId = theDecodedResourceText.substring(0, colonIndex); 270 String address = theDecodedResourceText.substring(colonIndex + 1); 271 Validate.notBlank(providerId, "No provider ID in ESR address: %s", theDecodedResourceText); 272 Validate.notBlank(address, "No address in ESR address: %s", theDecodedResourceText); 273 IExternallyStoredResourceService provider = 274 myExternallyStoredResourceServiceRegistry.getProvider(providerId); 275 retVal = (R) provider.fetchResource(address); 276 277 } else if (theResourceEncoding != ResourceEncodingEnum.DEL) { 278 279 IParser parser = new TolerantJsonParser( 280 getContext(theEntity.getFhirVersion()), LENIENT_ERROR_HANDLER, theEntity.getId()); 281 282 try { 283 retVal = parser.parseResource(theResourceType, theDecodedResourceText); 284 } catch (Exception e) { 285 StringBuilder b = new StringBuilder(); 286 b.append("Failed to parse database resource["); 287 b.append(myFhirContext.getResourceType(theResourceType)); 288 b.append("/"); 289 b.append(theEntity.getIdDt().getIdPart()); 290 b.append(" (pid "); 291 b.append(theEntity.getId()); 292 b.append(", version "); 293 b.append(theEntity.getFhirVersion().name()); 294 b.append("): "); 295 b.append(e.getMessage()); 296 String msg = b.toString(); 297 ourLog.error(msg, e); 298 throw new DataFormatException(Msg.code(928) + msg, e); 299 } 300 301 } else { 302 303 retVal = (R) myFhirContext 304 .getResourceDefinition(theEntity.getResourceType()) 305 .newInstance(); 306 } 307 return retVal; 308 } 309 310 @SuppressWarnings("unchecked") 311 private <R extends IBaseResource> Class<R> determineTypeToParse( 312 Class<R> theResourceType, @Nullable Collection<? extends BaseTag> tagList) { 313 Class<R> resourceType = theResourceType; 314 if (tagList != null) { 315 if (myFhirContext.hasDefaultTypeForProfile()) { 316 for (BaseTag nextTag : tagList) { 317 if (nextTag.getTag().getTagType() == TagTypeEnum.PROFILE) { 318 String profile = nextTag.getTag().getCode(); 319 if (isNotBlank(profile)) { 320 Class<? extends IBaseResource> newType = myFhirContext.getDefaultTypeForProfile(profile); 321 if (newType != null && theResourceType.isAssignableFrom(newType)) { 322 ourLog.debug("Using custom type {} for profile: {}", newType.getName(), profile); 323 resourceType = (Class<R>) newType; 324 break; 325 } 326 } 327 } 328 } 329 } 330 } 331 return resourceType; 332 } 333 334 @SuppressWarnings("unchecked") 335 @Override 336 public <R extends IBaseResource> R populateResourceMetadata( 337 IBaseResourceEntity theEntitySource, 338 boolean theForHistoryOperation, 339 @Nullable Collection<? extends BaseTag> tagList, 340 long theVersion, 341 R theResourceTarget) { 342 if (theResourceTarget instanceof IResource) { 343 IResource res = (IResource) theResourceTarget; 344 theResourceTarget = 345 (R) populateResourceMetadataHapi(theEntitySource, tagList, theForHistoryOperation, res, theVersion); 346 } else { 347 IAnyResource res = (IAnyResource) theResourceTarget; 348 theResourceTarget = 349 populateResourceMetadataRi(theEntitySource, tagList, theForHistoryOperation, res, theVersion); 350 } 351 return theResourceTarget; 352 } 353 354 @SuppressWarnings("unchecked") 355 private <R extends IResource> R populateResourceMetadataHapi( 356 IBaseResourceEntity theEntity, 357 @Nullable Collection<? extends BaseTag> theTagList, 358 boolean theForHistoryOperation, 359 R res, 360 Long theVersion) { 361 R retVal = res; 362 if (theEntity.getDeleted() != null) { 363 res = (R) myFhirContext.getResourceDefinition(res).newInstance(); 364 retVal = res; 365 ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted())); 366 if (theForHistoryOperation) { 367 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE); 368 } 369 } else if (theForHistoryOperation) { 370 /* 371 * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT. 372 */ 373 Date published = theEntity.getPublished().getValue(); 374 Date updated = theEntity.getUpdated().getValue(); 375 if (published.equals(updated)) { 376 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST); 377 } else { 378 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT); 379 } 380 } 381 382 res.setId(theEntity.getIdDt().withVersion(theVersion.toString())); 383 384 ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion())); 385 ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished()); 386 ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated()); 387 IDao.RESOURCE_PID.put(res, theEntity.getResourceId()); 388 389 if (theTagList != null) { 390 if (theEntity.isHasTags()) { 391 TagList tagList = new TagList(); 392 List<IBaseCoding> securityLabels = new ArrayList<>(); 393 List<IdDt> profiles = new ArrayList<>(); 394 for (BaseTag next : theTagList) { 395 TagDefinition nextTag = next.getTag(); 396 switch (nextTag.getTagType()) { 397 case PROFILE: 398 profiles.add(new IdDt(nextTag.getCode())); 399 break; 400 case SECURITY_LABEL: 401 IBaseCoding secLabel = 402 (IBaseCoding) myFhirContext.getVersion().newCodingDt(); 403 secLabel.setSystem(nextTag.getSystem()); 404 secLabel.setCode(nextTag.getCode()); 405 secLabel.setDisplay(nextTag.getDisplay()); 406 secLabel.setVersion(nextTag.getVersion()); 407 Boolean userSelected = nextTag.getUserSelected(); 408 if (userSelected != null) { 409 secLabel.setUserSelected(userSelected); 410 } 411 securityLabels.add(secLabel); 412 break; 413 case TAG: 414 Tag e = new Tag(nextTag.getSystem(), nextTag.getCode(), nextTag.getDisplay()); 415 e.setVersion(nextTag.getVersion()); 416 // careful! These are Boolean, not boolean. 417 e.setUserSelectedBoolean(nextTag.getUserSelected()); 418 tagList.add(e); 419 break; 420 } 421 } 422 if (tagList.size() > 0) { 423 ResourceMetadataKeyEnum.TAG_LIST.put(res, tagList); 424 } 425 if (securityLabels.size() > 0) { 426 ResourceMetadataKeyEnum.SECURITY_LABELS.put(res, toBaseCodingList(securityLabels)); 427 } 428 if (profiles.size() > 0) { 429 ResourceMetadataKeyEnum.PROFILES.put(res, profiles); 430 } 431 } 432 } 433 434 return retVal; 435 } 436 437 @SuppressWarnings("unchecked") 438 private <R extends IBaseResource> R populateResourceMetadataRi( 439 IBaseResourceEntity theEntity, 440 @Nullable Collection<? extends BaseTag> theTagList, 441 boolean theForHistoryOperation, 442 IAnyResource res, 443 Long theVersion) { 444 R retVal = (R) res; 445 if (theEntity.getDeleted() != null) { 446 res = (IAnyResource) myFhirContext.getResourceDefinition(res).newInstance(); 447 retVal = (R) res; 448 ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted())); 449 if (theForHistoryOperation) { 450 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE); 451 } 452 } else if (theForHistoryOperation) { 453 /* 454 * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT. 455 */ 456 Date published = theEntity.getPublished().getValue(); 457 Date updated = theEntity.getUpdated().getValue(); 458 if (published.equals(updated)) { 459 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST); 460 } else { 461 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT); 462 } 463 } 464 465 res.getMeta().setLastUpdated(null); 466 res.getMeta().setVersionId(null); 467 468 updateResourceMetadata(theEntity, res); 469 res.setId(res.getIdElement().withVersion(theVersion.toString())); 470 471 res.getMeta().setLastUpdated(theEntity.getUpdatedDate()); 472 IDao.RESOURCE_PID.put(res, theEntity.getResourceId()); 473 474 if (CollectionUtils.isNotEmpty(theTagList)) { 475 res.getMeta().getTag().clear(); 476 res.getMeta().getProfile().clear(); 477 res.getMeta().getSecurity().clear(); 478 for (BaseTag next : theTagList) { 479 switch (next.getTag().getTagType()) { 480 case PROFILE: 481 res.getMeta().addProfile(next.getTag().getCode()); 482 break; 483 case SECURITY_LABEL: 484 IBaseCoding sec = res.getMeta().addSecurity(); 485 sec.setSystem(next.getTag().getSystem()); 486 sec.setCode(next.getTag().getCode()); 487 sec.setDisplay(next.getTag().getDisplay()); 488 break; 489 case TAG: 490 IBaseCoding tag = res.getMeta().addTag(); 491 tag.setSystem(next.getTag().getSystem()); 492 tag.setCode(next.getTag().getCode()); 493 tag.setDisplay(next.getTag().getDisplay()); 494 tag.setVersion(next.getTag().getVersion()); 495 Boolean userSelected = next.getTag().getUserSelected(); 496 // the tag is created with a null userSelected, but the api is primitive boolean. 497 // Only update if we are non-null. 498 if (nonNull(userSelected)) { 499 tag.setUserSelected(userSelected); 500 } 501 break; 502 } 503 } 504 } 505 506 return retVal; 507 } 508 509 @Override 510 public void updateResourceMetadata(IBaseResourceEntity theEntitySource, IBaseResource theResourceTarget) { 511 IIdType id = theEntitySource.getIdDt(); 512 if (myFhirContext.getVersion().getVersion().isRi()) { 513 id = myFhirContext.getVersion().newIdType().setValue(id.getValue()); 514 } 515 516 if (id.hasResourceType() == false) { 517 id = id.withResourceType(theEntitySource.getResourceType()); 518 } 519 520 theResourceTarget.setId(id); 521 if (theResourceTarget instanceof IResource) { 522 ResourceMetadataKeyEnum.VERSION.put((IResource) theResourceTarget, id.getVersionIdPart()); 523 ResourceMetadataKeyEnum.UPDATED.put((IResource) theResourceTarget, theEntitySource.getUpdated()); 524 } else { 525 IBaseMetaType meta = theResourceTarget.getMeta(); 526 meta.setVersionId(id.getVersionIdPart()); 527 meta.setLastUpdated(theEntitySource.getUpdatedDate()); 528 } 529 } 530 531 private FhirContext getContext(FhirVersionEnum theVersion) { 532 Validate.notNull(theVersion, "theVersion must not be null"); 533 if (theVersion == myFhirContext.getVersion().getVersion()) { 534 return myFhirContext; 535 } 536 return FhirContext.forCached(theVersion); 537 } 538 539 private static String decodedResourceText( 540 byte[] resourceBytes, String resourceText, ResourceEncodingEnum resourceEncoding) { 541 String decodedResourceText; 542 if (resourceText != null) { 543 decodedResourceText = resourceText; 544 } else { 545 decodedResourceText = decodeResource(resourceBytes, resourceEncoding); 546 } 547 return decodedResourceText; 548 } 549 550 private static List<BaseCodingDt> toBaseCodingList(List<IBaseCoding> theSecurityLabels) { 551 ArrayList<BaseCodingDt> retVal = new ArrayList<>(theSecurityLabels.size()); 552 for (IBaseCoding next : theSecurityLabels) { 553 retVal.add((BaseCodingDt) next); 554 } 555 return retVal; 556 } 557}