Merge branch 'master' into fhirterser-getvalues-enhancements

This commit is contained in:
James Agnew 2018-10-29 09:55:34 -05:00 committed by GitHub
commit 4cd86596f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 4717 additions and 680 deletions

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.annotation;
* 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.
@ -20,15 +20,14 @@ package ca.uhn.fhir.rest.annotation;
* #L%
*/
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
/**
* RESTful method annotation used for a method which provides FHIR "operations".
*/
@ -37,10 +36,18 @@ import ca.uhn.fhir.model.valueset.BundleTypeEnum;
public @interface Operation {
/**
* The name of the operation, e.g. "<code>$everything</code>"
*
* This constant is a special return value for {@link #name()}. If this name is
* used, the given operation method will match all operation calls. This is
* generally not desirable, but can be useful if you have a server that should
* dynamically match any FHIR operations that are requested.
*/
String NAME_MATCH_ALL = "*";
/**
* The name of the operation, e.g. "<code>$everything</code>"
*
* <p>
* This may be specified with or without a leading
* This may be specified with or without a leading
* '$'. (If the leading '$' is omitted, it will be added internally by the API).
* </p>
*/
@ -61,10 +68,10 @@ public @interface Operation {
* (meaning roughly that it does not modify any data or state on the server)
* then this flag should be set to <code>true</code> (default is <code>false</code>).
* <p>
* One the server, setting this to <code>true</code> means that the
* One the server, setting this to <code>true</code> means that the
* server will allow the operation to be invoked using an <code>HTTP GET</code>
* (on top of the standard <code>HTTP POST</code>)
* </p>
* </p>
*/
boolean idempotent() default false;
@ -73,9 +80,9 @@ public @interface Operation {
* response to this operation.
*/
OperationParam[] returnParameters() default {};
/**
* If this operation returns a bundle, this parameter can be used to specify the
* If this operation returns a bundle, this parameter can be used to specify the
* bundle type to set in the bundle.
*/
BundleTypeEnum bundleType() default BundleTypeEnum.COLLECTION;

View File

@ -31,7 +31,7 @@ import java.util.List;
* @author james
*
*/
public class StringClientParam extends BaseClientParam implements IParam {
public class StringClientParam extends BaseClientParam implements IParam {
private final String myParamName;

View File

@ -107,7 +107,7 @@ public class TestUtil {
* environment
*/
public static void randomizeLocale() {
Locale[] availableLocales = {Locale.CANADA, Locale.GERMANY, Locale.TAIWAN};
Locale[] availableLocales = {Locale.CANADA, Locale.GERMANY, Locale.TAIWAN};
Locale.setDefault(availableLocales[(int) (Math.random() * availableLocales.length)]);
ourLog.info("Tests are running in locale: " + Locale.getDefault().getDisplayName());
if (Math.random() < 0.5) {

View File

@ -2086,6 +2086,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
*/
if (thePerformIndexing) {
calculateHashes(stringParams);
for (ResourceIndexedSearchParamString next : removeCommon(existingStringParams, stringParams)) {
next.setDaoConfig(myConfig);
myEntityManager.remove(next);
@ -2095,6 +2096,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
myEntityManager.persist(next);
}
calculateHashes(tokenParams);
for (ResourceIndexedSearchParamToken next : removeCommon(existingTokenParams, tokenParams)) {
myEntityManager.remove(next);
theEntity.getParamsToken().remove(next);
@ -2103,6 +2105,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
myEntityManager.persist(next);
}
calculateHashes(numberParams);
for (ResourceIndexedSearchParamNumber next : removeCommon(existingNumberParams, numberParams)) {
myEntityManager.remove(next);
theEntity.getParamsNumber().remove(next);
@ -2111,6 +2114,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
myEntityManager.persist(next);
}
calculateHashes(quantityParams);
for (ResourceIndexedSearchParamQuantity next : removeCommon(existingQuantityParams, quantityParams)) {
myEntityManager.remove(next);
theEntity.getParamsQuantity().remove(next);
@ -2120,6 +2124,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
// Store date SP's
calculateHashes(dateParams);
for (ResourceIndexedSearchParamDate next : removeCommon(existingDateParams, dateParams)) {
myEntityManager.remove(next);
theEntity.getParamsDate().remove(next);
@ -2129,6 +2134,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
// Store URI SP's
calculateHashes(uriParams);
for (ResourceIndexedSearchParamUri next : removeCommon(existingUriParams, uriParams)) {
myEntityManager.remove(next);
theEntity.getParamsUri().remove(next);
@ -2138,6 +2144,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
}
// Store Coords SP's
calculateHashes(coordsParams);
for (ResourceIndexedSearchParamCoords next : removeCommon(existingCoordsParams, coordsParams)) {
myEntityManager.remove(next);
theEntity.getParamsCoords().remove(next);
@ -2187,6 +2194,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return theEntity;
}
private void calculateHashes(Collection<? extends BaseResourceIndexedSearchParam> theStringParams) {
for (BaseResourceIndexedSearchParam next : theStringParams) {
next.calculateHashes();
}
}
protected ResourceTable updateEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable
entity, Date theDeletedTimestampOrNull, Date theUpdateTime) {
return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true);

View File

@ -155,6 +155,7 @@ public class DaoConfig {
private boolean myValidateSearchParameterExpressionsOnSave = true;
private List<Integer> mySearchPreFetchThresholds = Arrays.asList(500, 2000, -1);
private List<WarmCacheEntry> myWarmCacheEntries = new ArrayList<>();
private boolean myDisableHashBasedSearches;
/**
* Constructor
@ -1383,6 +1384,34 @@ public class DaoConfig {
return mySearchPreFetchThresholds;
}
/**
* If set to <code>true</code> (default is false) the server will not use
* hash based searches. These searches were introduced in HAPI FHIR 3.5.0
* and are the new default way of searching. However they require a very
* large data migration if an existing system has a large amount of data
* so this setting can be used to use the old search mechanism while data
* is migrated.
*
* @since 3.6.0
*/
public boolean getDisableHashBasedSearches() {
return myDisableHashBasedSearches;
}
/**
* If set to <code>true</code> (default is false) the server will not use
* hash based searches. These searches were introduced in HAPI FHIR 3.5.0
* and are the new default way of searching. However they require a very
* large data migration if an existing system has a large amount of data
* so this setting can be used to use the old search mechanism while data
* is migrated.
*
* @since 3.6.0
*/
public void setDisableHashBasedSearches(boolean theDisableHashBasedSearches) {
myDisableHashBasedSearches = theDisableHashBasedSearches;
}
public enum IndexEnabledEnum {
ENABLED,
DISABLED

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao;
* 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.
@ -55,6 +55,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
@ -94,6 +95,7 @@ public class SearchBuilder implements ISearchBuilder {
private static SearchParameterMap ourLastHandlerParamsForUnitTest;
private static String ourLastHandlerThreadForUnitTest;
private static boolean ourTrackHandlersForUnitTest;
private final boolean myDontUseHashesForSearch;
protected IResourceTagDao myResourceTagDao;
private IResourceSearchViewDao myResourceSearchViewDao;
private List<Long> myAlsoIncludePids;
@ -130,6 +132,7 @@ public class SearchBuilder implements ISearchBuilder {
myEntityManager = theEntityManager;
myFulltextSearchSvc = theFulltextSearchSvc;
myCallingDao = theDao;
myDontUseHashesForSearch = theDao.getConfig().getDisableHashBasedSearches();
myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao;
myForcedIdDao = theForcedIdDao;
myTerminologySvc = theTerminologySvc;
@ -304,6 +307,15 @@ public class SearchBuilder implements ISearchBuilder {
}
private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) {
// if (myDontUseHashesForSearch) {
// Join<ResourceTable, SearchParamPresent> paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT);
// Join<Object, Object> paramJoin = paramPresentJoin.join("mySearchParam", JoinType.LEFT);
//
// myPredicates.add(myBuilder.equal(paramJoin.get("myResourceName"), theResourceName));
// myPredicates.add(myBuilder.equal(paramJoin.get("myParamName"), theParamName));
// myPredicates.add(myBuilder.equal(paramPresentJoin.get("myPresent"), !theMissing));
// }
Join<ResourceTable, SearchParamPresent> paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT);
Expression<Long> hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class);
@ -841,10 +853,18 @@ public class SearchBuilder implements ISearchBuilder {
} else {
long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value);
Predicate hashPredicate = myBuilder.equal(join.get("myHashUri"), hashUri);
codePredicates.add(hashPredicate);
if (myDontUseHashesForSearch) {
Predicate predicate = myBuilder.equal(join.get("myUri").as(String.class), value);
codePredicates.add(predicate);
} else {
long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value);
Predicate hashPredicate = myBuilder.equal(join.get("myHashUri"), hashUri);
codePredicates.add(hashPredicate);
}
}
} else {
@ -868,6 +888,13 @@ public class SearchBuilder implements ISearchBuilder {
}
private Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From<?, ? extends BaseResourceIndexedSearchParam> theFrom, Predicate thePredicate) {
if (myDontUseHashesForSearch) {
Predicate resourceTypePredicate = myBuilder.equal(theFrom.get("myResourceType"), theResourceName);
Predicate paramNamePredicate = myBuilder.equal(theFrom.get("myParamName"), theParamName);
Predicate outerPredicate = myBuilder.and(resourceTypePredicate, paramNamePredicate, thePredicate);
return outerPredicate;
}
long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName);
Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity);
return myBuilder.and(hashIdentityPredicate, thePredicate);
@ -1079,6 +1106,37 @@ public class SearchBuilder implements ISearchBuilder {
throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass());
}
if (myDontUseHashesForSearch) {
Predicate system = null;
if (!isBlank(systemValue)) {
system = theBuilder.equal(theFrom.get("mySystem"), systemValue);
}
Predicate code = null;
if (!isBlank(unitsValue)) {
code = theBuilder.equal(theFrom.get("myUnits"), unitsValue);
}
cmpValue = ObjectUtils.defaultIfNull(cmpValue, ParamPrefixEnum.EQUAL);
final Expression<BigDecimal> path = theFrom.get("myValue");
String invalidMessageName = "invalidQuantityPrefix";
Predicate num = createPredicateNumeric(theResourceName, null, theFrom, theBuilder, theParam, cmpValue, valueValue, path, invalidMessageName);
Predicate singleCode;
if (system == null && code == null) {
singleCode = num;
} else if (system == null) {
singleCode = theBuilder.and(code, num);
} else if (code == null) {
singleCode = theBuilder.and(system, num);
} else {
singleCode = theBuilder.and(system, code, num);
}
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode);
}
Predicate hashPredicate;
if (!isBlank(systemValue) && !isBlank(unitsValue)) {
long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue);
@ -1130,6 +1188,31 @@ public class SearchBuilder implements ISearchBuilder {
+ ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm);
}
if (myDontUseHashesForSearch) {
String likeExpression = BaseHapiFhirDao.normalizeString(rawSearchTerm);
if (myCallingDao.getConfig().isAllowContainsSearches()) {
if (theParameter instanceof StringParam) {
if (((StringParam) theParameter).isContains()) {
likeExpression = createLeftAndRightMatchLikeExpression(likeExpression);
} else {
likeExpression = createLeftMatchLikeExpression(likeExpression);
}
} else {
likeExpression = createLeftMatchLikeExpression(likeExpression);
}
} else {
likeExpression = createLeftMatchLikeExpression(likeExpression);
}
Predicate singleCode = theBuilder.like(theFrom.get("myValueNormalized").as(String.class), likeExpression);
if (theParameter instanceof StringParam && ((StringParam) theParameter).isExact()) {
Predicate exactCode = theBuilder.equal(theFrom.get("myValueExact"), rawSearchTerm);
singleCode = theBuilder.and(singleCode, exactCode);
}
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode);
}
boolean exactMatch = theParameter instanceof StringParam && ((StringParam) theParameter).isExact();
if (exactMatch) {
@ -1234,6 +1317,92 @@ public class SearchBuilder implements ISearchBuilder {
return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false);
}
if (myDontUseHashesForSearch) {
ArrayList<Predicate> singleCodePredicates = new ArrayList<Predicate>();
if (codes != null) {
List<Predicate> orPredicates = new ArrayList<Predicate>();
Map<String, List<VersionIndependentConcept>> map = new HashMap<String, List<VersionIndependentConcept>>();
for (VersionIndependentConcept nextCode : codes) {
List<VersionIndependentConcept> systemCodes = map.get(nextCode.getSystem());
if (null == systemCodes) {
systemCodes = new ArrayList<>();
map.put(nextCode.getSystem(), systemCodes);
}
systemCodes.add(nextCode);
}
// Use "in" in case of large numbers of codes due to param modifiers
final Path<String> systemExpression = theFrom.get("mySystem");
final Path<String> valueExpression = theFrom.get("myValue");
for (Map.Entry<String, List<VersionIndependentConcept>> entry : map.entrySet()) {
CriteriaBuilder.In<String> codePredicate = theBuilder.in(valueExpression);
boolean haveAtLeastOneCode = false;
for (VersionIndependentConcept nextCode : entry.getValue()) {
if (isNotBlank(nextCode.getCode())) {
codePredicate.value(nextCode.getCode());
haveAtLeastOneCode = true;
}
}
if (entry.getKey() != null) {
Predicate systemPredicate = theBuilder.equal(systemExpression, entry.getKey());
if (haveAtLeastOneCode) {
orPredicates.add(theBuilder.and(systemPredicate, codePredicate));
} else {
orPredicates.add(systemPredicate);
}
} else {
orPredicates.add(codePredicate);
}
}
Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0]));
if (modifier == TokenParamModifier.NOT) {
or = theBuilder.not(or);
}
singleCodePredicates.add(or);
} else {
/*
* Ok, this is a normal query
*/
if (StringUtils.isNotBlank(system)) {
if (modifier != null && modifier == TokenParamModifier.NOT) {
singleCodePredicates.add(theBuilder.notEqual(theFrom.get("mySystem"), system));
} else {
singleCodePredicates.add(theBuilder.equal(theFrom.get("mySystem"), system));
}
} else if (system == null) {
// don't check the system
} else {
// If the system is "", we only match on null systems
singleCodePredicates.add(theBuilder.isNull(theFrom.get("mySystem")));
}
if (StringUtils.isNotBlank(code)) {
if (modifier != null && modifier == TokenParamModifier.NOT) {
singleCodePredicates.add(theBuilder.notEqual(theFrom.get("myValue"), code));
} else {
singleCodePredicates.add(theBuilder.equal(theFrom.get("myValue"), code));
}
} else {
/*
* As of HAPI FHIR 1.5, if the client searched for a token with a system but no specified value this means to
* match all tokens with the given value.
*
* I'm not sure I agree with this, but hey.. FHIR-I voted and this was the result :)
*/
// singleCodePredicates.add(theBuilder.isNull(theFrom.get("myValue")));
}
}
Predicate singleCode = theBuilder.and(toArray(singleCodePredicates));
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, singleCode);
}
/*
* Note: A null system value means "match any system", but
* an empty-string system value means "match values that
@ -1607,9 +1776,14 @@ public class SearchBuilder implements ISearchBuilder {
if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
thePredicates.add(join.get("mySourcePath").as(String.class).in(param.getPathsSplit()));
} else {
Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myResourceName, theSort.getParamName());
Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity);
thePredicates.add(joinParam1);
if (myDontUseHashesForSearch) {
Predicate joinParam1 = theBuilder.equal(join.get("myParamName"), theSort.getParamName());
thePredicates.add(joinParam1);
} else {
Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myResourceName, theSort.getParamName());
Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity);
thePredicates.add(joinParam1);
}
}
} else {
ourLog.debug("Reusing join for {}", theSort.getParamName());
@ -1668,7 +1842,7 @@ public class SearchBuilder implements ISearchBuilder {
//-- preload all tags with tag definition if any
Map<Long, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
Long resourceId = null;
Long resourceId;
for (ResourceSearchView next : resourceSearchViewList) {
Class<? extends IBaseResource> resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass();
@ -1706,7 +1880,7 @@ public class SearchBuilder implements ISearchBuilder {
private Map<Long, Collection<ResourceTag>> getResourceTagMap(Collection<ResourceSearchView> theResourceSearchViewList) {
List<Long> idList = new ArrayList<Long>(theResourceSearchViewList.size());
List<Long> idList = new ArrayList<>(theResourceSearchViewList.size());
//-- find all resource has tags
for (ResourceSearchView resource : theResourceSearchViewList) {

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.entity;
* 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.
@ -129,6 +129,8 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable {
public abstract IQueryParameterType toQueryParameterType();
public abstract void calculateHashes();
public static long calculateHashIdentity(String theResourceType, String theParamName) {
return hash(theResourceType, theParamName);
}

View File

@ -67,6 +67,7 @@ public class ResourceIndexedSearchParamCoords extends BaseResourceIndexedSearchP
setLongitude(theLongitude);
}
@Override
@PrePersist
public void calculateHashes() {
if (myHashIdentity == null) {

View File

@ -83,6 +83,7 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
myOriginalValue = theOriginalValue;
}
@Override
@PrePersist
public void calculateHashes() {
if (myHashIdentity == null) {

View File

@ -69,6 +69,7 @@ public class ResourceIndexedSearchParamNumber extends BaseResourceIndexedSearchP
setValue(theValue);
}
@Override
@PrePersist
public void calculateHashes() {
if (myHashIdentity == null) {

View File

@ -95,6 +95,7 @@ public class ResourceIndexedSearchParamQuantity extends BaseResourceIndexedSearc
setUnits(theUnits);
}
@Override
@PrePersist
public void calculateHashes() {
if (myHashIdentity == null) {

View File

@ -161,6 +161,7 @@ public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchP
myHashIdentity = theHashIdentity;
}
@Override
@PrePersist
public void calculateHashes() {
if (myHashNormalizedPrefix == null && myDaoConfig != null) {

View File

@ -108,6 +108,7 @@ public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchPa
setValue(theValue);
}
@Override
@PrePersist
public void calculateHashes() {
if (myHashSystem == null) {

View File

@ -82,6 +82,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara
setUri(theUri);
}
@Override
@PrePersist
public void calculateHashes() {
if (myHashUri == null) {

View File

@ -39,6 +39,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.util.ParametersUtil;
@ -307,7 +308,7 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic
ourLog.info("Triggering job[{}] search {} requesting resources {} - {}", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex);
List<Long> resourceIds = mySearchCoordinatorSvc.getResources(theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex);
ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex);
ourLog.info("Triggering job[{}] delivering {} resources", theJobDetails.getJobId(), resourceIds.size());
int highestIndexSubmitted = theJobDetails.getCurrentSearchLastUploadedIndex();
for (Long next : resourceIds) {
@ -374,9 +375,22 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic
msg.setNewPayload(myFhirContext, theResourceToTrigger);
return myExecutorService.submit(()->{
for (BaseSubscriptionInterceptor<?> next : mySubscriptionInterceptorList) {
next.submitResourceModified(msg);
for (int i = 0; ; i++) {
try {
for (BaseSubscriptionInterceptor<?> next : mySubscriptionInterceptorList) {
next.submitResourceModified(msg);
}
break;
} catch (Exception e) {
if (i >= 3) {
throw new InternalErrorException(e);
}
ourLog.warn("Exception while retriggering subscriptions (going to sleep and retry): {}", e.toString());
Thread.sleep(1000);
}
}
return null;
});

View File

@ -74,7 +74,6 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
private void deleteSearch(final Long theSearchPid) {
mySearchDao.findById(theSearchPid).ifPresent(searchToDelete -> {
ourLog.info("Deleting search {}/{} - Created[{}] -- Last returned[{}]", searchToDelete.getId(), searchToDelete.getUuid(), new InstantType(searchToDelete.getCreated()), new InstantType(searchToDelete.getSearchLastReturned()));
mySearchIncludeDao.deleteForSearch(searchToDelete.getId());
/*
@ -93,7 +92,10 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
// Only delete if we don't have results left in this search
if (resultPids.getNumberOfElements() < max) {
ourLog.info("Deleting search {}/{} - Created[{}] -- Last returned[{}]", searchToDelete.getId(), searchToDelete.getUuid(), new InstantType(searchToDelete.getCreated()), new InstantType(searchToDelete.getSearchLastReturned()));
mySearchDao.deleteByPid(searchToDelete.getId());
} else {
ourLog.info("Purged {} search results for deleted search {}/{}", resultPids.getSize(), searchToDelete.getId(), searchToDelete.getUuid());
}
});
}

View File

@ -478,8 +478,6 @@ public abstract class BaseSubscriptionInterceptor<S extends IBaseResource> exten
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// FIXME: remove
ourLog.info("** Sending processing message " + theMessage + " for: " + theMessage.getNewPayload(myCtx));
ourLog.trace("Sending resource modified message to processing channel");
getProcessingChannel().send(new ResourceModifiedJsonMessage(theMessage));
}

View File

@ -108,10 +108,6 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
operation.encoded(thePayloadType);
}
// FIXME: remove
ourLog.info("** This " + this + " Processing delivery message " + theMsg);
ourLog.info("Delivering {} rest-hook payload {} for {}", theMsg.getOperationType(), thePayloadResource.getIdElement().toUnqualified().getValue(), theSubscription.getIdElement(getContext()).toUnqualifiedVersionless().getValue());
try {

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
@ -107,12 +108,18 @@ public class TestR4Config extends BaseJavaConfigR4 {
.create(retVal)
.logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL")
.logSlowQueryBySlf4j(10, TimeUnit.SECONDS)
.countQuery(new ThreadQueryCountHolder())
// .countQuery(new ThreadQueryCountHolder())
.countQuery(singleQueryCountHolder())
.build();
return dataSource;
}
@Bean
public SingleQueryCountHolder singleQueryCountHolder() {
return new SingleQueryCountHolder();
}
@Override
@Bean()
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {

View File

@ -289,7 +289,7 @@ public abstract class BaseJpaTest {
return retVal;
}
protected List<IIdType> toUnqualifiedVersionlessIds(List<IBaseResource> theFound) {
protected List<IIdType> toUnqualifiedVersionlessIds(List<? extends IBaseResource> theFound) {
List<IIdType> retVal = new ArrayList<IIdType>();
for (IBaseResource next : theFound) {
retVal.add(next.getIdElement().toUnqualifiedVersionless());

View File

@ -1613,16 +1613,18 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
obs01.setSubject(new Reference(patientId01));
IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
Date between = new Date();
Thread.sleep(10);
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
Observation obs02 = new Observation();
obs02.setEffective(new DateTimeType(new Date()));
obs02.setSubject(new Reference(locId01));
IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless();
Thread.sleep(10);
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
Date after = new Date();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", new Object[] { patientId01, locId01, obsId01, obsId02 });

View File

@ -2865,16 +2865,22 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
p.addName().setFamily(methodName);
IIdType id1 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
p = new Patient();
p.addIdentifier().setSystem("urn:system2").setValue(methodName);
p.addName().setFamily(methodName);
IIdType id2 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
p = new Patient();
p.addIdentifier().setSystem("urn:system3").setValue(methodName);
p.addName().setFamily(methodName);
IIdType id3 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
p = new Patient();
p.addIdentifier().setSystem("urn:system4").setValue(methodName);
p.addName().setFamily(methodName);

View File

@ -4,7 +4,6 @@ import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.util.TestUtil;
import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
@ -18,10 +17,9 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4CreateTest.class);
@ -37,22 +35,22 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
Patient p = myFhirCtx.newXmlParser().parseResource(Patient.class, input);
String id = myPatientDao.create(p).getId().toUnqualifiedVersionless().getValue();
SearchParameterMap map= new SearchParameterMap();
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(Patient.SP_FAMILY, new StringParam(""));
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id));
map= new SearchParameterMap();
map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(Patient.SP_GIVEN, new StringParam(""));
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id));
map= new SearchParameterMap();
map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(Patient.SP_GIVEN, new StringParam("준수"));
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id));
map= new SearchParameterMap();
map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add(Patient.SP_GIVEN, new StringParam("")); // rightmost character only
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty());
@ -60,7 +58,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
}
@Test
public void testCreateWithUuidResourceStrategy() throws Exception {
public void testCreateWithUuidResourceStrategy() {
myDaoConfig.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.UUID);
Patient p = new Patient();
@ -110,26 +108,6 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
assertThat(output.getEntry().get(1).getResponse().getLocation(), matchesPattern("Patient/[a-z0-9]{8}-.*"));
}
@Test
public void testWritesPerformMinimalSqlStatements() {
Patient p = new Patient();
p.addIdentifier().setSystem("sys1").setValue("val1");
p.addIdentifier().setSystem("sys2").setValue("val2");
ourLog.info("** About to perform write");
new ThreadQueryCountHolder().getOrCreateQueryCount("").setInsert(0);
new ThreadQueryCountHolder().getOrCreateQueryCount("").setUpdate(0);
myPatientDao.create(p);
ourLog.info("** Done performing write");
ourLog.info("Inserts: {}", new ThreadQueryCountHolder().getOrCreateQueryCount("").getInsert());
ourLog.info("Updates: {}", new ThreadQueryCountHolder().getOrCreateQueryCount("").getUpdate());
}

View File

@ -1,14 +1,19 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.util.TestUtil;
import net.ttddyy.dsproxy.QueryCount;
import net.ttddyy.dsproxy.QueryCountHolder;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import static org.junit.Assert.assertEquals;
@ -18,6 +23,8 @@ import static org.junit.Assert.assertEquals;
})
public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
@Autowired
private SingleQueryCountHolder myCountHolder;
@After
public void afterResetDao() {
@ -25,22 +32,87 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields());
}
@Test
public void testWritesPerformMinimalSqlStatements() {
Patient p = new Patient();
p.addIdentifier().setSystem("sys1").setValue("val1");
p.addIdentifier().setSystem("sys2").setValue("val2");
ourLog.info("** About to perform write");
myCountHolder.clear();
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
ourLog.info("** Done performing write");
assertEquals(6, getQueryCount().getInsert());
assertEquals(0, getQueryCount().getUpdate());
/*
* Not update the value
*/
p = new Patient();
p.setId(id);
p.addIdentifier().setSystem("sys1").setValue("val3");
p.addIdentifier().setSystem("sys2").setValue("val4");
ourLog.info("** About to perform write 2");
myCountHolder.clear();
myPatientDao.update(p).getId().toUnqualifiedVersionless();
ourLog.info("** Done performing write 2");
assertEquals(2, getQueryCount().getInsert());
assertEquals(1, getQueryCount().getUpdate());
assertEquals(1, getQueryCount().getDelete());
}
@Test
public void testSearch() {
for (int i = 0; i < 20; i++) {
Patient p = new Patient();
p.addIdentifier().setSystem("sys1").setValue("val" + i);
myPatientDao.create(p);
}
myCountHolder.clear();
ourLog.info("** About to perform search");
IBundleProvider search = myPatientDao.search(new SearchParameterMap());
ourLog.info("** About to retrieve resources");
search.getResources(0, 20);
ourLog.info("** Done retrieving resources");
assertEquals(4, getQueryCount().getSelect());
assertEquals(2, getQueryCount().getInsert());
assertEquals(1, getQueryCount().getUpdate());
assertEquals(0, getQueryCount().getDelete());
}
private QueryCount getQueryCount() {
return myCountHolder.getQueryCountMap().get("");
}
@Test
public void testCreateClientAssignedId() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
QueryCountHolder.clear();
myCountHolder.clear();
ourLog.info("** Starting Update Non-Existing resource with client assigned ID");
Patient p = new Patient();
p.setId("A");
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(1, QueryCountHolder.getGrandTotal().getSelect());
assertEquals(4, QueryCountHolder.getGrandTotal().getInsert());
assertEquals(0, QueryCountHolder.getGrandTotal().getDelete());
assertEquals(1, getQueryCount().getSelect());
assertEquals(4, getQueryCount().getInsert());
assertEquals(0, getQueryCount().getDelete());
// Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID
assertEquals(1, QueryCountHolder.getGrandTotal().getUpdate());
assertEquals(1, getQueryCount().getUpdate());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(1, myResourceHistoryTableDao.count());
@ -50,17 +122,17 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
// Ok how about an update
QueryCountHolder.clear();
myCountHolder.clear();
ourLog.info("** Starting Update Existing resource with client assigned ID");
p = new Patient();
p.setId("A");
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(5, QueryCountHolder.getGrandTotal().getSelect());
assertEquals(1, QueryCountHolder.getGrandTotal().getInsert());
assertEquals(0, QueryCountHolder.getGrandTotal().getDelete());
assertEquals(1, QueryCountHolder.getGrandTotal().getUpdate());
assertEquals(5, getQueryCount().getSelect());
assertEquals(1, getQueryCount().getInsert());
assertEquals(0, getQueryCount().getDelete());
assertEquals(1, getQueryCount().getUpdate());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(2, myResourceHistoryTableDao.count());
@ -75,24 +147,24 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
public void testOneRowPerUpdate() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
QueryCountHolder.clear();
myCountHolder.clear();
Patient p = new Patient();
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
assertEquals(3, QueryCountHolder.getGrandTotal().getInsert());
assertEquals(3, getQueryCount().getInsert());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(1, myResourceHistoryTableDao.count());
});
QueryCountHolder.clear();
myCountHolder.clear();
p = new Patient();
p.setId(id);
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(1, QueryCountHolder.getGrandTotal().getInsert());
assertEquals(1, getQueryCount().getInsert());
runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count());
assertEquals(2, myResourceHistoryTableDao.count());
@ -101,6 +173,34 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
}
@Test
public void testUpdateReusesIndexes() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
myCountHolder.clear();
Patient pt = new Patient();
pt.setActive(true);
pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B");
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
ourLog.info("Now have {} deleted", getQueryCount().getDelete());
ourLog.info("Now have {} inserts", getQueryCount().getInsert());
myCountHolder.clear();
ourLog.info("** About to update");
pt.setId(id);
pt.getNameFirstRep().addGiven("GIVEN1C");
myPatientDao.update(pt);
ourLog.info("Now have {} deleted", getQueryCount().getDelete());
ourLog.info("Now have {} inserts", getQueryCount().getInsert());
assertEquals(0, getQueryCount().getDelete());
assertEquals(2, getQueryCount().getInsert());
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();

