
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 = 855 getConditionalCreateOrUpdateErrorMsg(theCreateOrUpdate, theIfNoneExist, theResource.fhirType()); 856 throw new InvalidRequestException(Msg.code(929) + errorMsg); 857 } 858 } 859 860 private String getConditionalCreateOrUpdateErrorMsg( 861 CreateOrUpdateByMatch theCreateOrUpdate, String url, String resourceType) { 862 return String.format( 863 "Failed to process conditional %s. " 864 + "The supplied resource %s of type %s did not satisfy the conditional URL.", 865 theCreateOrUpdate.name().toLowerCase(), url, resourceType); 866 } 867 868 @SuppressWarnings("unchecked") 869 @Override 870 public ResourceTable updateEntity( 871 RequestDetails theRequest, 872 final IBaseResource theResource, 873 IBasePersistedResource theEntity, 874 Date theDeletedTimestampOrNull, 875 boolean thePerformIndexing, 876 boolean theUpdateVersion, 877 TransactionDetails theTransactionDetails, 878 boolean theForceUpdate, 879 boolean theCreateNewHistoryEntry) { 880 Validate.notNull(theEntity, "entity must not be null"); 881 Validate.isTrue( 882 theDeletedTimestampOrNull != null || theResource != null, 883 "Must have either a resource[%s] or a deleted timestamp[%s] for resource PID[%s]", 884 theDeletedTimestampOrNull != null, 885 theResource != null, 886 theEntity.getPersistentId()); 887 888 ourLog.debug("Starting entity update"); 889 890 ResourceTable entity = (ResourceTable) theEntity; 891 892 /* 893 * This should be the very first thing.. 894 */ 895 if (theResource != null) { 896 if (thePerformIndexing && theDeletedTimestampOrNull == null) { 897 if (!ourValidationDisabledForUnitTest) { 898 validateResourceForStorage((T) theResource, entity); 899 } 900 } 901 if (!StringUtils.isBlank(entity.getResourceType())) { 902 String resourceType = myContext.getResourceType(theResource); 903 // This is just a sanity check and should never actually fail. 904 // We resolve the ID using IdLookupService, and there should be 905 // no way to get it to give you a mismatched type for an ID. 906 Validate.isTrue(resourceType.equals(entity.getResourceType())); 907 } 908 } 909 910 if (entity.getPublished() == null) { 911 ourLog.debug("Entity has published time: {}", theTransactionDetails.getTransactionDate()); 912 entity.setPublished(theTransactionDetails.getTransactionDate()); 913 } 914 915 ResourceIndexedSearchParams existingParams = null; 916 917 ResourceIndexedSearchParams newParams = null; 918 919 EncodedResource changed; 920 if (theDeletedTimestampOrNull != null) { 921 // DELETE 922 923 entity.setDeleted(theDeletedTimestampOrNull); 924 entity.setUpdated(theDeletedTimestampOrNull); 925 changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); 926 927 } else { 928 929 // CREATE or UPDATE 930 931 IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> existingSearchParams = 932 getSearchParamsMapFromTransaction(theTransactionDetails); 933 existingParams = existingSearchParams.get(entity); 934 if (existingParams == null) { 935 existingParams = ResourceIndexedSearchParams.withLists(entity); 936 /* 937 * If we have lots of resource links, this proactively fetches the targets so 938 * that we don't look them up one-by-one when comparing the new set to the 939 * old set later on 940 */ 941 if (existingParams.getResourceLinks().size() >= 10) { 942 List<Long> allPids = existingParams.getResourceLinks().stream() 943 .map(ResourceLink::getId) 944 .collect(Collectors.toList()); 945 new QueryChunker<Long>().chunk(allPids, chunkPids -> { 946 List<ResourceLink> targets = myResourceLinkDao.findByPidAndFetchTargetDetails(chunkPids); 947 ourLog.trace("Prefetched targets: {}", targets); 948 }); 949 } 950 existingSearchParams.put(entity, existingParams); 951 } 952 entity.setDeleted(null); 953 954 // TODO: is this IF statement always true? Try removing it 955 if (thePerformIndexing || theEntity.getVersion() == 1) { 956 957 newParams = ResourceIndexedSearchParams.withSets(); 958 959 RequestPartitionId requestPartitionId; 960 if (!myPartitionSettings.isPartitioningEnabled()) { 961 requestPartitionId = RequestPartitionId.allPartitions(); 962 } else if (entity.getPartitionId() != null) { 963 requestPartitionId = entity.getPartitionId().toPartitionId(); 964 } else { 965 requestPartitionId = myPartitionSettings.getDefaultRequestPartitionId(); 966 } 967 968 // Extract search params for resource 969 mySearchParamWithInlineReferencesExtractor.populateFromResource( 970 requestPartitionId, 971 newParams, 972 theTransactionDetails, 973 entity, 974 theResource, 975 existingParams, 976 theRequest, 977 thePerformIndexing); 978 979 if (CollectionUtils.isNotEmpty(newParams.myLinks)) { 980 setTargetResourceTypeIdForResourceLinks(newParams.myLinks); 981 } 982 983 // Actually persist the ResourceTable and ResourceHistoryTable entities 984 changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); 985 986 if (theForceUpdate) { 987 changed.setChanged(true); 988 } 989 990 if (changed.isChanged()) { 991 checkConditionalMatch( 992 entity, theUpdateVersion, theResource, thePerformIndexing, newParams, theRequest); 993 994 if (CURRENTLY_REINDEXING.get(theResource) != Boolean.TRUE) { 995 entity.setUpdated(theTransactionDetails.getTransactionDate()); 996 } 997 newParams.populateResourceTableSearchParamsPresentFlags(entity); 998 } 999 1000 } else { 1001 1002 entity.setUpdated(theTransactionDetails.getTransactionDate()); 1003 changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, false); 1004 } 1005 } 1006 1007 if (changed != null && changed.isChanged()) { 1008 populateFullTextFieldsAndSetEntityStatus(theRequest, myContext, theResource, entity, newParams); 1009 } 1010 1011 if (thePerformIndexing 1012 && changed != null 1013 && !changed.isChanged() 1014 && !theForceUpdate 1015 && myStorageSettings.isSuppressUpdatesWithNoChange() 1016 && (entity.getVersion() > 1 || theUpdateVersion)) { 1017 ourLog.debug( 1018 "Resource {} has not changed", 1019 entity.getIdDt().toUnqualified().getValue()); 1020 if (theResource != null) { 1021 myJpaStorageResourceParser.updateResourceMetadata(entity, theResource); 1022 } 1023 entity.setUnchangedInCurrentOperation(true); 1024 return entity; 1025 } 1026 1027 if (entity.getId().getId() != null && theUpdateVersion) { 1028 entity.markVersionUpdatedInCurrentTransaction(); 1029 } 1030 1031 /* 1032 * Save the resource itself 1033 */ 1034 if (entity.getId().getId() == null) { 1035 myEntityManager.persist(entity); 1036 1037 if (entity.getFhirId() == null) { 1038 entity.setFhirId(Long.toString(entity.getId().getId())); 1039 } 1040 1041 postPersist(entity, (T) theResource, theRequest); 1042 1043 } else if (entity.getDeleted() != null) { 1044 entity = myEntityManager.merge(entity); 1045 1046 postDelete(entity); 1047 1048 } else { 1049 entity = myEntityManager.merge(entity); 1050 1051 postUpdate(entity, (T) theResource, theRequest); 1052 } 1053 1054 if (theCreateNewHistoryEntry) { 1055 createHistoryEntry(theRequest, theResource, entity, changed); 1056 } 1057 1058 /* 1059 * Update the "search param present" table which is used for the 1060 * ?foo:missing=true queries 1061 * 1062 * Note that we're only populating this for reference params 1063 * because the index tables for all other types have a MISSING column 1064 * right on them for handling the :missing queries. We can't use the 1065 * index table for resource links (reference indexes) because we index 1066 * those by path and not by parameter name. 1067 */ 1068 if (thePerformIndexing && newParams != null) { 1069 AddRemoveCount presenceCount = 1070 mySearchParamPresenceSvc.updatePresence(entity, newParams.mySearchParamPresentEntities); 1071 1072 // Interceptor broadcast: JPA_PERFTRACE_INFO 1073 if (!presenceCount.isEmpty()) { 1074 IInterceptorBroadcaster compositeBroadcaster = 1075 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 1076 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { 1077 StorageProcessingMessage message = new StorageProcessingMessage(); 1078 message.setMessage( 1079 "For " + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added " 1080 + presenceCount.getAddCount() + " and removed " + presenceCount.getRemoveCount() 1081 + " resource search parameter presence entries"); 1082 HookParams params = new HookParams() 1083 .add(RequestDetails.class, theRequest) 1084 .addIfMatchesType(ServletRequestDetails.class, theRequest) 1085 .add(StorageProcessingMessage.class, message); 1086 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 1087 } 1088 } 1089 } 1090 1091 /* 1092 * Indexing 1093 */ 1094 if (thePerformIndexing) { 1095 if (newParams == null) { 1096 myExpungeService.deleteAllSearchParams(entity.getPersistentId()); 1097 entity.clearAllParamsPopulated(); 1098 } else { 1099 1100 // Synchronize search param indexes 1101 AddRemoveCount searchParamAddRemoveCount = 1102 myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase( 1103 newParams, entity, existingParams); 1104 1105 newParams.populateResourceTableParamCollections(entity); 1106 1107 // Interceptor broadcast: JPA_PERFTRACE_INFO 1108 if (!searchParamAddRemoveCount.isEmpty()) { 1109 IInterceptorBroadcaster compositeBroadcaster = 1110 CompositeInterceptorBroadcaster.newCompositeBroadcaster( 1111 myInterceptorBroadcaster, theRequest); 1112 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { 1113 StorageProcessingMessage message = new StorageProcessingMessage(); 1114 message.setMessage("For " 1115 + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added " 1116 + searchParamAddRemoveCount.getAddCount() + " and removed " 1117 + searchParamAddRemoveCount.getRemoveCount() 1118 + " resource search parameter index entries"); 1119 HookParams params = new HookParams() 1120 .add(RequestDetails.class, theRequest) 1121 .addIfMatchesType(ServletRequestDetails.class, theRequest) 1122 .add(StorageProcessingMessage.class, message); 1123 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 1124 } 1125 } 1126 1127 // Put the final set of search params into the transaction 1128 getSearchParamsMapFromTransaction(theTransactionDetails).put(entity, newParams); 1129 } 1130 } 1131 1132 if (theResource != null) { 1133 myJpaStorageResourceParser.updateResourceMetadata(entity, theResource); 1134 } 1135 1136 return entity; 1137 } 1138 1139 private static IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> getSearchParamsMapFromTransaction( 1140 TransactionDetails theTransactionDetails) { 1141 return theTransactionDetails.getOrCreateUserData( 1142 HapiTransactionService.XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS, IdentityHashMap::new); 1143 } 1144 1145 /** 1146 * Make sure that the match URL was actually appropriate for the supplied 1147 * resource, if so configured, or do it only for first version, since technically it 1148 * is possible (and legal) for someone to be using a conditional update 1149 * to match a resource and then update it in a way that it no longer 1150 * matches. 1151 */ 1152 private void checkConditionalMatch( 1153 ResourceTable theEntity, 1154 boolean theUpdateVersion, 1155 IBaseResource theResource, 1156 boolean thePerformIndexing, 1157 ResourceIndexedSearchParams theNewParams, 1158 RequestDetails theRequest) { 1159 1160 if (!thePerformIndexing) { 1161 return; 1162 } 1163 1164 if (theEntity.getCreatedByMatchUrl() == null && theEntity.getUpdatedByMatchUrl() == null) { 1165 return; 1166 } 1167 1168 // version is not updated at this point, but could be pending for update, which we consider here 1169 long pendingVersion = theEntity.getVersion(); 1170 if (theUpdateVersion && !theEntity.isVersionUpdatedInCurrentTransaction()) { 1171 pendingVersion++; 1172 } 1173 1174 if (myStorageSettings.isPreventInvalidatingConditionalMatchCriteria() || pendingVersion <= 1L) { 1175 String createOrUpdateUrl; 1176 CreateOrUpdateByMatch createOrUpdate; 1177 1178 if (theEntity.getCreatedByMatchUrl() != null) { 1179 createOrUpdateUrl = theEntity.getCreatedByMatchUrl(); 1180 createOrUpdate = CreateOrUpdateByMatch.CREATE; 1181 } else { 1182 createOrUpdateUrl = theEntity.getUpdatedByMatchUrl(); 1183 createOrUpdate = CreateOrUpdateByMatch.UPDATE; 1184 } 1185 1186 verifyMatchUrlForConditionalCreateOrUpdate( 1187 createOrUpdate, theResource, createOrUpdateUrl, theNewParams, theRequest); 1188 } 1189 } 1190 1191 public IBasePersistedResource<?> updateHistoryEntity( 1192 RequestDetails theRequest, 1193 T theResource, 1194 IBasePersistedResource<?> theEntity, 1195 IBasePersistedResource<?> theHistoryEntity, 1196 IIdType theResourceId, 1197 TransactionDetails theTransactionDetails, 1198 boolean isUpdatingCurrent) { 1199 Validate.notNull(theEntity); 1200 Validate.isTrue( 1201 theResource != null, 1202 "Must have either a resource[%s] for resource PID[%s]", 1203 theResource != null, 1204 theEntity.getPersistentId()); 1205 1206 ourLog.debug("Starting history entity update"); 1207 EncodedResource encodedResource = new EncodedResource(); 1208 ResourceHistoryTable historyEntity; 1209 1210 if (isUpdatingCurrent) { 1211 ResourceTable entity = (ResourceTable) theEntity; 1212 1213 IBaseResource oldResource; 1214 if (getStorageSettings().isMassIngestionMode()) { 1215 oldResource = null; 1216 } else { 1217 oldResource = myJpaStorageResourceParser.toResource(entity, false); 1218 } 1219 1220 notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, true); 1221 1222 ResourceTable savedEntity = updateEntity( 1223 theRequest, theResource, entity, null, true, false, theTransactionDetails, false, false); 1224 // Have to call populate again for the encodedResource, since using createHistoryEntry() will cause version 1225 // constraint failure, ie updating the same resource at the same time 1226 encodedResource = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); 1227 // For some reason the current version entity is not attached until after using updateEntity 1228 historyEntity = ((ResourceTable) readEntity(theResourceId, theRequest)).getCurrentVersionEntity(); 1229 1230 // Update version/lastUpdated so that interceptors see the correct version 1231 myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource); 1232 // Populate the PID in the resource, so it is available to hooks 1233 addPidToResource(savedEntity, theResource); 1234 1235 if (!savedEntity.isUnchangedInCurrentOperation()) { 1236 notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, false); 1237 } 1238 } else { 1239 historyEntity = (ResourceHistoryTable) theHistoryEntity; 1240 if (!StringUtils.isBlank(historyEntity.getResourceType())) { 1241 String resourceType = myContext.getResourceType(theResource); 1242 if (!resourceType.equals(historyEntity.getResourceType())) { 1243 throw new UnprocessableEntityException(Msg.code(930) + "Existing resource ID[" 1244 + historyEntity.getIdDt().toUnqualifiedVersionless() + "] is of type[" 1245 + historyEntity.getResourceType() 1246 + "] - Cannot update with [" + resourceType + "]"); 1247 } 1248 } 1249 1250 historyEntity.setDeleted(null); 1251 1252 // Check if resource is the same 1253 ResourceEncodingEnum encoding = myStorageSettings.getResourceEncoding(); 1254 List<String> excludeElements = new ArrayList<>(8); 1255 getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta()); 1256 String encodedResourceString = 1257 myResourceHistoryCalculator.encodeResource(theResource, encoding, excludeElements); 1258 byte[] resourceBinary = ResourceHistoryCalculator.getResourceBinary(encoding, encodedResourceString); 1259 final boolean changed = myResourceHistoryCalculator.isResourceHistoryChanged( 1260 historyEntity, resourceBinary, encodedResourceString); 1261 1262 historyEntity.setUpdated(theTransactionDetails.getTransactionDate()); 1263 1264 if (!changed && myStorageSettings.isSuppressUpdatesWithNoChange() && (historyEntity.getVersion() > 1)) { 1265 ourLog.debug( 1266 "Resource {} has not changed", 1267 historyEntity.getIdDt().toUnqualified().getValue()); 1268 myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource); 1269 return historyEntity; 1270 } 1271 1272 myResourceHistoryCalculator.populateEncodedResource( 1273 encodedResource, encodedResourceString, resourceBinary, encoding); 1274 } 1275 /* 1276 * Save the resource itself to the resourceHistoryTable 1277 */ 1278 historyEntity = myEntityManager.merge(historyEntity); 1279 historyEntity.setEncoding(encodedResource.getEncoding()); 1280 historyEntity.setResource(encodedResource.getResourceBinary()); 1281 historyEntity.setResourceTextVc(encodedResource.getResourceText()); 1282 myResourceHistoryTableDao.save(historyEntity); 1283 1284 myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource); 1285 1286 return historyEntity; 1287 } 1288 1289 private void populateEncodedResource( 1290 EncodedResource encodedResource, 1291 String encodedResourceString, 1292 byte[] theResourceBinary, 1293 ResourceEncodingEnum theEncoding) { 1294 encodedResource.setResourceText(encodedResourceString); 1295 encodedResource.setResourceBinary(theResourceBinary); 1296 encodedResource.setEncoding(theEncoding); 1297 } 1298 1299 private void createHistoryEntry( 1300 RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, EncodedResource theChanged) { 1301 boolean versionedTags = 1302 getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.VERSIONED; 1303 1304 ResourceHistoryTable historyEntry = null; 1305 long resourceVersion = theEntity.getVersion(); 1306 if (!myStorageSettings.isResourceDbHistoryEnabled() && resourceVersion > 1L) { 1307 /* 1308 * If we're not storing history, then just pull the current history 1309 * table row and update it. Note that there is always a chance that 1310 * this could return null if the current resourceVersion has been expunged 1311 * in which case we'll still create a new one 1312 */ 1313 historyEntry = myResourceHistoryTableDao.findForIdAndVersion( 1314 theEntity.getResourceId().toFk(), resourceVersion - 1); 1315 if (historyEntry != null) { 1316 theEntity.populateHistoryEntityVersionAndDates(historyEntry); 1317 if (versionedTags && theEntity.isHasTags()) { 1318 for (ResourceTag next : theEntity.getTags()) { 1319 historyEntry.addTag(next.getTag()); 1320 } 1321 } 1322 } 1323 } 1324 1325 /* 1326 * This should basically always be null unless resource history 1327 * is disabled on this server. In that case, we'll just be reusing 1328 * the previous version entity. 1329 */ 1330 if (historyEntry == null) { 1331 historyEntry = theEntity.toHistory(versionedTags && theEntity.getDeleted() == null); 1332 } 1333 1334 historyEntry.setEncoding(theChanged.getEncoding()); 1335 historyEntry.setResource(theChanged.getResourceBinary()); 1336 historyEntry.setResourceTextVc(theChanged.getResourceText()); 1337 1338 ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId()); 1339 myEntityManager.persist(historyEntry); 1340 theEntity.setCurrentVersionEntity(historyEntry); 1341 1342 // Save resource source 1343 String source = null; 1344 1345 if (theResource != null) { 1346 if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) { 1347 IBaseMetaType meta = theResource.getMeta(); 1348 source = MetaUtil.getSource(myContext, meta); 1349 } 1350 if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) { 1351 source = ((IBaseHasExtensions) theResource.getMeta()) 1352 .getExtension().stream() 1353 .filter(t -> HapiExtensions.EXT_META_SOURCE.equals(t.getUrl())) 1354 .filter(t -> t.getValue() instanceof IPrimitiveType) 1355 .map(t -> ((IPrimitiveType<?>) t.getValue()).getValueAsString()) 1356 .findFirst() 1357 .orElse(null); 1358 } 1359 } 1360 1361 String requestId = getRequestId(theRequest, source); 1362 source = MetaUtil.cleanProvenanceSourceUriOrEmpty(source); 1363 1364 boolean shouldStoreSource = 1365 myStorageSettings.getStoreMetaSourceInformation().isStoreSourceUri(); 1366 boolean shouldStoreRequestId = 1367 myStorageSettings.getStoreMetaSourceInformation().isStoreRequestId(); 1368 boolean haveSource = isNotBlank(source) && shouldStoreSource; 1369 boolean haveRequestId = isNotBlank(requestId) && shouldStoreRequestId; 1370 if (haveSource || haveRequestId) { 1371 if (haveRequestId) { 1372 String persistedRequestId = left(requestId, Constants.REQUEST_ID_LENGTH); 1373 historyEntry.setRequestId(persistedRequestId); 1374 } 1375 if (haveSource) { 1376 String persistedSource = left(source, ResourceHistoryTable.SOURCE_URI_LENGTH); 1377 historyEntry.setSourceUri(persistedSource); 1378 } 1379 if (theResource != null) { 1380 MetaUtil.populateResourceSource( 1381 myFhirContext, 1382 shouldStoreSource ? source : null, 1383 shouldStoreRequestId ? requestId : null, 1384 theResource); 1385 } 1386 } 1387 } 1388 1389 private String getRequestId(RequestDetails theRequest, String theSource) { 1390 if (myStorageSettings.isPreserveRequestIdInResourceBody()) { 1391 return StringUtils.substringAfter(theSource, "#"); 1392 } 1393 return theRequest != null ? theRequest.getRequestId() : null; 1394 } 1395 1396 @Override 1397 public DaoMethodOutcome updateInternal( 1398 RequestDetails theRequestDetails, 1399 T theResource, 1400 String theMatchUrl, 1401 boolean thePerformIndexing, 1402 boolean theForceUpdateVersion, 1403 IBasePersistedResource theEntity, 1404 IIdType theResourceId, 1405 @Nullable IBaseResource theOldResource, 1406 RestOperationTypeEnum theOperationType, 1407 TransactionDetails theTransactionDetails) { 1408 1409 ResourceTable entity = (ResourceTable) theEntity; 1410 1411 // We'll update the resource ID with the correct version later but for 1412 // now at least set it to something useful for the interceptors 1413 theResource.setId(entity.getIdDt()); 1414 1415 // Notify IServerOperationInterceptors about pre-action call 1416 notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, true); 1417 1418 entity.setUpdatedByMatchUrl(theMatchUrl); 1419 1420 // Perform update 1421 ResourceTable savedEntity = updateEntity( 1422 theRequestDetails, 1423 theResource, 1424 entity, 1425 null, 1426 thePerformIndexing, 1427 thePerformIndexing, 1428 theTransactionDetails, 1429 theForceUpdateVersion, 1430 thePerformIndexing); 1431 1432 /* 1433 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction), 1434 * we'll manually increase the version. This is important because we want the updated version number 1435 * to be reflected in the resource shared with interceptors 1436 */ 1437 if (!thePerformIndexing 1438 && !savedEntity.isUnchangedInCurrentOperation() 1439 && !ourDisableIncrementOnUpdateForUnitTest) { 1440 if (!theResourceId.hasVersionIdPart()) { 1441 theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion())); 1442 } 1443 incrementId(theResource, savedEntity, theResourceId); 1444 } 1445 1446 // Update version/lastUpdated so that interceptors see the correct version 1447 myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource); 1448 1449 // Populate the PID in the resource so it is available to hooks 1450 addPidToResource(savedEntity, theResource); 1451 1452 // Notify interceptors 1453 if (!savedEntity.isUnchangedInCurrentOperation()) { 1454 notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, false); 1455 } 1456 1457 Collection<? extends BaseTag> tagList = Collections.emptyList(); 1458 if (entity.isHasTags()) { 1459 tagList = entity.getTags(); 1460 } 1461 long version = entity.getVersion(); 1462 myJpaStorageResourceParser.populateResourceMetadata(entity, false, tagList, version, theResource); 1463 1464 boolean wasDeleted = false; 1465 if (theOldResource != null) { 1466 wasDeleted = theOldResource.isDeleted(); 1467 } 1468 1469 if (wasDeleted && !myStorageSettings.isDeleteEnabled()) { 1470 String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "cantUndeleteWithDeletesDisabled"); 1471 throw new InvalidRequestException(Msg.code(2573) + msg); 1472 } 1473 1474 DaoMethodOutcome outcome = toMethodOutcome( 1475 theRequestDetails, savedEntity, theResource, theMatchUrl, theOperationType) 1476 .setCreated(wasDeleted); 1477 1478 if (!thePerformIndexing) { 1479 IIdType id = getContext().getVersion().newIdType(); 1480 id.setValue(entity.getIdDt().getValue()); 1481 outcome.setId(id); 1482 } 1483 1484 // Only include a task timer if we're not in a sub-request (i.e. a transaction) 1485 // since individual item times don't actually make much sense in the context 1486 // of a transaction 1487 StopWatch w = null; 1488 if (theRequestDetails != null && !theRequestDetails.isSubRequest()) { 1489 if (theTransactionDetails != null && !theTransactionDetails.isFhirTransaction()) { 1490 w = new StopWatch(theTransactionDetails.getTransactionDate()); 1491 } 1492 } 1493 1494 populateOperationOutcomeForUpdate(w, outcome, theMatchUrl, outcome.getOperationType(), theTransactionDetails); 1495 1496 return outcome; 1497 } 1498 1499 private void notifyInterceptors( 1500 RequestDetails theRequestDetails, 1501 T theResource, 1502 IBaseResource theOldResource, 1503 TransactionDetails theTransactionDetails, 1504 boolean isUnchanged) { 1505 Pointcut interceptorPointcut = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED; 1506 1507 HookParams hookParams = new HookParams() 1508 .add(IBaseResource.class, theOldResource) 1509 .add(IBaseResource.class, theResource) 1510 .add(RequestDetails.class, theRequestDetails) 1511 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1512 .add(TransactionDetails.class, theTransactionDetails); 1513 1514 if (!isUnchanged) { 1515 hookParams.add( 1516 InterceptorInvocationTimingEnum.class, 1517 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 1518 interceptorPointcut = Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED; 1519 } 1520 1521 doCallHooks(theTransactionDetails, theRequestDetails, interceptorPointcut, hookParams); 1522 } 1523 1524 protected void addPidToResource(IResourceLookup<JpaPid> theEntity, IBaseResource theResource) { 1525 if (theResource instanceof IAnyResource) { 1526 IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId()); 1527 } else if (theResource instanceof IResource) { 1528 IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId()); 1529 } 1530 } 1531 1532 private void validateChildReferenceTargetTypes(IBase theElement, String thePath) { 1533 if (theElement == null) { 1534 return; 1535 } 1536 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass()); 1537 if (!(def instanceof BaseRuntimeElementCompositeDefinition)) { 1538 return; 1539 } 1540 1541 BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def; 1542 for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) { 1543 1544 List<IBase> values = nextChildDef.getAccessor().getValues(theElement); 1545 if (values == null || values.isEmpty()) { 1546 continue; 1547 } 1548 1549 String newPath = thePath + "." + nextChildDef.getElementName(); 1550 1551 for (IBase nextChild : values) { 1552 validateChildReferenceTargetTypes(nextChild, newPath); 1553 } 1554 1555 if (nextChildDef instanceof RuntimeChildResourceDefinition) { 1556 RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef; 1557 Set<String> validTypes = new HashSet<>(); 1558 boolean allowAny = false; 1559 for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) { 1560 if (nextValidType.isInterface()) { 1561 allowAny = true; 1562 break; 1563 } 1564 validTypes.add(getContext().getResourceType(nextValidType)); 1565 } 1566 1567 if (allowAny) { 1568 continue; 1569 } 1570 1571 if (getStorageSettings().isEnforceReferenceTargetTypes()) { 1572 for (IBase nextChild : values) { 1573 IBaseReference nextRef = (IBaseReference) nextChild; 1574 IIdType referencedId = nextRef.getReferenceElement(); 1575 if (!isBlank(referencedId.getResourceType())) { 1576 if (!isLogicalReference(referencedId)) { 1577 if (!referencedId.getValue().contains("?")) { 1578 if (!validTypes.contains(referencedId.getResourceType())) { 1579 throw new UnprocessableEntityException(Msg.code(931) 1580 + "Invalid reference found at path '" + newPath + "'. Resource type '" 1581 + referencedId.getResourceType() + "' is not valid for this path"); 1582 } 1583 } 1584 } 1585 } 1586 } 1587 } 1588 } 1589 } 1590 } 1591 1592 protected void validateMetaCount(int theMetaCount) { 1593 if (myStorageSettings.getResourceMetaCountHardLimit() != null) { 1594 if (theMetaCount > myStorageSettings.getResourceMetaCountHardLimit()) { 1595 throw new UnprocessableEntityException(Msg.code(932) + "Resource contains " + theMetaCount 1596 + " meta entries (tag/profile/security label), maximum is " 1597 + myStorageSettings.getResourceMetaCountHardLimit()); 1598 } 1599 } 1600 } 1601 1602 /** 1603 * 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 1604 * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check. 1605 * 1606 * @param theResource The resource that is about to be persisted 1607 * @param theEntityToSave TODO 1608 */ 1609 protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) { 1610 Object tag = null; 1611 1612 int totalMetaCount = 0; 1613 1614 if (theResource instanceof IResource) { 1615 IResource res = (IResource) theResource; 1616 TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res); 1617 if (tagList != null) { 1618 tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE); 1619 totalMetaCount += tagList.size(); 1620 } 1621 List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res); 1622 if (profileList != null) { 1623 totalMetaCount += profileList.size(); 1624 } 1625 } else { 1626 IAnyResource res = (IAnyResource) theResource; 1627 tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE); 1628 totalMetaCount += res.getMeta().getTag().size(); 1629 totalMetaCount += res.getMeta().getProfile().size(); 1630 totalMetaCount += res.getMeta().getSecurity().size(); 1631 } 1632 1633 if (tag != null) { 1634 throw new UnprocessableEntityException( 1635 Msg.code(933) 1636 + "Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data"); 1637 } 1638 1639 if (getStorageSettings().isEnforceReferenceTargetTypes()) { 1640 String resName = getContext().getResourceType(theResource); 1641 validateChildReferenceTargetTypes(theResource, resName); 1642 } 1643 1644 validateMetaCount(totalMetaCount); 1645 } 1646 1647 @PostConstruct 1648 public void start() {} 1649 1650 @VisibleForTesting 1651 public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) { 1652 myStorageSettings = theStorageSettings; 1653 } 1654 1655 /** 1656 * If configured to do so, extracts the FullText indexes for the given 1657 * entity. The {@link ResourceTable#setIndexStatus(EntityIndexStatusEnum) Index Status} 1658 * is updated to reflect whether fulltext indexing is being used on this entity. 1659 */ 1660 private void populateFullTextFieldsAndSetEntityStatus( 1661 RequestDetails theRequestDetails, 1662 final FhirContext theContext, 1663 final IBaseResource theResource, 1664 ResourceTable theEntity, 1665 ResourceIndexedSearchParams theNewParams) { 1666 if (myFulltextSearchSvc == null || myFulltextSearchSvc.isDisabled()) { 1667 theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_RDBMS_ONLY); 1668 return; 1669 } 1670 1671 // This will get changed if we end up setting either 1672 theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_RDBMS_ONLY); 1673 1674 // Standard FullText indexing 1675 if (myStorageSettings.isHibernateSearchIndexFullText()) { 1676 1677 // _content 1678 if (mySearchParamRegistry.hasActiveSearchParam( 1679 theEntity.getResourceType(), 1680 Constants.PARAM_CONTENT, 1681 ISearchParamRegistry.SearchParamLookupContextEnum.INDEX)) { 1682 Supplier<String> contentSupplier = () -> parseContentTextIntoWords(theContext, theResource); 1683 Pointcut pointcut = Pointcut.JPA_INDEX_EXTRACT_FULLTEXT_CONTENT; 1684 Consumer<String> contentEntitySetter = theEntity::setContentText; 1685 extractFullTextIndexData( 1686 theRequestDetails, theResource, theEntity, pointcut, contentSupplier, contentEntitySetter); 1687 } 1688 1689 // _text 1690 if (mySearchParamRegistry.hasActiveSearchParam( 1691 theEntity.getResourceType(), 1692 Constants.PARAM_TEXT, 1693 ISearchParamRegistry.SearchParamLookupContextEnum.INDEX)) { 1694 Supplier<String> textSupplier = () -> parseNarrativeTextIntoWords(theResource); 1695 Pointcut pointcut = Pointcut.JPA_INDEX_EXTRACT_FULLTEXT_TEXT; 1696 Consumer<String> textEntitySetter = theEntity::setNarrativeText; 1697 extractFullTextIndexData( 1698 theRequestDetails, theResource, theEntity, pointcut, textSupplier, textEntitySetter); 1699 } 1700 } 1701 1702 // Advanced indexing - Index standard search params in the FullText index 1703 if (myStorageSettings.isHibernateSearchIndexSearchParams()) { 1704 if (theResource != null) { 1705 ExtendedHSearchIndexData hSearchIndexData = 1706 myFulltextSearchSvc.extractLuceneIndexData(theResource, theEntity, theNewParams); 1707 theEntity.setLuceneIndexData(hSearchIndexData); 1708 } else { 1709 theEntity.setLuceneIndexData(null); 1710 } 1711 theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL); 1712 } 1713 } 1714 1715 private void extractFullTextIndexData( 1716 RequestDetails theRequestDetails, 1717 IBaseResource theResource, 1718 ResourceTable theEntity, 1719 Pointcut thePointcut, 1720 Supplier<String> theContentSupplier, 1721 Consumer<String> theEntityIndexSetter) { 1722 IInterceptorBroadcaster compositeBroadcaster = 1723 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); 1724 FullTextExtractionResponse contentOutcome = null; 1725 if (compositeBroadcaster.hasHooks(thePointcut)) { 1726 FullTextExtractionRequest contentRequest = new FullTextExtractionRequest( 1727 theEntity.getIdType(myContext), theResource, getResourceName(), theContentSupplier); 1728 HookParams contentParams = new HookParams().add(FullTextExtractionRequest.class, contentRequest); 1729 contentOutcome = (FullTextExtractionResponse) 1730 compositeBroadcaster.callHooksAndReturnObject(thePointcut, contentParams); 1731 } 1732 1733 if (contentOutcome == null || contentOutcome.isIndexNormally()) { 1734 theEntityIndexSetter.accept(theContentSupplier.get()); 1735 theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL); 1736 } else if (!contentOutcome.isDoNotIndex()) { 1737 theEntityIndexSetter.accept(contentOutcome.getPayload()); 1738 theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL); 1739 } 1740 } 1741 1742 @VisibleForTesting 1743 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 1744 myPartitionSettings = thePartitionSettings; 1745 } 1746 1747 /** 1748 * Do not call this method outside of unit tests 1749 */ 1750 @VisibleForTesting 1751 public void setJpaStorageResourceParserForUnitTest(IJpaStorageResourceParser theJpaStorageResourceParser) { 1752 myJpaStorageResourceParser = theJpaStorageResourceParser; 1753 } 1754 1755 @VisibleForTesting 1756 public void setResourceTypeCacheSvc(IResourceTypeCacheSvc theResourceTypeCacheSvc) { 1757 myResourceTypeCacheSvc = theResourceTypeCacheSvc; 1758 } 1759 1760 @Nullable 1761 @SuppressWarnings("unchecked") 1762 public static String parseContentTextIntoWords( 1763 @Nonnull FhirContext theContext, @Nullable IBaseResource theResource) { 1764 if (theResource == null) { 1765 return null; 1766 } 1767 1768 Class<IPrimitiveType<String>> stringType = (Class<IPrimitiveType<String>>) 1769 theContext.getElementDefinition("string").getImplementingClass(); 1770 1771 StringBuilder retVal = new StringBuilder(); 1772 List<IPrimitiveType<String>> childElements = 1773 theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType); 1774 for (IPrimitiveType<String> nextType : childElements) { 1775 if (stringType.equals(nextType.getClass())) { 1776 String nextValue = nextType.getValueAsString(); 1777 if (isNotBlank(nextValue)) { 1778 retVal.append(nextValue.replace("\n", " ").replace("\r", " ")); 1779 retVal.append("\n"); 1780 } 1781 } 1782 } 1783 return retVal.toString(); 1784 } 1785 1786 public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) { 1787 String resourceText = null; 1788 switch (theResourceEncoding) { 1789 case JSON: 1790 resourceText = new String(theResourceBytes, Charsets.UTF_8); 1791 break; 1792 case JSONC: 1793 resourceText = GZipUtil.decompress(theResourceBytes); 1794 break; 1795 case DEL: 1796 case ESR: 1797 break; 1798 } 1799 return resourceText; 1800 } 1801 1802 @Nullable 1803 private static String parseNarrativeTextIntoWords(@Nullable IBaseResource theResource) { 1804 if (theResource == null) { 1805 return null; 1806 } 1807 StringBuilder b = new StringBuilder(); 1808 if (theResource instanceof IResource) { 1809 IResource resource = (IResource) theResource; 1810 List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue()); 1811 if (xmlEvents != null) { 1812 for (XMLEvent next : xmlEvents) { 1813 if (next.isCharacters()) { 1814 Characters characters = next.asCharacters(); 1815 b.append(characters.getData()).append(" "); 1816 } 1817 } 1818 } 1819 } else if (theResource instanceof IDomainResource) { 1820 IDomainResource resource = (IDomainResource) theResource; 1821 try { 1822 String divAsString = resource.getText().getDivAsString(); 1823 List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString); 1824 if (xmlEvents != null) { 1825 for (XMLEvent next : xmlEvents) { 1826 if (next.isCharacters()) { 1827 Characters characters = next.asCharacters(); 1828 b.append(characters.getData()).append(" "); 1829 } 1830 } 1831 } 1832 } catch (Exception e) { 1833 throw new DataFormatException(Msg.code(934) + "Unable to convert DIV to string", e); 1834 } 1835 } 1836 return b.toString(); 1837 } 1838 1839 @VisibleForTesting 1840 public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) { 1841 ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest; 1842 } 1843 1844 /** 1845 * Do not call this method outside of unit tests 1846 */ 1847 @VisibleForTesting 1848 public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) { 1849 ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest; 1850 } 1851 1852 private enum CreateOrUpdateByMatch { 1853 CREATE, 1854 UPDATE 1855 } 1856 1857 private void setTargetResourceTypeIdForResourceLinks(Collection<ResourceLink> resourceLinks) { 1858 resourceLinks.stream() 1859 .filter(link -> link.getTargetResourceType() != null) 1860 .forEach(link -> link.setTargetResourceTypeId( 1861 myResourceTypeCacheSvc.getResourceTypeId(link.getTargetResourceType()))); 1862 } 1863}