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

@ -20,15 +20,14 @@ package ca.uhn.fhir.rest.annotation;
* #L% * #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.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; 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". * RESTful method annotation used for a method which provides FHIR "operations".
*/ */
@ -36,6 +35,14 @@ import ca.uhn.fhir.model.valueset.BundleTypeEnum;
@Target(value = ElementType.METHOD) @Target(value = ElementType.METHOD)
public @interface Operation { public @interface Operation {
/**
* 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>" * The name of the operation, e.g. "<code>$everything</code>"
* *

View File

@ -2086,6 +2086,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
*/ */
if (thePerformIndexing) { if (thePerformIndexing) {
calculateHashes(stringParams);
for (ResourceIndexedSearchParamString next : removeCommon(existingStringParams, stringParams)) { for (ResourceIndexedSearchParamString next : removeCommon(existingStringParams, stringParams)) {
next.setDaoConfig(myConfig); next.setDaoConfig(myConfig);
myEntityManager.remove(next); myEntityManager.remove(next);
@ -2095,6 +2096,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
myEntityManager.persist(next); myEntityManager.persist(next);
} }
calculateHashes(tokenParams);
for (ResourceIndexedSearchParamToken next : removeCommon(existingTokenParams, tokenParams)) { for (ResourceIndexedSearchParamToken next : removeCommon(existingTokenParams, tokenParams)) {
myEntityManager.remove(next); myEntityManager.remove(next);
theEntity.getParamsToken().remove(next); theEntity.getParamsToken().remove(next);
@ -2103,6 +2105,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
myEntityManager.persist(next); myEntityManager.persist(next);
} }
calculateHashes(numberParams);
for (ResourceIndexedSearchParamNumber next : removeCommon(existingNumberParams, numberParams)) { for (ResourceIndexedSearchParamNumber next : removeCommon(existingNumberParams, numberParams)) {
myEntityManager.remove(next); myEntityManager.remove(next);
theEntity.getParamsNumber().remove(next); theEntity.getParamsNumber().remove(next);
@ -2111,6 +2114,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
myEntityManager.persist(next); myEntityManager.persist(next);
} }
calculateHashes(quantityParams);
for (ResourceIndexedSearchParamQuantity next : removeCommon(existingQuantityParams, quantityParams)) { for (ResourceIndexedSearchParamQuantity next : removeCommon(existingQuantityParams, quantityParams)) {
myEntityManager.remove(next); myEntityManager.remove(next);
theEntity.getParamsQuantity().remove(next); theEntity.getParamsQuantity().remove(next);
@ -2120,6 +2124,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
} }
// Store date SP's // Store date SP's
calculateHashes(dateParams);
for (ResourceIndexedSearchParamDate next : removeCommon(existingDateParams, dateParams)) { for (ResourceIndexedSearchParamDate next : removeCommon(existingDateParams, dateParams)) {
myEntityManager.remove(next); myEntityManager.remove(next);
theEntity.getParamsDate().remove(next); theEntity.getParamsDate().remove(next);
@ -2129,6 +2134,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
} }
// Store URI SP's // Store URI SP's
calculateHashes(uriParams);
for (ResourceIndexedSearchParamUri next : removeCommon(existingUriParams, uriParams)) { for (ResourceIndexedSearchParamUri next : removeCommon(existingUriParams, uriParams)) {
myEntityManager.remove(next); myEntityManager.remove(next);
theEntity.getParamsUri().remove(next); theEntity.getParamsUri().remove(next);
@ -2138,6 +2144,7 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
} }
// Store Coords SP's // Store Coords SP's
calculateHashes(coordsParams);
for (ResourceIndexedSearchParamCoords next : removeCommon(existingCoordsParams, coordsParams)) { for (ResourceIndexedSearchParamCoords next : removeCommon(existingCoordsParams, coordsParams)) {
myEntityManager.remove(next); myEntityManager.remove(next);
theEntity.getParamsCoords().remove(next); theEntity.getParamsCoords().remove(next);
@ -2187,6 +2194,12 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> implements IDao,
return theEntity; return theEntity;
} }
private void calculateHashes(Collection<? extends BaseResourceIndexedSearchParam> theStringParams) {
for (BaseResourceIndexedSearchParam next : theStringParams) {
next.calculateHashes();
}
}
protected ResourceTable updateEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable protected ResourceTable updateEntity(RequestDetails theRequest, IBaseResource theResource, ResourceTable
entity, Date theDeletedTimestampOrNull, Date theUpdateTime) { entity, Date theDeletedTimestampOrNull, Date theUpdateTime) {
return updateEntity(theRequest, theResource, entity, theDeletedTimestampOrNull, true, true, theUpdateTime, false, true); 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 boolean myValidateSearchParameterExpressionsOnSave = true;
private List<Integer> mySearchPreFetchThresholds = Arrays.asList(500, 2000, -1); private List<Integer> mySearchPreFetchThresholds = Arrays.asList(500, 2000, -1);
private List<WarmCacheEntry> myWarmCacheEntries = new ArrayList<>(); private List<WarmCacheEntry> myWarmCacheEntries = new ArrayList<>();
private boolean myDisableHashBasedSearches;
/** /**
* Constructor * Constructor
@ -1383,6 +1384,34 @@ public class DaoConfig {
return mySearchPreFetchThresholds; 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 { public enum IndexEnabledEnum {
ENABLED, ENABLED,
DISABLED DISABLED

View File

@ -55,6 +55,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;
@ -94,6 +95,7 @@ public class SearchBuilder implements ISearchBuilder {
private static SearchParameterMap ourLastHandlerParamsForUnitTest; private static SearchParameterMap ourLastHandlerParamsForUnitTest;
private static String ourLastHandlerThreadForUnitTest; private static String ourLastHandlerThreadForUnitTest;
private static boolean ourTrackHandlersForUnitTest; private static boolean ourTrackHandlersForUnitTest;
private final boolean myDontUseHashesForSearch;
protected IResourceTagDao myResourceTagDao; protected IResourceTagDao myResourceTagDao;
private IResourceSearchViewDao myResourceSearchViewDao; private IResourceSearchViewDao myResourceSearchViewDao;
private List<Long> myAlsoIncludePids; private List<Long> myAlsoIncludePids;
@ -130,6 +132,7 @@ public class SearchBuilder implements ISearchBuilder {
myEntityManager = theEntityManager; myEntityManager = theEntityManager;
myFulltextSearchSvc = theFulltextSearchSvc; myFulltextSearchSvc = theFulltextSearchSvc;
myCallingDao = theDao; myCallingDao = theDao;
myDontUseHashesForSearch = theDao.getConfig().getDisableHashBasedSearches();
myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao; myResourceIndexedSearchParamUriDao = theResourceIndexedSearchParamUriDao;
myForcedIdDao = theForcedIdDao; myForcedIdDao = theForcedIdDao;
myTerminologySvc = theTerminologySvc; myTerminologySvc = theTerminologySvc;
@ -304,6 +307,15 @@ public class SearchBuilder implements ISearchBuilder {
} }
private void addPredicateParamMissing(String theResourceName, String theParamName, boolean theMissing) { 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); Join<ResourceTable, SearchParamPresent> paramPresentJoin = myResourceTableRoot.join("mySearchParamPresents", JoinType.LEFT);
Expression<Long> hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class); Expression<Long> hashPresence = paramPresentJoin.get("myHashPresence").as(Long.class);
@ -839,6 +851,13 @@ public class SearchBuilder implements ISearchBuilder {
Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate); Predicate hashAndUriPredicate = combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, join, uriPredicate);
codePredicates.add(hashAndUriPredicate); codePredicates.add(hashAndUriPredicate);
} else {
if (myDontUseHashesForSearch) {
Predicate predicate = myBuilder.equal(join.get("myUri").as(String.class), value);
codePredicates.add(predicate);
} else { } else {
long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value); long hashUri = ResourceIndexedSearchParamUri.calculateHashUri(theResourceName, theParamName, value);
@ -846,6 +865,7 @@ public class SearchBuilder implements ISearchBuilder {
codePredicates.add(hashPredicate); codePredicates.add(hashPredicate);
} }
}
} else { } else {
throw new IllegalArgumentException("Invalid URI type: " + nextOr.getClass()); throw new IllegalArgumentException("Invalid URI type: " + nextOr.getClass());
@ -868,6 +888,13 @@ public class SearchBuilder implements ISearchBuilder {
} }
private Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From<?, ? extends BaseResourceIndexedSearchParam> theFrom, Predicate thePredicate) { 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); long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(theResourceName, theParamName);
Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity); Predicate hashIdentityPredicate = myBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity);
return myBuilder.and(hashIdentityPredicate, thePredicate); return myBuilder.and(hashIdentityPredicate, thePredicate);
@ -1079,6 +1106,37 @@ public class SearchBuilder implements ISearchBuilder {
throw new IllegalArgumentException("Invalid quantity type: " + theParam.getClass()); 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; Predicate hashPredicate;
if (!isBlank(systemValue) && !isBlank(unitsValue)) { if (!isBlank(systemValue) && !isBlank(unitsValue)) {
long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue); long hash = ResourceIndexedSearchParamQuantity.calculateHashSystemAndUnits(theResourceName, theParamName, systemValue, unitsValue);
@ -1130,6 +1188,31 @@ public class SearchBuilder implements ISearchBuilder {
+ ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); + 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(); boolean exactMatch = theParameter instanceof StringParam && ((StringParam) theParameter).isExact();
if (exactMatch) { if (exactMatch) {
@ -1234,6 +1317,92 @@ public class SearchBuilder implements ISearchBuilder {
return new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false); 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 * Note: A null system value means "match any system", but
* an empty-string system value means "match values that * an empty-string system value means "match values that
@ -1606,11 +1775,16 @@ public class SearchBuilder implements ISearchBuilder {
if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
thePredicates.add(join.get("mySourcePath").as(String.class).in(param.getPathsSplit())); thePredicates.add(join.get("mySourcePath").as(String.class).in(param.getPathsSplit()));
} else {
if (myDontUseHashesForSearch) {
Predicate joinParam1 = theBuilder.equal(join.get("myParamName"), theSort.getParamName());
thePredicates.add(joinParam1);
} else { } else {
Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myResourceName, theSort.getParamName()); Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myResourceName, theSort.getParamName());
Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity); Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity);
thePredicates.add(joinParam1); thePredicates.add(joinParam1);
} }
}
} else { } else {
ourLog.debug("Reusing join for {}", theSort.getParamName()); ourLog.debug("Reusing join for {}", theSort.getParamName());
} }
@ -1668,7 +1842,7 @@ public class SearchBuilder implements ISearchBuilder {
//-- preload all tags with tag definition if any //-- preload all tags with tag definition if any
Map<Long, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList); Map<Long, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
Long resourceId = null; Long resourceId;
for (ResourceSearchView next : resourceSearchViewList) { for (ResourceSearchView next : resourceSearchViewList) {
Class<? extends IBaseResource> resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass(); 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) { 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 //-- find all resource has tags
for (ResourceSearchView resource : theResourceSearchViewList) { for (ResourceSearchView resource : theResourceSearchViewList) {

View File

@ -129,6 +129,8 @@ public abstract class BaseResourceIndexedSearchParam implements Serializable {
public abstract IQueryParameterType toQueryParameterType(); public abstract IQueryParameterType toQueryParameterType();
public abstract void calculateHashes();
public static long calculateHashIdentity(String theResourceType, String theParamName) { public static long calculateHashIdentity(String theResourceType, String theParamName) {
return hash(theResourceType, theParamName); return hash(theResourceType, theParamName);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -82,6 +82,7 @@ public class ResourceIndexedSearchParamUri extends BaseResourceIndexedSearchPara
setUri(theUri); setUri(theUri);
} }
@Override
@PrePersist @PrePersist
public void calculateHashes() { public void calculateHashes() {
if (myHashUri == null) { 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.StringParam;
import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.IResourceProvider; 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.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.util.ParametersUtil; 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); ourLog.info("Triggering job[{}] search {} requesting resources {} - {}", theJobDetails.getJobId(), theJobDetails.getCurrentSearchUuid(), fromIndex, toIndex);
List<Long> resourceIds = mySearchCoordinatorSvc.getResources(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(); int highestIndexSubmitted = theJobDetails.getCurrentSearchLastUploadedIndex();
for (Long next : resourceIds) { for (Long next : resourceIds) {
@ -374,9 +375,22 @@ public class SubscriptionTriggeringProvider implements IResourceProvider, Applic
msg.setNewPayload(myFhirContext, theResourceToTrigger); msg.setNewPayload(myFhirContext, theResourceToTrigger);
return myExecutorService.submit(()->{ return myExecutorService.submit(()->{
for (int i = 0; ; i++) {
try {
for (BaseSubscriptionInterceptor<?> next : mySubscriptionInterceptorList) { for (BaseSubscriptionInterceptor<?> next : mySubscriptionInterceptorList) {
next.submitResourceModified(msg); 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; return null;
}); });

View File

@ -74,7 +74,6 @@ public class StaleSearchDeletingSvcImpl implements IStaleSearchDeletingSvc {
private void deleteSearch(final Long theSearchPid) { private void deleteSearch(final Long theSearchPid) {
mySearchDao.findById(theSearchPid).ifPresent(searchToDelete -> { 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()); 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 // Only delete if we don't have results left in this search
if (resultPids.getNumberOfElements() < max) { 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()); 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() { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override @Override
public void afterCommit() { public void afterCommit() {
// FIXME: remove
ourLog.info("** Sending processing message " + theMessage + " for: " + theMessage.getNewPayload(myCtx));
ourLog.trace("Sending resource modified message to processing channel"); ourLog.trace("Sending resource modified message to processing channel");
getProcessingChannel().send(new ResourceModifiedJsonMessage(theMessage)); getProcessingChannel().send(new ResourceModifiedJsonMessage(theMessage));
} }

View File

@ -108,10 +108,6 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
operation.encoded(thePayloadType); 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()); ourLog.info("Delivering {} rest-hook payload {} for {}", theMsg.getOperationType(), thePayloadResource.getIdElement().toUnqualified().getValue(), theSubscription.getIdElement(getContext()).toUnqualifiedVersionless().getValue());
try { try {

View File

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

View File

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

View File

@ -1613,16 +1613,18 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
obs01.setSubject(new Reference(patientId01)); obs01.setSubject(new Reference(patientId01));
IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless(); IIdType obsId01 = myObservationDao.create(obs01, mySrd).getId().toUnqualifiedVersionless();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
Date between = new Date(); Date between = new Date();
Thread.sleep(10); ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
Observation obs02 = new Observation(); Observation obs02 = new Observation();
obs02.setEffective(new DateTimeType(new Date())); obs02.setEffective(new DateTimeType(new Date()));
obs02.setSubject(new Reference(locId01)); obs02.setSubject(new Reference(locId01));
IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless(); IIdType obsId02 = myObservationDao.create(obs02, mySrd).getId().toUnqualifiedVersionless();
Thread.sleep(10); ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
Date after = new Date(); Date after = new Date();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
ourLog.info("P1[{}] L1[{}] Obs1[{}] Obs2[{}]", new Object[] { patientId01, locId01, obsId01, obsId02 }); 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); p.addName().setFamily(methodName);
IIdType id1 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); IIdType id1 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
p = new Patient(); p = new Patient();
p.addIdentifier().setSystem("urn:system2").setValue(methodName); p.addIdentifier().setSystem("urn:system2").setValue(methodName);
p.addName().setFamily(methodName); p.addName().setFamily(methodName);
IIdType id2 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); IIdType id2 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
p = new Patient(); p = new Patient();
p.addIdentifier().setSystem("urn:system3").setValue(methodName); p.addIdentifier().setSystem("urn:system3").setValue(methodName);
p.addName().setFamily(methodName); p.addName().setFamily(methodName);
IIdType id3 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless(); IIdType id3 = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast(1);
p = new Patient(); p = new Patient();
p.addIdentifier().setSystem("urn:system4").setValue(methodName); p.addIdentifier().setSystem("urn:system4").setValue(methodName);
p.addName().setFamily(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.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IdType;
@ -18,10 +17,9 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals;
import static org.hamcrest.Matchers.matchesPattern; import static org.junit.Assert.assertThat;
import static org.junit.Assert.*;
public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4CreateTest.class); 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); Patient p = myFhirCtx.newXmlParser().parseResource(Patient.class, input);
String id = myPatientDao.create(p).getId().toUnqualifiedVersionless().getValue(); String id = myPatientDao.create(p).getId().toUnqualifiedVersionless().getValue();
SearchParameterMap map= new SearchParameterMap(); SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true); map.setLoadSynchronous(true);
map.add(Patient.SP_FAMILY, new StringParam("")); map.add(Patient.SP_FAMILY, new StringParam(""));
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id)); assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id));
map= new SearchParameterMap(); map = new SearchParameterMap();
map.setLoadSynchronous(true); map.setLoadSynchronous(true);
map.add(Patient.SP_GIVEN, new StringParam("")); map.add(Patient.SP_GIVEN, new StringParam(""));
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id)); assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id));
map= new SearchParameterMap(); map = new SearchParameterMap();
map.setLoadSynchronous(true); map.setLoadSynchronous(true);
map.add(Patient.SP_GIVEN, new StringParam("준수")); map.add(Patient.SP_GIVEN, new StringParam("준수"));
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id)); assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), contains(id));
map= new SearchParameterMap(); map = new SearchParameterMap();
map.setLoadSynchronous(true); map.setLoadSynchronous(true);
map.add(Patient.SP_GIVEN, new StringParam("")); // rightmost character only map.add(Patient.SP_GIVEN, new StringParam("")); // rightmost character only
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty()); assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), empty());
@ -60,7 +58,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
} }
@Test @Test
public void testCreateWithUuidResourceStrategy() throws Exception { public void testCreateWithUuidResourceStrategy() {
myDaoConfig.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.UUID); myDaoConfig.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.UUID);
Patient p = new Patient(); 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}-.*")); 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; package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig; 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 ca.uhn.fhir.util.TestUtil;
import net.ttddyy.dsproxy.QueryCount;
import net.ttddyy.dsproxy.QueryCountHolder; import net.ttddyy.dsproxy.QueryCountHolder;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.DateTimeType; import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -18,6 +23,8 @@ import static org.junit.Assert.assertEquals;
}) })
public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test { public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4QueryCountTest.class);
@Autowired
private SingleQueryCountHolder myCountHolder;
@After @After
public void afterResetDao() { public void afterResetDao() {
@ -25,22 +32,87 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
myDaoConfig.setIndexMissingFields(new DaoConfig().getIndexMissingFields()); 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 @Test
public void testCreateClientAssignedId() { public void testCreateClientAssignedId() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
QueryCountHolder.clear(); myCountHolder.clear();
ourLog.info("** Starting Update Non-Existing resource with client assigned ID"); ourLog.info("** Starting Update Non-Existing resource with client assigned ID");
Patient p = new Patient(); Patient p = new Patient();
p.setId("A"); p.setId("A");
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless(); myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(1, QueryCountHolder.getGrandTotal().getSelect()); assertEquals(1, getQueryCount().getSelect());
assertEquals(4, QueryCountHolder.getGrandTotal().getInsert()); assertEquals(4, getQueryCount().getInsert());
assertEquals(0, QueryCountHolder.getGrandTotal().getDelete()); assertEquals(0, getQueryCount().getDelete());
// Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID // Because of the forced ID's bidirectional link HFJ_RESOURCE <-> HFJ_FORCED_ID
assertEquals(1, QueryCountHolder.getGrandTotal().getUpdate()); assertEquals(1, getQueryCount().getUpdate());
runInTransaction(() -> { runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count()); assertEquals(1, myResourceTableDao.count());
assertEquals(1, myResourceHistoryTableDao.count()); assertEquals(1, myResourceHistoryTableDao.count());
@ -50,17 +122,17 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
// Ok how about an update // Ok how about an update
QueryCountHolder.clear(); myCountHolder.clear();
ourLog.info("** Starting Update Existing resource with client assigned ID"); ourLog.info("** Starting Update Existing resource with client assigned ID");
p = new Patient(); p = new Patient();
p.setId("A"); p.setId("A");
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless(); myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(5, QueryCountHolder.getGrandTotal().getSelect()); assertEquals(5, getQueryCount().getSelect());
assertEquals(1, QueryCountHolder.getGrandTotal().getInsert()); assertEquals(1, getQueryCount().getInsert());
assertEquals(0, QueryCountHolder.getGrandTotal().getDelete()); assertEquals(0, getQueryCount().getDelete());
assertEquals(1, QueryCountHolder.getGrandTotal().getUpdate()); assertEquals(1, getQueryCount().getUpdate());
runInTransaction(() -> { runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count()); assertEquals(1, myResourceTableDao.count());
assertEquals(2, myResourceHistoryTableDao.count()); assertEquals(2, myResourceHistoryTableDao.count());
@ -75,24 +147,24 @@ public class FhirResourceDaoR4QueryCountTest extends BaseJpaR4Test {
public void testOneRowPerUpdate() { public void testOneRowPerUpdate() {
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED); myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
QueryCountHolder.clear(); myCountHolder.clear();
Patient p = new Patient(); Patient p = new Patient();
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field p.getPhotoFirstRep().setCreationElement(new DateTimeType("2011")); // non-indexed field
IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless(); IIdType id = myPatientDao.create(p).getId().toUnqualifiedVersionless();
assertEquals(3, QueryCountHolder.getGrandTotal().getInsert()); assertEquals(3, getQueryCount().getInsert());
runInTransaction(() -> { runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count()); assertEquals(1, myResourceTableDao.count());
assertEquals(1, myResourceHistoryTableDao.count()); assertEquals(1, myResourceHistoryTableDao.count());
}); });
QueryCountHolder.clear(); myCountHolder.clear();
p = new Patient(); p = new Patient();
p.setId(id); p.setId(id);
p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field p.getPhotoFirstRep().setCreationElement(new DateTimeType("2012")); // non-indexed field
myPatientDao.update(p).getId().toUnqualifiedVersionless(); myPatientDao.update(p).getId().toUnqualifiedVersionless();
assertEquals(1, QueryCountHolder.getGrandTotal().getInsert()); assertEquals(1, getQueryCount().getInsert());
runInTransaction(() -> { runInTransaction(() -> {
assertEquals(1, myResourceTableDao.count()); assertEquals(1, myResourceTableDao.count());
assertEquals(2, myResourceHistoryTableDao.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 @AfterClass
public static void afterClassClearContext() { public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest(); 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 @Test
public void testUpdateUnknownNumericIdFails() { public void testUpdateUnknownNumericIdFails() {
Patient p = new Patient(); Patient p = new Patient();

View File

@ -250,6 +250,8 @@ public class RestHookTestWithInterceptorRegisteredToDaoConfigR4Test extends Base
waitForSize(0, ourCreatedObservations); waitForSize(0, ourCreatedObservations);
waitForSize(5, ourUpdatedObservations); waitForSize(5, ourUpdatedObservations);
ourLog.info("Have observations: {}", toUnqualifiedVersionlessIds(ourUpdatedObservations));
Assert.assertFalse(subscription1.getId().equals(subscription2.getId())); Assert.assertFalse(subscription1.getId().equals(subscription2.getId()));
Assert.assertFalse(observation1.getId().isEmpty()); Assert.assertFalse(observation1.getId().isEmpty());
Assert.assertFalse(observation2.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) * 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; int delta = 0;
for (int i = 0; i < theServletPath.length(); i++) { for (int i = 0; i < theServletPath.length(); i++) {
char next = theServletPath.charAt(i); char next = theServletPath.charAt(i);
@ -564,6 +564,20 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
return Collections.unmodifiableList(myInterceptors); 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 * Sets (or clears) the list of interceptors
* *
@ -597,6 +611,20 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
return myPlainProviders; 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. * 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 * @param servletPath the servelet path
* @return created resource 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)); return requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath));
} }
@ -630,6 +658,18 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
return myResourceProviders; 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 * 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 * 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 * If provided (default is <code>null</code>), the tenant identification
* strategy provides a mechanism for a multitenant server to identify which tenant * 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) { 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) { protected void throwUnknownResourceTypeException(String theResourceName) {
@ -1647,6 +1648,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
theResponse.getWriter().write(theException.getMessage()); 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) { private static boolean partIsOperation(String nextString) {
return nextString.length() > 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$' || nextString.equals(Constants.URL_TOKEN_METADATA)); return nextString.length() > 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$' || nextString.equals(Constants.URL_TOKEN_METADATA));
} }

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.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
@ -223,6 +224,7 @@ public abstract class BaseMethodBinding<T> {
*/ */
public abstract String getResourceName(); public abstract String getResourceName();
@Nonnull
public abstract RestOperationTypeEnum getRestOperationType(); 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.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import javax.annotation.Nonnull;
public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding { public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding {
public ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { public ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
@ -86,6 +88,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
return false; return false;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.METADATA; 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.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import javax.annotation.Nonnull;
public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam {
public CreateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { public CreateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
@ -47,6 +49,7 @@ public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
return null; return null;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.CREATE; 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.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nonnull;
public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody { public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody {
public DeleteMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { public DeleteMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(theMethod, theContext, theProvider, Delete.class, theMethod.getAnnotation(Delete.class).type()); super(theMethod, theContext, theProvider, Delete.class, theMethod.getAnnotation(Delete.class).type());
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.DELETE; 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.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@ -49,6 +50,7 @@ public class GraphQLMethodBinding extends BaseMethodBinding<String> {
return null; return null;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.GRAPHQL_REQUEST; 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.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import javax.annotation.Nonnull;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Date; import java.util.Date;
@ -91,6 +92,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
return BundleTypeEnum.HISTORY; return BundleTypeEnum.HISTORY;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return myResourceOperationType; return myResourceOperationType;

View File

@ -193,7 +193,8 @@ public class MethodUtil {
b.append(" or String or byte[]"); b.append(" or String or byte[]");
throw new ConfigurationException(b.toString()); 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) { } else if (nextAnnotation instanceof IdParam) {
param = new NullParameter(); param = new NullParameter();
} else if (nextAnnotation instanceof ServerBase) { } else if (nextAnnotation instanceof ServerBase) {

View File

@ -19,46 +19,54 @@ package ca.uhn.fhir.rest.server.method;
* limitations under the License. * limitations under the License.
* #L% * #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.ConfigurationException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.valueset.BundleTypeEnum; 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.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 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.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; 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 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 BundleTypeEnum myBundleType;
private boolean myCanOperateAtInstanceLevel; private boolean myCanOperateAtInstanceLevel;
private boolean myCanOperateAtServerLevel; private boolean myCanOperateAtServerLevel;
private boolean myCanOperateAtTypeLevel; private boolean myCanOperateAtTypeLevel;
private String myDescription; private String myDescription;
private final boolean myIdempotent;
private final Integer myIdParamIndex;
private final String myName;
private final RestOperationTypeEnum myOtherOperatiopnType;
private List<ReturnType> myReturnParams; private List<ReturnType> myReturnParams;
private final ReturnTypeEnum myReturnType;
protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType, boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType,
@ -161,6 +169,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return myDescription; return myDescription;
} }
public void setDescription(String theDescription) {
myDescription = theDescription;
}
/** /**
* Returns the name of the operation, starting with "$" * Returns the name of the operation, starting with "$"
*/ */
@ -173,6 +185,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return myBundleType; return myBundleType;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return myOtherOperatiopnType; return myOtherOperatiopnType;
@ -189,15 +202,19 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
@Override @Override
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) { public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
if (!myName.equals(theRequest.getOperation())) {
if (!myName.equals(WILDCARD_NAME)) {
return false;
}
}
if (getResourceName() == null) { if (getResourceName() == null) {
if (isNotBlank(theRequest.getResourceName())) { if (isNotBlank(theRequest.getResourceName())) {
return false; return false;
} }
} else if (!getResourceName().equals(theRequest.getResourceName())) {
return false;
} }
if (!myName.equals(theRequest.getOperation())) { if (getResourceName() != null && !getResourceName().equals(theRequest.getResourceName())) {
return false; return false;
} }
@ -221,7 +238,6 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return true; return true;
} }
@Override @Override
public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) { public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
RestOperationTypeEnum retVal = super.getRestOperationType(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)); theDetails.setResource((IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY));
} }
public void setDescription(String theDescription) {
myDescription = theDescription;
}
public static class ReturnType { public static class ReturnType {
private int myMax; private int myMax;
private int myMin; private int myMin;
@ -322,30 +333,30 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return myMax; return myMax;
} }
public int getMin() {
return myMin;
}
public String getName() {
return myName;
}
public String getType() {
return myType;
}
public void setMax(int theMax) { public void setMax(int theMax) {
myMax = theMax; myMax = theMax;
} }
public int getMin() {
return myMin;
}
public void setMin(int theMin) { public void setMin(int theMin) {
myMin = theMin; myMin = theMin;
} }
public String getName() {
return myName;
}
public void setName(String theName) { public void setName(String theName) {
myName = theName; myName = theName;
} }
public String getType() {
return myType;
}
public void setType(String theType) { public void setType(String theType) {
myType = 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 ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -166,6 +167,7 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
} }
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.GET_PAGE; 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.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails; 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 * Base class for an operation that has a resource type but not a resource body in the
* request body * request body
@ -86,6 +88,7 @@ public class PatchMethodBinding extends BaseOutcomeReturningMethodBindingWithRes
return retVal; return retVal;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.PATCH; 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.rest.server.exceptions.*;
import ca.uhn.fhir.util.DateUtils; import ca.uhn.fhir.util.DateUtils;
import javax.annotation.Nonnull;
public class ReadMethodBinding extends BaseResourceReturningMethodBinding { public class ReadMethodBinding extends BaseResourceReturningMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadMethodBinding.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadMethodBinding.class);
@ -91,6 +93,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding {
return retVal; return retVal;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return isVread() ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ; 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.IBaseBinary;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -52,15 +53,17 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ResourceParameter implements IParameter { public class ResourceParameter implements IParameter {
private final boolean myMethodIsOperation;
private Mode myMode; private Mode myMode;
private Class<? extends IBaseResource> myResourceType; 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(theParameterType, "theParameterType can not be null");
Validate.notNull(theMode, "theMode can not be null"); Validate.notNull(theMode, "theMode can not be null");
myResourceType = theParameterType; myResourceType = theParameterType;
myMode = theMode; myMode = theMode;
myMethodIsOperation = theMethodIsOperation;
Class<? extends IBaseResource> providerResourceType = null; Class<? extends IBaseResource> providerResourceType = null;
if (theProvider instanceof IResourceProvider) { if (theProvider instanceof IResourceProvider) {
@ -103,11 +106,20 @@ public class ResourceParameter implements IParameter {
return RestfulServerUtils.determineRequestEncodingNoDefault(theRequest); return RestfulServerUtils.determineRequestEncodingNoDefault(theRequest);
case RESOURCE: case RESOURCE:
default: default:
return parseResourceFromRequest(theRequest, theMethodBinding, myResourceType); 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) { public static Reader createRequestReader(RequestDetails theRequest, Charset charset) {
Reader requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents()), charset); Reader requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents()), charset);
return requestReader; return requestReader;
@ -126,7 +138,7 @@ public class ResourceParameter implements IParameter {
} }
@SuppressWarnings("unchecked") @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(); FhirContext ctx = theRequest.getServer().getFhirContext();
final Charset charset = determineRequestCharset(theRequest); final Charset charset = determineRequestCharset(theRequest);
@ -139,7 +151,6 @@ public class ResourceParameter implements IParameter {
String ctValue = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); String ctValue = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
if (ctValue != null) { if (ctValue != null) {
if (ctValue.startsWith("application/x-www-form-urlencoded")) { 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()); String msg = theRequest.getServer().getFhirContext().getLocalizer().getMessage(ResourceParameter.class, "invalidContentTypeInRequest", ctValue, theMethodBinding.getRestOperationType());
throw new InvalidRequestException(msg); 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.. // This shouldn't happen since we're reading from a byte array..
throw new InternalErrorException(e); throw new InternalErrorException(e);
} }
if (isBlank(body)) {
return null;
}
encoding = EncodingEnum.detectEncodingNoDefault(body); encoding = EncodingEnum.detectEncodingNoDefault(body);
if (encoding == null) { if (encoding == null) {
String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "noContentTypeInRequest", restOperationType); String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "noContentTypeInRequest", restOperationType);
@ -187,7 +201,7 @@ public class ResourceParameter implements IParameter {
public static IBaseResource parseResourceFromRequest(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding, Class<? extends IBaseResource> theResourceType) { public static IBaseResource parseResourceFromRequest(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding, Class<? extends IBaseResource> theResourceType) {
IBaseResource retVal = null; IBaseResource retVal = null;
if (IBaseBinary.class.isAssignableFrom(theResourceType)) { if (theResourceType != null && IBaseBinary.class.isAssignableFrom(theResourceType)) {
String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
if (EncodingEnum.forContentTypeStrict(ct) == null) { if (EncodingEnum.forContentTypeStrict(ct) == null) {
FhirContext ctx = theRequest.getServer().getFhirContext(); FhirContext ctx = theRequest.getServer().getFhirContext();
@ -216,8 +230,4 @@ public class ResourceParameter implements IParameter {
return retVal; 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.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import javax.annotation.Nonnull;
public class SearchMethodBinding extends BaseResourceReturningMethodBinding { public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
@ -108,6 +110,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
return myDescription; return myDescription;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.SEARCH_TYPE; return RestOperationTypeEnum.SEARCH_TYPE;

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.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.method.TransactionParameter.ParamStyle; import ca.uhn.fhir.rest.server.method.TransactionParameter.ParamStyle;
import javax.annotation.Nonnull;
public class TransactionMethodBinding extends BaseResourceReturningMethodBinding { public class TransactionMethodBinding extends BaseResourceReturningMethodBinding {
private int myTransactionParamIndex; private int myTransactionParamIndex;
@ -73,6 +75,7 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding
} }
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.TRANSACTION; 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.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import javax.annotation.Nonnull;
public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam {
public UpdateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { public UpdateMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
@ -98,6 +100,7 @@ public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
return null; return null;
} }
@Nonnull
@Override @Override
public RestOperationTypeEnum getRestOperationType() { public RestOperationTypeEnum getRestOperationType() {
return RestOperationTypeEnum.UPDATE; 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();
}
}

View File

@ -125,6 +125,22 @@
<action type="fix"> <action type="fix">
The FhirTerser <![CDATA[<code>getValues(...)</code>]]> methods were not properly handling modifier The FhirTerser <![CDATA[<code>getValues(...)</code>]]> methods were not properly handling modifier
extensions for verions of FHIR prior to DSTU3. This has been corrected. extensions for verions of FHIR prior to DSTU3. This has been corrected.
<action type="fix">
When updating resources in the JPA server, a bug caused index table entries to be refreshed
sometimes even though the index value hadn't changed. This issue did not cause incorrect search
results but had an effect on write performance. This has been corrected.
</action>
<action type="add">
The @Operation annotation used to declare operations on the Plain Server now
has a wildcard constant which may be used for the operation name. This allows
you to create a server that supports operations that are not known to the
server when it starts up. This is generally not advisable but can be useful
for some circumstances.
</action>
<action type="add">
When using an @Operation method in the Plain Server, it is now possible
to use a parameter annotated with @ResourceParam to receive the Parameters
(or other) resource supplied by the client as the request body.
</action> </action>
</release> </release>
@ -1321,7 +1337,8 @@ ALTER TABLE hfj_res_ver ALTER COLUMN res_text DROP NOT NULL;</pre>
Michael Lawley for the pull request! Michael Lawley for the pull request!
</action> </action>
<action type="add"> <action type="add">
Add <![CDATA[<code>Prefer</code> and <code>Cache-Control</code>]]> to the list of headers which are declared as Add <![CDATA[<code>Prefer</code> and <code>Cache-Control</code>]]> to the list of headers which are declared
as
being acceptable for CORS requests in CorsInterceptor, CLI, and JPA Example. being acceptable for CORS requests in CorsInterceptor, CLI, and JPA Example.
Thanks to Patrick Werner for the pull request! Thanks to Patrick Werner for the pull request!
</action> </action>
@ -1831,7 +1848,8 @@ Bundle bundle = client.search().forResource(Patient.class)
optimize something that did not need optimizing! optimize something that did not need optimizing!
</action> </action>
<action type="add"> <action type="add">
A new config property has been added to the JPA seerver DaoConfig called "setAutoCreatePlaceholderReferenceTargets". A new config property has been added to the JPA seerver DaoConfig called
"setAutoCreatePlaceholderReferenceTargets".
This property causes references to unknown resources in created/updated resources to have a placeholder This property causes references to unknown resources in created/updated resources to have a placeholder
target resource automatically created. target resource automatically created.
</action> </action>
@ -2131,7 +2149,8 @@ Bundle bundle = client.search().forResource(Patient.class)
</action> </action>
<action type="add"> <action type="add">
Add a utility method to JPA server: Add a utility method to JPA server:
<![CDATA[<code>IFhirResourceDao#removeTag(IIdType, TagTypeEnum, String, String)</code>]]>. This allows client code to remove tags <![CDATA[<code>IFhirResourceDao#removeTag(IIdType, TagTypeEnum, String, String)</code>]]>. This allows
client code to remove tags
from a resource without having a servlet request object in context. from a resource without having a servlet request object in context.
</action> </action>
<action type="fix"> <action type="fix">
@ -2525,8 +2544,10 @@ Bundle bundle = client.search().forResource(Patient.class)
<![CDATA[<code>IHttpRequest</code>]]> class: "bufferEntitity" should be "bufferEntity". <![CDATA[<code>IHttpRequest</code>]]> class: "bufferEntitity" should be "bufferEntity".
</action> </action>
<action type="add"> <action type="add">
ErrorHandler is now called (resulting in a warning by default, but can also be an exception) when arsing JSON if ErrorHandler is now called (resulting in a warning by default, but can also be an exception) when arsing
the resource ID is not a JSON string, or an object is found where an array is expected (e.g. repeating field). Thanks JSON if
the resource ID is not a JSON string, or an object is found where an array is expected (e.g. repeating
field). Thanks
to Jenni Syed of Cerner for providing a test case! to Jenni Syed of Cerner for providing a test case!
</action> </action>
<action type="fix"> <action type="fix">
@ -2649,8 +2670,10 @@ Bundle bundle = client.search().forResource(Patient.class)
]]> ]]>
</action> </action>
<action type="fix"> <action type="fix">
Fix a fairly significant issue in JPA Server when using the <![CDATA[<code>DatabaseBackedPagingProvider</code>]]>: When paging over the results Fix a fairly significant issue in JPA Server when using the
of a search / $everything operation, under certain circumstances resources may be missing from the last page of results <![CDATA[<code>DatabaseBackedPagingProvider</code>]]>: When paging over the results
of a search / $everything operation, under certain circumstances resources may be missing from the last page
of results
that is returned. Thanks to David Hay for reporting! that is returned. Thanks to David Hay for reporting!
</action> </action>
<action type="add"> <action type="add">
@ -2805,7 +2828,8 @@ Bundle bundle = client.search().forResource(Patient.class)
Kevin Tallevi for finding this! Kevin Tallevi for finding this!
</action> </action>
<action type="fix" issue="411"> <action type="fix" issue="411">
Fix #411 - Searching by <![CDATA[<code>POST [base]/_search</code>]]> with urlencoded parameters doesn't work correctly if Fix #411 - Searching by <![CDATA[<code>POST [base]/_search</code>]]> with urlencoded parameters doesn't work
correctly if
interceptors are accessing the parameters and there is are also interceptors are accessing the parameters and there is are also
parameters on the URL. Thanks to Jim Steel for reporting! parameters on the URL. Thanks to Jim Steel for reporting!
</action> </action>
@ -2918,7 +2942,8 @@ Bundle bundle = client.search().forResource(Patient.class)
</action> </action>
<action type="add"> <action type="add">
Both client and server now support the new Content Types decided in Both client and server now support the new Content Types decided in
<![CDATA[<a href="http://gforge.hl7.org/gf/project/fhir/tracker/?action=TrackerItemEdit&tracker_id=677&tracker_item_id=10199">FHIR #10199</a>]]>. <![CDATA[<a href="http://gforge.hl7.org/gf/project/fhir/tracker/?action=TrackerItemEdit&tracker_id=677&tracker_item_id=10199">FHIR #10199</a>]]>
.
<![CDATA[<br/><br/>]]> <![CDATA[<br/><br/>]]>
This means that the server now supports This means that the server now supports
<![CDATA[<code>application/fhir+xml</code> and <code>application/fhir+json</code>]]> <![CDATA[<code>application/fhir+xml</code> and <code>application/fhir+json</code>]]>
@ -3395,7 +3420,8 @@ Bundle bundle = client.search().forResource(Patient.class)
reporting! reporting!
</action> </action>
<action type="fix" issue="371"> <action type="fix" issue="371">
Update STU3 client and server to use the new sort parameter style (param1,-param2,param). Thanks to GitHub user @euz1e4r for Update STU3 client and server to use the new sort parameter style (param1,-param2,param). Thanks to GitHub
user @euz1e4r for
reporting! reporting!
</action> </action>
<action type="fix"> <action type="fix">
@ -3738,7 +3764,8 @@ Bundle bundle = client.search().forResource(Patient.class)
REST Server responded to HTTP OPTIONS requests with REST Server responded to HTTP OPTIONS requests with
any URI as being a request for the server's any URI as being a request for the server's
Conformance statement. This is incorrect, as only Conformance statement. This is incorrect, as only
a request for <![CDATA[<code>OPTIONS [base url]</code>]]> should be treated as such. Thanks to Michael Lawley for reporting! a request for <![CDATA[<code>OPTIONS [base url]</code>]]> should be treated as such. Thanks to Michael
Lawley for reporting!
</action> </action>
<action type="fix"> <action type="fix">
REST annotation style client was not able to handle extended operations REST annotation style client was not able to handle extended operations
@ -4200,7 +4227,8 @@ Bundle bundle = client.search().forResource(Patient.class)
</action> </action>
<action type="fix"> <action type="fix">
In server, if a client request is received and it has an Accept header indicating In server, if a client request is received and it has an Accept header indicating
that it supports both XML and JSON with equal weight, the server's default is used instead of the first entry in the list. that it supports both XML and JSON with equal weight, the server's default is used instead of the first
entry in the list.
</action> </action>
<action type="add"> <action type="add">
JPA server now supports searching with sort by token, quantity, JPA server now supports searching with sort by token, quantity,
@ -4257,7 +4285,8 @@ Bundle bundle = client.search().forResource(Patient.class)
to Alexander Kley for the fix! to Alexander Kley for the fix!
</action> </action>
<action type="add"> <action type="add">
JPA server now supports $everything on Patient and Encounter types (patient and encounter instance was already supported) JPA server now supports $everything on Patient and Encounter types (patient and encounter instance was
already supported)
</action> </action>
<action type="add"> <action type="add">
Generic client operation invocations now Generic client operation invocations now
@ -4404,7 +4433,8 @@ Bundle bundle = client.search().forResource(Patient.class)
</action> </action>
<action type="fix" issue="198"> <action type="fix" issue="198">
JPA server sorting often returned unexpected orders when multiple JPA server sorting often returned unexpected orders when multiple
indexes of the same type were found on the same resource (e.g. multiple string indexed fields). Thanks to Travis Cummings for reporting! indexes of the same type were found on the same resource (e.g. multiple string indexed fields). Thanks to
Travis Cummings for reporting!
</action> </action>
<action type="add"> <action type="add">
Add another method to IServerInterceptor which converts an exception generated on the server Add another method to IServerInterceptor which converts an exception generated on the server
@ -4513,10 +4543,13 @@ Bundle bundle = client.search().forResource(Patient.class)
</action> </action>
<action type="fix"> <action type="fix">
JPA server did not correctly index search parameters JPA server did not correctly index search parameters
of type "URI". Thanks to David Hay for reporting! Note that if you are using the JPA server, this change means that of type "URI". Thanks to David Hay for reporting! Note that if you are using the JPA server, this change
there are two new tables added to the database schema. Updating existing resources in the database may fail unless you means that
there are two new tables added to the database schema. Updating existing resources in the database may fail
unless you
set default values for the resource set default values for the resource
table by issuing a SQL command similar to the following (false may be 0 or something else, depending on the database platform in use) table by issuing a SQL command similar to the following (false may be 0 or something else, depending on the
database platform in use)
<![CDATA[<br/><code>update hfj_resource set sp_coords_present = false;<br/> <![CDATA[<br/><code>update hfj_resource set sp_coords_present = false;<br/>
update hfj_resource set sp_uri_present = false;</code>]]> update hfj_resource set sp_uri_present = false;</code>]]>
</action> </action>
@ -4561,7 +4594,8 @@ Bundle bundle = client.search().forResource(Patient.class)
in history in history
</action> </action>
<action type="fix" issue="222"> <action type="fix" issue="222">
JPA server returned deleted resources in search results when using the _tag, _id, _profile, or _security search parameters JPA server returned deleted resources in search results when using the _tag, _id, _profile, or _security
search parameters
</action> </action>
<action type="fix" issue="223"> <action type="fix" issue="223">
Fix issue with build on Windows. Thanks to Bryce van Dyk for the pull request! Fix issue with build on Windows. Thanks to Bryce van Dyk for the pull request!
@ -4581,7 +4615,8 @@ Bundle bundle = client.search().forResource(Patient.class)
Claude Nanjo for finding this. Claude Nanjo for finding this.
</action> </action>
<action type="fix" issue="164"> <action type="fix" issue="164">
Correct performance issue with :missing=true search requests where the parameter is a resource link. Thanks to wanghaisheng for all his help in testing this. Correct performance issue with :missing=true search requests where the parameter is a resource link. Thanks
to wanghaisheng for all his help in testing this.
</action> </action>
<action type="fix" issue="149"> <action type="fix" issue="149">
The self link in the Bundle returned by searches on the server does not respect the The self link in the Bundle returned by searches on the server does not respect the
@ -4598,7 +4633,8 @@ Bundle bundle = client.search().forResource(Patient.class)
Peter Girard for reporting! Peter Girard for reporting!
</action> </action>
<action type="add" issue="170"> <action type="add" issue="170">
Add better addXXX() methods to structures, which take the datatype being added as a parameter. Thanks to Claude Nanjo for the Add better addXXX() methods to structures, which take the datatype being added as a parameter. Thanks to
Claude Nanjo for the
suggestion! suggestion!
</action> </action>
<action type="add" issue="152"> <action type="add" issue="152">
@ -4668,7 +4704,8 @@ Bundle bundle = client.search().forResource(Patient.class)
q values specifying order of preference. Previously the q value was ignored. q values specifying order of preference. Previously the q value was ignored.
</action> </action>
<action type="add"> <action type="add">
Server in DSTU2 mode now indicates that whether it has support for Transaction operation or not. Thanks to Kevin Paschke for pointing out that this wasn't working! Server in DSTU2 mode now indicates that whether it has support for Transaction operation or not. Thanks to
Kevin Paschke for pointing out that this wasn't working!
</action> </action>
<action type="add" issue="166"> <action type="add" issue="166">
Questionnaire.title now gets correctly indexed in JPA server (it has no path, so it is a special case) Questionnaire.title now gets correctly indexed in JPA server (it has no path, so it is a special case)
@ -4754,7 +4791,8 @@ Bundle bundle = client.search().forResource(Patient.class)
McKenzie for reporting! McKenzie for reporting!
</action> </action>
<action type="fix" issue="128"> <action type="fix" issue="128">
Fix regression in 0.9 - Server responds with an HTTP 500 and a NullPointerException instead of an HTTP 400 and a useful error message if the client requests an unknown resource type Fix regression in 0.9 - Server responds with an HTTP 500 and a NullPointerException instead of an HTTP 400
and a useful error message if the client requests an unknown resource type
</action> </action>
<action type="add"> <action type="add">
Add support for Add support for
@ -4926,7 +4964,8 @@ Bundle bundle = client.search().forResource(Patient.class)
the patch! the patch!
</action> </action>
<action type="fix"> <action type="fix">
Transaction server operations incorrectly used the "Accept" header instead of the "Content-Type" header to determine the Transaction server operations incorrectly used the "Accept" header instead of the "Content-Type" header to
determine the
POST request encoding. Thanks to Rene Spronk for providing a test case! POST request encoding. Thanks to Rene Spronk for providing a test case!
</action> </action>
</release> </release>
@ -5042,7 +5081,8 @@ Bundle bundle = client.search().forResource(Patient.class)
</action> </action>
<action type="fix"> <action type="fix">
Server requests for Binary resources where the client has explicitly requested XML or JSON responses Server requests for Binary resources where the client has explicitly requested XML or JSON responses
(either with a <![CDATA[<code>_format</code>]]> URL parameter, or an <![CDATA[<code>Accept</code>]]> request header) (either with a <![CDATA[<code>_format</code>]]> URL parameter, or an <![CDATA[<code>Accept</code>]]> request
header)
will be responded to using the Binary FHIR resource type instead of as Binary blobs. This is will be responded to using the Binary FHIR resource type instead of as Binary blobs. This is
in accordance with the recommended behaviour in the FHIR specification. in accordance with the recommended behaviour in the FHIR specification.
</action> </action>
@ -5080,7 +5120,8 @@ Bundle bundle = client.search().forResource(Patient.class)
to baopingle for reporting and providing a test case! to baopingle for reporting and providing a test case!
</action> </action>
<action type="add"> <action type="add">
Sorting is now supported in the Web Testing UI (previously a button existed for sorting, but it didn't do anything) Sorting is now supported in the Web Testing UI (previously a button existed for sorting, but it didn't do
anything)
</action> </action>
<action type="add" issue="111"> <action type="add" issue="111">
Server will no longer include stack traces in the OperationOutcome returned to the client Server will no longer include stack traces in the OperationOutcome returned to the client
@ -5354,7 +5395,8 @@ Bundle bundle = client.search().forResource(Patient.class)
for reporting this! for reporting this!
</action> </action>
<action type="fix"> <action type="fix">
XHTML (in narratives) containing escapable characters (e.g. &lt; or &quot;) will now always have those characters XHTML (in narratives) containing escapable characters (e.g. &lt; or &quot;) will now always have those
characters
escaped properly in encoded messages. escaped properly in encoded messages.
</action> </action>
<action type="fix"> <action type="fix">
@ -5404,7 +5446,8 @@ Bundle bundle = client.search().forResource(Patient.class)
Thanks to Bill de Beaubien for reporting! Thanks to Bill de Beaubien for reporting!
</action> </action>
<action type="update"> <action type="update">
Documentation on contained resources contained a typo and did not actually produce contained resources. Thanks Documentation on contained resources contained a typo and did not actually produce contained resources.
Thanks
to David Hay of Orion Health for reporting! to David Hay of Orion Health for reporting!
</action> </action>
<action type="add" issue="31" dev="preston"> <action type="add" issue="31" dev="preston">
@ -5423,7 +5466,8 @@ Bundle bundle = client.search().forResource(Patient.class)
Petro Mykhailysyn for the pull request! Petro Mykhailysyn for the pull request!
</action> </action>
</release> </release>
<release version="0.6" date="2014-09-08" description="This release brings a number of new features and bug fixes!"> <release version="0.6" date="2014-09-08"
description="This release brings a number of new features and bug fixes!">
<!-- <!--
<action type="add"> <action type="add">
Allow generic client ... OAUTH Allow generic client ... OAUTH
@ -5522,13 +5566,15 @@ Bundle bundle = client.search().forResource(Patient.class)
Rename NotImpementedException to NotImplementedException (to correct typo) Rename NotImpementedException to NotImplementedException (to correct typo)
</action> </action>
<action type="fix"> <action type="fix">
Server setUseBrowserFriendlyContentType setting also respected for errors (e.g. OperationOutcome with 4xx/5xx status) Server setUseBrowserFriendlyContentType setting also respected for errors (e.g. OperationOutcome with
4xx/5xx status)
</action> </action>
<action type="fix"> <action type="fix">
Fix performance issue in date/time datatypes where pattern matchers were not static Fix performance issue in date/time datatypes where pattern matchers were not static
</action> </action>
<action type="fix"> <action type="fix">
Server now gives a more helpful error message if a @Read method has a search parameter (which is invalid, but Server now gives a more helpful error message if a @Read method has a search parameter (which is invalid,
but
previously lead to a very unhelpful error message). Thanks to Tahura Chaudhry of UHN for reporting! previously lead to a very unhelpful error message). Thanks to Tahura Chaudhry of UHN for reporting!
</action> </action>
<action type="fix"> <action type="fix">
@ -5601,7 +5647,8 @@ Bundle bundle = client.search().forResource(Patient.class)
for configurable logging, capturing requests and responses, and HTTP basic auth. for configurable logging, capturing requests and responses, and HTTP basic auth.
</action> </action>
<action type="fix"> <action type="fix">
Transaction client invocations with XML encoding were using the wrong content type ("application/xml+fhir" instead Transaction client invocations with XML encoding were using the wrong content type ("application/xml+fhir"
instead
of the correct "application/atom+xml"). Thanks to David Hay of Orion Health for surfacing this one! of the correct "application/atom+xml"). Thanks to David Hay of Orion Health for surfacing this one!
</action> </action>
<action type="add"> <action type="add">
@ -5680,7 +5727,8 @@ Bundle bundle = client.search().forResource(Patient.class)
Support for Query resources fixed (in parser) Support for Query resources fixed (in parser)
</action> </action>
<action type="fix"> <action type="fix">
Nested contained resources (e.g. encoding a resource with a contained resource that itself contains a resource) Nested contained resources (e.g. encoding a resource with a contained resource that itself contains a
resource)
now parse and encode correctly, meaning that all contained resources are placed in the "contained" element now parse and encode correctly, meaning that all contained resources are placed in the "contained" element
of the root resource, and the parser looks in the root resource for all container levels when stitching of the root resource, and the parser looks in the root resource for all container levels when stitching
contained resources back together. contained resources back together.
@ -5713,7 +5761,8 @@ Bundle bundle = client.search().forResource(Patient.class)
Support added for deleted-entry by/name, by/email, and comment from Tombstones spec Support added for deleted-entry by/name, by/email, and comment from Tombstones spec
</action> </action>
</release> </release>
<release version="0.3" date="2014-05-12" description="This release corrects lots of bugs and introduces the fluent client mode"> <release version="0.3" date="2014-05-12"
description="This release corrects lots of bugs and introduces the fluent client mode">
</release> </release>
<release version="0.2" date="2014-04-23"> <release version="0.2" date="2014-04-23">
</release> </release>