Switch JPA server to use new include style

This commit is contained in:
James Agnew 2015-03-22 13:04:52 +01:00
parent 7adf48a38a
commit 596dd664f9
30 changed files with 481 additions and 81 deletions

View File

@ -236,7 +236,7 @@ public class GenericClientExample {
.encodedJson()
.where(Patient.BIRTHDATE.beforeOrEquals().day("2012-01-22"))
.and(Patient.BIRTHDATE.after().day("2011-01-01"))
.include(Patient.INCLUDE_MANAGINGORGANIZATION)
.include(Patient.INCLUDE_ORGANIZATION)
.sort().ascending(Patient.BIRTHDATE)
.sort().descending(Patient.NAME).limitTo(123)
.execute();

View File

@ -599,6 +599,22 @@ public List<DiagnosticReport> getDiagnosticReport(
}
//END SNIPPET: pathSpec
//START SNIPPET: revInclude
@Search()
public List<DiagnosticReport> getDiagnosticReport(
@RequiredParam(name=DiagnosticReport.SP_IDENTIFIER)
TokenParam theIdentifier,
@IncludeParam()
Set<Include> theIncludes,
@IncludeParam(reverse=true)
Set<Include> theReverseIncludes
) {
return new ArrayList<DiagnosticReport>(); // populate this
}
//END SNIPPET: revInclude
//START SNIPPET: pathSpecSimple
@Search()

View File

@ -25,29 +25,38 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import ca.uhn.fhir.model.api.Include;
/**
* Method parameter which is used to indicate a parameter that will
* be populated with the "_include" values for a search param.
* be populated with the "_include" (or "_revinclude") values for a search param.
* The parameter annotated with this annotation is used for either "_include"
* or "_revinclude", depending on whether {@link #reverse()} has been
* set to <code>true</code> (default is <code>false</code>).
*
* <p>
* Only one parameter may be annotated with this annotation, and that
* Only up to two parameters may be annotated with this annotation (one each
* for <code>reverse=false</code> and <code>reverse=true</code>. That
* parameter should be one of the following:
* </p>
* <ul>
* <li><code>Collection&lt;PathSpecification&gt;</code></li>
* <li><code>List&lt;PathSpecification&gt;</code></li>
* <li><code>Set&lt;PathSpecification&gt;</code></li>
* <li><code>Collection&lt;Include&gt;</code></li>
* <li><code>List&lt;Include&gt;</code></li>
* <li><code>Set&lt;Include&gt;</code></li>
* </ul>
*
* @see Include
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value= {ElementType.PARAMETER})
public @interface IncludeParam {
/**
* Optional parameter, if provided the server will only allow the values
* Optional value, if provided the server will only allow the values
* within the given set. If an _include parameter is passed to the server
* which does not match any allowed values the server will return an error.
* <p>
* Values for this parameter takew the form that the FHIR specification
* Values for this parameter take the form that the FHIR specification
* defines for <code>_include</code> values, namely <code>[Resource Name].[path]</code>.
* For example: <code>"Patient.link.other"</code>
* or <code>"Encounter.partOf"</code>
@ -69,4 +78,11 @@ public @interface IncludeParam {
*/
String[] allow() default {};
/**
* If set to <code>true</code> (default is <code>false</code>), the values
* for this parameter correspond to the <code>_revinclude<code> parameter
* instead of the <code>_include<code> parameter.
*/
boolean reverse() default false;
}

View File

@ -65,4 +65,10 @@ public @interface Operation {
* </p>
*/
boolean idempotent() default false;
/**
* This parameter may be used to specify the parts which will be found in the
* response to this operation.
*/
OperationParam[] returnParameters() default {};
}

View File

@ -25,6 +25,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.hl7.fhir.instance.model.IBase;
/**
*/
@Retention(RetentionPolicy.RUNTIME)
@ -36,4 +38,10 @@ public @interface OperationParam {
*/
String name();
/**
* The type of the parameter. This will only have effect on <code>@OperationParam</code>
* annotations specified as values for {@link Operation#returnParameters()}
*/
Class<? extends IBase> type() default IBase.class;
}

View File

