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