Add config options for default Prefer header and _total param on server

This commit is contained in:
James Agnew 2019-01-04 16:12:45 -05:00
parent 8c87c7c089
commit 5b8fee869e
16 changed files with 402 additions and 189 deletions

View File

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

View File

@ -148,7 +148,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
theResource.setId(UUID.randomUUID().toString()); theResource.setId(UUID.randomUUID().toString());
} }
// FIXME: this is where one date is created
return doCreate(theResource, theIfNoneExist, thePerformIndexing, theUpdateTimestamp, theRequestDetails); 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.model.entity.ResourceEncodingEnum;
import ca.uhn.fhir.jpa.search.warm.WarmCacheEntry; import ca.uhn.fhir.jpa.search.warm.WarmCacheEntry;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
@ -25,9 +26,9 @@ import java.util.*;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -98,6 +99,7 @@ public class DaoConfig {
private boolean myUniqueIndexesEnabled = true; private boolean myUniqueIndexesEnabled = true;
private boolean myUniqueIndexesCheckedBeforeSave = true; private boolean myUniqueIndexesCheckedBeforeSave = true;
private boolean myEnforceReferentialIntegrityOnWrite = true; private boolean myEnforceReferentialIntegrityOnWrite = true;
private SearchTotalModeEnum myDefaultTotalMode = null;
private int myEverythingIncludesFetchPageSize = 50; private int myEverythingIncludesFetchPageSize = 50;
/** /**
* update setter javadoc if default changes * update setter javadoc if default changes
@ -141,7 +143,6 @@ public class DaoConfig {
private boolean myDisableHashBasedSearches; private boolean myDisableHashBasedSearches;
private boolean myEnableInMemorySubscriptionMatching = true; private boolean myEnableInMemorySubscriptionMatching = true;
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC; private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
/** /**
* Constructor * 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 * Returns a set of searches that should be kept "warm", meaning that
* searches will periodically be performed in the background to * searches will periodically be performed in the background to
@ -492,18 +517,6 @@ public class DaoConfig {
return myInterceptors; 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. * 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)} * 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 * This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
* to the server matching these types will be activated. * to the server matching these types will be activated.
*
*/ */
public DaoConfig addSupportedSubscriptionType(Subscription.SubscriptionChannelType theSubscriptionChannelType) { public DaoConfig addSupportedSubscriptionType(Subscription.SubscriptionChannelType theSubscriptionChannelType) {
myModelConfig.addSupportedSubscriptionType(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 * This setting indicates which subscription channel types are supported by the server. Any subscriptions submitted
* to the server matching these types will be activated. * to the server matching these types will be activated.
*
*/ */
public Set<Subscription.SubscriptionChannelType> getSupportedSubscriptionTypes() { public Set<Subscription.SubscriptionChannelType> getSupportedSubscriptionTypes() {
return myModelConfig.getSupportedSubscriptionTypes(); return myModelConfig.getSupportedSubscriptionTypes();
@ -1515,7 +1538,6 @@ public class DaoConfig {
} }
public enum IndexEnabledEnum { public enum IndexEnabledEnum {
ENABLED, ENABLED,
DISABLED DISABLED

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.search;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -103,9 +103,19 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private IPagingProvider myPagingProvider; private IPagingProvider myPagingProvider;
private int mySyncSize = DEFAULT_SYNC_SIZE; private int mySyncSize = DEFAULT_SYNC_SIZE;
/** Set in {@link #start()} */ /**
* Set in {@link #start()}
*/
private boolean myCustomIsolationSupported; private boolean myCustomIsolationSupported;
/**
* Constructor
*/
public SearchCoordinatorSvcImpl() {
CustomizableThreadFactory threadFactory = new CustomizableThreadFactory("search_coord_");
myExecutor = Executors.newCachedThreadPool(threadFactory);
}
@PostConstruct @PostConstruct
public void start() { public void start() {
if (myManagedTxManager instanceof JpaTransactionManager) { 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 @Override
public void cancelAllActiveSearches() { public void cancelAllActiveSearches() {
for (BaseTask next : myIdToSearchTask.values()) { for (BaseTask next : myIdToSearchTask.values()) {
@ -466,6 +468,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private List<Long> myPreviouslyAddedResourcePids; private List<Long> myPreviouslyAddedResourcePids;
private Integer myMaxResultsToFetch; private Integer myMaxResultsToFetch;
private int myCountFetchedDuringThisPass; private int myCountFetchedDuringThisPass;
/** /**
* Constructor * Constructor
*/ */
@ -763,7 +766,10 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
* before doing anything else. * before doing anything else.
*/ */
boolean wantOnlyCount = SummaryEnum.COUNT.equals(myParams.getSummaryMode()); 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) { if (wantCount) {
ourLog.trace("Performing count"); ourLog.trace("Performing count");
ISearchBuilder sb = newSearchBuilder(); 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); 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 javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.r4.model.Observation.ObservationStatus;
@ -341,7 +342,7 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
request = mock(HttpServletRequest.class); request = mock(HttpServletRequest.class);
StringAndListParam param; 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 = new StringAndListParam();
param.addAnd(new StringOrListParam().addOr(new StringParam("obsvalue1"))); param.addAnd(new StringOrListParam().addOr(new StringParam("obsvalue1")));
@ -433,7 +434,7 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
request = mock(HttpServletRequest.class); request = mock(HttpServletRequest.class);
StringAndListParam param; 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 = new StringAndListParam();
param.addAnd(new StringOrListParam().addOr(new StringParam("obsvalue1"))); param.addAnd(new StringOrListParam().addOr(new StringParam("obsvalue1")));
@ -485,7 +486,7 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
*/ */
@Test @Test
public void testSearchDontReindexForUpdateWithIndexDisabled() { public void testSearchDontReindexForUpdateWithIndexDisabled() {
BaseHapiFhirResourceDao.setDisableIncrementOnUpdateForUnitTest(true); BaseHapiFhirDao.setDisableIncrementOnUpdateForUnitTest(true);
Patient patient; Patient patient;
SearchParameterMap map; SearchParameterMap map;

View File

@ -33,6 +33,7 @@ public class ResourceProviderSummaryModeR4Test extends BaseResourceProviderR4Tes
mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null); mySearchCoordinatorSvcRaw.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE); mySearchCoordinatorSvcRaw.setSyncSizeForUnitTests(SearchCoordinatorSvcImpl.DEFAULT_SYNC_SIZE);
myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds()); myDaoConfig.setSearchPreFetchThresholds(new DaoConfig().getSearchPreFetchThresholds());
myDaoConfig.setDefaultTotalMode(null);
} }
@Override @Override
@ -93,6 +94,24 @@ public class ResourceProviderSummaryModeR4Test extends BaseResourceProviderR4Tes
assertEquals(10, outcome.getEntry().size()); 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 * No summary mode - Should return the first page of results but not
* have the total available yet * have the total available yet
@ -110,6 +129,26 @@ public class ResourceProviderSummaryModeR4Test extends BaseResourceProviderR4Tes
assertEquals(10, outcome.getEntry().size()); 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 @AfterClass
public static void afterClassClearContext() { public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest(); 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 @Test
public void testActiveSubscriptionShouldntReActivate() throws Exception { public void testActiveSubscriptionShouldntReActivate() throws Exception {

View File

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

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.subscription.module.matcher;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -21,34 +21,34 @@ package ca.uhn.fhir.jpa.subscription.module.matcher;
*/ */
public class SubscriptionMatchResult { 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 myMatch;
private final boolean mySupported; private final boolean mySupported;
private final String myUnsupportedParameter; private final String myUnsupportedParameter;
private final String myUnsupportedReason; private final String myUnsupportedReason;
private final String myMatcherShortName;
public SubscriptionMatchResult(boolean theMatch) { public SubscriptionMatchResult(boolean theMatch, String theMatcherShortName) {
this.myMatch = theMatch; this.myMatch = theMatch;
this.mySupported = true; this.mySupported = true;
this.myUnsupportedParameter = null; this.myUnsupportedParameter = null;
this.myUnsupportedReason = null; this.myUnsupportedReason = null;
this.myMatcherShortName = theMatcherShortName;
} }
public SubscriptionMatchResult(String theUnsupportedParameter) { public SubscriptionMatchResult(String theUnsupportedParameter, String theMatcherShortName) {
this.myMatch = false; this.myMatch = false;
this.mySupported = false; this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter; this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = "Parameter not supported"; 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.myMatch = false;
this.mySupported = false; this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter; this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = theUnsupportedReason; this.myUnsupportedReason = theUnsupportedReason;
this.myMatcherShortName = theMatcherShortName;
} }
public boolean supported() { public boolean supported() {
@ -62,4 +62,12 @@ public class SubscriptionMatchResult {
public String getUnsupportedReason() { public String getUnsupportedReason() {
return "Parameter: <" + myUnsupportedParameter + "> Reason: " + myUnsupportedReason; 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.ActiveSubscription;
import ca.uhn.fhir.jpa.subscription.module.cache.SubscriptionRegistry; 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.ISubscriptionMatcher;
import ca.uhn.fhir.jpa.subscription.module.matcher.SubscriptionMatchResult;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -113,11 +114,12 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
continue; continue;
} }
if (!mySubscriptionMatcher.match(nextCriteriaString, theMsg).matched()) { SubscriptionMatchResult matchResult = mySubscriptionMatcher.match(nextCriteriaString, theMsg);
if (!matchResult.matched()) {
continue; 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(); ResourceDeliveryMessage deliveryMsg = new ResourceDeliveryMessage();
deliveryMsg.setPayload(myFhirContext, theMsg.getNewPayload(myFhirContext)); deliveryMsg.setPayload(myFhirContext, theMsg.getNewPayload(myFhirContext));
@ -130,7 +132,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler {
if (deliveryChannel != null) { if (deliveryChannel != null) {
deliveryChannel.send(wrappedMsg); deliveryChannel.send(wrappedMsg);
} else { } 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; package ca.uhn.fhir.rest.api.server;
import ca.uhn.fhir.context.api.BundleInclusionRule; 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.IPagingProvider;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
@ -30,4 +31,7 @@ public interface IRestfulServer<T extends RequestDetails> extends IRestfulServer
BundleInclusionRule getBundleInclusionRule(); BundleInclusionRule getBundleInclusionRule();
void setDefaultPreferReturn(PreferReturnEnum theDefaultPreferReturn);
PreferReturnEnum getDefaultPreferReturn();
} }

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -31,10 +31,7 @@ import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.Destroy; import ca.uhn.fhir.rest.annotation.Destroy;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize; import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.*;
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.server.IFhirVersionServer; import ca.uhn.fhir.rest.api.server.IFhirVersionServer;
import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.ParseAction; 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.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.UnavailableException; 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"; 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 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; 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<IServerInterceptor> myInterceptors = new ArrayList<>();
private final List<Object> myPlainProviders = new ArrayList<>(); private final List<Object> myPlainProviders = new ArrayList<>();
private final List<IResourceProvider> myResourceProviders = 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 boolean myUseBrowserFriendlyContentTypes;
private ITenantIdentificationStrategy myTenantIdentificationStrategy; private ITenantIdentificationStrategy myTenantIdentificationStrategy;
private Date myConformanceDate; 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 * 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; String completeUrl;
Map<String, String[]> params = null; Map<String, String[]> params = null;
if (StringUtils.isNotBlank(theRequest.getQueryString())) { if (isNotBlank(theRequest.getQueryString())) {
completeUrl = requestUrl + "?" + theRequest.getQueryString(); completeUrl = requestUrl + "?" + theRequest.getQueryString();
/* /*
* By default, we manually parse the request params (the URL params, or the body for * 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()); 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) * Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20)
*/ */

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.method;
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -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.ConfigurationException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.*;
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.server.IRestfulResponse; import ca.uhn.fhir.rest.api.server.IRestfulResponse;
import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails; 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.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 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> { abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<MethodOutcome> {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class); static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseOutcomeReturningMethodBinding.class);
@ -198,19 +191,24 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
} }
if (allowPrefer) { if (allowPrefer) {
outcome = resource;
String prefer = theRequest.getHeader(Constants.HEADER_PREFER); String prefer = theRequest.getHeader(Constants.HEADER_PREFER);
PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer); PreferReturnEnum preferReturn = RestfulServerUtils.parsePreferHeader(prefer);
if (preferReturn != null) { if (preferReturn == null) {
if (preferReturn == PreferReturnEnum.MINIMAL) { preferReturn = theServer.getDefaultPreferReturn();
}
switch (preferReturn) {
case REPRESENTATION:
outcome = resource;
break;
case MINIMAL:
outcome = null; outcome = null;
} break;
else { case OPERATION_OUTCOME:
if (preferReturn == PreferReturnEnum.OPERATION_OUTCOME) { outcome = originalOutcome;
outcome = originalOutcome; break;
} }
}
}
} }
ResponseDetails responseDetails = new ResponseDetails(); ResponseDetails responseDetails = new ResponseDetails();

View File

@ -1,19 +1,16 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.containsString; import ca.uhn.fhir.context.FhirContext;
import static org.hamcrest.Matchers.not; import ca.uhn.fhir.rest.annotation.*;
import static org.hamcrest.Matchers.stringContainsInOrder; import ca.uhn.fhir.rest.api.Constants;
import static org.junit.Assert.assertEquals; import ca.uhn.fhir.rest.api.MethodOutcome;
import static org.junit.Assert.assertNull; import ca.uhn.fhir.rest.api.PreferReturnEnum;
import static org.junit.Assert.assertThat; import ca.uhn.fhir.rest.client.MyPatientWithExtensions;
import ca.uhn.fhir.util.PortUtil;
import java.nio.charset.StandardCharsets; import ca.uhn.fhir.util.TestUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; 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.HttpGet;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType; 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.server.Server;
import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; 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.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.DateType; import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass; import org.junit.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext; import java.nio.charset.StandardCharsets;
import ca.uhn.fhir.rest.annotation.Create; import java.util.ArrayList;
import ca.uhn.fhir.rest.annotation.IdParam; import java.util.List;
import ca.uhn.fhir.rest.annotation.Read; import java.util.concurrent.TimeUnit;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search; import static org.hamcrest.Matchers.*;
import ca.uhn.fhir.rest.api.Constants; import static org.junit.Assert.*;
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;
public class CreateR4Test { public class CreateR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CreateR4Test.class); 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 CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4(); private static FhirContext ourCtx = FhirContext.forR4();
private static int ourPort; private static int ourPort;
private static Server ourServer; private static Server ourServer;
private static RestfulServer ourServlet;
@After
public void after() {
ourServlet.setDefaultPreferReturn(RestfulServer.DEFAULT_PREFER_RETURN);
}
@Before @Before
public void before() { public void before() {
@ -122,12 +117,12 @@ public class CreateR4Test {
assertThat(responseContent, containsString("DIAG")); assertThat(responseContent, containsString("DIAG"));
} }
@Test @Test
public void testCreateReturnsRepresentation() throws Exception { public void testCreateReturnsRepresentation() throws Exception {
ourReturnOo = new OperationOutcome().addIssue(new OperationOutcomeIssueComponent().setDiagnostics("DIAG")); ourReturnOo = new OperationOutcome().addIssue(new OperationOutcomeIssueComponent().setDiagnostics("DIAG"));
String expectedResponseContent = "{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\"},\"gender\":\"male\"}"; String expectedResponseContent = "{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\"},\"gender\":\"male\"}";
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient"); HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.setEntity(new StringEntity("{\"resourceType\":\"Patient\", \"gender\":\"male\"}", ContentType.parse("application/fhir+json; charset=utf-8"))); httpPost.setEntity(new StringEntity("{\"resourceType\":\"Patient\", \"gender\":\"male\"}", ContentType.parse("application/fhir+json; charset=utf-8")));
HttpResponse status = ourClient.execute(httpPost); HttpResponse status = ourClient.execute(httpPost);
@ -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 @Test
public void testSearch() throws Exception { public void testSearch() throws Exception {
@ -248,36 +316,6 @@ public class CreateR4Test {
assertThat(responseContent, not(containsString("http://hl7.org/fhir/"))); 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 { public static class PatientProviderRead implements IResourceProvider {
@Read() @Read()
@ -299,12 +337,13 @@ public class CreateR4Test {
public Class<Patient> getResourceType() { public Class<Patient> getResourceType() {
return Patient.class; return Patient.class;
} }
@Create() @Create()
public MethodOutcome create(@ResourceParam Patient theIdParam) { public MethodOutcome create(@ResourceParam Patient thePatient) {
assertNull(theIdParam.getIdElement().getIdPart()); assertNull(thePatient.getIdElement().getIdPart());
theIdParam.setId("1"); thePatient.setId("1");
theIdParam.getMeta().setVersionId("1"); thePatient.getMeta().setVersionId("1");
return new MethodOutcome(new IdType("Patient", "1"), true).setOperationOutcome(ourReturnOo).setResource(theIdParam); 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

@ -34,21 +34,21 @@
subscription matching will query the database as before. subscription matching will query the database as before.
</action> </action>
<action type="change"> <action type="change">
Removed BaseSubscriptionInterceptor and all its subclasses (RestHook, EMail, WebSocket). These are replaced Removed BaseSubscriptionInterceptor and all its subclasses (RestHook, EMail, WebSocket). These are replaced
by two new interceptors: SubscriptionActivatingInterceptor that is responsible for activating subscriptions by two new interceptors: SubscriptionActivatingInterceptor that is responsible for activating subscriptions
and SubscriptionMatchingInterceptor that is responsible for matching incoming resources against activated and SubscriptionMatchingInterceptor that is responsible for matching incoming resources against activated
subscriptions. Call DaoConfig.addSupportedSubscriptionType(type) to configure which subscription types subscriptions. Call DaoConfig.addSupportedSubscriptionType(type) to configure which subscription types
are supported in your environment. The helper method SubscriptionInterceptorLoader.registerInterceptors() are supported in your environment. The helper method SubscriptionInterceptorLoader.registerInterceptors()
will check if any subscription types are supported, and if so then load active subscriptions into the will check if any subscription types are supported, and if so then load active subscriptions into the
SubscriptionRegistry and then register both the activating and matching interceptors. SubscriptionRegistry and then register both the activating and matching interceptors.
See https://github.com/jamesagnew/hapi-fhir/wiki/Proposed-Subscription-Design-Change for more See https://github.com/jamesagnew/hapi-fhir/wiki/Proposed-Subscription-Design-Change for more
details. details.
</action> </action>
<action type="change"> <action type="change">
Added support for matching subscriptions in a separate server from the REST Server. To do this, run the Added support for matching subscriptions in a separate server from the REST Server. To do this, run the
SubscriptionActivatingInterceptor on the REST server and the SubscriptionMatchingInterceptor in the SubscriptionActivatingInterceptor on the REST server and the SubscriptionMatchingInterceptor in the
standalone server. Classes required to support running a standalone subscription server are in the standalone server. Classes required to support running a standalone subscription server are in the
ca.uhn.fhir.jpa.subscription.module.standalone package. These classes are excluded by default from ca.uhn.fhir.jpa.subscription.module.standalone package. These classes are excluded by default from
the JPA ApplicationContext (that package is explicitly filtered out in the BaseConfig.java @ComponentScan). the JPA ApplicationContext (that package is explicitly filtered out in the BaseConfig.java @ComponentScan).
</action> </action>
<action type="add"> <action type="add">
@ -125,7 +125,7 @@
</action> </action>
<action type="add"> <action type="add">
JPA Migrator tool enhancements: JPA Migrator tool enhancements:
An invalid SQL syntax issue has been fixed when running the CLI JPA Migrator tool against An invalid SQL syntax issue has been fixed when running the CLI JPA Migrator tool against
Oracle or SQL Server. In addition, when using the "Dry Run" option, all generated SQL Oracle or SQL Server. In addition, when using the "Dry Run" option, all generated SQL
statements will be logged at the end of the run. Also, a case sensitivity issue when running against statements will be logged at the end of the run. Also, a case sensitivity issue when running against
some Postgres databases has been corrected. some Postgres databases has been corrected.
@ -146,7 +146,7 @@
</action> </action>
<action type="add"> <action type="add">
The resource reindexer can now detect when a resource's current version no longer The resource reindexer can now detect when a resource's current version no longer
exists in the database (e.g. because it was manually expunged), and can automatically exists in the database (e.g. because it was manually expunged), and can automatically
adjust the most recent version to adjust the most recent version to
account for this. account for this.
</action> </action>
@ -171,20 +171,21 @@
the correct content type cia the Accept header. the correct content type cia the Accept header.
</action> </action>
<action type="add" issue="917"> <action type="add" issue="917">
A new configuration item has been added to the FhirInstanceValidator that A new configuration item has been added to the FhirInstanceValidator that
allows you to specify additional "known extension domains", meaning allows you to specify additional "known extension domains", meaning
domains in which the validator will not complain about when it domains in which the validator will not complain about when it
encounters new extensions. Thanks to Heinz-Dieter Conradi for the encounters new extensions. Thanks to Heinz-Dieter Conradi for the
pull request! pull request!
</action> </action>
<action type="fix"> <action type="fix">
Under some circumstances, when a custom search parameter was added to the JPA server Under some circumstances, when a custom search parameter was added to the JPA server
resources could start reindexing before the new search parameter had been saved, meaning that resources could start reindexing before the new search parameter had been saved, meaning that
it was not applied to all resources. This has been corrected. it was not applied to all resources. This has been corrected.
</action> </action>
<action type="change"> <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
are no longer maintained. The README.md points users to a starter project they should use for examples. projects
are no longer maintained. The README.md points users to a starter project they should use for examples.
</action> </action>
<action type="change"> <action type="change">
Replaced use of BeanFactory with custom factory classes that Spring @Lookup the @Scope("prototype") beans Replaced use of BeanFactory with custom factory classes that Spring @Lookup the @Scope("prototype") beans
@ -194,27 +195,27 @@
Moved e-mail from address configuration from EmailInterceptor (which doesn't exist any more) to DaoConfig. Moved e-mail from address configuration from EmailInterceptor (which doesn't exist any more) to DaoConfig.
</action> </action>
<action type="add"> <action type="add">
Added 3 interfaces for services required by the standalone subscription server. The standalone subscription Added 3 interfaces for services required by the standalone subscription server. The standalone subscription
server doesn't have access to a database and so needs to get its resources using a FhirClient. Thus server doesn't have access to a database and so needs to get its resources using a FhirClient. Thus
for each of these interfaces, there are two implementations: a Dao implementaiton and a FhirClient for each of these interfaces, there are two implementations: a Dao implementaiton and a FhirClient
implementation. The interfaces thus introduced are ISubscriptionProvider (used to load subscriptions implementation. The interfaces thus introduced are ISubscriptionProvider (used to load subscriptions
into the SubscriptionRegistry), the IResourceProvider (used to get the latest version of a resource into the SubscriptionRegistry), the IResourceProvider (used to get the latest version of a resource
if the "get latest version" flag is set on the subscription) and ISearchParamProvider used to load if the "get latest version" flag is set on the subscription) and ISearchParamProvider used to load
custom search parameters. custom search parameters.
</action> </action>
<action type="change"> <action type="change">
Separated active subscription cache from the interceptors into a new Spring component called the Separated active subscription cache from the interceptors into a new Spring component called the
SubscriptionRegistry. This component maintains a cache of ActiveSubscriptions. An ActiveSubscription SubscriptionRegistry. This component maintains a cache of ActiveSubscriptions. An ActiveSubscription
contains the subscription, it's delivery channel, and a list of delivery handlers. contains the subscription, it's delivery channel, and a list of delivery handlers.
</action> </action>
<action type="change"> <action type="change">
Introduced a new Spring factory interface ISubscribableChannelFactory that is used to create delivery Introduced a new Spring factory interface ISubscribableChannelFactory that is used to create delivery
channels and handlers. By default, HAPI FHIR ships with a LinkedBlockingQueue implementation of the channels and handlers. By default, HAPI FHIR ships with a LinkedBlockingQueue implementation of the
delivery channel factory. If a different type of channel factory is required (e.g. JMS or Kafka), add it delivery channel factory. If a different type of channel factory is required (e.g. JMS or Kafka), add it
to your application context and mark it as @Primary. to your application context and mark it as @Primary.
</action> </action>
<action type="fix" issue="980"> <action type="fix" issue="980">
When using the HL7.org DSTU2 structures, a QuestionnaireResponse with a When using the HL7.org DSTU2 structures, a QuestionnaireResponse with a
value of type reference would fail to parse. Thanks to David Gileadi for value of type reference would fail to parse. Thanks to David Gileadi for
the pull request! the pull request!
</action> </action>
@ -231,6 +232,17 @@
JPA Subscription deliveries did not always include the accurate versionId if the Subscription 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. module was configured to use an external queuing engine. This has been corrected.
</action> </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>
<release version="3.6.0" date="2018-11-12" description="Food"> <release version="3.6.0" date="2018-11-12" description="Food">
<action type="add"> <action type="add">