@ -1323,6 +1323,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private String myCompartmentName;
private CriterionList myCriterion = new CriterionList();
private List<Include> myInclude = new ArrayList<Include>();
private List<Include> myRevInclude = new ArrayList<Include>();
private Integer myParamLimit;
private String myResourceId;
private String myResourceName;
@ -1356,6 +1357,10 @@ public class GenericClient extends BaseClient implements IGenericClient {
addParam(params, Constants.PARAM_INCLUDE, next.getValue());
}
for (Include next : myRevInclude) {
addParam(params, Constants.PARAM_REVINCLUDE, next.getValue());
}
for (SortInternal next : mySort) {
addParam(params, next.getParamName(), next.getParamValue());
}
@ -1444,6 +1449,12 @@ public class GenericClient extends BaseClient implements IGenericClient {
return this;
}
@Override
public IQuery revinclude(Include theInclude) {
myRevInclude.add(theInclude);
return this;
}
}
private static class SortInternal implements ISort {

View File

@ -26,7 +26,10 @@ import ca.uhn.fhir.rest.method.SearchStyleEnum;
public interface IQuery extends IClientExecutable<IQuery, Bundle>, IBaseQuery<IQuery> {
IQuery include(Include theIncludeManagingorganization);
/**
* Add an "_include" specification
*/
IQuery include(Include theInclude);
ISort sort();
@ -42,5 +45,12 @@ public interface IQuery extends IClientExecutable<IQuery, Bundle>, IBaseQuery<IQ
IQuery usingStyle(SearchStyleEnum theStyle);
IQuery withIdAndCompartment(String theResourceId, String theCompartmentName);
/**
* Add a "_revinclude" specification
*
* @since 1.0
*/
IQuery revinclude(Include theIncludeTarget);
}

View File

@ -43,9 +43,11 @@ class IncludeParameter extends BaseQueryParameter {
private Set<String> myAllow;
private Class<? extends Collection<Include>> myInstantiableCollectionType;
private Class<?> mySpecType;
private boolean myReverse;
public IncludeParameter(IncludeParam theAnnotation, Class<? extends Collection<Include>> theInstantiableCollectionType, Class<?> theSpecType) {
myInstantiableCollectionType = theInstantiableCollectionType;
myReverse = theAnnotation.reverse();
if (theAnnotation.allow().length > 0) {
myAllow = new HashSet<String>();
for (String next : theAnnotation.allow()) {
@ -93,7 +95,7 @@ class IncludeParameter extends BaseQueryParameter {
@Override
public String getName() {
return "_include";
return myReverse ? "_revinclude" : "_include";
}
@Override
@ -131,10 +133,10 @@ class IncludeParameter extends BaseQueryParameter {
}
String value = nextParamList.get(0);
if (myAllow != null) {
if (myAllow != null && !myAllow.isEmpty()) {
if (!myAllow.contains(value)) {
if (!myAllow.contains("*")) {
String msg = theContext.getLocalizer().getMessage(IncludeParameter.class, "invalidIncludeNameInRequest", value, new TreeSet<String>(myAllow).toString());
String msg = theContext.getLocalizer().getMessage(IncludeParameter.class, "invalidIncludeNameInRequest", value, new TreeSet<String>(myAllow).toString(), getName());
throw new InvalidRequestException(msg);
}
}

View File

@ -90,6 +90,7 @@ public class Constants {
public static final String PARAM_FORMAT = "_format";
public static final String PARAM_HISTORY = "_history";
public static final String PARAM_INCLUDE = "_include";
public static final String PARAM_REVINCLUDE = "_revinclude";
public static final String PARAM_NARRATIVE = "_narrative";
public static final String PARAM_PAGINGACTION = "_getpages";
public static final String PARAM_PAGINGOFFSET = "_getpagesoffset";

View File

@ -14,7 +14,7 @@ ca.uhn.fhir.rest.client.RestfulClientFactory.wrongVersionInConformance=The serve
ca.uhn.fhir.rest.method.OperationMethodBinding.methodNotSupported=HTTP Method {0} is not allowed for this operation. Allowed method(s): {1}
ca.uhn.fhir.rest.method.OperationParamBinder.urlParamNotPrimitive=Can not invoke operation {0} using HTTP GET because parameter {1} is not a primitive datatype
ca.uhn.fhir.rest.method.IncludeParameter.invalidIncludeNameInRequest=Invalid _include parameter value: "{0}". Valid values are: {1}
ca.uhn.fhir.rest.method.IncludeParameter.invalidIncludeNameInRequest=Invalid {2} parameter value: "{0}". Valid values are: {1}
ca.uhn.fhir.rest.method.IncludeParameter.orIncludeInRequest='OR' query parameters (values containing ',') are not supported in _include parameters
ca.uhn.fhir.rest.method.SearchMethodBinding.invalidSpecialParamName=Method [{0}] in provider [{1}] contains search parameter annotated to use name [{2}] - This name is reserved according to the FHIR specification and can not be used as a search parameter name.
@ -36,7 +36,7 @@ ca.uhn.fhir.jpa.dao.BaseFhirSystemDao.transactionEntryHasInvalidVerb=Transaction
ca.uhn.fhir.jpa.dao.BaseFhirSystemDao.transactionMissingUrl=Unable to perform {0}, no URL provided.
ca.uhn.fhir.jpa.dao.BaseFhirSystemDao.transactionInvalidUrl=Unable to perform {0}, URL provided is invalid: {1}
ca.uhn.fhir.jpa.dao.FhirResourceDao.duplicateCreateForcedId=Can not create entity with ID[{0}], a resource with this ID already exists
ca.uhn.fhir.jpa.dao.FhirResourceDao.failedToCreateWithClientAssignedNumericId=Can not create resource with ID[{0}], no resource with this ID exists and clients may only assign IDs which begin with a non-numeric character on this server
ca.uhn.fhir.jpa.dao.FhirResourceDao.failedToCreateWithClientAssignedId=Can not create resource with ID[{0}], ID must not be supplied on a create (POST) operation
ca.uhn.fhir.jpa.dao.FhirResourceDao.unableToDeleteNotFound=Unable to find resource matching URL "{0}". Deletion failed.
ca.uhn.fhir.jpa.dao.BaseFhirResourceDao.duplicateCreateForcedId=Can not create entity with ID[{0}], a resource with this ID already exists
ca.uhn.fhir.jpa.dao.BaseFhirResourceDao.failedToCreateWithClientAssignedNumericId=Can not create resource with ID[{0}], no resource with this ID exists and clients may only assign IDs which begin with a non-numeric character on this server
ca.uhn.fhir.jpa.dao.BaseFhirResourceDao.failedToCreateWithClientAssignedId=Can not create resource with ID[{0}], ID must not be supplied on a create (POST) operation
ca.uhn.fhir.jpa.dao.BaseFhirResourceDao.unableToDeleteNotFound=Unable to find resource matching URL "{0}". Deletion failed.

View File

@ -125,9 +125,9 @@ import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ObjectUtil;
@Transactional(propagation = Propagation.REQUIRED)
public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements IFhirResourceDao<T> {
public abstract class BaseFhirResourceDao<T extends IResource> extends BaseFhirDao implements IFhirResourceDao<T> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDao.class);
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseFhirResourceDao.class);
@PersistenceContext()
private EntityManager myEntityManager;
@ -703,10 +703,10 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
if (isNotBlank(theResource.getId().getIdPart())) {
if (getContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU1)) {
if (theResource.getId().isIdPartValidLong()) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart()));
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart()));
}
} else {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getId().getIdPart()));
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getId().getIdPart()));
}
}
@ -740,7 +740,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
if (entity.getForcedId() != null) {
try {
translateForcedIdToPid(theResource.getId());
throw new UnprocessableEntityException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "duplicateCreateForcedId", theResource.getId().getIdPart()));
throw new UnprocessableEntityException(getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "duplicateCreateForcedId", theResource.getId().getIdPart()));
} catch (ResourceNotFoundException e) {
// good, this ID doesn't exist so we can create it
}
@ -963,7 +963,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
Set<Long> resource = processMatchUrl(theUrl, myResourceType);
if (resource.isEmpty()) {
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "unableToDeleteNotFound", theUrl));
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "unableToDeleteNotFound", theUrl));
} else if (resource.size() > 1) {
throw new ResourceNotFoundException(getContext().getLocalizer().getMessage(BaseFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resource.size()));
}
@ -1367,15 +1367,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
for (Include next : theParams.getIncludes()) {
for (IResource nextResource : resources) {
RuntimeResourceDefinition def = getContext().getResourceDefinition(nextResource);
List<Object> values;
if ("*".equals(next.getValue())) {
values = new ArrayList<Object>();
values.addAll(t.getAllPopulatedChildElementsOfType(nextResource, BaseResourceReferenceDt.class));
} else if (next.getValue().startsWith(def.getName() + ".")) {
values = t.getValues(nextResource, next.getValue());
} else {
continue;
}
List<Object> values = getIncludeValues(t, next, nextResource, def);
for (Object object : values) {
if (object == null) {
@ -1420,6 +1412,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return retVal;
}
});
}
@ -1439,6 +1432,10 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return search(Collections.singletonMap(theParameterName, theValue));
}
protected abstract List<Object> getIncludeValues(FhirTerser theTerser, Include theInclude, IResource theResource, RuntimeResourceDefinition theResourceDef);
@Override
public Set<Long> searchForIds(Map<String, IQueryParameterType> theParams) {
SearchParameterMap map = new SearchParameterMap();
@ -1682,7 +1679,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
entity = readEntityLatestVersion(resourceId);
} catch (ResourceNotFoundException e) {
if (Character.isDigit(theResource.getId().getIdPart().charAt(0))) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(FhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart()));
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getId().getIdPart()));
}
return doCreate(theResource, null, true);
}

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.jpa.dao;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
import ca.uhn.fhir.util.FhirTerser;
public class FhirResourceDaoDstu1<T extends IResource> extends BaseFhirResourceDao<T> {
protected List<Object> getIncludeValues(FhirTerser t, Include next, IResource nextResource, RuntimeResourceDefinition def) {
List<Object> values;
if ("*".equals(next.getValue())) {
values = new ArrayList<Object>();
values.addAll(t.getAllPopulatedChildElementsOfType(nextResource, BaseResourceReferenceDt.class));
} else if (next.getValue().startsWith(def.getName() + ".")) {
values = t.getValues(nextResource, next.getValue());
} else {
values = Collections.emptyList();
}
return values;
}
}

