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