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