Merge branch 'master' into 1053-lastupdate-shouldnt-apply-to-includes

This commit is contained in:
James Agnew 2018-08-12 17:58:21 -04:00 committed by GitHub
commit f5d330dd03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2133 additions and 2460 deletions

View File

@ -108,7 +108,7 @@ public abstract class BaseCommand implements Comparable<BaseCommand> {
if (theOptions.getOption(theOpt) != null) {
throw new IllegalStateException("Duplicate option: " + theOpt);
}
if (theOptionGroup != null && theOptionGroup.getOptions().stream().anyMatch(t-> theOpt.equals(t.getOpt()))) {
if (theOptionGroup != null && theOptionGroup.getOptions().stream().anyMatch(t -> theOpt.equals(t.getOpt()))) {
throw new IllegalStateException("Duplicate option: " + theOpt);
}
}
@ -116,7 +116,7 @@ public abstract class BaseCommand implements Comparable<BaseCommand> {
if (theOptions.getOption(theLongOpt) != null) {
throw new IllegalStateException("Duplicate option: " + theLongOpt);
}
if (theOptionGroup != null && theOptionGroup.getOptions().stream().anyMatch(t-> theLongOpt.equals(t.getLongOpt()))) {
if (theOptionGroup != null && theOptionGroup.getOptions().stream().anyMatch(t -> theLongOpt.equals(t.getLongOpt()))) {
throw new IllegalStateException("Duplicate option: " + theOpt);
}
}
@ -362,8 +362,12 @@ public abstract class BaseCommand implements Comparable<BaseCommand> {
throw new ParseException("Invalid target server specified, must begin with 'http' or 'file'.");
}
return newClientWithBaseUrl(theCommandLine, baseUrl, theBasicAuthOptionName, theBearerTokenOptionName);
}
protected IGenericClient newClientWithBaseUrl(CommandLine theCommandLine, String theBaseUrl, String theBasicAuthOptionName, String theBearerTokenOptionName) {
myFhirCtx.getRestfulClientFactory().setSocketTimeout(10 * 60 * 1000);
IGenericClient retVal = myFhirCtx.newRestfulGenericClient(baseUrl);
IGenericClient retVal = myFhirCtx.newRestfulGenericClient(theBaseUrl);
String basicAuthHeaderValue = getAndParseOptionBasicAuthHeader(theCommandLine, theBasicAuthOptionName);
if (isNotBlank(basicAuthHeaderValue)) {

View File

@ -25,7 +25,7 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.AbstractHashMapResourceProvider;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import com.google.common.base.Charsets;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
@ -42,7 +42,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This is a subclass to implement FHIR operations specific to DSTU3 ConceptMap
* resources. Its superclass, {@link AbstractHashMapResourceProvider}, is a simple
* resources. Its superclass, {@link HashMapResourceProvider}, is a simple
* implementation of the resource provider interface that uses a HashMap to
* store all resources in memory.
* <p>
@ -53,7 +53,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* <li>Conditional update for DSTU3 ConceptMap resources by ConceptMap.url</li>
* </ul>
*/
public class HashMapResourceProviderConceptMapDstu3 extends AbstractHashMapResourceProvider<ConceptMap> {
public class HashMapResourceProviderConceptMapDstu3 extends HashMapResourceProvider<ConceptMap> {
@SuppressWarnings("unchecked")
public HashMapResourceProviderConceptMapDstu3(FhirContext theFhirContext) {
super(theFhirContext, ConceptMap.class);
@ -84,10 +84,10 @@ public class HashMapResourceProviderConceptMapDstu3 extends AbstractHashMapResou
return retVal;
}
@Override
@Update
public MethodOutcome updateConceptMapConditional(
public MethodOutcome update(
@ResourceParam ConceptMap theConceptMap,
@IdParam IdType theId,
@ConditionalUrlParam String theConditional) {
MethodOutcome methodOutcome = new MethodOutcome();
@ -112,14 +112,14 @@ public class HashMapResourceProviderConceptMapDstu3 extends AbstractHashMapResou
List<ConceptMap> conceptMaps = searchByUrl(url);
if (!conceptMaps.isEmpty()) {
methodOutcome = update(conceptMaps.get(0));
methodOutcome = super.update(conceptMaps.get(0), null);
} else {
methodOutcome = create(theConceptMap);
}
}
} else {
methodOutcome = update(theConceptMap);
methodOutcome = super.update(theConceptMap, null);
}
return methodOutcome;

View File

@ -25,7 +25,7 @@ import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.AbstractHashMapResourceProvider;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import com.google.common.base.Charsets;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
@ -42,7 +42,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This is a subclass to implement FHIR operations specific to R4 ConceptMap
* resources. Its superclass, {@link AbstractHashMapResourceProvider}, is a simple
* resources. Its superclass, {@link HashMapResourceProvider}, is a simple
* implementation of the resource provider interface that uses a HashMap to
* store all resources in memory.
* <p>
@ -53,7 +53,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* <li>Conditional update for R4 ConceptMap resources by ConceptMap.url</li>
* </ul>
*/
public class HashMapResourceProviderConceptMapR4 extends AbstractHashMapResourceProvider<ConceptMap> {
public class HashMapResourceProviderConceptMapR4 extends HashMapResourceProvider<ConceptMap> {
@SuppressWarnings("unchecked")
public HashMapResourceProviderConceptMapR4(FhirContext theFhirContext) {
super(theFhirContext, ConceptMap.class);
@ -84,16 +84,15 @@ public class HashMapResourceProviderConceptMapR4 extends AbstractHashMapResource
return retVal;
}
@Override
@Update
public MethodOutcome updateConceptMapConditional(
public MethodOutcome update(
@ResourceParam ConceptMap theConceptMap,
@IdParam IdType theId,
@ConditionalUrlParam String theConditional) {
MethodOutcome methodOutcome = new MethodOutcome();
if (theConditional != null) {
String url = null;
try {
@ -112,14 +111,14 @@ public class HashMapResourceProviderConceptMapR4 extends AbstractHashMapResource
List<ConceptMap> conceptMaps = searchByUrl(url);
if (!conceptMaps.isEmpty()) {
methodOutcome = update(conceptMaps.get(0));
methodOutcome = super.update(conceptMaps.get(0), null);
} else {
methodOutcome = create(theConceptMap);
}
}
} else {
methodOutcome = update(theConceptMap);
methodOutcome = super.update(theConceptMap, null);
}
return methodOutcome;

View File

@ -50,6 +50,7 @@ import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import javax.annotation.Nonnull;
import java.util.concurrent.ScheduledExecutorService;
@Configuration
@EnableScheduling
@ -100,10 +101,11 @@ public abstract class BaseConfig implements SchedulingConfigurer {
}
@Bean()
public ScheduledExecutorFactoryBean scheduledExecutorService() {
public ScheduledExecutorService scheduledExecutorService() {
ScheduledExecutorFactoryBean b = new ScheduledExecutorFactoryBean();
b.setPoolSize(5);
return b;
b.afterPropertiesSet();
return b.getObject();
}
@Bean(autowire = Autowire.BY_TYPE)
@ -147,8 +149,8 @@ public abstract class BaseConfig implements SchedulingConfigurer {
@Bean(name = TASK_EXECUTOR_NAME)
public TaskScheduler taskScheduler() {
ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler();
retVal.setConcurrentExecutor(scheduledExecutorService().getObject());
retVal.setScheduledExecutor(scheduledExecutorService().getObject());
retVal.setConcurrentExecutor(scheduledExecutorService());
retVal.setScheduledExecutor(scheduledExecutorService());
return retVal;
}

View File

@ -3,10 +3,8 @@ package ca.uhn.fhir.jpa.config.dstu3;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ParserOptions;
import ca.uhn.fhir.jpa.config.BaseConfig;
import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.ISearchParamRegistry;
import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.dstu3.TransactionProcessorVersionAdapterDstu3;
import ca.uhn.fhir.jpa.dao.dstu3.SearchParamExtractorDstu3;
import ca.uhn.fhir.jpa.dao.dstu3.SearchParamRegistryDstu3;
import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3;
@ -21,6 +19,7 @@ import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.CachingValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.r4.utils.IResourceValidator;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
@ -70,6 +69,16 @@ public class BaseDstu3Config extends BaseConfig {
return retVal;
}
@Bean
public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() {
return new TransactionProcessorVersionAdapterDstu3();
}
@Bean
public TransactionProcessor<Bundle, Bundle.BundleEntryComponent> transactionProcessor() {
return new TransactionProcessor<>();
}
@Bean(name = "myInstanceValidatorDstu3")
@Lazy
public IValidatorModule instanceValidatorDstu3() {

View File

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

View File

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

View File

@ -706,9 +706,9 @@ public class DaoConfig {
* references instead of being treated as real references.
* <p>
* A logical reference is a reference which is treated as an identifier, and
* does not neccesarily resolve. See {@link "http://hl7.org/fhir/references.html"} for
* does not neccesarily resolve. See <a href="http://hl7.org/fhir/references.html">references</a> for
* a description of logical references. For example, the valueset
* {@link "http://hl7.org/fhir/valueset-quantity-comparator.html"} is a logical
* <a href="http://hl7.org/fhir/valueset-quantity-comparator.html">valueset-quantity-comparator</a> is a logical
* reference.
* </p>
* <p>
@ -731,9 +731,9 @@ public class DaoConfig {
* references instead of being treated as real references.
* <p>
* A logical reference is a reference which is treated as an identifier, and
* does not neccesarily resolve. See {@link "http://hl7.org/fhir/references.html"} for
* does not neccesarily resolve. See <a href="http://hl7.org/fhir/references.html">references</a> for
* a description of logical references. For example, the valueset
* {@link "http://hl7.org/fhir/valueset-quantity-comparator.html"} is a logical
* <a href="http://hl7.org/fhir/valueset-quantity-comparator.html">valueset-quantity-comparator</a> is a logical
* reference.
* </p>
* <p>

View File

@ -91,6 +91,8 @@ public class SearchBuilder implements ISearchBuilder {
private static Long NO_MORE = -1L;
private static HandlerTypeEnum ourLastHandlerMechanismForUnitTest;
private static SearchParameterMap ourLastHandlerParamsForUnitTest;
private static String ourLastHandlerThreadForUnitTest;
private static boolean ourTrackHandlersForUnitTest;
private List<Long> myAlsoIncludePids;
private CriteriaBuilder myBuilder;
private BaseHapiFhirDao<?> myCallingDao;
@ -1296,8 +1298,11 @@ public class SearchBuilder implements ISearchBuilder {
}
Set<String> uniqueQueryStrings = BaseHapiFhirDao.extractCompositeStringUniquesValueChains(myResourceName, params);
ourLastHandlerParamsForUnitTest = theParams;
ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.UNIQUE_INDEX;
if (ourTrackHandlersForUnitTest) {
ourLastHandlerParamsForUnitTest = theParams;
ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.UNIQUE_INDEX;
ourLastHandlerThreadForUnitTest = Thread.currentThread().getName();
}
return new UniqueIndexIterator(uniqueQueryStrings);
}
@ -1308,8 +1313,11 @@ public class SearchBuilder implements ISearchBuilder {
}
}
ourLastHandlerParamsForUnitTest = theParams;
ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.STANDARD_QUERY;
if (ourTrackHandlersForUnitTest) {
ourLastHandlerParamsForUnitTest = theParams;
ourLastHandlerMechanismForUnitTest = HandlerTypeEnum.STANDARD_QUERY;
ourLastHandlerThreadForUnitTest = Thread.currentThread().getName();
}
return new QueryIterator();
}
@ -1746,7 +1754,7 @@ public class SearchBuilder implements ISearchBuilder {
boolean matchAll = "*".equals(nextInclude.getValue());
if (matchAll) {
String sql;
sql = "SELECT r FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids)";
sql = "SELECT r FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids) ";
TypedQuery<ResourceLink> q = theEntityManager.createQuery(sql, ResourceLink.class);
q.setParameter("target_pids", nextRoundMatches);
List<ResourceLink> results = q.getResultList();
@ -2107,14 +2115,16 @@ public class SearchBuilder implements ISearchBuilder {
}
@VisibleForTesting
public static SearchParameterMap getLastHandlerParamsForUnitTest() {
return ourLastHandlerParamsForUnitTest;
public static String getLastHandlerParamsForUnitTest() {
return ourLastHandlerParamsForUnitTest.toString() + " on thread [" + ourLastHandlerThreadForUnitTest +"]";
}
@VisibleForTesting
public static void resetLastHandlerMechanismForUnitTest() {
ourLastHandlerMechanismForUnitTest = null;
ourLastHandlerParamsForUnitTest = null;
ourLastHandlerThreadForUnitTest = null;
ourTrackHandlersForUnitTest = true;
}
static Predicate[] toArray(List<Predicate> thePredicates) {

View File

@ -0,0 +1,964 @@
package ca.uhn.fhir.jpa.dao;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.collect.ArrayListMultimap;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.*;
public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
public static final String URN_PREFIX = "urn:";
private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class);
private BaseHapiFhirDao myDao;
@Autowired
private PlatformTransactionManager myTxManager;
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
private EntityManager myEntityManager;
@Autowired
private FhirContext myContext;
@Autowired
private ITransactionProcessorVersionAdapter<BUNDLE, BUNDLEENTRY> myVersionAdapter;
public static boolean isPlaceholder(IIdType theId) {
if (theId != null && theId.getValue() != null) {
return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:");
}
return false;
}
private static String toStatusString(int theStatusCode) {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
}
private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) {
myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry);
}
private void handleTransactionCreateOrUpdateOutcome(Map<IIdType, IIdType> idSubstitutions, Map<IIdType, DaoMethodOutcome> idToPersistedOutcome, IIdType nextResourceId, DaoMethodOutcome outcome,
BUNDLEENTRY newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) {
IIdType newId = outcome.getId().toUnqualifiedVersionless();
IIdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
if (newId.equals(resourceId) == false) {
idSubstitutions.put(resourceId, newId);
if (isPlaceholder(resourceId)) {
/*
* The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient.
*/
IIdType id = myContext.getVersion().newIdType();
id.setValue(theResourceType + '/' + resourceId.getValue());
idSubstitutions.put(id, newId);
}
}
idToPersistedOutcome.put(newId, outcome);
if (outcome.getCreated().booleanValue()) {
myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED));
} else {
myVersionAdapter.setResponseStatus(newEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
}
Date lastModifier = getLastModified(theRes);
myVersionAdapter.setResponseLastModified(newEntry, lastModifier);
if (theRequestDetails != null) {
if (outcome.getResource() != null) {
String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER);
PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer);
if (preferReturn != null) {
if (preferReturn == PreferReturnEnum.REPRESENTATION) {
myVersionAdapter.setResource(newEntry, outcome.getResource());
}
}
}
}
}
private Date getLastModified(IBaseResource theRes) {
return theRes.getMeta().getLastUpdated();
}
private String performIdSubstitutionsInMatchUrl(Map<IIdType, IIdType> theIdSubstitutions, String theMatchUrl) {
String matchUrl = theMatchUrl;
if (isNotBlank(matchUrl)) {
for (Map.Entry<IIdType, IIdType> nextSubstitutionEntry : theIdSubstitutions.entrySet()) {
IIdType nextTemporaryId = nextSubstitutionEntry.getKey();
IIdType nextReplacementId = nextSubstitutionEntry.getValue();
String nextTemporaryIdPart = nextTemporaryId.getIdPart();
String nextReplacementIdPart = nextReplacementId.getValueAsString();
if (isUrn(nextTemporaryId) && nextTemporaryIdPart.length() > URN_PREFIX.length()) {
matchUrl = matchUrl.replace(nextTemporaryIdPart, nextReplacementIdPart);
matchUrl = matchUrl.replace(UrlUtil.escapeUrlParam(nextTemporaryIdPart), nextReplacementIdPart);
}
}
}
return matchUrl;
}
private boolean isUrn(IIdType theId) {
return defaultString(theId.getValue()).startsWith(URN_PREFIX);
}
public void setDao(BaseHapiFhirDao theDao) {
myDao = theDao;
}
public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) {
if (theRequestDetails != null) {
IServerInterceptor.ActionRequestDetails requestDetails = new IServerInterceptor.ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
myDao.notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
}
String actionName = "Transaction";
return processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, theRequest, actionName);
}
private BUNDLE processTransactionAsSubRequest(ServletRequestDetails theRequestDetails, BUNDLE theRequest, String theActionName) {
BaseHapiFhirDao.markRequestAsProcessingSubRequest(theRequestDetails);
try {
return processTransaction(theRequestDetails, theRequest, theActionName);
} finally {
BaseHapiFhirDao.clearRequestAsProcessingSubRequest(theRequestDetails);
}
}
private BUNDLE batch(final RequestDetails theRequestDetails, BUNDLE theRequest) {
ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size());
long start = System.currentTimeMillis();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode());
/*
* For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others
*/
for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) {
BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder();
TransactionCallback<BUNDLE> callback = theStatus -> {
BUNDLE subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode());
myVersionAdapter.addEntry(subRequestBundle, nextRequestEntry);
BUNDLE subResponseBundle = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request");
return subResponseBundle;
};
try {
// FIXME: this doesn't need to be a callback
BUNDLE nextResponseBundle = callback.doInTransaction(null);
BUNDLEENTRY subResponseEntry = myVersionAdapter.getEntries(nextResponseBundle).get(0);
myVersionAdapter.addEntry(resp, subResponseEntry);
/*
* If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it
*/
if (myVersionAdapter.getResource(subResponseEntry) == null) {
BUNDLEENTRY nextResponseBundleFirstEntry = myVersionAdapter.getEntries(nextResponseBundle).get(0);
myVersionAdapter.setResource(subResponseEntry, myVersionAdapter.getResource(nextResponseBundleFirstEntry));
}
} catch (BaseServerResponseException e) {
caughtEx.setException(e);
} catch (Throwable t) {
ourLog.error("Failure during BATCH sub transaction processing", t);
caughtEx.setException(new InternalErrorException(t));
}
if (caughtEx.getException() != null) {
BUNDLEENTRY nextEntry = myVersionAdapter.addEntry(resp);
populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry);
myVersionAdapter.setResponseStatus(nextEntry, toStatusString(caughtEx.getException().getStatusCode()));
}
}
long delay = System.currentTimeMillis() - start;
ourLog.info("Batch completed in {}ms", new Object[]{delay});
return resp;
}
private BUNDLE processTransaction(final ServletRequestDetails theRequestDetails, final BUNDLE theRequest, final String theActionName) {
validateDependencies();
String transactionType = myVersionAdapter.getBundleType(theRequest);
if (org.hl7.fhir.r4.model.Bundle.BundleType.BATCH.toCode().equals(transactionType)) {
return batch(theRequestDetails, theRequest);
}
if (transactionType == null) {
String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + Bundle.BundleType.TRANSACTION.toCode();
ourLog.warn(message);
transactionType = org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode();
}
if (!org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode().equals(transactionType)) {
throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType);
}
ourLog.debug("Beginning {} with {} resources", theActionName, myVersionAdapter.getEntries(theRequest).size());
final Date updateTime = new Date();
final StopWatch transactionStopWatch = new StopWatch();
final Set<IIdType> allIds = new LinkedHashSet<>();
final Map<IIdType, IIdType> idSubstitutions = new HashMap<>();
final Map<IIdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<>();
List<BUNDLEENTRY> requestEntries = myVersionAdapter.getEntries(theRequest);
// Do all entries have a verb?
for (int i = 0; i < myVersionAdapter.getEntries(theRequest).size(); i++) {
BUNDLEENTRY nextReqEntry = requestEntries.get(i);
String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry);
if (verb == null || !isValidVerb(verb)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", verb, i));
}
}
/*
* We want to execute the transaction request bundle elements in the order
* specified by the FHIR specification (see TransactionSorter) so we save the
* original order in the request, then sort it.
*
* Entries with a type of GET are removed from the bundle so that they
* can be processed at the very end. We do this because the incoming resources
* are saved in a two-phase way in order to deal with interdependencies, and
* we want the GET processing to use the final indexing state
*/
final BUNDLE response = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTIONRESPONSE.toCode());
List<BUNDLEENTRY> getEntries = new ArrayList<>();
final IdentityHashMap<BUNDLEENTRY, Integer> originalRequestOrder = new IdentityHashMap<>();
for (int i = 0; i < requestEntries.size(); i++) {
originalRequestOrder.put(requestEntries.get(i), i);
myVersionAdapter.addEntry(response);
if (myVersionAdapter.getEntryRequestVerb(requestEntries.get(i)).equals("GET")) {
getEntries.add(requestEntries.get(i));
}
}
/*
* See FhirSystemDaoDstu3Test#testTransactionWithPlaceholderIdInMatchUrl
* Basically if the resource has a match URL that references a placeholder,
* we try to handle the resource with the placeholder first.
*/
Set<String> placeholderIds = new HashSet<>();
final List<BUNDLEENTRY> entries = requestEntries;
for (BUNDLEENTRY nextEntry : entries) {
String fullUrl = myVersionAdapter.getFullUrl(nextEntry);
if (isNotBlank(fullUrl) && fullUrl.startsWith(URN_PREFIX)) {
placeholderIds.add(fullUrl);
}
}
Collections.sort(entries, new TransactionSorter(placeholderIds));
/*
* All of the write operations in the transaction (PUT, POST, etc.. basically anything
* except GET) are performed in their own database transaction before we do the reads.
* We do this because the reads (specifically the searches) often spawn their own
* secondary database transaction and if we allow that within the primary
* database transaction we can end up with deadlocks if the server is under
* heavy load with lots of concurrent transactions using all available
* database connections.
*/
TransactionTemplate txManager = new TransactionTemplate(myTxManager);
Map<BUNDLEENTRY, ResourceTable> entriesToProcess = txManager.execute(status -> {
Map<BUNDLEENTRY, ResourceTable> retVal = doTransactionWriteOperations(theRequestDetails, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, entries, transactionStopWatch);
transactionStopWatch.startTask("Commit writes to database");
return retVal;
});
transactionStopWatch.endCurrentTask();
for (Map.Entry<BUNDLEENTRY, ResourceTable> nextEntry : entriesToProcess.entrySet()) {
String responseLocation = nextEntry.getValue().getIdDt().toUnqualified().getValue();
String responseEtag = nextEntry.getValue().getIdDt().getVersionIdPart();
myVersionAdapter.setResponseLocation(nextEntry.getKey(), responseLocation);
myVersionAdapter.setResponseETag(nextEntry.getKey(), responseEtag);
}
/*
* Loop through the request and process any entries of type GET
*/
if (getEntries.size() > 0) {
transactionStopWatch.startTask("Process " + getEntries.size() + " GET entries");
}
for (BUNDLEENTRY nextReqEntry : getEntries) {
Integer originalOrder = originalRequestOrder.get(nextReqEntry);
BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(response).get(originalOrder);
ServletSubRequestDetails requestDetails = new ServletSubRequestDetails();
requestDetails.setServletRequest(theRequestDetails.getServletRequest());
requestDetails.setRequestType(RequestTypeEnum.GET);
requestDetails.setServer(theRequestDetails.getServer());
String url = extractTransactionUrlOrThrowException(nextReqEntry, "GET");
int qIndex = url.indexOf('?');
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
requestDetails.setParameters(new HashMap<>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = BaseHapiFhirDao.translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}
for (Map.Entry<String, Collection<String>> nextParamEntry : paramValues.asMap().entrySet()) {
String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]);
requestDetails.addParameter(nextParamEntry.getKey(), nextValue);
}
url = url.substring(0, qIndex);
}
requestDetails.setRequestPath(url);
requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase());
theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url);
BaseMethodBinding<?> method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url);
if (method == null) {
throw new IllegalArgumentException("Unable to handle GET " + url);
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_MATCH, myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry));
}
if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry))) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, myVersionAdapter.getEntryRequestIfNoneMatch(nextReqEntry));
}
Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url);
try {
IBaseResource resource = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails);
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
resource = filterNestedBundle(requestDetails, resource);
}
myVersionAdapter.setResource(nextRespEntry, resource);
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_200_OK));
} catch (NotModifiedException e) {
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
} catch (BaseServerResponseException e) {
ourLog.info("Failure processing transaction GET {}: {}", url, e.toString());
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(e.getStatusCode()));
populateEntryWithOperationOutcome(e, nextRespEntry);
}
}
transactionStopWatch.endCurrentTask();
ourLog.info("Transaction timing:\n{}", transactionStopWatch.formatTaskDurations());
return response;
}
private boolean isValidVerb(String theVerb) {
try {
return org.hl7.fhir.r4.model.Bundle.HTTPVerb.fromCode(theVerb) != null;
} catch (FHIRException theE) {
return false;
}
}
/**
* This method is called for nested bundles (e.g. if we received a transaction with an entry that
* was a GET search, this method is called on the bundle for the search result, that will be placed in the
* outer bundle). This method applies the _summary and _content parameters to the output of
* that bundle.
* <p>
* TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
*/
private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
IParser p = myContext.newJsonParser();
RestfulServerUtils.configureResponseParser(theRequestDetails, p);
return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
}
public void setEntityManager(EntityManager theEntityManager) {
myEntityManager = theEntityManager;
}
private void validateDependencies() {
Validate.notNull(myEntityManager);
Validate.notNull(myContext);
Validate.notNull(myDao);
Validate.notNull(myTxManager);
}
private IIdType newIdType(String theValue) {
return myContext.getVersion().newIdType().setValue(theValue);
}
private Map<BUNDLEENTRY, ResourceTable> doTransactionWriteOperations(ServletRequestDetails theRequestDetails, String theActionName, Date theUpdateTime, Set<IIdType> theAllIds,
Map<IIdType, IIdType> theIdSubstitutions, Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, BUNDLE theResponse, IdentityHashMap<BUNDLEENTRY, Integer> theOriginalRequestOrder, List<BUNDLEENTRY> theEntries, StopWatch theTransactionStopWatch) {
Set<String> deletedResources = new HashSet<>();
List<DeleteConflict> deleteConflicts = new ArrayList<>();
Map<BUNDLEENTRY, ResourceTable> entriesToProcess = new IdentityHashMap<>();
Set<ResourceTable> nonUpdatedEntities = new HashSet<>();
Set<ResourceTable> updatedEntities = new HashSet<>();
Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<>();
/*
* Loop through the request and process any entries of type
* PUT, POST or DELETE
*/
for (int i = 0; i < theEntries.size(); i++) {
if (i % 100 == 0) {
ourLog.debug("Processed {} non-GET entries out of {}", i, theEntries.size());
}
BUNDLEENTRY nextReqEntry = theEntries.get(i);
IBaseResource res = myVersionAdapter.getResource(nextReqEntry);
IIdType nextResourceId = null;
if (res != null) {
nextResourceId = res.getIdElement();
if (!nextResourceId.hasIdPart()) {
if (isNotBlank(myVersionAdapter.getFullUrl(nextReqEntry))) {
nextResourceId = newIdType(myVersionAdapter.getFullUrl(nextReqEntry));
}
}
if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+\\:.*") && !isPlaceholder(nextResourceId)) {
throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'");
}
if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) {
nextResourceId = newIdType(toResourceName(res.getClass()), nextResourceId.getIdPart());
res.setId(nextResourceId);
}
/*
* Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness
*/
if (isPlaceholder(nextResourceId)) {
if (!theAllIds.add(nextResourceId)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId));
}
} else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) {
IIdType nextId = nextResourceId.toUnqualifiedVersionless();
if (!theAllIds.add(nextId)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId));
}
}
}
String verb = myVersionAdapter.getEntryRequestVerb(nextReqEntry);
String resourceType = res != null ? myContext.getResourceDefinition(res).getName() : null;
Integer order = theOriginalRequestOrder.get(nextReqEntry);
BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(theResponse).get(order);
theTransactionStopWatch.startTask("Bundle.entry[" + i + "]: " + verb + " " + defaultString(resourceType));
switch (verb) {
case "POST": {
// CREATE
@SuppressWarnings("rawtypes")
IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
res.setId((String) null);
DaoMethodOutcome outcome;
String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry);
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
outcome = resourceDao.create(res, matchUrl, false, theRequestDetails);
if (nextResourceId != null) {
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails);
}
entriesToProcess.put(nextRespEntry, outcome.getEntity());
if (outcome.getCreated() == false) {
nonUpdatedEntities.add(outcome.getEntity());
} else {
if (isNotBlank(matchUrl)) {
conditionalRequestUrls.put(matchUrl, res.getClass());
}
}
break;
}
case "DELETE": {
// DELETE
String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url);
int status = Constants.STATUS_HTTP_204_NO_CONTENT;
if (parts.getResourceId() != null) {
IIdType deleteId = newIdType(parts.getResourceType(), parts.getResourceId());
if (!deletedResources.contains(deleteId.getValueAsString())) {
DaoMethodOutcome outcome = dao.delete(deleteId, deleteConflicts, theRequestDetails);
if (outcome.getEntity() != null) {
deletedResources.add(deleteId.getValueAsString());
entriesToProcess.put(nextRespEntry, outcome.getEntity());
}
}
} else {
String matchUrl = parts.getResourceType() + '?' + parts.getParams();
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(matchUrl, deleteConflicts, theRequestDetails);
List<ResourceTable> allDeleted = deleteOutcome.getDeletedEntities();
for (ResourceTable deleted : allDeleted) {
deletedResources.add(deleted.getIdDt().toUnqualifiedVersionless().getValueAsString());
}
if (allDeleted.isEmpty()) {
status = Constants.STATUS_HTTP_204_NO_CONTENT;
}
myVersionAdapter.setResponseOutcome(nextRespEntry, deleteOutcome.getOperationOutcome());
}
myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(status));
break;
}
case "PUT": {
// UPDATE
@SuppressWarnings("rawtypes")
IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass());
String url = extractTransactionUrlOrThrowException(nextReqEntry, verb);
DaoMethodOutcome outcome;
UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
if (isNotBlank(parts.getResourceId())) {
String version = null;
if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) {
version = ParameterUtil.parseETagValue(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry));
}
res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version));
outcome = resourceDao.update(res, null, false, theRequestDetails);
} else {
res.setId((String) null);
String matchUrl;
if (isNotBlank(parts.getParams())) {
matchUrl = parts.getResourceType() + '?' + parts.getParams();
} else {
matchUrl = parts.getResourceType();
}
matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl);
outcome = resourceDao.update(res, matchUrl, false, theRequestDetails);
if (Boolean.TRUE.equals(outcome.getCreated())) {
conditionalRequestUrls.put(matchUrl, res.getClass());
}
}
if (outcome.getCreated() == Boolean.FALSE) {
updatedEntities.add(outcome.getEntity());
}
handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res, theRequestDetails);
entriesToProcess.put(nextRespEntry, outcome.getEntity());
break;
}
case "GET":
default:
break;
}
theTransactionStopWatch.endCurrentTask();
}
/*
* Make sure that there are no conflicts from deletions. E.g. we can't delete something
* if something else has a reference to it.. Unless the thing that has a reference to it
* was also deleted as a part of this transaction, which is why we check this now at the
* end.
*/
deleteConflicts.removeIf(next ->
deletedResources.contains(next.getTargetId().toUnqualifiedVersionless().getValue()));
myDao.validateDeleteConflictsEmptyOrThrowException(deleteConflicts);
/*
* Perform ID substitutions and then index each resource we have saved
*/
FhirTerser terser = myContext.newTerser();
theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources");
for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) {
IBaseResource nextResource = nextOutcome.getResource();
if (nextResource == null) {
continue;
}
// References
List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(nextResource);
for (ResourceReferenceInfo nextRef : allRefs) {
IIdType nextId = nextRef.getResourceReference().getReferenceElement();
if (!nextId.hasIdPart()) {
continue;
}
if (theIdSubstitutions.containsKey(nextId)) {
IIdType newId = theIdSubstitutions.get(nextId);
ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId);
nextRef.getResourceReference().setReference(newId.getValue());
} else if (nextId.getValue().startsWith("urn:")) {
throw new InvalidRequestException("Unable to satisfy placeholder ID " + nextId.getValue() + " found in element named '" + nextRef.getName() + "' within resource of type: " + nextResource.getIdElement().getResourceType());
} else {
ourLog.debug(" * Reference [{}] does not exist in bundle", nextId);
}
}
// URIs
Class<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>) myContext.getElementDefinition("uri").getImplementingClass();
List<? extends IPrimitiveType<?>> allUris = terser.getAllPopulatedChildElementsOfType(nextResource, uriType);
for (IPrimitiveType<?> nextRef : allUris) {
if (nextRef instanceof IIdType) {
continue; // No substitution on the resource ID itself!
}
IIdType nextUriString = newIdType(nextRef.getValueAsString());
if (theIdSubstitutions.containsKey(nextUriString)) {
IIdType newId = theIdSubstitutions.get(nextUriString);
ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId);
nextRef.setValueAsString(newId.getValue());
} else {
ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString);
}
}
IPrimitiveType<Date> deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource);
Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null;
if (updatedEntities.contains(nextOutcome.getEntity())) {
myDao.updateInternal(theRequestDetails, nextResource, true, false, theRequestDetails, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource());
} else if (!nonUpdatedEntities.contains(nextOutcome.getEntity())) {
myDao.updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theUpdateTime, false, true);
}
}
theTransactionStopWatch.endCurrentTask();
theTransactionStopWatch.startTask("Flush writes to database");
flushJpaSession();
theTransactionStopWatch.endCurrentTask();
if (conditionalRequestUrls.size() > 0) {
theTransactionStopWatch.startTask("Check for conflicts in conditional resources");
}
/*
* Double check we didn't allow any duplicates we shouldn't have
*/
for (Map.Entry<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) {
String matchUrl = nextEntry.getKey();
Class<? extends IBaseResource> resType = nextEntry.getValue();
if (isNotBlank(matchUrl)) {
IFhirResourceDao<?> resourceDao = myDao.getDao(resType);
Set<Long> val = resourceDao.processMatchUrl(matchUrl);
if (val.size() > 1) {
throw new InvalidRequestException(
"Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?");
}
}
}
theTransactionStopWatch.endCurrentTask();
for (IIdType next : theAllIds) {
IIdType replacement = theIdSubstitutions.get(next);
if (replacement == null) {
continue;
}
if (replacement.equals(next)) {
continue;
}
ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement);
}
return entriesToProcess;
}
private IIdType newIdType(String theResourceType, String theResourceId, String theVersion) {
org.hl7.fhir.r4.model.IdType id = new org.hl7.fhir.r4.model.IdType(theResourceType, theResourceId, theVersion);
return myContext.getVersion().newIdType().setValue(id.getValue());
}
private IIdType newIdType(String theToResourceName, String theIdPart) {
return newIdType(theToResourceName, theIdPart, null);
}
private IFhirResourceDao getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
return myDao.getDaoOrThrowException(theClass);
}
protected void flushJpaSession() {
SessionImpl session = (SessionImpl) myEntityManager.unwrap(Session.class);
int insertionCount = session.getActionQueue().numberOfInsertions();
int updateCount = session.getActionQueue().numberOfUpdates();
StopWatch sw = new StopWatch();
myEntityManager.flush();
ourLog.debug("Session flush took {}ms for {} inserts and {} updates", sw.getMillis(), insertionCount, updateCount);
}
protected String toResourceName(Class<? extends IBaseResource> theResourceType) {
return myContext.getResourceDefinition(theResourceType).getName();
}
public void setContext(FhirContext theContext) {
myContext = theContext;
}
private String extractTransactionUrlOrThrowException(BUNDLEENTRY nextEntry, String verb) {
String url = myVersionAdapter.getEntryRequestUrl(nextEntry);
if (isBlank(url)) {
throw new InvalidRequestException(myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb));
}
return url;
}
private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlUtil.UrlParts theParts, String theVerb, String theUrl) {
RuntimeResourceDefinition resType;
try {
resType = myContext.getResourceDefinition(theParts.getResourceType());
} catch (DataFormatException e) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
IFhirResourceDao<? extends IBaseResource> dao = null;
if (resType != null) {
dao = myDao.getDao(resType.getImplementingClass());
}
if (dao == null) {
String msg = myContext.getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
// if (theParts.getResourceId() == null && theParts.getParams() == null) {
// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
// throw new InvalidRequestException(msg);
// }
return dao;
}
public interface ITransactionProcessorVersionAdapter<BUNDLE, BUNDLEENTRY> {
void setResponseStatus(BUNDLEENTRY theBundleEntry, String theStatus);
void setResponseLastModified(BUNDLEENTRY theBundleEntry, Date theLastModified);
void setResource(BUNDLEENTRY theBundleEntry, IBaseResource theResource);
IBaseResource getResource(BUNDLEENTRY theBundleEntry);
String getBundleType(BUNDLE theRequest);
void populateEntryWithOperationOutcome(BaseServerResponseException theCaughtEx, BUNDLEENTRY theEntry);
BUNDLE createBundle(String theBundleType);
List<BUNDLEENTRY> getEntries(BUNDLE theRequest);
void addEntry(BUNDLE theBundle, BUNDLEENTRY theEntry);
BUNDLEENTRY addEntry(BUNDLE theBundle);
String getEntryRequestVerb(BUNDLEENTRY theEntry);
String getFullUrl(BUNDLEENTRY theEntry);
String getEntryIfNoneExist(BUNDLEENTRY theEntry);
String getEntryRequestUrl(BUNDLEENTRY theEntry);
void setResponseLocation(BUNDLEENTRY theEntry, String theResponseLocation);
void setResponseETag(BUNDLEENTRY theEntry, String theEtag);
String getEntryRequestIfMatch(BUNDLEENTRY theEntry);
String getEntryRequestIfNoneExist(BUNDLEENTRY theEntry);
String getEntryRequestIfNoneMatch(BUNDLEENTRY theEntry);
void setResponseOutcome(BUNDLEENTRY theEntry, IBaseOperationOutcome theOperationOutcome);
}
private static class BaseServerResponseExceptionHolder {
private BaseServerResponseException myException;
public BaseServerResponseException getException() {
return myException;
}
public void setException(BaseServerResponseException myException) {
this.myException = myException;
}
}
/**
* Transaction Order, per the spec:
* <p>
* Process any DELETE interactions
* Process any POST interactions
* Process any PUT interactions
* Process any GET interactions
*/
//@formatter:off
public class TransactionSorter implements Comparator<BUNDLEENTRY> {
private Set<String> myPlaceholderIds;
public TransactionSorter(Set<String> thePlaceholderIds) {
myPlaceholderIds = thePlaceholderIds;
}
@Override
public int compare(BUNDLEENTRY theO1, BUNDLEENTRY theO2) {
int o1 = toOrder(theO1);
int o2 = toOrder(theO2);
if (o1 == o2) {
String matchUrl1 = toMatchUrl(theO1);
String matchUrl2 = toMatchUrl(theO2);
if (isBlank(matchUrl1) && isBlank(matchUrl2)) {
return 0;
}
if (isBlank(matchUrl1)) {
return -1;
}
if (isBlank(matchUrl2)) {
return 1;
}
boolean match1containsSubstitutions = false;
boolean match2containsSubstitutions = false;
for (String nextPlaceholder : myPlaceholderIds) {
if (matchUrl1.contains(nextPlaceholder)) {
match1containsSubstitutions = true;
}
if (matchUrl2.contains(nextPlaceholder)) {
match2containsSubstitutions = true;
}
}
if (match1containsSubstitutions && match2containsSubstitutions) {
return 0;
}
if (!match1containsSubstitutions && !match2containsSubstitutions) {
return 0;
}
if (match1containsSubstitutions) {
return 1;
} else {
return -1;
}
}
return o1 - o2;
}
private String toMatchUrl(BUNDLEENTRY theEntry) {
String verb = myVersionAdapter.getEntryRequestVerb(theEntry);
if (verb.equals("POST")) {
return myVersionAdapter.getEntryIfNoneExist(theEntry);
}
if (verb.equals("PUT") || verb.equals("DELETE")) {
String url = extractTransactionUrlOrThrowException(theEntry, verb);
UrlUtil.UrlParts parts = UrlUtil.parseUrl(url);
if (isBlank(parts.getResourceId())) {
return parts.getResourceType() + '?' + parts.getParams();
}
}
return null;
}
private int toOrder(BUNDLEENTRY theO1) {
int o1 = 0;
if (myVersionAdapter.getEntryRequestVerb(theO1) != null) {
switch (myVersionAdapter.getEntryRequestVerb(theO1)) {
case "DELETE":
o1 = 1;
break;
case "POST":
o1 = 2;
break;
case "PUT":
o1 = 3;
break;
case "GET":
o1 = 4;
break;
default:
o1 = 0;
break;
}
}
return o1;
}
}
}

