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 // Synchronize composite params 1289 mySearchParamWithInlineReferencesExtractor.storeUniqueComboParameters( 1290 newParams, entity, existingParams); 1291 } 1292 } 1293 1294 if (theResource != null) { 1295 myJpaStorageResourceParser.updateResourceMetadata(entity, theResource); 1296 } 1297 1298 return entity; 1299 } 1300 1301 /** 1302 * Make sure that the match URL was actually appropriate for the supplied 1303 * resource, if so configured, or do it only for first version, since technically it 1304 * is possible (and legal) for someone to be using a conditional update 1305 * to match a resource and then update it in a way that it no longer 1306 * matches. 1307 */ 1308 private void checkConditionalMatch( 1309 ResourceTable theEntity, 1310 boolean theUpdateVersion, 1311 IBaseResource theResource, 1312 boolean thePerformIndexing, 1313 ResourceIndexedSearchParams theNewParams, 1314 RequestDetails theRequest) { 1315 1316 if (!thePerformIndexing) { 1317 return; 1318 } 1319 1320 if (theEntity.getCreatedByMatchUrl() == null && theEntity.getUpdatedByMatchUrl() == null) { 1321 return; 1322 } 1323 1324 // version is not updated at this point, but could be pending for update, which we consider here 1325 long pendingVersion = theEntity.getVersion(); 1326 if (theUpdateVersion && !theEntity.isVersionUpdatedInCurrentTransaction()) { 1327 pendingVersion++; 1328 } 1329 1330 if (myStorageSettings.isPreventInvalidatingConditionalMatchCriteria() || pendingVersion <= 1L) { 1331 String createOrUpdateUrl; 1332 CreateOrUpdateByMatch createOrUpdate; 1333 1334 if (theEntity.getCreatedByMatchUrl() != null) { 1335 createOrUpdateUrl = theEntity.getCreatedByMatchUrl(); 1336 createOrUpdate = CreateOrUpdateByMatch.CREATE; 1337 } else { 1338 createOrUpdateUrl = theEntity.getUpdatedByMatchUrl(); 1339 createOrUpdate = CreateOrUpdateByMatch.UPDATE; 1340 } 1341 1342 verifyMatchUrlForConditionalCreateOrUpdate( 1343 createOrUpdate, theResource, createOrUpdateUrl, theNewParams, theRequest); 1344 } 1345 } 1346 1347 public IBasePersistedResource updateHistoryEntity( 1348 RequestDetails theRequest, 1349 T theResource, 1350 IBasePersistedResource theEntity, 1351 IBasePersistedResource theHistoryEntity, 1352 IIdType theResourceId, 1353 TransactionDetails theTransactionDetails, 1354 boolean isUpdatingCurrent) { 1355 Validate.notNull(theEntity); 1356 Validate.isTrue( 1357 theResource != null, 1358 "Must have either a resource[%s] for resource PID[%s]", 1359 theResource != null, 1360 theEntity.getPersistentId()); 1361 1362 ourLog.debug("Starting history entity update"); 1363 EncodedResource encodedResource = new EncodedResource(); 1364 ResourceHistoryTable historyEntity; 1365 1366 if (isUpdatingCurrent) { 1367 ResourceTable entity = (ResourceTable) theEntity; 1368 1369 IBaseResource oldResource; 1370 if (getStorageSettings().isMassIngestionMode()) { 1371 oldResource = null; 1372 } else { 1373 oldResource = myJpaStorageResourceParser.toResource(entity, false); 1374 } 1375 1376 notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, true); 1377 1378 ResourceTable savedEntity = updateEntity( 1379 theRequest, theResource, entity, null, true, false, theTransactionDetails, false, false); 1380 // Have to call populate again for the encodedResource, since using createHistoryEntry() will cause version 1381 // constraint failure, ie updating the same resource at the same time 1382 encodedResource = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true); 1383 // For some reason the current version entity is not attached until after using updateEntity 1384 historyEntity = ((ResourceTable) readEntity(theResourceId, theRequest)).getCurrentVersionEntity(); 1385 1386 // Update version/lastUpdated so that interceptors see the correct version 1387 myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource); 1388 // Populate the PID in the resource, so it is available to hooks 1389 addPidToResource(savedEntity, theResource); 1390 1391 if (!savedEntity.isUnchangedInCurrentOperation()) { 1392 notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, false); 1393 } 1394 } else { 1395 historyEntity = (ResourceHistoryTable) theHistoryEntity; 1396 if (!StringUtils.isBlank(historyEntity.getResourceType())) { 1397 validateIncomingResourceTypeMatchesExisting(theResource, historyEntity); 1398 } 1399 1400 historyEntity.setDeleted(null); 1401 1402 // Check if resource is the same 1403 ResourceEncodingEnum encoding = myStorageSettings.getResourceEncoding(); 1404 List<String> excludeElements = new ArrayList<>(8); 1405 getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta()); 1406 String encodedResourceString = 1407 myResourceHistoryCalculator.encodeResource(theResource, encoding, excludeElements); 1408 byte[] resourceBinary = ResourceHistoryCalculator.getResourceBinary(encoding, encodedResourceString); 1409 final boolean changed = myResourceHistoryCalculator.isResourceHistoryChanged( 1410 historyEntity, resourceBinary, encodedResourceString); 1411 1412 historyEntity.setUpdated(theTransactionDetails.getTransactionDate()); 1413 1414 if (!changed && myStorageSettings.isSuppressUpdatesWithNoChange() && (historyEntity.getVersion() > 1)) { 1415 ourLog.debug( 1416 "Resource {} has not changed", 1417 historyEntity.getIdDt().toUnqualified().getValue()); 1418 myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource); 1419 return historyEntity; 1420 } 1421 1422 myResourceHistoryCalculator.populateEncodedResource( 1423 encodedResource, encodedResourceString, resourceBinary, encoding); 1424 } 1425 /* 1426 * Save the resource itself to the resourceHistoryTable 1427 */ 1428 historyEntity = myEntityManager.merge(historyEntity); 1429 historyEntity.setEncoding(encodedResource.getEncoding()); 1430 historyEntity.setResource(encodedResource.getResourceBinary()); 1431 historyEntity.setResourceTextVc(encodedResource.getResourceText()); 1432 myResourceHistoryTableDao.save(historyEntity); 1433 1434 myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource); 1435 1436 return historyEntity; 1437 } 1438 1439 private void populateEncodedResource( 1440 EncodedResource encodedResource, 1441 String encodedResourceString, 1442 byte[] theResourceBinary, 1443 ResourceEncodingEnum theEncoding) { 1444 encodedResource.setResourceText(encodedResourceString); 1445 encodedResource.setResourceBinary(theResourceBinary); 1446 encodedResource.setEncoding(theEncoding); 1447 } 1448 1449 /** 1450 * TODO eventually consider refactoring this to be part of an interceptor. 1451 * <p> 1452 * Throws an exception if the partition of the request, and the partition of the existing entity do not match. 1453 * 1454 * @param theRequest the request. 1455 * @param entity the existing entity. 1456 */ 1457 private void failIfPartitionMismatch(RequestDetails theRequest, ResourceTable entity) { 1458 if (myPartitionSettings.isPartitioningEnabled() 1459 && theRequest != null 1460 && theRequest.getTenantId() != null 1461 && entity.getPartitionId() != null) { 1462 PartitionEntity partitionEntity = myPartitionLookupSvc.getPartitionByName(theRequest.getTenantId()); 1463 // partitionEntity should never be null 1464 if (partitionEntity != null 1465 && !partitionEntity.getId().equals(entity.getPartitionId().getPartitionId())) { 1466 throw new InvalidRequestException(Msg.code(2079) + "Resource " + entity.getResourceType() + "/" 1467 + entity.getId() + " is not known"); 1468 } 1469 } 1470 } 1471 1472 private void createHistoryEntry( 1473 RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, EncodedResource theChanged) { 1474 boolean versionedTags = 1475 getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.VERSIONED; 1476 1477 ResourceHistoryTable historyEntry = null; 1478 long resourceVersion = theEntity.getVersion(); 1479 boolean reusingHistoryEntity = false; 1480 if (!myStorageSettings.isResourceDbHistoryEnabled() && resourceVersion > 1L) { 1481 /* 1482 * If we're not storing history, then just pull the current history 1483 * table row and update it. Note that there is always a chance that 1484 * this could return null if the current resourceVersion has been expunged 1485 * in which case we'll still create a new one 1486 */ 1487 historyEntry = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance( 1488 theEntity.getResourceId(), resourceVersion - 1); 1489 if (historyEntry != null) { 1490 reusingHistoryEntity = true; 1491 theEntity.populateHistoryEntityVersionAndDates(historyEntry); 1492 if (versionedTags && theEntity.isHasTags()) { 1493 for (ResourceTag next : theEntity.getTags()) { 1494 historyEntry.addTag(next.getTag()); 1495 } 1496 } 1497 } 1498 } 1499 1500 /* 1501 * This should basically always be null unless resource history 1502 * is disabled on this server. In that case, we'll just be reusing 1503 * the previous version entity. 1504 */ 1505 if (historyEntry == null) { 1506 historyEntry = theEntity.toHistory(versionedTags); 1507 } 1508 1509 historyEntry.setEncoding(theChanged.getEncoding()); 1510 historyEntry.setResource(theChanged.getResourceBinary()); 1511 historyEntry.setResourceTextVc(theChanged.getResourceText()); 1512 1513 ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId()); 1514 myResourceHistoryTableDao.save(historyEntry); 1515 theEntity.setCurrentVersionEntity(historyEntry); 1516 1517 // Save resource source 1518 String source = null; 1519 1520 if (theResource != null) { 1521 if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) { 1522 IBaseMetaType meta = theResource.getMeta(); 1523 source = MetaUtil.getSource(myContext, meta); 1524 } 1525 if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) { 1526 source = ((IBaseHasExtensions) theResource.getMeta()) 1527 .getExtension().stream() 1528 .filter(t -> HapiExtensions.EXT_META_SOURCE.equals(t.getUrl())) 1529 .filter(t -> t.getValue() instanceof IPrimitiveType) 1530 .map(t -> ((IPrimitiveType<?>) t.getValue()).getValueAsString()) 1531 .findFirst() 1532 .orElse(null); 1533 } 1534 } 1535 1536 String requestId = getRequestId(theRequest, source); 1537 source = MetaUtil.cleanProvenanceSourceUriOrEmpty(source); 1538 1539 boolean shouldStoreSource = 1540 myStorageSettings.getStoreMetaSourceInformation().isStoreSourceUri(); 1541 boolean shouldStoreRequestId = 1542 myStorageSettings.getStoreMetaSourceInformation().isStoreRequestId(); 1543 boolean haveSource = isNotBlank(source) && shouldStoreSource; 1544 boolean haveRequestId = isNotBlank(requestId) && shouldStoreRequestId; 1545 if (haveSource || haveRequestId) { 1546 ResourceHistoryProvenanceEntity provenance = null; 1547 if (reusingHistoryEntity) { 1548 /* 1549 * If version history is disabled, then we may be reusing 1550 * a previous history entity. If that's the case, let's try 1551 * to reuse the previous provenance entity too. 1552 */ 1553 provenance = historyEntry.getProvenance(); 1554 } 1555 if (provenance == null) { 1556 provenance = historyEntry.toProvenance(); 1557 } 1558 provenance.setResourceHistoryTable(historyEntry); 1559 provenance.setResourceTable(theEntity); 1560 provenance.setPartitionId(theEntity.getPartitionId()); 1561 if (haveRequestId) { 1562 String persistedRequestId = left(requestId, Constants.REQUEST_ID_LENGTH); 1563 provenance.setRequestId(persistedRequestId); 1564 historyEntry.setRequestId(persistedRequestId); 1565 } 1566 if (haveSource) { 1567 String persistedSource = left(source, ResourceHistoryTable.SOURCE_URI_LENGTH); 1568 provenance.setSourceUri(persistedSource); 1569 historyEntry.setSourceUri(persistedSource); 1570 } 1571 if (theResource != null) { 1572 MetaUtil.populateResourceSource( 1573 myFhirContext, 1574 shouldStoreSource ? source : null, 1575 shouldStoreRequestId ? requestId : null, 1576 theResource); 1577 } 1578 1579 myEntityManager.persist(provenance); 1580 } 1581 } 1582 1583 private String getRequestId(RequestDetails theRequest, String theSource) { 1584 if (myStorageSettings.isPreserveRequestIdInResourceBody()) { 1585 return StringUtils.substringAfter(theSource, "#"); 1586 } 1587 return theRequest != null ? theRequest.getRequestId() : null; 1588 } 1589 1590 private void validateIncomingResourceTypeMatchesExisting(IBaseResource theResource, BaseHasResource entity) { 1591 String resourceType = myContext.getResourceType(theResource); 1592 if (!resourceType.equals(entity.getResourceType())) { 1593 throw new UnprocessableEntityException(Msg.code(930) + "Existing resource ID[" 1594 + entity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + entity.getResourceType() 1595 + "] - Cannot update with [" + resourceType + "]"); 1596 } 1597 } 1598 1599 @Override 1600 public DaoMethodOutcome updateInternal( 1601 RequestDetails theRequestDetails, 1602 T theResource, 1603 String theMatchUrl, 1604 boolean thePerformIndexing, 1605 boolean theForceUpdateVersion, 1606 IBasePersistedResource theEntity, 1607 IIdType theResourceId, 1608 @Nullable IBaseResource theOldResource, 1609 RestOperationTypeEnum theOperationType, 1610 TransactionDetails theTransactionDetails) { 1611 1612 ResourceTable entity = (ResourceTable) theEntity; 1613 1614 // We'll update the resource ID with the correct version later but for 1615 // now at least set it to something useful for the interceptors 1616 theResource.setId(entity.getIdDt()); 1617 1618 // Notify IServerOperationInterceptors about pre-action call 1619 notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, true); 1620 1621 entity.setUpdatedByMatchUrl(theMatchUrl); 1622 1623 // Perform update 1624 ResourceTable savedEntity = updateEntity( 1625 theRequestDetails, 1626 theResource, 1627 entity, 1628 null, 1629 thePerformIndexing, 1630 thePerformIndexing, 1631 theTransactionDetails, 1632 theForceUpdateVersion, 1633 thePerformIndexing); 1634 1635 /* 1636 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction), 1637 * we'll manually increase the version. This is important because we want the updated version number 1638 * to be reflected in the resource shared with interceptors 1639 */ 1640 if (!thePerformIndexing 1641 && !savedEntity.isUnchangedInCurrentOperation() 1642 && !ourDisableIncrementOnUpdateForUnitTest) { 1643 if (!theResourceId.hasVersionIdPart()) { 1644 theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion())); 1645 } 1646 incrementId(theResource, savedEntity, theResourceId); 1647 } 1648 1649 // Update version/lastUpdated so that interceptors see the correct version 1650 myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource); 1651 1652 // Populate the PID in the resource so it is available to hooks 1653 addPidToResource(savedEntity, theResource); 1654 1655 // Notify interceptors 1656 if (!savedEntity.isUnchangedInCurrentOperation()) { 1657 notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, false); 1658 } 1659 1660 Collection<? extends BaseTag> tagList = Collections.emptyList(); 1661 if (entity.isHasTags()) { 1662 tagList = entity.getTags(); 1663 } 1664 long version = entity.getVersion(); 1665 myJpaStorageResourceParser.populateResourceMetadata(entity, false, tagList, version, theResource); 1666 1667 boolean wasDeleted = false; 1668 if (theOldResource != null) { 1669 wasDeleted = theOldResource.isDeleted(); 1670 } 1671 1672 DaoMethodOutcome outcome = toMethodOutcome( 1673 theRequestDetails, savedEntity, theResource, theMatchUrl, theOperationType) 1674 .setCreated(wasDeleted); 1675 1676 if (!thePerformIndexing) { 1677 IIdType id = getContext().getVersion().newIdType(); 1678 id.setValue(entity.getIdDt().getValue()); 1679 outcome.setId(id); 1680 } 1681 1682 // Only include a task timer if we're not in a sub-request (i.e. a transaction) 1683 // since individual item times don't actually make much sense in the context 1684 // of a transaction 1685 StopWatch w = null; 1686 if (theRequestDetails != null && !theRequestDetails.isSubRequest()) { 1687 if (theTransactionDetails != null && !theTransactionDetails.isFhirTransaction()) { 1688 w = new StopWatch(theTransactionDetails.getTransactionDate()); 1689 } 1690 } 1691 1692 populateOperationOutcomeForUpdate(w, outcome, theMatchUrl, outcome.getOperationType()); 1693 1694 return outcome; 1695 } 1696 1697 private void notifyInterceptors( 1698 RequestDetails theRequestDetails, 1699 T theResource, 1700 IBaseResource theOldResource, 1701 TransactionDetails theTransactionDetails, 1702 boolean isUnchanged) { 1703 Pointcut interceptorPointcut = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED; 1704 1705 HookParams hookParams = new HookParams() 1706 .add(IBaseResource.class, theOldResource) 1707 .add(IBaseResource.class, theResource) 1708 .add(RequestDetails.class, theRequestDetails) 1709 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1710 .add(TransactionDetails.class, theTransactionDetails); 1711 1712 if (!isUnchanged) { 1713 hookParams.add( 1714 InterceptorInvocationTimingEnum.class, 1715 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 1716 interceptorPointcut = Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED; 1717 } 1718 1719 doCallHooks(theTransactionDetails, theRequestDetails, interceptorPointcut, hookParams); 1720 } 1721 1722 protected void addPidToResource(IResourceLookup<JpaPid> theEntity, IBaseResource theResource) { 1723 if (theResource instanceof IAnyResource) { 1724 IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId().getId()); 1725 } else if (theResource instanceof IResource) { 1726 IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId().getId()); 1727 } 1728 } 1729 1730 private void validateChildReferenceTargetTypes(IBase theElement, String thePath) { 1731 if (theElement == null) { 1732 return; 1733 } 1734 BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass()); 1735 if (!(def instanceof BaseRuntimeElementCompositeDefinition)) { 1736 return; 1737 } 1738 1739 BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def; 1740 for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) { 1741 1742 List<IBase> values = nextChildDef.getAccessor().getValues(theElement); 1743 if (values == null || values.isEmpty()) { 1744 continue; 1745 } 1746 1747 String newPath = thePath + "." + nextChildDef.getElementName(); 1748 1749 for (IBase nextChild : values) { 1750 validateChildReferenceTargetTypes(nextChild, newPath); 1751 } 1752 1753 if (nextChildDef instanceof RuntimeChildResourceDefinition) { 1754 RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef; 1755 Set<String> validTypes = new HashSet<>(); 1756 boolean allowAny = false; 1757 for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) { 1758 if (nextValidType.isInterface()) { 1759 allowAny = true; 1760 break; 1761 } 1762 validTypes.add(getContext().getResourceType(nextValidType)); 1763 } 1764 1765 if (allowAny) { 1766 continue; 1767 } 1768 1769 if (getStorageSettings().isEnforceReferenceTargetTypes()) { 1770 for (IBase nextChild : values) { 1771 IBaseReference nextRef = (IBaseReference) nextChild; 1772 IIdType referencedId = nextRef.getReferenceElement(); 1773 if (!isBlank(referencedId.getResourceType())) { 1774 if (!isLogicalReference(referencedId)) { 1775 if (!referencedId.getValue().contains("?")) { 1776 if (!validTypes.contains(referencedId.getResourceType())) { 1777 throw new UnprocessableEntityException(Msg.code(931) 1778 + "Invalid reference found at path '" + newPath + "'. Resource type '" 1779 + referencedId.getResourceType() + "' is not valid for this path"); 1780 } 1781 } 1782 } 1783 } 1784 } 1785 } 1786 } 1787 } 1788 } 1789 1790 protected void validateMetaCount(int theMetaCount) { 1791 if (myStorageSettings.getResourceMetaCountHardLimit() != null) { 1792 if (theMetaCount > myStorageSettings.getResourceMetaCountHardLimit()) { 1793 throw new UnprocessableEntityException(Msg.code(932) + "Resource contains " + theMetaCount 1794 + " meta entries (tag/profile/security label), maximum is " 1795 + myStorageSettings.getResourceMetaCountHardLimit()); 1796 } 1797 } 1798 } 1799 1800 /** 1801 * 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 1802 * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check. 1803 * 1804 * @param theResource The resource that is about to be persisted 1805 * @param theEntityToSave TODO 1806 */ 1807 protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) { 1808 Object tag = null; 1809 1810 int totalMetaCount = 0; 1811 1812 if (theResource instanceof IResource) { 1813 IResource res = (IResource) theResource; 1814 TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res); 1815 if (tagList != null) { 1816 tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE); 1817 totalMetaCount += tagList.size(); 1818 } 1819 List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res); 1820 if (profileList != null) { 1821 totalMetaCount += profileList.size(); 1822 } 1823 } else { 1824 IAnyResource res = (IAnyResource) theResource; 1825 tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE); 1826 totalMetaCount += res.getMeta().getTag().size(); 1827 totalMetaCount += res.getMeta().getProfile().size(); 1828 totalMetaCount += res.getMeta().getSecurity().size(); 1829 } 1830 1831 if (tag != null) { 1832 throw new UnprocessableEntityException( 1833 Msg.code(933) 1834 + "Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data"); 1835 } 1836 1837 if (getStorageSettings().isEnforceReferenceTargetTypes()) { 1838 String resName = getContext().getResourceType(theResource); 1839 validateChildReferenceTargetTypes(theResource, resName); 1840 } 1841 1842 validateMetaCount(totalMetaCount); 1843 } 1844 1845 @PostConstruct 1846 public void start() { 1847 // nothing yet 1848 } 1849 1850 @VisibleForTesting 1851 public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) { 1852 myStorageSettings = theStorageSettings; 1853 } 1854 1855 public void populateFullTextFields( 1856 final FhirContext theContext, 1857 final IBaseResource theResource, 1858 ResourceTable theEntity, 1859 ResourceIndexedSearchParams theNewParams) { 1860 if (theEntity.getDeleted() != null) { 1861 theEntity.setNarrativeText(null); 1862 theEntity.setContentText(null); 1863 } else { 1864 theEntity.setNarrativeText(parseNarrativeTextIntoWords(theResource)); 1865 theEntity.setContentText(parseContentTextIntoWords(theContext, theResource)); 1866 if (myStorageSettings.isAdvancedHSearchIndexing()) { 1867 ExtendedHSearchIndexData hSearchIndexData = 1868 myFulltextSearchSvc.extractLuceneIndexData(theResource, theNewParams); 1869 theEntity.setLuceneIndexData(hSearchIndexData); 1870 } 1871 } 1872 } 1873 1874 @VisibleForTesting 1875 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 1876 myPartitionSettings = thePartitionSettings; 1877 } 1878 1879 /** 1880 * Do not call this method outside of unit tests 1881 */ 1882 @VisibleForTesting 1883 public void setJpaStorageResourceParserForUnitTest(IJpaStorageResourceParser theJpaStorageResourceParser) { 1884 myJpaStorageResourceParser = theJpaStorageResourceParser; 1885 } 1886 1887 private class AddTagDefinitionToCacheAfterCommitSynchronization implements TransactionSynchronization { 1888 1889 private final TagDefinition myTagDefinition; 1890 private final MemoryCacheService.TagDefinitionCacheKey myKey; 1891 1892 public AddTagDefinitionToCacheAfterCommitSynchronization( 1893 MemoryCacheService.TagDefinitionCacheKey theKey, TagDefinition theTagDefinition) { 1894 myTagDefinition = theTagDefinition; 1895 myKey = theKey; 1896 } 1897 1898 @Override 1899 public void afterCommit() { 1900 myMemoryCacheService.put(MemoryCacheService.CacheEnum.TAG_DEFINITION, myKey, myTagDefinition); 1901 } 1902 } 1903 1904 @Nonnull 1905 public static MemoryCacheService.TagDefinitionCacheKey toTagDefinitionMemoryCacheKey( 1906 TagTypeEnum theTagType, String theScheme, String theTerm, String theVersion, Boolean theUserSelected) { 1907 return new MemoryCacheService.TagDefinitionCacheKey( 1908 theTagType, theScheme, theTerm, theVersion, theUserSelected); 1909 } 1910 1911 @SuppressWarnings("unchecked") 1912 public static String parseContentTextIntoWords(FhirContext theContext, IBaseResource theResource) { 1913 1914 Class<IPrimitiveType<String>> stringType = (Class<IPrimitiveType<String>>) 1915 theContext.getElementDefinition("string").getImplementingClass(); 1916 1917 StringBuilder retVal = new StringBuilder(); 1918 List<IPrimitiveType<String>> childElements = 1919 theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType); 1920 for (IPrimitiveType<String> nextType : childElements) { 1921 if (stringType.equals(nextType.getClass())) { 1922 String nextValue = nextType.getValueAsString(); 1923 if (isNotBlank(nextValue)) { 1924 retVal.append(nextValue.replace("\n", " ").replace("\r", " ")); 1925 retVal.append("\n"); 1926 } 1927 } 1928 } 1929 return retVal.toString(); 1930 } 1931 1932 public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) { 1933 String resourceText = null; 1934 switch (theResourceEncoding) { 1935 case JSON: 1936 resourceText = new String(theResourceBytes, Charsets.UTF_8); 1937 break; 1938 case JSONC: 1939 resourceText = GZipUtil.decompress(theResourceBytes); 1940 break; 1941 case DEL: 1942 case ESR: 1943 break; 1944 } 1945 return resourceText; 1946 } 1947 1948 private static String parseNarrativeTextIntoWords(IBaseResource theResource) { 1949 1950 StringBuilder b = new StringBuilder(); 1951 if (theResource instanceof IResource) { 1952 IResource resource = (IResource) theResource; 1953 List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue()); 1954 if (xmlEvents != null) { 1955 for (XMLEvent next : xmlEvents) { 1956 if (next.isCharacters()) { 1957 Characters characters = next.asCharacters(); 1958 b.append(characters.getData()).append(" "); 1959 } 1960 } 1961 } 1962 } else if (theResource instanceof IDomainResource) { 1963 IDomainResource resource = (IDomainResource) theResource; 1964 try { 1965 String divAsString = resource.getText().getDivAsString(); 1966 List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString); 1967 if (xmlEvents != null) { 1968 for (XMLEvent next : xmlEvents) { 1969 if (next.isCharacters()) { 1970 Characters characters = next.asCharacters(); 1971 b.append(characters.getData()).append(" "); 1972 } 1973 } 1974 } 1975 } catch (Exception e) { 1976 throw new DataFormatException(Msg.code(934) + "Unable to convert DIV to string", e); 1977 } 1978 } 1979 return b.toString(); 1980 } 1981 1982 @VisibleForTesting 1983 public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) { 1984 ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest; 1985 } 1986 1987 /** 1988 * Do not call this method outside of unit tests 1989 */ 1990 @VisibleForTesting 1991 public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) { 1992 ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest; 1993 } 1994 1995 private enum CreateOrUpdateByMatch { 1996 CREATE, 1997 UPDATE 1998 } 1999}