diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java index 3ec6c62431e..04cf5103996 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java @@ -3,10 +3,8 @@ package ca.uhn.fhir.jpa.config.dstu3; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.jpa.config.BaseConfig; -import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; -import ca.uhn.fhir.jpa.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; -import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; +import ca.uhn.fhir.jpa.dao.*; +import ca.uhn.fhir.jpa.dao.dstu3.TransactionProcessorVersionAdapterDstu3; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3; import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; @@ -21,6 +19,7 @@ import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.CachingValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; +import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.r4.utils.IResourceValidator; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.context.annotation.Bean; @@ -38,9 +37,9 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -70,6 +69,16 @@ public class BaseDstu3Config extends BaseConfig { return retVal; } + @Bean + public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() { + return new TransactionProcessorVersionAdapterDstu3(); + } + + @Bean + public TransactionProcessor transactionProcessor() { + return new TransactionProcessor<>(); + } + @Bean(name = "myInstanceValidatorDstu3") @Lazy public IValidatorModule instanceValidatorDstu3() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java index d55a3bc43fb..5dbcf649ff0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java @@ -3,12 +3,10 @@ package ca.uhn.fhir.jpa.config.r4; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.jpa.config.BaseConfig; -import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; -import ca.uhn.fhir.jpa.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; -import ca.uhn.fhir.jpa.dao.ISearchParamRegistry; +import ca.uhn.fhir.jpa.dao.*; import ca.uhn.fhir.jpa.dao.r4.SearchParamExtractorR4; import ca.uhn.fhir.jpa.dao.r4.SearchParamRegistryR4; +import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4; import ca.uhn.fhir.jpa.term.HapiTerminologySvcR4; @@ -23,6 +21,7 @@ import org.hl7.fhir.r4.hapi.ctx.IValidationSupport; import org.hl7.fhir.r4.hapi.rest.server.GraphQLProvider; import org.hl7.fhir.r4.hapi.validation.CachingValidationSupport; import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.utils.GraphQLEngine; import org.hl7.fhir.r4.utils.IResourceValidator.BestPracticeWarningLevel; import org.springframework.beans.factory.annotation.Autowire; @@ -73,6 +72,16 @@ public class BaseR4Config extends BaseConfig { return retVal; } + @Bean + public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() { + return new TransactionProcessorVersionAdapterR4(); + } + + @Bean + public TransactionProcessor transactionProcessor() { + return new TransactionProcessor<>(); + } + @Bean(name = "myGraphQLProvider") @Lazy public GraphQLProvider graphQLProvider() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 92a9dbca55c..cf26d4f1f2f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -206,7 +206,7 @@ public abstract class BaseHapiFhirDao implements IDao, private ApplicationContext myApplicationContext; private Map, IFhirResourceDao> myResourceTypeToDao; - protected void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { + public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { if (theRequestDetails != null) { theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); } @@ -1156,7 +1156,7 @@ public abstract class BaseHapiFhirDao implements IDao, return false; } - protected void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { + public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { if (theRequestDetails != null) { theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE); } @@ -1170,7 +1170,7 @@ public abstract class BaseHapiFhirDao implements IDao, return builder; } - protected void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) { + public void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails theRequestDetails) { if (theRequestDetails.getId() != null && theRequestDetails.getId().hasResourceType() && isNotBlank(theRequestDetails.getResourceType())) { if (theRequestDetails.getId().getResourceType().equals(theRequestDetails.getResourceType()) == false) { throw new InternalErrorException( @@ -2288,7 +2288,7 @@ public abstract class BaseHapiFhirDao implements IDao, } } - protected void validateDeleteConflictsEmptyOrThrowException(List theDeleteConflicts) { + public void validateDeleteConflictsEmptyOrThrowException(List theDeleteConflicts) { if (theDeleteConflicts.isEmpty()) { return; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java new file mode 100644 index 00000000000..cfa1376716c --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java @@ -0,0 +1,944 @@ +package ca.uhn.fhir.jpa.dao; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; +import ca.uhn.fhir.jpa.util.DeleteConflict; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.ParameterUtil; +import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.method.BaseMethodBinding; +import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ResourceReferenceInfo; +import ca.uhn.fhir.util.StopWatch; +import ca.uhn.fhir.util.UrlUtil; +import com.google.common.collect.ArrayListMultimap; +import org.apache.commons.lang3.Validate; +import org.apache.http.NameValuePair; +import org.hibernate.Session; +import org.hibernate.internal.SessionImpl; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; +import java.util.*; + +import static org.apache.commons.lang3.StringUtils.*; + +public class TransactionProcessor { + + public static final String URN_PREFIX = "urn:"; + private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class); + private BaseHapiFhirDao myDao; + @Autowired + private PlatformTransactionManager myTxManager; + @PersistenceContext(type = PersistenceContextType.TRANSACTION) + private EntityManager myEntityManager; + @Autowired + private FhirContext myContext; + @Autowired + private ITransactionProcessorVersionAdapter myVersionAdapter; + + public static boolean isPlaceholder(IIdType theId) { + if (theId != null && theId.getValue() != null) { + return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:"); + } + return false; + } + + private static String toStatusString(int theStatusCode) { + return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); + } + + private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) { + myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry); + } + + private void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map idToPersistedOutcome, IIdType nextResourceId, DaoMethodOutcome outcome, + BUNDLEENTRY newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) { + IIdType newId = outcome.getId().toUnqualifiedVersionless(); + IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); + if (newId.equals(resourceId) == false) { + idSubstitutions.put(resourceId, newId); + if (isPlaceholder(resourceId)) { + /* + * The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient. + */ + IIdType id = myContext.getVersion().newIdType(); + id.setValue(theResourceType + '/' + resourceId.getValue()); + idSubstitutions.put(id, newId); + } + } + idToPersistedOutcome.put(newId, outcome); + if (outcome.getCreated().booleanValue()) { + myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED)); + } else { + myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_200_OK)); + } + Date lastModifier = getLastModified(theRes); + myVersionAdapter.setResponseLastModified(newEntry, lastModifier); + + if (theRequestDetails != null) { + if (outcome.getResource() != null) { + String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER); + PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer); + if (preferReturn != null) { + if (preferReturn == PreferReturnEnum.REPRESENTATION) { + myVersionAdapter.setResource(newEntry, outcome.getResource()); + } + } + } + } + + } + + private Date getLastModified(IBaseResource theRes) { + return theRes.getMeta().getLastUpdated(); + } + + private String performIdSubstitutionsInMatchUrl(Map theIdSubstitutions, String theMatchUrl) { + String matchUrl = theMatchUrl; + if (isNotBlank(matchUrl)) { + for (Map.Entry nextSubstitutionEntry : theIdSubstitutions.entrySet()) { + IIdType nextTemporaryId = nextSubstitutionEntry.getKey(); + IIdType nextReplacementId = nextSubstitutionEntry.getValue(); + String nextTemporaryIdPart = nextTemporaryId.getIdPart(); + String nextReplacementIdPart = nextReplacementId.getValueAsString(); + if (isUrn(nextTemporaryId) && nextTemporaryIdPart.length() > URN_PREFIX.length()) { + matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart); + matchUrl = matchUrl.replace(UrlUtil.escapeUrlParam(nextTemporaryIdPart), nextReplacementIdPart); + } + } + } + return matchUrl; + } + + private boolean isUrn(IIdType theId) { + return defaultString(theId.getValue()).startsWith(URN_PREFIX); + } + + + public void setDao(BaseHapiFhirDao theDao) { + myDao = theDao; + } + + public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) { + if (theRequestDetails != null) { + IServerInterceptor.ActionRequestDetails requestDetails = new IServerInterceptor.ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null); + myDao.notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); + } + + String actionName = "Transaction"; + return processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, theRequest, actionName); + } + + private BUNDLE processTransactionAsSubRequest(ServletRequestDetails theRequestDetails, BUNDLE theRequest, String theActionName) { + BaseHapiFhirDao.markRequestAsProcessingSubRequest(theRequestDetails); + try { + return processTransaction(theRequestDetails, theRequest, theActionName); + } finally { + BaseHapiFhirDao.clearRequestAsProcessingSubRequest(theRequestDetails); + } + } + + private BUNDLE batch(final RequestDetails theRequestDetails, BUNDLE theRequest) { + ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size()); + long start = System.currentTimeMillis(); + + TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode()); + + /* + * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others + */ + + for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) { + + BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder(); + + TransactionCallback callback = theStatus -> { + BUNDLE subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode()); + myVersionAdapter.addEntry(subRequestBundle, nextRequestEntry); + + BUNDLE subResponseBundle = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); + return subResponseBundle; + }; + + try { + // FIXME: this doesn't need to be a callback + BUNDLE nextResponseBundle = callback.doInTransaction(null); + + BUNDLEENTRY subResponseEntry = myVersionAdapter.getEntries(nextResponseBundle).get(0); + myVersionAdapter.addEntry(resp, subResponseEntry); + + /* + * If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it + */ + if (myVersionAdapter.getResource(subResponseEntry) == null) { + BUNDLEENTRY nextResponseBundleFirstEntry = myVersionAdapter.getEntries(nextResponseBundle).get(0); + myVersionAdapter.setResource(subResponseEntry, myVersionAdapter.getResource(nextResponseBundleFirstEntry)); + } + + } catch (BaseServerResponseException e) { + caughtEx.setException(e); + } catch (Throwable t) { + ourLog.error("Failure during BATCH sub transaction processing", t); + caughtEx.setException(new InternalErrorException(t)); + } + + if (caughtEx.getException() != null) { + BUNDLEENTRY nextEntry = myVersionAdapter.addEntry(resp); + + populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry); + + myVersionAdapter.setResponseStatus(nextEntry, toStatusString(caughtEx.getException().getStatusCode())); + } + + } + + long delay = System.currentTimeMillis() - start; + ourLog.info("Batch completed in {}ms", new Object[]{delay}); + + return resp; + } + + private BUNDLE processTransaction(final ServletRequestDetails theRequestDetails, final BUNDLE theRequest, final String theActionName) { + validateDependencies(); + + String transactionType = myVersionAdapter.getBundleType(theRequest); + if (org.hl7.fhir.r4.model.Bundle.BundleType.BATCH.toCode().equals(transactionType)) { + return batch(theRequestDetails, theRequest); + } + + if (transactionType == null) { + String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + Bundle.BundleType.TRANSACTION.toCode(); + ourLog.warn(message); + transactionType = org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode(); + } + if (!org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode().equals(transactionType)) { + throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType); + } + + ourLog.debug("Beginning {} with {} resources", theActionName, myVersionAdapter.getEntries(theRequest).size()); + + final Date updateTime = new Date(); + final StopWatch transactionStopWatch = new StopWatch(); + + final Set allIds = new LinkedHashSet<>(); + final Map idSubstitutions = new HashMap<>(); + final Map idToPersistedOutcome = new HashMap<>(); + List requestEntries = myVersionAdapter.getEntries(theRequest); + + // Do all entries have a verb? + for (int i = 0; i < myVersionAdapter.getEntries(theRequest).size(); i++) { + BUNDLEENTRY nextReqEntry = requestEntries.get(i); + String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry); + if (verb == null || !isValidVerb(verb)) { + throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", verb, i)); + } + } + + /* + * We want to execute the transaction request bundle elements in the order + * specified by the FHIR specification (see TransactionSorter) so we save the + * original order in the request, then sort it. + * + * Entries with a type of GET are removed from the bundle so that they + * can be processed at the very end. We do this because the incoming resources + * are saved in a two-phase way in order to deal with interdependencies, and + * we want the GET processing to use the final indexing state + */ + final BUNDLE response = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTIONRESPONSE.toCode()); + List getEntries = new ArrayList<>(); + final IdentityHashMap originalRequestOrder = new IdentityHashMap<>(); + for (int i = 0; i < requestEntries.size(); i++) { + originalRequestOrder.put(requestEntries.get(i), i); + myVersionAdapter.addEntry(response); + if (myVersionAdapter.getEntryRequestVerb(requestEntries.get(i)).equals("GET")) { + getEntries.add(requestEntries.get(i)); + } + } + + /* + * See FhirSystemDaoDstu3Test#testTransactionWithPlaceholderIdInMatchUrl + * Basically if the resource has a match URL that references a placeholder, + * we try to handle the resource with the placeholder first. + */ + Set placeholderIds = new HashSet<>(); + final List entries = requestEntries; + for (BUNDLEENTRY nextEntry : entries) { + String fullUrl = myVersionAdapter.getFullUrl(nextEntry); + if (isNotBlank(fullUrl) && fullUrl.startsWith(URN_PREFIX)) { + placeholderIds.add(fullUrl); + } + } + Collections.sort(entries, new TransactionSorter(placeholderIds)); + + /* + * All of the write operations in the transaction (PUT, POST, etc.. basically anything + * except GET) are performed in their own database transaction before we do the reads. + * We do this because the reads (specifically the searches) often spawn their own + * secondary database transaction and if we allow that within the primary + * database transaction we can end up with deadlocks if the server is under + * heavy load with lots of concurrent transactions using all available + * database connections. + */ + TransactionTemplate txManager = new TransactionTemplate(myTxManager); + Map entriesToProcess = txManager.execute(status -> { + Map retVal = doTransactionWriteOperations(theRequestDetails, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries, transactionStopWatch); + + transactionStopWatch.startTask("Commit writes to database"); + return retVal; + }); + transactionStopWatch.endCurrentTask(); + + for (Map.Entry nextEntry : entriesToProcess.entrySet()) { + String responseLocation = nextEntry.getValue().getIdDt().toUnqualified().getValue(); + String responseEtag = nextEntry.getValue().getIdDt().getVersionIdPart(); + myVersionAdapter.setResponseLocation(nextEntry.getKey(), responseLocation); + myVersionAdapter.setResponseETag(nextEntry.getKey(), responseEtag); + } + + /* + * Loop through the request and process any entries of type GET + */ + if (getEntries.size() > 0) { + transactionStopWatch.startTask("Process " + getEntries.size() + " GET entries"); + } + for (BUNDLEENTRY nextReqEntry : getEntries) { + Integer originalOrder = originalRequestOrder.get(nextReqEntry); + BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(response).get(originalOrder); + + ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(); + requestDetails.setServletRequest(theRequestDetails.getServletRequest()); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setServer(theRequestDetails.getServer()); + + String url = extractTransactionUrlOrThrowException(nextReqEntry, "GET"); + + int qIndex = url.indexOf('?'); + ArrayListMultimap paramValues = ArrayListMultimap.create(); + requestDetails.setParameters(new HashMap<>()); + if (qIndex != -1) { + String params = url.substring(qIndex); + List parameters = BaseHapiFhirDao.translateMatchUrl(params); + for (NameValuePair next : parameters) { + paramValues.put(next.getName(), next.getValue()); + } + for (Map.Entry> nextParamEntry : paramValues.asMap().entrySet()) { + String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]); + requestDetails.addParameter(nextParamEntry.getKey(), nextValue); + } + url = url.substring(0, qIndex); + } + + requestDetails.setRequestPath(url); + requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase()); + + theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url); + BaseMethodBinding method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url); + if (method == null) { + throw new IllegalArgumentException("Unable to handle GET " + url); + } + + if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) { + requestDetails.addHeader(Constants.HEADER_IF_MATCH, myVersionAdapter.getEntryRequestIfMatch(nextReqEntry)); + } + if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry))) { + requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry)); + } + if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry))) { + requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry)); + } + + Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url); + try { + IBaseResource resource = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails); + if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) { + resource = filterNestedBundle(requestDetails, resource); + } + myVersionAdapter.setResource(nextRespEntry, resource); + myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_200_OK)); + } catch (NotModifiedException e) { + myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED)); + } catch (BaseServerResponseException e) { + ourLog.info("Failure processing transaction GET {}: {}", url, e.toString()); + myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(e.getStatusCode())); + populateEntryWithOperationOutcome(e, nextRespEntry); + } + + } + transactionStopWatch.endCurrentTask(); + + ourLog.info("Transaction timing:\n{}", transactionStopWatch.formatTaskDurations()); + + return response; + } + + private boolean isValidVerb(String theVerb) { + try { + return org.hl7.fhir.r4.model.Bundle.HTTPVerb.fromCode(theVerb) != null; + } catch (FHIRException theE) { + return false; + } + } + + /** + * This method is called for nested bundles (e.g. if we received a transaction with an entry that + * was a GET search, this method is called on the bundle for the search result, that will be placed in the + * outer bundle). This method applies the _summary and _content parameters to the output of + * that bundle. + *

+ * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future. + */ + private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) { + IParser p = myContext.newJsonParser(); + RestfulServerUtils.configureResponseParser(theRequestDetails, p); + return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource)); + } + + public void setEntityManager(EntityManager theEntityManager) { + myEntityManager = theEntityManager; + } + + private void validateDependencies() { + Validate.notNull(myEntityManager); + Validate.notNull(myContext); + Validate.notNull(myDao); + Validate.notNull(myTxManager); + } + + private IIdType newIdType(String theValue) { + return myContext.getVersion().newIdType().setValue(theValue); + } + + private Map doTransactionWriteOperations(ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set theAllIds, + Map theIdSubstitutions, Map theIdToPersistedOutcome, BUNDLE theResponse, IdentityHashMap theOriginalRequestOrder, List theEntries, StopWatch theTransactionStopWatch) { + Set deletedResources = new HashSet<>(); + List deleteConflicts = new ArrayList<>(); + Map entriesToProcess = new IdentityHashMap<>(); + Set nonUpdatedEntities = new HashSet<>(); + Set updatedEntities = new HashSet<>(); + Map> conditionalRequestUrls = new HashMap<>(); + + /* + * Loop through the request and process any entries of type + * PUT, POST or DELETE + */ + for (int i = 0; i < theEntries.size(); i++) { + + if (i % 100 == 0) { + ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size()); + } + + BUNDLEENTRY nextReqEntry = theEntries.get(i); + IBaseResource res = myVersionAdapter.getResource(nextReqEntry); + IIdType nextResourceId = null; + if (res != null) { + + nextResourceId = res.getIdElement(); + + if (!nextResourceId.hasIdPart()) { + if (isNotBlank(myVersionAdapter.getFullUrl(nextReqEntry))) { + nextResourceId = newIdType(myVersionAdapter.getFullUrl(nextReqEntry)); + } + } + + if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) { + throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); + } + + if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) { + nextResourceId = newIdType(toResourceName(res.getClass()), nextResourceId.getIdPart()); + res.setId(nextResourceId); + } + + /* + * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness + */ + if (isPlaceholder(nextResourceId)) { + if (!theAllIds.add(nextResourceId)) { + throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId)); + } + } else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) { + IIdType nextId = nextResourceId.toUnqualifiedVersionless(); + if (!theAllIds.add(nextId)) { + throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId)); + } + } + + } + + String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry); + String resourceType = res != null ? myContext.getResourceDefinition(res).getName() : null; + Integer order = theOriginalRequestOrder.get(nextReqEntry); + BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(theResponse).get(order); + + theTransactionStopWatch.startTask("Bundle.entry[" + i + "]: " + verb + " " + defaultString(resourceType)); + + switch (verb) { + case "POST": { + // CREATE + @SuppressWarnings("rawtypes") + IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); + res.setId((String) null); + DaoMethodOutcome outcome; + String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry); + matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); + outcome = resourceDao.create(res, matchUrl, false, theRequestDetails); + if (nextResourceId != null) { + handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); + } + entriesToProcess.put(nextRespEntry, outcome.getEntity()); + if (outcome.getCreated() == false) { + nonUpdatedEntities.add(outcome.getEntity()); + } else { + if (isNotBlank(matchUrl)) { + conditionalRequestUrls.put(matchUrl, res.getClass()); + } + } + + break; + } + case "DELETE": { + // DELETE + String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); + UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); + ca.uhn.fhir.jpa.dao.IFhirResourceDao dao = toDao(parts, verb, url); + int status = Constants.STATUS_HTTP_204_NO_CONTENT; + if (parts.getResourceId() != null) { + IIdType deleteId = newIdType(parts.getResourceType(), parts.getResourceId()); + if (!deletedResources.contains(deleteId.getValueAsString())) { + DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequestDetails); + if (outcome.getEntity() != null) { + deletedResources.add(deleteId.getValueAsString()); + entriesToProcess.put(nextRespEntry, outcome.getEntity()); + } + } + } else { + String matchUrl = parts.getResourceType() + '?' + parts.getParams(); + matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); + DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequestDetails); + List allDeleted = deleteOutcome.getDeletedEntities(); + for (ResourceTable deleted : allDeleted) { + deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString()); + } + if (allDeleted.isEmpty()) { + status = Constants.STATUS_HTTP_204_NO_CONTENT; + } + + myVersionAdapter.setResponseOutcome(nextRespEntry, deleteOutcome.getOperationOutcome()); + } + + myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(status)); + + break; + } + case "PUT": { + // UPDATE + @SuppressWarnings("rawtypes") + IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); + + String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); + + DaoMethodOutcome outcome; + UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); + if (isNotBlank(parts.getResourceId())) { + String version = null; + if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) { + version = ParameterUtil.parseETagValue(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry)); + } + res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version)); + outcome = resourceDao.update(res, null, false, theRequestDetails); + } else { + res.setId((String) null); + String matchUrl; + if (isNotBlank(parts.getParams())) { + matchUrl = parts.getResourceType() + '?' + parts.getParams(); + } else { + matchUrl = parts.getResourceType(); + } + matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); + outcome = resourceDao.update(res, matchUrl, false, theRequestDetails); + if (Boolean.TRUE.equals(outcome.getCreated())) { + conditionalRequestUrls.put(matchUrl, res.getClass()); + } + } + + if (outcome.getCreated() == Boolean.FALSE) { + updatedEntities.add(outcome.getEntity()); + } + + handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); + entriesToProcess.put(nextRespEntry, outcome.getEntity()); + break; + } + case "GET": + default: + break; + + } + + theTransactionStopWatch.endCurrentTask(); + } + + + /* + * Make sure that there are no conflicts from deletions. E.g. we can't delete something + * if something else has a reference to it.. Unless the thing that has a reference to it + * was also deleted as a part of this transaction, which is why we check this now at the + * end. + */ + + deleteConflicts.removeIf(next -> + deletedResources.contains(next.getTargetId().toUnqualifiedVersionless().getValue())); + myDao.validateDeleteConflictsEmptyOrThrowException(deleteConflicts); + + /* + * Perform ID substitutions and then index each resource we have saved + */ + + FhirTerser terser = myContext.newTerser(); + theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); + for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { + IBaseResource nextResource = nextOutcome.getResource(); + if (nextResource == null) { + continue; + } + + // References + List allRefs = terser.getAllResourceReferences(nextResource); + for (ResourceReferenceInfo nextRef : allRefs) { + IIdType nextId = nextRef.getResourceReference().getReferenceElement(); + if (!nextId.hasIdPart()) { + continue; + } + if (theIdSubstitutions.containsKey(nextId)) { + IIdType newId = theIdSubstitutions.get(nextId); + ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); + nextRef.getResourceReference().setReference(newId.getValue()); + } else if (nextId.getValue().startsWith("urn:")) { + throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType()); + } else { + ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); + } + } + + // URIs + Class> uriType = (Class>) myContext.getElementDefinition("uri").getImplementingClass(); + List> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, uriType); + for (IPrimitiveType nextRef : allUris) { + if (nextRef instanceof IIdType) { + continue; // No substitution on the resource ID itself! + } + IIdType nextUriString = newIdType(nextRef.getValueAsString()); + if (theIdSubstitutions.containsKey(nextUriString)) { + IIdType newId = theIdSubstitutions.get(nextUriString); + ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); + nextRef.setValueAsString(newId.getValue()); + } else { + ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); + } + } + + IPrimitiveType deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource); + Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; + + if (updatedEntities.contains(nextOutcome.getEntity())) { + myDao.updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); + } else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) { + myDao.updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); + } + } + + theTransactionStopWatch.endCurrentTask(); + theTransactionStopWatch.startTask("Flush writes to database"); + + flushJpaSession(); + + theTransactionStopWatch.endCurrentTask(); + if (conditionalRequestUrls.size() > 0) { + theTransactionStopWatch.startTask("Check for conflicts in conditional resources"); + } + + /* + * Double check we didn't allow any duplicates we shouldn't have + */ + for (Map.Entry> nextEntry : conditionalRequestUrls.entrySet()) { + String matchUrl = nextEntry.getKey(); + Class resType = nextEntry.getValue(); + if (isNotBlank(matchUrl)) { + IFhirResourceDao resourceDao = myDao.getDao(resType); + Set val = resourceDao.processMatchUrl(matchUrl); + if (val.size() > 1) { + throw new InvalidRequestException( + "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); + } + } + } + + theTransactionStopWatch.endCurrentTask(); + + for (IIdType next : theAllIds) { + IIdType replacement = theIdSubstitutions.get(next); + if (replacement == null) { + continue; + } + if (replacement.equals(next)) { + continue; + } + ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); + } + return entriesToProcess; + } + + private IIdType newIdType(String theResourceType, String theResourceId, String theVersion) { + org.hl7.fhir.r4.model.IdType id = new org.hl7.fhir.r4.model.IdType(theResourceType, theResourceId, theVersion); + return myContext.getVersion().newIdType().setValue(id.getValue()); + } + + private IIdType newIdType(String theToResourceName, String theIdPart) { + return newIdType(theToResourceName, theIdPart, null); + } + + private IFhirResourceDao getDaoOrThrowException(Class theClass) { + return myDao.getDaoOrThrowException(theClass); + } + + protected void flushJpaSession() { + SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class); + int insertionCount = session.getActionQueue().numberOfInsertions(); + int updateCount = session.getActionQueue().numberOfUpdates(); + + StopWatch sw = new StopWatch(); + myEntityManager.flush(); + ourLog.debug("Session flush took {}ms for {} inserts and {} updates", sw.getMillis(), insertionCount, updateCount); + } + + protected String toResourceName(Class theResourceType) { + return myContext.getResourceDefinition(theResourceType).getName(); + } + + public void setContext(FhirContext theContext) { + myContext = theContext; + } + + private String extractTransactionUrlOrThrowException(BUNDLEENTRY nextEntry, String verb) { + String url = myVersionAdapter.getEntryRequestUrl(nextEntry); + if (isBlank(url)) { + throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb)); + } + return url; + } + + private ca.uhn.fhir.jpa.dao.IFhirResourceDao toDao(UrlUtil.UrlParts theParts, String theVerb, String theUrl) { + RuntimeResourceDefinition resType; + try { + resType = myContext.getResourceDefinition(theParts.getResourceType()); + } catch (DataFormatException e) { + String msg = myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); + throw new InvalidRequestException(msg); + } + IFhirResourceDao dao = null; + if (resType != null) { + dao = myDao.getDao(resType.getImplementingClass()); + } + if (dao == null) { + String msg = myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); + throw new InvalidRequestException(msg); + } + + // if (theParts.getResourceId() == null && theParts.getParams() == null) { + // String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); + // throw new InvalidRequestException(msg); + // } + + return dao; + } + + public interface ITransactionProcessorVersionAdapter { + + void setResponseStatus(BUNDLEENTRY theBundleEntry, String theStatus); + + void setResponseLastModified(BUNDLEENTRY theBundleEntry, Date theLastModified); + + void setResource(BUNDLEENTRY theBundleEntry, IBaseResource theResource); + + IBaseResource getResource(BUNDLEENTRY theBundleEntry); + + String getBundleType(BUNDLE theRequest); + + void populateEntryWithOperationOutcome(BaseServerResponseException theCaughtEx, BUNDLEENTRY theEntry); + + BUNDLE createBundle(String theBundleType); + + List getEntries(BUNDLE theRequest); + + void addEntry(BUNDLE theBundle, BUNDLEENTRY theEntry); + + BUNDLEENTRY addEntry(BUNDLE theBundle); + + String getEntryRequestVerb(BUNDLEENTRY theEntry); + + String getFullUrl(BUNDLEENTRY theEntry); + + String getEntryIfNoneExist(BUNDLEENTRY theEntry); + + String getEntryRequestUrl(BUNDLEENTRY theEntry); + + void setResponseLocation(BUNDLEENTRY theEntry, String theResponseLocation); + + void setResponseETag(BUNDLEENTRY theEntry, String theEtag); + + String getEntryRequestIfMatch(BUNDLEENTRY theEntry); + + String getEntryRequestIfNoneExist(BUNDLEENTRY theEntry); + + String getEntryRequestIfNoneMatch(BUNDLEENTRY theEntry); + + void setResponseOutcome(BUNDLEENTRY theEntry, IBaseOperationOutcome theOperationOutcome); + } + + private static class BaseServerResponseExceptionHolder { + private BaseServerResponseException myException; + + public BaseServerResponseException getException() { + return myException; + } + + public void setException(BaseServerResponseException myException) { + this.myException = myException; + } + } + + /** + * Transaction Order, per the spec: + *

+ * Process any DELETE interactions + * Process any POST interactions + * Process any PUT interactions + * Process any GET interactions + */ + //@formatter:off + public class TransactionSorter implements Comparator { + + private Set myPlaceholderIds; + + public TransactionSorter(Set thePlaceholderIds) { + myPlaceholderIds = thePlaceholderIds; + } + + @Override + public int compare(BUNDLEENTRY theO1, BUNDLEENTRY theO2) { + int o1 = toOrder(theO1); + int o2 = toOrder(theO2); + + if (o1 == o2) { + String matchUrl1 = toMatchUrl(theO1); + String matchUrl2 = toMatchUrl(theO2); + if (isBlank(matchUrl1) && isBlank(matchUrl2)) { + return 0; + } + if (isBlank(matchUrl1)) { + return -1; + } + if (isBlank(matchUrl2)) { + return 1; + } + + boolean match1containsSubstitutions = false; + boolean match2containsSubstitutions = false; + for (String nextPlaceholder : myPlaceholderIds) { + if (matchUrl1.contains(nextPlaceholder)) { + match1containsSubstitutions = true; + } + if (matchUrl2.contains(nextPlaceholder)) { + match2containsSubstitutions = true; + } + } + + if (match1containsSubstitutions && match2containsSubstitutions) { + return 0; + } + if (!match1containsSubstitutions && !match2containsSubstitutions) { + return 0; + } + if (match1containsSubstitutions) { + return 1; + } else { + return -1; + } + } + + return o1 - o2; + } + + private String toMatchUrl(BUNDLEENTRY theEntry) { + String verb = myVersionAdapter.getEntryRequestVerb(theEntry); + if (verb.equals("POST")) { + return myVersionAdapter.getEntryIfNoneExist(theEntry); + } + if (verb.equals("PUT") || verb.equals("DELETE")) { + String url = extractTransactionUrlOrThrowException(theEntry, verb); + UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); + if (isBlank(parts.getResourceId())) { + return parts.getResourceType() + '?' + parts.getParams(); + } + } + return null; + } + + private int toOrder(BUNDLEENTRY theO1) { + int o1 = 0; + if (myVersionAdapter.getEntryRequestVerb(theO1) != null) { + switch (myVersionAdapter.getEntryRequestVerb(theO1)) { + case "DELETE": + o1 = 1; + break; + case "POST": + o1 = 2; + break; + case "PUT": + o1 = 3; + break; + case "GET": + o1 = 4; + break; + default: + o1 = 0; + break; + } + } + return o1; + } + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java index 2204f1c49f9..fd7cbbef632 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao.dstu3; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,610 +20,34 @@ package ca.uhn.fhir.jpa.dao.dstu3; * #L% */ -import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; -import ca.uhn.fhir.jpa.dao.DaoMethodOutcome; -import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.entity.TagDefinition; -import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; -import ca.uhn.fhir.jpa.util.DeleteConflict; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.PreferReturnEnum; -import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.ParameterUtil; -import ca.uhn.fhir.rest.server.RestfulServerUtils; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; -import ca.uhn.fhir.rest.server.method.BaseMethodBinding; -import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.util.FhirTerser; -import ca.uhn.fhir.util.StopWatch; -import ca.uhn.fhir.util.UrlUtil; -import ca.uhn.fhir.util.UrlUtil.UrlParts; -import com.google.common.collect.ArrayListMultimap; -import org.apache.commons.lang3.Validate; -import org.apache.http.NameValuePair; -import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.dstu3.model.Bundle.BundleEntryResponseComponent; -import org.hl7.fhir.dstu3.model.Bundle.BundleType; -import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb; -import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity; -import org.hl7.fhir.instance.model.api.*; +import org.hl7.fhir.dstu3.model.Meta; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; +import javax.annotation.PostConstruct; import javax.persistence.TypedQuery; -import java.util.*; -import java.util.Map.Entry; - -import static org.apache.commons.lang3.StringUtils.*; +import java.util.Collection; +import java.util.List; public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu3.class); - @Autowired - private PlatformTransactionManager myTxManager; + private TransactionProcessor myTransactionProcessor; - private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) { - ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size()); - long start = System.currentTimeMillis(); - - TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); - txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - - Bundle resp = new Bundle(); - resp.setType(BundleType.BATCHRESPONSE); - - /* - * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others - */ - - for (final BundleEntryComponent nextRequestEntry : theRequest.getEntry()) { - - BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder(); - - TransactionCallback callback = new TransactionCallback() { - @Override - public Bundle doInTransaction(TransactionStatus theStatus) { - Bundle subRequestBundle = new Bundle(); - subRequestBundle.setType(BundleType.TRANSACTION); - subRequestBundle.addEntry(nextRequestEntry); - - Bundle subResponseBundle = transaction((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); - return subResponseBundle; - } - }; - - try { - Bundle nextResponseBundle = callback.doInTransaction(null); - - BundleEntryComponent subResponseEntry = nextResponseBundle.getEntry().get(0); - resp.addEntry(subResponseEntry); - - /* - * If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it - */ - if (subResponseEntry.getResource() == null) { - subResponseEntry.setResource(nextResponseBundle.getEntry().get(0).getResource()); - } - - } catch (BaseServerResponseException e) { - caughtEx.setException(e); - } catch (Throwable t) { - ourLog.error("Failure during BATCH sub transaction processing", t); - caughtEx.setException(new InternalErrorException(t)); - } - - if (caughtEx.getException() != null) { - BundleEntryComponent nextEntry = resp.addEntry(); - - populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry); - - BundleEntryResponseComponent nextEntryResp = nextEntry.getResponse(); - nextEntryResp.setStatus(toStatusString(caughtEx.getException().getStatusCode())); - } - - } - - long delay = System.currentTimeMillis() - start; - ourLog.info("Batch completed in {}ms", new Object[] {delay}); - - return resp; + @PostConstruct + public void start() { + myTransactionProcessor.setDao(this); } - private Bundle doTransaction(final ServletRequestDetails theRequestDetails, final Bundle theRequest, final String theActionName) { - BundleType transactionType = theRequest.getTypeElement().getValue(); - if (transactionType == BundleType.BATCH) { - return batch(theRequestDetails, theRequest); - } - - if (transactionType == null) { - String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + BundleType.TRANSACTION.toCode(); - ourLog.warn(message); - transactionType = BundleType.TRANSACTION; - } - if (transactionType != BundleType.TRANSACTION) { - throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType.toCode()); - } - - ourLog.debug("Beginning {} with {} resources", theActionName, theRequest.getEntry().size()); - - final Date updateTime = new Date(); - final StopWatch transactionStopWatch = new StopWatch(); - - final Set allIds = new LinkedHashSet<>(); - final Map idSubstitutions = new HashMap<>(); - final Map idToPersistedOutcome = new HashMap<>(); - - // Do all entries have a verb? - for (int i = 0; i < theRequest.getEntry().size(); i++) { - BundleEntryComponent nextReqEntry = theRequest.getEntry().get(i); - HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue(); - if (verb == null) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", nextReqEntry.getRequest().getMethod(), i)); - } - } - - /* - * We want to execute the transaction request bundle elements in the order - * specified by the FHIR specification (see TransactionSorter) so we save the - * original order in the request, then sort it. - * - * Entries with a type of GET are removed from the bundle so that they - * can be processed at the very end. We do this because the incoming resources - * are saved in a two-phase way in order to deal with interdependencies, and - * we want the GET processing to use the final indexing state - */ - final Bundle response = new Bundle(); - List getEntries = new ArrayList(); - final IdentityHashMap originalRequestOrder = new IdentityHashMap(); - for (int i = 0; i < theRequest.getEntry().size(); i++) { - originalRequestOrder.put(theRequest.getEntry().get(i), i); - response.addEntry(); - if (theRequest.getEntry().get(i).getRequest().getMethodElement().getValue() == HTTPVerb.GET) { - getEntries.add(theRequest.getEntry().get(i)); - } - } - - /* - * See FhirSystemDaoDstu3Test#testTransactionWithPlaceholderIdInMatchUrl - * Basically if the resource has a match URL that references a placeholder, - * we try to handle the resource with the placeholder first. - */ - Set placeholderIds = new HashSet<>(); - final List entries = theRequest.getEntry(); - for (BundleEntryComponent nextEntry : entries) { - if (isNotBlank(nextEntry.getFullUrl()) && nextEntry.getFullUrl().startsWith(IdType.URN_PREFIX)) { - placeholderIds.add(nextEntry.getFullUrl()); - } - } - Collections.sort(entries, new TransactionSorter(placeholderIds)); - - /* - * All of the write operations in the transaction (PUT, POST, etc.. basically anything - * except GET) are performed in their own database transaction before we do the reads. - * We do this because the reads (specifically the searches) often spawn their own - * secondary database transaction and if we allow that within the primary - * database transaction we can end up with deadlocks if the server is under - * heavy load with lots of concurrent transactions using all available - * database connections. - */ - TransactionTemplate txManager = new TransactionTemplate(myTxManager); - Map entriesToProcess = txManager.execute(new TransactionCallback>() { - @Override - public Map doInTransaction(TransactionStatus status) { - Map retVal = doTransactionWriteOperations(theRequestDetails, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries, transactionStopWatch); - - transactionStopWatch.startTask("Commit writes to database"); - return retVal; - } - }); - transactionStopWatch.endCurrentTask(); - - for (Entry nextEntry : entriesToProcess.entrySet()) { - String responseLocation = nextEntry.getValue().getIdDt().toUnqualified().getValue(); - String responseEtag = nextEntry.getValue().getIdDt().getVersionIdPart(); - nextEntry.getKey().getResponse().setLocation(responseLocation); - nextEntry.getKey().getResponse().setEtag(responseEtag); - } - - /* - * Loop through the request and process any entries of type GET - */ - if (getEntries.size() > 0) { - transactionStopWatch.startTask("Process " + getEntries.size() + " GET entries"); - } - for (BundleEntryComponent nextReqEntry : getEntries) { - Integer originalOrder = originalRequestOrder.get(nextReqEntry); - BundleEntryComponent nextRespEntry = response.getEntry().get(originalOrder); - - ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(); - requestDetails.setServletRequest(theRequestDetails.getServletRequest()); - requestDetails.setRequestType(RequestTypeEnum.GET); - requestDetails.setServer(theRequestDetails.getServer()); - - String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerb.GET); - - int qIndex = url.indexOf('?'); - ArrayListMultimap paramValues = ArrayListMultimap.create(); - requestDetails.setParameters(new HashMap<>()); - if (qIndex != -1) { - String params = url.substring(qIndex); - List parameters = translateMatchUrl(params); - for (NameValuePair next : parameters) { - paramValues.put(next.getName(), next.getValue()); - } - for (Entry> nextParamEntry : paramValues.asMap().entrySet()) { - String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]); - requestDetails.addParameter(nextParamEntry.getKey(), nextValue); - } - url = url.substring(0, qIndex); - } - - requestDetails.setRequestPath(url); - requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase()); - - theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url); - BaseMethodBinding method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url); - if (method == null) { - throw new IllegalArgumentException("Unable to handle GET " + url); - } - - if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) { - requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextReqEntry.getRequest().getIfMatch()); - } - if (isNotBlank(nextReqEntry.getRequest().getIfNoneExist())) { - requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, nextReqEntry.getRequest().getIfNoneExist()); - } - if (isNotBlank(nextReqEntry.getRequest().getIfNoneMatch())) { - requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, nextReqEntry.getRequest().getIfNoneMatch()); - } - - Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url); - try { - IBaseResource resource = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails); - if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) { - resource = filterNestedBundle(requestDetails, resource); - } - nextRespEntry.setResource((Resource) resource); - nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK)); - } catch (NotModifiedException e) { - nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED)); - } catch (BaseServerResponseException e) { - ourLog.info("Failure processing transaction GET {}: {}", url, e.toString()); - nextRespEntry.getResponse().setStatus(toStatusString(e.getStatusCode())); - populateEntryWithOperationOutcome(e, nextRespEntry); - } - - } - transactionStopWatch.endCurrentTask(); - - response.setType(BundleType.TRANSACTIONRESPONSE); - - ourLog.info("Transaction timing:\n{}", transactionStopWatch.formatTaskDurations()); - - return response; - } - - private Map doTransactionWriteOperations(ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set theAllIds, - Map theIdSubstitutions, Map theIdToPersistedOutcome, Bundle theResponse, IdentityHashMap theOriginalRequestOrder, List theEntries, StopWatch theTransactionStopWatch) { - Set deletedResources = new HashSet<>(); - List deleteConflicts = new ArrayList<>(); - Map entriesToProcess = new IdentityHashMap<>(); - Set nonUpdatedEntities = new HashSet<>(); - Set updatedEntities = new HashSet<>(); - Map> conditionalRequestUrls = new HashMap<>(); - - /* - * Loop through the request and process any entries of type - * PUT, POST or DELETE - */ - for (int i = 0; i < theEntries.size(); i++) { - - if (i % 100 == 0) { - ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size()); - } - - BundleEntryComponent nextReqEntry = theEntries.get(i); - Resource res = nextReqEntry.getResource(); - IdType nextResourceId = null; - if (res != null) { - - nextResourceId = res.getIdElement(); - - if (!nextResourceId.hasIdPart()) { - if (isNotBlank(nextReqEntry.getFullUrl())) { - nextResourceId = new IdType(nextReqEntry.getFullUrl()); - } - } - - if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) { - throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); - } - - if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) { - nextResourceId = new IdType(toResourceName(res.getClass()), nextResourceId.getIdPart()); - res.setId(nextResourceId); - } - - /* - * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness - */ - if (isPlaceholder(nextResourceId)) { - if (!theAllIds.add(nextResourceId)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId)); - } - } else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) { - IdType nextId = nextResourceId.toUnqualifiedVersionless(); - if (!theAllIds.add(nextId)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId)); - } - } - - } - - HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue(); - String resourceType = res != null ? getContext().getResourceDefinition(res).getName() : null; - BundleEntryComponent nextRespEntry = theResponse.getEntry().get(theOriginalRequestOrder.get(nextReqEntry)); - - theTransactionStopWatch.startTask("Bundle.entry[" + i + "]: " + verb.name() + " " + defaultString(resourceType)); - - switch (verb) { - case POST: { - // CREATE - @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); - res.setId((String) null); - DaoMethodOutcome outcome; - String matchUrl = nextReqEntry.getRequest().getIfNoneExist(); - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - outcome = resourceDao.create(res, matchUrl, false, theRequestDetails); - if (nextResourceId != null) { - handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); - } - entriesToProcess.put(nextRespEntry, outcome.getEntity()); - if (outcome.getCreated() == false) { - nonUpdatedEntities.add(outcome.getEntity()); - } else { - if (isNotBlank(matchUrl)) { - conditionalRequestUrls.put(matchUrl, res.getClass()); - } - } - - break; - } - case DELETE: { - // DELETE - String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); - UrlParts parts = UrlUtil.parseUrl(url); - ca.uhn.fhir.jpa.dao.IFhirResourceDao dao = toDao(parts, verb.toCode(), url); - int status = Constants.STATUS_HTTP_204_NO_CONTENT; - if (parts.getResourceId() != null) { - IdType deleteId = new IdType(parts.getResourceType(), parts.getResourceId()); - if (!deletedResources.contains(deleteId.getValueAsString())) { - DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequestDetails); - if (outcome.getEntity() != null) { - deletedResources.add(deleteId.getValueAsString()); - entriesToProcess.put(nextRespEntry, outcome.getEntity()); - } - } - } else { - String matchUrl = parts.getResourceType() + '?' + parts.getParams(); - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequestDetails); - List allDeleted = deleteOutcome.getDeletedEntities(); - for (ResourceTable deleted : allDeleted) { - deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString()); - } - if (allDeleted.isEmpty()) { - status = Constants.STATUS_HTTP_204_NO_CONTENT; - } - - nextRespEntry.getResponse().setOutcome((Resource) deleteOutcome.getOperationOutcome()); - } - - nextRespEntry.getResponse().setStatus(toStatusString(status)); - - break; - } - case PUT: { - // UPDATE - @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); - - String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); - - DaoMethodOutcome outcome; - UrlParts parts = UrlUtil.parseUrl(url); - if (isNotBlank(parts.getResourceId())) { - String version = null; - if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) { - version = ParameterUtil.parseETagValue(nextReqEntry.getRequest().getIfMatch()); - } - res.setId(new IdType(parts.getResourceType(), parts.getResourceId(), version)); - outcome = resourceDao.update(res, null, false, theRequestDetails); - } else { - res.setId((String) null); - String matchUrl; - if (isNotBlank(parts.getParams())) { - matchUrl = parts.getResourceType() + '?' + parts.getParams(); - } else { - matchUrl = parts.getResourceType(); - } - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - outcome = resourceDao.update(res, matchUrl, false, theRequestDetails); - if (Boolean.TRUE.equals(outcome.getCreated())) { - conditionalRequestUrls.put(matchUrl, res.getClass()); - } - } - - if (outcome.getCreated() == Boolean.FALSE) { - updatedEntities.add(outcome.getEntity()); - } - - handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); - entriesToProcess.put(nextRespEntry, outcome.getEntity()); - break; - } - case GET: - case NULL: - break; - - } - - theTransactionStopWatch.endCurrentTask(); - } - - /* - * Make sure that there are no conflicts from deletions. E.g. we can't delete something - * if something else has a reference to it.. Unless the thing that has a reference to it - * was also deleted as a part of this transaction, which is why we check this now at the - * end. - */ - - deleteConflicts.removeIf(next -> - deletedResources.contains(next.getTargetId().toUnqualifiedVersionless().getValue())); - validateDeleteConflictsEmptyOrThrowException(deleteConflicts); - - /* - * Perform ID substitutions and then index each resource we have saved - */ - - FhirTerser terser = getContext().newTerser(); - theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); - for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { - IBaseResource nextResource = nextOutcome.getResource(); - if (nextResource == null) { - continue; - } - - // References - List allRefs = terser.getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class); - for (IBaseReference nextRef : allRefs) { - IIdType nextId = nextRef.getReferenceElement(); - if (!nextId.hasIdPart()) { - continue; - } - if (theIdSubstitutions.containsKey(nextId)) { - IdType newId = theIdSubstitutions.get(nextId); - ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); - nextRef.setReference(newId.getValue()); - } else if (nextId.getValue().startsWith("urn:")) { - throw new InvalidRequestException("Unable to satisfy placeholder ID: " + nextId.getValue()); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); - } - } - - // URIs - List allUris = terser.getAllPopulatedChildElementsOfType(nextResource, UriType.class); - for (UriType nextRef : allUris) { - if (nextRef instanceof IIdType) { - continue; // No substitution on the resource ID itself! - } - IdType nextUriString = new IdType(nextRef.getValueAsString()); - if (theIdSubstitutions.containsKey(nextUriString)) { - IdType newId = theIdSubstitutions.get(nextUriString); - ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); - nextRef.setValue(newId.getValue()); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); - } - } - - IPrimitiveType deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource); - Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; - - if (updatedEntities.contains(nextOutcome.getEntity())) { - updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); - } else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) { - updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); - } - } - - theTransactionStopWatch.endCurrentTask(); - theTransactionStopWatch.startTask("Flush writes to database"); - - flushJpaSession(); - - theTransactionStopWatch.endCurrentTask(); - if (conditionalRequestUrls.size() > 0) { - theTransactionStopWatch.startTask("Check for conflicts in conditional resources"); - } - - /* - * Double check we didn't allow any duplicates we shouldn't have - */ - for (Entry> nextEntry : conditionalRequestUrls.entrySet()) { - String matchUrl = nextEntry.getKey(); - Class resType = nextEntry.getValue(); - if (isNotBlank(matchUrl)) { - IFhirResourceDao resourceDao = getDao(resType); - Set val = resourceDao.processMatchUrl(matchUrl); - if (val.size() > 1) { - throw new InvalidRequestException( - "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); - } - } - } - - theTransactionStopWatch.endCurrentTask(); - - for (IdType next : theAllIds) { - IdType replacement = theIdSubstitutions.get(next); - if (replacement == null) { - continue; - } - if (replacement.equals(next)) { - continue; - } - ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); - } - return entriesToProcess; - } - - private String extractTransactionUrlOrThrowException(BundleEntryComponent nextEntry, HTTPVerb verb) { - String url = nextEntry.getRequest().getUrl(); - if (isBlank(url)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name())); - } - return url; - } - - /** - * This method is called for nested bundles (e.g. if we received a transaction with an entry that - * was a GET search, this method is called on the bundle for the search result, that will be placed in the - * outer bundle). This method applies the _summary and _content parameters to the output of - * that bundle. - *

- * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future. - */ - private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) { - IParser p = getContext().newJsonParser(); - RestfulServerUtils.configureResponseParser(theRequestDetails, p); - return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource)); - } - - @Override public Meta metaGetOperation(RequestDetails theRequestDetails) { // Notify interceptors @@ -634,60 +58,10 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { TypedQuery q = myEntityManager.createQuery(sql, TagDefinition.class); List tagDefinitions = q.getResultList(); - Meta retVal = toMeta(tagDefinitions); - - return retVal; + return toMeta(tagDefinitions); } - private String performIdSubstitutionsInMatchUrl(Map theIdSubstitutions, String theMatchUrl) { - String matchUrl = theMatchUrl; - if (isNotBlank(matchUrl)) { - for (Entry nextSubstitutionEntry : theIdSubstitutions.entrySet()) { - IdType nextTemporaryId = nextSubstitutionEntry.getKey(); - IdType nextReplacementId = nextSubstitutionEntry.getValue(); - String nextTemporaryIdPart = nextTemporaryId.getIdPart(); - String nextReplacementIdPart = nextReplacementId.getValueAsString(); - if (nextTemporaryId.isUrn() && nextTemporaryIdPart.length() > IdType.URN_PREFIX.length()) { - matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart); - matchUrl = matchUrl.replace(UrlUtil.escapeUrlParam(nextTemporaryIdPart), nextReplacementIdPart); - } - } - } - return matchUrl; - } - - private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BundleEntryComponent nextEntry) { - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setSeverity(IssueSeverity.ERROR).setDiagnostics(caughtEx.getMessage()); - nextEntry.getResponse().setOutcome(oo); - } - - private ca.uhn.fhir.jpa.dao.IFhirResourceDao toDao(UrlParts theParts, String theVerb, String theUrl) { - RuntimeResourceDefinition resType; - try { - resType = getContext().getResourceDefinition(theParts.getResourceType()); - } catch (DataFormatException e) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - throw new InvalidRequestException(msg); - } - IFhirResourceDao dao = null; - if (resType != null) { - dao = getDao(resType.getImplementingClass()); - } - if (dao == null) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - throw new InvalidRequestException(msg); - } - - // if (theParts.getResourceId() == null && theParts.getParams() == null) { - // String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - // throw new InvalidRequestException(msg); - // } - - return dao; - } - - protected Meta toMeta(Collection tagDefinitions) { + private Meta toMeta(Collection tagDefinitions) { Meta retVal = new Meta(); for (TagDefinition next : tagDefinitions) { switch (next.getTagType()) { @@ -708,187 +82,8 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { @Transactional(propagation = Propagation.NEVER) @Override public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { - if (theRequestDetails != null) { - ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null); - notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); - } - - String actionName = "Transaction"; - return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName); + return myTransactionProcessor.transaction(theRequestDetails, theRequest); } - private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) { - super.markRequestAsProcessingSubRequest(theRequestDetails); - try { - return doTransaction(theRequestDetails, theRequest, theActionName); - } finally { - super.clearRequestAsProcessingSubRequest(theRequestDetails); - } - } - - private static void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome, - BundleEntryComponent newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) { - IdType newId = (IdType) outcome.getId().toUnqualifiedVersionless(); - IdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); - if (newId.equals(resourceId) == false) { - idSubstitutions.put(resourceId, newId); - if (isPlaceholder(resourceId)) { - /* - * The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient. - */ - idSubstitutions.put(new IdType(theResourceType + '/' + resourceId.getValue()), newId); - } - } - idToPersistedOutcome.put(newId, outcome); - if (outcome.getCreated().booleanValue()) { - newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_201_CREATED)); - } else { - newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK)); - } - newEntry.getResponse().setLastModified(((Resource) theRes).getMeta().getLastUpdated()); - - if (theRequestDetails != null) { - if (outcome.getResource() != null) { - String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER); - PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer); - if (preferReturn != null) { - if (preferReturn == PreferReturnEnum.REPRESENTATION) { - newEntry.setResource((Resource) outcome.getResource()); - } - } - } - } - - } - - private static boolean isPlaceholder(IdType theId) { - if (theId != null && theId.getValue() != null) { - if (theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:")) { - return true; - } - } - return false; - } - - private static String toStatusString(int theStatusCode) { - return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); - } - - /** - * Transaction Order, per the spec: - *

- * Process any DELETE interactions - * Process any POST interactions - * Process any PUT interactions - * Process any GET interactions - */ - //@formatter:off - public class TransactionSorter implements Comparator { - - private Set myPlaceholderIds; - - public TransactionSorter(Set thePlaceholderIds) { - myPlaceholderIds = thePlaceholderIds; - } - - @Override - public int compare(BundleEntryComponent theO1, BundleEntryComponent theO2) { - int o1 = toOrder(theO1); - int o2 = toOrder(theO2); - - if (o1 == o2) { - String matchUrl1 = toMatchUrl(theO1); - String matchUrl2 = toMatchUrl(theO2); - if (isBlank(matchUrl1) && isBlank(matchUrl2)) { - return 0; - } - if (isBlank(matchUrl1)) { - return -1; - } - if (isBlank(matchUrl2)) { - return 1; - } - - boolean match1containsSubstitutions = false; - boolean match2containsSubstitutions = false; - for (String nextPlaceholder : myPlaceholderIds) { - if (matchUrl1.contains(nextPlaceholder)) { - match1containsSubstitutions = true; - } - if (matchUrl2.contains(nextPlaceholder)) { - match2containsSubstitutions = true; - } - } - - if (match1containsSubstitutions && match2containsSubstitutions) { - return 0; - } - if (!match1containsSubstitutions && !match2containsSubstitutions) { - return 0; - } - if (match1containsSubstitutions) { - return 1; - } else { - return -1; - } - } - - return o1 - o2; - } - - private String toMatchUrl(BundleEntryComponent theEntry) { - HTTPVerb verb = theEntry.getRequest().getMethod(); - if (verb == HTTPVerb.POST) { - return theEntry.getRequest().getIfNoneExist(); - } - if (verb == HTTPVerb.PUT || verb == HTTPVerb.DELETE) { - String url = extractTransactionUrlOrThrowException(theEntry, verb); - UrlParts parts = UrlUtil.parseUrl(url); - if (isBlank(parts.getResourceId())) { - return parts.getResourceType() + '?' + parts.getParams(); - } - } - return null; - } - - private int toOrder(BundleEntryComponent theO1) { - int o1 = 0; - if (theO1.getRequest().getMethodElement().getValue() != null) { - switch (theO1.getRequest().getMethodElement().getValue()) { - case DELETE: - o1 = 1; - break; - case POST: - o1 = 2; - break; - case PUT: - o1 = 3; - break; - case GET: - o1 = 4; - break; - case NULL: - o1 = 0; - break; - } - } - return o1; - } - - } - - //@formatter:off - - private static class BaseServerResponseExceptionHolder { - private BaseServerResponseException myException; - - public BaseServerResponseException getException() { - return myException; - } - - public void setException(BaseServerResponseException myException) { - this.myException = myException; - } - } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java new file mode 100644 index 00000000000..1b8d712f6ac --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java @@ -0,0 +1,133 @@ +package ca.uhn.fhir.jpa.dao.dstu3; + +import ca.uhn.fhir.jpa.dao.TransactionProcessor; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.dstu3.model.Resource; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.Date; +import java.util.List; + +public class TransactionProcessorVersionAdapterDstu3 implements TransactionProcessor.ITransactionProcessorVersionAdapter { + @Override + public void setResponseStatus(Bundle.BundleEntryComponent theBundleEntry, String theStatus) { + theBundleEntry.getResponse().setStatus(theStatus); + } + + @Override + public void setResponseLastModified(Bundle.BundleEntryComponent theBundleEntry, Date theLastModified) { + theBundleEntry.getResponse().setLastModified(theLastModified); + } + + @Override + public void setResource(Bundle.BundleEntryComponent theBundleEntry, IBaseResource theResource) { + theBundleEntry.setResource((Resource) theResource); + } + + @Override + public IBaseResource getResource(Bundle.BundleEntryComponent theBundleEntry) { + return theBundleEntry.getResource(); + } + + @Override + public String getBundleType(Bundle theRequest) { + if (theRequest.getType() == null) { + return null; + } + return theRequest.getTypeElement().getValue().toCode(); + } + + @Override + public void populateEntryWithOperationOutcome(BaseServerResponseException theCaughtEx, Bundle.BundleEntryComponent theEntry) { + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setSeverity(OperationOutcome.IssueSeverity.ERROR).setDiagnostics(theCaughtEx.getMessage()); + theEntry.getResponse().setOutcome(oo); + } + + @Override + public Bundle createBundle(String theBundleType) { + Bundle resp = new Bundle(); + try { + resp.setType(Bundle.BundleType.fromCode(theBundleType)); + } catch (FHIRException theE) { + throw new InternalErrorException("Unknown bundle type: " + theBundleType); + } + return resp; + } + + @Override + public List getEntries(Bundle theRequest) { + return theRequest.getEntry(); + } + + @Override + public void addEntry(Bundle theBundle, Bundle.BundleEntryComponent theEntry) { + theBundle.addEntry(theEntry); + } + + @Override + public Bundle.BundleEntryComponent addEntry(Bundle theBundle) { + return theBundle.addEntry(); + } + + @Override + public String getEntryRequestVerb(Bundle.BundleEntryComponent theEntry) { + String retVal = null; + Bundle.HTTPVerb value = theEntry.getRequest().getMethodElement().getValue(); + if (value != null) { + retVal = value.toCode(); + } + return retVal; + } + + @Override + public String getFullUrl(Bundle.BundleEntryComponent theEntry) { + return theEntry.getFullUrl(); + } + + @Override + public String getEntryIfNoneExist(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getIfNoneExist(); + } + + @Override + public String getEntryRequestUrl(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getUrl(); + } + + @Override + public void setResponseLocation(Bundle.BundleEntryComponent theEntry, String theResponseLocation) { + theEntry.getResponse().setLocation(theResponseLocation); + } + + @Override + public void setResponseETag(Bundle.BundleEntryComponent theEntry, String theEtag) { + theEntry.getResponse().setEtag(theEtag); + } + + @Override + public String getEntryRequestIfMatch(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getIfMatch(); + } + + @Override + public String getEntryRequestIfNoneExist(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getIfNoneExist(); + } + + @Override + public String getEntryRequestIfNoneMatch(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getIfNoneMatch(); + } + + @Override + public void setResponseOutcome(Bundle.BundleEntryComponent theEntry, IBaseOperationOutcome theOperationOutcome) { + theEntry.getResponse().setOutcome((Resource) theOperationOutcome); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java index ad74a60f642..00e7fe1650e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao.r4; * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,587 +20,34 @@ package ca.uhn.fhir.jpa.dao.r4; * #L% */ -import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; -import ca.uhn.fhir.jpa.dao.DaoMethodOutcome; -import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.entity.ResourceTable; +import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.entity.TagDefinition; -import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; -import ca.uhn.fhir.jpa.util.DeleteConflict; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.PreferReturnEnum; -import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.ParameterUtil; -import ca.uhn.fhir.rest.server.RestfulServerUtils; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; -import ca.uhn.fhir.rest.server.method.BaseMethodBinding; -import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.util.FhirTerser; -import ca.uhn.fhir.util.UrlUtil; -import ca.uhn.fhir.util.UrlUtil.UrlParts; -import com.google.common.collect.ArrayListMultimap; -import org.apache.commons.lang3.Validate; -import org.apache.http.NameValuePair; -import org.hl7.fhir.instance.model.api.*; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.Bundle.BundleEntryResponseComponent; -import org.hl7.fhir.r4.model.Bundle.BundleType; -import org.hl7.fhir.r4.model.Bundle.HTTPVerb; -import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; +import org.hl7.fhir.r4.model.Meta; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; +import javax.annotation.PostConstruct; import javax.persistence.TypedQuery; -import java.util.*; -import java.util.Map.Entry; - -import static org.apache.commons.lang3.StringUtils.*; +import java.util.Collection; +import java.util.List; public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR4.class); @Autowired - private PlatformTransactionManager myTxManager; + private TransactionProcessor myTransactionProcessor; - private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) { - ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size()); - long start = System.currentTimeMillis(); - - TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); - txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - - Bundle resp = new Bundle(); - resp.setType(BundleType.BATCHRESPONSE); - - /* - * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others - */ - - for (final BundleEntryComponent nextRequestEntry : theRequest.getEntry()) { - - BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder(); - - TransactionCallback callback = new TransactionCallback() { - @Override - public Bundle doInTransaction(TransactionStatus theStatus) { - Bundle subRequestBundle = new Bundle(); - subRequestBundle.setType(BundleType.TRANSACTION); - subRequestBundle.addEntry(nextRequestEntry); - - Bundle subResponseBundle = transaction((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); - return subResponseBundle; - } - }; - - try { - Bundle nextResponseBundle = callback.doInTransaction(null); - - BundleEntryComponent subResponseEntry = nextResponseBundle.getEntry().get(0); - resp.addEntry(subResponseEntry); - - /* - * If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it - */ - if (subResponseEntry.getResource() == null) { - subResponseEntry.setResource(nextResponseBundle.getEntry().get(0).getResource()); - } - - } catch (BaseServerResponseException e) { - caughtEx.setException(e); - } catch (Throwable t) { - ourLog.error("Failure during BATCH sub transaction processing", t); - caughtEx.setException(new InternalErrorException(t)); - } - - if (caughtEx.getException() != null) { - BundleEntryComponent nextEntry = resp.addEntry(); - - populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry); - - BundleEntryResponseComponent nextEntryResp = nextEntry.getResponse(); - nextEntryResp.setStatus(toStatusString(caughtEx.getException().getStatusCode())); - } - - } - - long delay = System.currentTimeMillis() - start; - ourLog.info("Batch completed in {}ms", new Object[] {delay}); - - return resp; - } - - private Bundle doTransaction(final ServletRequestDetails theRequestDetails, final Bundle theRequest, final String theActionName) { - BundleType transactionType = theRequest.getTypeElement().getValue(); - if (transactionType == BundleType.BATCH) { - return batch(theRequestDetails, theRequest); - } - - if (transactionType == null) { - String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + BundleType.TRANSACTION.toCode(); - ourLog.warn(message); - transactionType = BundleType.TRANSACTION; - } - if (transactionType != BundleType.TRANSACTION) { - throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType.toCode()); - } - - ourLog.debug("Beginning {} with {} resources", theActionName, theRequest.getEntry().size()); - - long start = System.currentTimeMillis(); - final Date updateTime = new Date(); - - final Set allIds = new LinkedHashSet<>(); - final Map idSubstitutions = new HashMap<>(); - final Map idToPersistedOutcome = new HashMap<>(); - - // Do all entries have a verb? - for (int i = 0; i < theRequest.getEntry().size(); i++) { - BundleEntryComponent nextReqEntry = theRequest.getEntry().get(i); - HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue(); - if (verb == null) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", nextReqEntry.getRequest().getMethod(), i)); - } - } - - /* - * We want to execute the transaction request bundle elements in the order - * specified by the FHIR specification (see TransactionSorter) so we save the - * original order in the request, then sort it. - * - * Entries with a type of GET are removed from the bundle so that they - * can be processed at the very end. We do this because the incoming resources - * are saved in a two-phase way in order to deal with interdependencies, and - * we want the GET processing to use the final indexing state - */ - final Bundle response = new Bundle(); - List getEntries = new ArrayList<>(); - final IdentityHashMap originalRequestOrder = new IdentityHashMap<>(); - for (int i = 0; i < theRequest.getEntry().size(); i++) { - originalRequestOrder.put(theRequest.getEntry().get(i), i); - response.addEntry(); - if (theRequest.getEntry().get(i).getRequest().getMethodElement().getValue() == HTTPVerb.GET) { - getEntries.add(theRequest.getEntry().get(i)); - } - } - - /* - * See FhirSystemDaoR4Test#testTransactionWithPlaceholderIdInMatchUrl - * Basically if the resource has a match URL that references a placeholder, - * we try to handle the resource with the placeholder first. - */ - Set placeholderIds = new HashSet(); - final List entries = theRequest.getEntry(); - for (BundleEntryComponent nextEntry : entries) { - if (isNotBlank(nextEntry.getFullUrl()) && nextEntry.getFullUrl().startsWith(IdType.URN_PREFIX)) { - placeholderIds.add(nextEntry.getFullUrl()); - } - } - Collections.sort(entries, new TransactionSorter(placeholderIds)); - - /* - * All of the write operations in the transaction (PUT, POST, etc.. basically anything - * except GET) are performed in their own database transaction before we do the reads. - * We do this because the reads (specifically the searches) often spawn their own - * secondary database transaction and if we allow that within the primary - * database transaction we can end up with deadlocks if the server is under - * heavy load with lots of concurrent transactions using all available - * database connections. - */ - TransactionTemplate txManager = new TransactionTemplate(myTxManager); - Map entriesToProcess = txManager.execute(new TransactionCallback>() { - @Override - public Map doInTransaction(TransactionStatus status) { - return doTransactionWriteOperations(theRequestDetails, theRequest, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries); - } - }); - for (Entry nextEntry : entriesToProcess.entrySet()) { - String responseLocation = nextEntry.getValue().getIdDt().toUnqualified().getValue(); - String responseEtag = nextEntry.getValue().getIdDt().getVersionIdPart(); - nextEntry.getKey().getResponse().setLocation(responseLocation); - nextEntry.getKey().getResponse().setEtag(responseEtag); - } - - /* - * Loop through the request and process any entries of type GET - */ - for (int i = 0; i < getEntries.size(); i++) { - BundleEntryComponent nextReqEntry = getEntries.get(i); - Integer originalOrder = originalRequestOrder.get(nextReqEntry); - BundleEntryComponent nextRespEntry = response.getEntry().get(originalOrder); - - ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(); - requestDetails.setServletRequest(theRequestDetails.getServletRequest()); - requestDetails.setRequestType(RequestTypeEnum.GET); - requestDetails.setServer(theRequestDetails.getServer()); - - String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerb.GET); - - int qIndex = url.indexOf('?'); - ArrayListMultimap paramValues = ArrayListMultimap.create(); - requestDetails.setParameters(new HashMap()); - if (qIndex != -1) { - String params = url.substring(qIndex); - List parameters = translateMatchUrl(params); - for (NameValuePair next : parameters) { - paramValues.put(next.getName(), next.getValue()); - } - for (java.util.Map.Entry> nextParamEntry : paramValues.asMap().entrySet()) { - String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]); - requestDetails.addParameter(nextParamEntry.getKey(), nextValue); - } - url = url.substring(0, qIndex); - } - - requestDetails.setRequestPath(url); - requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase()); - - theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url); - BaseMethodBinding method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url); - if (method == null) { - throw new IllegalArgumentException("Unable to handle GET " + url); - } - - if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) { - requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextReqEntry.getRequest().getIfMatch()); - } - if (isNotBlank(nextReqEntry.getRequest().getIfNoneExist())) { - requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, nextReqEntry.getRequest().getIfNoneExist()); - } - if (isNotBlank(nextReqEntry.getRequest().getIfNoneMatch())) { - requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, nextReqEntry.getRequest().getIfNoneMatch()); - } - - Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {0}", url); - try { - IBaseResource resource = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails); - if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) { - resource = filterNestedBundle(requestDetails, resource); - } - nextRespEntry.setResource((Resource) resource); - nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK)); - } catch (NotModifiedException e) { - nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED)); - } catch (BaseServerResponseException e) { - ourLog.info("Failure processing transaction GET {}: {}", url, e.toString()); - nextRespEntry.getResponse().setStatus(toStatusString(e.getStatusCode())); - populateEntryWithOperationOutcome(e, nextRespEntry); - } - - } - - long delay = System.currentTimeMillis() - start; - ourLog.info(theActionName + " completed in {}ms", new Object[] {delay}); - - response.setType(BundleType.TRANSACTIONRESPONSE); - return response; - } - - @SuppressWarnings("unchecked") - private Map doTransactionWriteOperations(RequestDetails theRequestDetails, Bundle theRequest, String theActionName, Date theUpdateTime, Set theAllIds, - Map theIdSubstitutions, Map theIdToPersistedOutcome, Bundle theResponse, IdentityHashMap theOriginalRequestOrder, List theEntries) { - Set deletedResources = new HashSet<>(); - List deleteConflicts = new ArrayList<>(); - Map entriesToProcess = new IdentityHashMap<>(); - Set nonUpdatedEntities = new HashSet<>(); - Set updatedEntities = new HashSet<>(); - Map> conditionalRequestUrls = new HashMap<>(); - - /* - * Loop through the request and process any entries of type - * PUT, POST or DELETE - */ - for (int i = 0; i < theEntries.size(); i++) { - - if (i % 100 == 0) { - ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size()); - } - - BundleEntryComponent nextReqEntry = theEntries.get(i); - Resource res = nextReqEntry.getResource(); - IdType nextResourceId = null; - if (res != null) { - - nextResourceId = res.getIdElement(); - - if (!nextResourceId.hasIdPart()) { - if (isNotBlank(nextReqEntry.getFullUrl())) { - nextResourceId = new IdType(nextReqEntry.getFullUrl()); - } - } - - if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) { - throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); - } - - if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) { - nextResourceId = new IdType(toResourceName(res.getClass()), nextResourceId.getIdPart()); - res.setId(nextResourceId); - } - - /* - * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness - */ - if (isPlaceholder(nextResourceId)) { - if (!theAllIds.add(nextResourceId)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId)); - } - } else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) { - IdType nextId = nextResourceId.toUnqualifiedVersionless(); - if (!theAllIds.add(nextId)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId)); - } - } - - } - - HTTPVerb verb = nextReqEntry.getRequest().getMethodElement().getValue(); - - String resourceType = res != null ? getContext().getResourceDefinition(res).getName() : null; - BundleEntryComponent nextRespEntry = theResponse.getEntry().get(theOriginalRequestOrder.get(nextReqEntry)); - - switch (verb) { - case POST: { - // CREATE - @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); - res.setId((String) null); - DaoMethodOutcome outcome; - String matchUrl = nextReqEntry.getRequest().getIfNoneExist(); - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - outcome = resourceDao.create(res, matchUrl, false, theRequestDetails); - if (nextResourceId != null) { - handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); - } - entriesToProcess.put(nextRespEntry, outcome.getEntity()); - if (outcome.getCreated() == false) { - nonUpdatedEntities.add(outcome.getEntity()); - } else { - if (isNotBlank(matchUrl)) { - conditionalRequestUrls.put(matchUrl, res.getClass()); - } - } - - break; - } - case DELETE: { - // DELETE - String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); - UrlParts parts = UrlUtil.parseUrl(url); - ca.uhn.fhir.jpa.dao.IFhirResourceDao dao = toDao(parts, verb.toCode(), url); - int status = Constants.STATUS_HTTP_204_NO_CONTENT; - if (parts.getResourceId() != null) { - IdType deleteId = new IdType(parts.getResourceType(), parts.getResourceId()); - if (!deletedResources.contains(deleteId.getValueAsString())) { - DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequestDetails); - if (outcome.getEntity() != null) { - deletedResources.add(deleteId.getValueAsString()); - entriesToProcess.put(nextRespEntry, outcome.getEntity()); - } - } - } else { - String matchUrl = parts.getResourceType() + '?' + parts.getParams(); - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequestDetails); - List allDeleted = deleteOutcome.getDeletedEntities(); - for (ResourceTable deleted : allDeleted) { - deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString()); - } - if (allDeleted.isEmpty()) { - status = Constants.STATUS_HTTP_204_NO_CONTENT; - } - - nextRespEntry.getResponse().setOutcome((Resource) deleteOutcome.getOperationOutcome()); - } - - nextRespEntry.getResponse().setStatus(toStatusString(status)); - - break; - } - case PUT: { - // UPDATE - @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); - - String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); - - DaoMethodOutcome outcome; - UrlParts parts = UrlUtil.parseUrl(url); - if (isNotBlank(parts.getResourceId())) { - String version = null; - if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) { - version = ParameterUtil.parseETagValue(nextReqEntry.getRequest().getIfMatch()); - } - res.setId(new IdType(parts.getResourceType(), parts.getResourceId(), version)); - outcome = resourceDao.update(res, null, false, theRequestDetails); - } else { - res.setId((String) null); - String matchUrl; - if (isNotBlank(parts.getParams())) { - matchUrl = parts.getResourceType() + '?' + parts.getParams(); - } else { - matchUrl = parts.getResourceType(); - } - matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); - outcome = resourceDao.update(res, matchUrl, false, theRequestDetails); - if (Boolean.TRUE.equals(outcome.getCreated())) { - conditionalRequestUrls.put(matchUrl, res.getClass()); - } - } - - if (outcome.getCreated() == Boolean.FALSE) { - updatedEntities.add(outcome.getEntity()); - } - - handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails); - entriesToProcess.put(nextRespEntry, outcome.getEntity()); - break; - } - case GET: - case NULL: - case HEAD: - case PATCH: - break; - - } - } - - /* - * Make sure that there are no conflicts from deletions. E.g. we can't delete something - * if something else has a reference to it.. Unless the thing that has a reference to it - * was also deleted as a part of this transaction, which is why we check this now at the - * end. - */ - - deleteConflicts.removeIf(next -> - deletedResources.contains(next.getTargetId().toUnqualifiedVersionless().getValue())); - validateDeleteConflictsEmptyOrThrowException(deleteConflicts); - - /* - * Perform ID substitutions and then index each resource we have saved - */ - - FhirTerser terser = getContext().newTerser(); - for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { - IBaseResource nextResource = nextOutcome.getResource(); - if (nextResource == null) { - continue; - } - - // References - List allRefs = terser.getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class); - for (IBaseReference nextRef : allRefs) { - IIdType nextId = nextRef.getReferenceElement(); - if (!nextId.hasIdPart()) { - continue; - } - if (theIdSubstitutions.containsKey(nextId)) { - IdType newId = theIdSubstitutions.get(nextId); - ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); - nextRef.setReference(newId.getValue()); - } else if (nextId.getValue().startsWith("urn:")) { - throw new InvalidRequestException("Unable to satisfy placeholder ID: " + nextId.getValue()); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); - } - } - - // URIs - List allUris = terser.getAllPopulatedChildElementsOfType(nextResource, UriType.class); - for (UriType nextRef : allUris) { - if (nextRef instanceof IIdType) { - continue; // No substitution on the resource ID itself! - } - IdType nextUriString = new IdType(nextRef.getValueAsString()); - if (theIdSubstitutions.containsKey(nextUriString)) { - IdType newId = theIdSubstitutions.get(nextUriString); - ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); - nextRef.setValue(newId.getValue()); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); - } - } - - IPrimitiveType deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource); - Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; - - if (updatedEntities.contains(nextOutcome.getEntity())) { - updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource()); - } else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) { - updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true); - } - } - - flushJpaSession(); - - /* - * Double check we didn't allow any duplicates we shouldn't have - */ - for (Entry> nextEntry : conditionalRequestUrls.entrySet()) { - String matchUrl = nextEntry.getKey(); - Class resType = nextEntry.getValue(); - if (isNotBlank(matchUrl)) { - IFhirResourceDao resourceDao = getDao(resType); - Set val = resourceDao.processMatchUrl(matchUrl); - if (val.size() > 1) { - throw new InvalidRequestException( - "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); - } - } - } - - for (IdType next : theAllIds) { - IdType replacement = theIdSubstitutions.get(next); - if (replacement == null) { - continue; - } - if (replacement.equals(next)) { - continue; - } - ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); - } - return entriesToProcess; - } - - private String extractTransactionUrlOrThrowException(BundleEntryComponent nextEntry, HTTPVerb verb) { - String url = nextEntry.getRequest().getUrl(); - if (isBlank(url)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name())); - } - return url; - } - - /** - * This method is called for nested bundles (e.g. if we received a transaction with an entry that - * was a GET search, this method is called on the bundle for the search result, that will be placed in the - * outer bundle). This method applies the _summary and _content parameters to the output of - * that bundle. - *

- * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future. - */ - private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) { - IParser p = getContext().newJsonParser(); - RestfulServerUtils.configureResponseParser(theRequestDetails, p); - return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource)); + @PostConstruct + public void start() { + myTransactionProcessor.setDao(this); } @@ -617,53 +64,6 @@ public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao { return toMeta(tagDefinitions); } - private String performIdSubstitutionsInMatchUrl(Map theIdSubstitutions, String theMatchUrl) { - String matchUrl = theMatchUrl; - if (isNotBlank(matchUrl)) { - for (Entry nextSubstitutionEntry : theIdSubstitutions.entrySet()) { - IdType nextTemporaryId = nextSubstitutionEntry.getKey(); - IdType nextReplacementId = nextSubstitutionEntry.getValue(); - String nextTemporaryIdPart = nextTemporaryId.getIdPart(); - String nextReplacementIdPart = nextReplacementId.getValueAsString(); - if (nextTemporaryId.isUrn() && nextTemporaryIdPart.length() > IdType.URN_PREFIX.length()) { - matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart); - matchUrl = matchUrl.replace(UrlUtil.escapeUrlParam(nextTemporaryIdPart), nextReplacementIdPart); - } - } - } - return matchUrl; - } - - private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BundleEntryComponent nextEntry) { - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setSeverity(IssueSeverity.ERROR).setDiagnostics(caughtEx.getMessage()); - nextEntry.getResponse().setOutcome(oo); - } - - private ca.uhn.fhir.jpa.dao.IFhirResourceDao toDao(UrlParts theParts, String theVerb, String theUrl) { - RuntimeResourceDefinition resType; - try { - resType = getContext().getResourceDefinition(theParts.getResourceType()); - } catch (DataFormatException e) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - throw new InvalidRequestException(msg); - } - IFhirResourceDao dao = null; - if (resType != null) { - dao = getDao(resType.getImplementingClass()); - } - if (dao == null) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - throw new InvalidRequestException(msg); - } - - // if (theParts.getResourceId() == null && theParts.getParams() == null) { - // String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - // throw new InvalidRequestException(msg); - // } - - return dao; - } protected Meta toMeta(Collection tagDefinitions) { Meta retVal = new Meta(); @@ -686,193 +86,7 @@ public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao { @Transactional(propagation = Propagation.NEVER) @Override public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { - if (theRequestDetails != null) { - ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null); - notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); - } - - String actionName = "Transaction"; - return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName); - } - - private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) { - super.markRequestAsProcessingSubRequest(theRequestDetails); - try { - return doTransaction(theRequestDetails, theRequest, theActionName); - } finally { - super.clearRequestAsProcessingSubRequest(theRequestDetails); - } - } - - private static void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome, - BundleEntryComponent newEntry, String theResourceType, IBaseResource theRes, RequestDetails theRequestDetails) { - IdType newId = (IdType) outcome.getId().toUnqualifiedVersionless(); - IdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); - if (newId.equals(resourceId) == false) { - idSubstitutions.put(resourceId, newId); - if (isPlaceholder(resourceId)) { - /* - * The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient. - */ - idSubstitutions.put(new IdType(theResourceType + '/' + resourceId.getValue()), newId); - } - } - idToPersistedOutcome.put(newId, outcome); - if (outcome.getCreated().booleanValue()) { - newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_201_CREATED)); - } else { - newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK)); - } - newEntry.getResponse().setLastModified(((Resource) theRes).getMeta().getLastUpdated()); - - if (theRequestDetails != null) { - if (outcome.getResource() != null) { - String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER); - PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer); - if (preferReturn != null) { - if (preferReturn == PreferReturnEnum.REPRESENTATION) { - newEntry.setResource((Resource) outcome.getResource()); - } - } - } - } - - } - - private static boolean isPlaceholder(IdType theId) { - if (theId != null && theId.getValue() != null) { - if (theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:")) { - return true; - } - } - return false; - } - - private static String toStatusString(int theStatusCode) { - return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); - } - - /** - * Transaction Order, per the spec: - *