View File

@ -0,0 +1,37 @@
package ca.uhn.fhir.jpa.dao;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt;
import ca.uhn.fhir.util.FhirTerser;
public class FhirResourceDaoDstu2<T extends IResource> extends BaseFhirResourceDao<T> {
protected List<Object> getIncludeValues(FhirTerser theTerser, Include theInclude, IResource theResource, RuntimeResourceDefinition theResourceDef) {
List<Object> values;
if ("*".equals(theInclude.getValue())) {
values = new ArrayList<Object>();
values.addAll(theTerser.getAllPopulatedChildElementsOfType(theResource, BaseResourceReferenceDt.class));
} else if (theInclude.getValue().startsWith(theResourceDef.getName() + ":")) {
values = new ArrayList<Object>();
RuntimeSearchParam sp = theResourceDef.getSearchParam(theInclude.getValue().substring(theInclude.getValue().indexOf(':')+1));
StringTokenizer tok = new StringTokenizer(sp.getPath(), "|");
while (tok.hasMoreElements()) {
String nextPath = tok.nextToken().trim();
values.addAll(theTerser.getValues(theResource, nextPath));
}
} else {
values = Collections.emptyList();
}
return values;
}
}

View File

@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Create;
@ -96,24 +97,48 @@ public class JpaResourceProviderDstu2<T extends IResource> extends BaseJpaResour
}
}
@Operation(name="$meta", idempotent=true)
public MetaDt meta() {
return getDao().metaGetOperation();
//@formatter:off
@Operation(name="$meta", idempotent=true, returnParameters= {
@OperationParam(name="return", type=MetaDt.class)
})
//@formatter:on
public Parameters meta() {
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaGetOperation());
return parameters;
}
@Operation(name="$meta", idempotent=true)
public MetaDt meta(@IdParam IdDt theId) {
return getDao().metaGetOperation(theId);
//@formatter:off
@Operation(name="$meta", idempotent=true, returnParameters= {
@OperationParam(name="return", type=MetaDt.class)
})
//@formatter:on
public Parameters meta(@IdParam IdDt theId) {
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaGetOperation(theId));
return parameters;
}
@Operation(name="$meta-add", idempotent=true)
public MetaDt metaAdd(@IdParam IdDt theId, @OperationParam(name="meta") MetaDt theMeta) {
return getDao().metaAddOperation(theId, theMeta);
//@formatter:off
@Operation(name="$meta-add", idempotent=true, returnParameters= {
@OperationParam(name="return", type=MetaDt.class)
})
//@formatter:on
public Parameters metaAdd(@IdParam IdDt theId, @OperationParam(name="meta") MetaDt theMeta) {
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaAddOperation(theId, theMeta));
return parameters;
}
@Operation(name="$meta-delete", idempotent=true)
public MetaDt metaDelete(@IdParam IdDt theId, @OperationParam(name="meta") MetaDt theMeta) {
return getDao().metaDeleteOperation(theId, theMeta);
//@formatter:off
@Operation(name="$meta-delete", idempotent=true, returnParameters= {
@OperationParam(name="return", type=MetaDt.class)
})
//@formatter:on
public Parameters metaDelete(@IdParam IdDt theId, @OperationParam(name="meta") MetaDt theMeta) {
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta));
return parameters;
}
}

