More work on getting conditional updates working

This commit is contained in:
James Agnew 2015-02-26 17:17:22 -05:00
parent ecd3620e27
commit 0f2eb230e7
18 changed files with 601 additions and 100 deletions

View File

@ -694,6 +694,29 @@ public MethodOutcome createPatient(@ResourceParam Patient thePatient) {
//END SNIPPET: create //END SNIPPET: create
//START SNIPPET: createConditional
@Create
public MethodOutcome createPatientConditional(
@ResourceParam Patient thePatient,
@ConditionalOperationParam String theConditionalUrl) {
if (theConditionalUrl != null) {
// We are doing a conditional create
// populate this with the ID of the existing resource which
// matches the conditional URL
return new MethodOutcome();
} else {
// We are doing a normal create
// populate this with the ID of the newly created resource
return new MethodOutcome();
}
}
//END SNIPPET: createConditional
//START SNIPPET: createClient //START SNIPPET: createClient
@Create @Create
public abstract MethodOutcome createNewPatient(@ResourceParam Patient thePatient); public abstract MethodOutcome createNewPatient(@ResourceParam Patient thePatient);

View File

@ -92,7 +92,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
myMethod = theMethod; myMethod = theMethod;
myContext = theContext; myContext = theContext;
myProvider = theProvider; myProvider = theProvider;
myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider); myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getResourceOperationType());
} }
public List<Class<?>> getAllowableParamAnnotations() { public List<Class<?>> getAllowableParamAnnotations() {

View File

@ -43,6 +43,7 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu
private String myResourceName; private String myResourceName;
private boolean myBinary; private boolean myBinary;
private Class<? extends IBaseResource> myResourceType; private Class<? extends IBaseResource> myResourceType;
private Integer myIdParamIndex;
public BaseOutcomeReturningMethodBindingWithResourceParam(Method theMethod, FhirContext theContext, Class<?> theMethodAnnotation, Object theProvider) { public BaseOutcomeReturningMethodBindingWithResourceParam(Method theMethod, FhirContext theContext, Class<?> theMethodAnnotation, Object theProvider) {
super(theMethod, theContext, theMethodAnnotation, theProvider); super(theMethod, theContext, theMethodAnnotation, theProvider);
@ -78,6 +79,8 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu
} }
index++; index++;
} }
myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod);
if (resourceParameter == null) { if (resourceParameter == null) {
throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " does not have a parameter annotated with @" + ResourceParam.class.getSimpleName()); throw new ConfigurationException("Method " + theMethod.getName() + " in type " + theMethod.getDeclaringClass().getCanonicalName() + " does not have a parameter annotated with @" + ResourceParam.class.getSimpleName());
@ -106,12 +109,11 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu
} }
} }
/**
* For subclasses to override
*/
@Override @Override
protected void addParametersForServerRequest(Request theRequest, Object[] theParams) { protected void addParametersForServerRequest(Request theRequest, Object[] theParams) {
// nothing if (myIdParamIndex != null) {
theParams[myIdParamIndex] = theRequest.getId();
}
} }
@Override @Override

View File

