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