View File

@ -24,15 +24,23 @@ import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.model.dstu2.composite.MetaDt;
import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.dstu2.resource.Parameters;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.Transaction;
import ca.uhn.fhir.rest.annotation.TransactionParam;
public class JpaSystemProviderDstu2 extends BaseJpaSystemProvider<Bundle> {
@Operation(name="$meta", idempotent=true)
public MetaDt operation() {
return getDao().metaGetOperation();
//@formatter:off
@Operation(name="$meta", idempotent=true, returnParameters= {
@OperationParam(name="return", type=MetaDt.class)
})
//@formatter:on
public Parameters operation() {
Parameters parameters = new Parameters();
parameters.addParameter().setName("return").setValue(getDao().metaGetOperation());
return parameters;
}
@Transaction

View File

@ -1399,7 +1399,7 @@ public class FhirResourceDaoDstu2Test {
// Named include
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringDt("Tester_testSearchWithIncludes_P1"));
params.addInclude(Patient.INCLUDE_MANAGINGORGANIZATION);
params.addInclude(Patient.INCLUDE_ORGANIZATION);
IBundleProvider search = ourPatientDao.search(params);
List<IResource> patients = toList(search);
assertEquals(2, patients.size());
@ -1410,7 +1410,7 @@ public class FhirResourceDaoDstu2Test {
// Named include with parent
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringDt("Tester_testSearchWithIncludes_P1"));
params.addInclude(Patient.INCLUDE_MANAGINGORGANIZATION);
params.addInclude(Patient.INCLUDE_ORGANIZATION);
params.addInclude(Organization.INCLUDE_PARTOF);
IBundleProvider search = ourPatientDao.search(params);
List<IResource> patients = toList(search);
@ -1470,7 +1470,7 @@ public class FhirResourceDaoDstu2Test {
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringDt("Tester_testSearchWithIncludesThatHaveTextId_P1"));
params.addInclude(Patient.INCLUDE_MANAGINGORGANIZATION);
params.addInclude(Patient.INCLUDE_ORGANIZATION);
IBundleProvider search = ourPatientDao.search(params);
List<IResource> patients = toList(search);
assertEquals(2, patients.size());

View File

@ -284,7 +284,7 @@ public class ResourceProviderDstu2Test {
.search()
.forResource(Patient.class)
.where(Patient.IDENTIFIER.exactly().systemAndIdentifier("urn:system:rpdstu2","testSearchWithInclude02"))
.include(Patient.INCLUDE_MANAGINGORGANIZATION)
.include(Patient.INCLUDE_ORGANIZATION)
.prettyPrint()
.execute();
//@formatter:on
@ -443,7 +443,7 @@ public class ResourceProviderDstu2Test {
Bundle res = ourClient.search()
.forResource(Encounter.class)
.where(Encounter.IDENTIFIER.exactly().systemAndCode("urn:foo", "testDeepChainingE1"))
.include(Encounter.INCLUDE_LOCATION_LOCATION)
.include(Encounter.INCLUDE_LOCATION)
.include(Location.INCLUDE_PARTOF)
.execute();
//@formatter:on

View File

@ -44,6 +44,7 @@ import ca.uhn.fhir.model.dstu.resource.Observation;
import ca.uhn.fhir.model.dstu.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.resource.Provenance;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.primitive.UriDt;
@ -668,6 +669,34 @@ public class GenericClientTest {
}
@SuppressWarnings("unused")
@Test
public void testSearchWithReverseInclude() throws Exception {
String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
//@formatter:off
Bundle response = client.search()
.forResource(Patient.class)
.encodedJson()
.revinclude(Provenance.INCLUDE_TARGET)
.execute();
//@formatter:on
assertEquals(
"http://example.com/fhir/Patient?_revinclude=Provenance.target&_format=json",
capt.getValue().getURI().toString());
}
@Test
public void testHistory() throws Exception {

View File

@ -60,6 +60,35 @@ public class GenericClientTestDstu2 {
myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
}
@SuppressWarnings("unused")
@Test
public void testSearchWithReverseInclude() throws Exception {
String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
//@formatter:off
Bundle response = client.search()
.forResource(Patient.class)
.encodedJson()
.revinclude(ca.uhn.fhir.model.dstu2.resource.Provenance.INCLUDE_TARGET)
.execute();
//@formatter:on
assertEquals(
"http://example.com/fhir/Patient?_revinclude=Provenance%3Atarget&_format=json",
capt.getValue().getURI().toString());
}
private String getPatientFeedWithOneResult() {
//@formatter:off
String msg = "<Bundle xmlns=\"http://hl7.org/fhir\">\n" +

View File

@ -0,0 +1,128 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
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.hamcrest.Matchers;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.util.PortUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class IncludeAndRevincludeParameterTest {
private static CloseableHttpClient ourClient;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(IncludeAndRevincludeParameterTest.class);
private static int ourPort;
private static Server ourServer;
private static FhirContext ourCtx;
private static Set<Include> ourIncludes;
private static Set<Include> ourReverseIncludes;
@Before
public void before() {
ourIncludes = null;
ourReverseIncludes = null;
}
@Test
public void testNoIncludes() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=normalInclude");
HttpResponse status = ourClient.execute(httpGet);
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(ourIncludes, hasSize(0));
assertThat(ourReverseIncludes, hasSize(0));
}
@Test
public void testWithBoth() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=normalInclude&_include=A.a&_include=B.b&_revinclude=C.c&_revinclude=D.d");
HttpResponse status = ourClient.execute(httpGet);
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(ourIncludes, hasSize(2));
assertThat(ourReverseIncludes, hasSize(2));
assertThat(ourIncludes, containsInAnyOrder(new Include("A.a"), new Include("B.b")));
assertThat(ourReverseIncludes, containsInAnyOrder(new Include("C.c"), new Include("D.d")));
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourCtx = new FhirContext();
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer();
servlet.setFhirContext(ourCtx);
servlet.setResourceProviders(new DummyPatientResourceProvider());
servlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
}
public static class DummyPatientResourceProvider implements IResourceProvider {
@Search(queryName = "normalInclude")
public List<Patient> normalInclude(
@IncludeParam() Set<Include> theIncludes,
@IncludeParam(reverse=true) Set<Include> theRevincludes
) {
ourIncludes = theIncludes;
ourReverseIncludes = theRevincludes;
return new ArrayList<Patient>();
}
@Override
public Class<? extends IResource> getResourceType() {
return Patient.class;
}
}
}

View File

@ -49,10 +49,10 @@ import ca.uhn.fhir.util.PortUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class IncludeTest {
public class IncludeDstu2Test {
private static CloseableHttpClient ourClient;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(IncludeTest.class);
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(IncludeDstu2Test.class);
private static int ourPort;
private static Server ourServer;
private static FhirContext ourCtx;

View File

@ -42,7 +42,6 @@ public class ValueSetGenerator {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValueSetGenerator.class);
private List<ValueSetFileDefinition> myResourceValueSetFiles;
private Set<ValueSetTm> myMarkedValueSets = new HashSet<ValueSetTm>();
private Map<String, ValueSetTm> myValueSets = new HashMap<String, ValueSetTm>();
private int myValueSetCount;
private int myConceptCount;

View File

@ -56,7 +56,7 @@ public abstract class BaseRootType extends BaseElement {
return retVal;
}
public void addSearchParameter(SearchParameter theParam) {
public void addSearchParameter(String theVersion, SearchParameter theParam) {
getSearchParameters();
mySearchParameters.add(theParam);

View File

@ -9,16 +9,21 @@ import org.apache.commons.lang3.StringUtils;
public class SearchParameter {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameter.class);
private List<String> myCompositeOf;
private List<String> myCompositeTypes;
private String myDescription;
private String myName;
private String myPath;
private String myResourceName;
private List<String> myTargetTypes;
private String myType;
private List<String> myCompositeOf;
private List<String> myCompositeTypes;
public SearchParameter() {
private String myVersion;
public SearchParameter(String theVersion, String theResourceName) {
this.myVersion = theVersion;
this.myResourceName = theResourceName;
}
public List<String> getCompositeOf() {
@ -65,8 +70,17 @@ public class SearchParameter {
public List<Include> getPaths() {
ArrayList<Include> retVal = new ArrayList<Include>();
for (String next : getPath().split("\\s*\\|\\s*")) {
retVal.add(new Include(next));
if ("dstu".equals(myVersion)) {
for (String next : getPath().split("\\s*\\|\\s*")) {
retVal.add(new Include(next));
}
} else {
if (myType == null) {
ourLog.warn("Search parameter {} has no type", myName);
}
if ("resource".equals(myType) || "reference".equals(myType)) {
retVal.add(new Include(myResourceName + ":" + myName));
}
}
return retVal;
}
@ -100,8 +114,8 @@ public class SearchParameter {
public void setName(String theName) {
if (theName != null && Character.isUpperCase(theName.charAt(0))) {
myName = theName.substring(theName.indexOf('.')+1);
}else {
myName = theName.substring(theName.indexOf('.') + 1);
} else {
myName = theName;
}
}
@ -118,7 +132,7 @@ public class SearchParameter {
myType = theType;
}
public static class Include implements Comparable<Include>{
public static class Include implements Comparable<Include> {
private String myPath;
public Include(String thePath) {
@ -126,11 +140,8 @@ public class SearchParameter {
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((myPath == null) ? 0 : myPath.hashCode());
return result;
public int compareTo(Include theO) {
return myPath.compareTo(theO.myPath);
}
@Override
@ -153,7 +164,8 @@ public class SearchParameter {
public String getIncludeName() {
String retVal = myPath;
retVal = retVal.substring(retVal.indexOf('.') + 1);
retVal = retVal.toUpperCase().replace('.', '_').replace("[X]", "");
retVal = retVal.substring(retVal.indexOf(':') + 1);
retVal = retVal.toUpperCase().replace('.', '_').replace("[X]", "").replace("-", "_");
return retVal;
}
@ -164,8 +176,11 @@ public class SearchParameter {
}
@Override
public int compareTo(Include theO) {
return myPath.compareTo(theO.myPath);
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((myPath == null) ? 0 : myPath.hashCode());
return result;
}
}

View File

@ -216,7 +216,7 @@ public abstract class BaseStructureSpreadsheetParser extends BaseStructureParser
for (int j = 1; j < rows.getLength(); j++) {
Element nextRow = (Element) rows.item(j);
SearchParameter sp = new SearchParameter();
SearchParameter sp = new SearchParameter(getVersion(), theResource.getName());
sp.setName(cellValue(nextRow, colName));
sp.setDescription(cellValue(nextRow, colDesc));
@ -227,7 +227,7 @@ public abstract class BaseStructureSpreadsheetParser extends BaseStructureParser
if (sp.getType().equals("composite")) {
compositeParams.add(sp);
} else {
theResource.addSearchParameter(sp);
theResource.addSearchParameter(getVersion(), sp);
}
}
}
@ -290,8 +290,8 @@ public abstract class BaseStructureSpreadsheetParser extends BaseStructureParser
for (SearchParameter part1 : compositeOf.get(0)) {
for (SearchParameter part2 : compositeOf.get(1)) {
SearchParameter composite = new SearchParameter();
theResource.addSearchParameter(composite);
SearchParameter composite = new SearchParameter(getVersion(), theResource.getName());
theResource.addSearchParameter(getVersion(), composite);
composite.setName(part1.getName() + "-" + part2.getName());
composite.setDescription(nextCompositeParam.getDescription());
composite.setPath(nextCompositeParam.getPath());

View File

@ -145,7 +145,7 @@ public class ProfileParser extends BaseStructureParser {
}
for (StructureSearchParam nextParam : nextStructure.getSearchParam()) {
SearchParameter param = new SearchParameter();
SearchParameter param = new SearchParameter(getVersion(), retVal.getName());
param.setName(nextParam.getName().getValue());
String path = defaultString(nextParam.getXpath().getValue());
@ -154,7 +154,7 @@ public class ProfileParser extends BaseStructureParser {
param.setType(nextParam.getType().getValue());
param.setDescription(nextParam.getDocumentation().getValue());
retVal.addSearchParameter(param);
retVal.addSearchParameter(getVersion(), param);
}
addResource(retVal);

View File

@ -66,8 +66,14 @@ public class ${className}ResourceProvider extends JpaResourceProvider${versionCa
@IncludeParam(allow= {
#foreach ( $param in $searchParamsReference )
#set ( $haveMore = $foreach.hasNext )
#foreach ( $include in $param.paths )
#if ( $version == 'dstu' )
#foreach ( $include in $param.paths )
"${include.path}" #{if}($foreach.hasNext || $haveMore), #{end}
#end
#else
#foreach ( $include in $includes )
"${include.path}" #{if}($foreach.hasNext || $haveMore), #{end}
#end
#end
#end
#{if}($searchParamsReference.empty == false), #{end}"*"

View File

@ -27,7 +27,7 @@
</util:list>
#foreach ( $res in $resources )
<bean id="my${res.name}Dao${versionCapitalized}" class="ca.uhn.fhir.jpa.dao.FhirResourceDao">
<bean id="my${res.name}Dao${versionCapitalized}" class="ca.uhn.fhir.jpa.dao.FhirResourceDao${versionCapitalized}">
<property name="resourceType" value="ca.uhn.fhir.model.${version}.resource.${res.declaringClassNameComplete}"/>
<property name="context" ref="myFhirContext${versionCapitalized}"/>
</bean>

View File

@ -38,6 +38,18 @@
<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
</action>
<action type="add">
Add support for
<![CDATA[<code>_revinclude</code>]]>
parameter in client, server, and JPA.
</action>
<action type="add">
Include constants on resources (such as
<![CDATA[<code>Observation.INCLUDE_VALUE_STRING</code>]]>)
have been switched in the DSTU2 structures to use
the new syntax required in DSTU2: [resource name]:[search param NAME]
insead of the DSTU1 style [resource name].[search param PATH]
</action>
</release>
<release version="0.9" date="2015-Mar-14">
<action type="add">

View File

@ -917,6 +917,22 @@
</subsection>
<subsection name="Reverse Resource Includes (_revinclude)">
<p>
To add support for reverse includes (via the <code>_revinclude</code> parameter),
use the same format as with the <code>_include</code> parameter (shown above)
but add <code>reverse=true</code> to the <code>@IncludeParam</code>
annotation, as shown below.
</p>
<macro name="snippet">
<param name="id" value="revInclude" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
</subsection>
<subsection name="Named Queries (_query)">
<p>