@ -20,19 +20,28 @@ package ca.uhn.fhir.rest.method;
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.lang3.Validate;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.rest.server.Constants;
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;
class ConditionalParamBinder implements IParameter { class ConditionalParamBinder implements IParameter {
ConditionalParamBinder() { private RestfulOperationTypeEnum myOperationType;
super();
ConditionalParamBinder(RestfulOperationTypeEnum theOperationType) {
Validate.notNull(theOperationType, "theOperationType can not be null");
myOperationType = theOperationType;
} }
@Override @Override
@ -42,9 +51,34 @@ class ConditionalParamBinder implements IParameter {
@Override @Override
public Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException { public Object translateQueryParametersIntoServerArgument(Request theRequest, Object theRequestContents) throws InternalErrorException, InvalidRequestException {
if (myOperationType == RestfulOperationTypeEnum.CREATE) {
String retVal = theRequest.getServletRequest().getHeader(Constants.HEADER_IF_NONE_EXIST);
if (isBlank(retVal)) {
return null;
}
if (retVal.startsWith(theRequest.getFhirServerBase())) {
retVal = retVal.substring(theRequest.getFhirServerBase().length());
}
return retVal;
} else if (myOperationType != RestfulOperationTypeEnum.DELETE && myOperationType != RestfulOperationTypeEnum.UPDATE) {
return null;
}
if (theRequest.getId() != null && theRequest.getId().hasIdPart()) { if (theRequest.getId() != null && theRequest.getId().hasIdPart()) {
return null; return null;
} }
boolean haveParam = false;
for (String next : theRequest.getParameters().keySet()) {
if (!next.startsWith("_")) {
haveParam=true;
break;
}
}
if (!haveParam) {
return null;
}
int questionMarkIndex = theRequest.getCompleteUrl().indexOf('?'); int questionMarkIndex = theRequest.getCompleteUrl().indexOf('?');
return theRequest.getResourceName() + theRequest.getCompleteUrl().substring(questionMarkIndex); return theRequest.getResourceName() + theRequest.getCompleteUrl().substring(questionMarkIndex);
} }

View File

@ -44,6 +44,7 @@ import ca.uhn.fhir.model.api.Tag;
import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
@ -300,7 +301,7 @@ public class MethodUtil {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static List<IParameter> getResourceParameters(FhirContext theContext, Method theMethod, Object theProvider) { public static List<IParameter> getResourceParameters(FhirContext theContext, Method theMethod, Object theProvider, RestfulOperationTypeEnum theRestfulOperationTypeEnum) {
List<IParameter> parameters = new ArrayList<IParameter>(); List<IParameter> parameters = new ArrayList<IParameter>();
Class<?>[] parameterTypes = theMethod.getParameterTypes(); Class<?>[] parameterTypes = theMethod.getParameterTypes();
@ -396,7 +397,7 @@ public class MethodUtil {
} else if (nextAnnotation instanceof TransactionParam) { } else if (nextAnnotation instanceof TransactionParam) {
param = new TransactionParamBinder(theContext); param = new TransactionParamBinder(theContext);
} else if (nextAnnotation instanceof ConditionalOperationParam) { } else if (nextAnnotation instanceof ConditionalOperationParam) {
param = new ConditionalParamBinder(); param = new ConditionalParamBinder(theRestfulOperationTypeEnum);
} else { } else {
continue; continue;
} }

View File

@ -37,6 +37,7 @@ import java.util.Map;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -814,6 +815,10 @@ public class RestfulServer extends HttpServlet {
/** /**
* This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the server being used. * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the server being used.
*
* @throws ServletException If the initialization failed. Note that you should consider throwing
* {@link UnavailableException} (which extends {@link ServletException}), as this is a flag
* to the servlet container that the servlet is not usable.
*/ */
protected void initialize() throws ServletException { protected void initialize() throws ServletException {
// nothing by default // nothing by default

View File

@ -1,36 +0,0 @@
package ca.uhn.fhir.jpa.entity;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class FhirPersister {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("fhir-spring-config.xml");
}
}

View File

@ -50,42 +50,22 @@ import ca.uhn.fhir.rest.server.IBundleProvider;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class JpaResourceProvider<T extends IResource> extends BaseJpaProvider implements IResourceProvider { public abstract class BaseJpaResourceProvider<T extends IResource> extends BaseJpaProvider implements IResourceProvider {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaResourceProvider.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseJpaResourceProvider.class);
private FhirContext myContext; private FhirContext myContext;
private IFhirResourceDao<T> myDao; private IFhirResourceDao<T> myDao;
public JpaResourceProvider() { public BaseJpaResourceProvider() {
// nothing // nothing
} }
public JpaResourceProvider(IFhirResourceDao<T> theDao) { public BaseJpaResourceProvider(IFhirResourceDao<T> theDao) {
myDao = theDao; myDao = theDao;
} }
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource) {
startRequest(theRequest);
try {
return myDao.create(theResource);
} finally {
endRequest(theRequest);
}
}
@Delete
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdDt theResource) {
startRequest(theRequest);
try {
return myDao.delete(theResource);
} finally {
endRequest(theRequest);
}
}
public FhirContext getContext() { public FhirContext getContext() {
return myContext; return myContext;
} }
@ -158,21 +138,6 @@ public class JpaResourceProvider<T extends IResource> extends BaseJpaProvider im
myDao = theDao; myDao = theDao;
} }
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdDt theId) {
startRequest(theRequest);
try {
theResource.setId(theId);
return myDao.update(theResource);
} catch (ResourceNotFoundException e) {
ourLog.info("Can't update resource with ID[" + theId.getValue() + "] because it doesn't exist, going to create it instead");
theResource.setId(theId);
return myDao.create(theResource);
} finally {
endRequest(theRequest);
}
}
@Validate @Validate
public MethodOutcome validate(HttpServletRequest theRequest, @ResourceParam T theResource) { public MethodOutcome validate(HttpServletRequest theRequest, @ResourceParam T theResource) {
startRequest(theRequest); startRequest(theRequest);

View File

@ -0,0 +1,83 @@
package ca.uhn.fhir.jpa.provider;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.Delete;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class JpaResourceProviderDstu1<T extends IResource> extends BaseJpaResourceProvider<T> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaResourceProviderDstu1.class);
public JpaResourceProviderDstu1() {
// nothing
}
public JpaResourceProviderDstu1(IFhirResourceDao<T> theDao) {
super(theDao);
}
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource) {
startRequest(theRequest);
try {
return getDao().create(theResource);
} finally {
endRequest(theRequest);
}
}
@Delete
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdDt theResource) {
startRequest(theRequest);
try {
return getDao().delete(theResource);
} finally {
endRequest(theRequest);
}
}
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdDt theId) {
startRequest(theRequest);
try {
theResource.setId(theId);
return getDao().update(theResource);
} catch (ResourceNotFoundException e) {
ourLog.info("Can't update resource with ID[" + theId.getValue() + "] because it doesn't exist, going to create it instead");
theResource.setId(theId);
return getDao().create(theResource);
} finally {
endRequest(theRequest);
}
}
}

