Merge branch 'subscription_cleanup' of github.com:jamesagnew/hapi-fhir into subscription_cleanup

This commit is contained in:
jamesagnew 2018-08-12 13:14:25 -04:00
commit 10ecc9bdfe
17 changed files with 1532 additions and 1760 deletions

View File

@ -3,10 +3,8 @@ package ca.uhn.fhir.jpa.config.dstu3;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.context.ParserOptions;
import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.dstu3.TransactionProcessorVersionAdapterDstu3;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3;
import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3; import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; 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.ctx.IValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.CachingValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.CachingValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.r4.utils.IResourceValidator; import org.hl7.fhir.r4.utils.IResourceValidator;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -70,6 +69,16 @@ public class BaseDstu3Config extends BaseConfig {
return retVal; return retVal;
} }
@Bean
public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() {
return new TransactionProcessorVersionAdapterDstu3();
}
@Bean
public TransactionProcessor<Bundle, Bundle.BundleEntryComponent> transactionProcessor() {
return new TransactionProcessor<>();
}
@Bean(name = "myInstanceValidatorDstu3") @Bean(name = "myInstanceValidatorDstu3")
@Lazy @Lazy
public IValidatorModule instanceValidatorDstu3() { public IValidatorModule instanceValidatorDstu3() {

View File

@ -3,12 +3,10 @@ package ca.uhn.fhir.jpa.config.r4;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ParserOptions; import ca.uhn.fhir.context.ParserOptions;
import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; import ca.uhn.fhir.jpa.dao.*;
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.r4.SearchParamExtractorR4; import ca.uhn.fhir.jpa.dao.r4.SearchParamExtractorR4;
import ca.uhn.fhir.jpa.dao.r4.SearchParamRegistryR4; 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.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4; import ca.uhn.fhir.jpa.provider.r4.TerminologyUploaderProviderR4;
import ca.uhn.fhir.jpa.term.HapiTerminologySvcR4; 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.rest.server.GraphQLProvider;
import org.hl7.fhir.r4.hapi.validation.CachingValidationSupport; import org.hl7.fhir.r4.hapi.validation.CachingValidationSupport;
import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator; 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.GraphQLEngine;
import org.hl7.fhir.r4.utils.IResourceValidator.BestPracticeWarningLevel; import org.hl7.fhir.r4.utils.IResourceValidator.BestPracticeWarningLevel;
import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowire;
@ -73,6 +72,16 @@ public class BaseR4Config extends BaseConfig {
return retVal; return retVal;
} }
@Bean
public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() {
return new TransactionProcessorVersionAdapterR4();
}
@Bean
public TransactionProcessor<Bundle, Bundle.BundleEntryComponent> transactionProcessor() {
return new TransactionProcessor<>();
}
@Bean(name = "myGraphQLProvider") @Bean(name = "myGraphQLProvider")
@Lazy @Lazy
public GraphQLProvider graphQLProvider() { public GraphQLProvider graphQLProvider() {

View File

@ -206,7 +206,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
private ApplicationContext myApplicationContext; private ApplicationContext myApplicationContext;
private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> myResourceTypeToDao; private Map<Class<? extends IBaseResource>, IFhirResourceDao<?>> myResourceTypeToDao;
protected void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { public static void clearRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
if (theRequestDetails != null) { if (theRequestDetails != null) {
theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST);
} }
@ -1156,7 +1156,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return false; return false;
} }
protected void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) { public static void markRequestAsProcessingSubRequest(ServletRequestDetails theRequestDetails) {
if (theRequestDetails != null) { if (theRequestDetails != null) {
theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE); theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE);
} }
@ -1170,7 +1170,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return builder; 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() != null && theRequestDetails.getId().hasResourceType() && isNotBlank(theRequestDetails.getResourceType())) {
if (theRequestDetails.getId().getResourceType().equals(theRequestDetails.getResourceType()) == false) { if (theRequestDetails.getId().getResourceType().equals(theRequestDetails.getResourceType()) == false) {
throw new InternalErrorException( throw new InternalErrorException(
@ -2288,7 +2288,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
} }
} }
protected void validateDeleteConflictsEmptyOrThrowException(List<DeleteConflict> theDeleteConflicts) { public void validateDeleteConflictsEmptyOrThrowException(List<DeleteConflict> theDeleteConflicts) {
if (theDeleteConflicts.isEmpty()) { if (theDeleteConflicts.isEmpty()) {
return; return;
} }

View File

@ -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<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
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<BUNDLE, BUNDLEENTRY> 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<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> 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<IIdType, IIdType> theIdSubstitutions, String theMatchUrl) {
String matchUrl = theMatchUrl;
if (isNotBlank(matchUrl)) {
for (Map.Entry<IIdType, IIdType> 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<BUNDLE> 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<IIdType> allIds = new LinkedHashSet<>();
final Map<IIdType, IIdType> idSubstitutions = new HashMap<>();
final Map<IIdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<>();
List<BUNDLEENTRY> 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<BUNDLEENTRY> getEntries = new ArrayList<>();
final IdentityHashMap<BUNDLEENTRY, Integer> 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<String> placeholderIds = new HashSet<>();
final List<BUNDLEENTRY> 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<BUNDLEENTRY, ResourceTable> entriesToProcess = txManager.execute(status -> {
Map<BUNDLEENTRY, ResourceTable> 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<BUNDLEENTRY, ResourceTable> 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<String, String> paramValues = ArrayListMultimap.create();
requestDetails.setParameters(new HashMap<>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = BaseHapiFhirDao.translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}
for (Map.Entry<String, Collection<String>> 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.
* <p>
* 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<BUNDLEENTRY, ResourceTable> doTransactionWriteOperations(ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set<IIdType> theAllIds,
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, BUNDLE theResponse, IdentityHashMap<BUNDLEENTRY, Integer> theOriginalRequestOrder, List<BUNDLEENTRY> theEntries, StopWatch theTransactionStopWatch) {
Set<String> deletedResources = new HashSet<>();
List<DeleteConflict> deleteConflicts = new ArrayList<>();
Map<BUNDLEENTRY, ResourceTable> entriesToProcess = new IdentityHashMap<>();
Set<ResourceTable> nonUpdatedEntities = new HashSet<>();
Set<ResourceTable> updatedEntities = new HashSet<>();
Map<String, Class<? extends IBaseResource>> 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<? extends IBaseResource> 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<ResourceTable> 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<ResourceReferenceInfo> 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<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>) myContext.getElementDefinition("uri").getImplementingClass();
List<? extends IPrimitiveType<?>> 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<Date> 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<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
String matchUrl = nextEntry.getKey();
Class<? extends IBaseResource> resType = nextEntry.getValue();
if (isNotBlank(matchUrl)) {
IFhirResourceDao<?> resourceDao = myDao.getDao(resType);
Set<Long> 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<? extends IBaseResource> 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<? extends IBaseResource> 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<? extends IBaseResource> 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<? extends IBaseResource> 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<BUNDLE, BUNDLEENTRY> {
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<BUNDLEENTRY> 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:
* <p>
* Process any DELETE interactions
* Process any POST interactions
* Process any PUT interactions
* Process any GET interactions
*/
//@formatter:off
public class TransactionSorter implements Comparator<BUNDLEENTRY> {
private Set<String> myPlaceholderIds;
public TransactionSorter(Set<String> 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;
}
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,610 +20,34 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.TransactionProcessor;
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.entity.TagDefinition; 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.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; 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.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding; import org.hl7.fhir.dstu3.model.Bundle;
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.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryResponseComponent; import org.hl7.fhir.dstu3.model.Meta;
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.springframework.beans.factory.annotation.Autowired; 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.Propagation;
import org.springframework.transaction.annotation.Transactional; 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 javax.persistence.TypedQuery;
import java.util.*; import java.util.Collection;
import java.util.Map.Entry; import java.util.List;
import static org.apache.commons.lang3.StringUtils.*;
public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> { public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu3.class);
@Autowired @Autowired
private PlatformTransactionManager myTxManager; private TransactionProcessor<Bundle, BundleEntryComponent> myTransactionProcessor;
private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) { @PostConstruct
ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size()); public void start() {
long start = System.currentTimeMillis(); myTransactionProcessor.setDao(this);
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<Bundle> callback = new TransactionCallback<Bundle>() {
@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());
final Date updateTime = new Date();
final StopWatch transactionStopWatch = new StopWatch();
final Set<IdType> allIds = new LinkedHashSet<>();
final Map<IdType, IdType> idSubstitutions = new HashMap<>();
final Map<IdType, DaoMethodOutcome> 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<BundleEntryComponent> getEntries = new ArrayList<BundleEntryComponent>();
final IdentityHashMap<BundleEntryComponent, Integer> originalRequestOrder = new IdentityHashMap<Bundle.BundleEntryComponent, Integer>();
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<String> placeholderIds = new HashSet<>();
final List<BundleEntryComponent> 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<BundleEntryComponent, ResourceTable> entriesToProcess = txManager.execute(new TransactionCallback<Map<BundleEntryComponent, ResourceTable>>() {
@Override
public Map<BundleEntryComponent, ResourceTable> doInTransaction(TransactionStatus status) {
Map<BundleEntryComponent, ResourceTable> retVal = doTransactionWriteOperations(theRequestDetails, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries, transactionStopWatch);
transactionStopWatch.startTask("Commit writes to database");
return retVal;
}
});
transactionStopWatch.endCurrentTask();
for (Entry<BundleEntryComponent, ResourceTable> 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<String, String> paramValues = ArrayListMultimap.create();
requestDetails.setParameters(new HashMap<>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}
for (Entry<String, Collection<String>> 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<BundleEntryComponent, ResourceTable> doTransactionWriteOperations(ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set<IdType> theAllIds,
Map<IdType, IdType> theIdSubstitutions, Map<IdType, DaoMethodOutcome> theIdToPersistedOutcome, Bundle theResponse, IdentityHashMap<BundleEntryComponent, Integer> theOriginalRequestOrder, List<BundleEntryComponent> theEntries, StopWatch theTransactionStopWatch) {
Set<String> deletedResources = new HashSet<>();
List<DeleteConflict> deleteConflicts = new ArrayList<>();
Map<BundleEntryComponent, ResourceTable> entriesToProcess = new IdentityHashMap<>();
Set<ResourceTable> nonUpdatedEntities = new HashSet<>();
Set<ResourceTable> updatedEntities = new HashSet<>();
Map<String, Class<? extends IBaseResource>> 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<? extends IBaseResource> 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<ResourceTable> 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<IBaseReference> 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<UriType> 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<Date> 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<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
String matchUrl = nextEntry.getKey();
Class<? extends IBaseResource> resType = nextEntry.getValue();
if (isNotBlank(matchUrl)) {
IFhirResourceDao<?> resourceDao = getDao(resType);
Set<Long> 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.
* <p>
* 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 @Override
public Meta metaGetOperation(RequestDetails theRequestDetails) { public Meta metaGetOperation(RequestDetails theRequestDetails) {
// Notify interceptors // Notify interceptors
@ -634,60 +58,10 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class); TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
List<TagDefinition> tagDefinitions = q.getResultList(); List<TagDefinition> tagDefinitions = q.getResultList();
Meta retVal = toMeta(tagDefinitions); return toMeta(tagDefinitions);
return retVal;
} }
private String performIdSubstitutionsInMatchUrl(Map<IdType, IdType> theIdSubstitutions, String theMatchUrl) { private Meta toMeta(Collection<TagDefinition> tagDefinitions) {
String matchUrl = theMatchUrl;
if (isNotBlank(matchUrl)) {
for (Entry<IdType, IdType> 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<? extends IBaseResource> 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<? extends IBaseResource> 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<TagDefinition> tagDefinitions) {
Meta retVal = new Meta(); Meta retVal = new Meta();
for (TagDefinition next : tagDefinitions) { for (TagDefinition next : tagDefinitions) {
switch (next.getTagType()) { switch (next.getTagType()) {
@ -708,187 +82,8 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@Override @Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
if (theRequestDetails != null) { return myTransactionProcessor.transaction(theRequestDetails, theRequest);
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<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> 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:
* <p>
* Process any DELETE interactions
* Process any POST interactions
* Process any PUT interactions
* Process any GET interactions
*/
//@formatter:off
public class TransactionSorter implements Comparator<BundleEntryComponent> {
private Set<String> myPlaceholderIds;
public TransactionSorter(Set<String> 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;
}
}
} }

View File

@ -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<Bundle, Bundle.BundleEntryComponent> {
@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<Bundle.BundleEntryComponent> 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);
}
}

View File

@ -143,6 +143,10 @@ public class FhirResourceDaoConceptMapR4 extends FhirResourceDaoR4<ConceptMap> i
translationMatch.setSource(new UriType(element.getConceptMapUrl())); translationMatch.setSource(new UriType(element.getConceptMapUrl()));
if (element.getConceptMapGroupElementTargets().size() == 1) {
translationMatch.setEquivalence(new CodeType(element.getConceptMapGroupElementTargets().get(0).getEquivalence().toCode()));
}
retVal.addMatch(translationMatch); retVal.addMatch(translationMatch);
} }
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao.r4;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,587 +20,34 @@ package ca.uhn.fhir.jpa.dao.r4;
* #L% * #L%
*/ */
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.TransactionProcessor;
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.entity.TagDefinition; 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.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; 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.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding; import org.hl7.fhir.r4.model.Bundle;
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.BundleEntryComponent; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Bundle.BundleEntryResponseComponent; import org.hl7.fhir.r4.model.Meta;
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.springframework.beans.factory.annotation.Autowired; 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.Propagation;
import org.springframework.transaction.annotation.Transactional; 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 javax.persistence.TypedQuery;
import java.util.*; import java.util.Collection;
import java.util.Map.Entry; import java.util.List;
import static org.apache.commons.lang3.StringUtils.*;
public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> { public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR4.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR4.class);
@Autowired @Autowired
private PlatformTransactionManager myTxManager; private TransactionProcessor<Bundle, BundleEntryComponent> myTransactionProcessor;
private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) { @PostConstruct
ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size()); public void start() {
long start = System.currentTimeMillis(); myTransactionProcessor.setDao(this);
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<Bundle> callback = new TransactionCallback<Bundle>() {
@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<IdType> allIds = new LinkedHashSet<>();
final Map<IdType, IdType> idSubstitutions = new HashMap<>();
final Map<IdType, DaoMethodOutcome> 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<BundleEntryComponent> getEntries = new ArrayList<>();
final IdentityHashMap<BundleEntryComponent, Integer> 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<String> placeholderIds = new HashSet<String>();
final List<BundleEntryComponent> 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<BundleEntryComponent, ResourceTable> entriesToProcess = txManager.execute(new TransactionCallback<Map<BundleEntryComponent, ResourceTable>>() {
@Override
public Map<BundleEntryComponent, ResourceTable> doInTransaction(TransactionStatus status) {
return doTransactionWriteOperations(theRequestDetails, theRequest, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries);
}
});
for (Entry<BundleEntryComponent, ResourceTable> 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<String, String> paramValues = ArrayListMultimap.create();
requestDetails.setParameters(new HashMap<String, String[]>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}
for (java.util.Map.Entry<String, Collection<String>> 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<BundleEntryComponent, ResourceTable> doTransactionWriteOperations(RequestDetails theRequestDetails, Bundle theRequest, String theActionName, Date theUpdateTime, Set<IdType> theAllIds,
Map<IdType, IdType> theIdSubstitutions, Map<IdType, DaoMethodOutcome> theIdToPersistedOutcome, Bundle theResponse, IdentityHashMap<BundleEntryComponent, Integer> theOriginalRequestOrder, List<BundleEntryComponent> theEntries) {
Set<String> deletedResources = new HashSet<>();
List<DeleteConflict> deleteConflicts = new ArrayList<>();
Map<BundleEntryComponent, ResourceTable> entriesToProcess = new IdentityHashMap<>();
Set<ResourceTable> nonUpdatedEntities = new HashSet<>();
Set<ResourceTable> updatedEntities = new HashSet<>();
Map<String, Class<? extends IBaseResource>> 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<? extends IBaseResource> 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<ResourceTable> 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<IBaseReference> 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<UriType> 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<Date> 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<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
String matchUrl = nextEntry.getKey();
Class<? extends IBaseResource> resType = nextEntry.getValue();
if (isNotBlank(matchUrl)) {
IFhirResourceDao<?> resourceDao = getDao(resType);
Set<Long> 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.
* <p>
* 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));
} }
@ -617,53 +64,6 @@ public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> {
return toMeta(tagDefinitions); return toMeta(tagDefinitions);
} }
private String performIdSubstitutionsInMatchUrl(Map<IdType, IdType> theIdSubstitutions, String theMatchUrl) {
String matchUrl = theMatchUrl;
if (isNotBlank(matchUrl)) {
for (Entry<IdType, IdType> 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<? extends IBaseResource> 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<? extends IBaseResource> 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<TagDefinition> tagDefinitions) { protected Meta toMeta(Collection<TagDefinition> tagDefinitions) {
Meta retVal = new Meta(); Meta retVal = new Meta();
@ -686,193 +86,7 @@ public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> {
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
@Override @Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
if (theRequestDetails != null) { return myTransactionProcessor.transaction(theRequestDetails, theRequest);
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<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> 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:
* <p>
* Process any DELETE interactions
* Process any POST interactions
* Process any PUT interactions
* Process any GET interactions
*/
//@formatter:off
public class TransactionSorter implements Comparator<BundleEntryComponent> {
private Set<String> myPlaceholderIds;
public TransactionSorter(Set<String> 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;
}
} }
} }

View File

@ -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<Bundle, Bundle.BundleEntryComponent> {
@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<Bundle.BundleEntryComponent> 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);
}
}

View File

@ -1343,14 +1343,18 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
predicates = new ArrayList<>(); predicates = new ArrayList<>();
coding = translationQuery.getCoding(); coding = translationQuery.getCoding();
String targetCode = null;
String targetCodeSystem = null;
if (coding.hasCode()) { if (coding.hasCode()) {
predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode())); predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode()));
targetCode = coding.getCode();
} else { } else {
throw new InvalidRequestException("A code must be provided for translation to occur."); throw new InvalidRequestException("A code must be provided for translation to occur.");
} }
if (coding.hasSystem()) { if (coding.hasSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem())); predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem()));
targetCodeSystem = coding.getSystem();
} }
if (coding.hasVersion()) { if (coding.hasVersion()) {
@ -1384,7 +1388,24 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
Iterator<TermConceptMapGroupElement> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults); Iterator<TermConceptMapGroupElement> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults);
while (scrollableResultsIterator.hasNext()) { while (scrollableResultsIterator.hasNext()) {
elements.add(scrollableResultsIterator.next()); TermConceptMapGroupElement nextElement = scrollableResultsIterator.next();
nextElement.getConceptMapGroupElementTargets().size();
myEntityManager.detach(nextElement);
if (isNotBlank(targetCode) && isNotBlank(targetCodeSystem)) {
for (Iterator<TermConceptMapGroupElementTarget> iter = nextElement.getConceptMapGroupElementTargets().iterator(); iter.hasNext(); ) {
TermConceptMapGroupElementTarget next = iter.next();
if (targetCodeSystem.equals(next.getSystem())) {
if (targetCode.equals(next.getCode())) {
continue;
}
}
iter.remove();
}
}
elements.add(nextElement);
} }
ourLastResultsFromTranslationWithReverseCache = false; // For testing. ourLastResultsFromTranslationWithReverseCache = false; // For testing.

View File

@ -395,6 +395,12 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
target.setDisplay("Target Code 45678"); target.setDisplay("Target Code 45678");
target.setEquivalence(ConceptMapEquivalence.WIDER); target.setEquivalence(ConceptMapEquivalence.WIDER);
// Add a duplicate
target = element.addTarget();
target.setCode("45678");
target.setDisplay("Target Code 45678");
target.setEquivalence(ConceptMapEquivalence.WIDER);
group = conceptMap.addGroup(); group = conceptMap.addGroup();
group.setSource(CS_URL); group.setSource(CS_URL);
group.setSourceVersion("Version 3"); group.setSourceVersion("Version 3");

View File

@ -613,7 +613,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(1, translationResult.getMatches().size()); assertEquals(1, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0); TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence()); assertEquals("narrower", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept(); Coding concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode()); assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay()); assertEquals("Source Code 78901", concept.getDisplay());
@ -625,6 +625,49 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
}); });
} }
@Test
public void testTranslateWithReverseHavingEquivalence() {
ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId);
ourLog.info("ConceptMap:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap));
new TransactionTemplate(myTxManager).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theStatus) {
/*
* Provided:
* source code
* source code system
* target code system
* reverse = true
*/
TranslationRequest translationRequest = new TranslationRequest();
translationRequest.getCodeableConcept().addCoding()
.setSystem(CS_URL_3)
.setCode("67890");
translationRequest.setTargetSystem(new UriType(CS_URL));
translationRequest.setReverse(true);
TranslationResult translationResult = myConceptMapDao.translate(translationRequest, null);
assertTrue(translationResult.getResult().booleanValue());
assertEquals("Matches found!", translationResult.getMessage().getValueAsString());
assertEquals(1, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay());
assertEquals(CS_URL, concept.getSystem());
assertEquals("Version 3", concept.getVersion());
assertFalse(concept.getUserSelected());
assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
assertEquals("wider", translationMatch.getEquivalence().getCode());
}
});
}
@Test @Test
public void testTranslateWithReverseByCodeSystemsAndSourceCodeUnmapped() { public void testTranslateWithReverseByCodeSystemsAndSourceCodeUnmapped() {
ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId); ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId);
@ -680,7 +723,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size()); assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0); TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence()); assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept(); Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode()); assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay()); assertEquals("Source Code 12345", concept.getDisplay());
@ -690,7 +733,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1); translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence()); assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept(); concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode()); assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay()); assertEquals("Source Code 78901", concept.getDisplay());
@ -733,7 +776,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(1, translationResult.getMatches().size()); assertEquals(1, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0); TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence()); assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept(); Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode()); assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay()); assertEquals("Source Code 12345", concept.getDisplay());
@ -776,7 +819,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(1, translationResult.getMatches().size()); assertEquals(1, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0); TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence()); assertEquals("narrower", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept(); Coding concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode()); assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay()); assertEquals("Source Code 78901", concept.getDisplay());
@ -817,7 +860,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size()); assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0); TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence()); assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept(); Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode()); assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay()); assertEquals("Source Code 12345", concept.getDisplay());
@ -827,7 +870,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1); translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence()); assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept(); concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode()); assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay()); assertEquals("Source Code 78901", concept.getDisplay());
@ -870,7 +913,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size()); assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0); TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence()); assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept(); Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode()); assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay()); assertEquals("Source Code 12345", concept.getDisplay());
@ -880,7 +923,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1); translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence()); assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept(); concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode()); assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay()); assertEquals("Source Code 78901", concept.getDisplay());
@ -921,7 +964,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size()); assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0); TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence()); assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept(); Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode()); assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay()); assertEquals("Source Code 12345", concept.getDisplay());
@ -931,7 +974,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1); translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence()); assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept(); concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode()); assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay()); assertEquals("Source Code 78901", concept.getDisplay());
@ -972,7 +1015,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size()); assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0); TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence()); assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept(); Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode()); assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay()); assertEquals("Source Code 12345", concept.getDisplay());
@ -982,7 +1025,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString()); assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1); translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence()); assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept(); concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode()); assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay()); assertEquals("Source Code 78901", concept.getDisplay());

