
001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 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.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.FhirVersionEnum; 027import ca.uhn.fhir.context.RuntimeChildResourceDefinition; 028import ca.uhn.fhir.context.RuntimeResourceDefinition; 029import ca.uhn.fhir.i18n.Msg; 030import ca.uhn.fhir.interceptor.api.HookParams; 031import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 032import ca.uhn.fhir.interceptor.api.Pointcut; 033import ca.uhn.fhir.interceptor.model.RequestPartitionId; 034import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 035import ca.uhn.fhir.jpa.api.dao.IDao; 036import ca.uhn.fhir.jpa.api.dao.IJpaDao; 037import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 038import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 039import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; 040import ca.uhn.fhir.jpa.cache.IResourceTypeCacheSvc; 041import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; 042import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; 043import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; 044import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; 045import ca.uhn.fhir.jpa.dao.expunge.ExpungeService; 046import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer; 047import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor; 048import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 049import ca.uhn.fhir.jpa.delete.DeleteConflictService; 050import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceAddress; 051import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceAddressMetadataKey; 052import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceServiceRegistry; 053import ca.uhn.fhir.jpa.model.config.PartitionSettings; 054import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 055import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 056import ca.uhn.fhir.jpa.model.dao.JpaPid; 057import ca.uhn.fhir.jpa.model.entity.BaseHasResource; 058import ca.uhn.fhir.jpa.model.entity.BaseTag; 059import ca.uhn.fhir.jpa.model.entity.EntityIndexStatusEnum; 060import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; 061import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 062import ca.uhn.fhir.jpa.model.entity.ResourceLink; 063import ca.uhn.fhir.jpa.model.entity.ResourceTable; 064import ca.uhn.fhir.jpa.model.entity.ResourceTag; 065import ca.uhn.fhir.jpa.model.entity.TagDefinition; 066import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 067import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData; 068import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 069import ca.uhn.fhir.jpa.model.util.JpaConstants; 070import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; 071import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper; 072import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 073import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; 074import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher; 075import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; 076import ca.uhn.fhir.jpa.term.api.ITermReadSvc; 077import ca.uhn.fhir.jpa.util.AddRemoveCount; 078import ca.uhn.fhir.jpa.util.QueryChunker; 079import ca.uhn.fhir.model.api.IResource; 080import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 081import ca.uhn.fhir.model.api.Tag; 082import ca.uhn.fhir.model.api.TagList; 083import ca.uhn.fhir.model.base.composite.BaseCodingDt; 084import ca.uhn.fhir.model.primitive.IdDt; 085import ca.uhn.fhir.parser.DataFormatException; 086import ca.uhn.fhir.rest.api.Constants; 087import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum; 088import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 089import ca.uhn.fhir.rest.api.server.RequestDetails; 090import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 091import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 092import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 093import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 094import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 095import ca.uhn.fhir.util.CoverageIgnore; 096import ca.uhn.fhir.util.HapiExtensions; 097import ca.uhn.fhir.util.MetaUtil; 098import ca.uhn.fhir.util.StopWatch; 099import ca.uhn.fhir.util.XmlUtil; 100import com.google.common.annotations.VisibleForTesting; 101import com.google.common.base.Charsets; 102import com.google.common.collect.Sets; 103import com.google.common.hash.HashCode; 104import jakarta.annotation.Nonnull; 105import jakarta.annotation.Nullable; 106import jakarta.annotation.PostConstruct; 107import jakarta.persistence.EntityManager; 108import jakarta.persistence.PersistenceContext; 109import jakarta.persistence.PersistenceContextType; 110import org.apache.commons.collections4.CollectionUtils; 111import org.apache.commons.lang3.NotImplementedException; 112import org.apache.commons.lang3.StringUtils; 113import org.apache.commons.lang3.Validate; 114import org.apache.commons.lang3.builder.EqualsBuilder; 115import org.hl7.fhir.instance.model.api.IAnyResource; 116import org.hl7.fhir.instance.model.api.IBase; 117import org.hl7.fhir.instance.model.api.IBaseCoding; 118import org.hl7.fhir.instance.model.api.IBaseExtension; 119import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 120import org.hl7.fhir.instance.model.api.IBaseMetaType; 121import org.hl7.fhir.instance.model.api.IBaseReference; 122import org.hl7.fhir.instance.model.api.IBaseResource; 123import org.hl7.fhir.instance.model.api.IDomainResource; 124import org.hl7.fhir.instance.model.api.IIdType; 125import org.hl7.fhir.instance.model.api.IPrimitiveType; 126import org.slf4j.Logger; 127import org.slf4j.LoggerFactory; 128import org.springframework.beans.BeansException; 129import org.springframework.beans.factory.annotation.Autowired; 130import org.springframework.context.ApplicationContext; 131import org.springframework.context.ApplicationContextAware; 132import org.springframework.stereotype.Repository; 133 134import java.util.ArrayList; 135import java.util.Collection; 136import java.util.Collections; 137import java.util.Date; 138import java.util.HashSet; 139import java.util.IdentityHashMap; 140import java.util.List; 141import java.util.Set; 142import java.util.StringTokenizer; 143import java.util.stream.Collectors; 144import javax.xml.stream.events.Characters; 145import javax.xml.stream.events.XMLEvent; 146 147import static java.util.Objects.nonNull; 148import static org.apache.commons.collections4.CollectionUtils.isEqualCollection; 149import static org.apache.commons.lang3.StringUtils.isBlank; 150import static org.apache.commons.lang3.StringUtils.isNotBlank; 151import static org.apache.commons.lang3.StringUtils.left; 152import static org.apache.commons.lang3.StringUtils.trim; 153 154/** 155 * TODO: JA - This class has only one subclass now. Historically it was a common 156 * ancestor for BaseHapiFhirSystemDao and BaseHapiFhirResourceDao but I've untangled 157 * the former from this hierarchy in order to simplify moving common functionality 158 * for resource DAOs into the hapi-fhir-storage project. This class should be merged 159 * into BaseHapiFhirResourceDao, but that should be done in its own dedicated PR 160 * since it'll be a noisy change. 161 */ 162@SuppressWarnings("WeakerAccess") 163@Repository 164public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStorageResourceDao<T> 165 implements IDao, IJpaDao<T>, ApplicationContextAware { 166 167 public static final String NS_JPA_PROFILE = "https://github.com/hapifhir/hapi-fhir/ns/jpa/profile"; 168 private static final Logger ourLog = LoggerFactory.getLogger(BaseHapiFhirDao.class); 169 private static boolean ourValidationDisabledForUnitTest; 170 private static boolean ourDisableIncrementOnUpdateForUnitTest = false; 171 172 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 173 protected EntityManager myEntityManager; 174 175 @Autowired 176 protected IIdHelperService<JpaPid> myIdHelperService; 177 178 @Autowired 179 protected ISearchCoordinatorSvc<JpaPid> mySearchCoordinatorSvc; 180 181 @Autowired 182 protected ITermReadSvc myTerminologySvc; 183 184 @Autowired 185 protected IResourceHistoryTableDao myResourceHistoryTableDao; 186 187 @Autowired 188 protected IResourceTableDao myResourceTableDao; 189 190 @Autowired 191 protected IResourceLinkDao myResourceLinkDao; 192 193 @Autowired 194 protected IResourceTagDao myResourceTagDao; 195 196 @Autowired 197 protected DeleteConflictService myDeleteConflictService; 198 199 @Autowired 200 protected IInterceptorBroadcaster myInterceptorBroadcaster; 201 202 @Autowired 203 protected InMemoryResourceMatcher myInMemoryResourceMatcher; 204 205 @Autowired 206 protected IJpaStorageResourceParser myJpaStorageResourceParser; 207 208 @Autowired 209 protected PartitionSettings myPartitionSettings; 210 211 @Autowired 212 ExpungeService myExpungeService; 213 214 @Autowired 215 private ExternallyStoredResourceServiceRegistry myExternallyStoredResourceServiceRegistry; 216 217 @Autowired 218 private ISearchParamPresenceSvc mySearchParamPresenceSvc; 219 220 @Autowired 221 private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor; 222 223 @Autowired 224 private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer; 225 226 private FhirContext myContext; 227 private ApplicationContext myApplicationContext; 228 229 @Autowired 230 private IPartitionLookupSvc myPartitionLookupSvc; 231 232 @Autowired(required = false) 233 private IFulltextSearchSvc myFulltextSearchSvc; 234 235 @Autowired 236 protected ResourceHistoryCalculator myResourceHistoryCalculator; 237 238 @Autowired 239 protected CacheTagDefinitionDao cacheTagDefinitionDao; 240 241 @Autowired 242 protected IResourceTypeCacheSvc myResourceTypeCacheSvc; 243 244 protected final CodingSpy myCodingSpy = new CodingSpy(); 245 246 @VisibleForTesting 247 public void setExternallyStoredResourceServiceRegistryForUnitTest( 248 ExternallyStoredResourceServiceRegistry theExternallyStoredResourceServiceRegistry) { 249 myExternallyStoredResourceServiceRegistry = theExternallyStoredResourceServiceRegistry; 250 } 251 252 @VisibleForTesting 253 public void setSearchParamPresenceSvc(ISearchParamPresenceSvc theSearchParamPresenceSvc) { 254 mySearchParamPresenceSvc = theSearchParamPresenceSvc; 255 } 256 257 @VisibleForTesting 258 public void setResourceHistoryCalculator(ResourceHistoryCalculator theResourceHistoryCalculator) { 259 myResourceHistoryCalculator = theResourceHistoryCalculator; 260 } 261 262 @Override 263 protected IInterceptorBroadcaster getInterceptorBroadcaster() { 264 return myInterceptorBroadcaster; 265 } 266 267 protected ApplicationContext getApplicationContext() { 268 return myApplicationContext; 269 } 270 271 @Override 272 public void setApplicationContext(@Nonnull ApplicationContext theApplicationContext) throws BeansException { 273 /* 274 * We do a null check here because Smile's module system tries to 275 * initialize the application context twice if two modules depend on 276 * the persistence module. The second time sets the dependency's appctx. 277 */ 278 if (myApplicationContext == null) { 279 myApplicationContext = theApplicationContext; 280 } 281 } 282 283 private void extractHapiTags( 284 TransactionDetails theTransactionDetails, 285 IResource theResource, 286 ResourceTable theEntity, 287 Set<ResourceTag> allDefs) { 288 TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource); 289 if (tagList != null) { 290 for (Tag next : tagList) { 291 TagDefinition def = cacheTagDefinitionDao.getTagOrNull( 292 theTransactionDetails, 293 TagTypeEnum.TAG, 294 next.getScheme(), 295 next.getTerm(), 296 next.getLabel(), 297 next.getVersion(), 298 myCodingSpy.getBooleanObject(next)); 299 if (def != null) { 300 ResourceTag tag = maybeAddTagToEntity(theEntity, def); 301 allDefs.add(tag); 302 theEntity.setHasTags(true); 303 } 304 } 305 } 306 307 List<BaseCodingDt> securityLabels = ResourceMetadataKeyEnum.SECURITY_LABELS.get(theResource); 308 if (securityLabels != null) { 309 for (BaseCodingDt next : securityLabels) { 310 TagDefinition def = cacheTagDefinitionDao.getTagOrNull( 311 theTransactionDetails, 312 TagTypeEnum.SECURITY_LABEL, 313 next.getSystemElement().getValue(), 314 next.getCodeElement().getValue(), 315 next.getDisplayElement().getValue(), 316 next.getVersionElement().getValue(), 317 next.getUserSelectedElement().getValue()); 318 if (def != null) { 319 ResourceTag tag = maybeAddTagToEntity(theEntity, def); 320 allDefs.add(tag); 321 theEntity.setHasTags(true); 322 } 323 } 324 } 325 326 List<IdDt> profiles = ResourceMetadataKeyEnum.PROFILES.get(theResource); 327 if (profiles != null) { 328 for (IIdType next : profiles) { 329 TagDefinition def = cacheTagDefinitionDao.getTagOrNull( 330 theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null, null, null); 331 if (def != null) { 332 ResourceTag tag = maybeAddTagToEntity(theEntity, def); 333 allDefs.add(tag); 334 theEntity.setHasTags(true); 335 } 336 } 337 } 338 } 339 340 private void extractRiTags( 341 TransactionDetails theTransactionDetails, 342 IAnyResource theResource, 343 ResourceTable theEntity, 344 Set<ResourceTag> theAllTags) { 345 List<? extends IBaseCoding> tagList = theResource.getMeta().getTag(); 346 if (tagList != null) { 347 for (IBaseCoding next : tagList) { 348 TagDefinition def = cacheTagDefinitionDao.getTagOrNull( 349 theTransactionDetails, 350 TagTypeEnum.TAG, 351 next.getSystem(), 352 next.getCode(), 353 next.getDisplay(), 354 next.getVersion(), 355 myCodingSpy.getBooleanObject(next)); 356 if (def != null) { 357 ResourceTag tag = maybeAddTagToEntity(theEntity, def); 358 theAllTags.add(tag); 359 theEntity.setHasTags(true); 360 } 361 } 362 } 363 364 List<? extends IBaseCoding> securityLabels = theResource.getMeta().getSecurity(); 365 if (securityLabels != null) { 366 for (IBaseCoding next : securityLabels) { 367 TagDefinition def = cacheTagDefinitionDao.getTagOrNull( 368 theTransactionDetails, 369 TagTypeEnum.SECURITY_LABEL, 370 next.getSystem(), 371 next.getCode(), 372 next.getDisplay(), 373 next.getVersion(), 374 myCodingSpy.getBooleanObject(next)); 375 if (def != null) { 376 ResourceTag tag = maybeAddTagToEntity(theEntity, def); 377 theAllTags.add(tag); 378 theEntity.setHasTags(true); 379 } 380 } 381 } 382 383 List<? extends IPrimitiveType<String>> profiles = theResource.getMeta().getProfile(); 384 if (profiles != null) { 385 for (IPrimitiveType<String> next : profiles) { 386 TagDefinition def = cacheTagDefinitionDao.getTagOrNull( 387 theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null, null, null); 388 if (def != null) { 389 ResourceTag tag = maybeAddTagToEntity(theEntity, def); 390 theAllTags.add(tag); 391 theEntity.setHasTags(true); 392 } 393 } 394 } 395 } 396 397 private void extractProfileTags( 398 TransactionDetails theTransactionDetails, 399 IBaseResource theResource, 400 ResourceTable theEntity, 401 Set<ResourceTag> theAllTags) { 402 RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource); 403 if (!def.isStandardType()) { 404 String profile = def.getResourceProfile(""); 405 if (isNotBlank(profile)) { 406 TagDefinition profileDef = cacheTagDefinitionDao.getTagOrNull( 407 theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, profile, null, null, null); 408 409 ResourceTag tag = maybeAddTagToEntity(theEntity, profileDef); 410 theAllTags.add(tag); 411 theEntity.setHasTags(true); 412 } 413 } 414 } 415 416 /** 417 * Utility method adding <code>theTagDefToAdd</code> to <code>theEntity</code>. Since the database may contain 418 * duplicate tagDefinitions, we perform a logical comparison, i.e. we don't care about the tagDefiniton.id, and add 419 * the tag to the entity only if the entity does not already have that tag. 420 * 421 * @param theEntity receiving the tagDefinition 422 * @param theTagDefToAdd to theEntity 423 * @return <code>theTagDefToAdd</code> wrapped in a resourceTag if it was added to <code>theEntity</code> or <code>theEntity</code>'s 424 * resourceTag encapsulating a tagDefinition that is logically equal to theTagDefToAdd. 425 */ 426 private ResourceTag maybeAddTagToEntity(ResourceTable theEntity, TagDefinition theTagDefToAdd) { 427 for (ResourceTag resourceTagFromEntity : theEntity.getTags()) { 428 TagDefinition tag = resourceTagFromEntity.getTag(); 429 EqualsBuilder equalsBuilder = new EqualsBuilder(); 430 equalsBuilder.append(tag.getSystem(), theTagDefToAdd.getSystem()); 431 equalsBuilder.append(tag.getCode(), theTagDefToAdd.getCode()); 432 equalsBuilder.append(tag.getDisplay(), theTagDefToAdd.getDisplay()); 433 equalsBuilder.append(tag.getVersion(), theTagDefToAdd.getVersion()); 434 equalsBuilder.append(tag.getUserSelected(), theTagDefToAdd.getUserSelected()); 435 equalsBuilder.append(tag.getTagType(), theTagDefToAdd.getTagType()); 436 437 if (equalsBuilder.isEquals()) { 438 return resourceTagFromEntity; 439 } 440 } 441 442 return theEntity.addTag(theTagDefToAdd); 443 } 444 445 private Set<ResourceTag> getAllTagDefinitions(ResourceTable theEntity) { 446 HashSet<ResourceTag> retVal = Sets.newHashSet(); 447 if (theEntity.isHasTags()) { 448 retVal.addAll(theEntity.getTags()); 449 } 450 return retVal; 451 } 452 453 @Override 454 public JpaStorageSettings getStorageSettings() { 455 return myStorageSettings; 456 } 457 458 @Override 459 public FhirContext getContext() { 460 return myContext; 461 } 462 463 @Autowired 464 public void setContext(FhirContext theContext) { 465 super.myFhirContext = theContext; 466 myContext = theContext; 467 } 468 469 void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) { 470 if (theResourceId == null || theResourceId.getVersionIdPart() == null) { 471 theSavedEntity.initializeVersion(); 472 } else { 473 theSavedEntity.markVersionUpdatedInCurrentTransaction(); 474 } 475 476 assert theResourceId != null; 477 String newVersion = Long.toString(theSavedEntity.getVersion()); 478 IIdType newId = theResourceId.withVersion(newVersion); 479 theResource.getIdElement().setValue(newId.getValue()); 480 } 481 482 public boolean isLogicalReference(IIdType theId) { 483 return LogicalReferenceHelper.isLogicalReference(myStorageSettings, theId); 484 } 485 486 /** 487 * Returns {@literal true} if the resource has changed (either the contents or the tags) 488 */ 489 protected EncodedResource populateResourceIntoEntity( 490 TransactionDetails theTransactionDetails, 491 RequestDetails theRequest, 492 IBaseResource theResource, 493 ResourceTable theEntity, 494 boolean thePerformIndexing) { 495 if (theEntity.getResourceType() == null) { 496 theEntity.setResourceType(toResourceName(theResource)); 497 } 498 if (theEntity.getResourceTypeId() == null && theResource != null) { 499 theEntity.setResourceTypeId(myResourceTypeCacheSvc.getResourceTypeId(toResourceName(theResource))); 500 } 501 502 byte[] resourceBinary; 503 String resourceText; 504 ResourceEncodingEnum encoding; 505 boolean changed = false; 506 507 if (theEntity.getDeleted() == null) { 508 509 if (thePerformIndexing) { 510 511 ExternallyStoredResourceAddress address = null; 512 if (myExternallyStoredResourceServiceRegistry.hasProviders()) { 513 address = ExternallyStoredResourceAddressMetadataKey.INSTANCE.get(theResource); 514 } 515 516 if (address != null) { 517 518 encoding = ResourceEncodingEnum.ESR; 519 resourceBinary = null; 520 resourceText = address.getProviderId() + ":" + address.getLocation(); 521 changed = true; 522 523 } else { 524 525 encoding = myStorageSettings.getResourceEncoding(); 526 527 String resourceType = theEntity.getResourceType(); 528 529 List<String> excludeElements = new ArrayList<>(8); 530 IBaseMetaType meta = theResource.getMeta(); 531 532 IBaseExtension<?, ?> sourceExtension = getExcludedElements(resourceType, excludeElements, meta); 533 534 theEntity.setFhirVersion(myContext.getVersion().getVersion()); 535 536 // TODO: LD: Once 2024-02 it out the door we should consider further refactoring here to move 537 // more of this logic within the calculator and eliminate more local variables 538 final ResourceHistoryState calculate = myResourceHistoryCalculator.calculateResourceHistoryState( 539 theResource, encoding, excludeElements); 540 541 resourceText = calculate.getResourceText(); 542 resourceBinary = calculate.getResourceBinary(); 543 encoding = calculate.getEncoding(); // This may be a no-op 544 final HashCode hashCode = calculate.getHashCode(); 545 546 String hashSha256 = hashCode.toString(); 547 if (!hashSha256.equals(theEntity.getHashSha256())) { 548 changed = true; 549 } 550 theEntity.setHashSha256(hashSha256); 551 552 if (sourceExtension != null) { 553 IBaseExtension<?, ?> newSourceExtension = ((IBaseHasExtensions) meta).addExtension(); 554 newSourceExtension.setUrl(sourceExtension.getUrl()); 555 newSourceExtension.setValue(sourceExtension.getValue()); 556 } 557 } 558 559 } else { 560 561 encoding = null; 562 resourceBinary = null; 563 resourceText = null; 564 } 565 566 boolean skipUpdatingTags = myStorageSettings.isMassIngestionMode() && theEntity.isHasTags(); 567 skipUpdatingTags |= myStorageSettings.getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE; 568 569 if (!skipUpdatingTags) { 570 changed |= updateTags(theTransactionDetails, theRequest, theResource, theEntity); 571 } 572 573 } else { 574 575 if (nonNull(theEntity.getHashSha256())) { 576 theEntity.setHashSha256(null); 577 changed = true; 578 } 579 580 resourceBinary = null; 581 resourceText = null; 582 encoding = ResourceEncodingEnum.DEL; 583 } 584 585 if (thePerformIndexing && !changed) { 586 if (theEntity.getId() == null) { 587 changed = true; 588 } else if (myStorageSettings.isMassIngestionMode()) { 589 590 // Don't check existing - We'll rely on the SHA256 hash only 591 592 } else if (theEntity.getVersion() == 1L && theEntity.getCurrentVersionEntity() == null) { 593 594 // No previous version if this is the first version 595 596 } else { 597 ResourceHistoryTable currentHistoryVersion = theEntity.getCurrentVersionEntity(); 598 if (currentHistoryVersion == null) { 599 currentHistoryVersion = myResourceHistoryTableDao.findForIdAndVersion( 600 theEntity.getId().toFk(), theEntity.getVersion()); 601 } 602 if (currentHistoryVersion == null || !currentHistoryVersion.hasResource()) { 603 changed = true; 604 } else { 605 // TODO: LD: Once 2024-02 it out the door we should consider further refactoring here to move 606 // more of this logic within the calculator and eliminate more local variables 607 changed = myResourceHistoryCalculator.isResourceHistoryChanged( 608 currentHistoryVersion, resourceBinary, resourceText); 609 } 610 } 611 } 612 613 EncodedResource retVal = new EncodedResource(); 614 retVal.setEncoding(encoding); 615 retVal.setResourceBinary(resourceBinary); 616 retVal.setResourceText(resourceText); 617 retVal.setChanged(changed); 618 619 return retVal; 620 } 621 622 /** 623 * helper to format the meta element for serialization of the resource. 624 * 625 * @param theResourceType the resource type of the resource 626 * @param theExcludeElements list of extensions in the meta element to exclude from serialization 627 * @param theMeta the meta element of the resource 628 * @return source extension if present in the meta element 629 */ 630 private IBaseExtension<?, ?> getExcludedElements( 631 String theResourceType, List<String> theExcludeElements, IBaseMetaType theMeta) { 632 boolean hasExtensions = false; 633 IBaseExtension<?, ?> sourceExtension = null; 634 if (theMeta instanceof IBaseHasExtensions) { 635 List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) theMeta).getExtension(); 636 if (!extensions.isEmpty()) { 637 hasExtensions = true; 638 639 /* 640 * FHIR DSTU3 did not have the Resource.meta.source field, so we use a 641 * custom HAPI FHIR extension in Resource.meta to store that field. However, 642 * we put the value for that field in a separate table, so we don't want to serialize 643 * it into the stored BLOB. Therefore: remove it from the resource temporarily 644 * and restore it afterward. 645 */ 646 if (myFhirContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) { 647 for (int i = 0; i < extensions.size(); i++) { 648 if (extensions.get(i).getUrl().equals(HapiExtensions.EXT_META_SOURCE)) { 649 sourceExtension = extensions.remove(i); 650 i--; 651 } 652 } 653 } 654 boolean allExtensionsRemoved = extensions.isEmpty(); 655 if (allExtensionsRemoved) { 656 hasExtensions = false; 657 } 658 } 659 } 660 661 theExcludeElements.add("id"); 662 boolean inlineTagMode = 663 getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE; 664 if (hasExtensions || inlineTagMode) { 665 if (!inlineTagMode) { 666 theExcludeElements.add(theResourceType + ".meta.profile"); 667 theExcludeElements.add(theResourceType + ".meta.tag"); 668 theExcludeElements.add(theResourceType + ".meta.security"); 669 } 670 theExcludeElements.add(theResourceType + ".meta.versionId"); 671 theExcludeElements.add(theResourceType + ".meta.lastUpdated"); 672 theExcludeElements.add(theResourceType + ".meta.source"); 673 } else { 674 /* 675 * If there are no extensions in the meta element, we can just exclude the 676 * whole meta element, which avoids adding an empty "meta":{} 677 * from showing up in the serialized JSON. 678 */ 679 theExcludeElements.add(theResourceType + ".meta"); 680 } 681 return sourceExtension; 682 } 683 684 protected boolean updateTags( 685 TransactionDetails theTransactionDetails, 686 RequestDetails theRequest, 687 IBaseResource theResource, 688 ResourceTable theEntity) { 689 Set<ResourceTag> allResourceTagsFromTheResource = new HashSet<>(); 690 Set<ResourceTag> allOriginalResourceTagsFromTheEntity = getAllTagDefinitions(theEntity); 691 692 if (theResource instanceof IResource) { 693 extractHapiTags(theTransactionDetails, (IResource) theResource, theEntity, allResourceTagsFromTheResource); 694 } else { 695 extractRiTags(theTransactionDetails, (IAnyResource) theResource, theEntity, allResourceTagsFromTheResource); 696 } 697 698 extractProfileTags(theTransactionDetails, theResource, theEntity, allResourceTagsFromTheResource); 699 700 // the extract[Hapi|Ri|Profile]Tags methods above will have populated the allResourceTagsFromTheResource Set 701 // AND 702 // added all tags from theResource.meta.tags to theEntity.meta.tags. the next steps are to: 703 // 1- remove duplicates; 704 // 2- remove tags from theEntity that are not present in theResource if header HEADER_META_SNAPSHOT_MODE 705 // is present in the request; 706 // 707 Set<ResourceTag> allResourceTagsNewAndOldFromTheEntity = getAllTagDefinitions(theEntity); 708 Set<TagDefinition> allTagDefinitionsPresent = new HashSet<>(); 709 710 allResourceTagsNewAndOldFromTheEntity.forEach(tag -> { 711 712 // Don't keep duplicate tags or tags with a missing definition 713 TagDefinition tagDefinition = tag.getTag(); 714 if (tagDefinition == null || !allTagDefinitionsPresent.add(tagDefinition)) { 715 theEntity.getTags().remove(tag); 716 } 717 718 // Drop any tags that have been removed 719 if (tagDefinition != null && !allResourceTagsFromTheResource.contains(tag)) { 720 if (shouldDroppedTagBeRemovedOnUpdate(theRequest, tag)) { 721 theEntity.getTags().remove(tag); 722 } else if (HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY.equals(tagDefinition.getSystem())) { 723 theEntity.getTags().remove(tag); 724 } 725 } 726 }); 727 728 // at this point, theEntity.meta.tags will be up to date: 729 // 1- it was stripped from tags that needed removing; 730 // 2- it has new tags from a resource update through theResource; 731 // 3- it has tags from the previous version; 732 // 733 // Since tags are merged on updates, we add tags from theEntity that theResource does not have 734 Set<ResourceTag> allUpdatedResourceTagsNewAndOldMinusRemovalsFromTheEntity = getAllTagDefinitions(theEntity); 735 736 allUpdatedResourceTagsNewAndOldMinusRemovalsFromTheEntity.forEach(aResourcetag -> { 737 if (!allResourceTagsFromTheResource.contains(aResourcetag)) { 738 IBaseCoding iBaseCoding = theResource 739 .getMeta() 740 .addTag() 741 .setCode(aResourcetag.getTag().getCode()) 742 .setSystem(aResourcetag.getTag().getSystem()) 743 .setVersion(aResourcetag.getTag().getVersion()); 744 745 allResourceTagsFromTheResource.add(aResourcetag); 746 747 if (aResourcetag.getTag().getUserSelected() != null) { 748 iBaseCoding.setUserSelected(aResourcetag.getTag().getUserSelected()); 749 } 750 } 751 }); 752 753 theEntity.setHasTags(!allUpdatedResourceTagsNewAndOldMinusRemovalsFromTheEntity.isEmpty()); 754 return !isEqualCollection(allOriginalResourceTagsFromTheEntity, allResourceTagsFromTheResource); 755 } 756 757 /** 758 * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database 759 * 760 * @param theEntity The resource 761 */ 762 protected void postDelete(ResourceTable theEntity) { 763 // nothing 764 } 765 766 /** 767 * Subclasses may override to provide behaviour. Called when a resource has been inserted into the database for the first time. 768 * 769 * @param theEntity The entity being updated (Do not modify the entity! Undefined behaviour will occur!) 770 * @param theResource The resource being persisted 771 * @param theRequestDetails The request details, needed for partition support 772 */ 773 protected void postPersist(ResourceTable theEntity, T theResource, RequestDetails theRequestDetails) { 774 // nothing 775 } 776 777 /** 778 * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database 779 * 780 * @param theEntity The resource 781 * @param theResource The resource being persisted 782 * @param theRequestDetails The request details, needed for partition support 783 */ 784 protected void postUpdate(ResourceTable theEntity, T theResource, RequestDetails theRequestDetails) { 785 // nothing 786 } 787 788 @Override 789 @CoverageIgnore 790 public BaseHasResource readEntity(IIdType theValueId, RequestDetails theRequest) { 791 throw new NotImplementedException(Msg.code(927)); 792 } 793 794 /** 795 * This method is called when an update to an existing resource detects that the resource supplied for update is missing a tag/profile/security label that the currently persisted resource holds. 796 * <p> 797 * The default implementation removes any profile declarations, but leaves tags and security labels in place. Subclasses may choose to override and change this behaviour. 798 * </p> 799 * <p> 800 * See <a href="http://hl7.org/fhir/resource.html#tag-updates">Updates to Tags, Profiles, and Security Labels</a> for a description of the logic that the default behaviour follows. 801 * </p> 802 * 803 * @param theTag The tag 804 * @return Returns <code>true</code> if the tag should be removed 805 */ 806 protected boolean shouldDroppedTagBeRemovedOnUpdate(RequestDetails theRequest, ResourceTag theTag) { 807 if (theTag.getTag() == null) { 808 return true; 809 } 810 811 Set<TagTypeEnum> metaSnapshotModeTokens = null; 812 813 if (theRequest != null) { 814 List<String> metaSnapshotMode = theRequest.getHeaders(JpaConstants.HEADER_META_SNAPSHOT_MODE); 815 if (metaSnapshotMode != null && !metaSnapshotMode.isEmpty()) { 816 metaSnapshotModeTokens = new HashSet<>(); 817 for (String nextHeaderValue : metaSnapshotMode) { 818 StringTokenizer tok = new StringTokenizer(nextHeaderValue, ","); 819 while (tok.hasMoreTokens()) { 820 switch (trim(tok.nextToken())) { 821 case "TAG": 822 metaSnapshotModeTokens.add(TagTypeEnum.TAG); 823 break; 824 case "PROFILE": 825 metaSnapshotModeTokens.add(TagTypeEnum.PROFILE); 826 break; 827 case "SECURITY_LABEL": 828 metaSnapshotModeTokens.add(TagTypeEnum.SECURITY_LABEL); 829 break; 830 } 831 } 832 } 833 } 834 } 835 836 if (metaSnapshotModeTokens == null) { 837 metaSnapshotModeTokens = Collections.singleton(TagTypeEnum.PROFILE); 838 } 839 840 return metaSnapshotModeTokens.contains(theTag.getTag().getTagType()); 841 } 842 843 String toResourceName(IBaseResource theResource) { 844 return myContext.getResourceType(theResource); 845 } 846 847 @VisibleForTesting 848 public void setEntityManager(EntityManager theEntityManager) { 849 myEntityManager = theEntityManager; 850 } 851 852 @VisibleForTesting 853 public void setSearchParamWithInlineReferencesExtractor( 854 SearchParamWithInlineReferencesExtractor theSearchParamWithInlineReferencesExtractor) { 855 mySearchParamWithInlineReferencesExtractor = theSearchParamWithInlineReferencesExtractor; 856 } 857 858 @VisibleForTesting 859 public void setResourceHistoryTableDao(IResourceHistoryTableDao theResourceHistoryTableDao) { 860 myResourceHistoryTableDao = theResourceHistoryTableDao; 861 } 862 863 @VisibleForTesting 864 public void setDaoSearchParamSynchronizer(DaoSearchParamSynchronizer theDaoSearchParamSynchronizer) { 865 myDaoSearchParamSynchronizer = theDaoSearchParamSynchronizer; 866 } 867 868 private void verifyMatchUrlForConditionalCreateOrUpdate( 869 CreateOrUpdateByMatch theCreateOrUpdate, 870 IBaseResource theResource, 871 String theIfNoneExist, 872 ResourceIndexedSearchParams theParams, 873 RequestDetails theRequestDetails) { 874 // Make sure that the match URL was actually appropriate for the supplied resource 875 InMemoryMatchResult outcome = 876 myInMemoryResourceMatcher.match(theIfNoneExist, theResource, theParams, theRequestDetails); 877 878 if (outcome.supported() && !outcome.matched()) { 879 String errorMsg = getConditionalCreateOrUpdateErrorMsg(theCreateOrUpdate); 880 throw new InvalidRequestException(Msg.code(929) + errorMsg); 881 } 882 } 883 884 private String getConditionalCreateOrUpdateErrorMsg(CreateOrUpdateByMatch theCreateOrUpdate) { 885 return String.format( 886 "Failed to process conditional %s. " + "The supplied resource did not satisfy the conditional URL.", 887 theCreateOrUpdate.name().toLowerCase()); 888 } 889 890 @SuppressWarnings("unchecked") 891 @Override 892 public ResourceTable updateEntity( 893 RequestDetails theRequest, 894 final IBaseResource theResource, 895 IBasePersistedResource theEntity, 896 Date theDeletedTimestampOrNull, 897 boolean thePerformIndexing, 898 boolean theUpdateVersion, 899 TransactionDetails theTransactionDetails, 900 boolean theForceUpdate, 901 boolean theCreateNewHistoryEntry) { 902 Validate.notNull(theEntity); 903 Validate.isTrue( 904 theDeletedTimestampOrNull != null || theResource != null, 905 "Must have either a resource[%s] or a deleted timestamp[%s] for resource PID[%s]", 906 theDeletedTimestampOrNull != null, 907 theResource != null, 908 theEntity.getPersistentId()); 909 910 ourLog.debug("Starting entity update"); 911 912 ResourceTable entity = (ResourceTable) theEntity; 913 914 /* 915 * This should be the very first thing.. 916 */ 917 if (theResource != null) { 918 if (thePerformIndexing && theDeletedTimestampOrNull == null) { 919 if (!ourValidationDisabledForUnitTest) { 920 validateResourceForStorage((T) theResource, entity); 921 } 922 } 923 if (!StringUtils.isBlank(entity.getResourceType())) { 924 String resourceType = myContext.getResourceType(theResource); 925 // This is just a sanity check and should never actually fail. 926 // We resolve the ID using IdLookupService, and there should be 927 // no way to get it to give you a mismatched type for an ID. 928 Validate.isTrue(resourceType.equals(entity.getResourceType())); 929 } 930 } 931 932 if (entity.getPublished() == null) { 933 ourLog.debug("Entity has published time: {}", theTransactionDetails.getTransactionDate()); 934 entity.setPublished(theTransactionDetails.getTransactionDate()); 935 } 936 937 ResourceIndexedSearchParams existingParams = null; 938 939 ResourceIndexedSearchParams newParams = null; 940 941 EncodedResource changed; 942 if (theDeletedTimestampOrNull != null) { 943 // DELETE 944 945 entity.setDeleted(theDeletedTimestampOrNull); 946 entity.setUpdated(theDeletedTimestampOrNull); 947 entity.setNarrativeText(null); 948 entity.setContentText(null); 949 entity.setIndexStatus(getEntityIndexedStatusEnum()); 950 changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); 951 952 } else { 953 954 // CREATE or UPDATE 955 956 IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> existingSearchParams = 957 getSearchParamsMapFromTransaction(theTransactionDetails); 958 existingParams = existingSearchParams.get(entity); 959 if (existingParams == null) { 960 existingParams = ResourceIndexedSearchParams.withLists(entity); 961 /* 962 * If we have lots of resource links, this proactively fetches the targets so 963 * that we don't look them up one-by-one when comparing the new set to the 964 * old set later on 965 */ 966 if (existingParams.getResourceLinks().size() >= 10) { 967 List<Long> allPids = existingParams.getResourceLinks().stream() 968 .map(ResourceLink::getId) 969 .collect(Collectors.toList()); 970 new QueryChunker<Long>().chunk(allPids, chunkPids -> { 971 List<ResourceLink> targets = myResourceLinkDao.findByPidAndFetchTargetDetails(chunkPids); 972 ourLog.trace("Prefetched targets: {}", targets); 973 }); 974 } 975 existingSearchParams.put(entity, existingParams); 976 } 977 entity.setDeleted(null); 978 979 // TODO: is this IF statement always true? Try removing it 980 if (thePerformIndexing || theEntity.getVersion() == 1) { 981 982 newParams = ResourceIndexedSearchParams.withSets(); 983 984 RequestPartitionId requestPartitionId; 985 if (!myPartitionSettings.isPartitioningEnabled()) { 986 requestPartitionId = RequestPartitionId.allPartitions(); 987 } else if (entity.getPartitionId() != null) { 988 requestPartitionId = entity.getPartitionId().toPartitionId(); 989 } else { 990 requestPartitionId = 991 RequestPartitionId.fromPartitionId(myPartitionSettings.getDefaultPartitionId()); 992 } 993 994 // Extract search params for resource 995 mySearchParamWithInlineReferencesExtractor.populateFromResource( 996 requestPartitionId, 997 newParams, 998 theTransactionDetails, 999 entity, 1000 theResource, 1001 existingParams, 1002 theRequest, 1003 thePerformIndexing); 1004 1005 if (CollectionUtils.isNotEmpty(newParams.myLinks)) { 1006 setTargetResourceTypeIdForResourceLinks(newParams.myLinks); 1007 } 1008 1009 // Actually persist the ResourceTable and ResourceHistoryTable entities 1010 changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); 1011 1012 if (theForceUpdate) { 1013 changed.setChanged(true); 1014 } 1015 1016 if (changed.isChanged()) { 1017 checkConditionalMatch( 1018 entity, theUpdateVersion, theResource, thePerformIndexing, newParams, theRequest); 1019 1020 if (CURRENTLY_REINDEXING.get(theResource) != Boolean.TRUE) { 1021 entity.setUpdated(theTransactionDetails.getTransactionDate()); 1022 } 1023 newParams.populateResourceTableSearchParamsPresentFlags(entity); 1024 entity.setIndexStatus(getEntityIndexedStatusEnum()); 1025 } 1026 1027 if (myFulltextSearchSvc != null && !myFulltextSearchSvc.isDisabled()) { 1028 populateFullTextFields(myContext, theResource, entity, newParams); 1029 } 1030 1031 } else { 1032 1033 entity.setUpdated(theTransactionDetails.getTransactionDate()); 1034 entity.setIndexStatus(null); 1035 1036 changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, false); 1037 } 1038 } 1039 1040 if (thePerformIndexing 1041 && changed != null 1042 && !changed.isChanged() 1043 && !theForceUpdate 1044 && myStorageSettings.isSuppressUpdatesWithNoChange() 1045 && (entity.getVersion() > 1 || theUpdateVersion)) { 1046 ourLog.debug( 1047 "Resource {} has not changed", 1048 entity.getIdDt().toUnqualified().getValue()); 1049 if (theResource != null) { 1050 myJpaStorageResourceParser.updateResourceMetadata(entity, theResource); 1051 } 1052 entity.setUnchangedInCurrentOperation(true); 1053 return entity; 1054 } 1055 1056 if (entity.getId().getId() != null && theUpdateVersion) { 1057 entity.markVersionUpdatedInCurrentTransaction(); 1058 } 1059 1060 /* 1061 * Save the resource itself 1062 */ 1063 if (entity.getId().getId() == null) { 1064 myEntityManager.persist(entity); 1065 1066 if (entity.getFhirId() == null) { 1067 entity.setFhirId(Long.toString(entity.getId().getId())); 1068 } 1069 1070 postPersist(entity, (T) theResource, theRequest); 1071 1072 } else if (entity.getDeleted() != null) { 1073 entity = myEntityManager.merge(entity); 1074 1075 postDelete(entity); 1076 1077 } else { 1078 entity = myEntityManager.merge(entity); 1079 1080 postUpdate(entity, (T) theResource, theRequest); 1081 } 1082 1083 if (theCreateNewHistoryEntry) { 1084 createHistoryEntry(theRequest, theResource, entity, changed); 1085 } 1086 1087 /* 1088 * Update the "search param present" table which is used for the 1089 * ?foo:missing=true queries 1090 * 1091 * Note that we're only populating this for reference params 1092 * because the index tables for all other types have a MISSING column 1093 * right on them for handling the :missing queries. We can't use the 1094 * index table for resource links (reference indexes) because we index 1095 * those by path and not by parameter name. 1096 */ 1097 if (thePerformIndexing && newParams != null) { 1098 AddRemoveCount presenceCount = 1099 mySearchParamPresenceSvc.updatePresence(entity, newParams.mySearchParamPresentEntities); 1100 1101 // Interceptor broadcast: JPA_PERFTRACE_INFO 1102 if (!presenceCount.isEmpty()) { 1103 IInterceptorBroadcaster compositeBroadcaster = 1104 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 1105 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { 1106 StorageProcessingMessage message = new StorageProcessingMessage(); 1107 message.setMessage( 1108 "For " + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added " 1109 + presenceCount.getAddCount() + " and removed " + presenceCount.getRemoveCount() 1110 + " resource search parameter presence entries"); 1111 HookParams params = new HookParams() 1112 .add(RequestDetails.class, theRequest) 1113 .addIfMatchesType(ServletRequestDetails.class, theRequest) 1114 .add(StorageProcessingMessage.class, message); 1115 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 1116 } 1117 } 1118 } 1119 1120 /* 1121 * Indexing 1122 */ 1123 if (thePerformIndexing) { 1124 if (newParams == null) { 1125 myExpungeService.deleteAllSearchParams(entity.getPersistentId()); 1126 entity.clearAllParamsPopulated(); 1127 } else { 1128 1129 // Synchronize search param indexes 1130 AddRemoveCount searchParamAddRemoveCount = 1131 myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase( 1132 newParams, entity, existingParams); 1133 1134 newParams.populateResourceTableParamCollections(entity); 1135 1136 // Interceptor broadcast: JPA_PERFTRACE_INFO 1137 if (!searchParamAddRemoveCount.isEmpty()) { 1138 IInterceptorBroadcaster compositeBroadcaster = 1139 CompositeInterceptorBroadcaster.newCompositeBroadcaster( 1140 myInterceptorBroadcaster, theRequest); 1141 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { 1142 StorageProcessingMessage message = new StorageProcessingMessage(); 1143 message.setMessage("For " 1144 + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added " 1145 + searchParamAddRemoveCount.getAddCount() + " and removed " 1146 + searchParamAddRemoveCount.getRemoveCount() 1147 + " resource search parameter index entries"); 1148 HookParams params = new HookParams() 1149 .add(RequestDetails.class, theRequest) 1150 .addIfMatchesType(ServletRequestDetails.class, theRequest) 1151 .add(StorageProcessingMessage.class, message); 1152 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 1153 } 1154 } 1155 1156 // Put the final set of search params into the transaction 1157 getSearchParamsMapFromTransaction(theTransactionDetails).put(entity, newParams); 1158 } 1159 } 1160 1161 if (theResource != null) { 1162 myJpaStorageResourceParser.updateResourceMetadata(entity, theResource); 1163 } 1164 1165 return entity; 1166 } 1167 1168 private static IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> getSearchParamsMapFromTransaction( 1169 TransactionDetails theTransactionDetails) { 1170 return theTransactionDetails.getOrCreateUserData( 1171 HapiTransactionService.XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS, IdentityHashMap::new); 1172 } 1173 1174 /** 1175 * This methor returns the {@link EntityIndexStatusEnum} value that should be 1176 * used for a successfully fully indexed resource. This method will return 1177 * {@link EntityIndexStatusEnum#INDEXED_ALL} or {@link EntityIndexStatusEnum#INDEXED_RDBMS_ONLY} 1178 * depending on configuration. 1179 */ 1180 @Nonnull 1181 private EntityIndexStatusEnum getEntityIndexedStatusEnum() { 1182 if (myStorageSettings.isHibernateSearchIndexFullText() 1183 || myStorageSettings.isHibernateSearchIndexSearchParams()) { 1184 return EntityIndexStatusEnum.INDEXED_ALL; 1185 } else { 1186 return EntityIndexStatusEnum.INDEXED_RDBMS_ONLY; 1187 } 1188 } 1189 1190 /** 1191 * Make sure that the match URL was actually appropriate for the supplied 1192 * resource, if so configured, or do it only for first version, since technically it 1193 * is possible (and legal) for someone to be using a conditional update 1194 * to match a resource and then update it in a way that it no longer 1195 * matches. 1196 */ 1197 private void checkConditionalMatch( 1198 ResourceTable theEntity, 1199 boolean theUpdateVersion, 1200 IBaseResource theResource, 1201 boolean thePerformIndexing, 1202 ResourceIndexedSearchParams theNewParams, 1203 RequestDetails theRequest) { 1204 1205 if (!thePerformIndexing) { 1206 return; 1207 } 1208 1209 if (theEntity.getCreatedByMatchUrl() == null && theEntity.getUpdatedByMatchUrl() == null) { 1210 return; 1211 } 1212 1213 // version is not updated at this point, but could be pending for update, which we consider here 1214 long pendingVersion = theEntity.getVersion(); 1215 if (theUpdateVersion && !theEntity.isVersionUpdatedInCurrentTransaction()) { 1216 pendingVersion++; 1217 } 1218 1219 if (myStorageSettings.isPreventInvalidatingConditionalMatchCriteria() || pendingVersion <= 1L) { 1220 String createOrUpdateUrl; 1221 CreateOrUpdateByMatch createOrUpdate; 1222 1223 if (theEntity.getCreatedByMatchUrl() != null) { 1224 createOrUpdateUrl = theEntity.getCreatedByMatchUrl(); 1225 createOrUpdate = CreateOrUpdateByMatch.CREATE; 1226 } else { 1227 createOrUpdateUrl = theEntity.getUpdatedByMatchUrl(); 1228 createOrUpdate = CreateOrUpdateByMatch.UPDATE; 1229 } 1230 1231 verifyMatchUrlForConditionalCreateOrUpdate( 1232 createOrUpdate, theResource, createOrUpdateUrl, theNewParams, theRequest); 1233 } 1234 } 1235 1236 public IBasePersistedResource<?> updateHistoryEntity( 1237 RequestDetails theRequest, 1238 T theResource, 1239 IBasePersistedResource<?> theEntity, 1240 IBasePersistedResource<?> theHistoryEntity, 1241 IIdType theResourceId, 1242 TransactionDetails theTransactionDetails, 1243 boolean isUpdatingCurrent) { 1244 Validate.notNull(theEntity); 1245 Validate.isTrue( 1246 theResource != null, 1247 "Must have either a resource[%s] for resource PID[%s]", 1248 theResource != null, 1249 theEntity.getPersistentId()); 1250 1251 ourLog.debug("Starting history entity update"); 1252 EncodedResource encodedResource = new EncodedResource(); 1253 ResourceHistoryTable historyEntity; 1254 1255 if (isUpdatingCurrent) { 1256 ResourceTable entity = (ResourceTable) theEntity; 1257 1258 IBaseResource oldResource; 1259 if (getStorageSettings().isMassIngestionMode()) { 1260 oldResource = null; 1261 } else { 1262 oldResource = myJpaStorageResourceParser.toResource(entity, false); 1263 } 1264 1265 notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, true); 1266 1267 ResourceTable savedEntity = updateEntity( 1268 theRequest, theResource, entity, null, true, false, theTransactionDetails, false, false); 1269 // Have to call populate again for the encodedResource, since using createHistoryEntry() will cause version 1270 // constraint failure, ie updating the same resource at the same time 1271 encodedResource = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); 1272 // For some reason the current version entity is not attached until after using updateEntity 1273 historyEntity = ((ResourceTable) readEntity(theResourceId, theRequest)).getCurrentVersionEntity(); 1274 1275 // Update version/lastUpdated so that interceptors see the correct version 1276 myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource); 1277 // Populate the PID in the resource, so it is available to hooks 1278 addPidToResource(savedEntity, theResource); 1279 1280 if (!savedEntity.isUnchangedInCurrentOperation()) { 1281 notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, false); 1282 } 1283 } else { 1284 historyEntity = (ResourceHistoryTable) theHistoryEntity; 1285 if (!StringUtils.isBlank(historyEntity.getResourceType())) { 1286 String resourceType = myContext.getResourceType(theResource); 1287 if (!resourceType.equals(historyEntity.getResourceType())) { 1288 throw new UnprocessableEntityException(Msg.code(930) + "Existing resource ID[" 1289 + historyEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" 1290 + historyEntity.getResourceType() 1291 + "] - Cannot update with [" + resourceType + "]"); 1292 } 1293 } 1294 1295 historyEntity.setDeleted(null); 1296 1297 // Check if resource is the same 1298 ResourceEncodingEnum encoding = myStorageSettings.getResourceEncoding(); 1299 List<String> excludeElements = new ArrayList<>(8); 1300 getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta()); 1301 String encodedResourceString = 1302 myResourceHistoryCalculator.encodeResource(theResource, encoding, excludeElements); 1303 byte[] resourceBinary = ResourceHistoryCalculator.getResourceBinary(encoding, encodedResourceString); 1304 final boolean changed = myResourceHistoryCalculator.isResourceHistoryChanged( 1305 historyEntity, resourceBinary, encodedResourceString); 1306 1307 historyEntity.setUpdated(theTransactionDetails.getTransactionDate()); 1308 1309 if (!changed && myStorageSettings.isSuppressUpdatesWithNoChange() && (historyEntity.getVersion() > 1)) { 1310 ourLog.debug( 1311 "Resource {} has not changed", 1312 historyEntity.getIdDt().toUnqualified().getValue()); 1313 myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource); 1314 return historyEntity; 1315 } 1316 1317 myResourceHistoryCalculator.populateEncodedResource( 1318 encodedResource, encodedResourceString, resourceBinary, encoding); 1319 } 1320 /* 1321 * Save the resource itself to the resourceHistoryTable 1322 */ 1323 historyEntity = myEntityManager.merge(historyEntity); 1324 historyEntity.setEncoding(encodedResource.getEncoding()); 1325 historyEntity.setResource(encodedResource.getResourceBinary()); 1326 historyEntity.setResourceTextVc(encodedResource.getResourceText()); 1327 myResourceHistoryTableDao.save(historyEntity); 1328 1329 myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource); 1330 1331 return historyEntity; 1332 } 1333 1334 private void populateEncodedResource( 1335 EncodedResource encodedResource, 1336 String encodedResourceString, 1337 byte[] theResourceBinary, 1338 ResourceEncodingEnum theEncoding) { 1339 encodedResource.setResourceText(encodedResourceString); 1340 encodedResource.setResourceBinary(theResourceBinary); 1341 encodedResource.setEncoding(theEncoding); 1342 } 1343 1344 private void createHistoryEntry( 1345 RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, EncodedResource theChanged) { 1346 boolean versionedTags = 1347 getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.VERSIONED; 1348 1349 ResourceHistoryTable historyEntry = null; 1350 long resourceVersion = theEntity.getVersion(); 1351 if (!myStorageSettings.isResourceDbHistoryEnabled() && resourceVersion > 1L) { 1352 /* 1353 * If we're not storing history, then just pull the current history 1354 * table row and update it. Note that there is always a chance that 1355 * this could return null if the current resourceVersion has been expunged 1356 * in which case we'll still create a new one 1357 */ 1358 historyEntry = myResourceHistoryTableDao.findForIdAndVersion( 1359 theEntity.getResourceId().toFk(), resourceVersion - 1); 1360 if (historyEntry != null) { 1361 theEntity.populateHistoryEntityVersionAndDates(historyEntry); 1362 if (versionedTags && theEntity.isHasTags()) { 1363 for (ResourceTag next : theEntity.getTags()) { 1364 historyEntry.addTag(next.getTag()); 1365 } 1366 } 1367 } 1368 } 1369 1370 /* 1371 * This should basically always be null unless resource history 1372 * is disabled on this server. In that case, we'll just be reusing 1373 * the previous version entity. 1374 */ 1375 if (historyEntry == null) { 1376 historyEntry = theEntity.toHistory(versionedTags && theEntity.getDeleted() == null); 1377 } 1378 1379 historyEntry.setEncoding(theChanged.getEncoding()); 1380 historyEntry.setResource(theChanged.getResourceBinary()); 1381 historyEntry.setResourceTextVc(theChanged.getResourceText()); 1382 1383 ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId()); 1384 myEntityManager.persist(historyEntry); 1385 theEntity.setCurrentVersionEntity(historyEntry); 1386 1387 // Save resource source 1388 String source = null; 1389 1390 if (theResource != null) { 1391 if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) { 1392 IBaseMetaType meta = theResource.getMeta(); 1393 source = MetaUtil.getSource(myContext, meta); 1394 } 1395 if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) { 1396 source = ((IBaseHasExtensions) theResource.getMeta()) 1397 .getExtension().stream() 1398 .filter(t -> HapiExtensions.EXT_META_SOURCE.equals(t.getUrl())) 1399 .filter(t -> t.getValue() instanceof IPrimitiveType) 1400 .map(t -> ((IPrimitiveType<?>) t.getValue()).getValueAsString()) 1401 .findFirst() 1402 .orElse(null); 1403 } 1404 } 1405 1406 String requestId = getRequestId(theRequest, source); 1407 source = MetaUtil.cleanProvenanceSourceUriOrEmpty(source); 1408 1409 boolean shouldStoreSource = 1410 myStorageSettings.getStoreMetaSourceInformation().isStoreSourceUri(); 1411 boolean shouldStoreRequestId = 1412 myStorageSettings.getStoreMetaSourceInformation().isStoreRequestId(); 1413 boolean haveSource = isNotBlank(source) && shouldStoreSource; 1414 boolean haveRequestId = isNotBlank(requestId) && shouldStoreRequestId; 1415 if (haveSource || haveRequestId) { 1416 if (haveRequestId) { 1417 String persistedRequestId = left(requestId, Constants.REQUEST_ID_LENGTH); 1418 historyEntry.setRequestId(persistedRequestId); 1419 } 1420 if (haveSource) { 1421 String persistedSource = left(source, ResourceHistoryTable.SOURCE_URI_LENGTH); 1422 historyEntry.setSourceUri(persistedSource); 1423 } 1424 if (theResource != null) { 1425 MetaUtil.populateResourceSource( 1426 myFhirContext, 1427 shouldStoreSource ? source : null, 1428 shouldStoreRequestId ? requestId : null, 1429 theResource); 1430 } 1431 } 1432 } 1433 1434 private String getRequestId(RequestDetails theRequest, String theSource) { 1435 if (myStorageSettings.isPreserveRequestIdInResourceBody()) { 1436 return StringUtils.substringAfter(theSource, "#"); 1437 } 1438 return theRequest != null ? theRequest.getRequestId() : null; 1439 } 1440 1441 @Override 1442 public DaoMethodOutcome updateInternal( 1443 RequestDetails theRequestDetails, 1444 T theResource, 1445 String theMatchUrl, 1446 boolean thePerformIndexing, 1447 boolean theForceUpdateVersion, 1448 IBasePersistedResource theEntity, 1449 IIdType theResourceId, 1450 @Nullable IBaseResource theOldResource, 1451 RestOperationTypeEnum theOperationType, 1452 TransactionDetails theTransactionDetails) { 1453 1454 ResourceTable entity = (ResourceTable) theEntity; 1455 1456 // We'll update the resource ID with the correct version later but for 1457 // now at least set it to something useful for the interceptors 1458 theResource.setId(entity.getIdDt()); 1459 1460 // Notify IServerOperationInterceptors about pre-action call 1461 notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, true); 1462 1463 entity.setUpdatedByMatchUrl(theMatchUrl); 1464 1465 // Perform update 1466 ResourceTable savedEntity = updateEntity( 1467 theRequestDetails, 1468 theResource, 1469 entity, 1470 null, 1471 thePerformIndexing, 1472 thePerformIndexing, 1473 theTransactionDetails, 1474 theForceUpdateVersion, 1475 thePerformIndexing); 1476 1477 /* 1478 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction), 1479 * we'll manually increase the version. This is important because we want the updated version number 1480 * to be reflected in the resource shared with interceptors 1481 */ 1482 if (!thePerformIndexing 1483 && !savedEntity.isUnchangedInCurrentOperation() 1484 && !ourDisableIncrementOnUpdateForUnitTest) { 1485 if (!theResourceId.hasVersionIdPart()) { 1486 theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion())); 1487 } 1488 incrementId(theResource, savedEntity, theResourceId); 1489 } 1490 1491 // Update version/lastUpdated so that interceptors see the correct version 1492 myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource); 1493 1494 // Populate the PID in the resource so it is available to hooks 1495 addPidToResource(savedEntity, theResource); 1496 1497 // Notify interceptors 1498 if (!savedEntity.isUnchangedInCurrentOperation()) { 1499 notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, false); 1500 } 1501 1502 Collection<? extends BaseTag> tagList = Collections.emptyList(); 1503 if (entity.isHasTags()) { 1504 tagList = entity.getTags(); 1505 } 1506 long version = entity.getVersion(); 1507 myJpaStorageResourceParser.populateResourceMetadata(entity, false, tagList, version, theResource); 1508 1509 boolean wasDeleted = false; 1510 if (theOldResource != null) { 1511 wasDeleted = theOldResource.isDeleted(); 1512 } 1513 1514 if (wasDeleted && !myStorageSettings.isDeleteEnabled()) { 1515 String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "cantUndeleteWithDeletesDisabled"); 1516 throw new InvalidRequestException(Msg.code(2573) + msg); 1517 } 1518 1519 DaoMethodOutcome outcome = toMethodOutcome( 1520 theRequestDetails, savedEntity, theResource, theMatchUrl, theOperationType) 1521 .setCreated(wasDeleted); 1522 1523 if (!thePerformIndexing) { 1524 IIdType id = getContext().getVersion().newIdType(); 1525 id.setValue(entity.getIdDt().getValue()); 1526 outcome.setId(id); 1527 } 1528 1529 // Only include a task timer if we're not in a sub-request (i.e. a transaction) 1530 // since individual item times don't actually make much sense in the context 1531 // of a transaction 1532 StopWatch w = null; 1533 if (theRequestDetails != null && !theRequestDetails.isSubRequest()) { 1534 if (theTransactionDetails != null && !theTransactionDetails.isFhirTransaction()) { 1535 w = new StopWatch(theTransactionDetails.getTransactionDate()); 1536 } 1537 } 1538 1539 populateOperationOutcomeForUpdate(w, outcome, theMatchUrl, outcome.getOperationType(), theTransactionDetails); 1540 1541 return outcome; 1542 } 1543 1544 private void notifyInterceptors( 1545 RequestDetails theRequestDetails, 1546 T theResource, 1547 IBaseResource theOldResource, 1548 TransactionDetails theTransactionDetails, 1549 boolean isUnchanged) { 1550 Pointcut interceptorPointcut = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED; 1551 1552 HookParams hookParams = new HookParams() 1553 .add(IBaseResource.class, theOldResource) 1554 .add(IBaseResource.class, theResource) 1555 .add(RequestDetails.class, theRequestDetails) 1556 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1557 .add(TransactionDetails.class, theTransactionDetails); 1558 1559 if (!isUnchanged) { 1560 hookParams.add( 1561 InterceptorInvocationTimingEnum.class, 1562 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 1563 interceptorPointcut = Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED; 1564 } 1565 1566 doCallHooks(theTransactionDetails, theRequestDetails, interceptorPointcut, hookParams); 1567 } 1568 1569 protected void addPidToResource(IResourceLookup<JpaPid> theEntity, IBaseResource theResource) { 1570 if (theResource instanceof IAnyResource) { 1571 IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId()); 1572 } else if (theResource instanceof IResource) { 1573 IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId()); 1574 } 1575 } 1576 1577 private void validateChildReferenceTargetTypes(IBase theElement, String thePath) { 1578 if (theElement == null) { 1579 return; 1580 } 1581 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass()); 1582 if (!(def instanceof BaseRuntimeElementCompositeDefinition)) { 1583 return; 1584 } 1585 1586 BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def; 1587 for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) { 1588 1589 List<IBase> values = nextChildDef.getAccessor().getValues(theElement); 1590 if (values == null || values.isEmpty()) { 1591 continue; 1592 } 1593 1594 String newPath = thePath + "." + nextChildDef.getElementName(); 1595 1596 for (IBase nextChild : values) { 1597 validateChildReferenceTargetTypes(nextChild, newPath); 1598 } 1599 1600 if (nextChildDef instanceof RuntimeChildResourceDefinition) { 1601 RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef; 1602 Set<String> validTypes = new HashSet<>(); 1603 boolean allowAny = false; 1604 for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) { 1605 if (nextValidType.isInterface()) { 1606 allowAny = true; 1607 break; 1608 } 1609 validTypes.add(getContext().getResourceType(nextValidType)); 1610 } 1611 1612 if (allowAny) { 1613 continue; 1614 } 1615 1616 if (getStorageSettings().isEnforceReferenceTargetTypes()) { 1617 for (IBase nextChild : values) { 1618 IBaseReference nextRef = (IBaseReference) nextChild; 1619 IIdType referencedId = nextRef.getReferenceElement(); 1620 if (!isBlank(referencedId.getResourceType())) { 1621 if (!isLogicalReference(referencedId)) { 1622 if (!referencedId.getValue().contains("?")) { 1623 if (!validTypes.contains(referencedId.getResourceType())) { 1624 throw new UnprocessableEntityException(Msg.code(931) 1625 + "Invalid reference found at path '" + newPath + "'. Resource type '" 1626 + referencedId.getResourceType() + "' is not valid for this path"); 1627 } 1628 } 1629 } 1630 } 1631 } 1632 } 1633 } 1634 } 1635 } 1636 1637 protected void validateMetaCount(int theMetaCount) { 1638 if (myStorageSettings.getResourceMetaCountHardLimit() != null) { 1639 if (theMetaCount > myStorageSettings.getResourceMetaCountHardLimit()) { 1640 throw new UnprocessableEntityException(Msg.code(932) + "Resource contains " + theMetaCount 1641 + " meta entries (tag/profile/security label), maximum is " 1642 + myStorageSettings.getResourceMetaCountHardLimit()); 1643 } 1644 } 1645 } 1646 1647 /** 1648 * This method is invoked immediately before storing a new resource, or an update to an existing resource to allow the DAO to ensure that it is valid for persistence. By default, checks for the 1649 * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check. 1650 * 1651 * @param theResource The resource that is about to be persisted 1652 * @param theEntityToSave TODO 1653 */ 1654 protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) { 1655 Object tag = null; 1656 1657 int totalMetaCount = 0; 1658 1659 if (theResource instanceof IResource) { 1660 IResource res = (IResource) theResource; 1661 TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res); 1662 if (tagList != null) { 1663 tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE); 1664 totalMetaCount += tagList.size(); 1665 } 1666 List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res); 1667 if (profileList != null) { 1668 totalMetaCount += profileList.size(); 1669 } 1670 } else { 1671 IAnyResource res = (IAnyResource) theResource; 1672 tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE); 1673 totalMetaCount += res.getMeta().getTag().size(); 1674 totalMetaCount += res.getMeta().getProfile().size(); 1675 totalMetaCount += res.getMeta().getSecurity().size(); 1676 } 1677 1678 if (tag != null) { 1679 throw new UnprocessableEntityException( 1680 Msg.code(933) 1681 + "Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data"); 1682 } 1683 1684 if (getStorageSettings().isEnforceReferenceTargetTypes()) { 1685 String resName = getContext().getResourceType(theResource); 1686 validateChildReferenceTargetTypes(theResource, resName); 1687 } 1688 1689 validateMetaCount(totalMetaCount); 1690 } 1691 1692 @PostConstruct 1693 public void start() {} 1694 1695 @VisibleForTesting 1696 public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) { 1697 myStorageSettings = theStorageSettings; 1698 } 1699 1700 public void populateFullTextFields( 1701 final FhirContext theContext, 1702 final IBaseResource theResource, 1703 ResourceTable theEntity, 1704 ResourceIndexedSearchParams theNewParams) { 1705 if (theEntity.getDeleted() != null) { 1706 theEntity.setNarrativeText(null); 1707 theEntity.setContentText(null); 1708 } else { 1709 if (myStorageSettings.isHibernateSearchIndexFullText()) { 1710 theEntity.setNarrativeText(parseNarrativeTextIntoWords(theResource)); 1711 theEntity.setContentText(parseContentTextIntoWords(theContext, theResource)); 1712 } 1713 if (myStorageSettings.isHibernateSearchIndexSearchParams()) { 1714 ExtendedHSearchIndexData hSearchIndexData = 1715 myFulltextSearchSvc.extractLuceneIndexData(theResource, theEntity, theNewParams); 1716 theEntity.setLuceneIndexData(hSearchIndexData); 1717 } 1718 } 1719 } 1720 1721 @VisibleForTesting 1722 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 1723 myPartitionSettings = thePartitionSettings; 1724 } 1725 1726 /** 1727 * Do not call this method outside of unit tests 1728 */ 1729 @VisibleForTesting 1730 public void setJpaStorageResourceParserForUnitTest(IJpaStorageResourceParser theJpaStorageResourceParser) { 1731 myJpaStorageResourceParser = theJpaStorageResourceParser; 1732 } 1733 1734 @VisibleForTesting 1735 public void setResourceTypeCacheSvc(IResourceTypeCacheSvc theResourceTypeCacheSvc) { 1736 myResourceTypeCacheSvc = theResourceTypeCacheSvc; 1737 } 1738 1739 @SuppressWarnings("unchecked") 1740 public static String parseContentTextIntoWords(FhirContext theContext, IBaseResource theResource) { 1741 1742 Class<IPrimitiveType<String>> stringType = (Class<IPrimitiveType<String>>) 1743 theContext.getElementDefinition("string").getImplementingClass(); 1744 1745 StringBuilder retVal = new StringBuilder(); 1746 List<IPrimitiveType<String>> childElements = 1747 theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType); 1748 for (IPrimitiveType<String> nextType : childElements) { 1749 if (stringType.equals(nextType.getClass())) { 1750 String nextValue = nextType.getValueAsString(); 1751 if (isNotBlank(nextValue)) { 1752 retVal.append(nextValue.replace("\n", " ").replace("\r", " ")); 1753 retVal.append("\n"); 1754 } 1755 } 1756 } 1757 return retVal.toString(); 1758 } 1759 1760 public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) { 1761 String resourceText = null; 1762 switch (theResourceEncoding) { 1763 case JSON: 1764 resourceText = new String(theResourceBytes, Charsets.UTF_8); 1765 break; 1766 case JSONC: 1767 resourceText = GZipUtil.decompress(theResourceBytes); 1768 break; 1769 case DEL: 1770 case ESR: 1771 break; 1772 } 1773 return resourceText; 1774 } 1775 1776 private static String parseNarrativeTextIntoWords(IBaseResource theResource) { 1777 1778 StringBuilder b = new StringBuilder(); 1779 if (theResource instanceof IResource) { 1780 IResource resource = (IResource) theResource; 1781 List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue()); 1782 if (xmlEvents != null) { 1783 for (XMLEvent next : xmlEvents) { 1784 if (next.isCharacters()) { 1785 Characters characters = next.asCharacters(); 1786 b.append(characters.getData()).append(" "); 1787 } 1788 } 1789 } 1790 } else if (theResource instanceof IDomainResource) { 1791 IDomainResource resource = (IDomainResource) theResource; 1792 try { 1793 String divAsString = resource.getText().getDivAsString(); 1794 List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString); 1795 if (xmlEvents != null) { 1796 for (XMLEvent next : xmlEvents) { 1797 if (next.isCharacters()) { 1798 Characters characters = next.asCharacters(); 1799 b.append(characters.getData()).append(" "); 1800 } 1801 } 1802 } 1803 } catch (Exception e) { 1804 throw new DataFormatException(Msg.code(934) + "Unable to convert DIV to string", e); 1805 } 1806 } 1807 return b.toString(); 1808 } 1809 1810 @VisibleForTesting 1811 public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) { 1812 ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest; 1813 } 1814 1815 /** 1816 * Do not call this method outside of unit tests 1817 */ 1818 @VisibleForTesting 1819 public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) { 1820 ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest; 1821 } 1822 1823 private enum CreateOrUpdateByMatch { 1824 CREATE, 1825 UPDATE 1826 } 1827 1828 private void setTargetResourceTypeIdForResourceLinks(Collection<ResourceLink> resourceLinks) { 1829 resourceLinks.stream() 1830 .filter(link -> link.getTargetResourceType() != null) 1831 .forEach(link -> link.setTargetResourceTypeId( 1832 myResourceTypeCacheSvc.getResourceTypeId(link.getTargetResourceType()))); 1833 } 1834}