View File

@ -20,610 +20,34 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* #L%
*/
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.UrlUtil.UrlParts;
import com.google.common.collect.ArrayListMultimap;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryResponseComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.dstu3.model.Meta;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
import javax.persistence.TypedQuery;
import java.util.*;
import java.util.Map.Entry;
import static org.apache.commons.lang3.StringUtils.*;
import java.util.Collection;
import java.util.List;
public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu3.class);
@Autowired
private PlatformTransactionManager myTxManager;
private TransactionProcessor<Bundle, BundleEntryComponent> myTransactionProcessor;
private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) {
ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size());
long start = System.currentTimeMillis();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
Bundle resp = new Bundle();
resp.setType(BundleType.BATCHRESPONSE);
/*
* For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others
*/
for (final BundleEntryComponent nextRequestEntry : theRequest.getEntry()) {
BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder();
TransactionCallback<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;
@PostConstruct
public void start() {
myTransactionProcessor.setDao(this);
}
private Bundle doTransaction(final ServletRequestDetails theRequestDetails, final Bundle theRequest, final String theActionName) {
BundleType transactionType = theRequest.getTypeElement().getValue();
if (transactionType == BundleType.BATCH) {
return batch(theRequestDetails, theRequest);
}
if (transactionType == null) {
String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + BundleType.TRANSACTION.toCode();
ourLog.warn(message);
transactionType = BundleType.TRANSACTION;
}
if (transactionType != BundleType.TRANSACTION) {
throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType.toCode());
}
ourLog.debug("Beginning {} with {} resources", theActionName, theRequest.getEntry().size());
final Date updateTime = new Date();
final StopWatch transactionStopWatch = new StopWatch();
final Set<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
public Meta metaGetOperation(RequestDetails theRequestDetails) {
// Notify interceptors
@ -634,60 +58,10 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
List<TagDefinition> tagDefinitions = q.getResultList();
Meta retVal = toMeta(tagDefinitions);
return retVal;
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) {
private Meta toMeta(Collection<TagDefinition> tagDefinitions) {
Meta retVal = new Meta();
for (TagDefinition next : tagDefinitions) {
switch (next.getTagType()) {
@ -708,187 +82,8 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
@Transactional(propagation = Propagation.NEVER)
@Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
}
String actionName = "Transaction";
return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName);
return myTransactionProcessor.transaction(theRequestDetails, theRequest);
}
private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
super.markRequestAsProcessingSubRequest(theRequestDetails);
try {
return doTransaction(theRequestDetails, theRequest, theActionName);
} finally {
super.clearRequestAsProcessingSubRequest(theRequestDetails);
}
}
private static void handleTransactionCreateOrUpdateOutcome(Map<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome,
BundleEntryComponent newEntry, String theResourceType, IBaseResource theRes, ServletRequestDetails theRequestDetails) {
IdType newId = (IdType) outcome.getId().toUnqualifiedVersionless();
IdType resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless();
if (newId.equals(resourceId) == false) {
idSubstitutions.put(resourceId, newId);
if (isPlaceholder(resourceId)) {
/*
* The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient.
*/
idSubstitutions.put(new IdType(theResourceType + '/' + resourceId.getValue()), newId);
}
}
idToPersistedOutcome.put(newId, outcome);
if (outcome.getCreated().booleanValue()) {
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_201_CREATED));
} else {
newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
}
newEntry.getResponse().setLastModified(((Resource) theRes).getMeta().getLastUpdated());
if (theRequestDetails != null) {
if (outcome.getResource() != null) {
String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER);
PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer);
if (preferReturn != null) {
if (preferReturn == PreferReturnEnum.REPRESENTATION) {
newEntry.setResource((Resource) outcome.getResource());
}
}
}
}
}
private static boolean isPlaceholder(IdType theId) {
if (theId != null && theId.getValue() != null) {
if (theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:")) {
return true;
}
}
return false;
}
private static String toStatusString(int theStatusCode) {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
}
/**
* Transaction Order, per the spec:
* <p>
* Process any DELETE interactions
* Process any POST interactions
* Process any PUT interactions
* Process any GET interactions
*/
//@formatter:off
public class TransactionSorter implements Comparator<BundleEntryComponent> {
private Set<String> myPlaceholderIds;
public TransactionSorter(Set<String> thePlaceholderIds) {
myPlaceholderIds = thePlaceholderIds;
}
@Override
public int compare(BundleEntryComponent theO1, BundleEntryComponent theO2) {
int o1 = toOrder(theO1);
int o2 = toOrder(theO2);
if (o1 == o2) {
String matchUrl1 = toMatchUrl(theO1);
String matchUrl2 = toMatchUrl(theO2);
if (isBlank(matchUrl1) && isBlank(matchUrl2)) {
return 0;
}
if (isBlank(matchUrl1)) {
return -1;
}
if (isBlank(matchUrl2)) {
return 1;
}
boolean match1containsSubstitutions = false;
boolean match2containsSubstitutions = false;
for (String nextPlaceholder : myPlaceholderIds) {
if (matchUrl1.contains(nextPlaceholder)) {
match1containsSubstitutions = true;
}
if (matchUrl2.contains(nextPlaceholder)) {
match2containsSubstitutions = true;
}
}
if (match1containsSubstitutions && match2containsSubstitutions) {
return 0;
}
if (!match1containsSubstitutions && !match2containsSubstitutions) {
return 0;
}
if (match1containsSubstitutions) {
return 1;
} else {
return -1;
}
}
return o1 - o2;
}
private String toMatchUrl(BundleEntryComponent theEntry) {
HTTPVerb verb = theEntry.getRequest().getMethod();
if (verb == HTTPVerb.POST) {
return theEntry.getRequest().getIfNoneExist();
}
if (verb == HTTPVerb.PUT || verb == HTTPVerb.DELETE) {
String url = extractTransactionUrlOrThrowException(theEntry, verb);
UrlParts parts = UrlUtil.parseUrl(url);
if (isBlank(parts.getResourceId())) {
return parts.getResourceType() + '?' + parts.getParams();
}
}
return null;
}
private int toOrder(BundleEntryComponent theO1) {
int o1 = 0;
if (theO1.getRequest().getMethodElement().getValue() != null) {
switch (theO1.getRequest().getMethodElement().getValue()) {
case DELETE:
o1 = 1;
break;
case POST:
o1 = 2;
break;
case PUT:
o1 = 3;
break;
case GET:
o1 = 4;
break;
case NULL:
o1 = 0;
break;
}
}
return o1;
}
}
//@formatter:off
private static class BaseServerResponseExceptionHolder {
private BaseServerResponseException myException;
public BaseServerResponseException getException() {
return myException;
}
public void setException(BaseServerResponseException myException) {
this.myException = myException;
}
}
}