View File

@ -16,6 +16,7 @@ import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.*;
@ -50,6 +51,11 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR4Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR4Test.class);
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@After @After
public void after() { public void after() {
myDaoConfig.setAllowInlineMatchUrlReferences(false); myDaoConfig.setAllowInlineMatchUrlReferences(false);
@ -187,7 +193,6 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
} }
@Test @Test
public void testBatchCreateWithBadRead() { public void testBatchCreateWithBadRead() {
Bundle request = new Bundle(); Bundle request = new Bundle();
@ -1520,26 +1525,6 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
} }
} }
@Test
public void testTransactionDoesNotAllowDanglingTemporaryIds() throws Exception {
String input = IOUtils.toString(getClass().getResourceAsStream("/cdr-bundle.json"), StandardCharsets.UTF_8);
Bundle bundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, input);
BundleEntryComponent entry = bundle.addEntry();
Patient p = new Patient();
p.getManagingOrganization().setReference("urn:uuid:30ce60cf-f7cb-4196-961f-cadafa8b7ff5");
entry.setResource(p);
entry.getRequest().setMethod(HTTPVerb.POST);
entry.getRequest().setUrl("Patient");
try {
mySystemDao.transaction(mySrd, bundle);
fail();
} catch (InvalidRequestException e) {
assertEquals("Unable to satisfy placeholder ID: urn:uuid:30ce60cf-f7cb-4196-961f-cadafa8b7ff5", e.getMessage());
}
}
@Test @Test
public void testTransactionDoesNotLeavePlaceholderIds() { public void testTransactionDoesNotLeavePlaceholderIds() {
String input; String input;
@ -1624,7 +1609,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar")); map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar"));
search = myPatientDao.search(map); search = myPatientDao.search(map);
assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdPatientId.toUnqualifiedVersionless().getValue())); assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdPatientId.toUnqualifiedVersionless().getValue()));
pat = (Patient) search.getResources(0,1).get(0); pat = (Patient) search.getResources(0, 1).get(0);
assertEquals("foo", pat.getIdentifierFirstRep().getSystem()); assertEquals("foo", pat.getIdentifierFirstRep().getSystem());
// Observation // Observation
map = new SearchParameterMap(); map = new SearchParameterMap();
@ -1632,7 +1617,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog")); map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog"));
search = myObservationDao.search(map); search = myObservationDao.search(map);
assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdObservationId.toUnqualifiedVersionless().getValue())); assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdObservationId.toUnqualifiedVersionless().getValue()));
obs = (Observation) search.getResources(0,1).get(0); obs = (Observation) search.getResources(0, 1).get(0);
assertEquals("foo", obs.getIdentifierFirstRep().getSystem()); assertEquals("foo", obs.getIdentifierFirstRep().getSystem());
assertEquals(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference()); assertEquals(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference());
@ -1689,7 +1674,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar")); map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar"));
search = myPatientDao.search(map); search = myPatientDao.search(map);
assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdPatientId.toUnqualifiedVersionless().getValue())); assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdPatientId.toUnqualifiedVersionless().getValue()));
pat = (Patient) search.getResources(0,1).get(0); pat = (Patient) search.getResources(0, 1).get(0);
assertEquals("foo", pat.getIdentifierFirstRep().getSystem()); assertEquals("foo", pat.getIdentifierFirstRep().getSystem());
// Observation // Observation
map = new SearchParameterMap(); map = new SearchParameterMap();
@ -1697,7 +1682,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog")); map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog"));
search = myObservationDao.search(map); search = myObservationDao.search(map);
assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdObservationId.toUnqualifiedVersionless().getValue())); assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdObservationId.toUnqualifiedVersionless().getValue()));
obs = (Observation) search.getResources(0,1).get(0); obs = (Observation) search.getResources(0, 1).get(0);
assertEquals("foo", obs.getIdentifierFirstRep().getSystem()); assertEquals("foo", obs.getIdentifierFirstRep().getSystem());
assertEquals(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference()); assertEquals(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference());
@ -1755,7 +1740,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar")); map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar"));
search = myPatientDao.search(map); search = myPatientDao.search(map);
assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdPatientId.toUnqualifiedVersionless().getValue())); assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdPatientId.toUnqualifiedVersionless().getValue()));
pat = (Patient) search.getResources(0,1).get(0); pat = (Patient) search.getResources(0, 1).get(0);
assertEquals("foo", pat.getIdentifierFirstRep().getSystem()); assertEquals("foo", pat.getIdentifierFirstRep().getSystem());
// Observation // Observation
map = new SearchParameterMap(); map = new SearchParameterMap();
@ -1763,7 +1748,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog")); map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog"));
search = myObservationDao.search(map); search = myObservationDao.search(map);
assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdObservationId.toUnqualifiedVersionless().getValue())); assertThat(toUnqualifiedVersionlessIdValues(search), contains(createdObservationId.toUnqualifiedVersionless().getValue()));
obs = (Observation) search.getResources(0,1).get(0); obs = (Observation) search.getResources(0, 1).get(0);
assertEquals("foo", obs.getIdentifierFirstRep().getSystem()); assertEquals("foo", obs.getIdentifierFirstRep().getSystem());
assertEquals(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference()); assertEquals(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference());
assertEquals(ObservationStatus.FINAL, obs.getStatus()); assertEquals(ObservationStatus.FINAL, obs.getStatus());
@ -2132,6 +2117,29 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
assertNull(nextEntry.getResource()); 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 @Test
public void testTransactionSearchWithCount() { public void testTransactionSearchWithCount() {
String methodName = "testTransactionSearchWithCount"; String methodName = "testTransactionSearchWithCount";
@ -3047,44 +3055,6 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
assertEquals(1, found.size().intValue()); assertEquals(1, found.size().intValue());
} }
@Test
public void testTransactionWithRelativeOidIds() {
Bundle res = new Bundle();
res.setType(BundleType.TRANSACTION);
Patient p1 = new Patient();
p1.setId("urn:oid:0.1.2.3");
p1.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds01");
res.addEntry().setResource(p1).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient");
Observation o1 = new Observation();
o1.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds02");
o1.setSubject(new Reference("urn:oid:0.1.2.3"));
res.addEntry().setResource(o1).getRequest().setMethod(HTTPVerb.POST).setUrl("Observation");
Observation o2 = new Observation();
o2.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds03");
o2.setSubject(new Reference("urn:oid:0.1.2.3"));
res.addEntry().setResource(o2).getRequest().setMethod(HTTPVerb.POST).setUrl("Observation");
Bundle resp = mySystemDao.transaction(mySrd, res);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals(BundleType.TRANSACTIONRESPONSE, resp.getTypeElement().getValue());
assertEquals(3, resp.getEntry().size());
assertTrue(resp.getEntry().get(0).getResponse().getLocation(), new IdType(resp.getEntry().get(0).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
assertTrue(resp.getEntry().get(1).getResponse().getLocation(), new IdType(resp.getEntry().get(1).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
assertTrue(resp.getEntry().get(2).getResponse().getLocation(), new IdType(resp.getEntry().get(2).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
o1 = myObservationDao.read(new IdType(resp.getEntry().get(1).getResponse().getLocation()), mySrd);
o2 = myObservationDao.read(new IdType(resp.getEntry().get(2).getResponse().getLocation()), mySrd);
assertThat(o1.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart()));
assertThat(o2.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart()));
}
// //
// //
// /** // /**
@ -3187,6 +3157,44 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
// //
// } // }
@Test
public void testTransactionWithRelativeOidIds() {
Bundle res = new Bundle();
res.setType(BundleType.TRANSACTION);
Patient p1 = new Patient();
p1.setId("urn:oid:0.1.2.3");
p1.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds01");
res.addEntry().setResource(p1).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient");
Observation o1 = new Observation();
o1.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds02");
o1.setSubject(new Reference("urn:oid:0.1.2.3"));
res.addEntry().setResource(o1).getRequest().setMethod(HTTPVerb.POST).setUrl("Observation");
Observation o2 = new Observation();
o2.addIdentifier().setSystem("system").setValue("testTransactionWithRelativeOidIds03");
o2.setSubject(new Reference("urn:oid:0.1.2.3"));
res.addEntry().setResource(o2).getRequest().setMethod(HTTPVerb.POST).setUrl("Observation");
Bundle resp = mySystemDao.transaction(mySrd, res);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals(BundleType.TRANSACTIONRESPONSE, resp.getTypeElement().getValue());
assertEquals(3, resp.getEntry().size());
assertTrue(resp.getEntry().get(0).getResponse().getLocation(), new IdType(resp.getEntry().get(0).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
assertTrue(resp.getEntry().get(1).getResponse().getLocation(), new IdType(resp.getEntry().get(1).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
assertTrue(resp.getEntry().get(2).getResponse().getLocation(), new IdType(resp.getEntry().get(2).getResponse().getLocation()).getIdPart().matches("^[0-9]+$"));
o1 = myObservationDao.read(new IdType(resp.getEntry().get(1).getResponse().getLocation()), mySrd);
o2 = myObservationDao.read(new IdType(resp.getEntry().get(2).getResponse().getLocation()), mySrd);
assertThat(o1.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart()));
assertThat(o2.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart()));
}
/** /**
* This is not the correct way to do it, but we'll allow it to be lenient * This is not the correct way to do it, but we'll allow it to be lenient
*/ */
@ -3230,7 +3238,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
@Test @Test
public void testTransactionWithReplacement() { public void testTransactionWithReplacement() {
byte[] bytes = new byte[] {0, 1, 2, 3, 4}; byte[] bytes = new byte[]{0, 1, 2, 3, 4};
Binary binary = new Binary(); Binary binary = new Binary();
binary.setId(IdType.newRandomUuid()); binary.setId(IdType.newRandomUuid());
@ -3399,9 +3407,4 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
} }
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
} }

View File

@ -988,9 +988,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(1, getNumberOfParametersByName(respParams, "match")); assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1069,9 +1069,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(2, getNumberOfParametersByName(respParams, "match")); assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1083,9 +1083,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(1); param = getParametersByName(respParams, "match").get(1);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1135,9 +1135,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(2, getNumberOfParametersByName(respParams, "match")); assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1149,9 +1149,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(1); param = getParametersByName(respParams, "match").get(1);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1205,9 +1205,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(4, getNumberOfParametersByName(respParams, "match")); assertEquals(4, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1219,9 +1219,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(1); param = getParametersByName(respParams, "match").get(1);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1247,9 +1247,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(3); param = getParametersByName(respParams, "match").get(3);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1298,9 +1298,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(2, getNumberOfParametersByName(respParams, "match")); assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1312,9 +1312,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(1); param = getParametersByName(respParams, "match").get(1);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1365,9 +1365,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(2, getNumberOfParametersByName(respParams, "match")); assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1379,9 +1379,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(1); param = getParametersByName(respParams, "match").get(1);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1432,9 +1432,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(1, getNumberOfParametersByName(respParams, "match")); assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1485,9 +1485,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(1, getNumberOfParametersByName(respParams, "match")); assertEquals(1, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1536,9 +1536,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(2, getNumberOfParametersByName(respParams, "match")); assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1550,9 +1550,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(1); param = getParametersByName(respParams, "match").get(1);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1601,9 +1601,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(2, getNumberOfParametersByName(respParams, "match")); assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1615,9 +1615,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(1); param = getParametersByName(respParams, "match").get(1);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());
@ -1659,9 +1659,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(2, getNumberOfParametersByName(respParams, "match")); assertEquals(2, getNumberOfParametersByName(respParams, "match"));
param = getParametersByName(respParams, "match").get(0); param = getParametersByName(respParams, "match").get(0);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
ParametersParameterComponent part = getPartByName(param, "equivalence"); ParametersParameterComponent part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("equal", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
Coding coding = (Coding) part.getValue(); Coding coding = (Coding) part.getValue();
assertEquals("12345", coding.getCode()); assertEquals("12345", coding.getCode());
@ -1673,9 +1673,9 @@ public class ResourceProviderR4ConceptMapTest extends BaseResourceProviderR4Test
assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString()); assertEquals(CM_URL, ((UriType) part.getValue()).getValueAsString());
param = getParametersByName(respParams, "match").get(1); param = getParametersByName(respParams, "match").get(1);
assertEquals(2, param.getPart().size()); assertEquals(3, param.getPart().size());
part = getPartByName(param, "equivalence"); part = getPartByName(param, "equivalence");
assertFalse(part.hasValue()); assertEquals("narrower", ((CodeType)part.getValue()).getCode());
part = getPartByName(param, "concept"); part = getPartByName(param, "concept");
coding = (Coding) part.getValue(); coding = (Coding) part.getValue();
assertEquals("78901", coding.getCode()); assertEquals("78901", coding.getCode());

View File

@ -190,12 +190,22 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test {
assertEquals("Version 1", element.getSystemVersion()); assertEquals("Version 1", element.getSystemVersion());
assertEquals(VS_URL, element.getValueSet()); assertEquals(VS_URL, element.getValueSet());
assertEquals(CM_URL, element.getConceptMapUrl()); assertEquals(CM_URL, element.getConceptMapUrl());
assertEquals(1, element.getConceptMapGroupElementTargets().size());
assertEquals(2, element.getConceptMapGroupElementTargets().size());
target = element.getConceptMapGroupElementTargets().get(0); target = element.getConceptMapGroupElementTargets().get(0);
ourLog.info("ConceptMap.group(0).element(1).target(0):\n" + target.toString()); ourLog.info("ConceptMap.group(0).element(1).target(0):\n" + target.toString());
assertEquals("45678", target.getCode());
assertEquals("Target Code 45678", target.getDisplay());
assertEquals(CS_URL_2, target.getSystem());
assertEquals("Version 2", target.getSystemVersion());
assertEquals(ConceptMapEquivalence.WIDER, target.getEquivalence());
assertEquals(VS_URL_2, target.getValueSet());
assertEquals(CM_URL, target.getConceptMapUrl());
// We had deliberately added a duplicate, and here it is...
target = element.getConceptMapGroupElementTargets().get(1);
ourLog.info("ConceptMap.group(0).element(1).target(1):\n" + target.toString());
assertEquals("45678", target.getCode()); assertEquals("45678", target.getCode());
assertEquals("Target Code 45678", target.getDisplay()); assertEquals("Target Code 45678", target.getDisplay());
assertEquals(CS_URL_2, target.getSystem()); assertEquals(CS_URL_2, target.getSystem());

View File

@ -195,3 +195,38 @@ drop column SP_ID from table HFJ_RES_PARAM_PRESENT;
drop index IDX_SEARCHPARM_RESTYPE_SPNAME; drop index IDX_SEARCHPARM_RESTYPE_SPNAME;
drop index IDX_RESPARMPRESENT_SPID_RESID; drop index IDX_RESPARMPRESENT_SPID_RESID;
drop table HFJ_SEARCH_PARM; drop table HFJ_SEARCH_PARM;
# Delete everything
update hfj_res_ver set forced_id_pid = null where res_id in (select res_id from hfj_resource);
update hfj_resource set forced_id_pid = null where res_id in (select res_id from hfj_resource);
delete from hfj_history_tag where res_id in (select res_id from hfj_resource);
delete from hfj_res_ver where res_id in (select res_id from hfj_resource);
delete from hfj_forced_id where resource_pid in (select res_id from hfj_resource);
delete from hfj_res_link where src_resource_id in (select res_id from hfj_resource);
delete from hfj_res_link where target_resource_id in (select res_id from hfj_resource);
delete from hfj_spidx_coords;
delete from hfj_spidx_date;
delete from hfj_spidx_number;
delete from hfj_spidx_quantity;
delete from hfj_spidx_string;
delete from hfj_spidx_token;
delete from hfj_spidx_uri;
delete from hfj_res_tag;
delete from hfj_search_result;
delete from hfj_res_param_present;
delete from hfj_idx_cmp_string_uniq;
delete from hfj_subscription_stats;
DELETE FROM TRM_CONCEPT_MAP_GRP_ELM_TGT;
DELETE FROM TRM_CONCEPT_MAP_GRP_ELEMENT;
DELETE FROM TRM_CONCEPT_MAP_GROUP;
DELETE FROM TRM_CONCEPT_MAP;
DELETE FROM TRM_CONCEPT_DESIG;
DELETE FROM TRM_CONCEPT_PC_LINK;
DELETE FROM TRM_CONCEPT_PROPERTY;
DELETE FROM TRM_CONCEPT;
DELETE FROM TRM_CODESYSTEM_VER;
DELETE FROM TRM_CODESYSTEM;
delete from hfj_resource;

View File

@ -249,6 +249,19 @@
queue (only the ID), which should reduce the memory/disk footprint of the queue queue (only the ID), which should reduce the memory/disk footprint of the queue
when it grows long. when it grows long.
</action> </action>
<action type="add">
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.
</action>
<action type="add">
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.
</action>
<action type="add"> <action type="add">
ResponseHighlighterInterceptor now displays the total size of the output and ResponseHighlighterInterceptor now displays the total size of the output and
an estimate of the transfer time at the bottom of the response. an estimate of the transfer time at the bottom of the response.