View File

@ -0,0 +1,96 @@
package ca.uhn.fhir.jpa.provider;
/*
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2015 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.Delete;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class JpaResourceProviderDstu2<T extends IResource> extends BaseJpaResourceProvider<T> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaResourceProviderDstu2.class);
public JpaResourceProviderDstu2() {
// nothing
}
public JpaResourceProviderDstu2(IFhirResourceDao<T> theDao) {
super(theDao);
}
@Create
public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalOperationParam String theConditional) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().create(theResource, theConditional);
} else {
return getDao().create(theResource);
}
} finally {
endRequest(theRequest);
}
}
@Delete
public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdDt theResource, @ConditionalOperationParam String theConditional) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().deleteByUrl(theConditional);
} else {
return getDao().delete(theResource);
}
} finally {
endRequest(theRequest);
}
}
@Update
public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdDt theId, @ConditionalOperationParam String theConditional) {
startRequest(theRequest);
try {
if (theConditional != null) {
return getDao().update(theResource, theConditional);
} else {
theResource.setId(theId);
return getDao().update(theResource);
}
} catch (ResourceNotFoundException e) {
ourLog.info("Can't update resource with ID[" + theId.getValue() + "] because it doesn't exist, going to create it instead");
theResource.setId(theId);
return getDao().create(theResource);
} finally {
endRequest(theRequest);
}
}
}

View File

@ -1,10 +1,9 @@
package ca.uhn.fhir.jpa.provider; package ca.uhn.fhir.jpa.provider;
import ca.uhn.fhir.jpa.provider.JpaResourceProvider;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.resource.Questionnaire; import ca.uhn.fhir.model.dstu.resource.Questionnaire;
public class QuestionnaireResourceProvider extends JpaResourceProvider<Questionnaire> { public class QuestionnaireResourceProvider extends JpaResourceProviderDstu1<Questionnaire> {
@Override @Override
public Class<? extends IResource> getResourceType() { public Class<? extends IResource> getResourceType() {

View File

@ -1,13 +1,29 @@
package ca.uhn.fhir.jpa.provider; package ca.uhn.fhir.jpa.provider;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*; import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
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.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
@ -45,6 +61,7 @@ import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.gclient.TokenClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
@ -63,6 +80,8 @@ public class ResourceProviderDstu2Test {
private static Server ourServer; private static Server ourServer;
private static IFhirResourceDao<Organization> ourOrganizationDao; private static IFhirResourceDao<Organization> ourOrganizationDao;
private static DaoConfig ourDaoConfig; private static DaoConfig ourDaoConfig;
private static CloseableHttpClient ourHttpClient;
private static String ourServerBase;
// private static JpaConformanceProvider ourConfProvider; // private static JpaConformanceProvider ourConfProvider;
@ -92,9 +111,24 @@ public class ResourceProviderDstu2Test {
} }
} }
public void testTryToCreateResourceWithNumericId() { @Test
public void testCreateResourceWithNumericId() throws IOException {
String resource = "<Patient xmlns=\"http://hl7.org/fhir\"><id value=\"1777\"/><meta><versionId value=\"1\"/><lastUpdated value=\"2015-02-25T15:47:48Z\"/></meta></Patient>"; String resource = "<Patient xmlns=\"http://hl7.org/fhir\"><id value=\"1777\"/><meta><versionId value=\"1\"/><lastUpdated value=\"2015-02-25T15:47:48Z\"/></meta></Patient>";
HttpPost post = new HttpPost(ourServerBase + "/Patient");
post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
try {
assertEquals(201, response.getStatusLine().getStatusCode());
assertThat(response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(), startsWith(ourServerBase + "/Patient/"));
assertThat(response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(), endsWith("/_history/1"));
assertThat(response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(), not(containsString("1777")));
} finally {
response.close();
}
} }
/** /**
@ -523,6 +557,7 @@ public class ResourceProviderDstu2Test {
public static void afterClass() throws Exception { public static void afterClass() throws Exception {
ourServer.stop(); ourServer.stop();
ourAppCtx.stop(); ourAppCtx.stop();
ourHttpClient.close();
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -534,7 +569,7 @@ public class ResourceProviderDstu2Test {
ourFhirCtx = FhirContext.forDstu2(); ourFhirCtx = FhirContext.forDstu2();
restServer.setFhirContext(ourFhirCtx); restServer.setFhirContext(ourFhirCtx);
String serverBase = "http://localhost:" + port + "/fhir/context"; ourServerBase = "http://localhost:" + port + "/fhir/context";
ourAppCtx = new ClassPathXmlApplicationContext("hapi-fhir-server-resourceproviders-dstu2.xml", "fhir-jpabase-spring-test-config.xml"); ourAppCtx = new ClassPathXmlApplicationContext("hapi-fhir-server-resourceproviders-dstu2.xml", "fhir-jpabase-spring-test-config.xml");
@ -565,9 +600,13 @@ public class ResourceProviderDstu2Test {
ourServer.start(); ourServer.start();
ourFhirCtx.getRestfulClientFactory().setSocketTimeout(600 * 1000); ourFhirCtx.getRestfulClientFactory().setSocketTimeout(600 * 1000);
ourClient = ourFhirCtx.newRestfulGenericClient(serverBase); ourClient = ourFhirCtx.newRestfulGenericClient(ourServerBase);
ourClient.registerInterceptor(new LoggingInterceptor(true)); ourClient.registerInterceptor(new LoggingInterceptor(true));
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourHttpClient = builder.build();
} }

View File

@ -145,9 +145,9 @@ public class PagingTest {
assertEquals(2, bundle.getEntries().size()); assertEquals(2, bundle.getEntries().size());
assertEquals("2", bundle.getEntries().get(0).getId().getIdPart()); assertEquals("2", bundle.getEntries().get(0).getId().getIdPart());
assertEquals("3", bundle.getEntries().get(1).getId().getIdPart()); assertEquals("3", bundle.getEntries().get(1).getId().getIdPart());
assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=4&" + Constants.PARAM_COUNT + "=2", bundle.getLinkNext().getValue()); assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=4&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkNext().getValue());
assertEquals(base + '/' + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2", bundle.getLinkSelf().getValue()); assertEquals(base + '/' + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkSelf().getValue());
assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=0&" + Constants.PARAM_COUNT + "=2", bundle.getLinkPrevious().getValue()); assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=0&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkPrevious().getValue());
} }
} }

View File

@ -0,0 +1,200 @@
package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
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.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
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.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.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.dstu2.resource.DiagnosticOrder;
import ca.uhn.fhir.model.dstu2.resource.DiagnosticReport;
import ca.uhn.fhir.model.dstu2.resource.Observation;
import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Organization;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
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.util.PortUtil;
/**
* Created by dsotnikov on 2/25/2014.
*/
public class CreateConditionalTest {
private static CloseableHttpClient ourClient;
private static String ourLastConditionalUrl;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CreateConditionalTest.class);
private static int ourPort;
private static Server ourServer;
private static IdDt ourLastId;
private static IdDt ourLastIdParam;
private static boolean ourLastRequestWasSearch;
@Before
public void before() {
ourLastId = null;
ourLastConditionalUrl = null;
ourLastIdParam = null;
ourLastRequestWasSearch = false;
}
@Test
public void testCreateWithConditionalUrl() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.addHeader(Constants.HEADER_IF_NONE_EXIST, "Patient?identifier=system%7C001");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("content-location").getValue());
assertNull(ourLastId.getValue());
assertNull(ourLastIdParam);
assertEquals("Patient?identifier=system%7C001", ourLastConditionalUrl);
}
@Test
public void testCreateWithoutConditionalUrl() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/2?_format=true&_pretty=true");
httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(201, status.getStatusLine().getStatusCode());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue());
assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("content-location").getValue());
assertEquals("Patient/2", ourLastId.toUnqualified().getValue());
assertEquals("Patient/2", ourLastIdParam.toUnqualified().getValue());
assertNull(ourLastConditionalUrl);
}
@Test
public void testSearchStillWorks() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_pretty=true");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertTrue(ourLastRequestWasSearch);
assertNull(ourLastId);
assertNull(ourLastIdParam);
assertNull(ourLastConditionalUrl);
}
@AfterClass
public static void afterClass() throws Exception {
ourServer.stop();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
PatientProvider patientProvider = new PatientProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer();
servlet.setResourceProviders(patientProvider);
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 PatientProvider implements IResourceProvider {
@Override
public Class<? extends IResource> getResourceType() {
return Patient.class;
}
@Search
public List<IResource> search(@OptionalParam(name="foo") StringDt theString) {
ourLastRequestWasSearch = true;
return new ArrayList<IResource>();
}
@Create()
public MethodOutcome createPatient(@ResourceParam Patient thePatient, @ConditionalOperationParam String theConditional, @IdParam IdDt theIdParam) {
ourLastConditionalUrl = theConditional;
ourLastId = thePatient.getId();
ourLastIdParam = theIdParam;
return new MethodOutcome(new IdDt("Patient/001/_history/002"));
}
}
}

