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