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