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

This commit is contained in:
jamesagnew 2019-01-05 11:33:08 -05:00
commit 8f8385627f
17 changed files with 802 additions and 592 deletions

View File

@ -1305,9 +1305,6 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, true);
// FIXME: remove
ourLog.info("** Updated setting to: " + new InstantType(theUpdateTime).getValueAsString());
theEntity.setUpdated(theUpdateTime);
if (theResource instanceof IResource) {
theEntity.setLanguage(((IResource) theResource).getLanguage().getValue());
@ -1322,11 +1319,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
changed = populateResourceIntoEntity(theRequest, theResource, theEntity, false);
// FIXME: remove
ourLog.info("** Updated setting to: " + new InstantType(theUpdateTime).getValueAsString());
theEntity.setUpdated(theUpdateTime);
// theEntity.setLanguage(theResource.getLanguage().getValue());
theEntity.setIndexStatus(null);
}

View File

@ -148,7 +148,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
theResource.setId(UUID.randomUUID().toString());
}
// FIXME: this is where one date is created
return doCreate(theResource, theIfNoneExist, thePerformIndexing, theUpdateTimestamp, theRequestDetails);
}

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.search.warm.WarmCacheEntry;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
@ -98,6 +99,7 @@ public class DaoConfig {
private boolean myUniqueIndexesEnabled = true;
private boolean myUniqueIndexesCheckedBeforeSave = true;
private boolean myEnforceReferentialIntegrityOnWrite = true;
private SearchTotalModeEnum myDefaultTotalMode = null;
private int myEverythingIncludesFetchPageSize = 50;
/**
* update setter javadoc if default changes
@ -141,7 +143,6 @@ public class DaoConfig {
private boolean myDisableHashBasedSearches;
private boolean myEnableInMemorySubscriptionMatching = true;
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
/**
* Constructor
*/
@ -159,6 +160,30 @@ public class DaoConfig {
}
}
/**
* If a non-null value is supplied (default is <code>null</code>), a default
* for the <code>_total</code> parameter may be specified here. For example,
* setting this value to {@link SearchTotalModeEnum#ACCURATE} will force a
* count to always be calculated for all searches. This can have a performance impact
* since it means that a count query will always be performed, but this is desirable
* for some solutions.
*/
public SearchTotalModeEnum getDefaultTotalMode() {
return myDefaultTotalMode;
}
/**
* If a non-null value is supplied (default is <code>null</code>), a default
* for the <code>_total</code> parameter may be specified here. For example,
* setting this value to {@link SearchTotalModeEnum#ACCURATE} will force a
* count to always be calculated for all searches. This can have a performance impact
* since it means that a count query will always be performed, but this is desirable
* for some solutions.
*/
public void setDefaultTotalMode(SearchTotalModeEnum theDefaultTotalMode) {
myDefaultTotalMode = theDefaultTotalMode;
}
/**
* Returns a set of searches that should be kept "warm", meaning that
* searches will periodically be performed in the background to
@ -492,18 +517,6 @@ public class DaoConfig {
return myInterceptors;
}
public void registerInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
if (!myInterceptors.contains(theInterceptor)) {
myInterceptors.add(theInterceptor);
}
}
public void unregisterInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor);
}
/**
* This may be used to optionally register server interceptors directly against the DAOs.
*/
@ -521,6 +534,18 @@ public class DaoConfig {
}
}
public void registerInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
if (!myInterceptors.contains(theInterceptor)) {
myInterceptors.add(theInterceptor);
}
}
public void unregisterInterceptor(IServerInterceptor theInterceptor) {
Validate.notNull(theInterceptor, "Interceptor can not be null");
myInterceptors.remove(theInterceptor);
}
/**
* See {@link #setMaximumExpansionSize(int)}
*/
@ -1477,7 +1502,6 @@ public class DaoConfig {
/**
* This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
* to the server matching these types will be activated.
*
*/
public DaoConfig addSupportedSubscriptionType(Subscription.SubscriptionChannelType theSubscriptionChannelType) {
myModelConfig.addSupportedSubscriptionType(theSubscriptionChannelType);
@ -1487,7 +1511,6 @@ public class DaoConfig {
/**
* This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
* to the server matching these types will be activated.
*
*/
public Set<Subscription.SubscriptionChannelType> getSupportedSubscriptionTypes() {
return myModelConfig.getSupportedSubscriptionTypes();
@ -1515,7 +1538,6 @@ public class DaoConfig {
}
public enum IndexEnabledEnum {
ENABLED,
DISABLED

View File

@ -103,9 +103,19 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private IPagingProvider myPagingProvider;
private int mySyncSize = DEFAULT_SYNC_SIZE;
/** Set in {@link #start()} */
/**
* Set in {@link #start()}
*/
private boolean myCustomIsolationSupported;
/**
* Constructor
*/
public SearchCoordinatorSvcImpl() {
CustomizableThreadFactory threadFactory = new CustomizableThreadFactory("search_coord_");
myExecutor = Executors.newCachedThreadPool(threadFactory);
}
@PostConstruct
public void start() {
if (myManagedTxManager instanceof JpaTransactionManager) {
@ -119,14 +129,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
}
}
/**
* Constructor
*/
public SearchCoordinatorSvcImpl() {
CustomizableThreadFactory threadFactory = new CustomizableThreadFactory("search_coord_");
myExecutor = Executors.newCachedThreadPool(threadFactory);
}
@Override
public void cancelAllActiveSearches() {
for (BaseTask next : myIdToSearchTask.values()) {
@ -466,6 +468,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private List<Long> myPreviouslyAddedResourcePids;
private Integer myMaxResultsToFetch;
private int myCountFetchedDuringThisPass;
/**
* Constructor
*/
@ -763,7 +766,10 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* before doing anything else.
*/
boolean wantOnlyCount = SummaryEnum.COUNT.equals(myParams.getSummaryMode());
boolean wantCount = wantOnlyCount || SearchTotalModeEnum.ACCURATE.equals(myParams.getSearchTotalMode());
boolean wantCount =
wantOnlyCount ||
SearchTotalModeEnum.ACCURATE.equals(myParams.getSearchTotalMode()) ||
(myParams.getSearchTotalMode() == null && SearchTotalModeEnum.ACCURATE.equals(myDaoConfig.getDefaultTotalMode()));
if (wantCount) {
ourLog.trace("Performing count");
ISearchBuilder sb = newSearchBuilder();

View File

@ -61,7 +61,7 @@ public class DaoSubscriptionMatcher implements ISubscriptionMatcher {
ourLog.debug("Subscription check found {} results for query: {}", results.size(), criteria);
return new SubscriptionMatchResult(results.size() > 0);
return new SubscriptionMatchResult(results.size() > 0, "DATABASE");
}
/**

View File

@ -8,6 +8,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
@ -341,7 +342,7 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
request = mock(HttpServletRequest.class);
StringAndListParam param;
ourLog.info("Pt1:{} Pt2:{} Obs1:{} Obs2:{} Obs3:{}", new Object[] {ptId1.getIdPart(), ptId2.getIdPart(), obsId1.getIdPart(), obsId2.getIdPart(), obsId3.getIdPart()});
ourLog.info("Pt1:{} Pt2:{} Obs1:{} Obs2:{} Obs3:{}", ptId1.getIdPart(), ptId2.getIdPart(), obsId1.getIdPart(), obsId2.getIdPart(), obsId3.getIdPart());
param = new StringAndListParam();
param.addAnd(new StringOrListParam().addOr(new StringParam("obsvalue1")));
@ -433,7 +434,7 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
request = mock(HttpServletRequest.class);
StringAndListParam param;
ourLog.info("Pt1:{} Pt2:{} Obs1:{} Obs2:{} Obs3:{}", new Object[] {ptId1.getIdPart(), ptId2.getIdPart(), obsId1.getIdPart(), obsId2.getIdPart(), obsId3.getIdPart()});
ourLog.info("Pt1:{} Pt2:{} Obs1:{} Obs2:{} Obs3:{}", ptId1.getIdPart(), ptId2.getIdPart(), obsId1.getIdPart(), obsId2.getIdPart(), obsId3.getIdPart());
param = new StringAndListParam();
param.addAnd(new StringOrListParam().addOr(new StringParam("obsvalue1")));
@ -485,7 +486,7 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
*/
@Test
public void testSearchDontReindexForUpdateWithIndexDisabled() {
BaseHapiFhirResourceDao.setDisableIncrementOnUpdateForUnitTest(true);
BaseHapiFhirDao.setDisableIncrementOnUpdateForUnitTest(true);
Patient patient;
SearchParameterMap map;

View File

@ -33,6 +33,7 @@ public class ResourceProviderSummaryModeR4Test extends BaseResourceProviderR4Tes
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
myDaoConfig.setDefaultTotalMode(null);
}
@Override
@ -93,6 +94,24 @@ public class ResourceProviderSummaryModeR4Test extends BaseResourceProviderR4Tes
assertEquals(10, outcome.getEntry().size());
}
/**
* Count and data via config - Should include both a count and the data portions of results
*/
@Test
public void testSearchWithTotalAccurateSpecifiedAsDefault() {
myDaoConfig.setDefaultTotalMode(SearchTotalModeEnum.ACCURATE);
Bundle outcome = ourClient
.search()
.forResource(Patient.class)
.where(Patient.ACTIVE.exactly().code("true"))
.returnBundle(Bundle.class)
.execute();
assertEquals(new Integer(104), outcome.getTotalElement().getValue());
assertEquals(10, outcome.getEntry().size());
}
/**
* No summary mode - Should return the first page of results but not
* have the total available yet
@ -110,6 +129,26 @@ public class ResourceProviderSummaryModeR4Test extends BaseResourceProviderR4Tes
assertEquals(10, outcome.getEntry().size());
}
/**
* No summary mode - Should return the first page of results but not
* have the total available yet
*/
@Test
public void testSearchTotalNoneOverridingDefault() {
myDaoConfig.setDefaultTotalMode(SearchTotalModeEnum.ACCURATE);
Bundle outcome = ourClient
.search()
.forResource(Patient.class)
.where(Patient.ACTIVE.exactly().code("true"))
.totalMode(SearchTotalModeEnum.NONE)
.returnBundle(Bundle.class)
.execute();
assertEquals(null, outcome.getTotalElement().getValue());
assertEquals(10, outcome.getEntry().size());
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -176,6 +176,27 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
}
@Test
public void testRepeatedDeliveries() throws Exception {
String payload = "application/fhir+json";
String code = "1000000050";
String criteria1 = "Observation?";
createSubscription(criteria1, payload);
waitForActivatedSubscriptionCount(1);
for (int i = 0; i < 100; i++) {
Observation observation = new Observation();
observation.getIdentifierFirstRep().setSystem("foo").setValue("ID" + i);
observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT");
observation.setStatus(Observation.ObservationStatus.FINAL);
myObservationDao.create(observation);
}
waitForSize(100, ourUpdatedObservations);
}
@Test
public void testActiveSubscriptionShouldntReActivate() throws Exception {

View File

@ -42,6 +42,7 @@ import java.util.function.Predicate;
@Service
public class CriteriaResourceMatcher {
public static final String CRITERIA = "CRITERIA";
@Autowired
private MatchUrlService myMatchUrlService;
@Autowired
@ -52,7 +53,7 @@ public class CriteriaResourceMatcher {
try {
searchParameterMap = myMatchUrlService.translateMatchUrl(theCriteria, theResourceDefinition);
} catch (UnsupportedOperationException e) {
return new SubscriptionMatchResult(theCriteria);
return new SubscriptionMatchResult(theCriteria, CRITERIA);
}
searchParameterMap.clean();
if (searchParameterMap.getLastUpdated() != null) {
@ -67,13 +68,13 @@ public class CriteriaResourceMatcher {
return result;
}
}
return SubscriptionMatchResult.MATCH;
return new SubscriptionMatchResult(true, CRITERIA);
}
// This method is modelled from SearchBuilder.searchForIdsWithAndOr()
private SubscriptionMatchResult matchIdsWithAndOr(String theParamName, List<List<? extends IQueryParameterType>> theAndOrParams, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) {
if (theAndOrParams.isEmpty()) {
return SubscriptionMatchResult.MATCH;
return new SubscriptionMatchResult(true, CRITERIA);
}
if (hasQualifiers(theAndOrParams)) {
@ -91,19 +92,19 @@ public class CriteriaResourceMatcher {
}
if (theParamName.equals(IAnyResource.SP_RES_ID)) {
return new SubscriptionMatchResult(theParamName);
return new SubscriptionMatchResult(theParamName, CRITERIA);
} else if (theParamName.equals(IAnyResource.SP_RES_LANGUAGE)) {
return new SubscriptionMatchResult(theParamName);
return new SubscriptionMatchResult(theParamName, CRITERIA);
} else if (theParamName.equals(Constants.PARAM_HAS)) {
return new SubscriptionMatchResult(theParamName);
return new SubscriptionMatchResult(theParamName, CRITERIA);
} else if (theParamName.equals(Constants.PARAM_TAG) || theParamName.equals(Constants.PARAM_PROFILE) || theParamName.equals(Constants.PARAM_SECURITY)) {
return new SubscriptionMatchResult(theParamName);
return new SubscriptionMatchResult(theParamName, CRITERIA);
} else {
@ -123,16 +124,16 @@ public class CriteriaResourceMatcher {
case URI:
case DATE:
case REFERENCE:
return new SubscriptionMatchResult(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)));
return new SubscriptionMatchResult(theAndOrParams.stream().anyMatch(nextAnd -> matchParams(theResourceName, theParamName, theParamDef, nextAnd, theSearchParams)), CRITERIA);
case COMPOSITE:
case HAS:
case SPECIAL:
default:
return new SubscriptionMatchResult(theParamName);
return new SubscriptionMatchResult(theParamName, CRITERIA);
}
} else {
if (Constants.PARAM_CONTENT.equals(theParamName) || Constants.PARAM_TEXT.equals(theParamName)) {
return new SubscriptionMatchResult(theParamName);
return new SubscriptionMatchResult(theParamName, CRITERIA);
} else {
throw new InvalidRequestException("Unknown search parameter " + theParamName + " for resource type " + theResourceName);
}

View File

@ -21,34 +21,34 @@ package ca.uhn.fhir.jpa.subscription.module.matcher;
*/
public class SubscriptionMatchResult {
// This could be an enum, but we may want to include details about unsupported matches in the future
public static final SubscriptionMatchResult MATCH = new SubscriptionMatchResult(true);
public static final SubscriptionMatchResult NO_MATCH = new SubscriptionMatchResult(false);
private final boolean myMatch;
private final boolean mySupported;
private final String myUnsupportedParameter;
private final String myUnsupportedReason;
private final String myMatcherShortName;
public SubscriptionMatchResult(boolean theMatch) {
public SubscriptionMatchResult(boolean theMatch, String theMatcherShortName) {
this.myMatch = theMatch;
this.mySupported = true;
this.myUnsupportedParameter = null;
this.myUnsupportedReason = null;
this.myMatcherShortName = theMatcherShortName;
}
public SubscriptionMatchResult(String theUnsupportedParameter) {
public SubscriptionMatchResult(String theUnsupportedParameter, String theMatcherShortName) {
this.myMatch = false;
this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = "Parameter not supported";
this.myMatcherShortName = theMatcherShortName;
}
public SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason) {
public SubscriptionMatchResult(String theUnsupportedParameter, String theUnsupportedReason, String theMatcherShortName) {
this.myMatch = false;
this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = theUnsupportedReason;
this.myMatcherShortName = theMatcherShortName;
}
public boolean supported() {
@ -62,4 +62,12 @@ public class SubscriptionMatchResult {
public String getUnsupportedReason() {
return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason;
}
/**
* Returns a short name of the matcher that generated this
* response, for use in logging
*/
public String matcherShortName() {
return myMatcherShortName;
}
}

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.subscription.module.ResourceModifiedMessage;
import ca.uhn.fhir.jpa.subscription.module.cache.ActiveSubscription;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry;
import ca.uhn.fhir.jpa.subscription.module.matcher.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
@ -113,11 +114,12 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
continue;
}
if (!mySubscriptionMatcher.match(nextCriteriaString, theMsg).matched()) {
SubscriptionMatchResult matchResult = mySubscriptionMatcher.match(nextCriteriaString, theMsg);
if (!matchResult.matched()) {
continue;
}
ourLog.debug("Found match: queueing rest-hook notification for resource: {}", id.toUnqualifiedVersionless().getValue());
ourLog.info("Subscription {} was matched by resource {} using matcher {}", nextActiveSubscription.getSubscription().getIdElement(myFhirContext).getValue(), id.toUnqualifiedVersionless().getValue(), matchResult.matcherShortName());
ResourceDeliveryMessage deliveryMsg = new ResourceDeliveryMessage();
deliveryMsg.setPayload(myFhirContext, theMsg.getNewPayload(myFhirContext));
@ -130,7 +132,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
if (deliveryChannel != null) {
deliveryChannel.send(wrappedMsg);
} else {
ourLog.warn("Do not have deliovery channel for subscription {}", nextActiveSubscription.getIdElement(myFhirContext));
ourLog.warn("Do not have delivery channel for subscription {}", nextActiveSubscription.getIdElement(myFhirContext));
}
}
}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.api.server;
import ca.uhn.fhir.context.api.BundleInclusionRule;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
@ -30,4 +31,7 @@ public interface IRestfulServer<T extends RequestDetails> extends IRestfulServer
BundleInclusionRule getBundleInclusionRule();
void setDefaultPreferReturn(PreferReturnEnum theDefaultPreferReturn);
PreferReturnEnum getDefaultPreferReturn();
}

View File

@ -31,10 +31,7 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.Destroy;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IFhirVersionServer;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.ParseAction;
@ -55,6 +52,8 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
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 javax.servlet.ServletException;
import javax.servlet.UnavailableException;
@ -96,8 +95,12 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
*/
public static final String SERVLET_CONTEXT_ATTRIBUTE = "ca.uhn.fhir.rest.server.RestfulServer.servlet_context";
private static final ExceptionHandlingInterceptor DEFAULT_EXCEPTION_HANDLER = new ExceptionHandlingInterceptor();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class);
private static final Logger ourLog = LoggerFactory.getLogger(RestfulServer.class);
private static final long serialVersionUID = 1L;
/**
* Default value for {@link #setDefaultPreferReturn(PreferReturnEnum)}
*/
public static final PreferReturnEnum DEFAULT_PREFER_RETURN = PreferReturnEnum.REPRESENTATION;
private final List<IServerInterceptor> myInterceptors = new ArrayList<>();
private final List<Object> myPlainProviders = new ArrayList<>();
private final List<IResourceProvider> myResourceProviders = new ArrayList<>();
@ -126,6 +129,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
private boolean myUseBrowserFriendlyContentTypes;
private ITenantIdentificationStrategy myTenantIdentificationStrategy;
private Date myConformanceDate;
private PreferReturnEnum myDefaultPreferReturn = DEFAULT_PREFER_RETURN;
/**
* Constructor. Note that if no {@link FhirContext} is passed in to the server (either through the constructor, or
@ -823,7 +827,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
String completeUrl;
Map<String, String[]> params = null;
if (StringUtils.isNotBlank(theRequest.getQueryString())) {
if (isNotBlank(theRequest.getQueryString())) {
completeUrl = requestUrl + "?" + theRequest.getQueryString();
/*
* By default, we manually parse the request params (the URL params, or the body for
@ -1629,6 +1633,39 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
theResponse.getWriter().write(theException.getMessage());
}
/**
* By default, server create/update/patch/transaction methods return a copy of the resource
* as it was stored. This may be overridden by the client using the
* <code>Prefer</code> header.
* <p>
* This setting changes the default behaviour if no Prefer header is supplied by the client.
* The default is {@link PreferReturnEnum#REPRESENTATION}
* </p>
*
* @see <a href="http://hl7.org/fhir/http.html#ops">HL7 FHIR Specification</a> section on the Prefer header
*/
@Override
public PreferReturnEnum getDefaultPreferReturn() {
return myDefaultPreferReturn;
}
/**
* By default, server create/update/patch/transaction methods return a copy of the resource
* as it was stored. This may be overridden by the client using the
* <code>Prefer</code> header.
* <p>
* This setting changes the default behaviour if no Prefer header is supplied by the client.
* The default is {@link PreferReturnEnum#REPRESENTATION}
* </p>
*
* @see <a href="http://hl7.org/fhir/http.html#ops">HL7 FHIR Specification</a> section on the Prefer header
*/
@Override
public void setDefaultPreferReturn(PreferReturnEnum theDefaultPreferReturn) {
Validate.notNull(theDefaultPreferReturn, "theDefaultPreferReturn must not be null");
myDefaultPreferReturn = theDefaultPreferReturn;
}
/**
* Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20)
*/

View File

@ -21,28 +21,9 @@ package ca.uhn.fhir.rest.server.method;
*/
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
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.SummaryEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IRestfulResponse;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -52,6 +33,18 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<MethodOutcome> {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class);
@ -198,19 +191,24 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
}
if (allowPrefer) {
outcome = resource;
String prefer = theRequest.getHeader(Constants.HEADER_PREFER);
PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer);
if (preferReturn != null) {
if (preferReturn == PreferReturnEnum.MINIMAL) {
if (preferReturn == null) {
preferReturn = theServer.getDefaultPreferReturn();
}
switch (preferReturn) {
case REPRESENTATION:
outcome = resource;
break;
case MINIMAL:
outcome = null;
}
else {
if (preferReturn == PreferReturnEnum.OPERATION_OUTCOME) {
break;
case OPERATION_OUTCOME:
outcome = originalOutcome;
break;
}
}
}
}
ResponseDetails responseDetails = new ResponseDetails();

View File

@ -1,9 +1,6 @@
package org.hl7.fhir.r4.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException;
@ -243,7 +240,7 @@ public class LiquidEngine implements IEvaluationContext {
n.closeUp();
if (terminators.length > 0)
if (!Utilities.existsInList(close, terminators))
throw new FHIRException("Script "+name+": Script "+name+": Found end of script looking for "+terminators);
throw new FHIRException("Script "+name+": Script "+name+": Found end of script looking for "+ Arrays.asList(terminators));
return close;
}

View File

@ -1,19 +1,16 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
import ca.uhn.fhir.rest.client.MyPatientWithExtensions;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
@ -24,37 +21,35 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.*;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.MyPatientWithExtensions;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
public class CreateR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CreateR4Test.class);
public static IBaseOperationOutcome ourReturnOo;
public static OperationOutcome ourReturnOo;
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4();
private static int ourPort;
private static Server ourServer;
private static RestfulServer ourServlet;
@After
public void after() {
ourServlet.setDefaultPreferReturn(RestfulServer.DEFAULT_PREFER_RETURN);
}
@Before
public void before() {
@ -219,6 +214,79 @@ public class CreateR4Test {
}
@Test
public void testCreatePreferDefaultRepresentation() throws Exception {
ourReturnOo = new OperationOutcome();
ourReturnOo.addIssue().setDiagnostics("FOO");
Patient p = new Patient();
p.setActive(true);
String body = ourCtx.newJsonParser().encodeResourceToString(p);
HttpPost httpPost;
httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.setEntity(new StringEntity(body, ContentType.parse("application/fhir+json; charset=utf-8")));
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("application/fhir+json;charset=utf-8", status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue());
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response was:\n{}", responseContent);
assertThat(responseContent, containsString("\"resourceType\":\"Patient\""));
}
}
@Test
public void testCreatePreferDefaultOperationOutcome() throws Exception {
ourReturnOo = new OperationOutcome();
ourReturnOo.addIssue().setDiagnostics("FOO");
Patient p = new Patient();
p.setActive(true);
String body = ourCtx.newJsonParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.setEntity(new StringEntity(body, ContentType.parse("application/fhir+json; charset=utf-8")));
ourServlet.setDefaultPreferReturn(PreferReturnEnum.OPERATION_OUTCOME);
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("application/fhir+json;charset=utf-8", status.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue());
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response was:\n{}", responseContent);
assertThat(responseContent, containsString("\"resourceType\":\"OperationOutcome\""));
}
}
@Test
public void testCreatePreferDefaultMinimal() throws Exception {
ourReturnOo = new OperationOutcome();
ourReturnOo.addIssue().setDiagnostics("FOO");
Patient p = new Patient();
p.setActive(true);
String body = ourCtx.newJsonParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.setEntity(new StringEntity(body, ContentType.parse("application/fhir+json; charset=utf-8")));
ourServlet.setDefaultPreferReturn(PreferReturnEnum.MINIMAL);
try (CloseableHttpResponse status = ourClient.execute(httpPost)) {
assertEquals(201, status.getStatusLine().getStatusCode());
assertNull(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE));
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
assertThat(responseContent, emptyOrNullString());
}
}
@Test
public void testSearch() throws Exception {
@ -248,36 +316,6 @@ public class CreateR4Test {
assertThat(responseContent, not(containsString("http://hl7.org/fhir/")));
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
PatientProviderCreate patientProviderCreate = new PatientProviderCreate();
PatientProviderRead patientProviderRead = new PatientProviderRead();
PatientProviderSearch patientProviderSearch = new PatientProviderSearch();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(patientProviderCreate, patientProviderRead, patientProviderSearch);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
public static class PatientProviderRead implements IResourceProvider {
@Read()
@ -299,12 +337,13 @@ public class CreateR4Test {
public Class<Patient> getResourceType() {
return Patient.class;
}
@Create()
public MethodOutcome create(@ResourceParam Patient theIdParam) {
assertNull(theIdParam.getIdElement().getIdPart());
theIdParam.setId("1");
theIdParam.getMeta().setVersionId("1");
return new MethodOutcome(new IdType("Patient", "1"), true).setOperationOutcome(ourReturnOo).setResource(theIdParam);
public MethodOutcome create(@ResourceParam Patient thePatient) {
assertNull(thePatient.getIdElement().getIdPart());
thePatient.setId("1");
thePatient.getMeta().setVersionId("1");
return new MethodOutcome(new IdType("Patient", "1"), true).setOperationOutcome(ourReturnOo).setResource(thePatient);
}
}
@ -336,4 +375,35 @@ public class CreateR4Test {
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
PatientProviderCreate patientProviderCreate = new PatientProviderCreate();
PatientProviderRead patientProviderRead = new PatientProviderRead();
PatientProviderSearch patientProviderSearch = new PatientProviderSearch();
ServletHandler proxyHandler = new ServletHandler();
ourServlet = new RestfulServer(ourCtx);
ourServlet.setResourceProviders(patientProviderCreate, patientProviderRead, patientProviderSearch);
ServletHolder servletHolder = new ServletHolder(ourServlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
}

View File

@ -183,7 +183,8 @@
it was not applied to all resources. This has been corrected.
</action>
<action type="change">
In example-projects/README.md and hapi-fhir-jpaserver-example/README.md, incidate that these examples projects
In example-projects/README.md and hapi-fhir-jpaserver-example/README.md, incidate that these examples
projects
are no longer maintained. The README.md points users to a starter project they should use for examples.
</action>
<action type="change">
@ -231,6 +232,17 @@
JPA Subscription deliveries did not always include the accurate versionId if the Subscription
module was configured to use an external queuing engine. This has been corrected.
</action>
<action type="add">
It is now possible in a plain or JPA server to specify the default return
type for create/update operations when no Prefer header has been provided
by the client.
</action>
<action type="add">
It is now possible in a JPA server to specify the _total calculation
behaviour if no parameter is supplied by the client. This is done using a
new setting on the DaoConfig. This can be used to force a total to
always be calculated for searches, including large ones.
</action>
</release>
<release version="3.6.0" date="2018-11-12" description="Food">
<action type="add">