View File

@ -668,33 +668,6 @@ public class FhirResourceDaoR4UpdateTest extends BaseJpaR4Test {
}
@Test
public void testUpdateReusesIndexes() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
QueryCountHolder.clear();
Patient pt = new Patient();
pt.setActive(true);
pt.addName().setFamily("FAMILY1").addGiven("GIVEN1A").addGiven("GIVEN1B");
IIdType id = myPatientDao.create(pt).getId().toUnqualifiedVersionless();
ourLog.info("Now have {} deleted", QueryCountHolder.getGrandTotal().getDelete());
ourLog.info("Now have {} inserts", QueryCountHolder.getGrandTotal().getInsert());
QueryCountHolder.clear();
ourLog.info("** About to update");
pt.setId(id);
pt.getNameFirstRep().addGiven("GIVEN1C");
myPatientDao.update(pt);
ourLog.info("Now have {} deleted", QueryCountHolder.getGrandTotal().getDelete());
ourLog.info("Now have {} inserts", QueryCountHolder.getGrandTotal().getInsert());
assertEquals(0, QueryCountHolder.getGrandTotal().getDelete());
assertEquals(4, QueryCountHolder.getGrandTotal().getInsert());
}
@Test
public void testUpdateUnknownNumericIdFails() {
Patient p = new Patient();

View File

@ -250,6 +250,8 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base
waitForSize(0, ourCreatedObservations);
waitForSize(5, ourUpdatedObservations);
ourLog.info("Have observations: {}", toUnqualifiedVersionlessIds(ourUpdatedObservations));
Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
Assert.assertFalse(observation1.getId().isEmpty());
Assert.assertFalse(observation2.getId().isEmpty());

View File

@ -349,7 +349,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
/**
* Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20)
*/
protected int escapedLength(String theServletPath) {
protected static int escapedLength(String theServletPath) {
int delta = 0;
for (int i = 0; i < theServletPath.length(); i++) {
char next = theServletPath.charAt(i);
@ -564,6 +564,20 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
return Collections.unmodifiableList(myInterceptors);
}
/**
* Sets (or clears) the list of interceptors
*
* @param theInterceptors The list of interceptors (may be null)
*/
public void setInterceptors(IServerInterceptor... theInterceptors) {
Validate.noNullElements(theInterceptors, "theInterceptors must not contain any null elements");
myInterceptors.clear();
if (theInterceptors != null) {
myInterceptors.addAll(Arrays.asList(theInterceptors));
}
}
/**
* Sets (or clears) the list of interceptors
*
@ -597,6 +611,20 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
return myPlainProviders;
}
/**
* Sets the non-resource specific providers which implement method calls on this server.
*
* @see #setResourceProviders(Collection)
*/
public void setPlainProviders(Collection<Object> theProviders) {
Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
myPlainProviders.clear();
if (theProviders != null) {
myPlainProviders.addAll(theProviders);
}
}
/**
* Sets the non-resource specific providers which implement method calls on this server.
*
@ -615,7 +643,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
* @param servletPath the servelet path
* @return created resource path
*/
protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) {
protected static String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) {
return requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath));
}
@ -630,6 +658,18 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
return myResourceProviders;
}
/**
* Sets the resource providers for this server
*/
public void setResourceProviders(Collection<IResourceProvider> theProviders) {
Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
myResourceProviders.clear();
if (theProviders != null) {
myResourceProviders.addAll(theProviders);
}
}
/**
* Sets the resource providers for this server
*/
@ -1521,34 +1561,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
}
/**
* Sets (or clears) the list of interceptors
*
* @param theInterceptors The list of interceptors (may be null)
*/
public void setInterceptors(IServerInterceptor... theInterceptors) {
Validate.noNullElements(theInterceptors, "theInterceptors must not contain any null elements");
myInterceptors.clear();
if (theInterceptors != null) {
myInterceptors.addAll(Arrays.asList(theInterceptors));
}
}
/**
* Sets the non-resource specific providers which implement method calls on this server.
*
* @see #setResourceProviders(Collection)
*/
public void setPlainProviders(Collection<Object> theProviders) {
Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
myPlainProviders.clear();
if (theProviders != null) {
myPlainProviders.addAll(theProviders);
}
}
/**
* Sets the non-resource specific providers which implement method calls on this server
*
@ -1563,18 +1575,6 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
}
/**
* Sets the resource providers for this server
*/
public void setResourceProviders(Collection<IResourceProvider> theProviders) {
Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
myResourceProviders.clear();
if (theProviders != null) {
myResourceProviders.addAll(theProviders);
}
}
/**
* If provided (default is <code>null</code>), the tenant identification
* strategy provides a mechanism for a multitenant server to identify which tenant
@ -1585,7 +1585,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
}
protected void throwUnknownFhirOperationException(RequestDetails requestDetails, String requestPath, RequestTypeEnum theRequestType) {
throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "unknownMethod", theRequestType.name(), requestPath, requestDetails.getParameters().keySet()));
FhirContext fhirContext = myFhirContext;
throwUnknownFhirOperationException(requestDetails, requestPath, theRequestType, fhirContext);
}
protected void throwUnknownResourceTypeException(String theResourceName) {
@ -1647,6 +1648,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
theResponse.getWriter().write(theException.getMessage());
}
public static void throwUnknownFhirOperationException(RequestDetails requestDetails, String requestPath, RequestTypeEnum theRequestType, FhirContext theFhirContext) {
throw new InvalidRequestException(theFhirContext.getLocalizer().getMessage(RestfulServer.class, "unknownMethod", theRequestType.name(), requestPath, requestDetails.getParameters().keySet()));
}
private static boolean partIsOperation(String nextString) {
return nextString.length() > 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$' || nextString.equals(Constants.URL_TOKEN_METADATA));
}

View File

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

View File

@ -46,6 +46,7 @@ import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
@ -223,6 +224,7 @@ public abstract class BaseMethodBinding<T> {
*/
public abstract String getResourceName();
@Nonnull
public abstract RestOperationTypeEnum getRestOperationType();
/**

View File

@ -37,6 +37,8 @@ import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import javax.annotation.Nonnull;
public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding {
public ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
@ -86,6 +88,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
return false;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.METADATA;

View File

@ -36,6 +36,8 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import javax.annotation.Nonnull;
public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam {
public CreateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
@ -47,6 +49,7 @@ public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
return null;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.CREATE;

View File

@ -30,12 +30,15 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody {
public DeleteMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(theMethod, theContext, theProvider, Delete.class, theMethod.getAnnotation(Delete.class).type());
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.DELETE;

View File

@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
@ -49,6 +50,7 @@ public class GraphQLMethodBinding extends BaseMethodBinding<String> {
return null;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.GRAPHQL_REQUEST;

View File

@ -39,6 +39,7 @@ import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
@ -91,6 +92,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
return BundleTypeEnum.HISTORY;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return myResourceOperationType;

View File

@ -193,7 +193,8 @@ public class MethodUtil {
b.append(" or String or byte[]");
throw new ConfigurationException(b.toString());
}
param = new ResourceParameter((Class<? extends IBaseResource>) parameterType, theProvider, mode);
boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null;
param = new ResourceParameter((Class<? extends IBaseResource>) parameterType, theProvider, mode, methodIsOperation);
} else if (nextAnnotation instanceof IdParam) {
param = new NullParameter();
} else if (nextAnnotation instanceof ServerBase) {

View File

@ -19,50 +19,58 @@ package ca.uhn.fhir.rest.server.method;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.*;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
public static final String WILDCARD_NAME = "$" + Operation.NAME_MATCH_ALL;
private final boolean myIdempotent;
private final Integer myIdParamIndex;
private final String myName;
private final RestOperationTypeEnum myOtherOperatiopnType;
private final ReturnTypeEnum myReturnType;
private BundleTypeEnum myBundleType;
private boolean myCanOperateAtInstanceLevel;
private boolean myCanOperateAtServerLevel;
private boolean myCanOperateAtTypeLevel;
private String myDescription;
private final boolean myIdempotent;
private final Integer myIdParamIndex;
private final String myName;
private final RestOperationTypeEnum myOtherOperatiopnType;
private List<ReturnType> myReturnParams;
private final ReturnTypeEnum myReturnType;
protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType,
OperationParam[] theReturnParams, BundleTypeEnum theBundleType) {
boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType,
OperationParam[] theReturnParams, BundleTypeEnum theBundleType) {
super(theReturnResourceType, theMethod, theContext, theProvider);
myBundleType = theBundleType;
@ -91,7 +99,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (isBlank(theOperationName)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName()
+ " but this annotation has no name defined");
+ " but this annotation has no name defined");
}
if (theOperationName.startsWith("$") == false) {
theOperationName = "$" + theOperationName;
@ -152,15 +160,19 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
Operation theAnnotation) {
Operation theAnnotation) {
this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.returnParameters(),
theAnnotation.bundleType());
theAnnotation.bundleType());
}
public String getDescription() {
return myDescription;
}
public void setDescription(String theDescription) {
myDescription = theDescription;
}
/**
* Returns the name of the operation, starting with "$"
*/
@ -173,6 +185,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return myBundleType;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return myOtherOperatiopnType;
@ -189,15 +202,19 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
@Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (!myName.equals(theRequest.getOperation())) {
if (!myName.equals(WILDCARD_NAME)) {
return false;
}
}
if (getResourceName() == null) {
if (isNotBlank(theRequest.getResourceName())) {
return false;
}
} else if (!getResourceName().equals(theRequest.getResourceName())) {
return false;
}
if (!myName.equals(theRequest.getOperation())) {
if (getResourceName() != null && !getResourceName().equals(theRequest.getResourceName())) {
return false;
}
@ -221,7 +238,6 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return true;
}
@Override
public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
RestOperationTypeEnum retVal = super.getRestOperationType(theRequestDetails);
@ -304,11 +320,6 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
theDetails.setResource((IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY));
}
public void setDescription(String theDescription) {
myDescription = theDescription;
}
public static class ReturnType {
private int myMax;
private int myMin;
@ -322,30 +333,30 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return myMax;
}
public int getMin() {
return myMin;
}
public String getName() {
return myName;
}
public String getType() {
return myType;
}
public void setMax(int theMax) {
myMax = theMax;
}
public int getMin() {
return myMin;
}
public void setMin(int theMin) {
myMin = theMin;
}
public String getName() {
return myName;
}
public void setName(String theName) {
myName = theName;
}
public String getType() {
return myType;
}
public void setType(String theType) {
myType = theType;
}