View File

@ -0,0 +1,153 @@
package ca.uhn.fhir.jpa.dao.dstu3;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.Date;
import java.util.List;
public class TransactionProcessorVersionAdapterDstu3 implements TransactionProcessor.ITransactionProcessorVersionAdapter<Bundle, Bundle.BundleEntryComponent> {
@Override
public void setResponseStatus(Bundle.BundleEntryComponent theBundleEntry, String theStatus) {
theBundleEntry.getResponse().setStatus(theStatus);
}
@Override
public void setResponseLastModified(Bundle.BundleEntryComponent theBundleEntry, Date theLastModified) {
theBundleEntry.getResponse().setLastModified(theLastModified);
}
@Override
public void setResource(Bundle.BundleEntryComponent theBundleEntry, IBaseResource theResource) {
theBundleEntry.setResource((Resource) theResource);
}
@Override
public IBaseResource getResource(Bundle.BundleEntryComponent theBundleEntry) {
return theBundleEntry.getResource();
}
@Override
public String getBundleType(Bundle theRequest) {
if (theRequest.getType() == null) {
return null;
}
return theRequest.getTypeElement().getValue().toCode();
}
@Override
public void populateEntryWithOperationOutcome(BaseServerResponseException theCaughtEx, Bundle.BundleEntryComponent theEntry) {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setSeverity(OperationOutcome.IssueSeverity.ERROR).setDiagnostics(theCaughtEx.getMessage());
theEntry.getResponse().setOutcome(oo);
}
@Override
public Bundle createBundle(String theBundleType) {
Bundle resp = new Bundle();
try {
resp.setType(Bundle.BundleType.fromCode(theBundleType));
} catch (FHIRException theE) {
throw new InternalErrorException("Unknown bundle type: " + theBundleType);
}
return resp;
}
@Override
public List<Bundle.BundleEntryComponent> getEntries(Bundle theRequest) {
return theRequest.getEntry();
}
@Override
public void addEntry(Bundle theBundle, Bundle.BundleEntryComponent theEntry) {
theBundle.addEntry(theEntry);
}
@Override
public Bundle.BundleEntryComponent addEntry(Bundle theBundle) {
return theBundle.addEntry();
}
@Override
public String getEntryRequestVerb(Bundle.BundleEntryComponent theEntry) {
String retVal = null;
Bundle.HTTPVerb value = theEntry.getRequest().getMethodElement().getValue();
if (value != null) {
retVal = value.toCode();
}
return retVal;
}
@Override
public String getFullUrl(Bundle.BundleEntryComponent theEntry) {
return theEntry.getFullUrl();
}
@Override
public String getEntryIfNoneExist(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getIfNoneExist();
}
@Override
public String getEntryRequestUrl(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getUrl();
}
@Override
public void setResponseLocation(Bundle.BundleEntryComponent theEntry, String theResponseLocation) {
theEntry.getResponse().setLocation(theResponseLocation);
}
@Override
public void setResponseETag(Bundle.BundleEntryComponent theEntry, String theEtag) {
theEntry.getResponse().setEtag(theEtag);
}
@Override
public String getEntryRequestIfMatch(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getIfMatch();
}
@Override
public String getEntryRequestIfNoneExist(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getIfNoneExist();
}
@Override
public String getEntryRequestIfNoneMatch(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getIfNoneMatch();
}
@Override
public void setResponseOutcome(Bundle.BundleEntryComponent theEntry, IBaseOperationOutcome theOperationOutcome) {
theEntry.getResponse().setOutcome((Resource) theOperationOutcome);
}
}

View File

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

View File

@ -20,587 +20,34 @@ package ca.uhn.fhir.jpa.dao.r4;
* #L%
*/
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.UrlUtil.UrlParts;
import com.google.common.collect.ArrayListMultimap;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.r4.model.Bundle.BundleEntryResponseComponent;
import org.hl7.fhir.r4.model.Bundle.BundleType;
import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.r4.model.Meta;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
import javax.persistence.TypedQuery;
import java.util.*;
import java.util.Map.Entry;
import static org.apache.commons.lang3.StringUtils.*;
import java.util.Collection;
import java.util.List;
public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR4.class);
@Autowired
private PlatformTransactionManager myTxManager;
private TransactionProcessor<Bundle, BundleEntryComponent> myTransactionProcessor;
private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) {
ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size());
long start = System.currentTimeMillis();
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
Bundle resp = new Bundle();
resp.setType(BundleType.BATCHRESPONSE);
/*
* For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others
*/
for (final BundleEntryComponent nextRequestEntry : theRequest.getEntry()) {
BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder();
TransactionCallback<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));
@PostConstruct
public void start() {
myTransactionProcessor.setDao(this);
}
@ -617,53 +64,6 @@ public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> {
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) {
Meta retVal = new Meta();
@ -686,193 +86,7 @@ public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao<Bundle, Meta> {
@Transactional(propagation = Propagation.NEVER)
@Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
}
String actionName = "Transaction";
return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName);
}
private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
super.markRequestAsProcessingSubRequest(theRequestDetails);
try {
return doTransaction(theRequestDetails, theRequest, theActionName);
} finally {
super.clearRequestAsProcessingSubRequest(theRequestDetails);
}
}
private static void handleTransactionCreateOrUpdateOutcome(Map<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;
}
return myTransactionProcessor.transaction(theRequestDetails, theRequest);
}
}

View File

@ -0,0 +1,153 @@
package ca.uhn.fhir.jpa.dao.r4;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.dao.TransactionProcessor;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.Date;
import java.util.List;
public class TransactionProcessorVersionAdapterR4 implements TransactionProcessor.ITransactionProcessorVersionAdapter<Bundle, Bundle.BundleEntryComponent> {
@Override
public void setResponseStatus(Bundle.BundleEntryComponent theBundleEntry, String theStatus) {
theBundleEntry.getResponse().setStatus(theStatus);
}
@Override
public void setResponseLastModified(Bundle.BundleEntryComponent theBundleEntry, Date theLastModified) {
theBundleEntry.getResponse().setLastModified(theLastModified);
}
@Override
public void setResource(Bundle.BundleEntryComponent theBundleEntry, IBaseResource theResource) {
theBundleEntry.setResource((Resource) theResource);
}
@Override
public IBaseResource getResource(Bundle.BundleEntryComponent theBundleEntry) {
return theBundleEntry.getResource();
}
@Override
public String getBundleType(Bundle theRequest) {
if (theRequest.getType() == null) {
return null;
}
return theRequest.getTypeElement().getValue().toCode();
}
@Override
public void populateEntryWithOperationOutcome(BaseServerResponseException theCaughtEx, Bundle.BundleEntryComponent theEntry) {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().setSeverity(OperationOutcome.IssueSeverity.ERROR).setDiagnostics(theCaughtEx.getMessage());
theEntry.getResponse().setOutcome(oo);
}
@Override
public Bundle createBundle(String theBundleType) {
Bundle resp = new Bundle();
try {
resp.setType(Bundle.BundleType.fromCode(theBundleType));
} catch (FHIRException theE) {
throw new InternalErrorException("Unknown bundle type: " + theBundleType);
}
return resp;
}
@Override
public List<Bundle.BundleEntryComponent> getEntries(Bundle theRequest) {
return theRequest.getEntry();
}
@Override
public void addEntry(Bundle theBundle, Bundle.BundleEntryComponent theEntry) {
theBundle.addEntry(theEntry);
}
@Override
public Bundle.BundleEntryComponent addEntry(Bundle theBundle) {
return theBundle.addEntry();
}
@Override
public String getEntryRequestVerb(Bundle.BundleEntryComponent theEntry) {
String retVal = null;
Bundle.HTTPVerb value = theEntry.getRequest().getMethodElement().getValue();
if (value != null) {
retVal = value.toCode();
}
return retVal;
}
@Override
public String getFullUrl(Bundle.BundleEntryComponent theEntry) {
return theEntry.getFullUrl();
}
@Override
public String getEntryIfNoneExist(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getIfNoneExist();
}
@Override
public String getEntryRequestUrl(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getUrl();
}
@Override
public void setResponseLocation(Bundle.BundleEntryComponent theEntry, String theResponseLocation) {
theEntry.getResponse().setLocation(theResponseLocation);
}
@Override
public void setResponseETag(Bundle.BundleEntryComponent theEntry, String theEtag) {
theEntry.getResponse().setEtag(theEtag);
}
@Override
public String getEntryRequestIfMatch(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getIfMatch();
}
@Override
public String getEntryRequestIfNoneExist(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getIfNoneExist();
}
@Override
public String getEntryRequestIfNoneMatch(Bundle.BundleEntryComponent theEntry) {
return theEntry.getRequest().getIfNoneMatch();
}
@Override
public void setResponseOutcome(Bundle.BundleEntryComponent theEntry, IBaseOperationOutcome theOperationOutcome) {
theEntry.getResponse().setOutcome((Resource) theOperationOutcome);
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -38,6 +38,9 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.hl7.fhir.exceptions.FHIRException;
@ -47,9 +50,11 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.ExecutorSubscribableChannel;
@ -72,16 +77,17 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
static final String SUBSCRIPTION_TYPE = "Subscription.channel.type";
private static final Integer MAX_SUBSCRIPTION_RESULTS = 1000;
private SubscribableChannel myProcessingChannel;
private SubscribableChannel myDeliveryChannel;
private Map<String, SubscribableChannel> myDeliveryChannel;
private ExecutorService myProcessingExecutor;
private int myExecutorThreadCount;
private SubscriptionActivatingSubscriber mySubscriptionActivatingSubscriber;
private MessageHandler mySubscriptionCheckingSubscriber;
private ConcurrentHashMap<String, CanonicalSubscription> myIdToSubscription = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, SubscribableChannel> mySubscribableChannel = new ConcurrentHashMap<>();
private Multimap<String, MessageHandler> myIdToDeliveryHandler = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
private Logger ourLog = LoggerFactory.getLogger(BaseSubscriptionInterceptor.class);
private ThreadPoolExecutor myDeliveryExecutor;
private LinkedBlockingQueue<Runnable> myProcessingExecutorQueue;
private LinkedBlockingQueue<Runnable> myDeliveryExecutorQueue;
private IFhirResourceDao<?> mySubscriptionDao;
@Autowired
private List<IFhirResourceDao<?>> myResourceDaos;
@ -234,6 +240,46 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
return retVal;
}
protected SubscribableChannel createDeliveryChannel(CanonicalSubscription theSubscription) {
String subscriptionId = theSubscription.getIdElement(myCtx).getIdPart();
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000);
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("subscription-delivery-" + subscriptionId + "-%d")
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", executorQueue.size());
StopWatch sw = new StopWatch();
try {
executorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
ourLog.info("Slot become available after {}ms", sw.getMillis());
}
};
ThreadPoolExecutor deliveryExecutor = new ThreadPoolExecutor(
1,
getExecutorThreadCount(),
0L,
TimeUnit.MILLISECONDS,
executorQueue,
threadFactory,
rejectedExecutionHandler);
return new ExecutorSubscribableChannel(deliveryExecutor);
}
/**
* Returns an empty handler if the interceptor will manually handle registration and unregistration
*/
protected abstract Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription);
public abstract Subscription.SubscriptionChannelType getChannelType();
@SuppressWarnings("unchecked")
@ -255,16 +301,12 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
return (IFhirResourceDao<R>) myResourceTypeToDao.get(theType);
}
public SubscribableChannel getDeliveryChannel() {
return myDeliveryChannel;
}
public void setDeliveryChannel(SubscribableChannel theDeliveryChannel) {
myDeliveryChannel = theDeliveryChannel;
protected MessageChannel getDeliveryChannel(CanonicalSubscription theSubscription) {
return mySubscribableChannel.get(theSubscription.getIdElement(myCtx).getIdPart());
}
public int getExecutorQueueSizeForUnitTests() {
return myProcessingExecutorQueue.size() + myDeliveryExecutorQueue.size();
return myProcessingExecutorQueue.size();
}
public int getExecutorThreadCount() {
@ -332,31 +374,38 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
mySubscriptionActivatingSubscriber.activateAndRegisterSubscriptionIfRequired(resource);
}
for (Enumeration<String> keyEnum = myIdToSubscription.keys(); keyEnum.hasMoreElements(); ) {
String next = keyEnum.nextElement();
if (!allIds.contains(next)) {
ourLog.info("Unregistering Subscription/{} as it no longer exists", next);
myIdToSubscription.remove(next);
}
}
unregisterAllSubscriptionsNotInCollection(allIds);
}
@SuppressWarnings("unused")
@PreDestroy
public void preDestroy() {
getProcessingChannel().unsubscribe(mySubscriptionCheckingSubscriber);
unregisterDeliverySubscriber();
unregisterAllSubscriptionsNotInCollection(Collections.emptyList());
}
protected abstract void registerDeliverySubscriber();
public void registerHandler(String theSubscriptionId, MessageHandler theHandler) {
mySubscribableChannel.get(theSubscriptionId).subscribe(theHandler);
myIdToDeliveryHandler.put(theSubscriptionId, theHandler);
}
public void registerSubscription(IIdType theId, S theSubscription) {
@SuppressWarnings("UnusedReturnValue")
public CanonicalSubscription registerSubscription(IIdType theId, S theSubscription) {
Validate.notNull(theId);
Validate.notBlank(theId.getIdPart());
String subscriptionId = theId.getIdPart();
Validate.notBlank(subscriptionId);
Validate.notNull(theSubscription);
myIdToSubscription.put(theId.getIdPart(), canonicalize(theSubscription));
CanonicalSubscription canonicalized = canonicalize(theSubscription);
SubscribableChannel deliveryChannel = createDeliveryChannel(canonicalized);
Optional<MessageHandler> deliveryHandler = createDeliveryHandler(canonicalized);
mySubscribableChannel.put(subscriptionId, deliveryChannel);
myIdToSubscription.put(subscriptionId, canonicalized);
deliveryHandler.ifPresent(handler -> registerHandler(subscriptionId, handler));
return canonicalized;
}
protected void registerSubscriptionCheckingSubscriber() {
@ -440,19 +489,16 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
if (getProcessingChannel() == null) {
myProcessingExecutorQueue = new LinkedBlockingQueue<>(1000);
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", myProcessingExecutorQueue.size());
StopWatch sw = new StopWatch();
try {
myProcessingExecutorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
ourLog.info("Slot become available after {}ms", sw.getMillis());
RejectedExecutionHandler rejectedExecutionHandler = (theRunnable, theExecutor) -> {
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", myProcessingExecutorQueue.size());
StopWatch sw = new StopWatch();
try {
myProcessingExecutorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
ourLog.info("Slot become available after {}ms", sw.getMillis());
};
ThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("subscription-proc-%d")
@ -470,44 +516,11 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
setProcessingChannel(new ExecutorSubscribableChannel(myProcessingExecutor));
}
if (getDeliveryChannel() == null) {
myDeliveryExecutorQueue = new LinkedBlockingQueue<>(1000);
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("subscription-delivery-%d")
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable theRunnable, ThreadPoolExecutor theExecutor) {
ourLog.info("Note: Executor queue is full ({} elements), waiting for a slot to become available!", myDeliveryExecutorQueue.size());
StopWatch sw = new StopWatch();
try {
myDeliveryExecutorQueue.put(theRunnable);
} catch (InterruptedException theE) {
throw new RejectedExecutionException("Task " + theRunnable.toString() +
" rejected from " + theE.toString());
}
ourLog.info("Slot become available after {}ms", sw.getMillis());
}
};
myDeliveryExecutor = new ThreadPoolExecutor(
1,
getExecutorThreadCount(),
0L,
TimeUnit.MILLISECONDS,
myDeliveryExecutorQueue,
threadFactory,
rejectedExecutionHandler);
setDeliveryChannel(new ExecutorSubscribableChannel(myDeliveryExecutor));
}
if (mySubscriptionActivatingSubscriber == null) {
mySubscriptionActivatingSubscriber = new SubscriptionActivatingSubscriber(getSubscriptionDao(), getChannelType(), this, myTxManager, myAsyncTaskExecutor);
}
registerSubscriptionCheckingSubscriber();
registerDeliverySubscriber();
TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@ -523,13 +536,46 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
sendToProcessingChannel(theMsg);
}
protected abstract void unregisterDeliverySubscriber();
private void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) {
for (String next : new ArrayList<>(myIdToSubscription.keySet())) {
if (!theAllIds.contains(next)) {
ourLog.info("Unregistering Subscription/{}", next);
CanonicalSubscription subscription = myIdToSubscription.get(next);
unregisterSubscription(subscription.getIdElement(myCtx));
}
}
}
public void unregisterSubscription(IIdType theId) {
public void unregisterHandler(String theSubscriptionId, MessageHandler theMessageHandler) {
SubscribableChannel channel = mySubscribableChannel.get(theSubscriptionId);
if (channel != null) {
if (channel instanceof DisposableBean) {
try {
((DisposableBean) channel).destroy();
} catch (Exception e) {
ourLog.error("Failed to destroy channel bean", e);
}
}
channel.unsubscribe(theMessageHandler);
}
mySubscribableChannel.remove(theSubscriptionId, theMessageHandler);
}
@SuppressWarnings("UnusedReturnValue")
public CanonicalSubscription unregisterSubscription(IIdType theId) {
Validate.notNull(theId);
Validate.notBlank(theId.getIdPart());
myIdToSubscription.remove(theId.getIdPart());
String subscriptionId = theId.getIdPart();
Validate.notBlank(subscriptionId);
for (MessageHandler next : new ArrayList<>(myIdToDeliveryHandler.get(subscriptionId))) {
unregisterHandler(subscriptionId, next);
}
mySubscribableChannel.remove(subscriptionId);
return myIdToSubscription.remove(subscriptionId);
}

View File

@ -126,15 +126,12 @@ public class SubscriptionActivatingSubscriber {
activateSubscription(activeStatus, theSubscription, requestedStatus);
}
} else if (activeStatus.equals(statusString)) {
if (!mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement())) {
ourLog.info("Registering active subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
}
mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription);
registerSubscriptionUnlessAlreadyRegistered(theSubscription);
} else {
if (mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement())) {
ourLog.info("Removing {} subscription {}", statusString, theSubscription.getIdElement().toUnqualified().getValue());
mySubscriptionInterceptor.unregisterSubscription(theSubscription.getIdElement());
}
mySubscriptionInterceptor.unregisterSubscription(theSubscription.getIdElement());
}
}
@ -145,7 +142,7 @@ public class SubscriptionActivatingSubscriber {
try {
SubscriptionUtil.setStatus(myCtx, subscription, theActiveStatus);
mySubscriptionDao.update(subscription);
mySubscriptionInterceptor.registerSubscription(subscription.getIdElement(), subscription);
registerSubscriptionUnlessAlreadyRegistered(subscription);
} catch (final UnprocessableEntityException e) {
ourLog.info("Changing status of {} to ERROR", subscription.getIdElement());
SubscriptionUtil.setStatus(myCtx, subscription, "error");
@ -179,6 +176,16 @@ public class SubscriptionActivatingSubscriber {
}
private void registerSubscriptionUnlessAlreadyRegistered(IBaseResource theSubscription) {
if (mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement())) {
ourLog.info("Updating already-registered active subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
mySubscriptionInterceptor.unregisterSubscription(theSubscription.getIdElement());
} else {
ourLog.info("Registering active subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
}
mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription);
}
@VisibleForTesting
public static void setWaitForSubscriptionActivationSynchronouslyForUnitTest(boolean theWaitForSubscriptionActivationSynchronouslyForUnitTest) {
ourWaitForSubscriptionActivationSynchronouslyForUnitTest = theWaitForSubscriptionActivationSynchronouslyForUnitTest;

View File

@ -34,6 +34,7 @@ import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import java.util.List;
@ -117,7 +118,12 @@ public class SubscriptionCheckingSubscriber extends BaseSubscriptionSubscriber {
deliveryMsg.setPayloadId(msg.getId(getContext()));
ResourceDeliveryJsonMessage wrappedMsg = new ResourceDeliveryJsonMessage(deliveryMsg);
getSubscriptionInterceptor().getDeliveryChannel().send(wrappedMsg);
MessageChannel deliveryChannel = getSubscriptionInterceptor().getDeliveryChannel(nextSubscription);
if (deliveryChannel != null) {
deliveryChannel.send(wrappedMsg);
} else {
ourLog.warn("Do not have deliovery channel for subscription {}", nextSubscription.getIdElement(getContext()));
}
}

View File

@ -21,13 +21,14 @@ package ca.uhn.fhir.jpa.subscription.email;
*/
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHandler;
import javax.annotation.PostConstruct;
import java.util.Optional;
public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
private SubscriptionDeliveringEmailSubscriber mySubscriptionDeliverySubscriber;
/**
* This is set to autowired=false just so that implementors can supply this
@ -37,6 +38,11 @@ public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
private IEmailSender myEmailSender;
private String myDefaultFromAddress = "noreply@unknown.com";
@Override
protected Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription) {
return Optional.of(new SubscriptionDeliveringEmailSubscriber(getSubscriptionDao(), getChannelType(), this));
}
@Override
public org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType getChannelType() {
return org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.EMAIL;
@ -69,23 +75,5 @@ public class SubscriptionEmailInterceptor extends BaseSubscriptionInterceptor {
myEmailSender = theEmailSender;
}
@Override
protected void registerDeliverySubscriber() {
if (mySubscriptionDeliverySubscriber == null) {
mySubscriptionDeliverySubscriber = new SubscriptionDeliveringEmailSubscriber(getSubscriptionDao(), getChannelType(), this);
}
getDeliveryChannel().subscribe(mySubscriptionDeliverySubscriber);
}
// @PostConstruct
// public void start() {
// Validate.notNull(myEmailSender, "emailSender has not been configured");
//
// super.start();
// }
@Override
protected void unregisterDeliverySubscriber() {
getDeliveryChannel().unsubscribe(mySubscriptionDeliverySubscriber);
}
}

View File

@ -21,16 +21,16 @@ package ca.uhn.fhir.jpa.subscription.resthook;
*/
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import org.springframework.messaging.MessageHandler;
import java.util.Optional;
public class SubscriptionRestHookInterceptor extends BaseSubscriptionInterceptor {
private SubscriptionDeliveringRestHookSubscriber mySubscriptionDeliverySubscriber;
@Override
protected void registerDeliverySubscriber() {
if (mySubscriptionDeliverySubscriber == null) {
mySubscriptionDeliverySubscriber = new SubscriptionDeliveringRestHookSubscriber(getSubscriptionDao(), getChannelType(), this);
}
getDeliveryChannel().subscribe(mySubscriptionDeliverySubscriber);
protected Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription) {
return Optional.of(new SubscriptionDeliveringRestHookSubscriber(getSubscriptionDao(), getChannelType(), this));
}
@Override
@ -38,8 +38,4 @@ public class SubscriptionRestHookInterceptor extends BaseSubscriptionInterceptor
return org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType.RESTHOOK;
}
@Override
protected void unregisterDeliverySubscriber() {
getDeliveryChannel().unsubscribe(mySubscriptionDeliverySubscriber);
}
}

View File

@ -109,12 +109,14 @@ public class SubscriptionWebsocketHandler extends TextWebSocketHandler implement
mySession = theSession;
mySubscription = theSubscription;
mySubscriptionWebsocketInterceptor.getDeliveryChannel().subscribe(this);
String subscriptionId = mySubscription.getIdElement(myCtx).getIdPart();
mySubscriptionWebsocketInterceptor.registerHandler(subscriptionId, this);
}
@Override
public void closing() {
mySubscriptionWebsocketInterceptor.getDeliveryChannel().unsubscribe(this);
String subscriptionId = mySubscription.getIdElement(myCtx).getIdPart();
mySubscriptionWebsocketInterceptor.unregisterHandler(subscriptionId, this);
}
private void deliver() {

View File

@ -23,10 +23,14 @@ package ca.uhn.fhir.jpa.subscription.websocket;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.dao.data.ISubscriptionTableDao;
import ca.uhn.fhir.jpa.subscription.BaseSubscriptionInterceptor;
import ca.uhn.fhir.jpa.subscription.CanonicalSubscription;
import org.hl7.fhir.r4.model.Subscription;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHandler;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.Optional;
public class SubscriptionWebsocketInterceptor extends BaseSubscriptionInterceptor {
@Autowired
@ -38,26 +42,15 @@ public class SubscriptionWebsocketInterceptor extends BaseSubscriptionIntercepto
@Autowired
private IResourceTableDao myResourceTableDao;
@Override
protected Optional<MessageHandler> createDeliveryHandler(CanonicalSubscription theSubscription) {
return Optional.empty();
}
@Override
public Subscription.SubscriptionChannelType getChannelType() {
return Subscription.SubscriptionChannelType.WEBSOCKET;
}
@Override
protected void registerDeliverySubscriber() {
/*
* nothing, since individual websocket connections
* register themselves
*/
}
@Override
protected void unregisterDeliverySubscriber() {
/*
* nothing, since individual websocket connections
* register themselves
*/
}
}

View File

@ -1343,14 +1343,18 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
predicates = new ArrayList<>();
coding = translationQuery.getCoding();
String targetCode = null;
String targetCodeSystem = null;
if (coding.hasCode()) {
predicates.add(criteriaBuilder.equal(targetJoin.get("myCode"), coding.getCode()));
targetCode = coding.getCode();
} else {
throw new InvalidRequestException("A code must be provided for translation to occur.");
}
if (coding.hasSystem()) {
predicates.add(criteriaBuilder.equal(groupJoin.get("myTarget"), coding.getSystem()));
targetCodeSystem = coding.getSystem();
}
if (coding.hasVersion()) {
@ -1384,7 +1388,24 @@ public abstract class BaseHapiTerminologySvcImpl implements IHapiTerminologySvc,
Iterator<TermConceptMapGroupElement> scrollableResultsIterator = new ScrollableResultsIterator<>(scrollableResults);
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.

View File

@ -1,115 +0,0 @@
package ca.uhn.fhir.jpa.util;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import javax.xml.ws.http.HTTPException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* Rest service utilities. Generally used in the tests
*/
public class RestUtilities {
public static final String CONTEXT_PATH = "";
public static final String APPLICATION_JSON = "application/json";
/**
* Get the response for a CXF REST service without an object parameter
*
* @param url
* @param typeRequest
* @return
* @throws IOException
*/
public static String getResponse(String url, MethodRequest typeRequest) throws IOException {
return getResponse(url, (StringEntity) null, typeRequest);
}
/**
* Get the response for a CXF REST service with an object parameter
*
* @param url
* @param parameterEntity
* @param typeRequest
* @return
* @throws IOException
*/
public static String getResponse(String url, StringEntity parameterEntity, MethodRequest typeRequest) throws IOException {
HttpClient httpclient = new DefaultHttpClient();
HttpResponse response;
switch (typeRequest) {
case POST:
HttpPost httppost = new HttpPost(url);
httppost.setHeader("Content-type", APPLICATION_JSON);
if (parameterEntity != null) {
httppost.setEntity(parameterEntity);
}
response = httpclient.execute(httppost);
break;
case PUT:
HttpPut httpPut = new HttpPut(url);
httpPut.setHeader("Content-type", APPLICATION_JSON);
if (parameterEntity != null) {
httpPut.setEntity(parameterEntity);
}
response = httpclient.execute(httpPut);
break;
case DELETE:
HttpDelete httpDelete = new HttpDelete(url);
httpDelete.setHeader("Content-type", APPLICATION_JSON);
response = httpclient.execute(httpDelete);
break;
case GET:
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("Content-type", APPLICATION_JSON);
response = httpclient.execute(httpGet);
break;
default:
throw new IllegalArgumentException("Cannot handle type request " + typeRequest);
}
if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() >= 300) {
throw new HTTPException(response.getStatusLine().getStatusCode());
}
if (response.getStatusLine().getStatusCode() == 204) {
return "";
}
//Closes connections that have already been closed by the server
//org.apache.http.NoHttpResponseException: The target server failed to respond
httpclient.getConnectionManager().closeIdleConnections(1, TimeUnit.SECONDS);
return EntityUtils.toString(response.getEntity());
}
}

View File

@ -105,7 +105,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
DataSource dataSource = ProxyDataSourceBuilder
.create(retVal)
.logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
.logSlowQueryBySlf4j(10, TimeUnit.SECONDS)
.countQuery(new ThreadQueryCountHolder())
.build();

View File

@ -34,7 +34,6 @@ import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.mockito.Mockito;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
@ -49,6 +48,7 @@ import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import static ca.uhn.fhir.util.TestUtil.randomizeLocale;
import static org.junit.Assert.*;
@ -77,16 +77,6 @@ public abstract class BaseJpaTest {
BaseHapiFhirResourceDao.setDisableIncrementOnUpdateForUnitTest(false);
}
@Before
public void beforeCreateSrd() {
mySrd = mock(ServletRequestDetails.class, Mockito.RETURNS_DEEP_STUBS);
when(mySrd.getRequestOperationCallback()).thenReturn(myRequestOperationCallback);
myServerInterceptorList = new ArrayList<>();
when(mySrd.getServer().getInterceptors()).thenReturn(myServerInterceptorList);
when(mySrd.getUserData()).thenReturn(new HashMap<>());
when(mySrd.getHeaders(eq(JpaConstants.HEADER_META_SNAPSHOT_MODE))).thenReturn(new ArrayList<>());
}
@After
public void afterValidateNoTransaction() {
PlatformTransactionManager txManager = getTxManager();
@ -103,6 +93,7 @@ public abstract class BaseJpaTest {
if (currentSession != null) {
currentSession.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
isReadOnly.set(connection.isReadOnly());
}
@ -113,6 +104,16 @@ public abstract class BaseJpaTest {
}
}
@Before
public void beforeCreateSrd() {
mySrd = mock(ServletRequestDetails.class, Mockito.RETURNS_DEEP_STUBS);
when(mySrd.getRequestOperationCallback()).thenReturn(myRequestOperationCallback);
myServerInterceptorList = new ArrayList<>();
when(mySrd.getServer().getInterceptors()).thenReturn(myServerInterceptorList);
when(mySrd.getUserData()).thenReturn(new HashMap<>());
when(mySrd.getHeaders(eq(JpaConstants.HEADER_META_SNAPSHOT_MODE))).thenReturn(new ArrayList<>());
}
@Before
public void beforeRandomizeLocale() {
randomizeLocale();
@ -299,6 +300,7 @@ public abstract class BaseJpaTest {
return retVal.toArray(new String[retVal.size()]);
}
@SuppressWarnings("RedundantThrows")
@AfterClass
public static void afterClassClearContext() throws Exception {
TestUtil.clearAllStaticFieldsForUnitTest();
@ -360,7 +362,19 @@ public abstract class BaseJpaTest {
}
}
if (sw.getMillis() >= 15000) {
fail("Size " + theList.size() + " is != target " + theTarget + " - Got: " + theList.toString());
String describeResults = theList
.stream()
.map(t -> {
if (t == null) {
return "null";
}
if (t instanceof IBaseResource) {
return ((IBaseResource)t).getIdElement().getValue();
}
return t.toString();
})
.collect(Collectors.joining(", "));
fail("Size " + theList.size() + " is != target " + theTarget + " - Got: " + describeResults);
}
}

View File

@ -235,6 +235,7 @@ public abstract class BaseJpaDstu2Test extends BaseJpaTest {
return retVal;
}
@SuppressWarnings("RedundantThrows")
@AfterClass
public static void afterClassClearContext() throws Exception {
TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -1409,26 +1409,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
}
}
@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
public void testTransactionDoesNotLeavePlaceholderIds() throws Exception {
String input;

View File

@ -395,6 +395,12 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
target.setDisplay("Target Code 45678");
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.setSource(CS_URL);
group.setSourceVersion("Version 3");

View File

@ -613,7 +613,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(1, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence());
assertEquals("narrower", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode());
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
public void testTranslateWithReverseByCodeSystemsAndSourceCodeUnmapped() {
ConceptMap conceptMap = myConceptMapDao.read(myConceptMapId);
@ -680,7 +723,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence());
assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay());
@ -690,7 +733,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence());
assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay());
@ -733,7 +776,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(1, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence());
assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay());
@ -776,7 +819,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(1, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence());
assertEquals("narrower", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay());
@ -817,7 +860,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence());
assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay());
@ -827,7 +870,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence());
assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay());
@ -870,7 +913,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence());
assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay());
@ -880,7 +923,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence());
assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay());
@ -921,7 +964,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence());
assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay());
@ -931,7 +974,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence());
assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay());
@ -972,7 +1015,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(2, translationResult.getMatches().size());
TranslationMatch translationMatch = translationResult.getMatches().get(0);
assertNull(translationMatch.getEquivalence());
assertEquals("equal", translationMatch.getEquivalence().getCode());
Coding concept = translationMatch.getConcept();
assertEquals("12345", concept.getCode());
assertEquals("Source Code 12345", concept.getDisplay());
@ -982,7 +1025,7 @@ public class FhirResourceDaoR4ConceptMapTest extends BaseJpaR4Test {
assertEquals(CM_URL, translationMatch.getSource().getValueAsString());
translationMatch = translationResult.getMatches().get(1);
assertNull(translationMatch.getEquivalence());
assertEquals("narrower", translationMatch.getEquivalence().getCode());
concept = translationMatch.getConcept();
assertEquals("78901", concept.getCode());
assertEquals("Source Code 78901", concept.getDisplay());

View File

@ -1,11 +1,9 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.entity.ResourceIndexedCompositeStringUnique;
import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.search.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.util.JpaConstants;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@ -29,7 +27,6 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.hamcrest.Matchers.*;
@ -56,6 +53,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
public void before() {
myDaoConfig.setDefaultSearchParamsCanBeOverridden(true);
myDaoConfig.setSchedulingDisabled(true);
SearchBuilder.resetLastHandlerMechanismForUnitTest();
}
private void createUniqueBirthdateAndGenderSps() {
@ -94,6 +92,8 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
mySearchParameterDao.update(sp);
mySearchParamRegsitry.forceRefresh();
SearchBuilder.resetLastHandlerMechanismForUnitTest();
}
@ -755,7 +755,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
params.add("birthdate", new DateParam("2011-01-01"));
IBundleProvider results = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue()));
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest().toString(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
}
@ -780,7 +780,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
IBundleProvider results = myPatientDao.search(params);
String searchId = results.getUuid();
assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1));
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest().toString(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
// Other order
SearchBuilder.resetLastHandlerMechanismForUnitTest();
@ -799,14 +799,14 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
params.add("birthdate", new DateParam("2011-01-03"));
results = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIdValues(results), empty());
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest().toString(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
SearchBuilder.resetLastHandlerMechanismForUnitTest();
params = new SearchParameterMap();
params.add("birthdate", new DateParam("2011-01-03"));
results = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIdValues(results), empty());
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest().toString(), SearchBuilder.HandlerTypeEnum.STANDARD_QUERY, SearchBuilder.getLastHandlerMechanismForUnitTest());
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest(), SearchBuilder.HandlerTypeEnum.STANDARD_QUERY, SearchBuilder.getLastHandlerMechanismForUnitTest());
}
@ -872,7 +872,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
SearchBuilder.resetLastHandlerMechanismForUnitTest();
IIdType id1 = myPatientDao.update(pt1, "Patient?name=FAMILY1&organization:Organization=ORG").getId().toUnqualifiedVersionless();
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest().toString(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
uniques = myResourceIndexedCompositeStringUniqueDao.findAll();
assertEquals(1, uniques.size());
assertEquals("Patient/" + id1.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue());
@ -886,7 +886,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test {
SearchBuilder.resetLastHandlerMechanismForUnitTest();
id1 = myPatientDao.update(pt1, "Patient?name=FAMILY1&organization:Organization=ORG").getId().toUnqualifiedVersionless();
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest().toString(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
assertEquals(SearchBuilder.getLastHandlerParamsForUnitTest(), SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest());
uniques = myResourceIndexedCompositeStringUniqueDao.findAll();
assertEquals(1, uniques.size());
assertEquals("Patient/" + id1.getIdPart(), uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue());

View File

@ -16,6 +16,7 @@ import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.util.TestUtil;
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.IIdType;
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);
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@After
public void after() {
myDaoConfig.setAllowInlineMatchUrlReferences(false);
@ -187,7 +193,6 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testBatchCreateWithBadRead() {
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
public void testTransactionDoesNotLeavePlaceholderIds() {
String input;
@ -1624,7 +1609,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar"));
search = myPatientDao.search(map);
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());
// Observation
map = new SearchParameterMap();
@ -1632,7 +1617,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog"));
search = myObservationDao.search(map);
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(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference());
@ -1689,7 +1674,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar"));
search = myPatientDao.search(map);
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());
// Observation
map = new SearchParameterMap();
@ -1697,7 +1682,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog"));
search = myObservationDao.search(map);
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(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference());
@ -1755,7 +1740,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Patient.SP_IDENTIFIER, new TokenParam("foo", "bar"));
search = myPatientDao.search(map);
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());
// Observation
map = new SearchParameterMap();
@ -1763,7 +1748,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
map.add(Observation.SP_IDENTIFIER, new TokenParam("foo", "dog"));
search = myObservationDao.search(map);
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(createdPatientId.toUnqualifiedVersionless().getValue(), obs.getSubject().getReference());
assertEquals(ObservationStatus.FINAL, obs.getStatus());
@ -2132,6 +2117,29 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
assertNull(nextEntry.getResource());
}
@Test
public void testTransactionWithUnknownTemnporaryIdReference() {
String methodName = "testTransactionWithUnknownTemnporaryIdReference";
Bundle request = new Bundle();
Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient");
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue(methodName);
p.getManagingOrganization().setReference(IdType.newRandomUuid().getValue());
request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient");
try {
mySystemDao.transaction(mySrd, request);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), Matchers.matchesPattern("Unable to satisfy placeholder ID urn:uuid:[0-9a-z-]+ found in element named 'managingOrganization' within resource of type: Patient"));
}
}
@Test
public void testTransactionSearchWithCount() {
String methodName = "testTransactionSearchWithCount";
@ -3047,44 +3055,6 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
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
*/
@ -3230,7 +3238,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
@Test
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.setId(IdType.newRandomUuid());
@ -3399,9 +3407,4 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest {
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

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

View File

@ -206,7 +206,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigDstu2Test extends B
subscriptionTemp.setCriteria(criteria1);
ourClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
waitForQueueToDrain();
Observation observation2 = sendObservation(code, "SNOMED-CT");

View File

@ -52,6 +52,7 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc
@Before
public void beforeSetSubscriptionActivatingInterceptor() {
SubscriptionActivatingSubscriber.setWaitForSubscriptionActivationSynchronouslyForUnitTest(true);
getRestHookSubscriptionInterceptor().initSubscriptions();
}
@ -102,8 +103,6 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc
createSubscription(criteria1, payload, ourListenerServerBase);
createSubscription(criteria2, payload, ourListenerServerBase);
assertFalse(hasRestHookSubscriptionInterceptor());
ourRestServer.registerInterceptor(getRestHookSubscriptionInterceptor());
getRestHookSubscriptionInterceptor().initSubscriptions();

View File

@ -355,6 +355,53 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test {
Assert.assertFalse(observation2.getId().isEmpty());
}
@Test
public void testUpdateSubscriptionToMatchLater() throws Exception {
String payload = "application/xml";
String code = "1000000050";
String criteriaBad = "Observation?code=SNOMED-CT|" + code + "111&_format=xml";
ourLog.info("** About to create non-matching subscription");
Subscription subscription2 = createSubscription(criteriaBad, payload, ourListenerServerBase);
ourLog.info("** About to send observation that wont match");
Observation observation1 = sendObservation(code, "SNOMED-CT");
// Criteria didn't match, shouldn't see any updates
waitForQueueToDrain();
Thread.sleep(1000);
assertEquals(0, ourUpdatedObservations.size());
Subscription subscriptionTemp = myClient.read().resource(Subscription.class).withId(subscription2.getId()).execute();
Assert.assertNotNull(subscriptionTemp);
String criteriaGood = "Observation?code=SNOMED-CT|" + code + "&_format=xml";
subscriptionTemp.setCriteria(criteriaGood);
ourLog.info("** About to update subscription");
myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute();
waitForQueueToDrain();
ourLog.info("** About to send Observation 2");
Observation observation2 = sendObservation(code, "SNOMED-CT");
waitForQueueToDrain();
// Should see a subscription notification this time
waitForSize(0, ourCreatedObservations);
waitForSize(1, ourUpdatedObservations);
myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute();
Observation observationTemp3 = sendObservation(code, "SNOMED-CT");
// No more matches
Thread.sleep(1000);
assertEquals(1, ourUpdatedObservations.size());
}
@Test
public void testRestHookSubscriptionApplicationXmlJson() throws Exception {
String payload = "application/fhir+xml";

View File

@ -150,7 +150,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base
waitForSize(0, ourCreatedObservations);
waitForSize(4, ourUpdatedObservations);
Observation observation3 = myClient.read(Observation.class, observationTemp3.getId());
Observation observation3 = myClient.read().resource(Observation.class).withId(observationTemp3.getId()).execute();
CodeableConcept codeableConcept = new CodeableConcept();
observation3.setCode(codeableConcept);
Coding coding = codeableConcept.addCoding();
@ -223,7 +223,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base
waitForSize(0, ourCreatedObservations);
waitForSize(4, ourUpdatedObservations);
Observation observation3 = myClient.read(Observation.class, observationTemp3.getId());
Observation observation3 = myClient.read().resource(Observation.class).withId(observationTemp3.getId()).execute();
CodeableConcept codeableConcept = new CodeableConcept();
observation3.setCode(codeableConcept);
Coding coding = codeableConcept.addCoding();
@ -236,7 +236,7 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base
waitForSize(0, ourCreatedObservations);
waitForSize(4, ourUpdatedObservations);
Observation observation3a = myClient.read(Observation.class, observationTemp3.getId());
Observation observation3a = myClient.read().resource(Observation.class).withId(observationTemp3.getId()).execute();
CodeableConcept codeableConcept1 = new CodeableConcept();
observation3a.setCode(codeableConcept1);

View File

@ -47,6 +47,7 @@ public class WebsocketWithSubscriptionIdR4Test extends BaseResourceProviderR4Tes
private WebSocketClient myWebSocketClient;
private SocketImplementation mySocketImplementation;
@Override
@After
public void after() throws Exception {
super.after();
@ -60,6 +61,7 @@ public class WebsocketWithSubscriptionIdR4Test extends BaseResourceProviderR4Tes
myWebSocketClient.stop();
}
@Override
@Before
public void before() throws Exception {
super.before();

View File

@ -190,12 +190,22 @@ public class TerminologySvcImplR4Test extends BaseJpaR4Test {
assertEquals("Version 1", element.getSystemVersion());
assertEquals(VS_URL, element.getValueSet());
assertEquals(CM_URL, element.getConceptMapUrl());
assertEquals(1, element.getConceptMapGroupElementTargets().size());
assertEquals(2, element.getConceptMapGroupElementTargets().size());
target = element.getConceptMapGroupElementTargets().get(0);
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("Target Code 45678", target.getDisplay());
assertEquals(CS_URL_2, target.getSystem());

View File

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

View File

@ -300,7 +300,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
} else {
resourceBinding = myResourceNameToBinding.get(resourceName);
if (resourceBinding == null) {
throw new ResourceNotFoundException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToBinding.keySet());
throwUnknownResourceTypeException(resourceName);
}
}
@ -316,7 +316,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
if (isBlank(requestPath)) {
throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "rootRequest"));
}
throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "unknownMethod", requestType.name(), requestPath, requestDetails.getParameters().keySet()));
throwUnknownFhirOperationException(requestDetails, requestPath, requestType);
}
return resourceMethod;
}
@ -569,10 +569,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
*
* @param theList The list of interceptors (may be null)
*/
public void setInterceptors(IServerInterceptor... theList) {
public void setInterceptors(List<IServerInterceptor> theList) {
myInterceptors.clear();
if (theList != null) {
myInterceptors.addAll(Arrays.asList(theList));
myInterceptors.addAll(theList);
}
}
@ -602,11 +602,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
*
* @see #setResourceProviders(Collection)
*/
public void setPlainProviders(Collection<Object> theProviders) {
myPlainProviders.clear();
if (theProviders != null) {
myPlainProviders.addAll(theProviders);
}
public void setPlainProviders(Object... theProv) {
setPlainProviders(Arrays.asList(theProv));
}
/**
@ -636,10 +633,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
/**
* Sets the resource providers for this server
*/
public void setResourceProviders(Collection<IResourceProvider> theResourceProviders) {
public void setResourceProviders(IResourceProvider... theResourceProviders) {
myResourceProviders.clear();
if (theResourceProviders != null) {
myResourceProviders.addAll(theResourceProviders);
myResourceProviders.addAll(Arrays.asList(theResourceProviders));
}
}
@ -1512,10 +1509,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
*
* @param theList The list of interceptors (may be null)
*/
public void setInterceptors(List<IServerInterceptor> theList) {
public void setInterceptors(IServerInterceptor... theList) {
myInterceptors.clear();
if (theList != null) {
myInterceptors.addAll(theList);
myInterceptors.addAll(Arrays.asList(theList));
}
}
@ -1524,8 +1521,11 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
*
* @see #setResourceProviders(Collection)
*/
public void setPlainProviders(Object... theProv) {
setPlainProviders(Arrays.asList(theProv));
public void setPlainProviders(Collection<Object> theProviders) {
myPlainProviders.clear();
if (theProviders != null) {
myPlainProviders.addAll(theProviders);
}
}
/**
@ -1543,10 +1543,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
/**
* Sets the resource providers for this server
*/
public void setResourceProviders(IResourceProvider... theResourceProviders) {
public void setResourceProviders(Collection<IResourceProvider> theResourceProviders) {
myResourceProviders.clear();
if (theResourceProviders != null) {
myResourceProviders.addAll(Arrays.asList(theResourceProviders));
myResourceProviders.addAll(theResourceProviders);
}
}
@ -1559,6 +1559,14 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
myTenantIdentificationStrategy = theTenantIdentificationStrategy;
}
protected void throwUnknownFhirOperationException(RequestDetails requestDetails, String requestPath, RequestTypeEnum theRequestType) {
throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "unknownMethod", theRequestType.name(), requestPath, requestDetails.getParameters().keySet()));
}
protected void throwUnknownResourceTypeException(String theResourceName) {
throw new ResourceNotFoundException("Unknown resource type '" + theResourceName + "' - Server knows how to handle: " + myResourceNameToBinding.keySet());
}
public void unregisterInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor);

View File

@ -13,7 +13,9 @@ import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -440,206 +442,237 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
}
theServletResponse.setContentType(Constants.CT_HTML_WITH_UTF8);
StringBuilder b = new StringBuilder();
b.append("<html lang=\"en\">\n");
b.append(" <head>\n");
b.append(" <meta charset=\"utf-8\" />\n");
b.append(" <style>\n");
b.append(".httpStatusDiv {");
b.append(" font-size: 1.2em;");
b.append(" font-weight: bold;");
b.append("}");
b.append(".hlQuot { color: #88F; }\n");
b.append(".hlQuot a { text-decoration: underline; text-decoration-color: #CCC; }\n");
b.append(".hlQuot a:HOVER { text-decoration: underline; text-decoration-color: #008; }\n");
b.append(".hlQuot .uuid, .hlQuot .dateTime {\n");
b.append(" user-select: all;\n");
b.append(" -moz-user-select: all;\n");
b.append(" -webkit-user-select: all;\n");
b.append(" -ms-user-select: element;\n");
b.append("}\n");
b.append(".hlAttr {\n");
b.append(" color: #888;\n");
b.append("}\n");
b.append(".hlTagName {\n");
b.append(" color: #006699;\n");
b.append("}\n");
b.append(".hlControl {\n");
b.append(" color: #660000;\n");
b.append("}\n");
b.append(".hlText {\n");
b.append(" color: #000000;\n");
b.append("}\n");
b.append(".hlUrlBase {\n");
b.append("}");
b.append(".headersDiv {\n");
b.append(" padding: 10px;");
b.append(" margin-left: 10px;");
b.append(" border: 1px solid #CCC;");
b.append(" border-radius: 10px;");
b.append("}");
b.append(".headersRow {\n");
b.append("}");
b.append(".headerName {\n");
b.append(" color: #888;\n");
b.append(" font-family: monospace;\n");
b.append("}");
b.append(".headerValue {\n");
b.append(" color: #88F;\n");
b.append(" font-family: monospace;\n");
b.append("}");
b.append(".responseBodyTable {");
b.append(" width: 100%;\n");
b.append(" margin-left: 0px;\n");
b.append(" margin-top: -10px;\n");
b.append(" position: relative;\n");
b.append("}");
b.append(".responseBodyTableFirstColumn {");
b.append(" position: absolute;\n");
b.append(" width: 70px;\n");
b.append("}");
b.append(".responseBodyTableSecondColumn {");
b.append(" position: absolute;\n");
b.append(" margin-left: 70px;\n");
b.append(" vertical-align: top;\n");
b.append(" left: 0px;\n");
b.append(" right: 0px;\n");
b.append("}");
b.append(".lineAnchor A {");
b.append(" text-decoration: none;");
b.append(" padding-left: 20px;");
b.append("}");
b.append(".lineAnchor {");
b.append(" display: block;");
b.append(" padding-right: 20px;");
b.append("}");
b.append(".selectedLine {");
b.append(" background-color: #EEF;");
b.append(" font-weight: bold;");
b.append("}");
b.append("H1 {");
b.append(" font-size: 1.1em;");
b.append(" color: #666;");
b.append("}");
b.append("BODY {\n");
b.append(" font-family: Arial;\n");
b.append("}");
b.append(" </style>\n");
b.append(" </head>\n");
b.append("\n");
b.append(" <body>");
StringBuilder outputBuffer = new StringBuilder();
outputBuffer.append("<html lang=\"en\">\n");
outputBuffer.append(" <head>\n");
outputBuffer.append(" <meta charset=\"utf-8\" />\n");
outputBuffer.append(" <style>\n");
outputBuffer.append(".httpStatusDiv {");
outputBuffer.append(" font-size: 1.2em;");
outputBuffer.append(" font-weight: bold;");
outputBuffer.append("}");
outputBuffer.append(".hlQuot { color: #88F; }\n");
outputBuffer.append(".hlQuot a { text-decoration: underline; text-decoration-color: #CCC; }\n");
outputBuffer.append(".hlQuot a:HOVER { text-decoration: underline; text-decoration-color: #008; }\n");
outputBuffer.append(".hlQuot .uuid, .hlQuot .dateTime {\n");
outputBuffer.append(" user-select: all;\n");
outputBuffer.append(" -moz-user-select: all;\n");
outputBuffer.append(" -webkit-user-select: all;\n");
outputBuffer.append(" -ms-user-select: element;\n");
outputBuffer.append("}\n");
outputBuffer.append(".hlAttr {\n");
outputBuffer.append(" color: #888;\n");
outputBuffer.append("}\n");
outputBuffer.append(".hlTagName {\n");
outputBuffer.append(" color: #006699;\n");
outputBuffer.append("}\n");
outputBuffer.append(".hlControl {\n");
outputBuffer.append(" color: #660000;\n");
outputBuffer.append("}\n");
outputBuffer.append(".hlText {\n");
outputBuffer.append(" color: #000000;\n");
outputBuffer.append("}\n");
outputBuffer.append(".hlUrlBase {\n");
outputBuffer.append("}");
outputBuffer.append(".headersDiv {\n");
outputBuffer.append(" padding: 10px;");
outputBuffer.append(" margin-left: 10px;");
outputBuffer.append(" border: 1px solid #CCC;");
outputBuffer.append(" border-radius: 10px;");
outputBuffer.append("}");
outputBuffer.append(".headersRow {\n");
outputBuffer.append("}");
outputBuffer.append(".headerName {\n");
outputBuffer.append(" color: #888;\n");
outputBuffer.append(" font-family: monospace;\n");
outputBuffer.append("}");
outputBuffer.append(".headerValue {\n");
outputBuffer.append(" color: #88F;\n");
outputBuffer.append(" font-family: monospace;\n");
outputBuffer.append("}");
outputBuffer.append(".responseBodyTable {");
outputBuffer.append(" width: 100%;\n");
outputBuffer.append(" margin-left: 0px;\n");
outputBuffer.append(" margin-top: -10px;\n");
outputBuffer.append(" position: relative;\n");
outputBuffer.append("}");
outputBuffer.append(".responseBodyTableFirstColumn {");
// outputBuffer.append(" position: absolute;\n");
// outputBuffer.append(" width: 70px;\n");
outputBuffer.append("}");
outputBuffer.append(".responseBodyTableSecondColumn {");
outputBuffer.append(" position: absolute;\n");
outputBuffer.append(" margin-left: 70px;\n");
outputBuffer.append(" vertical-align: top;\n");
outputBuffer.append(" left: 0px;\n");
outputBuffer.append(" right: 0px;\n");
outputBuffer.append("}");
outputBuffer.append(".responseBodyTableSecondColumn PRE {");
outputBuffer.append(" margin: 0px;");
outputBuffer.append("}");
outputBuffer.append(".sizeInfo {");
outputBuffer.append(" margin-top: 20px;");
outputBuffer.append(" font-size: 0.8em;");
outputBuffer.append("}");
outputBuffer.append(".lineAnchor A {");
outputBuffer.append(" text-decoration: none;");
outputBuffer.append(" padding-left: 20px;");
outputBuffer.append("}");
outputBuffer.append(".lineAnchor {");
outputBuffer.append(" display: block;");
outputBuffer.append(" padding-right: 20px;");
outputBuffer.append("}");
outputBuffer.append(".selectedLine {");
outputBuffer.append(" background-color: #EEF;");
outputBuffer.append(" font-weight: bold;");
outputBuffer.append("}");
outputBuffer.append("H1 {");
outputBuffer.append(" font-size: 1.1em;");
outputBuffer.append(" color: #666;");
outputBuffer.append("}");
outputBuffer.append("BODY {\n");
outputBuffer.append(" font-family: Arial;\n");
outputBuffer.append("}");
outputBuffer.append(" </style>\n");
outputBuffer.append(" </head>\n");
outputBuffer.append("\n");
outputBuffer.append(" <body>");
b.append("<p>");
b.append("This result is being rendered in HTML for easy viewing. ");
b.append("You may access this content as ");
outputBuffer.append("<p>");
outputBuffer.append("This result is being rendered in HTML for easy viewing. ");
outputBuffer.append("You may access this content as ");
b.append("<a href=\"");
b.append(createLinkHref(parameters, Constants.FORMAT_JSON));
b.append("\">Raw JSON</a> or ");
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_JSON));
outputBuffer.append("\">Raw JSON</a> or ");
b.append("<a href=\"");
b.append(createLinkHref(parameters, Constants.FORMAT_XML));
b.append("\">Raw XML</a>, ");
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMAT_XML));
outputBuffer.append("\">Raw XML</a>, ");
b.append(" or view this content in ");
outputBuffer.append(" or view this content in ");
b.append("<a href=\"");
b.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
b.append("\">HTML JSON</a> ");
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_JSON));
outputBuffer.append("\">HTML JSON</a> ");
b.append("or ");
b.append("<a href=\"");
b.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML));
b.append("\">HTML XML</a>.");
outputBuffer.append("or ");
outputBuffer.append("<a href=\"");
outputBuffer.append(createLinkHref(parameters, Constants.FORMATS_HTML_XML));
outputBuffer.append("\">HTML XML</a>.");
Date startTime = (Date) theServletRequest.getAttribute(RestfulServer.REQUEST_START_TIME);
if (startTime != null) {
long time = System.currentTimeMillis() - startTime.getTime();
b.append(" Response generated in ");
b.append(time);
b.append("ms.");
outputBuffer.append(" Response generated in ");
outputBuffer.append(time);
outputBuffer.append("ms.");
}
b.append("</p>");
outputBuffer.append("</p>");
b.append("\n");
outputBuffer.append("\n");
// status (e.g. HTTP 200 OK)
String statusName = Constants.HTTP_STATUS_NAMES.get(theServletResponse.getStatus());
statusName = defaultString(statusName);
b.append("<div class=\"httpStatusDiv\">");
b.append("HTTP ");
b.append(theServletResponse.getStatus());
b.append(" ");
b.append(statusName);
b.append("</div>");
outputBuffer.append("<div class=\"httpStatusDiv\">");
outputBuffer.append("HTTP ");
outputBuffer.append(theServletResponse.getStatus());
outputBuffer.append(" ");
outputBuffer.append(statusName);
outputBuffer.append("</div>");
b.append("\n");
b.append("\n");
outputBuffer.append("\n");
outputBuffer.append("\n");
try {
if (isShowRequestHeaders()) {
streamRequestHeaders(theServletRequest, b);
streamRequestHeaders(theServletRequest, outputBuffer);
}
if (isShowResponseHeaders()) {
streamResponseHeaders(theRequestDetails, theServletResponse, b);
streamResponseHeaders(theRequestDetails, theServletResponse, outputBuffer);
}
} catch (Throwable t) {
// ignore (this will hit if we're running in a servlet 2.5 environment)
}
b.append("<h1>Response Body</h1>");
outputBuffer.append("<h1>Response Body</h1>");
b.append("<div class=\"responseBodyTable\">");
outputBuffer.append("<div class=\"responseBodyTable\">");
// Response Body
b.append("<div class=\"responseBodyTableSecondColumn\"><pre>");
outputBuffer.append("<div class=\"responseBodyTableSecondColumn\"><pre>");
StringBuilder target = new StringBuilder();
int linesCount = format(encoded, target, encoding);
b.append(target);
b.append("</pre></div>");
outputBuffer.append(target);
outputBuffer.append("</pre></div>");
// Line Numbers
b.append("<div class=\"responseBodyTableFirstColumn\"><pre>");
outputBuffer.append("<div class=\"responseBodyTableFirstColumn\"><pre>");
for (int i = 1; i <= linesCount; i++) {
b.append("<div class=\"lineAnchor\" id=\"anchor");
b.append(i);
b.append("\">");
outputBuffer.append("<div class=\"lineAnchor\" id=\"anchor");
outputBuffer.append(i);
outputBuffer.append("\">");
b.append("<a href=\"#L");
b.append(i);
b.append("\" name=\"L");
b.append(i);
b.append("\" id=\"L");
b.append(i);
b.append("\">");
b.append(i);
b.append("</a></div>");
outputBuffer.append("<a href=\"#L");
outputBuffer.append(i);
outputBuffer.append("\" name=\"L");
outputBuffer.append(i);
outputBuffer.append("\" id=\"L");
outputBuffer.append(i);
outputBuffer.append("\">");
outputBuffer.append(i);
outputBuffer.append("</a></div>");
}
b.append("</div></td>");
outputBuffer.append("</div></td>");
b.append("</div>");
outputBuffer.append("</div>");
b.append("\n");
outputBuffer.append("\n");
InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js");
String jsStr = jsStream != null ? IOUtils.toString(jsStream, "UTF-8") : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')";
jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest());
b.append("<script type=\"text/javascript\">");
b.append(jsStr);
b.append("</script>\n");
outputBuffer.append("<script type=\"text/javascript\">");
outputBuffer.append(jsStr);
outputBuffer.append("</script>\n");
b.append("</body>");
b.append("</html>");
String out = b.toString();
StopWatch writeSw = new StopWatch();
theServletResponse.getWriter().append(outputBuffer);
theServletResponse.getWriter().flush();
theServletResponse.getWriter().append("<div class=\"sizeInfo\">");
theServletResponse.getWriter().append("Wrote ");
writeLength(theServletResponse, encoded.length());
theServletResponse.getWriter().append(" (");
writeLength(theServletResponse, outputBuffer.length());
theServletResponse.getWriter().append(" total including HTML)");
theServletResponse.getWriter().append(" in estimated ");
theServletResponse.getWriter().append(writeSw.toString());
theServletResponse.getWriter().append("</div>");
theServletResponse.getWriter().append("</body>");
theServletResponse.getWriter().append("</html>");
theServletResponse.getWriter().append(out);
theServletResponse.getWriter().close();
} catch (IOException e) {
throw new InternalErrorException(e);
}
}
private void writeLength(HttpServletResponse theServletResponse, int theLength) throws IOException {
double kb = ((double)theLength) / FileUtils.ONE_KB;
if (kb <= 1000) {
theServletResponse.getWriter().append(String.format("%.1f", kb)).append(" KB");
} else {
double mb = kb / 1000;
theServletResponse.getWriter().append(String.format("%.1f", mb)).append(" MB");
}
}
private void streamResponseHeaders(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, StringBuilder b) {
if (theServletResponse.getHeaderNames().isEmpty() == false) {
b.append("<h1>Response Headers</h1>");

View File

@ -56,7 +56,7 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<MethodOutcome> {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class);
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE);
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE, RestOperationTypeEnum.PATCH);
private boolean myReturnVoid;
@ -99,11 +99,11 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
return Constants.STATUS_HTTP_200_OK;
case UPDATE:
case PATCH:
if (response == null || response.getCreated() == null || Boolean.FALSE.equals(response.getCreated())) {
return Constants.STATUS_HTTP_200_OK;
}
return Constants.STATUS_HTTP_201_CREATED;
case VALIDATE:
case DELETE:
default:

