Merge branch 'subscription_cleanup' of github.com:jamesagnew/hapi-fhir into subscription_cleanup
This commit is contained in:
commit
10ecc9bdfe
|
@ -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() {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue