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