View File

@ -1,198 +0,0 @@
package ca.uhn.fhir.rest.server.provider;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* This class is a simple implementation of the resource provider
* interface that uses a HashMap to store all resources in memory.
* It is essentially a copy of {@link ca.uhn.fhir.rest.server.provider.HashMapResourceProvider}
* with the {@link Update} and {@link ResourceParam} annotations removed from method
* {@link ca.uhn.fhir.rest.server.provider.HashMapResourceProvider#update(IBaseResource)}.
* Non-generic subclasses of this abstract class may implement their own annotated methods (e.g. a conditional
* update method specifically for ConceptMap resources).
* <p>
* This class currently supports the following FHIR operations:
* </p>
* <ul>
* <li>Create</li>
* <li>Update existing resource</li>
* <li>Update non-existing resource (e.g. create with client-supplied ID)</li>
* <li>Delete</li>
* <li>Search by resource type with no parameters</li>
* </ul>
*
* @param <T> The resource type to support
*/
public class AbstractHashMapResourceProvider<T extends IBaseResource> implements IResourceProvider {
private static final Logger ourLog = LoggerFactory.getLogger(AbstractHashMapResourceProvider.class);
private final Class<T> myResourceType;
private final FhirContext myFhirContext;
private final String myResourceName;
protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new HashMap<>();
private long myNextId;
/**
* Constructor
*
* @param theFhirContext The FHIR context
* @param theResourceType The resource type to support
*/
@SuppressWarnings("WeakerAccess")
public AbstractHashMapResourceProvider(FhirContext theFhirContext, Class<T> theResourceType) {
myFhirContext = theFhirContext;
myResourceType = theResourceType;
myResourceName = myFhirContext.getResourceDefinition(theResourceType).getName();
clear();
}
/**
* Clear all data held in this resource provider
*/
public void clear() {
myNextId = 1;
myIdToVersionToResourceMap.clear();
}
@Create
public MethodOutcome create(@ResourceParam T theResource) {
long idPart = myNextId++;
String idPartAsString = Long.toString(idPart);
Long versionIdPart = 1L;
IIdType id = store(theResource, idPartAsString, versionIdPart);
return new MethodOutcome()
.setCreated(true)
.setId(id);
}
@Delete
public MethodOutcome delete(@IdParam IIdType theId) {
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
if (versions == null || versions.isEmpty()) {
throw new ResourceNotFoundException(theId);
}
long nextVersion = versions.lastEntry().getKey() + 1L;
IIdType id = store(null, theId.getIdPart(), nextVersion);
return new MethodOutcome()
.setId(id);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return myResourceType;
}
private synchronized TreeMap<Long, T> getVersionToResource(String theIdPart) {
if (!myIdToVersionToResourceMap.containsKey(theIdPart)) {
myIdToVersionToResourceMap.put(theIdPart, new TreeMap<Long, T>());
}
return myIdToVersionToResourceMap.get(theIdPart);
}
@Read(version = true)
public IBaseResource read(@IdParam IIdType theId) {
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
if (versions == null || versions.isEmpty()) {
throw new ResourceNotFoundException(theId);
}
if (theId.hasVersionIdPart()) {
Long versionId = theId.getVersionIdPartAsLong();
if (!versions.containsKey(versionId)) {
throw new ResourceNotFoundException(theId);
} else {
T resource = versions.get(versionId);
if (resource == null) {
throw new ResourceGoneException(theId);
}
return resource;
}
} else {
return versions.lastEntry().getValue();
}
}
@Search
public List<IBaseResource> search() {
List<IBaseResource> retVal = new ArrayList<>();
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
if (next.isEmpty() == false) {
retVal.add(next.lastEntry().getValue());
}
}
return retVal;
}
private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart) {
IIdType id = myFhirContext.getVersion().newIdType();
id.setParts(null, myResourceName, theIdPart, Long.toString(theVersionIdPart));
if (theResource != null) {
theResource.setId(id);
}
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
versionToResource.put(theVersionIdPart, theResource);
ourLog.info("Storing resource with ID: {}", id.getValue());
return id;
}
public MethodOutcome update(T theResource) {
String idPartAsString = theResource.getIdElement().getIdPart();
TreeMap<Long, T> versionToResource = getVersionToResource(idPartAsString);
Long versionIdPart;
Boolean created;
if (versionToResource.isEmpty()) {
versionIdPart = 1L;
created = true;
} else {
versionIdPart = versionToResource.lastKey() + 1L;
created = false;
}
IIdType id = store(theResource, idPartAsString, versionIdPart);
return new MethodOutcome()
.setCreated(created)
.setId(id);
}
}