- * Process any DELETE interactions - * Process any POST interactions - * Process any PUT interactions - * Process any GET interactions - */ - //@formatter:off - public class TransactionSorter implements Comparator { - - private Set myPlaceholderIds; - - public TransactionSorter(Set thePlaceholderIds) { - myPlaceholderIds = thePlaceholderIds; - } - - @Override - public int compare(BundleEntryComponent theO1, BundleEntryComponent theO2) { - int o1 = toOrder(theO1); - int o2 = toOrder(theO2); - - if (o1 == o2) { - String matchUrl1 = toMatchUrl(theO1); - String matchUrl2 = toMatchUrl(theO2); - if (isBlank(matchUrl1) && isBlank(matchUrl2)) { - return 0; - } - if (isBlank(matchUrl1)) { - return -1; - } - if (isBlank(matchUrl2)) { - return 1; - } - - boolean match1containsSubstitutions = false; - boolean match2containsSubstitutions = false; - for (String nextPlaceholder : myPlaceholderIds) { - if (matchUrl1.contains(nextPlaceholder)) { - match1containsSubstitutions = true; - } - if (matchUrl2.contains(nextPlaceholder)) { - match2containsSubstitutions = true; - } - } - - if (match1containsSubstitutions && match2containsSubstitutions) { - return 0; - } - if (!match1containsSubstitutions && !match2containsSubstitutions) { - return 0; - } - if (match1containsSubstitutions) { - return 1; - } else { - return -1; - } - } - - return o1 - o2; - } - - private String toMatchUrl(BundleEntryComponent theEntry) { - HTTPVerb verb = theEntry.getRequest().getMethod(); - if (verb == HTTPVerb.POST) { - return theEntry.getRequest().getIfNoneExist(); - } - if (verb == HTTPVerb.PUT || verb == HTTPVerb.DELETE) { - String url = extractTransactionUrlOrThrowException(theEntry, verb); - UrlParts parts = UrlUtil.parseUrl(url); - if (isBlank(parts.getResourceId())) { - return parts.getResourceType() + '?' + parts.getParams(); - } - } - return null; - } - - private int toOrder(BundleEntryComponent theO1) { - int o1 = 0; - if (theO1.getRequest().getMethodElement().getValue() != null) { - switch (theO1.getRequest().getMethodElement().getValue()) { - case DELETE: - o1 = 1; - break; - case POST: - o1 = 2; - break; - case PUT: - o1 = 3; - break; - case PATCH: - o1 = 4; - break; - case HEAD: - o1 = 5; - break; - case GET: - o1 = 6; - break; - case NULL: - o1 = 0; - break; - } - } - return o1; - } - - } - - //@formatter:off - - private static class BaseServerResponseExceptionHolder { - private BaseServerResponseException myException; - - public BaseServerResponseException getException() { - return myException; - } - - public void setException(BaseServerResponseException myException) { - this.myException = myException; - } + return myTransactionProcessor.transaction(theRequestDetails, theRequest); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java new file mode 100644 index 00000000000..44b73c08a5d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java @@ -0,0 +1,133 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.TransactionProcessor; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.Date; +import java.util.List; + +public class TransactionProcessorVersionAdapterR4 implements TransactionProcessor.ITransactionProcessorVersionAdapter { + @Override + public void setResponseStatus(Bundle.BundleEntryComponent theBundleEntry, String theStatus) { + theBundleEntry.getResponse().setStatus(theStatus); + } + + @Override + public void setResponseLastModified(Bundle.BundleEntryComponent theBundleEntry, Date theLastModified) { + theBundleEntry.getResponse().setLastModified(theLastModified); + } + + @Override + public void setResource(Bundle.BundleEntryComponent theBundleEntry, IBaseResource theResource) { + theBundleEntry.setResource((Resource) theResource); + } + + @Override + public IBaseResource getResource(Bundle.BundleEntryComponent theBundleEntry) { + return theBundleEntry.getResource(); + } + + @Override + public String getBundleType(Bundle theRequest) { + if (theRequest.getType() == null) { + return null; + } + return theRequest.getTypeElement().getValue().toCode(); + } + + @Override + public void populateEntryWithOperationOutcome(BaseServerResponseException theCaughtEx, Bundle.BundleEntryComponent theEntry) { + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setSeverity(OperationOutcome.IssueSeverity.ERROR).setDiagnostics(theCaughtEx.getMessage()); + theEntry.getResponse().setOutcome(oo); + } + + @Override + public Bundle createBundle(String theBundleType) { + Bundle resp = new Bundle(); + try { + resp.setType(Bundle.BundleType.fromCode(theBundleType)); + } catch (FHIRException theE) { + throw new InternalErrorException("Unknown bundle type: " + theBundleType); + } + return resp; + } + + @Override + public List getEntries(Bundle theRequest) { + return theRequest.getEntry(); + } + + @Override + public void addEntry(Bundle theBundle, Bundle.BundleEntryComponent theEntry) { + theBundle.addEntry(theEntry); + } + + @Override + public Bundle.BundleEntryComponent addEntry(Bundle theBundle) { + return theBundle.addEntry(); + } + + @Override + public String getEntryRequestVerb(Bundle.BundleEntryComponent theEntry) { + String retVal = null; + Bundle.HTTPVerb value = theEntry.getRequest().getMethodElement().getValue(); + if (value != null) { + retVal = value.toCode(); + } + return retVal; + } + + @Override + public String getFullUrl(Bundle.BundleEntryComponent theEntry) { + return theEntry.getFullUrl(); + } + + @Override + public String getEntryIfNoneExist(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getIfNoneExist(); + } + + @Override + public String getEntryRequestUrl(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getUrl(); + } + + @Override + public void setResponseLocation(Bundle.BundleEntryComponent theEntry, String theResponseLocation) { + theEntry.getResponse().setLocation(theResponseLocation); + } + + @Override + public void setResponseETag(Bundle.BundleEntryComponent theEntry, String theEtag) { + theEntry.getResponse().setEtag(theEtag); + } + + @Override + public String getEntryRequestIfMatch(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getIfMatch(); + } + + @Override + public String getEntryRequestIfNoneExist(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getIfNoneExist(); + } + + @Override + public String getEntryRequestIfNoneMatch(Bundle.BundleEntryComponent theEntry) { + return theEntry.getRequest().getIfNoneMatch(); + } + + @Override + public void setResponseOutcome(Bundle.BundleEntryComponent theEntry, IBaseOperationOutcome theOperationOutcome) { + theEntry.getResponse().setOutcome((Resource) theOperationOutcome); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java index 24af953ad46..4c7a7c05070 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java @@ -16,6 +16,8 @@ import ca.uhn.fhir.rest.server.exceptions.*; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.util.TestUtil; import org.apache.commons.io.IOUtils; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; @@ -42,6 +44,7 @@ import java.util.Set; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -2132,6 +2135,30 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { assertNull(nextEntry.getResource()); } + + @Test + public void testTransactionWithUnknownTemnporaryIdReference() { + String methodName = "testTransactionWithUnknownTemnporaryIdReference"; + + Bundle request = new Bundle(); + + Patient p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); + + p = new Patient(); + p.addIdentifier().setSystem("urn:system").setValue(methodName); + p.getManagingOrganization().setReference(IdType.newRandomUuid().getValue()); + request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); + + try { + mySystemDao.transaction(mySrd, request); + fail(); + } catch (InvalidRequestException e) { + assertThat(e.getMessage(), Matchers.matchesPattern("Unable to satisfy placeholder ID urn:uuid:[0-9a-z-]+ found in element named 'managingOrganization' within resource of type: Patient")); + } + } + @Test public void testTransactionSearchWithCount() { String methodName = "testTransactionSearchWithCount"; diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 4733be32e91..d01eb6ab8c2 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -253,6 +253,15 @@ When performing a ConceptMap/$translate operation with reverse="true" in the arguments, the equivalency flag is now set on the response just as it is for a non-reverse lookup. + + When executing a FHIR transaction in JPA server, if the request bundle contains + placeholder IDs references (i.e. "urn:uuid:*" references) that can not be resolved + anywhere else in the bundle, a user friendly error is now returned. Previously, + a cryptic error containing only the UUID was returned. As a part of this change, + transaction processing has now been consolidated into a single codebase for DSTU3 + / R4 (and future) versions of FHIR. This should greatly improve maintainability + and consistency for transaction processing. +