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