View File

@ -2,10 +2,13 @@ package ca.uhn.fhir.rest.server;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity; import org.apache.http.entity.StringEntity;
@ -31,9 +34,12 @@ import ca.uhn.fhir.model.dstu2.resource.OperationOutcome;
import ca.uhn.fhir.model.dstu2.resource.Organization; import ca.uhn.fhir.model.dstu2.resource.Organization;
import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.ConditionalOperationParam; import ca.uhn.fhir.rest.annotation.ConditionalOperationParam;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.ResourceParam; 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.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.PortUtil;
@ -50,6 +56,7 @@ public class UpdateConditionalTest {
private static Server ourServer; private static Server ourServer;
private static IdDt ourLastId; private static IdDt ourLastId;
private static IdDt ourLastIdParam; private static IdDt ourLastIdParam;
private static boolean ourLastRequestWasSearch;
@ -58,6 +65,7 @@ public class UpdateConditionalTest {
ourLastId = null; ourLastId = null;
ourLastConditionalUrl = null; ourLastConditionalUrl = null;
ourLastIdParam = null; ourLastIdParam = null;
ourLastRequestWasSearch = false;
} }
@Test @Test
@ -112,6 +120,29 @@ public class UpdateConditionalTest {
} }
@Test
public void testSearchStillWorks() throws Exception {
Patient patient = new Patient();
patient.addIdentifier().setValue("002");
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_pretty=true");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertTrue(ourLastRequestWasSearch);
assertNull(ourLastId);
assertNull(ourLastIdParam);
assertNull(ourLastConditionalUrl);
}
@AfterClass @AfterClass
public static void afterClass() throws Exception { public static void afterClass() throws Exception {
ourServer.stop(); ourServer.stop();
@ -147,6 +178,11 @@ public class UpdateConditionalTest {
return Patient.class; return Patient.class;
} }
@Search
public List<IResource> search(@OptionalParam(name="foo") StringDt theString) {
ourLastRequestWasSearch = true;
return new ArrayList<IResource>();
}
@Update() @Update()
public MethodOutcome updatePatient(@ResourceParam Patient thePatient, @ConditionalOperationParam String theConditional, @IdParam IdDt theIdParam) { public MethodOutcome updatePatient(@ResourceParam Patient thePatient, @ConditionalOperationParam String theConditional, @IdParam IdDt theIdParam) {

View File

@ -19,6 +19,7 @@ import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
import org.apache.commons.lang.WordUtils;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.MojoFailureException;
@ -444,6 +445,12 @@ public abstract class BaseStructureParser {
ctx.put("includes", (theResource.getIncludes())); ctx.put("includes", (theResource.getIncludes()));
ctx.put("esc", new EscapeTool()); ctx.put("esc", new EscapeTool());
String capitalize = WordUtils.capitalize(myVersion);
if ("Dstu".equals(capitalize)) {
capitalize="Dstu1";
}
ctx.put("versionCapitalized", capitalize);
VelocityEngine v = new VelocityEngine(); VelocityEngine v = new VelocityEngine();
v.setProperty("resource.loader", "cp"); v.setProperty("resource.loader", "cp");
v.setProperty("cp.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); v.setProperty("cp.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
@ -525,6 +532,13 @@ public abstract class BaseStructureParser {
ctx.put("version", myVersion); ctx.put("version", myVersion);
ctx.put("versionEnumName", determineVersionEnum().name()); ctx.put("versionEnumName", determineVersionEnum().name());
ctx.put("esc", new EscapeTool()); ctx.put("esc", new EscapeTool());
String capitalize = WordUtils.capitalize(myVersion);
if ("Dstu".equals(capitalize)) {
capitalize="Dstu1";
}
ctx.put("versionCapitalized", capitalize);
VelocityEngine v = new VelocityEngine(); VelocityEngine v = new VelocityEngine();
v.setProperty("resource.loader", "cp"); v.setProperty("resource.loader", "cp");

View File

@ -5,7 +5,7 @@ import java.util.*;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.jpa.provider.JpaResourceProvider; import ca.uhn.fhir.jpa.provider.JpaResourceProvider${versionCapitalized};
import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.model.api.*; import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.api.annotation.*; import ca.uhn.fhir.model.api.annotation.*;
@ -18,7 +18,7 @@ import ca.uhn.fhir.model.dstu.resource.Binary;
// import ca.uhn.fhir.model.dstu2.resource.Bundle; // import ca.uhn.fhir.model.dstu2.resource.Bundle;
import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.Bundle;
public class ${className}ResourceProvider extends JpaResourceProvider<${className}> { public class ${className}ResourceProvider extends JpaResourceProvider${versionCapitalized}<${className}> {
@Override @Override
public Class<? extends IResource> getResourceType() { public Class<? extends IResource> getResourceType() {

View File

@ -474,6 +474,35 @@
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" /> <param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro> </macro>
<h4>Conditional Creates</h4>
<p>
The FHIR specification also allows "conditional creates". A conditional
create has an additional header called <code>If-None-Exist</code>
which the client will supply on the HTTP request. The client will
populate this header with a search URL such as <code>Patient?identifier=foo</code>.
See the FHIR specification for details on the semantics for correctly
implementing conditional create.
</p>
<p>
When a conditional create is detected (i.e. when the create request contains
a populated <code>If-None-Exist</code> header), if a method parameter annotated
with the
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ConditionalOperationParam.html">@ConditionalOperationParam</a>
is detected, it will be populated with the value of this header.
</p>
<macro name="snippet">
<param name="id" value="createConditional" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URL and HTTP header to perform a conditional create:
<br />
<code>http://fhir.example.com/Patient<br/>If-None-Exist: Patient?identifier=system%7C0001</code>
</p>
<a name="type_search" /> <a name="type_search" />
</section> </section>
@ -535,9 +564,9 @@
<p> <p>
Parameters which take a string as their format should use the Parameters which take a string as their format should use the
<a href="./apidocs/ca/uhn/fhir/rest/param/StringParam.html">StringParam</a> <code><a href="./apidocs/ca/uhn/fhir/rest/param/StringParam.html">StringParam</a></code>
type. They may also use normal java Strings, although it is type. They may also use normal java <code>String</code>, although it is
not possible to use the ":exact" qualifier in that case. not possible to use the <code>:exact</code> qualifier in that case.
</p> </p>
<macro name="snippet"> <macro name="snippet">
@ -971,7 +1000,7 @@
<p> <p>
To accept a composite parameter, use a parameter type which implements the To accept a composite parameter, use a parameter type which implements the
<a href="./apidocs/ca/uhn/fhir/model/api/IQueryParameterAnd.html">IQueryParameterAnd</a> <a href="./apidocs/ca/uhn/fhir/model/api/IQueryParameterAnd.html">IQueryParameterAnd</a>
interface. interface (which in turn encapsulates the corresponding IQueryParameterOr types).
</p> </p>
<p> <p>
An example follows which shows a search for Patients by address, where multiple string An example follows which shows a search for Patients by address, where multiple string
@ -991,6 +1020,17 @@
<param name="id" value="searchMultipleAnd" /> <param name="id" value="searchMultipleAnd" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" /> <param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro> </macro>
<p>
Note that AND parameters join multiple OR parameters together, but
the inverse is not true. In other words, it is possible in FHIR
to use AND search parameters to specify a search criteria of
<code>(A=1 OR A=2) AND (B=1 OR B=2)</code>
but it is not possible to specify
<code>(A=1 AND B=1) OR (A=2 AND B=2)</code> (aside from
in very specific cases where a composite parameter has been
specifically defined).
</p>
<h4>AND Relationship Query Parameters for Dates</h4> <h4>AND Relationship Query Parameters for Dates</h4>