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