Fix #519 - Issues with JPA capabilitystatement related to the _id param

This commit is contained in:
James Agnew 2017-01-13 21:19:19 -06:00
parent 0dd1d77bec
commit fa1ad5ba85
10 changed files with 157 additions and 40 deletions

View File

@ -28,17 +28,8 @@ import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBase;
@ -408,7 +399,23 @@ class ModelScanner {
Map<String, RuntimeSearchParam> nameToParam = new HashMap<String, RuntimeSearchParam>(); Map<String, RuntimeSearchParam> nameToParam = new HashMap<String, RuntimeSearchParam>();
Map<Field, SearchParamDefinition> compositeFields = new LinkedHashMap<Field, SearchParamDefinition>(); Map<Field, SearchParamDefinition> compositeFields = new LinkedHashMap<Field, SearchParamDefinition>();
for (Field nextField : theClass.getFields()) { /*
* Make sure we pick up fields in interfaces too.. This ensures that we
* grab the _id field which generally gets picked up via interface
*/
Set<Field> fields = new HashSet<Field>(Arrays.asList(theClass.getFields()));
Class<?> nextClass = theClass;
do {
for (Class<?> nextInterface : nextClass.getInterfaces()) {
fields.addAll(Arrays.asList(nextInterface.getFields()));
}
nextClass = nextClass.getSuperclass();
} while (nextClass.equals(Object.class) == false);
/*
* Now scan the fields for search params
*/
for (Field nextField : fields) {
SearchParamDefinition searchParam = pullAnnotation(nextField, SearchParamDefinition.class); SearchParamDefinition searchParam = pullAnnotation(nextField, SearchParamDefinition.class);
if (searchParam != null) { if (searchParam != null) {
RestSearchParameterTypeEnum paramType = RestSearchParameterTypeEnum.forCode(searchParam.type().toLowerCase()); RestSearchParameterTypeEnum paramType = RestSearchParameterTypeEnum.forCode(searchParam.type().toLowerCase());

View File

@ -6,6 +6,8 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.ObjectUtils;
import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
/* /*
@ -34,6 +36,8 @@ import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
*/ */
public class TokenClientParam extends BaseClientParam implements IParam { public class TokenClientParam extends BaseClientParam implements IParam {
private static final String[] EMPTY_STRING_LIST = new String[0];
private String myParamName; private String myParamName;
public TokenClientParam(String theParamName) { public TokenClientParam(String theParamName) {
@ -77,6 +81,16 @@ public class TokenClientParam extends BaseClientParam implements IParam {
return new TokenCriterion(getParamName(), defaultString(theSystem), theCode); return new TokenCriterion(getParamName(), defaultString(theSystem), theCode);
} }
@Override
public ICriterion<?> systemAndValues(String theSystem, String... theValues) {
return new TokenCriterion(getParamName(), defaultString(theSystem), convertToList(theValues));
}
private List<String> convertToList(String[] theValues) {
String[] values = ObjectUtils.defaultIfNull(theValues, EMPTY_STRING_LIST);
return Arrays.asList(values);
}
@Override @Override
public ICriterion<?> systemAndValues(String theSystem, Collection<String> theValues) { public ICriterion<?> systemAndValues(String theSystem, Collection<String> theValues) {
return new TokenCriterion(getParamName(), defaultString(theSystem), theValues); return new TokenCriterion(getParamName(), defaultString(theSystem), theValues);
@ -111,6 +125,7 @@ public class TokenClientParam extends BaseClientParam implements IParam {
*/ */
ICriterion<TokenClientParam> code(String theIdentifier); ICriterion<TokenClientParam> code(String theIdentifier);
/** /**
* Creates a search criterion that matches against the given identifier (system and code if both are present, or whatever is present) * Creates a search criterion that matches against the given identifier (system and code if both are present, or whatever is present)
* *
@ -160,15 +175,6 @@ public class TokenClientParam extends BaseClientParam implements IParam {
*/ */
ICriterion<TokenClientParam> systemAndCode(String theSystem, String theCode); ICriterion<TokenClientParam> systemAndCode(String theSystem, String theCode);
/**
* Creates a search criterion that matches a given system with a collection of possible
* values (this will be used to form a comma-separated OR query)
*
* @param theSystem The system, which will be used with each value
* @param theValues The values
*/
public ICriterion<?> systemAndValues(String theSystem, Collection<String> theValues);
/** /**
* Creates a search criterion that matches against the given system and identifier * Creates a search criterion that matches against the given system and identifier
* *
@ -180,6 +186,24 @@ public class TokenClientParam extends BaseClientParam implements IParam {
*/ */
ICriterion<TokenClientParam> systemAndIdentifier(String theSystem, String theIdentifier); ICriterion<TokenClientParam> systemAndIdentifier(String theSystem, String theIdentifier);
/**
* Creates a search criterion that matches a given system with a collection of possible
* values (this will be used to form a comma-separated OR query)
*
* @param theSystem The system, which will be used with each value
* @param theValues The values
*/
ICriterion<?> systemAndValues(String theSystem, String... theValues);
/**
* Creates a search criterion that matches a given system with a collection of possible
* values (this will be used to form a comma-separated OR query)
*
* @param theSystem The system, which will be used with each value
* @param theValues The values
*/
public ICriterion<?> systemAndValues(String theSystem, Collection<String> theValues);
} }
} }

View File

@ -1,7 +1,7 @@
package org.hl7.fhir.instance.model.api; package org.hl7.fhir.instance.model.api;
import ca.uhn.fhir.model.api.annotation.SearchParamDefinition; import ca.uhn.fhir.model.api.annotation.SearchParamDefinition;
import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam;
/* /*
* #%L * #%L
@ -35,7 +35,7 @@ public interface IAnyResource extends IBaseResource {
/** /**
* Search parameter constant for <b>_id</b> * Search parameter constant for <b>_id</b>
*/ */
@SearchParamDefinition(name="_id", path="", description="The ID of the resource", type="string" ) @SearchParamDefinition(name="_id", path="", description="The ID of the resource", type="token" )
public static final String SP_RES_ID = "_id"; public static final String SP_RES_ID = "_id";
/** /**
@ -46,7 +46,7 @@ public interface IAnyResource extends IBaseResource {
* Path: <b>Resource._id</b><br> * Path: <b>Resource._id</b><br>
* </p> * </p>
*/ */
public static final StringClientParam RES_ID = new StringClientParam(IAnyResource.SP_RES_ID); public static final TokenClientParam RES_ID = new TokenClientParam(IAnyResource.SP_RES_ID);
String getId(); String getId();

View File

@ -2283,17 +2283,25 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
id2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); id2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
} }
//@formatter:off
Bundle found = ourClient Bundle found = ourClient
.search() .search()
.forResource(Patient.class) .forResource(Patient.class)
.where(BaseResource.RES_ID.matches().values(id1.getIdPart(), id2.getIdPart())) .where(BaseResource.RES_ID.exactly().systemAndValues(null, id1.getIdPart(), id2.getIdPart()))
.and(BaseResource.RES_ID.matches().value(id1.getIdPart())) .and(BaseResource.RES_ID.exactly().systemAndCode(null, id1.getIdPart()))
.returnBundle(Bundle.class) .returnBundle(Bundle.class)
.execute(); .execute();
//@formatter:on
assertThat(toUnqualifiedVersionlessIds(found), containsInAnyOrder(id1)); assertThat(toUnqualifiedVersionlessIds(found), containsInAnyOrder(id1));
found = ourClient
.search()
.forResource(Patient.class)
.where(BaseResource.RES_ID.exactly().systemAndValues(null, id1.getIdPart(), id2.getIdPart()))
.returnBundle(Bundle.class)
.execute();
assertThat(toUnqualifiedVersionlessIds(found), containsInAnyOrder(id1, id2));
} }
@Test @Test

View File

@ -0,0 +1,69 @@
package ca.uhn.fhir.jpa.provider.dstu3;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.hl7.fhir.dstu3.model.CapabilityStatement;
import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceComponent;
import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent;
import org.junit.AfterClass;
import org.junit.Test;
import ca.uhn.fhir.util.TestUtil;
public class ServerDstu3Test extends BaseResourceProviderDstu3Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerDstu3Test.class);
/**
* See #519
*/
@Test
public void saveIdParamOnlyAppearsOnce() throws IOException {
HttpGet get = new HttpGet(ourServerBase + "/metadata?_pretty=true&_format=xml");
CloseableHttpResponse resp = ourHttpClient.execute(get);
try {
ourLog.info(resp.toString());
assertEquals(200, resp.getStatusLine().getStatusCode());
String respString = IOUtils.toString(resp.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(respString);
CapabilityStatement cs = myFhirCtx.newXmlParser().parseResource(CapabilityStatement.class, respString);
for (CapabilityStatementRestResourceComponent nextResource : cs.getRest().get(0).getResource()) {
ourLog.info("Testing resource: " + nextResource.getType());
Set<String> sps = new HashSet<String>();
for (CapabilityStatementRestResourceSearchParamComponent nextSp : nextResource.getSearchParam()) {
if (sps.add(nextSp.getName()) == false) {
fail("Duplicate search parameter " + nextSp.getName() + " for resource " + nextResource.getType());
}
}
if (!sps.contains("_id")) {
fail("No search parameter _id for resource " + nextResource.getType());
}
}
} finally {
IOUtils.closeQuietly(resp.getEntity().getContent());
}
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -47,7 +47,7 @@ import org.hl7.fhir.exceptions.FHIRException;
* A binary resource can contain any content, whether text, image, pdf, zip archive, etc. * A binary resource can contain any content, whether text, image, pdf, zip archive, etc.
*/ */
@ResourceDef(name="Binary", profile="http://hl7.org/fhir/Profile/Binary") @ResourceDef(name="Binary", profile="http://hl7.org/fhir/Profile/Binary")
public class Binary extends BaseBinary implements IBaseBinary { public class Binary extends BaseBinary implements IBaseBinary, IAnyResource {
/** /**
* MimeType of the binary content represented as a standard MimeType (BCP 13). * MimeType of the binary content represented as a standard MimeType (BCP 13).

View File

@ -36,6 +36,15 @@ public class ModelScannerDstu3Test {
TestUtil.clearAllStaticFieldsForUnitTest(); TestUtil.clearAllStaticFieldsForUnitTest();
} }
@Test
public void testScanBundle() {
FhirContext ctx = FhirContext.forDstu3();
RuntimeResourceDefinition def = ctx.getResourceDefinition("Bundle");
assertNotNull(def.getSearchParam("composition"));
assertNotNull(def.getSearchParam("_id"));
}
@Test @Test
public void testBundleMustImplementIBaseBundle() throws DataFormatException { public void testBundleMustImplementIBaseBundle() throws DataFormatException {

View File

@ -236,10 +236,14 @@ public class TinderJpaRestServerMojo extends AbstractMojo {
TinderJpaRestServerMojo mojo = new TinderJpaRestServerMojo(); TinderJpaRestServerMojo mojo = new TinderJpaRestServerMojo();
mojo.myProject = new MavenProject(); mojo.myProject = new MavenProject();
mojo.version = "dstu3"; mojo.version = "dstu2";
mojo.packageBase = "ca.uhn.test"; mojo.packageBase = "ca.uhn.test";
mojo.configPackageBase = "ca.uhn.test"; mojo.configPackageBase = "ca.uhn.test";
mojo.baseResourceNames = new ArrayList<String>(Arrays.asList("structuredefinition", "observation")); mojo.baseResourceNames = new ArrayList<String>(Arrays.asList(
// "observation"
// "communicationrequest"
"binary"
));
mojo.targetDirectory = new File("target/generated/valuesets"); mojo.targetDirectory = new File("target/generated/valuesets");
mojo.targetResourceDirectory = new File("target/generated/valuesets"); mojo.targetResourceDirectory = new File("target/generated/valuesets");
mojo.targetResourceSpringBeansFile = "tmp_beans.xml"; mojo.targetResourceSpringBeansFile = "tmp_beans.xml";

View File

@ -45,14 +45,6 @@ public class ${className}ResourceProvider extends
ca.uhn.fhir.rest.method.RequestDetails theRequestDetails, ca.uhn.fhir.rest.method.RequestDetails theRequestDetails,
@Description(shortDefinition="The resource identity")
@OptionalParam(name="_id")
StringAndListParam theId,
@Description(shortDefinition="The resource language")
@OptionalParam(name="_language")
StringAndListParam theResourceLanguage,
@Description(shortDefinition="Search the contents of the resource's data using a fulltext search") @Description(shortDefinition="Search the contents of the resource's data using a fulltext search")
@OptionalParam(name=ca.uhn.fhir.rest.server.Constants.PARAM_CONTENT) @OptionalParam(name=ca.uhn.fhir.rest.server.Constants.PARAM_CONTENT)
StringAndListParam theFtContent, StringAndListParam theFtContent,
@ -141,8 +133,6 @@ public class ${className}ResourceProvider extends
startRequest(theServletRequest); startRequest(theServletRequest);
try { try {
SearchParameterMap paramMap = new SearchParameterMap(); SearchParameterMap paramMap = new SearchParameterMap();
paramMap.add("_id", theId);
paramMap.add("_language", theResourceLanguage);
paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_CONTENT, theFtContent); paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_CONTENT, theFtContent);
paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_TEXT, theFtText); paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_TEXT, theFtText);
paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_TAG, theSearchForTag); paramMap.add(ca.uhn.fhir.rest.server.Constants.PARAM_TAG, theSearchForTag);

View File

@ -192,6 +192,12 @@
before the interceptor could see it. Thanks to before the interceptor could see it. Thanks to
Eeva Turkka for reporting! Eeva Turkka for reporting!
</action> </action>
<action type="fix" issue="519">
JPA server exported CapabilityStatement includes
double entries for the _id parameter and uses the
wrong type (string instead of token). Thanks to
Robert Lichtenberger for reporting!
</action>
</release> </release>
<release version="2.1" date="2016-11-11"> <release version="2.1" date="2016-11-11">
<action type="add"> <action type="add">