View File

@ -38,6 +38,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
@ -166,6 +167,7 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
}
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.GET_PAGE;

View File

@ -38,6 +38,8 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
/**
* Base class for an operation that has a resource type but not a resource body in the
* request body
@ -86,6 +88,7 @@ public class PatchMethodBinding extends BaseOutcomeReturningMethodBindingWithRes
return retVal;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.PATCH;

View File

@ -44,6 +44,8 @@ import ca.uhn.fhir.rest.server.ETagSupportEnum;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.util.DateUtils;
import javax.annotation.Nonnull;
public class ReadMethodBinding extends BaseResourceReturningMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadMethodBinding.class);
@ -91,6 +93,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding {
return retVal;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return isVread() ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;

View File

@ -38,6 +38,7 @@ import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
@ -52,15 +53,17 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ResourceParameter implements IParameter {
private final boolean myMethodIsOperation;
private Mode myMode;
private Class<? extends IBaseResource> myResourceType;
public ResourceParameter(Class<? extends IBaseResource> theParameterType, Object theProvider, Mode theMode) {
public ResourceParameter(Class<? extends IBaseResource> theParameterType, Object theProvider, Mode theMode, boolean theMethodIsOperation) {
Validate.notNull(theParameterType, "theParameterType can not be null");
Validate.notNull(theMode, "theMode can not be null");
myResourceType = theParameterType;
myMode = theMode;
myMethodIsOperation = theMethodIsOperation;
Class<? extends IBaseResource> providerResourceType = null;
if (theProvider instanceof IResourceProvider) {
@ -90,24 +93,33 @@ public class ResourceParameter implements IParameter {
@Override
public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
switch (myMode) {
case BODY:
try {
return IOUtils.toString(createRequestReader(theRequest));
} catch (IOException e) {
// Shouldn't happen since we're reading from a byte array
throw new InternalErrorException("Failed to load request", e);
}
case BODY_BYTE_ARRAY:
return theRequest.loadRequestContents();
case ENCODING:
return RestfulServerUtils.determineRequestEncodingNoDefault(theRequest);
case RESOURCE:
default:
return parseResourceFromRequest(theRequest, theMethodBinding, myResourceType);
case BODY:
try {
return IOUtils.toString(createRequestReader(theRequest));
} catch (IOException e) {
// Shouldn't happen since we're reading from a byte array
throw new InternalErrorException("Failed to load request", e);
}
case BODY_BYTE_ARRAY:
return theRequest.loadRequestContents();
case ENCODING:
return RestfulServerUtils.determineRequestEncodingNoDefault(theRequest);
case RESOURCE:
default:
Class<? extends IBaseResource> resourceTypeToParse = myResourceType;
if (myMethodIsOperation) {
// Operations typically have a Parameters resource as the body
resourceTypeToParse = null;
}
return parseResourceFromRequest(theRequest, theMethodBinding, resourceTypeToParse);
}
// }
}
public enum Mode {
BODY, BODY_BYTE_ARRAY, ENCODING, RESOURCE
}
public static Reader createRequestReader(RequestDetails theRequest, Charset charset) {
Reader requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents()), charset);
return requestReader;
@ -118,7 +130,7 @@ public class ResourceParameter implements IParameter {
}
public static Charset determineRequestCharset(RequestDetails theRequest) {
Charset charset = theRequest.getCharset();
Charset charset = theRequest.getCharset();
if (charset == null) {
charset = Charset.forName("UTF-8");
}
@ -126,7 +138,7 @@ public class ResourceParameter implements IParameter {
}
@SuppressWarnings("unchecked")
public static <T extends IBaseResource> T loadResourceFromRequest(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding, Class<T> theResourceType) {
public static <T extends IBaseResource> T loadResourceFromRequest(RequestDetails theRequest, @Nonnull BaseMethodBinding<?> theMethodBinding, Class<T> theResourceType) {
FhirContext ctx = theRequest.getServer().getFhirContext();
final Charset charset = determineRequestCharset(theRequest);
@ -139,7 +151,6 @@ public class ResourceParameter implements IParameter {
String ctValue = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
if (ctValue != null) {
if (ctValue.startsWith("application/x-www-form-urlencoded")) {
//FIXME potential null access theMethodBinding
String msg = theRequest.getServer().getFhirContext().getLocalizer().getMessage(ResourceParameter.class, "invalidContentTypeInRequest", ctValue, theMethodBinding.getRestOperationType());
throw new InvalidRequestException(msg);
}
@ -155,6 +166,9 @@ public class ResourceParameter implements IParameter {
// This shouldn't happen since we're reading from a byte array..
throw new InternalErrorException(e);
}
if (isBlank(body)) {
return null;
}
encoding = EncodingEnum.detectEncodingNoDefault(body);
if (encoding == null) {
String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "noContentTypeInRequest", restOperationType);
@ -168,7 +182,7 @@ public class ResourceParameter implements IParameter {
}
IParser parser = encoding.newParser(ctx);
parser.setServerBaseUrl(theRequest.getFhirServerBase());
parser.setServerBaseUrl(theRequest.getFhirServerBase());
T retVal;
try {
if (theResourceType != null) {
@ -180,14 +194,14 @@ public class ResourceParameter implements IParameter {
String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "failedToParseRequest", encoding.name(), e.getMessage());
throw new InvalidRequestException(msg);
}
return retVal;
}
public static IBaseResource parseResourceFromRequest(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding, Class<? extends IBaseResource> theResourceType) {
IBaseResource retVal = null;
if (IBaseBinary.class.isAssignableFrom(theResourceType)) {
if (theResourceType != null && IBaseBinary.class.isAssignableFrom(theResourceType)) {
String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
if (EncodingEnum.forContentTypeStrict(ct) == null) {
FhirContext ctx = theRequest.getServer().getFhirContext();
@ -209,15 +223,11 @@ public class ResourceParameter implements IParameter {
}
}
}
if (retVal == null) {
retVal = loadResourceFromRequest(theRequest, theMethodBinding, theResourceType);
}
return retVal;
}
public enum Mode {
BODY, BODY_BYTE_ARRAY, ENCODING, RESOURCE
}
}

View File

@ -47,6 +47,8 @@ import ca.uhn.fhir.rest.param.QualifierDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import javax.annotation.Nonnull;
public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
@ -108,6 +110,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return myDescription;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.SEARCH_TYPE;

View File

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

View File

@ -44,6 +44,8 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.method.TransactionParameter.ParamStyle;
import javax.annotation.Nonnull;
public class TransactionMethodBinding extends BaseResourceReturningMethodBinding {
private int myTransactionParamIndex;
@ -73,6 +75,7 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding
}
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.TRANSACTION;

View File

@ -39,6 +39,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import javax.annotation.Nonnull;
public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam {
public UpdateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
@ -98,6 +100,7 @@ public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
return null;
}
@Nonnull
@Override
public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.UPDATE;

View File

@ -0,0 +1,276 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils;
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;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
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.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class OperationGenericServerR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationGenericServerR4Test.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx;
private static IdType ourLastId;
private static String ourLastMethod;
private static StringType ourLastParam1;
private static Patient ourLastParam2;
private static int ourPort;
private static Server ourServer;
private static Parameters ourLastResourceParam;
@Before
public void before() {
ourLastParam1 = null;
ourLastParam2 = null;
ourLastId = null;
ourLastMethod = "";
ourLastResourceParam = null;
}
@Test
public void testOperationOnInstance() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new StringType("PARAM1val"));
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse status = ourClient.execute(httpPost);
try {
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
status.getEntity().getContent().close();
assertEquals("PARAM1val", ourLastParam1.getValue());
assertEquals(true, ourLastParam2.getActive());
assertEquals("123", ourLastId.getIdPart());
assertEquals("$OP_INSTANCE", ourLastMethod);
assertEquals("PARAM1", ourLastResourceParam.getParameterFirstRep().getName());
Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response);
assertEquals("RET1", resp.getParameter().get(0).getName());
} finally {
status.getEntity().getContent().close();
}
}
@Test
public void testOperationOnServer() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new StringType("PARAM1val"));
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/$OP_SERVER");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse status = ourClient.execute(httpPost);
try {
assertEquals(200, status.getStatusLine().getStatusCode());
String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
assertEquals("PARAM1", ourLastResourceParam.getParameterFirstRep().getName());
assertEquals("PARAM1val", ourLastParam1.getValue());
assertEquals(true, ourLastParam2.getActive());
assertEquals("$OP_SERVER", ourLastMethod);
Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response);
assertEquals("RET1", resp.getParameter().get(0).getName());
} finally {
status.getEntity().getContent().close();
}
}
@Test
public void testOperationOnType() throws Exception {
Parameters p = new Parameters();
p.addParameter().setName("PARAM1").setValue(new StringType("PARAM1val"));
p.addParameter().setName("PARAM2").setResource(new Patient().setActive(true));
String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$OP_TYPE");
httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse status = ourClient.execute(httpPost);
try {
String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(response);
assertEquals(200, status.getStatusLine().getStatusCode());
status.getEntity().getContent().close();
assertEquals("PARAM1", ourLastResourceParam.getParameterFirstRep().getName());
assertEquals("PARAM1val", ourLastParam1.getValue());
assertEquals(true, ourLastParam2.getActive());
assertEquals("$OP_TYPE", ourLastMethod);
Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response);
assertEquals("RET1", resp.getParameter().get(0).getName());
} finally {
status.getEntity().getContent().close();
}
}
@Test
public void testOperationWithGetUsingParams() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$OP_TYPE?PARAM1=PARAM1val");
CloseableHttpResponse status = ourClient.execute(httpGet);
try {
String response = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(response);
assertEquals(200, status.getStatusLine().getStatusCode());
status.getEntity().getContent().close();
assertNull(ourLastResourceParam);
assertEquals("PARAM1val", ourLastParam1.getValue());
assertNull(ourLastParam2);
assertEquals("$OP_TYPE", ourLastMethod);
Parameters resp = ourCtx.newXmlParser().parseResource(Parameters.class, response);
assertEquals("RET1", resp.getParameter().get(0).getName());
} finally {
status.getEntity().getContent().close();
}
}
@SuppressWarnings("unused")
public static class PatientProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Operation(name = Operation.NAME_MATCH_ALL)
public Parameters opInstance(
@ResourceParam() IBaseResource theResourceParam,
@IdParam IdType theId,
@OperationParam(name = "PARAM1") StringType theParam1,
@OperationParam(name = "PARAM2") Patient theParam2
) {
ourLastMethod = "$OP_INSTANCE";
ourLastId = theId;
ourLastParam1 = theParam1;
ourLastParam2 = theParam2;
ourLastResourceParam = (Parameters) theResourceParam;
Parameters retVal = new Parameters();
retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1"));
return retVal;
}
@SuppressWarnings("unused")
@Operation(name = Operation.NAME_MATCH_ALL, idempotent = true)
public Parameters opType(
@ResourceParam() IBaseResource theResourceParam,
@OperationParam(name = "PARAM1") StringType theParam1,
@OperationParam(name = "PARAM2") Patient theParam2,
@OperationParam(name = "PARAM3", min = 2, max = 5) List<StringType> theParam3,
@OperationParam(name = "PARAM4", min = 1) List<StringType> theParam4
) {
ourLastMethod = "$OP_TYPE";
ourLastParam1 = theParam1;
ourLastParam2 = theParam2;
ourLastResourceParam = (Parameters) theResourceParam;
Parameters retVal = new Parameters();
retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1"));
return retVal;
}
}
@SuppressWarnings("unused")
public static class PlainProvider {
@Operation(name = Operation.NAME_MATCH_ALL)
public Parameters opServer(
@ResourceParam() IBaseResource theResourceParam,
@OperationParam(name = "PARAM1") StringType theParam1,
@OperationParam(name = "PARAM2") Patient theParam2
) {
ourLastMethod = "$OP_SERVER";
ourLastParam1 = theParam1;
ourLastParam2 = theParam2;
ourLastResourceParam = (Parameters) theResourceParam;
Parameters retVal = new Parameters();
retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1"));
return retVal;
}
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourCtx = FhirContext.forR4();
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(ourCtx);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2));
servlet.setFhirContext(ourCtx);
servlet.setResourceProviders(new PatientProvider());
servlet.setPlainProviders(new PlainProvider());
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();
}
}

File diff suppressed because it is too large Load Diff