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