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;
@ -25,9 +26,9 @@ import java.util.*;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -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

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

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

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

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.method;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -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) {
outcome = originalOutcome;
}
}
}
break;
case OPERATION_OUTCOME:
outcome = originalOutcome;
break;
}
}
ResponseDetails responseDetails = new ResponseDetails();

View File

@ -1,403 +1,400 @@
package org.hl7.fhir.r4.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.Tuple;
import org.hl7.fhir.r4.model.ExpressionNode;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.TypeDetails;
import org.hl7.fhir.r4.utils.FHIRPathEngine.ExpressionNodeWithOffset;
import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.utilities.Utilities;
import javafx.scene.Parent;
public class LiquidEngine implements IEvaluationContext {
public interface ILiquidEngineIcludeResolver {
public String fetchInclude(LiquidEngine engine, String name);
}
private IEvaluationContext externalHostServices;
private FHIRPathEngine engine;
private ILiquidEngineIcludeResolver includeResolver;
private class LiquidEngineContext {
private Object externalContext;
private Map<String, Base> vars = new HashMap<>();
public LiquidEngineContext(Object externalContext) {
super();
this.externalContext = externalContext;
}
public LiquidEngineContext(LiquidEngineContext existing) {
super();
externalContext = existing.externalContext;
vars.putAll(existing.vars);
}
}
public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) {
super();
this.externalHostServices = hostServices;
engine = new FHIRPathEngine(context);
engine.setHostServices(this);
}
public ILiquidEngineIcludeResolver getIncludeResolver() {
return includeResolver;
}
public void setIncludeResolver(ILiquidEngineIcludeResolver includeResolver) {
this.includeResolver = includeResolver;
}
public LiquidDocument parse(String source, String sourceName) throws Exception {
return new LiquidParser(source).parse(sourceName);
}
public String evaluate(LiquidDocument document, Resource resource, Object appContext) throws FHIRException {
StringBuilder b = new StringBuilder();
LiquidEngineContext ctxt = new LiquidEngineContext(appContext);
for (LiquidNode n : document.body) {
n.evaluate(b, resource, ctxt);
}
return b.toString();
}
private abstract class LiquidNode {
protected void closeUp() {}
public abstract void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException;
}
private class LiquidConstant extends LiquidNode {
private String constant;
private StringBuilder b = new StringBuilder();
@Override
protected void closeUp() {
constant = b.toString();
b = null;
}
public void addChar(char ch) {
b.append(ch);
}
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) {
b.append(constant);
}
}
private class LiquidStatement extends LiquidNode {
private String statement;
private ExpressionNode compiled;
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null)
compiled = engine.parse(statement);
b.append(engine.evaluateToString(ctxt, resource, resource, compiled));
}
}
private class LiquidIf extends LiquidNode {
private String condition;
private ExpressionNode compiled;
private List<LiquidNode> thenBody = new ArrayList<>();
private List<LiquidNode> elseBody = new ArrayList<>();
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null)
compiled = engine.parse(condition);
boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, compiled);
List<LiquidNode> list = ok ? thenBody : elseBody;
for (LiquidNode n : list) {
n.evaluate(b, resource, ctxt);
}
}
}
private class LiquidLoop extends LiquidNode {
private String varName;
private String condition;
private ExpressionNode compiled;
private List<LiquidNode> body = new ArrayList<>();
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null)
compiled = engine.parse(condition);
List<Base> list = engine.evaluate(ctxt, resource, resource, compiled);
LiquidEngineContext lctxt = new LiquidEngineContext(ctxt);
for (Base o : list) {
lctxt.vars.put(varName, o);
for (LiquidNode n : body) {
n.evaluate(b, resource, lctxt);
}
}
}
}
private class LiquidInclude extends LiquidNode {
private String page;
private Map<String, ExpressionNode> params = new HashMap<>();
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
String src = includeResolver.fetchInclude(LiquidEngine.this, page);
LiquidParser parser = new LiquidParser(src);
LiquidDocument doc = parser.parse(page);
LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext);
Tuple incl = new Tuple();
nctxt.vars.put("include", incl);
for (String s : params.keySet()) {
incl.addProperty(s, engine.evaluate(ctxt, resource, resource, params.get(s)));
}
for (LiquidNode n : doc.body) {
n.evaluate(b, resource, nctxt);
}
}
}
public static class LiquidDocument {
private List<LiquidNode> body = new ArrayList<>();
}
private class LiquidParser {
private String source;
private int cursor;
private String name;
public LiquidParser(String source) {
this.source = source;
cursor = 0;
}
private char next1() {
if (cursor >= source.length())
return 0;
else
return source.charAt(cursor);
}
private char next2() {
if (cursor >= source.length()-1)
return 0;
else
return source.charAt(cursor+1);
}
private char grab() {
cursor++;
return source.charAt(cursor-1);
}
public LiquidDocument parse(String name) throws FHIRException {
this.name = name;
LiquidDocument doc = new LiquidDocument();
parseList(doc.body, new String[0]);
return doc;
}
private String parseList(List<LiquidNode> list, String[] terminators) throws FHIRException {
String close = null;
while (cursor < source.length()) {
if (next1() == '{' && (next2() == '%' || next2() == '{' )) {
if (next2() == '%') {
String cnt = parseTag('%');
if (Utilities.existsInList(cnt, terminators)) {
close = cnt;
break;
} else if (cnt.startsWith("if "))
list.add(parseIf(cnt));
else if (cnt.startsWith("loop "))
list.add(parseLoop(cnt.substring(4).trim()));
else if (cnt.startsWith("include "))
list.add(parseInclude(cnt.substring(7).trim()));
else
throw new FHIRException("Script "+name+": Script "+name+": Unknown flow control statement "+cnt);
} else { // next2() == '{'
list.add(parseStatement());
}
} else {
if (list.size() == 0 || !(list.get(list.size()-1) instanceof LiquidConstant))
list.add(new LiquidConstant());
((LiquidConstant) list.get(list.size()-1)).addChar(grab());
}
}
for (LiquidNode n : list)
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);
return close;
}
private LiquidNode parseIf(String cnt) throws FHIRException {
LiquidIf res = new LiquidIf();
res.condition = cnt.substring(3).trim();
String term = parseList(res.thenBody, new String[] { "else", "endif"} );
if ("else".equals(term))
term = parseList(res.elseBody, new String[] { "endif"} );
return res;
}
private LiquidNode parseInclude(String cnt) throws FHIRException {
int i = 1;
while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
i++;
if (i == cnt.length() || i == 0)
throw new FHIRException("Script "+name+": Error reading include: "+cnt);
LiquidInclude res = new LiquidInclude();
res.page = cnt.substring(0, i);
while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
i++;
while (i < cnt.length()) {
int j = i;
while (i < cnt.length() && cnt.charAt(i) != '=')
i++;
if (i >= cnt.length() || j == i)
throw new FHIRException("Script "+name+": Error reading include: "+cnt);
String n = cnt.substring(j, i);
if (res.params.containsKey(n))
throw new FHIRException("Script "+name+": Error reading include: "+cnt);
i++;
ExpressionNodeWithOffset t = engine.parsePartial(cnt, i);
i = t.getOffset();
res.params.put(n, t.getNode());
while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
i++;
}
return res;
}
private LiquidNode parseLoop(String cnt) throws FHIRException {
int i = 0;
while (!Character.isWhitespace(cnt.charAt(i)))
i++;
LiquidLoop res = new LiquidLoop();
res.varName = cnt.substring(0, i);
while (Character.isWhitespace(cnt.charAt(i)))
i++;
int j = i;
while (!Character.isWhitespace(cnt.charAt(i)))
i++;
if (!"in".equals(cnt.substring(j, i)))
throw new FHIRException("Script "+name+": Script "+name+": Error reading loop: "+cnt);
res.condition = cnt.substring(i).trim();
parseList(res.body, new String[] { "endloop"} );
return res;
}
private String parseTag(char ch) throws FHIRException {
grab();
grab();
StringBuilder b = new StringBuilder();
while (cursor < source.length() && !(next1() == '%' && next2() == '}')) {
b.append(grab());
}
if (!(next1() == '%' && next2() == '}'))
throw new FHIRException("Script "+name+": Unterminated Liquid statement {% "+b.toString());
grab();
grab();
return b.toString().trim();
}
private LiquidStatement parseStatement() throws FHIRException {
grab();
grab();
StringBuilder b = new StringBuilder();
while (cursor < source.length() && !(next1() == '}' && next2() == '}')) {
b.append(grab());
}
if (!(next1() == '}' && next2() == '}'))
throw new FHIRException("Script "+name+": Unterminated Liquid statement {{ "+b.toString());
grab();
grab();
LiquidStatement res = new LiquidStatement();
res.statement = b.toString().trim();
return res;
}
}
@Override
public Base resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
if (ctxt.vars.containsKey(name))
return ctxt.vars.get(name);
if (externalHostServices == null)
return null;
return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext);
}
@Override
public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return externalHostServices.resolveConstantType(ctxt.externalContext, name);
}
@Override
public boolean log(String argument, List<Base> focus) {
if (externalHostServices == null)
return false;
return externalHostServices.log(argument, focus);
}
@Override
public FunctionDetails resolveFunction(String functionName) {
if (externalHostServices == null)
return null;
return externalHostServices.resolveFunction(functionName);
}
@Override
public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return externalHostServices.checkFunction(ctxt.externalContext, functionName, parameters);
}
@Override
public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return externalHostServices.executeFunction(ctxt.externalContext, functionName, parameters);
}
@Override
public Base resolveReference(Object appContext, String url) throws FHIRException {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return resolveReference(ctxt.externalContext, url);
}
@Override
public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
if (externalHostServices == null)
return false;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return conformsToProfile(ctxt.externalContext, item, url);
}
}
package org.hl7.fhir.r4.utils;
import java.util.*;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException;
import org.hl7.fhir.r4.context.IWorkerContext;
import org.hl7.fhir.r4.model.Base;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.Tuple;
import org.hl7.fhir.r4.model.ExpressionNode;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.TypeDetails;
import org.hl7.fhir.r4.utils.FHIRPathEngine.ExpressionNodeWithOffset;
import org.hl7.fhir.r4.utils.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.utilities.Utilities;
import javafx.scene.Parent;
public class LiquidEngine implements IEvaluationContext {
public interface ILiquidEngineIcludeResolver {
public String fetchInclude(LiquidEngine engine, String name);
}
private IEvaluationContext externalHostServices;
private FHIRPathEngine engine;
private ILiquidEngineIcludeResolver includeResolver;
private class LiquidEngineContext {
private Object externalContext;
private Map<String, Base> vars = new HashMap<>();
public LiquidEngineContext(Object externalContext) {
super();
this.externalContext = externalContext;
}
public LiquidEngineContext(LiquidEngineContext existing) {
super();
externalContext = existing.externalContext;
vars.putAll(existing.vars);
}
}
public LiquidEngine(IWorkerContext context, IEvaluationContext hostServices) {
super();
this.externalHostServices = hostServices;
engine = new FHIRPathEngine(context);
engine.setHostServices(this);
}
public ILiquidEngineIcludeResolver getIncludeResolver() {
return includeResolver;
}
public void setIncludeResolver(ILiquidEngineIcludeResolver includeResolver) {
this.includeResolver = includeResolver;
}
public LiquidDocument parse(String source, String sourceName) throws Exception {
return new LiquidParser(source).parse(sourceName);
}
public String evaluate(LiquidDocument document, Resource resource, Object appContext) throws FHIRException {
StringBuilder b = new StringBuilder();
LiquidEngineContext ctxt = new LiquidEngineContext(appContext);
for (LiquidNode n : document.body) {
n.evaluate(b, resource, ctxt);
}
return b.toString();
}
private abstract class LiquidNode {
protected void closeUp() {}
public abstract void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException;
}
private class LiquidConstant extends LiquidNode {
private String constant;
private StringBuilder b = new StringBuilder();
@Override
protected void closeUp() {
constant = b.toString();
b = null;
}
public void addChar(char ch) {
b.append(ch);
}
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) {
b.append(constant);
}
}
private class LiquidStatement extends LiquidNode {
private String statement;
private ExpressionNode compiled;
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null)
compiled = engine.parse(statement);
b.append(engine.evaluateToString(ctxt, resource, resource, compiled));
}
}
private class LiquidIf extends LiquidNode {
private String condition;
private ExpressionNode compiled;
private List<LiquidNode> thenBody = new ArrayList<>();
private List<LiquidNode> elseBody = new ArrayList<>();
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null)
compiled = engine.parse(condition);
boolean ok = engine.evaluateToBoolean(ctxt, resource, resource, compiled);
List<LiquidNode> list = ok ? thenBody : elseBody;
for (LiquidNode n : list) {
n.evaluate(b, resource, ctxt);
}
}
}
private class LiquidLoop extends LiquidNode {
private String varName;
private String condition;
private ExpressionNode compiled;
private List<LiquidNode> body = new ArrayList<>();
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
if (compiled == null)
compiled = engine.parse(condition);
List<Base> list = engine.evaluate(ctxt, resource, resource, compiled);
LiquidEngineContext lctxt = new LiquidEngineContext(ctxt);
for (Base o : list) {
lctxt.vars.put(varName, o);
for (LiquidNode n : body) {
n.evaluate(b, resource, lctxt);
}
}
}
}
private class LiquidInclude extends LiquidNode {
private String page;
private Map<String, ExpressionNode> params = new HashMap<>();
@Override
public void evaluate(StringBuilder b, Resource resource, LiquidEngineContext ctxt) throws FHIRException {
String src = includeResolver.fetchInclude(LiquidEngine.this, page);
LiquidParser parser = new LiquidParser(src);
LiquidDocument doc = parser.parse(page);
LiquidEngineContext nctxt = new LiquidEngineContext(ctxt.externalContext);
Tuple incl = new Tuple();
nctxt.vars.put("include", incl);
for (String s : params.keySet()) {
incl.addProperty(s, engine.evaluate(ctxt, resource, resource, params.get(s)));
}
for (LiquidNode n : doc.body) {
n.evaluate(b, resource, nctxt);
}
}
}
public static class LiquidDocument {
private List<LiquidNode> body = new ArrayList<>();
}
private class LiquidParser {
private String source;
private int cursor;
private String name;
public LiquidParser(String source) {
this.source = source;
cursor = 0;
}
private char next1() {
if (cursor >= source.length())
return 0;
else
return source.charAt(cursor);
}
private char next2() {
if (cursor >= source.length()-1)
return 0;
else
return source.charAt(cursor+1);
}
private char grab() {
cursor++;
return source.charAt(cursor-1);
}
public LiquidDocument parse(String name) throws FHIRException {
this.name = name;
LiquidDocument doc = new LiquidDocument();
parseList(doc.body, new String[0]);
return doc;
}
private String parseList(List<LiquidNode> list, String[] terminators) throws FHIRException {
String close = null;
while (cursor < source.length()) {
if (next1() == '{' && (next2() == '%' || next2() == '{' )) {
if (next2() == '%') {
String cnt = parseTag('%');
if (Utilities.existsInList(cnt, terminators)) {
close = cnt;
break;
} else if (cnt.startsWith("if "))
list.add(parseIf(cnt));
else if (cnt.startsWith("loop "))
list.add(parseLoop(cnt.substring(4).trim()));
else if (cnt.startsWith("include "))
list.add(parseInclude(cnt.substring(7).trim()));
else
throw new FHIRException("Script "+name+": Script "+name+": Unknown flow control statement "+cnt);
} else { // next2() == '{'
list.add(parseStatement());
}
} else {
if (list.size() == 0 || !(list.get(list.size()-1) instanceof LiquidConstant))
list.add(new LiquidConstant());
((LiquidConstant) list.get(list.size()-1)).addChar(grab());
}
}
for (LiquidNode n : list)
n.closeUp();
if (terminators.length > 0)
if (!Utilities.existsInList(close, terminators))
throw new FHIRException("Script "+name+": Script "+name+": Found end of script looking for "+ Arrays.asList(terminators));
return close;
}
private LiquidNode parseIf(String cnt) throws FHIRException {
LiquidIf res = new LiquidIf();
res.condition = cnt.substring(3).trim();
String term = parseList(res.thenBody, new String[] { "else", "endif"} );
if ("else".equals(term))
term = parseList(res.elseBody, new String[] { "endif"} );
return res;
}
private LiquidNode parseInclude(String cnt) throws FHIRException {
int i = 1;
while (i < cnt.length() && !Character.isWhitespace(cnt.charAt(i)))
i++;
if (i == cnt.length() || i == 0)
throw new FHIRException("Script "+name+": Error reading include: "+cnt);
LiquidInclude res = new LiquidInclude();
res.page = cnt.substring(0, i);
while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
i++;
while (i < cnt.length()) {
int j = i;
while (i < cnt.length() && cnt.charAt(i) != '=')
i++;
if (i >= cnt.length() || j == i)
throw new FHIRException("Script "+name+": Error reading include: "+cnt);
String n = cnt.substring(j, i);
if (res.params.containsKey(n))
throw new FHIRException("Script "+name+": Error reading include: "+cnt);
i++;
ExpressionNodeWithOffset t = engine.parsePartial(cnt, i);
i = t.getOffset();
res.params.put(n, t.getNode());
while (i < cnt.length() && Character.isWhitespace(cnt.charAt(i)))
i++;
}
return res;
}
private LiquidNode parseLoop(String cnt) throws FHIRException {
int i = 0;
while (!Character.isWhitespace(cnt.charAt(i)))
i++;
LiquidLoop res = new LiquidLoop();
res.varName = cnt.substring(0, i);
while (Character.isWhitespace(cnt.charAt(i)))
i++;
int j = i;
while (!Character.isWhitespace(cnt.charAt(i)))
i++;
if (!"in".equals(cnt.substring(j, i)))
throw new FHIRException("Script "+name+": Script "+name+": Error reading loop: "+cnt);
res.condition = cnt.substring(i).trim();
parseList(res.body, new String[] { "endloop"} );
return res;
}
private String parseTag(char ch) throws FHIRException {
grab();
grab();
StringBuilder b = new StringBuilder();
while (cursor < source.length() && !(next1() == '%' && next2() == '}')) {
b.append(grab());
}
if (!(next1() == '%' && next2() == '}'))
throw new FHIRException("Script "+name+": Unterminated Liquid statement {% "+b.toString());
grab();
grab();
return b.toString().trim();
}
private LiquidStatement parseStatement() throws FHIRException {
grab();
grab();
StringBuilder b = new StringBuilder();
while (cursor < source.length() && !(next1() == '}' && next2() == '}')) {
b.append(grab());
}
if (!(next1() == '}' && next2() == '}'))
throw new FHIRException("Script "+name+": Unterminated Liquid statement {{ "+b.toString());
grab();
grab();
LiquidStatement res = new LiquidStatement();
res.statement = b.toString().trim();
return res;
}
}
@Override
public Base resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
if (ctxt.vars.containsKey(name))
return ctxt.vars.get(name);
if (externalHostServices == null)
return null;
return externalHostServices.resolveConstant(ctxt.externalContext, name, beforeContext);
}
@Override
public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return externalHostServices.resolveConstantType(ctxt.externalContext, name);
}
@Override
public boolean log(String argument, List<Base> focus) {
if (externalHostServices == null)
return false;
return externalHostServices.log(argument, focus);
}
@Override
public FunctionDetails resolveFunction(String functionName) {
if (externalHostServices == null)
return null;
return externalHostServices.resolveFunction(functionName);
}
@Override
public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return externalHostServices.checkFunction(ctxt.externalContext, functionName, parameters);
}
@Override
public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return externalHostServices.executeFunction(ctxt.externalContext, functionName, parameters);
}
@Override
public Base resolveReference(Object appContext, String url) throws FHIRException {
if (externalHostServices == null)
return null;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return resolveReference(ctxt.externalContext, url);
}
@Override
public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
if (externalHostServices == null)
return false;
LiquidEngineContext ctxt = (LiquidEngineContext) appContext;
return conformsToProfile(ctxt.externalContext, item, url);
}
}

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() {
@ -122,12 +117,12 @@ public class CreateR4Test {
assertThat(responseContent, containsString("DIAG"));
}
@Test
public void testCreateReturnsRepresentation() throws Exception {
ourReturnOo = new OperationOutcome().addIssue(new OperationOutcomeIssueComponent().setDiagnostics("DIAG"));
String expectedResponseContent = "{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\"},\"gender\":\"male\"}";
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.setEntity(new StringEntity("{\"resourceType\":\"Patient\", \"gender\":\"male\"}", ContentType.parse("application/fhir+json; charset=utf-8")));
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
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

@ -34,21 +34,21 @@
subscription matching will query the database as before.
</action>
<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
and SubscriptionMatchingInterceptor that is responsible for matching incoming resources against activated
subscriptions. Call DaoConfig.addSupportedSubscriptionType(type) to configure which subscription types
are supported in your environment. The helper method SubscriptionInterceptorLoader.registerInterceptors()
subscriptions. Call DaoConfig.addSupportedSubscriptionType(type) to configure which subscription types
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
SubscriptionRegistry and then register both the activating and matching interceptors.
See https://github.com/jamesagnew/hapi-fhir/wiki/Proposed-Subscription-Design-Change for more
details.
</action>
<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
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
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
the JPA ApplicationContext (that package is explicitly filtered out in the BaseConfig.java @ComponentScan).
</action>
<action type="add">
@ -125,7 +125,7 @@
</action>
<action type="add">
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
statements will be logged at the end of the run. Also, a case sensitivity issue when running against
some Postgres databases has been corrected.
@ -146,7 +146,7 @@
</action>
<action type="add">
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
account for this.
</action>
@ -171,20 +171,21 @@
the correct content type cia the Accept header.
</action>
<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
domains in which the validator will not complain about when it
encounters new extensions. Thanks to Heinz-Dieter Conradi for the
pull request!
</action>
<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
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
are no longer maintained. The README.md points users to a starter project they should use for examples.
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">
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.
</action>
<action type="add">
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
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
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
if the "get latest version" flag is set on the subscription) and ISearchParamProvider used to load
custom search parameters.
</action>
<action type="change">
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.
</action>
<action type="change">
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
delivery channel factory. If a different type of channel factory is required (e.g. JMS or Kafka), add it
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
to your application context and mark it as @Primary.
</action>
<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
the pull request!
</action>
@ -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">