Parameter type cleanup from connectathon

This commit is contained in:
jamesagnew 2014-09-17 08:32:15 -04:00
parent 5701572e08
commit 1fecb8ad76
18 changed files with 241 additions and 143 deletions

View File

@ -10,6 +10,16 @@
<action type="add">
Documentation update, thanks to Suranga Nath Kasthurirathne of the OpenMRS project.
</action>
<action type="fix">
Server implementation was not correctly figuring out its own FHIR Base URL when deployed
on Amazon Web Service server. Thanks to Jeffrey Ting and Bill De Beaubien of
Systems Made Simple for their help in figuring out this issue!
</action>
<action type="fix">
XML Parser failed to encode fields with both a resource reference child and
a primitive type child. Thanks to Jeffrey Ting and Bill De Beaubien of
Systems Made Simple for their help in figuring out this issue!
</action>
</release>
<release version="0.6" date="2014-Sep-08" description="This release brings a number of new features and bug fixes!">
<!--

View File

@ -172,12 +172,14 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding<Metho
break;
case UPDATE:
if (response.getCreated() == null || Boolean.FALSE.equals(response.getCreated())) {
if (response == null || response.getCreated() == null || Boolean.FALSE.equals(response.getCreated())) {
servletResponse.setStatus(Constants.STATUS_HTTP_200_OK);
} else {
servletResponse.setStatus(Constants.STATUS_HTTP_201_CREATED);
}
addLocationHeader(theRequest, servletResponse, response);
if (response != null && response.getId() != null) {
addLocationHeader(theRequest, servletResponse, response);
}
break;
case VALIDATE:

View File

@ -25,6 +25,7 @@ import java.util.List;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
import ca.uhn.fhir.rest.method.QualifiedParamList;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -46,6 +47,8 @@ public abstract class BaseAndListParam<T extends IQueryParameterOr<?>> implement
}
}
public abstract SearchParamTypeEnum getSearchParamType();
abstract T newInstance();
@Override

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
/*
* #%L
@ -38,6 +39,11 @@ public class CompositeAndListParam<A extends IQueryParameterType, B extends IQue
CompositeOrListParam<A,B> newInstance() {
return new CompositeOrListParam<A,B>(myLeftType, myRightType);
}
@Override
public SearchParamTypeEnum getSearchParamType() {
return SearchParamTypeEnum.COMPOSITE;
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
/*
* #%L
* HAPI FHIR - Core Library
@ -28,4 +30,9 @@ public class DateAndListParam extends BaseAndListParam<DateOrListParam> {
return new DateOrListParam();
}
@Override
public SearchParamTypeEnum getSearchParamType() {
return SearchParamTypeEnum.DATE;
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
/*
* #%L
* HAPI FHIR - Core Library
@ -27,5 +29,10 @@ public class NumberAndListParam extends BaseAndListParam<NumberOrListParam> {
NumberOrListParam newInstance() {
return new NumberOrListParam();
}
@Override
public SearchParamTypeEnum getSearchParamType() {
return SearchParamTypeEnum.NUMBER;
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
/*
* #%L
* HAPI FHIR - Core Library
@ -28,4 +30,9 @@ public class QuantityAndListParam extends BaseAndListParam<QuantityOrListParam>
return new QuantityOrListParam();
}
@Override
public SearchParamTypeEnum getSearchParamType() {
return SearchParamTypeEnum.QUANTITY;
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
/*
* #%L
* HAPI FHIR - Core Library
@ -28,4 +30,10 @@ public class ReferenceAndListParam extends BaseAndListParam<ReferenceOrListParam
return new ReferenceOrListParam();
}
@Override
public SearchParamTypeEnum getSearchParamType() {
return SearchParamTypeEnum.REFERENCE;
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
/*
* #%L
* HAPI FHIR - Core Library
@ -28,4 +30,9 @@ public class StringAndListParam extends BaseAndListParam<StringOrListParam> {
return new StringOrListParam();
}
@Override
public SearchParamTypeEnum getSearchParamType() {
return SearchParamTypeEnum.STRING;
}
}

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.param;
import ca.uhn.fhir.model.dstu.valueset.SearchParamTypeEnum;
/*
* #%L
* HAPI FHIR - Core Library
@ -28,4 +30,10 @@ public class TokenAndListParam extends BaseAndListParam<TokenOrListParam> {
return new TokenOrListParam();
}
@Override
public SearchParamTypeEnum getSearchParamType() {
return SearchParamTypeEnum.TOKEN;
}
}

View File

@ -951,6 +951,8 @@ public class RestfulServer extends HttpServlet {
if (theServer.getPagingProvider() == null) {
numToReturn = theResult.size();
resourceList = theResult.getResources(0, numToReturn);
validateResourceListNotNull(resourceList);
} else {
IPagingProvider pagingProvider = theServer.getPagingProvider();
if (theLimit == null) {
@ -961,6 +963,7 @@ public class RestfulServer extends HttpServlet {
numToReturn = Math.min(numToReturn, theResult.size() - theOffset);
resourceList = theResult.getResources(theOffset, numToReturn + theOffset);
validateResourceListNotNull(resourceList);
if (theSearchId != null) {
searchId = theSearchId;
@ -1012,6 +1015,12 @@ public class RestfulServer extends HttpServlet {
return bundle;
}
private static void validateResourceListNotNull(List<IResource> theResourceList) {
if (theResourceList == null) {
throw new InternalErrorException("IBundleProvider returned a null list of resources - This is not allowed");
}
}
public static Bundle createBundleFromResourceList(FhirContext theContext, String theAuthor, List<IResource> theResult, String theServerBase, String theCompleteUrl, int theTotalResults) {
Bundle bundle = new Bundle();
bundle.getAuthorName().setValue(theAuthor);

View File

@ -20,7 +20,7 @@ package ca.uhn.fhir.rest.server;
* #L%
*/
import java.util.HashMap;
import java.util.LinkedHashMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.BaseAndListParam;
@ -39,7 +39,7 @@ import ca.uhn.fhir.rest.param.StringOrListParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
public class SearchParameterMap extends HashMap<String, BaseAndListParam<?>> {
public class SearchParameterMap extends LinkedHashMap<String, BaseAndListParam<?>> {
private static final long serialVersionUID = 1L;

View File

@ -20,41 +20,41 @@ package ca.uhn.fhir.util;
* #L%
*/
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.List;
/**
* Provides server ports
*/
public class PortUtil {
private static List<Integer> ourPorts = new ArrayList<Integer>();
/**
* This is really only used for unit tests but is included in the library so it can be reused across modules. Use with caution.
*/
public static int findFreePort() {
ServerSocket server;
try {
server = new ServerSocket(0);
int port = server.getLocalPort();
ourPorts.add(port);
server.close();
Thread.sleep(500);
return port;
} catch (IOException e) {
throw new Error(e);
} catch (InterruptedException e) {
throw new Error(e);
}
}
public static List<Integer> list() {
return ourPorts;
}
}
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.List;
/**
* Provides server ports
*/
public class PortUtil {
private static List<Integer> ourPorts = new ArrayList<Integer>();
/**
* This is really only used for unit tests but is included in the library so it can be reused across modules. Use with caution.
*/
public static int findFreePort() {
ServerSocket server;
try {
server = new ServerSocket(0);
int port = server.getLocalPort();
ourPorts.add(port);
server.close();
Thread.sleep(500);
return port;
} catch (IOException e) {
throw new Error(e);
} catch (InterruptedException e) {
throw new Error(e);
}
}
public static List<Integer> list() {
return ourPorts;
}
}

View File

@ -18,7 +18,7 @@ import ca.uhn.fhir.util.ElementUtil;
@ResourceDef(name="Patient")
public class MyObservationWithExtensions extends Patient {
@Extension(url = "urn:patientext:att", definedLocally = false, isModifier = false)
@Child(name = "extAtt", order = 0)
private AttachmentDt myExtAtt;

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.model.dstu.resource.DiagnosticOrder;
import ca.uhn.fhir.model.dstu.resource.DiagnosticReport;
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.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.IdParam;
@ -279,6 +280,21 @@ public class UpdateTest {
fail();
}
@Test
public void testUpdateWithNoReturn() throws Exception {
Organization patient = new Organization();
patient.addIdentifier().setValue("002");
HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Organization/001");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourClient.execute(httpPost);
assertEquals(200, response.getStatusLine().getStatusCode());
response.close();
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
@ -295,7 +311,7 @@ public class UpdateTest {
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer();
servlet.setResourceProviders(patientProvider, ourReportProvider, new ObservationProvider());
servlet.setResourceProviders(patientProvider, ourReportProvider, new ObservationProvider(), new OrganizationResourceProvider());
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
@ -308,6 +324,23 @@ public class UpdateTest {
}
public static class OrganizationResourceProvider implements IResourceProvider
{
@Override
public Class<? extends IResource> getResourceType() {
return Organization.class;
}
@SuppressWarnings("unused")
@Update
public MethodOutcome update(@IdParam IdDt theId, @ResourceParam Organization theOrganization) {
return new MethodOutcome();
}
}
public static class DiagnosticReportProvider implements IResourceProvider {
private TagList myLastTags;

View File

@ -374,17 +374,6 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return entity;
}
private void validateGivenIdIsAppropriateToRetrieveResource(IdDt theId, BaseHasResource entity) {
if (entity.getForcedId() != null) {
if (theId.isIdPartValidLong()) {
// This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that
// as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer to the
// forced ID)
throw new ResourceNotFoundException(theId);
}
}
}
@Override
public void removeTag(IdDt theId, String theScheme, String theTerm) {
StopWatch w = new StopWatch();
@ -564,34 +553,6 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return retVal;
}
private void createSort(CriteriaBuilder theBuilder, Root<ResourceTable> theFrom, SortSpec theSort, List<Order> theOrders, List<Predicate> thePredicates) {
if (theSort == null || isBlank(theSort.getParamName())) {
return;
}
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceType);
RuntimeSearchParam param = resourceDef.getSearchParam(theSort.getParamName());
if (param == null) {
throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'");
}
String joinAttrName = "myParamsString";
String sortAttrName = "myValueExact";
switch (param.getParamType()) {
case STRING: {
From<?, ?> stringJoin = theFrom.join(joinAttrName, JoinType.LEFT);
Predicate p = theBuilder.equal(stringJoin.get("myParamName"), theSort.getParamName());
Predicate pn = theBuilder.isNull(stringJoin.get("myParamName"));
thePredicates.add(theBuilder.or(p, pn));
theOrders.add(theBuilder.asc(stringJoin.get(sortAttrName)));
break;
}
}
createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates);
}
@Override
public IBundleProvider search(String theParameterName, IQueryParameterType theValue) {
return search(Collections.singletonMap(theParameterName, theValue));
@ -904,6 +865,51 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return found;
}
private Set<Long> addPredicateLanguage(Set<Long> thePids, List<List<? extends IQueryParameterType>> theList) {
if (theList == null || theList.isEmpty()) {
return thePids;
}
if (theList.size() > 1) {
throw new InvalidRequestException("Language parameter can not have more than one AND value, found " + theList.size());
}
CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
Root<ResourceTable> from = cq.from(ResourceTable.class);
cq.select(from.get("myId").as(Long.class));
Set<String> values = new HashSet<String>();
for (IQueryParameterType next : theList.get(0)) {
if (next instanceof StringParam) {
String nextValue = ((StringParam) next).getValue();
if (isBlank(nextValue)) {
continue;
}
values.add(nextValue);
} else {
throw new InternalErrorException("Lanugage parameter must be of type " + StringParam.class.getCanonicalName() + " - Got " + next.getClass().getCanonicalName());
}
}
if (values.isEmpty()) {
return thePids;
}
Predicate typePredicate = builder.equal(from.get("myResourceType"), myResourceName);
Predicate langPredicate = from.get("myLanguage").as(String.class).in(values);
Predicate masterCodePredicate = builder.and(typePredicate, langPredicate);
if (thePids.size() > 0) {
Predicate inPids = (from.get("myId").in(thePids));
cq.where(builder.and(masterCodePredicate, inPids));
} else {
cq.where(masterCodePredicate);
}
TypedQuery<Long> q = myEntityManager.createQuery(cq);
return new HashSet<Long>(q.getResultList());
}
private Set<Long> addPredicateNumber(String theParamName, Set<Long> thePids, List<? extends IQueryParameterType> theList) {
if (theList == null || theList.isEmpty()) {
return thePids;
@ -1203,51 +1209,6 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return new HashSet<Long>(q.getResultList());
}
private Set<Long> addPredicateLanguage(Set<Long> thePids, List<List<? extends IQueryParameterType>> theList) {
if (theList == null || theList.isEmpty()) {
return thePids;
}
if (theList.size() > 1) {
throw new InvalidRequestException("Language parameter can not have more than one AND value, found " + theList.size());
}
CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = builder.createQuery(Long.class);
Root<ResourceTable> from = cq.from(ResourceTable.class);
cq.select(from.get("myId").as(Long.class));
Set<String> values = new HashSet<String>();
for (IQueryParameterType next : theList.get(0)) {
if (next instanceof StringParam) {
String nextValue = ((StringParam) next).getValue();
if (isBlank(nextValue)) {
continue;
}
values.add(nextValue);
} else {
throw new InternalErrorException("Lanugage parameter must be of type " + StringParam.class.getCanonicalName() + " - Got " + next.getClass().getCanonicalName());
}
}
if (values.isEmpty()) {
return thePids;
}
Predicate typePredicate = builder.equal(from.get("myResourceType"), myResourceName);
Predicate langPredicate = from.get("myLanguage").as(String.class).in(values);
Predicate masterCodePredicate = builder.and(typePredicate, langPredicate);
if (thePids.size() > 0) {
Predicate inPids = (from.get("myId").in(thePids));
cq.where(builder.and(masterCodePredicate, inPids));
} else {
cq.where(masterCodePredicate);
}
TypedQuery<Long> q = myEntityManager.createQuery(cq);
return new HashSet<Long>(q.getResultList());
}
private Set<Long> addPredicateToken(String theParamName, Set<Long> thePids, List<? extends IQueryParameterType> theList) {
if (theList == null || theList.isEmpty()) {
return thePids;
@ -1410,6 +1371,34 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return singleCode;
}
private void createSort(CriteriaBuilder theBuilder, Root<ResourceTable> theFrom, SortSpec theSort, List<Order> theOrders, List<Predicate> thePredicates) {
if (theSort == null || isBlank(theSort.getParamName())) {
return;
}
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceType);
RuntimeSearchParam param = resourceDef.getSearchParam(theSort.getParamName());
if (param == null) {
throw new InvalidRequestException("Unknown sort parameter '" + theSort.getParamName() + "'");
}
String joinAttrName = "myParamsString";
String sortAttrName = "myValueExact";
switch (param.getParamType()) {
case STRING: {
From<?, ?> stringJoin = theFrom.join(joinAttrName, JoinType.LEFT);
Predicate p = theBuilder.equal(stringJoin.get("myParamName"), theSort.getParamName());
Predicate pn = theBuilder.isNull(stringJoin.get("myParamName"));
thePredicates.add(theBuilder.or(p, pn));
theOrders.add(theBuilder.asc(stringJoin.get(sortAttrName)));
break;
}
}
createSort(theBuilder, theFrom, theSort.getChain(), theOrders, thePredicates);
}
private void loadResourcesByPid(Collection<Long> theIncludePids, List<IResource> theResourceListToPopulate) {
if (theIncludePids.isEmpty()) {
return;
@ -1497,6 +1486,17 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements
return qp;
}
private void validateGivenIdIsAppropriateToRetrieveResource(IdDt theId, BaseHasResource entity) {
if (entity.getForcedId() != null) {
if (theId.isIdPartValidLong()) {
// This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that
// as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer to the
// forced ID)
throw new ResourceNotFoundException(theId);
}
}
}
private void validateResourceType(BaseHasResource entity) {
if (!myResourceName.equals(entity.getResourceType())) {
throw new ResourceNotFoundException("Resource with ID " + entity.getIdDt().getIdPart() + " exists but it is not of type " + myResourceName + ", found resource of type " + entity.getResourceType());

View File

@ -121,12 +121,6 @@
<version>${jetty_version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty_version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>

View File

@ -23,6 +23,7 @@ import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -106,13 +107,8 @@ public class PatientResourceProvider implements IResourceProvider {
}
/**
* The "@Search" annotation indicates that this method supports the search operation. You may have many different method annotated with this annotation, to support many different search criteria.
* This example searches by family name.
*
* @param theIdentifier
* This operation takes one parameter which is the search criteria. It is annotated with the "@Required" annotation. This annotation takes one argument, a string containing the name of
* the search criteria. The datatype here is StringDt, but there are other possible parameter types depending on the specific search criteria.
* @return This method returns a list of Patients. This list may contain multiple matching resources, or it may also be empty.
* The "@Create" annotation indicates that this method implements "create=type", which adds a
* new instance of a resource to the server.
*/
@Create()
public MethodOutcome createPatient(@ResourceParam Patient thePatient) {
@ -217,7 +213,8 @@ public class PatientResourceProvider implements IResourceProvider {
}
/**
* The "@Update" annotation indicates that this method supports replacing an existing resource (by ID) with a new instance of that resource.
* The "@Update" annotation indicates that this method supports replacing an existing
* resource (by ID) with a new instance of that resource.
*
* @param theId
* This is the ID of the patient to update
@ -225,7 +222,7 @@ public class PatientResourceProvider implements IResourceProvider {
* This is the actual resource to save
* @return This method returns a "MethodOutcome"
*/
@Create()
@Update()
public MethodOutcome updatePatient(@IdParam IdDt theId, @ResourceParam Patient thePatient) {
validateResource(thePatient);