View File

@ -34,6 +34,7 @@ import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.ValidateUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -44,6 +45,8 @@ import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import static org.apache.commons.lang3.StringUtils.isBlank;
/**
* This class is a simple implementation of the resource provider
* interface that uses a HashMap to store all resources in memory.
@ -65,8 +68,8 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
private final Class<T> myResourceType;
private final FhirContext myFhirContext;
private final String myResourceName;
protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new LinkedHashMap<>();
protected Map<String, LinkedList<T>> myIdToHistory = new LinkedHashMap<>();
protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = Collections.synchronizedMap(new LinkedHashMap<>());
protected Map<String, LinkedList<T>> myIdToHistory = Collections.synchronizedMap(new LinkedHashMap<>());
protected LinkedList<T> myTypeHistory = new LinkedList<>();
private long myNextId;
private AtomicLong myDeleteCount = new AtomicLong(0);
@ -188,9 +191,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
}
private synchronized TreeMap<Long, T> getVersionToResource(String theIdPart) {
if (!myIdToVersionToResourceMap.containsKey(theIdPart)) {
myIdToVersionToResourceMap.put(theIdPart, new TreeMap<>());
}
myIdToVersionToResourceMap.computeIfAbsent(theIdPart, t -> new TreeMap<>());
return myIdToVersionToResourceMap.get(theIdPart);
}
@ -333,17 +334,22 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
myTypeHistory.addFirst(theResource);
// Store to ID history map
if (!myIdToHistory.containsKey(theIdPart)) {
myIdToHistory.put(theIdPart, new LinkedList<>());
}
myIdToHistory.computeIfAbsent(theIdPart, t -> new LinkedList<>());
myIdToHistory.get(theIdPart).addFirst(theResource);
// Return the newly assigned ID including the version ID
return id;
}
/**
* @param theConditional This is provided only so that subclasses can implement if they want
*/
@Update
public MethodOutcome update(@ResourceParam T theResource) {
public MethodOutcome update(
@ResourceParam T theResource,
@ConditionalUrlParam String theConditional) {
ValidateUtil.isTrueOrThrowInvalidRequest(isBlank(theConditional), "This server doesn't support conditional update");
String idPartAsString = theResource.getIdElement().getIdPart();
TreeMap<Long, T> versionToResource = getVersionToResource(idPartAsString);

View File

@ -1786,7 +1786,7 @@ public class AuthorizationInterceptorDstu2Test {
httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
assertEquals(204, status.getStatusLine().getStatusCode());
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}

View File

@ -1687,7 +1687,7 @@ public class AuthorizationInterceptorR4Test {
patch.setEntity(new StringEntity(patchBody, ContentType.create(Constants.CT_JSON_PATCH, Charsets.UTF_8)));
CloseableHttpResponse status = ourClient.execute(patch);
extractResponseAndClose(status);
assertEquals(204, status.getStatusLine().getStatusCode());
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}
@ -2964,7 +2964,7 @@ public class AuthorizationInterceptorR4Test {
httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json")));
status = ourClient.execute(httpPost);
extractResponseAndClose(status);
assertEquals(204, status.getStatusLine().getStatusCode());
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}

View File

@ -17,8 +17,8 @@
<properties>
<features.file>features.xml</features.file>
<pax-logging-version>1.10.1</pax-logging-version>
<felix-framework-version>6.0.0</felix-framework-version>
<pax-logging-version>1.8.6</pax-logging-version>
<felix-framework-version>3.2.2</felix-framework-version>
</properties>
<dependencies>

View File

@ -33,7 +33,7 @@
</description>
<properties>
<pax.exam.version>4.12.0</pax.exam.version>
<pax.exam.version>4.9.1</pax.exam.version>
</properties>
<dependencies>

View File

@ -468,6 +468,10 @@
<developer>
<id>jbalbien</id>
</developer>
<developer>
<id>alinleonard</id>
<name>Alin Leonard</name>
</developer>
</developers>
<licenses>
@ -489,7 +493,7 @@
<!-- Dependency Versions -->
<activation_api_version>1.2.0</activation_api_version>
<apache_karaf_version>4.2.0</apache_karaf_version>
<apache_karaf_version>4.1.4</apache_karaf_version>
<aries_spifly_version>1.0.10</aries_spifly_version>
<caffeine_version>2.6.2</caffeine_version>
<commons_codec_version>1.11</commons_codec_version>
@ -818,7 +822,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.1.1</version>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>

View File

@ -253,6 +253,26 @@
A bug was fixed in JPA server searches: When performing a search with a _lastUpdate
filter, the filter was applied to any _include values, which it should not have been.
Thanks to Deepak Garg for reporting!
<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">
ResponseHighlighterInterceptor now displays the total size of the output and
an estimate of the transfer time at the bottom of the response.
</action>
<action type="add" issue="1022">
The Prefer header is now honoured for HTTP PATCH requests. Thanks to
Alin Leonard for the Pull Request!
</action>
</release>
<release version="3.4.0" date="2018-05-28">