
001package ca.uhn.fhir.jpa.dao; 002 003/* 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.FhirVersionEnum; 024import ca.uhn.fhir.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.api.HookParams; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.interceptor.model.RequestPartitionId; 029import ca.uhn.fhir.jpa.api.config.DaoConfig; 030import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 031import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 032import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 033import ca.uhn.fhir.jpa.api.model.DeleteConflictList; 034import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; 035import ca.uhn.fhir.jpa.api.model.ExpungeOptions; 036import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; 037import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome; 038import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 039import ca.uhn.fhir.jpa.dao.index.IdHelperService; 040import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 041import ca.uhn.fhir.jpa.delete.DeleteConflictUtil; 042import ca.uhn.fhir.jpa.model.entity.BaseHasResource; 043import ca.uhn.fhir.jpa.model.entity.BaseTag; 044import ca.uhn.fhir.jpa.model.entity.ForcedId; 045import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 046import ca.uhn.fhir.jpa.model.entity.ResourceTable; 047import ca.uhn.fhir.jpa.model.entity.TagDefinition; 048import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 049import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; 050import ca.uhn.fhir.jpa.model.util.JpaConstants; 051import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 052import ca.uhn.fhir.jpa.partition.SystemRequestDetails; 053import ca.uhn.fhir.jpa.patch.FhirPatch; 054import ca.uhn.fhir.jpa.patch.JsonPatchUtils; 055import ca.uhn.fhir.jpa.patch.XmlPatchUtils; 056import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; 057import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum; 058import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; 059import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 060import ca.uhn.fhir.jpa.searchparam.ResourceSearch; 061import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 062import ca.uhn.fhir.jpa.util.MemoryCacheService; 063import ca.uhn.fhir.model.api.IQueryParameterType; 064import ca.uhn.fhir.model.dstu2.resource.ListResource; 065import ca.uhn.fhir.model.primitive.IdDt; 066import ca.uhn.fhir.parser.DataFormatException; 067import ca.uhn.fhir.rest.api.CacheControlDirective; 068import ca.uhn.fhir.rest.api.Constants; 069import ca.uhn.fhir.rest.api.EncodingEnum; 070import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum; 071import ca.uhn.fhir.rest.api.MethodOutcome; 072import ca.uhn.fhir.rest.api.PatchTypeEnum; 073import ca.uhn.fhir.rest.api.RequestTypeEnum; 074import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 075import ca.uhn.fhir.rest.api.SearchContainedModeEnum; 076import ca.uhn.fhir.rest.api.ValidationModeEnum; 077import ca.uhn.fhir.rest.api.server.IBundleProvider; 078import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 079import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 080import ca.uhn.fhir.rest.api.server.RequestDetails; 081import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; 082import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 083import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter; 084import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; 085import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 086import ca.uhn.fhir.rest.param.HasParam; 087import ca.uhn.fhir.rest.server.IPagingProvider; 088import ca.uhn.fhir.rest.server.IRestfulServerDefaults; 089import ca.uhn.fhir.rest.server.RestfulServerUtils; 090import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 091import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 092import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 093import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 094import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 095import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 096import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 097import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; 098import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 099import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 100import ca.uhn.fhir.util.ObjectUtil; 101import ca.uhn.fhir.util.OperationOutcomeUtil; 102import ca.uhn.fhir.util.ReflectionUtil; 103import ca.uhn.fhir.util.StopWatch; 104import ca.uhn.fhir.validation.FhirValidator; 105import ca.uhn.fhir.validation.IInstanceValidatorModule; 106import ca.uhn.fhir.validation.IValidationContext; 107import ca.uhn.fhir.validation.IValidatorModule; 108import ca.uhn.fhir.validation.ValidationOptions; 109import ca.uhn.fhir.validation.ValidationResult; 110import com.google.common.annotations.VisibleForTesting; 111import org.apache.commons.lang3.Validate; 112import org.apache.commons.text.WordUtils; 113import org.hl7.fhir.instance.model.api.IBaseCoding; 114import org.hl7.fhir.instance.model.api.IBaseMetaType; 115import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 116import org.hl7.fhir.instance.model.api.IBaseParameters; 117import org.hl7.fhir.instance.model.api.IBaseResource; 118import org.hl7.fhir.instance.model.api.IIdType; 119import org.hl7.fhir.instance.model.api.IPrimitiveType; 120import org.springframework.batch.core.JobExecution; 121import org.springframework.batch.core.JobParametersInvalidException; 122import org.springframework.beans.factory.annotation.Autowired; 123import org.springframework.beans.factory.annotation.Required; 124import org.springframework.transaction.PlatformTransactionManager; 125import org.springframework.transaction.TransactionDefinition; 126import org.springframework.transaction.annotation.Propagation; 127import org.springframework.transaction.annotation.Transactional; 128import org.springframework.transaction.support.TransactionSynchronizationAdapter; 129import org.springframework.transaction.support.TransactionSynchronizationManager; 130import org.springframework.transaction.support.TransactionTemplate; 131 132import javax.annotation.Nonnull; 133import javax.annotation.Nullable; 134import javax.annotation.PostConstruct; 135import javax.persistence.NoResultException; 136import javax.persistence.TypedQuery; 137import javax.servlet.http.HttpServletResponse; 138import java.io.IOException; 139import java.util.ArrayList; 140import java.util.Collection; 141import java.util.Collections; 142import java.util.Date; 143import java.util.HashSet; 144import java.util.Iterator; 145import java.util.List; 146import java.util.Optional; 147import java.util.Set; 148import java.util.UUID; 149import java.util.function.Supplier; 150import java.util.stream.Collectors; 151 152import static org.apache.commons.lang3.StringUtils.defaultString; 153import static org.apache.commons.lang3.StringUtils.isBlank; 154import static org.apache.commons.lang3.StringUtils.isNotBlank; 155 156public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> { 157 158 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class); 159 160 @Autowired 161 protected PlatformTransactionManager myPlatformTransactionManager; 162 @Autowired(required = false) 163 protected IFulltextSearchSvc mySearchDao; 164 @Autowired 165 protected HapiTransactionService myTransactionService; 166 @Autowired 167 private MatchResourceUrlService myMatchResourceUrlService; 168 @Autowired 169 private IResourceReindexingSvc myResourceReindexingSvc; 170 @Autowired 171 private SearchBuilderFactory mySearchBuilderFactory; 172 @Autowired 173 private DaoRegistry myDaoRegistry; 174 @Autowired 175 private IRequestPartitionHelperSvc myRequestPartitionHelperService; 176 @Autowired 177 private MatchUrlService myMatchUrlService; 178 @Autowired 179 private IDeleteExpungeJobSubmitter myDeleteExpungeJobSubmitter; 180 181 private IInstanceValidatorModule myInstanceValidator; 182 private String myResourceName; 183 private Class<T> myResourceType; 184 185 @Autowired 186 private MemoryCacheService myMemoryCacheService; 187 private TransactionTemplate myTxTemplate; 188 189 @Override 190 public DaoMethodOutcome create(final T theResource) { 191 return create(theResource, null, true, new TransactionDetails(), null); 192 } 193 194 @Override 195 public DaoMethodOutcome create(final T theResource, RequestDetails theRequestDetails) { 196 return create(theResource, null, true, new TransactionDetails(), theRequestDetails); 197 } 198 199 @Override 200 public DaoMethodOutcome create(final T theResource, String theIfNoneExist) { 201 return create(theResource, theIfNoneExist, null); 202 } 203 204 @Override 205 public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) { 206 return create(theResource, theIfNoneExist, true, new TransactionDetails(), theRequestDetails); 207 } 208 209 @VisibleForTesting 210 public void setTransactionService(HapiTransactionService theTransactionService) { 211 myTransactionService = theTransactionService; 212 } 213 214 @Override 215 public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, @Nonnull TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) { 216 return myTransactionService.execute(theRequestDetails, theTransactionDetails, tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails)); 217 } 218 219 @VisibleForTesting 220 public void setRequestPartitionHelperService(IRequestPartitionHelperSvc theRequestPartitionHelperService) { 221 myRequestPartitionHelperService = theRequestPartitionHelperService; 222 } 223 224 /** 225 * Called for FHIR create (POST) operations 226 */ 227 protected DaoMethodOutcome doCreateForPost(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) { 228 if (theResource == null) { 229 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody"); 230 throw new InvalidRequestException(Msg.code(956) + msg); 231 } 232 233 if (isNotBlank(theResource.getIdElement().getIdPart())) { 234 if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 235 String message = getMessageSanitized("failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart()); 236 throw new InvalidRequestException(Msg.code(957) + message, createErrorOperationOutcome(message, "processing")); 237 } else { 238 // As of DSTU3, ID and version in the body should be ignored for a create/update 239 theResource.setId(""); 240 } 241 } 242 243 if (getConfig().getResourceServerIdStrategy() == DaoConfig.IdStrategyEnum.UUID) { 244 theResource.setId(UUID.randomUUID().toString()); 245 theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE); 246 } 247 248 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource, getResourceName()); 249 return doCreateForPostOrPut(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails, requestPartitionId); 250 } 251 252 /** 253 * Called both for FHIR create (POST) operations (via {@link #doCreateForPost(IBaseResource, String, boolean, TransactionDetails, RequestDetails)} 254 * as well as for FHIR update (PUT) where we're doing a create-with-client-assigned-ID (via {@link #doUpdate(IBaseResource, String, boolean, boolean, RequestDetails, TransactionDetails)}. 255 */ 256 private DaoMethodOutcome doCreateForPostOrPut(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 257 StopWatch w = new StopWatch(); 258 259 preProcessResourceForStorage(theResource); 260 preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing); 261 262 ResourceTable entity = new ResourceTable(); 263 entity.setResourceType(toResourceName(theResource)); 264 entity.setPartitionId(myRequestPartitionHelperService.toStoragePartition(theRequestPartitionId)); 265 entity.setCreatedByMatchUrl(theIfNoneExist); 266 entity.setVersion(1); 267 268 if (isNotBlank(theIfNoneExist)) { 269 Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType, theTransactionDetails, theRequest); 270 if (match.size() > 1) { 271 String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size()); 272 throw new PreconditionFailedException(Msg.code(958) + msg); 273 } else if (match.size() == 1) { 274 ResourcePersistentId pid = match.iterator().next(); 275 276 Supplier<LazyDaoMethodOutcome.EntityAndResource> entitySupplier = () -> { 277 return myTxTemplate.execute(tx -> { 278 ResourceTable foundEntity = myEntityManager.find(ResourceTable.class, pid.getId()); 279 IBaseResource resource = toResource(foundEntity, false); 280 theResource.setId(resource.getIdElement().getValue()); 281 return new LazyDaoMethodOutcome.EntityAndResource(foundEntity, resource); 282 }); 283 }; 284 285 Supplier<IIdType> idSupplier = () -> { 286 return myTxTemplate.execute(tx -> { 287 IIdType retVal = myIdHelperService.translatePidIdToForcedId(myFhirContext, myResourceName, pid); 288 if (!retVal.hasVersionIdPart()) { 289 IIdType idWithVersion = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid.getIdAsLong()); 290 if (idWithVersion == null) { 291 Long version = myResourceTableDao.findCurrentVersionByPid(pid.getIdAsLong()); 292 if (version != null) { 293 retVal = myFhirContext.getVersion().newIdType().setParts(retVal.getBaseUrl(), retVal.getResourceType(), retVal.getIdPart(), Long.toString(version)); 294 myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid.getIdAsLong(), retVal); 295 } 296 } else { 297 retVal = idWithVersion; 298 } 299 } 300 return retVal; 301 }); 302 }; 303 304 return toMethodOutcomeLazy(theRequest, pid, entitySupplier, idSupplier).setCreated(false).setNop(true); 305 } 306 } 307 308 // Notify interceptors 309 if (theRequest != null) { 310 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), theResource); 311 notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails); 312 } 313 314 String resourceIdBeforeStorage = theResource.getIdElement().getIdPart(); 315 boolean resourceHadIdBeforeStorage = isNotBlank(resourceIdBeforeStorage); 316 boolean resourceIdWasServerAssigned = theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED) == Boolean.TRUE; 317 318 HookParams hookParams; 319 320 // Notify interceptor for accepting/rejecting client assigned ids 321 if (!resourceIdWasServerAssigned && resourceHadIdBeforeStorage) { 322 hookParams = new HookParams() 323 .add(IBaseResource.class, theResource) 324 .add(RequestDetails.class, theRequest); 325 doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_CLIENT_ASSIGNED_ID, hookParams); 326 } 327 328 // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED 329 hookParams = new HookParams() 330 .add(IBaseResource.class, theResource) 331 .add(RequestDetails.class, theRequest) 332 .addIfMatchesType(ServletRequestDetails.class, theRequest) 333 .add(TransactionDetails.class, theTransactionDetails); 334 doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams); 335 336 if (resourceHadIdBeforeStorage && !resourceIdWasServerAssigned) { 337 validateResourceIdCreation(theResource, theRequest); 338 } 339 340 // Perform actual DB update 341 // this call will also update the metadata 342 ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, false, theTransactionDetails, false, thePerformIndexing); 343 344 // Store the resource forced ID if necessary 345 ResourcePersistentId persistentId = new ResourcePersistentId(updatedEntity.getResourceId()); 346 if (resourceHadIdBeforeStorage) { 347 if (resourceIdWasServerAssigned) { 348 boolean createForPureNumericIds = true; 349 createForcedIdIfNeeded(entity, resourceIdBeforeStorage, createForPureNumericIds); 350 } else { 351 boolean createForPureNumericIds = getConfig().getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ALPHANUMERIC; 352 createForcedIdIfNeeded(entity, resourceIdBeforeStorage, createForPureNumericIds); 353 } 354 } else { 355 switch (getConfig().getResourceClientIdStrategy()) { 356 case NOT_ALLOWED: 357 case ALPHANUMERIC: 358 break; 359 case ANY: 360 boolean createForPureNumericIds = true; 361 createForcedIdIfNeeded(updatedEntity, theResource.getIdElement().getIdPart(), createForPureNumericIds); 362 // for client ID mode ANY, we will always have a forced ID. If we ever 363 // stop populating the transient forced ID be warned that we use it 364 // (and expect it to be set correctly) farther below. 365 assert updatedEntity.getTransientForcedId() != null; 366 break; 367 } 368 } 369 370 // Populate the resource with its actual final stored ID from the entity 371 theResource.setId(entity.getIdDt()); 372 373 // Pre-cache the resource ID 374 persistentId.setAssociatedResourceId(entity.getIdType(myFhirContext)); 375 myIdHelperService.addResolvedPidToForcedId(persistentId, theRequestPartitionId, getResourceName(), entity.getTransientForcedId(), null); 376 theTransactionDetails.addResolvedResourceId(persistentId.getAssociatedResourceId(), persistentId); 377 378 // Pre-cache the match URL 379 if (theIfNoneExist != null) { 380 myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theIfNoneExist, persistentId); 381 } 382 383 // Update the version/last updated in the resource so that interceptors get 384 // the correct version 385 // TODO - the above updateEntity calls updateResourceMetadata 386 // Maybe we don't need this call here? 387 updateResourceMetadata(entity, theResource); 388 389 // Populate the PID in the resource so it is available to hooks 390 addPidToResource(entity, theResource); 391 392 // Notify JPA interceptors 393 if (!updatedEntity.isUnchangedInCurrentOperation()) { 394 hookParams = new HookParams() 395 .add(IBaseResource.class, theResource) 396 .add(RequestDetails.class, theRequest) 397 .addIfMatchesType(ServletRequestDetails.class, theRequest) 398 .add(TransactionDetails.class, theTransactionDetails) 399 .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 400 doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams); 401 } 402 403 DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource).setCreated(true); 404 if (!thePerformIndexing) { 405 outcome.setId(theResource.getIdElement()); 406 } 407 408 String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart()); 409 outcome.setOperationOutcome(createInfoOperationOutcome(msg)); 410 411 ourLog.debug(msg); 412 return outcome; 413 } 414 415 private void createForcedIdIfNeeded(ResourceTable theEntity, String theResourceId, boolean theCreateForPureNumericIds) { 416 if (isNotBlank(theResourceId) && theEntity.getForcedId() == null) { 417 if (theCreateForPureNumericIds || !IdHelperService.isValidPid(theResourceId)) { 418 ForcedId forcedId = new ForcedId(); 419 forcedId.setResourceType(theEntity.getResourceType()); 420 forcedId.setForcedId(theResourceId); 421 forcedId.setResource(theEntity); 422 forcedId.setPartitionId(theEntity.getPartitionId()); 423 424 /* 425 * As of Hibernate 5.6.2, assigning the forced ID to the 426 * resource table causes an extra update to happen, even 427 * though the ResourceTable entity isn't actually changed 428 * (there is a @OneToOne reference on ResourceTable to the 429 * ForcedId table, but the actual column is on the ForcedId 430 * table so it doesn't actually make sense to update the table 431 * when this is set). But to work around that we avoid 432 * actually assigning ResourceTable#myForcedId here. 433 * 434 * It's conceivable they may fix this in the future, or 435 * they may not. 436 * 437 * If you want to try assigning the forced it to the resource 438 * entity (by calling ResourceTable#setForcedId) try running 439 * the tests FhirResourceDaoR4QueryCountTest to verify that 440 * nothing has broken as a result. 441 * JA 20220121 442 */ 443 theEntity.setTransientForcedId(forcedId.getForcedId()); 444 myForcedIdDao.save(forcedId); 445 } 446 } 447 } 448 449 void validateResourceIdCreation(T theResource, RequestDetails theRequest) { 450 DaoConfig.ClientIdStrategyEnum strategy = getConfig().getResourceClientIdStrategy(); 451 452 if (strategy == DaoConfig.ClientIdStrategyEnum.NOT_ALLOWED) { 453 if (!isSystemRequest(theRequest)) { 454 throw new ResourceNotFoundException(Msg.code(959) + getMessageSanitized("failedToCreateWithClientAssignedIdNotAllowed", theResource.getIdElement().getIdPart())); 455 } 456 } 457 458 if (strategy == DaoConfig.ClientIdStrategyEnum.ALPHANUMERIC) { 459 if (theResource.getIdElement().isIdPartValidLong()) { 460 throw new InvalidRequestException(Msg.code(960) + getMessageSanitized("failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart())); 461 } 462 } 463 } 464 465 protected String getMessageSanitized(String theKey, String theIdPart) { 466 return getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, theKey, theIdPart); 467 } 468 469 private boolean isSystemRequest(RequestDetails theRequest) { 470 return theRequest instanceof SystemRequestDetails; 471 } 472 473 private IInstanceValidatorModule getInstanceValidator() { 474 return myInstanceValidator; 475 } 476 477 @Override 478 public DaoMethodOutcome delete(IIdType theId) { 479 return delete(theId, null); 480 } 481 482 @Override 483 public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) { 484 TransactionDetails transactionDetails = new TransactionDetails(); 485 486 validateIdPresentForDelete(theId); 487 validateDeleteEnabled(); 488 489 return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> { 490 DeleteConflictList deleteConflicts = new DeleteConflictList(); 491 if (isNotBlank(theId.getValue())) { 492 deleteConflicts.setResourceIdMarkedForDeletion(theId); 493 } 494 495 StopWatch w = new StopWatch(); 496 497 DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails, transactionDetails); 498 499 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); 500 501 ourLog.debug("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); 502 return retVal; 503 }); 504 } 505 506 /** 507 * Creates a base method outcome for a delete request for the provided ID. 508 * <p> 509 * Additional information may be set on the outcome. 510 * 511 * @param theId - the id of the object being deleted. Eg: Patient/123 512 */ 513 private DaoMethodOutcome createMethodOutcomeForDelete(String theId) { 514 DaoMethodOutcome outcome = new DaoMethodOutcome(); 515 516 IIdType id = getContext().getVersion().newIdType(); 517 id.setValue(theId); 518 outcome.setId(id); 519 520 IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); 521 String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, "successfulDeletes", 1, 0); 522 String severity = "information"; 523 String code = "informational"; 524 OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); 525 outcome.setOperationOutcome(oo); 526 527 return outcome; 528 } 529 530 @Override 531 public DaoMethodOutcome delete(IIdType theId, 532 DeleteConflictList theDeleteConflicts, 533 RequestDetails theRequestDetails, 534 @Nonnull TransactionDetails theTransactionDetails) { 535 validateIdPresentForDelete(theId); 536 validateDeleteEnabled(); 537 538 final ResourceTable entity; 539 try { 540 entity = readEntityLatestVersion(theId, theRequestDetails, theTransactionDetails); 541 } catch (ResourceNotFoundException ex) { 542 // we don't want to throw 404s. 543 // if not found, return an outcome anyways. 544 // Because no object actually existed, we'll 545 // just set the id and nothing else 546 DaoMethodOutcome outcome = createMethodOutcomeForDelete(theId.getValue()); 547 return outcome; 548 } 549 550 if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) { 551 throw new ResourceVersionConflictException(Msg.code(961) + "Trying to delete " + theId + " but this is not the current version"); 552 } 553 554 // Don't delete again if it's already deleted 555 if (entity.getDeleted() != null) { 556 DaoMethodOutcome outcome = createMethodOutcomeForDelete(entity.getIdDt().getValue()); 557 558 // used to exist, so we'll set the persistent id 559 outcome.setPersistentId(new ResourcePersistentId(entity.getResourceId())); 560 outcome.setEntity(entity); 561 562 return outcome; 563 } 564 565 StopWatch w = new StopWatch(); 566 567 T resourceToDelete = toResource(myResourceType, entity, null, false); 568 theDeleteConflicts.setResourceIdMarkedForDeletion(theId); 569 570 // Notify IServerOperationInterceptors about pre-action call 571 HookParams hook = new HookParams() 572 .add(IBaseResource.class, resourceToDelete) 573 .add(RequestDetails.class, theRequestDetails) 574 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 575 .add(TransactionDetails.class, theTransactionDetails); 576 doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook); 577 578 myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequestDetails, theTransactionDetails); 579 580 preDelete(resourceToDelete, entity); 581 582 // Notify interceptors 583 if (theRequestDetails != null) { 584 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getContext(), theId.getResourceType(), theId); 585 notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); 586 } 587 588 ResourceTable savedEntity = updateEntityForDelete(theRequestDetails, theTransactionDetails, entity); 589 resourceToDelete.setId(entity.getIdDt()); 590 591 // Notify JPA interceptors 592 HookParams hookParams = new HookParams() 593 .add(IBaseResource.class, resourceToDelete) 594 .add(RequestDetails.class, theRequestDetails) 595 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 596 .add(TransactionDetails.class, theTransactionDetails) 597 .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)); 598 599 600 doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); 601 602 DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, savedEntity, resourceToDelete).setCreated(true); 603 604 IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); 605 String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, "successfulDeletes", 1, w.getMillis()); 606 String severity = "information"; 607 String code = "informational"; 608 OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); 609 outcome.setOperationOutcome(oo); 610 611 return outcome; 612 } 613 614 @Override 615 public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequest) { 616 validateDeleteEnabled(); 617 618 TransactionDetails transactionDetails = new TransactionDetails(); 619 ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl); 620 621 if (resourceSearch.isDeleteExpunge()) { 622 return deleteExpunge(theUrl, theRequest); 623 } 624 625 return myTransactionService.execute(theRequest, transactionDetails, tx -> { 626 DeleteConflictList deleteConflicts = new DeleteConflictList(); 627 DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest); 628 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); 629 return outcome; 630 }); 631 } 632 633 /** 634 * This method gets called by {@link #deleteByUrl(String, RequestDetails)} as well as by 635 * transaction processors 636 */ 637 @Override 638 public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequestDetails) { 639 validateDeleteEnabled(); 640 TransactionDetails transactionDetails = new TransactionDetails(); 641 642 return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> doDeleteByUrl(theUrl, deleteConflicts, theRequestDetails)); 643 } 644 645 @Nonnull 646 private DeleteMethodOutcome doDeleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequest) { 647 ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl); 648 SearchParameterMap paramMap = resourceSearch.getSearchParameterMap(); 649 paramMap.setLoadSynchronous(true); 650 651 Set<ResourcePersistentId> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequest, null); 652 653 if (resourceIds.size() > 1) { 654 if (!getConfig().isAllowMultipleDelete()) { 655 throw new PreconditionFailedException(Msg.code(962) + getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resourceIds.size())); 656 } 657 } 658 659 return deletePidList(theUrl, resourceIds, deleteConflicts, theRequest); 660 } 661 662 private DeleteMethodOutcome deleteExpunge(String theUrl, RequestDetails theRequest) { 663 if (!getConfig().canDeleteExpunge()) { 664 throw new MethodNotAllowedException(Msg.code(963) + "_expunge is not enabled on this server: " + getConfig().cannotDeleteExpungeReason()); 665 } 666 667 if (theUrl.contains(Constants.PARAMETER_CASCADE_DELETE) || (theRequest.getHeader(Constants.HEADER_CASCADE) != null && theRequest.getHeader(Constants.HEADER_CASCADE).equals(Constants.CASCADE_DELETE))) { 668 throw new InvalidRequestException(Msg.code(964) + "_expunge cannot be used with _cascade"); 669 } 670 671 List<String> urlsToDeleteExpunge = Collections.singletonList(theUrl); 672 try { 673 JobExecution jobExecution = myDeleteExpungeJobSubmitter.submitJob(getConfig().getExpungeBatchSize(), urlsToDeleteExpunge, theRequest); 674 return new DeleteMethodOutcome(createInfoOperationOutcome("Delete job submitted with id " + jobExecution.getId())); 675 } catch (JobParametersInvalidException e) { 676 throw new InvalidRequestException(Msg.code(965) + "Invalid Delete Expunge Request: " + e.getMessage(), e); 677 } 678 } 679 680 @Nonnull 681 @Override 682 public DeleteMethodOutcome deletePidList(String theUrl, Collection<ResourcePersistentId> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest) { 683 StopWatch w = new StopWatch(); 684 TransactionDetails transactionDetails = new TransactionDetails(); 685 List<ResourceTable> deletedResources = new ArrayList<>(); 686 for (ResourcePersistentId pid : theResourceIds) { 687 ResourceTable entity = myEntityManager.find(ResourceTable.class, pid.getId()); 688 deletedResources.add(entity); 689 690 T resourceToDelete = toResource(myResourceType, entity, null, false); 691 692 // Notify IServerOperationInterceptors about pre-action call 693 HookParams hooks = new HookParams() 694 .add(IBaseResource.class, resourceToDelete) 695 .add(RequestDetails.class, theRequest) 696 .addIfMatchesType(ServletRequestDetails.class, theRequest) 697 .add(TransactionDetails.class, transactionDetails); 698 doCallHooks(transactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks); 699 700 myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequest, transactionDetails); 701 702 // Notify interceptors 703 IdDt idToDelete = entity.getIdDt(); 704 if (theRequest != null) { 705 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete); 706 notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails); 707 } 708 709 // Perform delete 710 711 updateEntityForDelete(theRequest, transactionDetails, entity); 712 resourceToDelete.setId(entity.getIdDt()); 713 714 // Notify JPA interceptors 715 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { 716 @Override 717 public void beforeCommit(boolean readOnly) { 718 HookParams hookParams = new HookParams() 719 .add(IBaseResource.class, resourceToDelete) 720 .add(RequestDetails.class, theRequest) 721 .addIfMatchesType(ServletRequestDetails.class, theRequest) 722 .add(TransactionDetails.class, transactionDetails) 723 .add(InterceptorInvocationTimingEnum.class, transactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)); 724 doCallHooks(transactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); 725 } 726 }); 727 } 728 729 IBaseOperationOutcome oo; 730 if (deletedResources.isEmpty()) { 731 oo = OperationOutcomeUtil.newInstance(getContext()); 732 String message = getMessageSanitized("unableToDeleteNotFound", theUrl); 733 String severity = "warning"; 734 String code = "not-found"; 735 OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); 736 } else { 737 oo = OperationOutcomeUtil.newInstance(getContext()); 738 String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, "successfulDeletes", deletedResources.size(), w.getMillis()); 739 String severity = "information"; 740 String code = "informational"; 741 OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code); 742 } 743 744 ourLog.debug("Processed delete on {} (matched {} resource(s)) in {}ms", theUrl, deletedResources.size(), w.getMillis()); 745 746 DeleteMethodOutcome retVal = new DeleteMethodOutcome(); 747 retVal.setDeletedEntities(deletedResources); 748 retVal.setOperationOutcome(oo); 749 return retVal; 750 } 751 752 private void validateDeleteEnabled() { 753 if (!getConfig().isDeleteEnabled()) { 754 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "deleteBlockedBecauseDisabled"); 755 throw new PreconditionFailedException(Msg.code(966) + msg); 756 } 757 } 758 759 private void validateIdPresentForDelete(IIdType theId) { 760 if (theId == null || !theId.hasIdPart()) { 761 throw new InvalidRequestException(Msg.code(967) + "Can not perform delete, no ID provided"); 762 } 763 } 764 765 @PostConstruct 766 public void detectSearchDaoDisabled() { 767 if (mySearchDao != null && mySearchDao.isDisabled()) { 768 mySearchDao = null; 769 } 770 } 771 772 private <MT extends IBaseMetaType> void doMetaAdd(MT theMetaAdd, BaseHasResource theEntity, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 773 IBaseResource oldVersion = toResource(theEntity, false); 774 775 List<TagDefinition> tags = toTagList(theMetaAdd); 776 for (TagDefinition nextDef : tags) { 777 778 boolean hasTag = false; 779 for (BaseTag next : new ArrayList<>(theEntity.getTags())) { 780 if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) && 781 ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) && 782 ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) { 783 hasTag = true; 784 break; 785 } 786 } 787 788 if (!hasTag) { 789 theEntity.setHasTags(true); 790 791 TagDefinition def = getTagOrNull(theTransactionDetails, nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay()); 792 if (def != null) { 793 BaseTag newEntity = theEntity.addTag(def); 794 if (newEntity.getTagId() == null) { 795 myEntityManager.persist(newEntity); 796 } 797 } 798 } 799 } 800 801 validateMetaCount(theEntity.getTags().size()); 802 803 myEntityManager.merge(theEntity); 804 805 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 806 IBaseResource newVersion = toResource(theEntity, false); 807 HookParams preStorageParams = new HookParams() 808 .add(IBaseResource.class, oldVersion) 809 .add(IBaseResource.class, newVersion) 810 .add(RequestDetails.class, theRequestDetails) 811 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 812 .add(TransactionDetails.class, theTransactionDetails); 813 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); 814 815 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 816 HookParams preCommitParams = new HookParams() 817 .add(IBaseResource.class, oldVersion) 818 .add(IBaseResource.class, newVersion) 819 .add(RequestDetails.class, theRequestDetails) 820 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 821 .add(TransactionDetails.class, theTransactionDetails) 822 .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)); 823 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); 824 825 } 826 827 private <MT extends IBaseMetaType> void doMetaDelete(MT theMetaDel, BaseHasResource theEntity, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 828 829 // wipmb mb update hibernate search index if we are storing resources - it assumes inline tags. 830 IBaseResource oldVersion = toResource(theEntity, false); 831 832 List<TagDefinition> tags = toTagList(theMetaDel); 833 834 for (TagDefinition nextDef : tags) { 835 for (BaseTag next : new ArrayList<BaseTag>(theEntity.getTags())) { 836 if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) && 837 ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) && 838 ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) { 839 myEntityManager.remove(next); 840 theEntity.getTags().remove(next); 841 } 842 } 843 } 844 845 if (theEntity.getTags().isEmpty()) { 846 theEntity.setHasTags(false); 847 } 848 849 theEntity = myEntityManager.merge(theEntity); 850 851 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 852 IBaseResource newVersion = toResource(theEntity, false); 853 HookParams preStorageParams = new HookParams() 854 .add(IBaseResource.class, oldVersion) 855 .add(IBaseResource.class, newVersion) 856 .add(RequestDetails.class, theRequestDetails) 857 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 858 .add(TransactionDetails.class, theTransactionDetails); 859 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); 860 861 HookParams preCommitParams = new HookParams() 862 .add(IBaseResource.class, oldVersion) 863 .add(IBaseResource.class, newVersion) 864 .add(RequestDetails.class, theRequestDetails) 865 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 866 .add(TransactionDetails.class, theTransactionDetails) 867 .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)); 868 869 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); 870 871 } 872 873 private void validateExpungeEnabled() { 874 if (!getConfig().isExpungeEnabled()) { 875 throw new MethodNotAllowedException(Msg.code(968) + "$expunge is not enabled on this server"); 876 } 877 } 878 879 @Override 880 @Transactional(propagation = Propagation.NEVER) 881 public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { 882 validateExpungeEnabled(); 883 return forceExpungeInExistingTransaction(theId, theExpungeOptions, theRequest); 884 } 885 886 @Override 887 public ExpungeOutcome forceExpungeInExistingTransaction(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { 888 TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); 889 890 BaseHasResource entity = txTemplate.execute(t -> readEntity(theId, theRequest)); 891 Validate.notNull(entity, "Resource with ID %s not found in database", theId); 892 893 if (theId.hasVersionIdPart()) { 894 BaseHasResource currentVersion; 895 currentVersion = txTemplate.execute(t -> readEntity(theId.toVersionless(), theRequest)); 896 Validate.notNull(currentVersion, "Current version of resource with ID %s not found in database", theId.toVersionless()); 897 898 if (entity.getVersion() == currentVersion.getVersion()) { 899 throw new PreconditionFailedException(Msg.code(969) + "Can not perform version-specific expunge of resource " + theId.toUnqualified().getValue() + " as this is the current version"); 900 } 901 902 return myExpungeService.expunge(getResourceName(), new ResourcePersistentId(entity.getResourceId(), entity.getVersion()), theExpungeOptions, theRequest); 903 } 904 905 return myExpungeService.expunge(getResourceName(), new ResourcePersistentId(entity.getResourceId()), theExpungeOptions, theRequest); 906 } 907 908 @Override 909 @Transactional(propagation = Propagation.NEVER) 910 public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) { 911 ourLog.info("Beginning TYPE[{}] expunge operation", getResourceName()); 912 913 return myExpungeService.expunge(getResourceName(), null, theExpungeOptions, theRequestDetails); 914 } 915 916 @Override 917 public String getResourceName() { 918 return myResourceName; 919 } 920 921 @Override 922 public Class<T> getResourceType() { 923 return myResourceType; 924 } 925 926 @SuppressWarnings("unchecked") 927 @Required 928 public void setResourceType(Class<? extends IBaseResource> theTableType) { 929 myResourceType = (Class<T>) theTableType; 930 } 931 932 @Override 933 @Transactional 934 public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) { 935 // Notify interceptors 936 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails); 937 notifyInterceptors(RestOperationTypeEnum.HISTORY_TYPE, requestDetails); 938 939 StopWatch w = new StopWatch(); 940 IBundleProvider retVal = super.history(theRequestDetails, myResourceName, null, theSince, theUntil, theOffset); 941 ourLog.debug("Processed history on {} in {}ms", myResourceName, w.getMillisAndRestart()); 942 return retVal; 943 } 944 945 @Override 946 @Transactional 947 public IBundleProvider history(final IIdType theId, final Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequest) { 948 if (theRequest != null) { 949 // Notify interceptors 950 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theId); 951 notifyInterceptors(RestOperationTypeEnum.HISTORY_INSTANCE, requestDetails); 952 } 953 954 StopWatch w = new StopWatch(); 955 956 IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless(); 957 BaseHasResource entity = readEntity(id, theRequest); 958 959 IBundleProvider retVal = super.history(theRequest, myResourceName, entity.getId(), theSince, theUntil, theOffset); 960 961 ourLog.debug("Processed history on {} in {}ms", id, w.getMillisAndRestart()); 962 return retVal; 963 } 964 965 protected boolean isPagingProviderDatabaseBacked(RequestDetails theRequestDetails) { 966 if (theRequestDetails == null || theRequestDetails.getServer() == null) { 967 return false; 968 } 969 IRestfulServerDefaults server = theRequestDetails.getServer(); 970 IPagingProvider pagingProvider = server.getPagingProvider(); 971 return pagingProvider != null; 972 } 973 974 protected void markResourcesMatchingExpressionAsNeedingReindexing(Boolean theCurrentlyReindexing, String theExpression) { 975 // Avoid endless loops 976 if (Boolean.TRUE.equals(theCurrentlyReindexing)) { 977 return; 978 } 979 980 if (getConfig().isMarkResourcesForReindexingUponSearchParameterChange()) { 981 982 String expression = defaultString(theExpression); 983 984 Set<String> typesToMark = myDaoRegistry 985 .getRegisteredDaoTypes() 986 .stream() 987 .filter(t -> WordUtils.containsAllWords(expression, t)) 988 .collect(Collectors.toSet()); 989 990 for (String resourceType : typesToMark) { 991 ourLog.debug("Marking all resources of type {} for reindexing due to updated search parameter with path: {}", resourceType, theExpression); 992 993 TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); 994 txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); 995 txTemplate.execute(t -> { 996 myResourceReindexingSvc.markAllResourcesForReindexing(resourceType); 997 return null; 998 }); 999 1000 ourLog.debug("Marked resources of type {} for reindexing", resourceType); 1001 } 1002 1003 } 1004 1005 mySearchParamRegistry.requestRefresh(); 1006 } 1007 1008 @Override 1009 @Transactional 1010 public <MT extends IBaseMetaType> MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest) { 1011 TransactionDetails transactionDetails = new TransactionDetails(); 1012 1013 // Notify interceptors 1014 if (theRequest != null) { 1015 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theResourceId); 1016 notifyInterceptors(RestOperationTypeEnum.META_ADD, requestDetails); 1017 } 1018 1019 StopWatch w = new StopWatch(); 1020 BaseHasResource entity = readEntity(theResourceId, theRequest); 1021 if (entity == null) { 1022 throw new ResourceNotFoundException(Msg.code(1993) + theResourceId); 1023 } 1024 1025 ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest, transactionDetails); 1026 if (latestVersion.getVersion() != entity.getVersion()) { 1027 doMetaAdd(theMetaAdd, entity, theRequest, transactionDetails); 1028 } else { 1029 doMetaAdd(theMetaAdd, latestVersion, theRequest, transactionDetails); 1030 1031 // Also update history entry 1032 ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(entity.getId(), entity.getVersion()); 1033 doMetaAdd(theMetaAdd, history, theRequest, transactionDetails); 1034 } 1035 1036 ourLog.debug("Processed metaAddOperation on {} in {}ms", theResourceId, w.getMillisAndRestart()); 1037 1038 @SuppressWarnings("unchecked") 1039 MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequest); 1040 return retVal; 1041 } 1042 1043 @Override 1044 @Transactional 1045 public <MT extends IBaseMetaType> MT metaDeleteOperation(IIdType theResourceId, MT theMetaDel, RequestDetails theRequest) { 1046 TransactionDetails transactionDetails = new TransactionDetails(); 1047 1048 // Notify interceptors 1049 if (theRequest != null) { 1050 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theResourceId); 1051 notifyInterceptors(RestOperationTypeEnum.META_DELETE, requestDetails); 1052 } 1053 1054 StopWatch w = new StopWatch(); 1055 BaseHasResource entity = readEntity(theResourceId, theRequest); 1056 if (entity == null) { 1057 throw new ResourceNotFoundException(Msg.code(1994) + theResourceId); 1058 } 1059 1060 ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest, transactionDetails); 1061 if (latestVersion.getVersion() != entity.getVersion()) { 1062 doMetaDelete(theMetaDel, entity, theRequest, transactionDetails); 1063 } else { 1064 doMetaDelete(theMetaDel, latestVersion, theRequest, transactionDetails); 1065 1066 // Also update history entry 1067 ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(entity.getId(), entity.getVersion()); 1068 doMetaDelete(theMetaDel, history, theRequest, transactionDetails); 1069 } 1070 1071 ourLog.debug("Processed metaDeleteOperation on {} in {}ms", theResourceId.getValue(), w.getMillisAndRestart()); 1072 1073 @SuppressWarnings("unchecked") 1074 MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequest); 1075 return retVal; 1076 } 1077 1078 @Override 1079 @Transactional 1080 public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequest) { 1081 // Notify interceptors 1082 if (theRequest != null) { 1083 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theId); 1084 notifyInterceptors(RestOperationTypeEnum.META, requestDetails); 1085 } 1086 1087 Set<TagDefinition> tagDefs = new HashSet<>(); 1088 BaseHasResource entity = readEntity(theId, theRequest); 1089 for (BaseTag next : entity.getTags()) { 1090 tagDefs.add(next.getTag()); 1091 } 1092 MT retVal = toMetaDt(theType, tagDefs); 1093 1094 retVal.setLastUpdated(entity.getUpdatedDate()); 1095 retVal.setVersionId(Long.toString(entity.getVersion())); 1096 1097 return retVal; 1098 } 1099 1100 @Override 1101 @Transactional 1102 public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) { 1103 // Notify interceptors 1104 if (theRequestDetails != null) { 1105 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, getResourceName(), null); 1106 notifyInterceptors(RestOperationTypeEnum.META, requestDetails); 1107 } 1108 1109 String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)"; 1110 TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class); 1111 q.setParameter("res_type", myResourceName); 1112 List<TagDefinition> tagDefinitions = q.getResultList(); 1113 1114 return toMetaDt(theType, tagDefinitions); 1115 } 1116 1117 @Override 1118 public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest) { 1119 TransactionDetails transactionDetails = new TransactionDetails(); 1120 return myTransactionService.execute(theRequest, transactionDetails, tx -> doPatch(theId, theConditionalUrl, thePatchType, thePatchBody, theFhirPatchBody, theRequest, transactionDetails)); 1121 } 1122 1123 private DaoMethodOutcome doPatch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest, TransactionDetails theTransactionDetails) { 1124 ResourceTable entityToUpdate; 1125 if (isNotBlank(theConditionalUrl)) { 1126 1127 Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theConditionalUrl, myResourceType, theTransactionDetails, theRequest); 1128 if (match.size() > 1) { 1129 String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "PATCH", theConditionalUrl, match.size()); 1130 throw new PreconditionFailedException(Msg.code(972) + msg); 1131 } else if (match.size() == 1) { 1132 ResourcePersistentId pid = match.iterator().next(); 1133 entityToUpdate = myEntityManager.find(ResourceTable.class, pid.getId()); 1134 } else { 1135 String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "invalidMatchUrlNoMatches", theConditionalUrl); 1136 throw new ResourceNotFoundException(Msg.code(973) + msg); 1137 } 1138 1139 } else { 1140 entityToUpdate = readEntityLatestVersion(theId, theRequest, theTransactionDetails); 1141 if (theId.hasVersionIdPart()) { 1142 if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) { 1143 throw new ResourceVersionConflictException(Msg.code(974) + "Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch"); 1144 } 1145 } 1146 } 1147 1148 validateResourceType(entityToUpdate); 1149 1150 IBaseResource resourceToUpdate = toResource(entityToUpdate, false); 1151 IBaseResource destination; 1152 switch (thePatchType) { 1153 case JSON_PATCH: 1154 destination = JsonPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody); 1155 break; 1156 case XML_PATCH: 1157 destination = XmlPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody); 1158 break; 1159 case FHIR_PATCH_XML: 1160 case FHIR_PATCH_JSON: 1161 default: 1162 IBaseParameters fhirPatchJson = theFhirPatchBody; 1163 new FhirPatch(getContext()).apply(resourceToUpdate, fhirPatchJson); 1164 destination = resourceToUpdate; 1165 break; 1166 } 1167 1168 @SuppressWarnings("unchecked") 1169 T destinationCasted = (T) destination; 1170 return update(destinationCasted, null, true, theRequest); 1171 } 1172 1173 @PostConstruct 1174 @Override 1175 public void start() { 1176 assert getConfig() != null; 1177 1178 ourLog.debug("Starting resource DAO for type: {}", getResourceName()); 1179 myInstanceValidator = getApplicationContext().getBean(IInstanceValidatorModule.class); 1180 myTxTemplate = new TransactionTemplate(myPlatformTransactionManager); 1181 super.start(); 1182 } 1183 1184 @PostConstruct 1185 public void postConstruct() { 1186 RuntimeResourceDefinition def = getContext().getResourceDefinition(myResourceType); 1187 myResourceName = def.getName(); 1188 } 1189 1190 /** 1191 * Subclasses may override to provide behaviour. Invoked within a delete 1192 * transaction with the resource that is about to be deleted. 1193 */ 1194 protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete) { 1195 // nothing by default 1196 } 1197 1198 @Override 1199 @Transactional 1200 public T readByPid(ResourcePersistentId thePid) { 1201 return readByPid(thePid, false); 1202 } 1203 1204 @Override 1205 @Transactional 1206 public T readByPid(ResourcePersistentId thePid, boolean theDeletedOk) { 1207 StopWatch w = new StopWatch(); 1208 1209 Optional<ResourceTable> entity = myResourceTableDao.findById(thePid.getIdAsLong()); 1210 if (!entity.isPresent()) { 1211 throw new ResourceNotFoundException(Msg.code(975) + "No resource found with PID " + thePid); 1212 } 1213 if (entity.get().getDeleted() != null && !theDeletedOk) { 1214 throw createResourceGoneException(entity.get()); 1215 } 1216 1217 T retVal = toResource(myResourceType, entity.get(), null, false); 1218 1219 ourLog.debug("Processed read on {} in {}ms", thePid, w.getMillis()); 1220 return retVal; 1221 } 1222 1223 @Override 1224 public T read(IIdType theId) { 1225 return read(theId, null); 1226 } 1227 1228 @Override 1229 public T read(IIdType theId, RequestDetails theRequestDetails) { 1230 return read(theId, theRequestDetails, false); 1231 } 1232 1233 @Override 1234 public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) { 1235 validateResourceTypeAndThrowInvalidRequestException(theId); 1236 TransactionDetails transactionDetails = new TransactionDetails(); 1237 1238 return myTransactionService.execute(theRequest, transactionDetails, tx -> doRead(theId, theRequest, theDeletedOk)); 1239 } 1240 1241 public T doRead(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) { 1242 assert TransactionSynchronizationManager.isActualTransactionActive(); 1243 1244 // Notify interceptors 1245 if (theRequest != null && theRequest.getServer() != null) { 1246 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theId); 1247 RestOperationTypeEnum operationType = theId.hasVersionIdPart() ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ; 1248 notifyInterceptors(operationType, requestDetails); 1249 } 1250 1251 StopWatch w = new StopWatch(); 1252 BaseHasResource entity = readEntity(theId, theRequest); 1253 validateResourceType(entity); 1254 1255 T retVal = toResource(myResourceType, entity, null, false); 1256 1257 if (theDeletedOk == false) { 1258 if (entity.getDeleted() != null) { 1259 throw createResourceGoneException(entity); 1260 } 1261 } 1262 1263 // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES 1264 { 1265 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(retVal); 1266 HookParams params = new HookParams() 1267 .add(IPreResourceAccessDetails.class, accessDetails) 1268 .add(RequestDetails.class, theRequest) 1269 .addIfMatchesType(ServletRequestDetails.class, theRequest); 1270 CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); 1271 if (accessDetails.isDontReturnResourceAtIndex(0)) { 1272 throw new ResourceNotFoundException(Msg.code(1995) + "Resource " + theId + " is not known"); 1273 } 1274 } 1275 1276 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 1277 { 1278 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal); 1279 HookParams params = new HookParams() 1280 .add(IPreResourceShowDetails.class, showDetails) 1281 .add(RequestDetails.class, theRequest) 1282 .addIfMatchesType(ServletRequestDetails.class, theRequest); 1283 CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); 1284 //noinspection unchecked 1285 retVal = (T) showDetails.getResource(0); 1286 } 1287 1288 ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); 1289 return retVal; 1290 } 1291 1292 @Override 1293 @Transactional 1294 public BaseHasResource readEntity(IIdType theId, RequestDetails theRequest) { 1295 return readEntity(theId, true, theRequest); 1296 } 1297 1298 @Override 1299 @Transactional 1300 public String getCurrentVersionId(IIdType theReferenceElement) { 1301 return Long.toString(readEntity(theReferenceElement.toVersionless(), null).getVersion()); 1302 } 1303 1304 @SuppressWarnings("unchecked") 1305 @Override 1306 public void reindex(ResourcePersistentId theResourcePersistentId, RequestDetails theRequest, TransactionDetails theTransactionDetails) { 1307 Optional<ResourceTable> entityOpt = myResourceTableDao.findById(theResourcePersistentId.getIdAsLong()); 1308 if (!entityOpt.isPresent()) { 1309 ourLog.warn("Unable to find entity with PID: {}", theResourcePersistentId.getId()); 1310 return; 1311 } 1312 1313 ResourceTable entity = entityOpt.get(); 1314 try { 1315 T resource = (T) toResource(entity, false); 1316 reindex(resource, entity); 1317 } catch (BaseServerResponseException | DataFormatException e) { 1318 myResourceTableDao.updateIndexStatus(entity.getId(), INDEX_STATUS_INDEXING_FAILED); 1319 throw e; 1320 } 1321 } 1322 1323 @Override 1324 @Transactional 1325 public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) { 1326 validateResourceTypeAndThrowInvalidRequestException(theId); 1327 1328 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequest, getResourceName(), theId); 1329 1330 BaseHasResource entity; 1331 ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(requestPartitionId, getResourceName(), theId.getIdPart()); 1332 Set<Integer> readPartitions = null; 1333 if (requestPartitionId.isAllPartitions()) { 1334 entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong()); 1335 } else { 1336 readPartitions = myRequestPartitionHelperService.toReadPartitions(requestPartitionId); 1337 if (readPartitions.size() == 1) { 1338 if (readPartitions.contains(null)) { 1339 entity = myResourceTableDao.readByPartitionIdNull(pid.getIdAsLong()).orElse(null); 1340 } else { 1341 entity = myResourceTableDao.readByPartitionId(readPartitions.iterator().next(), pid.getIdAsLong()).orElse(null); 1342 } 1343 } else { 1344 if (readPartitions.contains(null)) { 1345 List<Integer> readPartitionsWithoutNull = readPartitions.stream().filter(t -> t != null).collect(Collectors.toList()); 1346 entity = myResourceTableDao.readByPartitionIdsOrNull(readPartitionsWithoutNull, pid.getIdAsLong()).orElse(null); 1347 } else { 1348 entity = myResourceTableDao.readByPartitionIds(readPartitions, pid.getIdAsLong()).orElse(null); 1349 } 1350 } 1351 } 1352 1353 // Verify that the resource is for the correct partition 1354 if (entity != null && readPartitions != null && entity.getPartitionId() != null) { 1355 if (!readPartitions.contains(entity.getPartitionId().getPartitionId())) { 1356 ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId()); 1357 entity = null; 1358 } 1359 } 1360 1361 if (entity == null) { 1362 throw new ResourceNotFoundException(Msg.code(1996) + "Resource " + theId + " is not known"); 1363 } 1364 1365 if (theId.hasVersionIdPart()) { 1366 if (theId.isVersionIdPartValidLong() == false) { 1367 throw new ResourceNotFoundException(Msg.code(978) + getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless())); 1368 } 1369 if (entity.getVersion() != theId.getVersionIdPartAsLong()) { 1370 entity = null; 1371 } 1372 } 1373 1374 if (entity == null) { 1375 if (theId.hasVersionIdPart()) { 1376 TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class); 1377 q.setParameter("RID", pid.getId()); 1378 q.setParameter("RTYP", myResourceName); 1379 q.setParameter("RVER", theId.getVersionIdPartAsLong()); 1380 try { 1381 entity = q.getSingleResult(); 1382 } catch (NoResultException e) { 1383 throw new ResourceNotFoundException(Msg.code(979) + getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless())); 1384 } 1385 } 1386 } 1387 1388 Validate.notNull(entity); 1389 validateResourceType(entity); 1390 1391 if (theCheckForForcedId) { 1392 validateGivenIdIsAppropriateToRetrieveResource(theId, entity); 1393 } 1394 return entity; 1395 } 1396 1397 @Nonnull 1398 protected ResourceTable readEntityLatestVersion(IIdType theId, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 1399 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequestDetails, getResourceName(), theId); 1400 return readEntityLatestVersion(theId, requestPartitionId, theTransactionDetails); 1401 } 1402 1403 @Nonnull 1404 private ResourceTable readEntityLatestVersion(IIdType theId, @Nonnull RequestPartitionId theRequestPartitionId, TransactionDetails theTransactionDetails) { 1405 validateResourceTypeAndThrowInvalidRequestException(theId); 1406 1407 ResourcePersistentId persistentId = null; 1408 if (theTransactionDetails != null) { 1409 if (theTransactionDetails.isResolvedResourceIdEmpty(theId.toUnqualifiedVersionless())) { 1410 throw new ResourceNotFoundException(Msg.code(1997) + theId); 1411 } 1412 if (theTransactionDetails.hasResolvedResourceIds()) { 1413 persistentId = theTransactionDetails.getResolvedResourceId(theId); 1414 } 1415 } 1416 1417 if (persistentId == null) { 1418 persistentId = myIdHelperService.resolveResourcePersistentIds(theRequestPartitionId, getResourceName(), theId.getIdPart()); 1419 } 1420 1421 ResourceTable entity = myEntityManager.find(ResourceTable.class, persistentId.getId()); 1422 if (entity == null) { 1423 throw new ResourceNotFoundException(Msg.code(1998) + theId); 1424 } 1425 validateGivenIdIsAppropriateToRetrieveResource(theId, entity); 1426 entity.setTransientForcedId(theId.getIdPart()); 1427 return entity; 1428 } 1429 1430 @Override 1431 public void reindex(T theResource, ResourceTable theEntity) { 1432 assert TransactionSynchronizationManager.isActualTransactionActive(); 1433 1434 ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getId()); 1435 if (theResource != null) { 1436 CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE); 1437 } 1438 1439 TransactionDetails transactionDetails = new TransactionDetails(theEntity.getUpdatedDate()); 1440 updateEntity(null, theResource, theEntity, theEntity.getDeleted(), true, false, transactionDetails, true, false); 1441 if (theResource != null) { 1442 CURRENTLY_REINDEXING.put(theResource, null); 1443 } 1444 } 1445 1446 @Transactional 1447 @Override 1448 public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) { 1449 removeTag(theId, theTagType, theScheme, theTerm, null); 1450 } 1451 1452 @Transactional 1453 @Override 1454 public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequest) { 1455 // Notify interceptors 1456 if (theRequest != null) { 1457 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getResourceName(), theId); 1458 notifyInterceptors(RestOperationTypeEnum.DELETE_TAGS, requestDetails); 1459 } 1460 1461 StopWatch w = new StopWatch(); 1462 BaseHasResource entity = readEntity(theId, theRequest); 1463 if (entity == null) { 1464 throw new ResourceNotFoundException(Msg.code(1999) + theId); 1465 } 1466 1467 for (BaseTag next : new ArrayList<>(entity.getTags())) { 1468 if (ObjectUtil.equals(next.getTag().getTagType(), theTagType) && 1469 ObjectUtil.equals(next.getTag().getSystem(), theScheme) && 1470 ObjectUtil.equals(next.getTag().getCode(), theTerm)) { 1471 myEntityManager.remove(next); 1472 entity.getTags().remove(next); 1473 } 1474 } 1475 1476 if (entity.getTags().isEmpty()) { 1477 entity.setHasTags(false); 1478 } 1479 1480 myEntityManager.merge(entity); 1481 1482 ourLog.debug("Processed remove tag {}/{} on {} in {}ms", theScheme, theTerm, theId.getValue(), w.getMillisAndRestart()); 1483 } 1484 1485 @Transactional(propagation = Propagation.SUPPORTS) 1486 @Override 1487 public IBundleProvider search(final SearchParameterMap theParams) { 1488 return search(theParams, null); 1489 } 1490 1491 @Transactional(propagation = Propagation.SUPPORTS) 1492 @Override 1493 public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest) { 1494 return search(theParams, theRequest, null); 1495 } 1496 1497 @Transactional(propagation = Propagation.SUPPORTS) 1498 @Override 1499 public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest, HttpServletResponse theServletResponse) { 1500 1501 if (theParams.getSearchContainedMode() == SearchContainedModeEnum.BOTH) { 1502 throw new MethodNotAllowedException(Msg.code(983) + "Contained mode 'both' is not currently supported"); 1503 } 1504 if (theParams.getSearchContainedMode() != SearchContainedModeEnum.FALSE && !myModelConfig.isIndexOnContainedResources()) { 1505 throw new MethodNotAllowedException(Msg.code(984) + "Searching with _contained mode enabled is not enabled on this server"); 1506 } 1507 1508 if (getConfig().getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) { 1509 for (List<List<IQueryParameterType>> nextAnds : theParams.values()) { 1510 for (List<? extends IQueryParameterType> nextOrs : nextAnds) { 1511 for (IQueryParameterType next : nextOrs) { 1512 if (next.getMissing() != null) { 1513 throw new MethodNotAllowedException(Msg.code(985) + ":missing modifier is disabled on this server"); 1514 } 1515 } 1516 } 1517 } 1518 } 1519 1520 translateListSearchParams(theParams); 1521 1522 notifySearchInterceptors(theParams, theRequest); 1523 1524 CacheControlDirective cacheControlDirective = new CacheControlDirective(); 1525 if (theRequest != null) { 1526 cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)); 1527 } 1528 1529 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams, null); 1530 IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective, theRequest, requestPartitionId); 1531 1532 if (retVal instanceof PersistedJpaBundleProvider) { 1533 PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) retVal; 1534 if (provider.getCacheStatus() == SearchCacheStatusEnum.HIT) { 1535 if (theServletResponse != null && theRequest != null) { 1536 String value = "HIT from " + theRequest.getFhirServerBase(); 1537 theServletResponse.addHeader(Constants.HEADER_X_CACHE, value); 1538 } 1539 } 1540 } 1541 1542 return retVal; 1543 } 1544 1545 private void translateListSearchParams(SearchParameterMap theParams) { 1546 Iterator<String> keyIterator = theParams.keySet().iterator(); 1547 1548 // Translate _list=42 to _has=List:item:_id=42 1549 while (keyIterator.hasNext()) { 1550 String key = keyIterator.next(); 1551 if (Constants.PARAM_LIST.equals((key))) { 1552 List<List<IQueryParameterType>> andOrValues = theParams.get(key); 1553 theParams.remove(key); 1554 List<List<IQueryParameterType>> hasParamValues = new ArrayList<>(); 1555 for (List<IQueryParameterType> orValues : andOrValues) { 1556 List<IQueryParameterType> orList = new ArrayList<>(); 1557 for (IQueryParameterType value : orValues) { 1558 orList.add(new HasParam("List", ListResource.SP_ITEM, ListResource.SP_RES_ID, value.getValueAsQueryToken(null))); 1559 } 1560 hasParamValues.add(orList); 1561 } 1562 theParams.put(Constants.PARAM_HAS, hasParamValues); 1563 } 1564 } 1565 } 1566 1567 private void notifySearchInterceptors(SearchParameterMap theParams, RequestDetails theRequest) { 1568 if (theRequest != null) { 1569 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, getContext(), getResourceName(), null); 1570 notifyInterceptors(RestOperationTypeEnum.SEARCH_TYPE, requestDetails); 1571 1572 if (theRequest.isSubRequest()) { 1573 Integer max = getConfig().getMaximumSearchResultCountInTransaction(); 1574 if (max != null) { 1575 Validate.inclusiveBetween(1, Integer.MAX_VALUE, max, "Maximum search result count in transaction ust be a positive integer"); 1576 theParams.setLoadSynchronousUpTo(getConfig().getMaximumSearchResultCountInTransaction()); 1577 } 1578 } 1579 1580 final Integer offset = RestfulServerUtils.extractOffsetParameter(theRequest); 1581 if (offset != null || !isPagingProviderDatabaseBacked(theRequest)) { 1582 theParams.setLoadSynchronous(true); 1583 if (offset != null) { 1584 Validate.inclusiveBetween(0, Integer.MAX_VALUE, offset, "Offset must be a positive integer"); 1585 } 1586 theParams.setOffset(offset); 1587 } 1588 1589 Integer count = RestfulServerUtils.extractCountParameter(theRequest); 1590 if (count != null) { 1591 Integer maxPageSize = theRequest.getServer().getMaximumPageSize(); 1592 if (maxPageSize != null && count > maxPageSize) { 1593 ourLog.info("Reducing {} from {} to {} which is the maximum allowable page size.", Constants.PARAM_COUNT, count, maxPageSize); 1594 count = maxPageSize; 1595 } 1596 theParams.setCount(count); 1597 } else if (theRequest.getServer().getDefaultPageSize() != null) { 1598 theParams.setCount(theRequest.getServer().getDefaultPageSize()); 1599 } 1600 } 1601 } 1602 1603 @Override 1604 public List<ResourcePersistentId> searchForIds(SearchParameterMap theParams, RequestDetails theRequest, @Nullable IBaseResource theConditionalOperationTargetOrNull) { 1605 TransactionDetails transactionDetails = new TransactionDetails(); 1606 1607 return myTransactionService.execute(theRequest, transactionDetails, tx -> { 1608 1609 if (theParams.getLoadSynchronousUpTo() != null) { 1610 theParams.setLoadSynchronousUpTo(Math.min(getConfig().getInternalSynchronousSearchSize(), theParams.getLoadSynchronousUpTo())); 1611 } else { 1612 theParams.setLoadSynchronousUpTo(getConfig().getInternalSynchronousSearchSize()); 1613 } 1614 1615 ISearchBuilder builder = mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType()); 1616 1617 List<ResourcePersistentId> ids = new ArrayList<>(); 1618 1619 String uuid = UUID.randomUUID().toString(); 1620 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams, theConditionalOperationTargetOrNull); 1621 1622 SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); 1623 try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) { 1624 while (iter.hasNext()) { 1625 ids.add(iter.next()); 1626 } 1627 } catch (IOException e) { 1628 ourLog.error("IO failure during database access", e); 1629 } 1630 1631 return ids; 1632 }); 1633 } 1634 1635 protected <MT extends IBaseMetaType> MT toMetaDt(Class<MT> theType, Collection<TagDefinition> tagDefinitions) { 1636 MT retVal = ReflectionUtil.newInstance(theType); 1637 for (TagDefinition next : tagDefinitions) { 1638 switch (next.getTagType()) { 1639 case PROFILE: 1640 retVal.addProfile(next.getCode()); 1641 break; 1642 case SECURITY_LABEL: 1643 retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay()); 1644 break; 1645 case TAG: 1646 retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay()); 1647 break; 1648 } 1649 } 1650 return retVal; 1651 } 1652 1653 private ArrayList<TagDefinition> toTagList(IBaseMetaType theMeta) { 1654 ArrayList<TagDefinition> retVal = new ArrayList<>(); 1655 1656 for (IBaseCoding next : theMeta.getTag()) { 1657 retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay())); 1658 } 1659 for (IBaseCoding next : theMeta.getSecurity()) { 1660 retVal.add(new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay())); 1661 } 1662 for (IPrimitiveType<String> next : theMeta.getProfile()) { 1663 retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseHapiFhirDao.NS_JPA_PROFILE, next.getValue(), null)); 1664 } 1665 1666 return retVal; 1667 } 1668 1669 @Override 1670 public DaoMethodOutcome update(T theResource) { 1671 return update(theResource, null, null); 1672 } 1673 1674 @Override 1675 public DaoMethodOutcome update(T theResource, RequestDetails theRequestDetails) { 1676 return update(theResource, null, theRequestDetails); 1677 } 1678 1679 @Override 1680 public DaoMethodOutcome update(T theResource, String theMatchUrl) { 1681 return update(theResource, theMatchUrl, null); 1682 } 1683 1684 @Override 1685 public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) { 1686 return update(theResource, theMatchUrl, true, theRequestDetails); 1687 } 1688 1689 @Override 1690 public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) { 1691 return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails, new TransactionDetails()); 1692 } 1693 1694 @Override 1695 public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, @Nonnull TransactionDetails theTransactionDetails) { 1696 if (theResource == null) { 1697 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody"); 1698 throw new InvalidRequestException(Msg.code(986) + msg); 1699 } 1700 if (!theResource.getIdElement().hasIdPart() && isBlank(theMatchUrl)) { 1701 String type = myFhirContext.getResourceType(theResource); 1702 String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "updateWithNoId", type); 1703 throw new InvalidRequestException(Msg.code(987) + msg); 1704 } 1705 1706 /* 1707 * Resource updates will modify/update the version of the resource with the new version. This is generally helpful, 1708 * but leads to issues if the transaction is rolled back and retried. So if we do a rollback, we reset the resource 1709 * version to what it was. 1710 */ 1711 String id = theResource.getIdElement().getValue(); 1712 Runnable onRollback = () -> theResource.getIdElement().setValue(id); 1713 1714 // Execute the update in a retryable transaction 1715 return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback); 1716 } 1717 1718 private DaoMethodOutcome doUpdate(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, TransactionDetails theTransactionDetails) { 1719 StopWatch w = new StopWatch(); 1720 1721 T resource = theResource; 1722 1723 preProcessResourceForStorage(resource); 1724 preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing); 1725 1726 ResourceTable entity = null; 1727 1728 IIdType resourceId; 1729 if (isNotBlank(theMatchUrl)) { 1730 Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType, theTransactionDetails, theRequest, theResource); 1731 if (match.size() > 1) { 1732 String msg = getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size()); 1733 throw new PreconditionFailedException(Msg.code(988) + msg); 1734 } else if (match.size() == 1) { 1735 ResourcePersistentId pid = match.iterator().next(); 1736 entity = myEntityManager.find(ResourceTable.class, pid.getId()); 1737 resourceId = entity.getIdDt(); 1738 } else { 1739 DaoMethodOutcome outcome = create(resource, null, thePerformIndexing, theTransactionDetails, theRequest); 1740 1741 // Pre-cache the match URL 1742 if (outcome.getPersistentId() != null) { 1743 myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theMatchUrl, outcome.getPersistentId()); 1744 } 1745 1746 return outcome; 1747 } 1748 } else { 1749 /* 1750 * Note: resourceId will not be null or empty here, because we 1751 * check it and reject requests in 1752 * BaseOutcomeReturningMethodBindingWithResourceParam 1753 */ 1754 resourceId = theResource.getIdElement(); 1755 assert resourceId != null; 1756 assert resourceId.hasIdPart(); 1757 1758 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName()); 1759 1760 boolean create = false; 1761 1762 if (theRequest != null) { 1763 String existenceCheck = theRequest.getHeader(JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK); 1764 if (JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK_DISABLED.equals(existenceCheck)) { 1765 create = true; 1766 } 1767 } 1768 1769 if (!create) { 1770 try { 1771 entity = readEntityLatestVersion(resourceId, requestPartitionId, theTransactionDetails); 1772 } catch (ResourceNotFoundException e) { 1773 create = true; 1774 } 1775 } 1776 1777 if (create) { 1778 return doCreateForPostOrPut(resource, null, thePerformIndexing, theTransactionDetails, theRequest, requestPartitionId); 1779 } 1780 } 1781 1782 if (resourceId.hasVersionIdPart() && Long.parseLong(resourceId.getVersionIdPart()) != entity.getVersion()) { 1783 throw new ResourceVersionConflictException(Msg.code(989) + "Trying to update " + resourceId + " but this is not the current version"); 1784 } 1785 1786 if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) { 1787 throw new UnprocessableEntityException(Msg.code(990) + "Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type[" + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]"); 1788 } 1789 1790 IBaseResource oldResource; 1791 if (getConfig().isMassIngestionMode()) { 1792 oldResource = null; 1793 } else { 1794 oldResource = toResource(entity, false); 1795 } 1796 1797 /* 1798 * Mark the entity as not deleted - This is also done in the actual updateInternal() 1799 * method later on so it usually doesn't matter whether we do it here, but in the 1800 * case of a transaction with multiple PUTs we don't get there until later so 1801 * having this here means that a transaction can have a reference in one 1802 * resource to another resource in the same transaction that is being 1803 * un-deleted by the transaction. Wacky use case, sure. But it's real. 1804 * 1805 * See SystemProviderR4Test#testTransactionReSavesPreviouslyDeletedResources 1806 * for a test that needs this. 1807 */ 1808 boolean wasDeleted = entity.getDeleted() != null; 1809 entity.setDeleted(null); 1810 1811 /* 1812 * If we aren't indexing, that means we're doing this inside a transaction. 1813 * The transaction will do the actual storage to the database a bit later on, 1814 * after placeholder IDs have been replaced, by calling {@link #updateInternal} 1815 * directly. So we just bail now. 1816 */ 1817 if (!thePerformIndexing) { 1818 resource.setId(entity.getIdDt().getValue()); 1819 DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, resource).setCreated(wasDeleted); 1820 outcome.setPreviousResource(oldResource); 1821 if (!outcome.isNop()) { 1822 // Technically this may not end up being right since we might not increment if the 1823 // contents turn out to be the same 1824 outcome.setId(outcome.getId().withVersion(Long.toString(outcome.getId().getVersionIdPartAsLong() + 1))); 1825 } 1826 return outcome; 1827 } 1828 1829 /* 1830 * Otherwise, we're not in a transaction 1831 */ 1832 ResourceTable savedEntity = updateInternal(theRequest, resource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource, theTransactionDetails); 1833 DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, resource).setCreated(wasDeleted); 1834 1835 if (!thePerformIndexing) { 1836 IIdType id = getContext().getVersion().newIdType(); 1837 id.setValue(entity.getIdDt().getValue()); 1838 outcome.setId(id); 1839 } 1840 1841 String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulUpdate", outcome.getId(), w.getMillisAndRestart()); 1842 outcome.setOperationOutcome(createInfoOperationOutcome(msg)); 1843 1844 ourLog.debug(msg); 1845 return outcome; 1846 } 1847 1848 @Override 1849 @Transactional(propagation = Propagation.SUPPORTS) 1850 public MethodOutcome validate(T theResource, IIdType theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile, RequestDetails theRequest) { 1851 TransactionDetails transactionDetails = new TransactionDetails(); 1852 1853 if (theRequest != null) { 1854 ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, theResource, null, theId); 1855 notifyInterceptors(RestOperationTypeEnum.VALIDATE, requestDetails); 1856 } 1857 1858 if (theMode == ValidationModeEnum.DELETE) { 1859 if (theId == null || theId.hasIdPart() == false) { 1860 throw new InvalidRequestException(Msg.code(991) + "No ID supplied. ID is required when validating with mode=DELETE"); 1861 } 1862 final ResourceTable entity = readEntityLatestVersion(theId, theRequest, transactionDetails); 1863 1864 // Validate that there are no resources pointing to the candidate that 1865 // would prevent deletion 1866 DeleteConflictList deleteConflicts = new DeleteConflictList(); 1867 if (getConfig().isEnforceReferentialIntegrityOnDelete()) { 1868 myDeleteConflictService.validateOkToDelete(deleteConflicts, entity, true, theRequest, new TransactionDetails()); 1869 } 1870 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); 1871 1872 IBaseOperationOutcome oo = createInfoOperationOutcome("Ok to delete"); 1873 return new MethodOutcome(new IdDt(theId.getValue()), oo); 1874 } 1875 1876 FhirValidator validator = getContext().newValidator(); 1877 validator.setInterceptorBroadcaster(CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest)); 1878 validator.registerValidatorModule(getInstanceValidator()); 1879 validator.registerValidatorModule(new IdChecker(theMode)); 1880 1881 IBaseResource resourceToValidateById = null; 1882 if (theId != null && theId.hasResourceType() && theId.hasIdPart()) { 1883 Class<? extends IBaseResource> type = getContext().getResourceDefinition(theId.getResourceType()).getImplementingClass(); 1884 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDaoOrNull(type); 1885 resourceToValidateById = dao.read(theId, theRequest); 1886 } 1887 1888 1889 ValidationResult result; 1890 ValidationOptions options = new ValidationOptions() 1891 .addProfileIfNotBlank(theProfile); 1892 1893 if (theResource == null) { 1894 if (resourceToValidateById != null) { 1895 result = validator.validateWithResult(resourceToValidateById, options); 1896 } else { 1897 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "cantValidateWithNoResource"); 1898 throw new InvalidRequestException(Msg.code(992) + msg); 1899 } 1900 } else if (isNotBlank(theRawResource)) { 1901 result = validator.validateWithResult(theRawResource, options); 1902 } else { 1903 result = validator.validateWithResult(theResource, options); 1904 } 1905 1906 if (result.isSuccessful()) { 1907 MethodOutcome retVal = new MethodOutcome(); 1908 retVal.setOperationOutcome(result.toOperationOutcome()); 1909 return retVal; 1910 } else { 1911 throw new PreconditionFailedException(Msg.code(993) + "Validation failed", result.toOperationOutcome()); 1912 } 1913 1914 } 1915 1916 /** 1917 * Get the resource definition from the criteria which specifies the resource type 1918 */ 1919 @Override 1920 public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) { 1921 String resourceName; 1922 if (criteria == null || criteria.trim().isEmpty()) { 1923 throw new IllegalArgumentException(Msg.code(994) + "Criteria cannot be empty"); 1924 } 1925 if (criteria.contains("?")) { 1926 resourceName = criteria.substring(0, criteria.indexOf("?")); 1927 } else { 1928 resourceName = criteria; 1929 } 1930 1931 return getContext().getResourceDefinition(resourceName); 1932 } 1933 1934 private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) { 1935 if (entity.getForcedId() != null) { 1936 if (getConfig().getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) { 1937 if (theId.isIdPartValidLong()) { 1938 // This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that 1939 // as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer 1940 // to the 1941 // forced ID) 1942 throw new ResourceNotFoundException(Msg.code(2000) + theId); 1943 } 1944 } 1945 } 1946 } 1947 1948 private void validateResourceType(BaseHasResource entity) { 1949 validateResourceType(entity, myResourceName); 1950 } 1951 1952 private void validateResourceTypeAndThrowInvalidRequestException(IIdType theId) { 1953 if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) { 1954 // Note- Throw a HAPI FHIR exception here so that hibernate doesn't try to translate it into a database exception 1955 throw new InvalidRequestException(Msg.code(996) + "Incorrect resource type (" + theId.getResourceType() + ") for this DAO, wanted: " + myResourceName); 1956 } 1957 } 1958 1959 @VisibleForTesting 1960 public void setIdHelperSvcForUnitTest(IIdHelperService theIdHelperService) { 1961 myIdHelperService = theIdHelperService; 1962 } 1963 1964 private static class IdChecker implements IValidatorModule { 1965 1966 private final ValidationModeEnum myMode; 1967 1968 IdChecker(ValidationModeEnum theMode) { 1969 myMode = theMode; 1970 } 1971 1972 @Override 1973 public void validateResource(IValidationContext<IBaseResource> theCtx) { 1974 boolean hasId = theCtx.getResource().getIdElement().hasIdPart(); 1975 if (myMode == ValidationModeEnum.CREATE) { 1976 if (hasId) { 1977 throw new UnprocessableEntityException(Msg.code(997) + "Resource has an ID - ID must not be populated for a FHIR create"); 1978 } 1979 } else if (myMode == ValidationModeEnum.UPDATE) { 1980 if (hasId == false) { 1981 throw new UnprocessableEntityException(Msg.code(998) + "Resource has no ID - ID must be populated for a FHIR update"); 1982 } 1983 } 1984 1985 } 1986 1987 } 1988 1989}