diff --git a/examples/pom.xml b/examples/pom.xml index cafac499030..db55ac45dfb 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -40,6 +40,24 @@ javax.servlet-api provided + + + ca.uhn.hapi.fhir + hapi-fhir-jaxrsserver-base + 1.4-SNAPSHOT + + + javax.ws.rs + javax.ws.rs-api + 2.0 + provided + + + javax.ejb + ejb-api + 3.0 + provided + diff --git a/examples/src/main/java/example/JaxRsConformanceProvider.java b/examples/src/main/java/example/JaxRsConformanceProvider.java new file mode 100644 index 00000000000..24bac3ee5e1 --- /dev/null +++ b/examples/src/main/java/example/JaxRsConformanceProvider.java @@ -0,0 +1,41 @@ +package example; + +import java.util.concurrent.ConcurrentHashMap; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsConformanceProvider; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IResourceProvider; + +/** + * Conformance Rest Service + * + * @author Peter Van Houte + */ + // START SNIPPET: jax-rs-conformance +@Path("") +@Stateless +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +public class JaxRsConformanceProvider extends AbstractJaxRsConformanceProvider { + + @EJB + private JaxRsPatientRestProvider provider; + + public JaxRsConformanceProvider() { + super("My Server Version", "My Server Description", "My Server Name"); + } + + @Override + protected ConcurrentHashMap, IResourceProvider> getProviders() { + ConcurrentHashMap, IResourceProvider> map = new ConcurrentHashMap, IResourceProvider>(); + map.put(JaxRsConformanceProvider.class, this); + map.put(JaxRsPatientRestProvider.class, provider); + return map; + } +} +// END SNIPPET: jax-rs-conformance diff --git a/examples/src/main/java/example/JaxRsPatientRestProvider.java b/examples/src/main/java/example/JaxRsPatientRestProvider.java new file mode 100644 index 00000000000..d23f5dc9513 --- /dev/null +++ b/examples/src/main/java/example/JaxRsPatientRestProvider.java @@ -0,0 +1,82 @@ +package example; + +import java.lang.reflect.InvocationTargetException; + +import javax.ejb.Local; +import javax.ejb.Stateless; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsResourceProvider; +import ca.uhn.fhir.model.dstu2.resource.Parameters; +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.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.server.Constants; + +/** + * A demo JaxRs Patient Rest Provider + */ +@Local +@Stateless +// START SNIPPET: jax-rs-provider-construction +@Path("/Patient") +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +public class JaxRsPatientRestProvider extends AbstractJaxRsResourceProvider { + + public JaxRsPatientRestProvider() { + super(JaxRsPatientRestProvider.class); + } +// END SNIPPET: jax-rs-provider-construction + + @Override + public Class getResourceType() { + return Patient.class; + } + + + @Create + public MethodOutcome create(@ResourceParam final Patient patient, @ConditionalUrlParam String theConditional) { + // create the patient ... + return new MethodOutcome(new IdDt(1L)).setCreated(true); + } + +// START SNIPPET: jax-rs-provider-operation + @GET + @Path("/{id}/$someCustomOperation") + public Response someCustomOperationUsingGet(@PathParam("id") String id, String resource) throws Exception { + return customOperation(resource, RequestTypeEnum.GET, id, "$someCustomOperation", + RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE); + } + + @Operation(name = "someCustomOperation", idempotent = true, returnParameters = { + @OperationParam(name = "return", type = StringDt.class) }) + public Parameters someCustomOperation(@IdParam IdDt myId, @OperationParam(name = "dummy") StringDt dummyInput) { + Parameters parameters = new Parameters(); + parameters.addParameter().setName("return").setValue(new StringDt("My Dummy Result")); + return parameters; + } + // END SNIPPET: jax-rs-provider-operation + + @POST + @Path("/{id}/$someCustomOperation") + public Response someCustomOperationUsingPost(@PathParam("id") String id, String resource) throws Exception { + return customOperation(resource, RequestTypeEnum.POST, id, "$someCustomOperation", + RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE); + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GetPage.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GetPage.java new file mode 100644 index 00000000000..b347033d74c --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/GetPage.java @@ -0,0 +1,20 @@ +package ca.uhn.fhir.rest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * INTERNAL API (do not use): REST method annotation for the method called when a client requests a page. + *

+ * This annotation is currently intended as an internal part of HAPI's API. At some point we + * will hopefully provide a way to create alternate implementations of the GetPage mewthod. If + * you would like to help out or have ideas, please get in touch! + *

+ */ +@Target(value= ElementType.METHOD) +@Retention(value=RetentionPolicy.RUNTIME) +public @interface GetPage { + // nothing +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/PageIdParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/PageIdParam.java new file mode 100644 index 00000000000..ed5a493fd99 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/PageIdParam.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.rest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Parameter annotation which specifies the parameter to receive the ID of the page + * being requested. Parameter must be of type {@link String} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface PageIdParam { + // nothing +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseAddOrDeleteTagsMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseAddOrDeleteTagsMethodBinding.java index e79b2edb99f..679438e35db 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseAddOrDeleteTagsMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseAddOrDeleteTagsMethodBinding.java @@ -45,6 +45,7 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -154,8 +155,8 @@ abstract class BaseAddOrDeleteTagsMethodBinding extends BaseMethodBinding } @Override - public void invokeServer(RestfulServer theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { - Object[] params = createParametersForServerRequest(theRequest, null); + public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { + Object[] params = createParametersForServerRequest(theRequest); params[myIdParamIndex] = theRequest.getId(); @@ -164,7 +165,7 @@ abstract class BaseAddOrDeleteTagsMethodBinding extends BaseMethodBinding } IParser parser = createAppropriateParserForParsingServerRequest(theRequest); - Reader reader = theRequest.getServletRequest().getReader(); + Reader reader = theRequest.getReader(); try { TagList tagList = parser.parseTagList(reader); params[myTagListParamIndex] = tagList; @@ -175,21 +176,13 @@ abstract class BaseAddOrDeleteTagsMethodBinding extends BaseMethodBinding for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = theServer.getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, theRequest.getServletRequest(), theRequest.getServletResponse()); + boolean continueProcessing = next.outgoingResponse(theRequest); if (!continueProcessing) { - return; + return null; } } - - HttpServletResponse response = theRequest.getServletResponse(); - response.setContentType(Constants.CT_TEXT); - response.setStatus(Constants.STATUS_HTTP_200_OK); - response.setCharacterEncoding(Constants.CHARSET_NAME_UTF8); - - theServer.addHeadersToResponse(response); - - PrintWriter writer = response.getWriter(); - writer.close(); + + return theRequest.getResponse().returnResponse(null, Constants.STATUS_HTTP_200_OK, false, null, null); } @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java index 1a81d325cc5..97fe24ced0e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java @@ -50,6 +50,7 @@ import ca.uhn.fhir.rest.annotation.AddTags; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Delete; import ca.uhn.fhir.rest.annotation.DeleteTags; +import ca.uhn.fhir.rest.annotation.GetPage; import ca.uhn.fhir.rest.annotation.GetTags; import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.Metadata; @@ -69,7 +70,7 @@ import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -81,15 +82,12 @@ import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ReflectionUtil; public abstract class BaseMethodBinding implements IClientResponseHandler { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class); - /** - * @see BaseMethodBinding#loadRequestContents(RequestDetails) - */ - private static volatile IRequestReader ourRequestReader; private FhirContext myContext; private Method myMethod; private List myParameters; @@ -131,7 +129,7 @@ public abstract class BaseMethodBinding implements IClientResponseHandler } protected IParser createAppropriateParserForParsingServerRequest(RequestDetails theRequest) { - String contentTypeHeader = theRequest.getServletRequest().getHeader("content-type"); + String contentTypeHeader = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); EncodingEnum encoding; if (isBlank(contentTypeHeader)) { encoding = EncodingEnum.XML; @@ -151,14 +149,14 @@ public abstract class BaseMethodBinding implements IClientResponseHandler return parser; } - protected Object[] createParametersForServerRequest(RequestDetails theRequest, byte[] theRequestContents) { + protected Object[] createParametersForServerRequest(RequestDetails theRequest) { Object[] params = new Object[getParameters().size()]; for (int i = 0; i < getParameters().size(); i++) { IParameter param = getParameters().get(i); if (param == null) { continue; } - params[i] = param.translateQueryParametersIntoServerArgument(theRequest, theRequestContents, this); + params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this); } return params; } @@ -251,9 +249,9 @@ public abstract class BaseMethodBinding implements IClientResponseHandler public abstract BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException; - public abstract void invokeServer(RestfulServer theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException; + public abstract Object invokeServer(IRestfulServer theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException; - protected final Object invokeServerMethod(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) { + protected final Object invokeServerMethod(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) { // Handle server action interceptors RestOperationTypeEnum operationType = getRestOperationType(theRequest); if (operationType != null) { @@ -293,41 +291,6 @@ public abstract class BaseMethodBinding implements IClientResponseHandler return mySupportsConditionalMultiple; } - protected byte[] loadRequestContents(RequestDetails theRequest) throws IOException { - /* - * This is weird, but this class is used both in clients and in servers, and we want to avoid needing to depend on servlet-api in clients since there is no point. So we dynamically load a class - * that does the servlet processing in servers. Down the road it may make sense to just split the method binding classes into server and client versions, but this isn't actually a huge deal I - * don't think. - */ - IRequestReader reader = ourRequestReader; - if (reader == null) { - try { - Class.forName("javax.servlet.ServletInputStream"); - String className = BaseMethodBinding.class.getName() + "$" + "ActiveRequestReader"; - try { - reader = (IRequestReader) Class.forName(className).newInstance(); - } catch (Exception e1) { - throw new ConfigurationException("Failed to instantiate class " + className, e1); - } - } catch (ClassNotFoundException e) { - String className = BaseMethodBinding.class.getName() + "$" + "InactiveRequestReader"; - try { - reader = (IRequestReader) Class.forName(className).newInstance(); - } catch (Exception e1) { - throw new ConfigurationException("Failed to instantiate class " + className, e1); - } - } - ourRequestReader = reader; - } - - InputStream inputStream = reader.getInputStream(theRequest); - byte[] requestContents = IOUtils.toByteArray(inputStream); - - theRequest.setRawRequest(requestContents); - - return requestContents; - } - /** * Subclasses may override this method (but should also call super.{@link #populateActionRequestDetailsForInterceptor(RequestDetails, ActionRequestDetails, Object[])} to provide method specifics to the * interceptors. @@ -421,12 +384,17 @@ public abstract class BaseMethodBinding implements IClientResponseHandler DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class); Transaction transaction = theMethod.getAnnotation(Transaction.class); Operation operation = theMethod.getAnnotation(Operation.class); + GetPage getPage = theMethod.getAnnotation(GetPage.class); // ** if you add another annotation above, also add it to the next line: - if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, getTags, addTags, deleteTags, transaction, operation)) { + if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, getTags, addTags, deleteTags, transaction, operation, getPage)) { return null; } + if (getPage != null) { + return new PageMethodBinding(theContext, theMethod); + } + Class returnType; Class returnTypeFromRp = null; @@ -641,19 +609,19 @@ public abstract class BaseMethodBinding implements IClientResponseHandler } /** - * @see BaseMethodBinding#loadRequestContents(RequestDetails) + * @see ServletRequestDetails#getByteStreamRequestContents() */ - static class ActiveRequestReader implements IRequestReader { + public static class ActiveRequestReader implements IRequestReader { @Override public InputStream getInputStream(RequestDetails theRequestDetails) throws IOException { - return theRequestDetails.getServletRequest().getInputStream(); + return theRequestDetails.getInputStream(); } } /** - * @see BaseMethodBinding#loadRequestContents(RequestDetails) + * @see ServletRequestDetails#getByteStreamRequestContents() */ - static class InactiveRequestReader implements IRequestReader { + public static class InactiveRequestReader implements IRequestReader { @Override public InputStream getInputStream(RequestDetails theRequestDetails) { throw new IllegalStateException("The servlet-api JAR is not found on the classpath. Please check that this library is available."); @@ -661,9 +629,9 @@ public abstract class BaseMethodBinding implements IClientResponseHandler } /** - * @see BaseMethodBinding#loadRequestContents(RequestDetails) + * @see ServletRequestDetails#getByteStreamRequestContents() */ - private static interface IRequestReader { + public static interface IRequestReader { InputStream getInputStream(RequestDetails theRequestDetails) throws IOException; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java index 7d9f021ed63..37316520fa4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Method; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -41,9 +42,11 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -68,24 +71,6 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { // if (requestContainsResource()) { // requestContents = parseIncomingServerResource(theRequest); // } else { // requestContents = null; // } - Object[] params = createParametersForServerRequest(theRequest, requestContents); + Object[] params = createParametersForServerRequest(theRequest); addParametersForServerRequest(theRequest, params); - HttpServletResponse servletResponse = theRequest.getServletResponse(); /* * No need to catch nd handle exceptions here, we already handle them one level up @@ -182,13 +164,16 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding= 0; i--) { IServerInterceptor next = theServer.getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, outcome, theRequest.getServletRequest(), theRequest.getServletResponse()); + boolean continueProcessing = next.outgoingResponse(theRequest, outcome); if (!continueProcessing) { - return; + return null; } } - boolean allowPrefer = false; + return returnResponse(theRequest, response, outcome, resource); + } + + private int getOperationStatus(MethodOutcome response) { switch (getRestOperationType()) { case CREATE: if (response == null) { @@ -196,23 +181,17 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { - byte[] requestContents = loadRequestContents(theRequest); +// byte[] requestContents = loadRequestContents(theRequest); + byte[] requestContents = null; final ResourceOrDstu1Bundle responseObject = invokeServer(theServer, theRequest, requestContents); @@ -245,19 +244,20 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = theServer.getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getResource(), theRequest.getServletRequest(), theRequest.getServletResponse()); + boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getResource()); if (!continueProcessing) { - return; + return null; } } boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); - HttpServletResponse response = theRequest.getServletResponse(); - RestfulServerUtils.streamResponseAsResource(theServer, response, responseObject.getResource(), prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), - isAddContentLocationHeader(), theRequest); + + return theRequest.getResponse().streamResponseAsResource(responseObject.getResource(), prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), + isAddContentLocationHeader()); + } else { // Is this request coming from a browser - String uaHeader = theRequest.getServletRequest().getHeader("user-agent"); + String uaHeader = theRequest.getHeader("user-agent"); boolean requestIsBrowser = false; if (uaHeader != null && uaHeader.contains("Mozilla")) { requestIsBrowser = true; @@ -265,26 +265,24 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = theServer.getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getDstu1Bundle(), theRequest.getServletRequest(), theRequest.getServletResponse()); + boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getDstu1Bundle()); if (!continueProcessing) { ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; + return null; } } - HttpServletResponse response = theRequest.getServletResponse(); - RestfulServerUtils.streamResponseAsBundle(theServer, response, responseObject.getDstu1Bundle(), theRequest.getFhirServerBase(), summaryMode, theRequest.isRespondGzip(), requestIsBrowser, - theRequest); + return theRequest.getResponse().streamResponseAsBundle(responseObject.getDstu1Bundle(), summaryMode, theRequest.isRespondGzip(), requestIsBrowser); } } - public ResourceOrDstu1Bundle invokeServer(RestfulServer theServer, RequestDetails theRequest, byte[] requestContents) { + public ResourceOrDstu1Bundle invokeServer(IRestfulServer theServer, RequestDetails theRequest, byte[] requestContents) { // Method params Object[] params = new Object[getParameters().size()]; for (int i = 0; i < getParameters().size(); i++) { IParameter param = getParameters().get(i); if (param != null) { - params[i] = param.translateQueryParametersIntoServerArgument(theRequest, requestContents, this); + params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this); } } @@ -300,7 +298,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi /* * Figure out the self-link for this request */ - String serverBase = theServer.getServerBaseForRequest(theRequest.getServletRequest()); + String serverBase = theRequest.getServerBaseForRequest(); String linkSelf; StringBuilder b = new StringBuilder(); b.append(serverBase); @@ -349,6 +347,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi responseObject = new ResourceOrDstu1Bundle(resource); break; + } else { Set includes = getRequestIncludesFromParams(params); @@ -356,14 +355,20 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi if (count == null) { count = result.preferredPageSize(); } - + + Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET); + if (offsetI == null || offsetI < 0) { + offsetI = 0; + } + int start = Math.max(0, Math.min(offsetI, result.size() - 1)); + IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); - EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest.getServletRequest(), theServer.getDefaultResponseEncoding()); + EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest, theServer.getDefaultResponseEncoding()); EncodingEnum linkEncoding = theRequest.getParameters().containsKey(Constants.PARAM_FORMAT) ? responseEncoding : null; boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); - bundleFactory.initializeBundleFromBundleProvider(theServer, result, linkEncoding, theRequest.getFhirServerBase(), linkSelf, prettyPrint, 0, count, null, getResponseBundleType(), includes); + bundleFactory.initializeBundleFromBundleProvider(theServer, result, linkEncoding, theRequest.getFhirServerBase(), linkSelf, prettyPrint, start, count, null, getResponseBundleType(), includes); Bundle bundle = bundleFactory.getDstu1Bundle(); if (bundle != null) { responseObject = new ResourceOrDstu1Bundle(bundle); @@ -393,7 +398,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi return responseObject; } - public abstract Object invokeServer(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException; + public abstract Object invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException; /** * Should the response include a Content-Location header. Search method bunding (and any others?) may override this to disable the content-location, since it doesn't make sense diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java index b97b05436ac..bcb77d9f9ca 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java @@ -61,10 +61,10 @@ class ConditionalParamBinder implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { if (myOperationType == RestOperationTypeEnum.CREATE) { - String retVal = theRequest.getServletRequest().getHeader(Constants.HEADER_IF_NONE_EXIST); + String retVal = theRequest.getHeader(Constants.HEADER_IF_NONE_EXIST); if (isBlank(retVal)) { return null; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConformanceMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConformanceMethodBinding.java index 3e751b41c6a..c6ecbcbcf1f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConformanceMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConformanceMethodBinding.java @@ -31,6 +31,7 @@ import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.IBundleProvider; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -72,7 +73,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding } @Override - public IBundleProvider invokeServer(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException { + public IBundleProvider invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException { IBaseResource conf = (IBaseResource) invokeServerMethod(theServer, theRequest, theMethodParams); return new SimpleBundleProvider(conf); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CountParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CountParameter.java index 87003347a4e..6ce2ea39276 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CountParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CountParameter.java @@ -54,7 +54,7 @@ public class CountParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { String[] sinceParams = theRequest.getParameters().remove(Constants.PARAM_COUNT); if (sinceParams != null) { if (sinceParams.length > 0) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchMethodBinding.java index 00acc18a617..8b4296fbf41 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchMethodBinding.java @@ -37,7 +37,7 @@ import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IDynamicSearchResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -86,7 +86,7 @@ public class DynamicSearchMethodBinding extends BaseResourceReturningMethodBindi } @Override - public IBundleProvider invokeServer(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { + public IBundleProvider invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { if (myIdParamIndex != null) { theMethodParams[myIdParamIndex] = theRequest.getId(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchParameter.java index bafd18ff861..ad89e934285 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DynamicSearchParameter.java @@ -67,7 +67,7 @@ public class DynamicSearchParameter implements IParameter { @SuppressWarnings("unchecked") @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { SearchParameterMap retVal = new SearchParameterMap(); for (String next : theRequest.getParameters().keySet()) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ElementsParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ElementsParameter.java index 62768163752..d08f12630cc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ElementsParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ElementsParameter.java @@ -71,7 +71,7 @@ public class ElementsParameter implements IParameter { @Override @SuppressWarnings({ "rawtypes", "unchecked" }) - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { Set value = getElementsValueOrNull(theRequest); if (value == null || value.isEmpty()) { return null; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/GetTagsMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/GetTagsMethodBinding.java index 30e52674fb4..47d44af4794 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/GetTagsMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/GetTagsMethodBinding.java @@ -21,15 +21,12 @@ package ca.uhn.fhir.rest.method; */ import java.io.IOException; -import java.io.PrintWriter; import java.io.Reader; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletResponse; - import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.ConfigurationException; @@ -44,10 +41,8 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.server.Constants; -import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; @@ -168,8 +163,8 @@ public class GetTagsMethodBinding extends BaseMethodBinding { } @Override - public void invokeServer(RestfulServer theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { - Object[] params = createParametersForServerRequest(theRequest, null); + public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { + Object[] params = createParametersForServerRequest(theRequest); if (myIdParamIndex != null) { params[myIdParamIndex] = theRequest.getId(); @@ -182,29 +177,13 @@ public class GetTagsMethodBinding extends BaseMethodBinding { for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = theServer.getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, resp, theRequest.getServletRequest(), theRequest.getServletResponse()); + boolean continueProcessing = next.outgoingResponse(theRequest, resp); if (!continueProcessing) { - return; + return null; } } - EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theServer, theRequest.getServletRequest()); - - HttpServletResponse response = theRequest.getServletResponse(); - response.setContentType(responseEncoding.getResourceContentType()); - response.setStatus(Constants.STATUS_HTTP_200_OK); - response.setCharacterEncoding(Constants.CHARSET_NAME_UTF8); - - theServer.addHeadersToResponse(response); - - IParser parser = responseEncoding.newParser(getContext()); - parser.setPrettyPrint(RestfulServerUtils.prettyPrintResponse(theServer, theRequest)); - PrintWriter writer = response.getWriter(); - try { - parser.encodeTagListToWriter(resp, writer); - } finally { - writer.close(); - } + return theRequest.getResponse().returnResponse(ParseAction.create(resp), Constants.STATUS_HTTP_200_OK, false, null, null); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java index f5a2a0330b9..fb83132e0d3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HistoryMethodBinding.java @@ -26,7 +26,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.List; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -43,7 +42,7 @@ import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -155,7 +154,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding { } @Override - public IBundleProvider invokeServer(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { + public IBundleProvider invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { if (myIdParamIndex != null) { theMethodParams[myIdParamIndex] = theRequest.getId(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/IParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/IParameter.java index 51bd0febf5d..c98baad5171 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/IParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/IParameter.java @@ -48,7 +48,7 @@ public interface IParameter { * @param theMethodBinding TODO * @return Returns the argument object as it will be passed to the {@link IResourceProvider} method. */ - Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException; + Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException; void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/IRestfulHeader.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/IRestfulHeader.java new file mode 100644 index 00000000000..2abce5282a8 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/IRestfulHeader.java @@ -0,0 +1,5 @@ +package ca.uhn.fhir.rest.method; + +public interface IRestfulHeader { + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/NullParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/NullParameter.java index 428bbd27f62..b115c43546d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/NullParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/NullParameter.java @@ -39,7 +39,7 @@ class NullParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { // nothing return null; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java index 9242fb42fcc..6365f7f7637 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java @@ -51,7 +51,7 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.server.IBundleProvider; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; @@ -246,7 +246,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { } @Override - public Object invokeServer(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException { + public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException { if (theRequest.getRequestType() == RequestTypeEnum.POST) { // always ok } else if (theRequest.getRequestType() == RequestTypeEnum.GET) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java index bd1c5efa7be..cf9bf56e18a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java @@ -24,7 +24,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; -import java.util.Date; import java.util.List; import java.util.Map; @@ -156,7 +155,7 @@ public class OperationParameter implements IParameter { @SuppressWarnings("unchecked") @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { List matchingParamValues = new ArrayList(); if (theRequest.getRequestType() == RequestTypeEnum.GET) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PageMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PageMethodBinding.java new file mode 100644 index 00000000000..a19d5459790 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PageMethodBinding.java @@ -0,0 +1,178 @@ +package ca.uhn.fhir.rest.method; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.http.HttpServletResponse; + +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.model.valueset.BundleTypeEnum; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; +import ca.uhn.fhir.rest.method.BaseResourceReturningMethodBinding.ResourceOrDstu1Bundle; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.IBundleProvider; +import ca.uhn.fhir.rest.server.IPagingProvider; +import ca.uhn.fhir.rest.server.IRestfulServer; +import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory; +import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import net.sourceforge.cobertura.CoverageIgnore; + +public class PageMethodBinding extends BaseResourceReturningMethodBinding { + + public PageMethodBinding(FhirContext theContext, Method theMethod) { + super(null, theMethod, theContext, null); + } + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PageMethodBinding.class); + + public IBaseResource provider() { + return null; + } + + @Override + protected BundleTypeEnum getResponseBundleType() { + return null; + } + + @Override + public ReturnTypeEnum getReturnType() { + return ReturnTypeEnum.BUNDLE; + } + + @Override + public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { + return handlePagingRequest(theServer, theRequest, null, theRequest.getParameters().get(Constants.PARAM_PAGINGACTION)[0]); + } + + @Override + public ResourceOrDstu1Bundle invokeServer(IRestfulServer theServer, RequestDetails theRequest, byte[] requestContents) { + IBase bundle = handlePagingRequest(theServer, theRequest, null, theRequest.getParameters().get(Constants.PARAM_PAGINGACTION)[0]); + if (bundle instanceof Bundle) { + return new ResourceOrDstu1Bundle((Bundle) bundle); + } else { + return new ResourceOrDstu1Bundle((IBaseResource) bundle); + } + } + + private IBase handlePagingRequest(IRestfulServer theServer, RequestDetails theRequest, HttpServletResponse theResponse, String thePagingAction) { + IPagingProvider pagingProvider = theServer.getPagingProvider(); + if (pagingProvider == null) { + throw new InvalidRequestException("This server does not support paging"); + } + IBundleProvider resultList = pagingProvider.retrieveResultList(thePagingAction); + if (resultList == null) { + ourLog.info("Client requested unknown paging ID[{}]", thePagingAction); + throw new ResourceGoneException("Search ID[" + thePagingAction + "] does not exist and may have expired."); + } + + Integer count = RestfulServerUtils.extractCountParameter(theRequest); + if (count == null) { + count = pagingProvider.getDefaultPageSize(); + } else if (count > pagingProvider.getMaximumPageSize()) { + count = pagingProvider.getMaximumPageSize(); + } + + Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET); + if (offsetI == null || offsetI < 0) { + offsetI = 0; + } + + int start = Math.min(offsetI, resultList.size() - 1); + + EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest, theServer.getDefaultResponseEncoding()); + boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); + + IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); + + Set includes = new HashSet(); + String[] reqIncludes = theRequest.getParameters().get(Constants.PARAM_INCLUDE); + if (reqIncludes != null) { + for (String nextInclude : reqIncludes) { + includes.add(new Include(nextInclude)); + } + } + + String linkSelfBase = theRequest.getFhirServerBase(); // myServerAddressStrategy.determineServerBase(getServletContext(), + // theRequest.getServletRequest()); + String completeUrl = theRequest.getCompleteUrl(); + String linkSelf = linkSelfBase + completeUrl.substring(theRequest.getCompleteUrl().indexOf('?')); + + BundleTypeEnum bundleType = null; + String[] bundleTypeValues = theRequest.getParameters().get(Constants.PARAM_BUNDLETYPE); + if (bundleTypeValues != null) { + bundleType = BundleTypeEnum.VALUESET_BINDER.fromCodeString(bundleTypeValues[0]); + } + + bundleFactory.initializeBundleFromBundleProvider(theServer, resultList, responseEncoding, theRequest.getFhirServerBase(), linkSelf, prettyPrint, start, count, thePagingAction, bundleType, includes); + + Bundle bundle = bundleFactory.getDstu1Bundle(); + if (bundle != null) { + return bundle; + } else { + return bundleFactory.getResourceBundle(); + } + // if (bundle != null) { + // for (int i = getInterceptors().size() - 1; i >= 0; i--) { + // IServerInterceptor next = getInterceptors().get(i); + // boolean continueProcessing = next.outgoingResponse(theRequest, bundle, theRequest.getServletRequest(), + // theRequest.getServletResponse()); + // if (!continueProcessing) { + // ourLog.debug("Interceptor {} returned false, not continuing processing"); + // return; + // } + // } + // theRequest.getResponse().streamResponseAsBundle(bundle, summaryMode, respondGzip, requestIsBrowser); + // } else { + // IBaseResource resBundle = bundleFactory.getResourceBundle(); + // for (int i = getInterceptors().size() - 1; i >= 0; i--) { + // IServerInterceptor next = getInterceptors().get(i); + // boolean continueProcessing = next.outgoingResponse(theRequest, resBundle, theRequest.getServletRequest(), + // theRequest.getServletResponse()); + // if (!continueProcessing) { + // ourLog.debug("Interceptor {} returned false, not continuing processing"); + // return; + // } + // } + // theRequest.getResponse().streamResponseAsResource(resBundle, prettyPrint, summaryMode, + // Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), false); + // } + } + + @Override + public RestOperationTypeEnum getRestOperationType() { + return RestOperationTypeEnum.GET_PAGE; + } + + @Override + public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) { + String[] pageId = theRequest.getParameters().get(Constants.PARAM_PAGINGACTION); + if (pageId == null || pageId.length == 0 || isBlank(pageId[0])) { + return false; + } + if (theRequest.getRequestType() != RequestTypeEnum.GET) { + return false; + } + return true; + } + + @CoverageIgnore + @Override + public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { + throw new UnsupportedOperationException(); + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ParseAction.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ParseAction.java new file mode 100644 index 00000000000..a3b3e29cc14 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ParseAction.java @@ -0,0 +1,44 @@ +package ca.uhn.fhir.rest.method; + +import java.io.IOException; +import java.io.Writer; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.model.api.TagList; +import ca.uhn.fhir.parser.IParser; + +/** + * @author Peter Van Houte + * + * @param A functional class that parses an outcome + */ +public abstract class ParseAction { + + protected T theOutcome; + + protected ParseAction(T outcome) { + this.theOutcome = outcome; + } + + public abstract void execute(IParser parser, Writer writer) throws IOException; + + public static ParseAction create(TagList outcome) { + return outcome == null ? null : new ParseAction(outcome) { + @Override + public void execute(IParser theParser, Writer theWriter) throws IOException { + theParser.encodeTagListToWriter(this.theOutcome, theWriter); + } + }; + } + + public static ParseAction create(IBaseResource outcome) { + return outcome == null ? null : new ParseAction(outcome) { + @Override + public void execute(IParser theParser, Writer theWriter) throws IOException { + theParser.encodeResourceToWriter(this.theOutcome, theWriter); + } + }; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java index bb0ae9a495b..ae5cad5595e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ReadMethodBinding.java @@ -51,7 +51,7 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.IBundleProvider; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -212,7 +212,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem } @Override - public IBundleProvider invokeServer(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { + public IBundleProvider invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { theMethodParams[myIdIndex] = MethodUtil.convertIdToType(theRequest.getId(), myIdParameterType); if (myVersionIdIndex != null) { theMethodParams[myVersionIdIndex] = new IdDt(theRequest.getId().getVersionIdPart()); @@ -222,7 +222,7 @@ public class ReadMethodBinding extends BaseResourceReturningMethodBinding implem IBundleProvider retVal = toResourceList(response); if (theRequest.getServer().getETagSupport() == ETagSupportEnum.ENABLED) { - String ifNoneMatch = theRequest.getFirstHeader(Constants.HEADER_IF_NONE_MATCH_LC); + String ifNoneMatch = theRequest.getHeader(Constants.HEADER_IF_NONE_MATCH_LC); if (retVal.size() == 1 && StringUtils.isNotBlank(ifNoneMatch)) { List responseResources = retVal.getResources(0, 1); IBaseResource responseResource = responseResources.get(0); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java index b4aa63017d7..61b00ae6a2d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java @@ -1,5 +1,10 @@ package ca.uhn.fhir.rest.method; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; + /* * #%L * HAPI FHIR - Core Library @@ -25,37 +30,31 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.IRestfulResponse; +import ca.uhn.fhir.rest.server.IRestfulServerDefaults; -public class RequestDetails { +public abstract class RequestDetails { - private Map> myHeaders = new HashMap>(); + private byte[] myRequestContents; private String myCompartmentName; private String myCompleteUrl; private String myFhirServerBase; private IdDt myId; private String myOperation; private Map myParameters; - private byte[] myRawRequest; private String myRequestPath; private RequestTypeEnum myRequestType; private String myResourceName; private boolean myRespondGzip; private RestOperationTypeEnum myRestOperationType; private String mySecondaryOperation; - private RestfulServer myServer; - private HttpServletRequest myServletRequest; - private HttpServletResponse myServletResponse; private Map> myUnqualifiedToQualifiedNames; - + private IRestfulResponse myResponse; + public String getCompartmentName() { return myCompartmentName; } @@ -64,6 +63,10 @@ public class RequestDetails { return myCompleteUrl; } + /** + * The fhir server base url, independant of the query being executed + * @return the fhir server base url + */ public String getFhirServerBase() { return myFhirServerBase; } @@ -80,10 +83,6 @@ public class RequestDetails { return myParameters; } - public byte[] getRawRequest() { - return myRawRequest; - } - /** * The part of the request URL that comes after the server base. *

@@ -110,17 +109,7 @@ public class RequestDetails { return mySecondaryOperation; } - public RestfulServer getServer() { - return myServer; - } - - public HttpServletRequest getServletRequest() { - return myServletRequest; - } - - public HttpServletResponse getServletResponse() { - return myServletResponse; - } + public abstract IRestfulServerDefaults getServer(); public Map> getUnqualifiedToQualifiedNames() { return myUnqualifiedToQualifiedNames; @@ -178,10 +167,6 @@ public class RequestDetails { } - public void setRawRequest(byte[] theRawRequest) { - myRawRequest = theRawRequest; - } - public void setRequestPath(String theRequestPath) { assert theRequestPath.length() == 0 || theRequestPath.charAt(0) != '/'; myRequestPath = theRequestPath; @@ -207,46 +192,66 @@ public class RequestDetails { mySecondaryOperation = theSecondaryOperation; } - public void setServer(RestfulServer theServer) { - myServer = theServer; - } + public IRestfulResponse getResponse() { + return myResponse; + } - public void setServletRequest(HttpServletRequest theRequest) { - myServletRequest = theRequest; - } + public void setResponse(IRestfulResponse theResponse) { + this.myResponse = theResponse; + } - public void setServletResponse(HttpServletResponse theServletResponse) { - myServletResponse = theServletResponse; - } - - public static RequestDetails withResourceAndParams(String theResourceName, RequestTypeEnum theRequestType, Set theParamNames) { - RequestDetails retVal = new RequestDetails(); - retVal.setResourceName(theResourceName); - retVal.setRequestType(theRequestType); - Map paramNames = new HashMap(); - for (String next : theParamNames) { - paramNames.put(next, new String[0]); + public abstract String getHeader(String name); + + public final byte[] loadRequestContents(RequestDetails theRequest) { + if (myRequestContents == null) { + myRequestContents = getByteStreamRequestContents(); } - retVal.setParameters(paramNames); - return retVal; + return myRequestContents; } - public void addHeader(String theName, String theValue) { - String lowerCase = theName.toLowerCase(); - ArrayList list = myHeaders.get(lowerCase); - if (list == null) { - list = new ArrayList(); - myHeaders.put(lowerCase, list); - } - list.add(theValue); - } + protected abstract byte[] getByteStreamRequestContents(); - public String getFirstHeader(String theName) { - ArrayList list = myHeaders.get(theName.toLowerCase()); - if (list == null || list.isEmpty()) { - return null; - } - return list.get(0); - } + public abstract List getHeaders(String name); + + /** + * Retrieves the body of the request as character data using + * a BufferedReader. The reader translates the character + * data according to the character encoding used on the body. + * Either this method or {@link #getInputStream} may be called to read the + * body, not both. + * + * @return a Reader containing the body of the request + * + * @exception UnsupportedEncodingException if the character set encoding + * used is not supported and the text cannot be decoded + * + * @exception IllegalStateException if {@link #getInputStream} method + * has been called on this request + * + * @exception IOException if an input or output exception occurred + * + * @see javax.servlet.http.HttpServletRequest#getInputStream + */ + public abstract Reader getReader() throws IOException; + + /** + * Retrieves the body of the request as binary data. + * Either this method or {@link #getReader} may be called to + * read the body, not both. + * + * @return a {@link InputStream} object containing + * the body of the request + * + * @exception IllegalStateException if the {@link #getReader} method + * has already been called for this request + * + * @exception IOException if an input or output exception occurred + */ + public abstract InputStream getInputStream() throws IOException; + + /** + * Returns the server base URL (with no trailing '/') for a given request + */ + public abstract String getServerBaseForRequest(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetailsParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetailsParameter.java index adab1dcb613..f4c1a15ed14 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetailsParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetailsParameter.java @@ -43,7 +43,7 @@ class RequestDetailsParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { return theRequest; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java index 78e49f612ee..5e7b14ada08 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SearchMethodBinding.java @@ -47,7 +47,7 @@ import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.param.BaseQueryParameter; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IBundleProvider; -import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -266,7 +266,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { } @Override - public IBundleProvider invokeServer(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { + public IBundleProvider invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { if (myIdParamIndex != null) { theMethodParams[myIdParamIndex] = theRequest.getId(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServerBaseParamBinder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServerBaseParamBinder.java index 8e7f44af757..3b3de0328d1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServerBaseParamBinder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServerBaseParamBinder.java @@ -43,7 +43,7 @@ class ServerBaseParamBinder implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { return theRequest.getFhirServerBase(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletRequestParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletRequestParameter.java index 31b132a9340..cff46c8bb1a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletRequestParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletRequestParameter.java @@ -30,6 +30,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; class ServletRequestParameter implements IParameter { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServletRequestParameter.class); @@ -43,8 +44,8 @@ class ServletRequestParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { - return theRequest.getServletRequest(); + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + return ((ServletRequestDetails) theRequest).getServletRequest(); } @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletResponseParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletResponseParameter.java index 89e571e8b1b..8137b5009c4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletResponseParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletResponseParameter.java @@ -30,6 +30,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; class ServletResponseParameter implements IParameter { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServletResponseParameter.class); @@ -43,8 +44,8 @@ class ServletResponseParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { - return theRequest.getServletResponse(); + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + return ((ServletRequestDetails) theRequest).getServletResponse(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SinceParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SinceParameter.java index 5e3a3f90a0d..d1cbd1e0b07 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SinceParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SinceParameter.java @@ -54,7 +54,7 @@ class SinceParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { String[] sinceParams = theRequest.getParameters().remove(Constants.PARAM_SINCE); if (sinceParams != null) { if (sinceParams.length > 0) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SortParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SortParameter.java index 27b21ee28a5..38e8bc51bd0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SortParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SortParameter.java @@ -65,7 +65,7 @@ public class SortParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { if (!theRequest.getParameters().containsKey(Constants.PARAM_SORT)) { if (!theRequest.getParameters().containsKey(Constants.PARAM_SORT_ASC)) { if (!theRequest.getParameters().containsKey(Constants.PARAM_SORT_DESC)) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SummaryEnumParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SummaryEnumParameter.java index b656e9ae106..d76a9362b12 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SummaryEnumParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/SummaryEnumParameter.java @@ -67,7 +67,7 @@ public class SummaryEnumParameter implements IParameter { @Override @SuppressWarnings({ "rawtypes", "unchecked" }) - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { Set value = getSummaryValueOrNull(theRequest); if (value == null || value.isEmpty()) { return null; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/TransactionMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/TransactionMethodBinding.java index bab344c93b9..4439e895cbe 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/TransactionMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/TransactionMethodBinding.java @@ -44,6 +44,7 @@ import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.param.TransactionParameter; import ca.uhn.fhir.rest.param.TransactionParameter.ParamStyle; import ca.uhn.fhir.rest.server.IBundleProvider; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -119,7 +120,7 @@ public class TransactionMethodBinding extends BaseResourceReturningMethodBinding @SuppressWarnings("unchecked") @Override - public Object invokeServer(RestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { + public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { /* * The design of HAPI's transaction method for DSTU1 support assumed that a transaction was just an update on a diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java index 0a4a5d9eac5..d463e9c7dc6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java @@ -37,7 +37,7 @@ import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { +public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceParam { private Integer myIdParameterIndex; @@ -58,7 +58,7 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP * We are being a bit lenient here, since technically the client is supposed to include the version in the * Content-Location header, but we allow it in the PUT URL as well.. */ - String locationHeader = theRequest.getServletRequest().getHeader(Constants.HEADER_CONTENT_LOCATION); + String locationHeader = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION); IdDt id = theRequest.getId(); if (isNotBlank(locationHeader)) { id = new IdDt(locationHeader); @@ -69,7 +69,7 @@ class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceP } } - String ifMatchValue = theRequest.getServletRequest().getHeader(Constants.HEADER_IF_MATCH); + String ifMatchValue = theRequest.getHeader(Constants.HEADER_IF_MATCH); if (isNotBlank(ifMatchValue)) { ifMatchValue = MethodUtil.parseETagValue(ifMatchValue); if (id != null && id.hasVersionIdPart() == false) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseQueryParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseQueryParameter.java index c27715a72c2..f38412016d4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseQueryParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseQueryParameter.java @@ -133,7 +133,7 @@ public abstract class BaseQueryParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { List paramList = new ArrayList(); String name = getName(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ResourceParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ResourceParameter.java index c4e941218e6..f626ae783c2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ResourceParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ResourceParameter.java @@ -30,7 +30,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.nio.charset.Charset; import java.util.Collection; -import java.util.Enumeration; import java.util.List; import java.util.Map; @@ -102,38 +101,37 @@ public class ResourceParameter implements IParameter { } @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { - switch (myMode) { - case BODY: - try { - return IOUtils.toString(createRequestReader(theRequest, theRequestContents)); - } catch (IOException e) { - // Shouldn't happen since we're reading from a byte array - throw new InternalErrorException("Failed to load request"); + switch (myMode) { + case BODY: + try { + return IOUtils.toString(createRequestReader(theRequest)); + } + catch (IOException e) { + // Shouldn't happen since we're reading from a byte array + throw new InternalErrorException("Failed to load request"); + } + case ENCODING: + return RestfulServerUtils.determineRequestEncoding(theRequest); + case RESOURCE: + default: + return parseResourceFromRequest(theRequest, theMethodBinding, myResourceType); } - case ENCODING: - return RestfulServerUtils.determineRequestEncoding(theRequest); - case RESOURCE: - break; - } - - IBaseResource retVal = parseResourceFromRequest(theRequest, theMethodBinding, myResourceType); - - return retVal; +// } } - - static Reader createRequestReader(byte[] theRequestContents, Charset charset) { - Reader requestReader = new InputStreamReader(new ByteArrayInputStream(theRequestContents), charset); + + public static Reader createRequestReader(RequestDetails theRequest, Charset charset) { + Reader requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents(theRequest)), charset); return requestReader; } - static Reader createRequestReader(RequestDetails theRequest, byte[] theRequestContents) { - return createRequestReader(theRequestContents, determineRequestCharset(theRequest)); - } + public static Reader createRequestReader(RequestDetails theRequest) throws IOException { + return createRequestReader(theRequest, determineRequestCharset(theRequest)); + } - static Charset determineRequestCharset(RequestDetails theRequest) { - String ct = theRequest.getServletRequest().getHeader(Constants.HEADER_CONTENT_TYPE); + public static Charset determineRequestCharset(RequestDetails theRequest) { + String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); Charset charset = null; if (isNotBlank(ct)) { @@ -144,20 +142,21 @@ public class ResourceParameter implements IParameter { charset = Charset.forName("UTF-8"); } return charset; - } - + } + @SuppressWarnings("unchecked") - public static T loadResourceFromRequest(RequestDetails theRequest, BaseMethodBinding theMethodBinding, Class theResourceType) { + public static T loadResourceFromRequest(RequestDetails theRequest, BaseMethodBinding theMethodBinding, + Class theResourceType) { FhirContext ctx = theRequest.getServer().getFhirContext(); final Charset charset = determineRequestCharset(theRequest); - Reader requestReader = createRequestReader(theRequest.getRawRequest(), charset); + Reader requestReader = createRequestReader(theRequest, charset); RestOperationTypeEnum restOperationType = theMethodBinding != null ? theMethodBinding.getRestOperationType() : null; EncodingEnum encoding = RestfulServerUtils.determineRequestEncodingNoDefault(theRequest); if (encoding == null) { - String ctValue = theRequest.getServletRequest().getHeader(Constants.HEADER_CONTENT_TYPE); + String ctValue = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); if (ctValue != null) { if (ctValue.startsWith("application/x-www-form-urlencoded")) { String msg = theRequest.getServer().getFhirContext().getLocalizer().getMessage(ResourceParameter.class, "invalidContentTypeInRequest", ctValue, theMethodBinding.getRestOperationType()); @@ -171,7 +170,7 @@ public class ResourceParameter implements IParameter { String body; try { body = IOUtils.toString(requestReader); - } catch (IOException e) { + } catch (IOException e) { // This shouldn't happen since we're reading from a byte array.. throw new InternalErrorException(e); } @@ -180,7 +179,7 @@ public class ResourceParameter implements IParameter { String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "noContentTypeInRequest", restOperationType); throw new InvalidRequestException(msg); } else { - requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.getRawRequest()), charset); + requestReader = new InputStreamReader(new ByteArrayInputStream(theRequest.loadRequestContents(theRequest)), charset); } } else { String msg = ctx.getLocalizer().getMessage(ResourceParameter.class, "invalidContentTypeInRequest", ctValue, restOperationType); @@ -203,8 +202,7 @@ public class ResourceParameter implements IParameter { if (theRequest.getServer().getFhirContext().getVersion().getVersion().equals(FhirVersionEnum.DSTU1)) { TagList tagList = new TagList(); - for (Enumeration enumeration = theRequest.getServletRequest().getHeaders(Constants.HEADER_CATEGORY); enumeration.hasMoreElements();) { - String nextTagComplete = enumeration.nextElement(); + for (String nextTagComplete : theRequest.getHeaders(Constants.HEADER_CATEGORY)) { MethodUtil.parseTagValue(tagList, nextTagComplete); } if (tagList.isEmpty() == false) { @@ -212,23 +210,23 @@ public class ResourceParameter implements IParameter { } } return retVal; - } - + } + public static IBaseResource parseResourceFromRequest(RequestDetails theRequest, BaseMethodBinding theMethodBinding, Class theResourceType) { IBaseResource retVal; if (IBaseBinary.class.isAssignableFrom(theResourceType)) { FhirContext ctx = theRequest.getServer().getFhirContext(); - String ct = theRequest.getServletRequest().getHeader(Constants.HEADER_CONTENT_TYPE); + String ct = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); IBaseBinary binary = (IBaseBinary) ctx.getResourceDefinition("Binary").newInstance(); binary.setContentType(ct); - binary.setContent(theRequest.getRawRequest()); + binary.setContent(theRequest.loadRequestContents(theRequest)); retVal = binary; } else { retVal = loadResourceFromRequest(theRequest, theMethodBinding, theResourceType); } return retVal; - } + } public enum Mode { BODY, ENCODING, RESOURCE diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TransactionParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TransactionParameter.java index 5cc912672d7..2c56a61110f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TransactionParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TransactionParameter.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.param; +import java.io.IOException; + /* * #%L * HAPI FHIR - Core Library @@ -48,6 +50,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; public class TransactionParameter implements IParameter { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TransactionParameter.class); private FhirContext myContext; private ParamStyle myParamStyle; private Class myResourceBundleType; @@ -96,35 +99,41 @@ public class TransactionParameter implements IParameter { // nothing } - + @Override - public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, byte[] theRequestContents, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { - + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { // TODO: don't use a default encoding, just fail! EncodingEnum encoding = RestfulServerUtils.determineRequestEncoding(theRequest); - IParser parser = encoding.newParser(myContext); + IParser parser = encoding.newParser(theRequest.getServer().getFhirContext()); + + Reader reader; + try { + reader = ResourceParameter.createRequestReader(theRequest); + } + catch (IOException e) { + ourLog.error("Could not load request resource", e); + throw new InvalidRequestException(String.format("Could not load request resource: %s", e.getMessage())); + } - Reader reader = ResourceParameter.createRequestReader(theRequest, theRequestContents); - switch (myParamStyle) { - case DSTU1_BUNDLE: { - Bundle bundle; - bundle = parser.parseBundle(reader); - return bundle; - } - case RESOURCE_LIST: { - Bundle bundle = parser.parseBundle(reader); - ArrayList resourceList = new ArrayList(); - for (BundleEntry next : bundle.getEntries()) { - if (next.getResource() != null) { - resourceList.add(next.getResource()); - } + case DSTU1_BUNDLE: { + Bundle bundle; + bundle = parser.parseBundle(reader); + return bundle; } - return resourceList; - } - case RESOURCE_BUNDLE: - return parser.parseResource(myResourceBundleType, reader); + case RESOURCE_LIST: { + Bundle bundle = parser.parseBundle(reader); + ArrayList resourceList = new ArrayList(); + for (BundleEntry next : bundle.getEntries()) { + if (next.getResource() != null) { + resourceList.add(next.getResource()); + } + } + return resourceList; + } + case RESOURCE_BUNDLE: + return parser.parseResource(myResourceBundleType, reader); } throw new IllegalStateException("Unknown type: " + myParamStyle); // should not happen diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulResponse.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulResponse.java new file mode 100644 index 00000000000..afbb3000fd3 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulResponse.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.rest.server; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.Set; + +import org.hl7.fhir.instance.model.api.IBaseBinary; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.SummaryEnum; +import ca.uhn.fhir.rest.method.ParseAction; + +public interface IRestfulResponse { + + Object streamResponseAsResource(IBaseResource resource, boolean prettyPrint, Set summaryMode, int operationStatus, boolean respondGzip, boolean addContentLocationHeader) + throws IOException; + Object streamResponseAsBundle(Bundle bundle, Set summaryMode, boolean respondGzip, boolean requestIsBrowser) + throws IOException; + + Object returnResponse(ParseAction outcome, int operationStatus, boolean allowPrefer, MethodOutcome response, String resourceName) throws IOException; + + Writer getResponseWriter(int statusCode, String contentType, String charset, boolean respondGzip) throws UnsupportedEncodingException, IOException; + Object sendWriterResponse(int status, String contentType, String charset, Writer writer) throws IOException; + + void addHeader(String headerKey, String headerValue); + + Object sendAttachmentResponse(IBaseBinary bin, int stausCode, String contentType) throws IOException; +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServer.java new file mode 100644 index 00000000000..da77bd1c9c2 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServer.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.rest.server; + +import java.util.List; + +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * 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% + */ +public interface IRestfulServer extends IRestfulServerDefaults { + + List getInterceptors(); + + IPagingProvider getPagingProvider(); + + BundleInclusionRule getBundleInclusionRule(); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServerDefaults.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServerDefaults.java new file mode 100644 index 00000000000..6029f2ebeb5 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServerDefaults.java @@ -0,0 +1,46 @@ +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.context.FhirContext; + +public interface IRestfulServerDefaults { + + /** + * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to + * creating their own. + */ + FhirContext getFhirContext(); + + /** + * Should the server "pretty print" responses by default (requesting clients can always override this default by supplying an Accept header in the request, or a _pretty + * parameter in the request URL. + *

+ * The default is false + *

+ * + * @return Returns the default pretty print setting + */ + boolean isDefaultPrettyPrint(); + + /** + * @return Returns the server support for ETags (will not be null). Default is {@link RestfulServer#DEFAULT_ETAG_SUPPORT} + */ + ETagSupportEnum getETagSupport(); + + /** + * @return Returns the setting for automatically adding profile tags + */ + AddProfileTagEnum getAddProfileTag(); + + /** + * @return Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with the _format URL parameter, or with an Accept header + * in the request. The default is {@link EncodingEnum#XML}. Will not return null. + */ + EncodingEnum getDefaultResponseEncoding(); + + /** + * @return If true the server will use browser friendly content-types (instead of standard FHIR ones) when it detects that the request is coming from a browser + * instead of a FHIR + */ + boolean isUseBrowserFriendlyContentTypes(); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServerUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServerUtil.java new file mode 100644 index 00000000000..b55330d0836 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IRestfulServerUtil.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.rest.server; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.rest.method.BaseMethodBinding; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.param.ResourceParameter.Mode; +import ca.uhn.fhir.rest.param.TransactionParameter.ParamStyle; + +public interface IRestfulServerUtil { + + Object getResourceParameter( + RequestDetails requestDetails, + Mode myMode, + BaseMethodBinding theMethodBinding, + Class myResourceType); + + Object getRequestResource(RequestDetails theRequest, ParamStyle myParamStyle, Class myResourceBundleType); + + T loadResourceFromRequest(RequestDetails theRequest, BaseMethodBinding theMethodBinding, Class theResourceType); + + IBaseResource parseResourceFromRequest(RequestDetails theRequest, BaseMethodBinding theMethodBinding, Class theResourceType); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IVersionSpecificBundleFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IVersionSpecificBundleFactory.java index 77394f88f84..d2298ee5394 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IVersionSpecificBundleFactory.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/IVersionSpecificBundleFactory.java @@ -40,7 +40,7 @@ public interface IVersionSpecificBundleFactory { void addRootPropertiesToBundle(String theAuthor, String theServerBase, String theCompleteUrl, Integer theTotalResults, BundleTypeEnum theBundleType, IPrimitiveType theLastUpdated); - void initializeBundleFromBundleProvider(RestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, + void initializeBundleFromBundleProvider(IRestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, int theOffset, Integer theCount, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes); Bundle getDstu1Bundle(); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/PageProvider.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/PageProvider.java new file mode 100644 index 00000000000..e6dc7af9992 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/PageProvider.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.rest.annotation.GetPage; + +public class PageProvider { + +// @GetPage(dstu1=true) +// public Bundle getPageDstu1() { +// return null; +// } + + @GetPage() + public IResource getPage() { + return null; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java new file mode 100644 index 00000000000..7d30b9f7959 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java @@ -0,0 +1,66 @@ +package ca.uhn.fhir.rest.server; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.rest.api.SummaryEnum; +import ca.uhn.fhir.rest.method.RequestDetails; + +public abstract class RestfulResponse implements IRestfulResponse { + + private T theRequestDetails; + private ConcurrentHashMap theHeaders = new ConcurrentHashMap(); + + public RestfulResponse(T requestDetails) { + this.theRequestDetails = requestDetails; + } + + @Override + public final Object streamResponseAsResource(IBaseResource resource, boolean prettyPrint, Set summaryMode, + int statusCode, boolean respondGzip, boolean addContentLocationHeader) + throws IOException { + return RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), resource, prettyPrint, summaryMode, statusCode, respondGzip, addContentLocationHeader, + respondGzip, getRequestDetails()); + + } + + @Override + public Object streamResponseAsBundle(Bundle bundle, Set summaryMode, boolean respondGzip, boolean requestIsBrowser) + throws IOException { + return RestfulServerUtils.streamResponseAsBundle(theRequestDetails.getServer(), bundle, summaryMode, requestIsBrowser, respondGzip, getRequestDetails()); + } + + @Override + public void addHeader(String headerKey, String headerValue) { + this.getHeaders().put(headerKey, headerValue); + } + + /** + * Get the http headers + * @return the headers + */ + public ConcurrentHashMap getHeaders() { + return theHeaders; + } + + /** + * Get the requestDetails + * @return the requestDetails + */ + public T getRequestDetails() { + return theRequestDetails; + } + + /** + * Set the requestDetails + * @param requestDetails the requestDetails to set + */ + public void setRequestDetails(T requestDetails) { + this.theRequestDetails = requestDetails; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index c55159994f2..9ac8c538d2f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -20,9 +20,10 @@ package ca.uhn.fhir.rest.server; * #L% */ import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -31,15 +32,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Enumeration; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.jar.Manifest; import javax.servlet.ServletException; import javax.servlet.UnavailableException; @@ -47,6 +46,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -55,18 +55,17 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.ProvidedResourceScanner; import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.model.api.Bundle; -import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.valueset.BundleTypeEnum; +import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.annotation.Destroy; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Initialize; +import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RequestTypeEnum; -import ca.uhn.fhir.rest.api.RestOperationTypeEnum; -import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.method.BaseMethodBinding; import ca.uhn.fhir.rest.method.ConformanceMethodBinding; +import ca.uhn.fhir.rest.method.PageMethodBinding; +import ca.uhn.fhir.rest.method.ParseAction; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -74,11 +73,12 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.VersionUtil; -public class RestfulServer extends HttpServlet { +public class RestfulServer extends HttpServlet implements IRestfulServer { private static final ExceptionHandlingInterceptor DEFAULT_EXCEPTION_HANDLER = new ExceptionHandlingInterceptor(); private static final long serialVersionUID = 1L; @@ -319,15 +319,12 @@ public class RestfulServer extends HttpServlet { return count; } - /** - * Returns the setting for automatically adding profile tags - * - * @see #setAddProfileTag(AddProfileTagEnum) - */ + @Override public AddProfileTagEnum getAddProfileTag() { return myAddProfileTag; } + @Override public BundleInclusionRule getBundleInclusionRule() { return myBundleInclusionRule; } @@ -336,13 +333,12 @@ public class RestfulServer extends HttpServlet { * Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with the _format URL parameter, or with an Accept header * in the request. The default is {@link EncodingEnum#XML}. Will not return null. */ + @Override public EncodingEnum getDefaultResponseEncoding() { return myDefaultResponseEncoding; } - /** - * Returns the server support for ETags (will not be null). Default is {@link #DEFAULT_ETAG_SUPPORT} - */ + @Override public ETagSupportEnum getETagSupport() { return myETagSupport; } @@ -351,6 +347,7 @@ public class RestfulServer extends HttpServlet { * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to * creating their own. */ + @Override public FhirContext getFhirContext() { if (myFhirContext == null) { myFhirContext = new FhirContext(); @@ -365,10 +362,12 @@ public class RestfulServer extends HttpServlet { /** * Returns a ist of all registered server interceptors */ + @Override public List getInterceptors() { return Collections.unmodifiableList(myInterceptors); } + @Override public IPagingProvider getPagingProvider() { return myPagingProvider; } @@ -414,7 +413,7 @@ public class RestfulServer extends HttpServlet { public IServerAddressStrategy getServerAddressStrategy() { return myServerAddressStrategy; } - + /** * Returns the server base URL (with no trailing '/') for a given request */ @@ -466,99 +465,15 @@ public class RestfulServer extends HttpServlet { return myServerVersion; } - private void handlePagingRequest(RequestDetails theRequest, HttpServletResponse theResponse, String thePagingAction) throws IOException { - IBundleProvider resultList = getPagingProvider().retrieveResultList(thePagingAction); - if (resultList == null) { - ourLog.info("Client requested unknown paging ID[{}]", thePagingAction); - theResponse.setStatus(Constants.STATUS_HTTP_410_GONE); - addHeadersToResponse(theResponse); - theResponse.setContentType("text/plain"); - theResponse.setCharacterEncoding("UTF-8"); - theResponse.getWriter().append("Search ID[" + thePagingAction + "] does not exist and may have expired."); - theResponse.getWriter().close(); - return; - } - - Integer count = RestfulServerUtils.extractCountParameter(theRequest); - if (count == null) { - count = getPagingProvider().getDefaultPageSize(); - } else if (count > getPagingProvider().getMaximumPageSize()) { - count = getPagingProvider().getMaximumPageSize(); - } - - Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET); - if (offsetI == null || offsetI < 0) { - offsetI = 0; - } - - int start = Math.min(offsetI, resultList.size() - 1); - - EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest.getServletRequest(), getDefaultResponseEncoding()); - boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(this, theRequest); - boolean requestIsBrowser = requestIsBrowser(theRequest.getServletRequest()); - Set summaryMode = RestfulServerUtils.determineSummaryMode(theRequest); - boolean respondGzip = theRequest.isRespondGzip(); - - IVersionSpecificBundleFactory bundleFactory = getFhirContext().newBundleFactory(); - - Set includes = new HashSet(); - String[] reqIncludes = theRequest.getServletRequest().getParameterValues(Constants.PARAM_INCLUDE); - if (reqIncludes != null) { - for (String nextInclude : reqIncludes) { - includes.add(new Include(nextInclude)); - } - } - - String linkSelfBase = getServerAddressStrategy().determineServerBase(getServletContext(), theRequest.getServletRequest()); - String linkSelf = linkSelfBase + theRequest.getCompleteUrl().substring(theRequest.getCompleteUrl().indexOf('?')); - - BundleTypeEnum bundleType = null; - String[] bundleTypeValues = theRequest.getParameters().get(Constants.PARAM_BUNDLETYPE); - if (bundleTypeValues != null) { - bundleType = BundleTypeEnum.VALUESET_BINDER.fromCodeString(bundleTypeValues[0]); - } - - bundleFactory.initializeBundleFromBundleProvider(this, resultList, responseEncoding, theRequest.getFhirServerBase(), linkSelf, prettyPrint, start, count, thePagingAction, bundleType, includes); - - Bundle bundle = bundleFactory.getDstu1Bundle(); - if (bundle != null) { - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, bundle, theRequest.getServletRequest(), theRequest.getServletResponse()); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - RestfulServerUtils.streamResponseAsBundle(this, theResponse, bundle, theRequest.getFhirServerBase(), summaryMode, respondGzip, requestIsBrowser, theRequest); - } else { - IBaseResource resBundle = bundleFactory.getResourceBundle(); - for (int i = getInterceptors().size() - 1; i >= 0; i--) { - IServerInterceptor next = getInterceptors().get(i); - boolean continueProcessing = next.outgoingResponse(theRequest, resBundle, theRequest.getServletRequest(), theRequest.getServletResponse()); - if (!continueProcessing) { - ourLog.debug("Interceptor {} returned false, not continuing processing"); - return; - } - } - RestfulServerUtils.streamResponseAsResource(this, theResponse, resBundle, prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), false, theRequest); - } - } protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { String fhirServerBase = null; boolean requestIsBrowser = requestIsBrowser(theRequest); - RequestDetails requestDetails = new RequestDetails(); + ServletRequestDetails requestDetails = new ServletRequestDetails(); requestDetails.setServer(this); requestDetails.setRequestType(theRequestType); requestDetails.setServletRequest(theRequest); requestDetails.setServletResponse(theResponse); - for (Enumeration iter = theRequest.getHeaderNames(); iter.hasMoreElements(); ) { - String nextName = iter.nextElement(); - for (Enumeration valueIter = theRequest.getHeaders(nextName); valueIter.hasMoreElements();) { - requestDetails.addHeader(nextName, valueIter.nextElement()); - } - } try { @@ -625,19 +540,19 @@ public class RestfulServer extends HttpServlet { requestDetails.setFhirServerBase(fhirServerBase); requestDetails.setCompleteUrl(completeUrl); - String pagingAction = theRequest.getParameter(Constants.PARAM_PAGINGACTION); - if (getPagingProvider() != null && isNotBlank(pagingAction)) { - requestDetails.setRestOperationType(RestOperationTypeEnum.GET_PAGE); - if (theRequestType != RequestTypeEnum.GET) { - /* - * We reconstruct the link-self URL using the request parameters, and this would break if the parameters came in using a POST. We could probably work around that but why bother unless - * someone comes up with a reason for needing it. - */ - throw new InvalidRequestException(getFhirContext().getLocalizer().getMessage(RestfulServer.class, "getPagesNonHttpGet")); - } - handlePagingRequest(requestDetails, theResponse, pagingAction); - return; - } +// String pagingAction = theRequest.getParameter(Constants.PARAM_PAGINGACTION); +// if (getPagingProvider() != null && isNotBlank(pagingAction)) { +// requestDetails.setRestOperationType(RestOperationTypeEnum.GET_PAGE); +// if (theRequestType != RequestTypeEnum.GET) { +// /* +// * We reconstruct the link-self URL using the request parameters, and this would break if the parameters came in using a POST. We could probably work around that but why bother unless +// * someone comes up with a reason for needing it. +// */ +// throw new InvalidRequestException(getFhirContext().getLocalizer().getMessage(RestfulServer.class, "getPagesNonHttpGet")); +// } +// handlePagingRequest(requestDetails, theResponse, pagingAction); +// return; +// } BaseMethodBinding resourceMethod = determineResourceMethod(requestDetails, requestPath); @@ -892,6 +807,13 @@ public class RestfulServer extends HttpServlet { invokeInitialize(next); } } + + try { + findResourceMethods(new PageProvider()); + } catch (Exception ex) { + ourLog.error("An error occurred while loading request handlers!", ex); + throw new ServletException("Failed to initialize FHIR Restful server", ex); + } myStarted = true; ourLog.info("A FHIR has been lit on this server"); @@ -1117,10 +1039,12 @@ public class RestfulServer extends HttpServlet { * * @return Returns the default pretty print setting */ + @Override public boolean isDefaultPrettyPrint() { return myDefaultPrettyPrint; } + @Override public boolean isUseBrowserFriendlyContentTypes() { return myUseBrowserFriendlyContentTypes; } @@ -1369,4 +1293,86 @@ public class RestfulServer extends HttpServlet { return userAgent != null && userAgent.contains("Mozilla"); } + public Object returnResponse(ServletRequestDetails theRequest, ParseAction outcome, int operationStatus, boolean allowPrefer, MethodOutcome response, + String resourceName) throws IOException { + HttpServletResponse servletResponse = theRequest.getServletResponse(); + servletResponse.setStatus(operationStatus); + servletResponse.setCharacterEncoding(Constants.CHARSET_NAME_UTF8); + addHeadersToResponse(servletResponse); + if(allowPrefer) { + addContentLocationHeaders(theRequest, servletResponse, response, resourceName); + } + if (outcome != null) { + EncodingEnum encoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequest); + servletResponse.setContentType(encoding.getResourceContentType()); + Writer writer = servletResponse.getWriter(); + IParser parser = encoding.newParser(getFhirContext()); + parser.setPrettyPrint(RestfulServerUtils.prettyPrintResponse(this, theRequest)); + try { + outcome.execute(parser, writer); + } finally { + writer.close(); + } + } else { + servletResponse.setContentType(Constants.CT_TEXT_WITH_UTF8); + Writer writer = servletResponse.getWriter(); + writer.close(); + } + // getMethod().in + return null; + } + + private void addContentLocationHeaders(RequestDetails theRequest, HttpServletResponse servletResponse, MethodOutcome response, String resourceName) { + if (response != null && response.getId() != null) { + addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_LOCATION, resourceName); + addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_CONTENT_LOCATION, resourceName); + } + } + + private void addLocationHeader(RequestDetails theRequest, HttpServletResponse theResponse, MethodOutcome response, String headerLocation, String resourceName) { + StringBuilder b = new StringBuilder(); + b.append(theRequest.getFhirServerBase()); + b.append('/'); + b.append(resourceName); + b.append('/'); + b.append(response.getId().getIdPart()); + if (response.getId().hasVersionIdPart()) { + b.append("/" + Constants.PARAM_HISTORY + "/"); + b.append(response.getId().getVersionIdPart()); + } else if (response.getVersionId() != null && response.getVersionId().isEmpty() == false) { + b.append("/" + Constants.PARAM_HISTORY + "/"); + b.append(response.getVersionId().getValue()); + } + theResponse.addHeader(headerLocation, b.toString()); + + } + + public RestulfulServerConfiguration createConfiguration() { + RestulfulServerConfiguration result = new RestulfulServerConfiguration(); + result.setResourceBindings(getResourceBindings()); + result.setServerBindings(getServerBindings()); + result.setImplementationDescription(getImplementationDescription()); + result.setServerVersion(getServerVersion()); + result.setServerName(getServerName()); + result.setFhirContext(getFhirContext()); + result.setServerAddressStrategy(myServerAddressStrategy); + InputStream inputStream = null; + try { + inputStream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF"); + if (inputStream != null) { + Manifest manifest = new Manifest(inputStream); + result.setConformanceDate(manifest.getMainAttributes().getValue("Build-Time")); + } + } + catch (IOException e) { + // fall through + } + finally { + if (inputStream != null) { + IOUtils.closeQuietly(inputStream); + } + } + return result; + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java index 23e3ae7e32b..5b919784fe5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java @@ -23,7 +23,6 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.IOException; -import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.URLEncoder; @@ -33,17 +32,15 @@ import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.GZIPOutputStream; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.DateUtils; @@ -215,10 +212,10 @@ public class RestfulServerUtils { public static EncodingEnum determineRequestEncodingNoDefault(RequestDetails theReq) { EncodingEnum retVal = null; - Enumeration acceptValues = theReq.getServletRequest().getHeaders(Constants.HEADER_CONTENT_TYPE); + Iterator acceptValues = theReq.getHeaders(Constants.HEADER_CONTENT_TYPE).iterator(); if (acceptValues != null) { - while (acceptValues.hasMoreElements() && retVal == null) { - String nextAcceptHeaderValue = acceptValues.nextElement(); + while (acceptValues.hasNext() && retVal == null) { + String nextAcceptHeaderValue = acceptValues.next(); if (nextAcceptHeaderValue != null && isNotBlank(nextAcceptHeaderValue)) { for (String nextPart : nextAcceptHeaderValue.split(",")) { int scIdx = nextPart.indexOf(';'); @@ -243,8 +240,8 @@ public class RestfulServerUtils { /** * Returns null if the request doesn't express that it wants FHIR. If it expresses that it wants XML and JSON equally, returns thePrefer. */ - public static EncodingEnum determineResponseEncodingNoDefault(HttpServletRequest theReq, EncodingEnum thePrefer) { - String[] format = theReq.getParameterValues(Constants.PARAM_FORMAT); + public static EncodingEnum determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer) { + String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT); if (format != null) { for (String nextFormat : format) { EncodingEnum retVal = Constants.FORMAT_VAL_TO_ENCODING.get(nextFormat); @@ -259,12 +256,11 @@ public class RestfulServerUtils { */ // text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, text/plain;q=0.8, image/png, */*;q=0.5 - Enumeration acceptValues = theReq.getHeaders(Constants.HEADER_ACCEPT); - if (acceptValues != null) { - float bestQ = -1f; - EncodingEnum retVal = null; - while (acceptValues.hasMoreElements()) { - String nextAcceptHeaderValue = acceptValues.nextElement(); + List acceptValues = theReq.getHeaders(Constants.HEADER_ACCEPT); + float bestQ = -1f; + EncodingEnum retVal = null; + if (acceptValues != null) { + for (String nextAcceptHeaderValue : acceptValues) { StringTokenizer tok = new StringTokenizer(nextAcceptHeaderValue, ","); while (tok.hasMoreTokens()) { String nextToken = tok.nextToken(); @@ -375,10 +371,10 @@ public class RestfulServerUtils { /** * Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's "_format" parameter and "Accept:" HTTP header. */ - public static EncodingEnum determineResponseEncodingWithDefault(RestfulServer theServer, HttpServletRequest theReq) { - EncodingEnum retVal = determineResponseEncodingNoDefault(theReq, theServer.getDefaultResponseEncoding()); + public static EncodingEnum determineResponseEncodingWithDefault(RequestDetails theReq) { + EncodingEnum retVal = determineResponseEncodingNoDefault(theReq, theReq.getServer().getDefaultResponseEncoding()); if (retVal == null) { - retVal = theServer.getDefaultResponseEncoding(); + retVal = theReq.getServer().getDefaultResponseEncoding(); } return retVal; } @@ -420,14 +416,13 @@ public class RestfulServerUtils { } public static Integer extractCountParameter(RequestDetails theRequest) { - String paramName = Constants.PARAM_COUNT; - return tryToExtractNamedParameter(theRequest, paramName); + return RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_COUNT); } public static IParser getNewParser(FhirContext theContext, RequestDetails theRequestDetails) { // Determine response encoding - EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails.getServer(), theRequestDetails.getServletRequest()); + EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails); IParser parser; switch (responseEncoding) { case JSON: @@ -444,17 +439,6 @@ public class RestfulServerUtils { return parser; } - static Writer getWriter(HttpServletResponse theHttpResponse, boolean theRespondGzip) throws UnsupportedEncodingException, IOException { - Writer writer; - if (theRespondGzip) { - theHttpResponse.addHeader(Constants.HEADER_CONTENT_ENCODING, Constants.ENCODING_GZIP); - writer = new OutputStreamWriter(new GZIPOutputStream(theHttpResponse.getOutputStream()), "UTF-8"); - } else { - writer = theHttpResponse.getWriter(); - } - return writer; - } - public static Set parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) { Set retVal = new HashSet(); @@ -476,7 +460,8 @@ public class RestfulServerUtils { try { q = Float.parseFloat(value); q = Math.max(q, 0.0f); - } catch (NumberFormatException e) { + } + catch (NumberFormatException e) { ourLog.debug("Invalid Accept header q value: {}", value); } } @@ -536,7 +521,7 @@ public class RestfulServerUtils { return null; } - public static boolean prettyPrintResponse(RestfulServer theServer, RequestDetails theRequest) { + public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) { Map requestParams = theRequest.getParameters(); String[] pretty = requestParams.get(Constants.PARAM_PRETTY); boolean prettyPrint; @@ -548,10 +533,9 @@ public class RestfulServerUtils { } } else { prettyPrint = theServer.isDefaultPrettyPrint(); - Enumeration acceptValues = theRequest.getServletRequest().getHeaders(Constants.HEADER_ACCEPT); + List acceptValues = theRequest.getHeaders(Constants.HEADER_ACCEPT); if (acceptValues != null) { - while (acceptValues.hasMoreElements()) { - String nextAcceptHeaderValue = acceptValues.nextElement(); + for (String nextAcceptHeaderValue : acceptValues) { if (nextAcceptHeaderValue.contains("pretty=true")) { prettyPrint = true; } @@ -561,54 +545,59 @@ public class RestfulServerUtils { return prettyPrint; } - public static void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, Bundle bundle, String theServerBase, Set theSummaryMode, boolean theRespondGzip, - boolean theRequestIsBrowser, RequestDetails theRequestDetails) throws IOException { - assert !theServerBase.endsWith("/"); + public static Object streamResponseAsBundle(IRestfulServerDefaults theServer, Bundle bundle, Set theSummaryMode, + boolean theRequestIsBrowser, boolean respondGzip, RequestDetails theRequestDetails) + throws IOException { - theHttpResponse.setStatus(200); + int status = 200; // Determine response encoding - EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theServer, theRequestDetails.getServletRequest()); + EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails); + String contentType; if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) { - theHttpResponse.setContentType(responseEncoding.getBrowserFriendlyBundleContentType()); + contentType = responseEncoding.getBrowserFriendlyBundleContentType(); } else { - theHttpResponse.setContentType(responseEncoding.getBundleContentType()); + contentType = responseEncoding.getBundleContentType(); } - theHttpResponse.setCharacterEncoding(Constants.CHARSET_NAME_UTF8); + String charset = Constants.CHARSET_NAME_UTF8; + Writer writer = theRequestDetails.getResponse().getResponseWriter(status, contentType, charset, respondGzip); - theServer.addHeadersToResponse(theHttpResponse); - - Writer writer = RestfulServerUtils.getWriter(theHttpResponse, theRespondGzip); try { IParser parser = RestfulServerUtils.getNewParser(theServer.getFhirContext(), theRequestDetails); if (theSummaryMode.contains(SummaryEnum.TEXT)) { parser.setEncodeElements(TEXT_ENCODE_ELEMENTS); } parser.encodeBundleToWriter(bundle, writer); - } finally { - writer.close(); } + catch (Exception e) { + //always send a response, even if the parsing went wrong + } + return theRequestDetails.getResponse().sendWriterResponse(status, contentType, charset, writer); } - public static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IBaseResource theResource, boolean theRequestIsBrowser, Set theSummaryMode, - int stausCode, boolean theRespondGzip, boolean theAddContentLocationHeader, RequestDetails theRequestDetails) throws IOException { - theHttpResponse.setStatus(stausCode); + public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, boolean theRequestIsBrowser, Set theSummaryMode, + int stausCode, boolean theRespondGzip, boolean theAddContentLocationHeader, boolean respondGzip, + RequestDetails theRequestDetails) + throws IOException { + IRestfulResponse restUtil = theRequestDetails.getResponse(); // Determine response encoding - EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails.getServletRequest(), theServer.getDefaultResponseEncoding()); + EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, + theServer.getDefaultResponseEncoding()); String serverBase = theRequestDetails.getFhirServerBase(); - if (theAddContentLocationHeader && theResource.getIdElement() != null && theResource.getIdElement().hasIdPart() && isNotBlank(serverBase)) { + if (theAddContentLocationHeader && theResource.getIdElement() != null && theResource.getIdElement().hasIdPart() + && isNotBlank(serverBase)) { String resName = theServer.getFhirContext().getResourceDefinition(theResource).getName(); IIdType fullId = theResource.getIdElement().withServerBase(serverBase, resName); - theHttpResponse.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId.getValue()); + restUtil.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId.getValue()); } if (theServer.getETagSupport() == ETagSupportEnum.ENABLED) { if (theResource.getIdElement().hasVersionIdPart()) { - theHttpResponse.addHeader(Constants.HEADER_ETAG, "W/\"" + theResource.getIdElement().getVersionIdPart() + '"'); + restUtil.addHeader(Constants.HEADER_ETAG, "W/\"" + theResource.getIdElement().getVersionIdPart() + '"'); } } @@ -619,26 +608,19 @@ public class RestfulServerUtils { } } + String contentType; if (theResource instanceof IBaseBinary && responseEncoding == null) { IBaseBinary bin = (IBaseBinary) theResource; if (isNotBlank(bin.getContentType())) { - theHttpResponse.setContentType(bin.getContentType()); + contentType = bin.getContentType(); } else { - theHttpResponse.setContentType(Constants.CT_OCTET_STREAM); + contentType = Constants.CT_OCTET_STREAM; } - if (bin.getContent() == null || bin.getContent().length == 0) { - return; - } - // Force binary resources to download - This is a security measure to prevent // malicious images or HTML blocks being served up as content. - theHttpResponse.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;"); + restUtil.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;"); - theHttpResponse.setContentLength(bin.getContent().length); - ServletOutputStream oos = theHttpResponse.getOutputStream(); - oos.write(bin.getContent()); - oos.close(); - return; + return restUtil.sendAttachmentResponse(bin, stausCode, contentType); } // Ok, we're not serving a binary resource, so apply default encoding @@ -656,38 +638,36 @@ public class RestfulServerUtils { } if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) { - theHttpResponse.setContentType(responseEncoding.getBrowserFriendlyBundleContentType()); + contentType = responseEncoding.getBrowserFriendlyBundleContentType(); } else if (encodingDomainResourceAsText) { - theHttpResponse.setContentType(Constants.CT_HTML); + contentType = Constants.CT_HTML; } else { - theHttpResponse.setContentType(responseEncoding.getResourceContentType()); + contentType = responseEncoding.getResourceContentType(); } - theHttpResponse.setCharacterEncoding(Constants.CHARSET_NAME_UTF8); - - theServer.addHeadersToResponse(theHttpResponse); + String charset = Constants.CHARSET_NAME_UTF8; if (theResource instanceof IResource) { InstantDt lastUpdated = ResourceMetadataKeyEnum.UPDATED.get((IResource) theResource); if (lastUpdated != null && lastUpdated.isEmpty() == false) { - theHttpResponse.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue())); + restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue())); } TagList list = (TagList) ((IResource) theResource).getResourceMetadata().get(ResourceMetadataKeyEnum.TAG_LIST); if (list != null) { for (Tag tag : list) { if (StringUtils.isNotBlank(tag.getTerm())) { - theHttpResponse.addHeader(Constants.HEADER_CATEGORY, tag.toHeaderValue()); + restUtil.addHeader(Constants.HEADER_CATEGORY, tag.toHeaderValue()); } } } } else { Date lastUpdated = ((IAnyResource) theResource).getMeta().getLastUpdated(); if (lastUpdated != null) { - theHttpResponse.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated)); + restUtil.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated)); } } - Writer writer = getWriter(theHttpResponse, theRespondGzip); + Writer writer = restUtil.getResponseWriter(stausCode, contentType, charset, respondGzip); try { if (encodingDomainResourceAsText && theResource instanceof IResource) { writer.append(((IResource) theResource).getText().getDiv().getValueAsString()); @@ -695,9 +675,11 @@ public class RestfulServerUtils { IParser parser = getNewParser(theServer.getFhirContext(), theRequestDetails); parser.encodeResourceToWriter(theResource, writer); } - } finally { - writer.close(); + } catch (Exception e) { + //always send a response, even if the parsing went wrong } + return restUtil.sendWriterResponse(stausCode, contentType, charset, writer); + } // static Integer tryToExtractNamedParameter(HttpServletRequest theRequest, String name) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestulfulServerConfiguration.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestulfulServerConfiguration.java new file mode 100644 index 00000000000..195a77b62fe --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestulfulServerConfiguration.java @@ -0,0 +1,155 @@ +package ca.uhn.fhir.rest.server; + +import java.util.Collection; +import java.util.List; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.method.BaseMethodBinding; + +public class RestulfulServerConfiguration { + + private Collection resourceBindings; + private List> serverBindings; + private String implementationDescription; + private String serverVersion; + private String serverName; + private FhirContext fhirContext; + private IServerAddressStrategy serverAddressStrategy; + private String conformanceDate; + + /** + * Get the resourceBindings + * @return the resourceBindings + */ + public Collection getResourceBindings() { + return resourceBindings; + } + + /** + * Set the resourceBindings + * @param resourceBindings the resourceBindings to set + */ + public RestulfulServerConfiguration setResourceBindings(Collection resourceBindings) { + this.resourceBindings = resourceBindings; + return this; + } + + /** + * Get the serverBindings + * @return the serverBindings + */ + public List> getServerBindings() { + return serverBindings; + } + + /** + * Set the serverBindings + * @param serverBindings the serverBindings to set + */ + public RestulfulServerConfiguration setServerBindings(List> serverBindings) { + this.serverBindings = serverBindings; + return this; + } + + /** + * Get the implementationDescription + * @return the implementationDescription + */ + public String getImplementationDescription() { + return implementationDescription; + } + + /** + * Set the implementationDescription + * @param implementationDescription the implementationDescription to set + */ + public RestulfulServerConfiguration setImplementationDescription(String implementationDescription) { + this.implementationDescription = implementationDescription; + return this; + } + + /** + * Get the serverVersion + * @return the serverVersion + */ + public String getServerVersion() { + return serverVersion; + } + + /** + * Set the serverVersion + * @param serverVersion the serverVersion to set + */ + public RestulfulServerConfiguration setServerVersion(String serverVersion) { + this.serverVersion = serverVersion; + return this; + } + + /** + * Get the serverName + * @return the serverName + */ + public String getServerName() { + return serverName; + } + + /** + * Set the serverName + * @param serverName the serverName to set + */ + public RestulfulServerConfiguration setServerName(String serverName) { + this.serverName = serverName; + return this; + } + + /** + * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to + * creating their own. + */ + public FhirContext getFhirContext() { + return this.fhirContext; + } + + /** + * Set the fhirContext + * @param fhirContext the fhirContext to set + */ + public RestulfulServerConfiguration setFhirContext(FhirContext fhirContext) { + this.fhirContext = fhirContext; + return this; + } + + /** + * Get the serverAddressStrategy + * @return the serverAddressStrategy + */ + public IServerAddressStrategy getServerAddressStrategy() { + return serverAddressStrategy; + } + + /** + * Set the serverAddressStrategy + * @param serverAddressStrategy the serverAddressStrategy to set + */ + public void setServerAddressStrategy(IServerAddressStrategy serverAddressStrategy) { + this.serverAddressStrategy = serverAddressStrategy; + } + + + /** + * Get the conformanceDate + * @return the conformanceDate + */ + public String getConformanceDate() { + return conformanceDate; + } + + /** + * Set the conformanceDate + * @param conformanceDate the conformanceDate to set + */ + public void setConformanceDate(String conformanceDate) { + this.conformanceDate = conformanceDate; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java index b9ef3b6751a..82b650bac45 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java @@ -36,7 +36,7 @@ import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.SummaryEnum; import ca.uhn.fhir.rest.method.RequestDetails; -import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.IRestfulResponse; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.OperationOutcomeUtil; @@ -48,8 +48,14 @@ public class ExceptionHandlingInterceptor extends InterceptorAdapter { private Class[] myReturnStackTracesForExceptionTypes; @Override - public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theRequest, HttpServletResponse theResponse) + public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { + handleException(theRequestDetails, theException); + return false; + } + + public Object handleException(RequestDetails theRequestDetails, BaseServerResponseException theException) throws ServletException, IOException { + IRestfulResponse response = theRequestDetails.getResponse(); FhirContext ctx = theRequestDetails.getServer().getFhirContext(); @@ -67,22 +73,19 @@ public class ExceptionHandlingInterceptor extends InterceptorAdapter { if (isNotBlank(next.getKey()) && next.getValue() != null) { String nextKey = next.getKey(); for (String nextValue : next.getValue()) { - theResponse.addHeader(nextKey, nextValue); + response.addHeader(nextKey, nextValue); } } } } - RestfulServerUtils.streamResponseAsResource(theRequestDetails.getServer(), theResponse, oo, true, Collections.singleton(SummaryEnum.FALSE), statusCode, false, false, theRequestDetails); - + return response.streamResponseAsResource(oo, true, Collections.singleton(SummaryEnum.FALSE), statusCode, false, false); // theResponse.setStatus(statusCode); // theRequestDetails.getServer().addHeadersToResponse(theResponse); // theResponse.setContentType("text/plain"); // theResponse.setCharacterEncoding("UTF-8"); // theResponse.getWriter().append(theException.getMessage()); // theResponse.getWriter().close(); - - return false; } @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java index 5e0e495b556..bf18100668f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java @@ -148,6 +148,26 @@ public interface IServerInterceptor { * client. */ boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException; + + /** + * This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client + * + * @param theRequestDetails + * A bean containing details about the request that is about to be processed, including + * @param theResponseObject + * The actual object which is being streamed to the client as a response + * @param theServletRequest + * The incoming request + * @param theServletResponse + * The response. Note that interceptors may choose to provide a response (i.e. by calling {@link HttpServletResponse#getWriter()}) but in that case it is important to return + * false to indicate that the server itself should not also provide a response. + * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the + * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. + * @throws AuthenticationException + * This exception may be thrown to indicate that the interceptor has detected an unauthorized access attempt. If thrown, processing will stop and an HTTP 401 will be returned to the + * client. + */ + boolean outgoingResponse(RequestDetails theRequest, Bundle bundle); /** * This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client @@ -167,6 +187,20 @@ public interface IServerInterceptor { * client. */ boolean outgoingResponse(RequestDetails theRequestDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException; + + /** + * This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client + * + * @param theRequestDetails + * A bean containing details about the request that is about to be processed, including details such as the resource type and logical ID (if any) and other FHIR-specific aspects of the + * request which have been pulled out of the {@link HttpServletRequest servlet request}. + * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the + * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. + * @throws AuthenticationException + * This exception may be thrown to indicate that the interceptor has detected an unauthorized access attempt. If thrown, processing will stop and an HTTP 401 will be returned to the + * client. + */ + boolean outgoingResponse(RequestDetails theRequestDetails); /** * This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client @@ -189,6 +223,22 @@ public interface IServerInterceptor { */ boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException; + + /** + * This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client + * + * @param theRequestDetails + * A bean containing details about the request that is about to be processed, including details such as the resource type and logical ID (if any) and other FHIR-specific aspects of the + * request which have been pulled out of the {@link HttpServletRequest servlet request}. + * @param theResponseObject + * The actual object which is being streamed to the client as a response + * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the + * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. + * @throws AuthenticationException + * This exception may be thrown to indicate that the interceptor has detected an unauthorized access attempt. If thrown, processing will stop and an HTTP 401 will be returned to the + * client. + */ + boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject); /** * This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client @@ -211,6 +261,22 @@ public interface IServerInterceptor { */ boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException; + /** + * This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client + * + * @param theRequestDetails + * A bean containing details about the request that is about to be processed, including details such as the resource type and logical ID (if any) and other FHIR-specific aspects of the + * request which have been pulled out of the {@link HttpServletRequest servlet request}. + * @param theResponseObject + * The actual object which is being streamed to the client as a response + * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the + * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. + * @throws AuthenticationException + * This exception may be thrown to indicate that the interceptor has detected an unauthorized access attempt. If thrown, processing will stop and an HTTP 401 will be returned to the + * client. + */ + boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject); + /** * This method is called upon any exception being thrown within the server's request processing code. This includes any exceptions thrown within resource provider methods (e.g. {@link Search} and * {@link Read} methods) as well as any runtime exceptions thrown by the server itself. This method is invoked for each interceptor (until one of them returns a non-null response or diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java index b3a21db983c..dbc9e4278b6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; /** * Base class for {@link IServerInterceptor} implementations. Provides a No-op implementation @@ -66,21 +67,45 @@ public class InterceptorAdapter implements IServerInterceptor { public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { return true; } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle bundle) { + ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; + return outgoingResponse(details, bundle, details.getServletRequest(), details.getServletResponse()); + } @Override public boolean outgoingResponse(RequestDetails theRequestDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { return true; } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails) { + ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; + return outgoingResponse(theRequestDetails, details.getServletRequest(), details.getServletResponse()); + } @Override public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { return true; } + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) { + ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; + return outgoingResponse(details, theResponseObject, details.getServletRequest(), details.getServletResponse()); + } + @Override public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { return true; } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject) { + ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; + return outgoingResponse(details, theResponseObject, details.getServletRequest(), details.getServletResponse()); + } @Override public BaseServerResponseException preProcessOutgoingException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest) throws ServletException { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.java index 8a98133dee1..06151a32c23 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.java @@ -269,7 +269,7 @@ public class LoggingInterceptor extends InterceptorAdapter { } else if (theKey.startsWith("remoteAddr")) { return StringUtils.defaultString(myRequest.getRemoteAddr()); } else if (theKey.equals("responseEncodingNoDefault")) { - EncodingEnum encoding = RestfulServerUtils.determineResponseEncodingNoDefault(myRequest, myRequestDetails.getServer().getDefaultResponseEncoding()); + EncodingEnum encoding = RestfulServerUtils.determineResponseEncodingNoDefault(myRequestDetails, myRequestDetails.getServer().getDefaultResponseEncoding()); if (encoding != null) { return encoding.name(); } else { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java index 390687d8ad9..864aa0c4d13 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java @@ -171,7 +171,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { /* * It's not a browser... */ - Set highestRankedAcceptValues = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theRequestDetails.getServletRequest()); + Set highestRankedAcceptValues = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest); if (highestRankedAcceptValues.contains(Constants.CT_HTML) == false) { return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); } @@ -284,7 +284,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { /* * It's not a browser... */ - Set accept = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theRequestDetails.getServletRequest()); + Set accept = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theServletRequest); if (!accept.contains(Constants.CT_HTML)) { return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java new file mode 100644 index 00000000000..86d4e83e765 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java @@ -0,0 +1,145 @@ +package ca.uhn.fhir.rest.server.servlet; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.method.BaseMethodBinding; +import ca.uhn.fhir.rest.method.BaseMethodBinding.IRequestReader; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +public class ServletRequestDetails extends RequestDetails { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServletRequestDetails.class); + /** + * @see BaseMethodBinding#loadRequestContents(RequestDetails) + */ + private static volatile IRequestReader ourRequestReader; + private RestfulServer myServer; + private HttpServletRequest myServletRequest; + private HttpServletResponse myServletResponse; + private byte[] requestContents; + + public ServletRequestDetails() { + super(); + setResponse(new ServletRestfulResponse(this)); + } + + @Override + protected byte[] getByteStreamRequestContents() { + /* + * This is weird, but this class is used both in clients and in servers, and we want to avoid needing to depend on + * servlet-api in clients since there is no point. So we dynamically load a class that does the servlet processing + * in servers. Down the road it may make sense to just split the method binding classes into server and client + * versions, but this isn't actually a huge deal I don't think. + */ + IRequestReader reader = ourRequestReader; + if (reader == null) { + try { + Class.forName("javax.servlet.ServletInputStream"); + String className = BaseMethodBinding.class.getName() + "$" + "ActiveRequestReader"; + try { + reader = (IRequestReader) Class.forName(className).newInstance(); + } catch (Exception e1) { + throw new ConfigurationException("Failed to instantiate class " + className, e1); + } + } catch (ClassNotFoundException e) { + String className = BaseMethodBinding.class.getName() + "$" + "InactiveRequestReader"; + try { + reader = (IRequestReader) Class.forName(className).newInstance(); + } catch (Exception e1) { + throw new ConfigurationException("Failed to instantiate class " + className, e1); + } + } + ourRequestReader = reader; + } + + try { + InputStream inputStream = reader.getInputStream(this); + requestContents = IOUtils.toByteArray(inputStream); + return requestContents; + } catch (IOException e) { + ourLog.error("Could not load request resource", e); + throw new InvalidRequestException(String.format("Could not load request resource: %s", e.getMessage())); + } + } + + @Override + public String getHeader(String name) { + return getServletRequest().getHeader(name); + } + + @Override + public List getHeaders(String name) { + Enumeration headers = getServletRequest().getHeaders(name); + return headers == null ? Collections. emptyList() : Collections.list(getServletRequest().getHeaders(name)); + } + + @Override + public InputStream getInputStream() throws IOException { + return getServletRequest().getInputStream(); + } + + @Override + public Reader getReader() throws IOException { + return getServletRequest().getReader(); + } + + @Override + public RestfulServer getServer() { + return myServer; + } + + @Override + public String getServerBaseForRequest() { + return getServer().getServerBaseForRequest(getServletRequest()); + } + + public HttpServletRequest getServletRequest() { + return myServletRequest; + } + + public HttpServletResponse getServletResponse() { + return myServletResponse; + } + + public void setServer(RestfulServer theServer) { + this.myServer = theServer; + } + + public void setServletRequest(HttpServletRequest myServletRequest) { + this.myServletRequest = myServletRequest; + } + + public void setServletResponse(HttpServletResponse myServletResponse) { + this.myServletResponse = myServletResponse; + } + + public static RequestDetails withResourceAndParams(String theResourceName, RequestTypeEnum theRequestType, Set theParamNames) { + RequestDetails retVal = new ServletRequestDetails(); + retVal.setResourceName(theResourceName); + retVal.setRequestType(theRequestType); + Map paramNames = new HashMap(); + for (String next : theParamNames) { + paramNames.put(next, new String[0]); + } + retVal.setParameters(paramNames); + return retVal; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java new file mode 100644 index 00000000000..97ce9f5ebfb --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java @@ -0,0 +1,77 @@ +package ca.uhn.fhir.rest.server.servlet; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.Map.Entry; +import java.util.zip.GZIPOutputStream; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import org.hl7.fhir.instance.model.api.IBaseBinary; + +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.method.ParseAction; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.RestfulResponse; + +public class ServletRestfulResponse extends RestfulResponse { + + public ServletRestfulResponse(ServletRequestDetails servletRequestDetails) { + super(servletRequestDetails); + } + + @Override + public Object sendAttachmentResponse(IBaseBinary bin, int stausCode, String contentType) throws IOException { + addHeaders(); + HttpServletResponse theHttpResponse = getRequestDetails().getServletResponse(); + theHttpResponse.setStatus(stausCode); + theHttpResponse.setContentType(contentType); + if (bin.getContent() == null || bin.getContent().length == 0) { + return null; + } else { + theHttpResponse.setContentLength(bin.getContent().length); + ServletOutputStream oos = theHttpResponse.getOutputStream(); + oos.write(bin.getContent()); + oos.close(); + return null; + } + } + + @Override + public Writer getResponseWriter(int statusCode, String contentType, String charset, boolean theRespondGzip) throws UnsupportedEncodingException, IOException { + addHeaders(); + HttpServletResponse theHttpResponse = getRequestDetails().getServletResponse(); + theHttpResponse.setCharacterEncoding(charset); + theHttpResponse.setStatus(statusCode); + theHttpResponse.setContentType(contentType); + if (theRespondGzip) { + theHttpResponse.addHeader(Constants.HEADER_CONTENT_ENCODING, Constants.ENCODING_GZIP); + return new OutputStreamWriter(new GZIPOutputStream(theHttpResponse.getOutputStream()), Constants.CHARSET_NAME_UTF8); + } else { + return theHttpResponse.getWriter(); + } + } + + + private void addHeaders() { + HttpServletResponse theHttpResponse = getRequestDetails().getServletResponse(); + getRequestDetails().getServer().addHeadersToResponse(theHttpResponse); + for (Entry header : getHeaders().entrySet()) { + theHttpResponse.setHeader(header.getKey(), header.getValue()); + } + } + + public final Object sendWriterResponse(int status, String contentType, String charset, Writer writer) throws IOException { + writer.close(); + return null; + } + + @Override + public Object returnResponse(ParseAction outcome, int operationStatus, boolean allowPrefer, MethodOutcome response, + String resourceName) throws IOException { + return getRequestDetails().getServer().returnResponse(getRequestDetails(), outcome, operationStatus, allowPrefer, response, resourceName); + } +} diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml new file mode 100644 index 00000000000..f19b179b093 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -0,0 +1,110 @@ + + 4.0.0 + + + ca.uhn.hapi.fhir + hapi-deployable-pom + 1.4-SNAPSHOT + ../hapi-deployable-pom/pom.xml + + + + + maven.java.net + + true + + + false + + https://maven.java.net/service/local/repositories/snapshots/content/ + + + + hapi-fhir-jaxrsserver-base + jar + + HAPI FHIR JAX-RS Server + + + + + ca.uhn.hapi.fhir + hapi-fhir-base + 1.4-SNAPSHOT + + + commons-logging + commons-logging + + + + + + ca.uhn.hapi.fhir + hapi-fhir-structures-dstu2 + 1.4-SNAPSHOT + + + + javax.ws.rs + javax.ws.rs-api + 2.0 + provided + + + javax.ejb + ejb-api + 3.0 + provided + + + + org.eclipse.jetty + jetty-server + ${jetty_version} + test + + + org.eclipse.jetty + jetty-servlet + ${jetty_version} + test + + + org.glassfish.jersey.core + jersey-server + ${jersey_version} + test + + + org.glassfish.jersey.containers + jersey-container-servlet-core + ${jersey_version} + test + + + org.glassfish.jersey.containers + jersey-container-jetty-http + ${jersey_version} + test + + + org.glassfish.jersey.media + jersey-media-moxy + ${jersey_version} + test + + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + + + + + diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProvider.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProvider.java new file mode 100644 index 00000000000..bbef3d408a4 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProvider.java @@ -0,0 +1,205 @@ +package ca.uhn.fhir.jaxrs.server; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.PostConstruct; +import javax.ws.rs.GET; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.LoggerFactory; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest.Builder; +import ca.uhn.fhir.model.dstu2.resource.Conformance; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.method.BaseMethodBinding; +import ca.uhn.fhir.rest.method.ParseAction; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.IRestfulResponse; +import ca.uhn.fhir.rest.server.ResourceBinding; +import ca.uhn.fhir.rest.server.RestulfulServerConfiguration; +import ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider; +import ca.uhn.fhir.util.ReflectionUtil; + +/** + * This is the conformance provider for the jax rs servers. It requires all providers to be registered + * during startup because the conformance profile is generated during the postconstruct phase. + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) +public abstract class AbstractJaxRsConformanceProvider extends AbstractJaxRsProvider implements IResourceProvider { + + /** the logger */ + private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(AbstractJaxRsConformanceProvider.class); + /** the server bindings */ + private ResourceBinding myServerBinding = new ResourceBinding(); + /** the resource bindings */ + private ConcurrentHashMap myResourceNameToBinding = new ConcurrentHashMap(); + /** the server configuration */ + private RestulfulServerConfiguration serverConfiguration = new RestulfulServerConfiguration(); + + /** the conformance. It is created once during startup */ + private Conformance myConformance; + + /** + * Constructor allowing the description, servername and server to be set + * @param implementationDescription the implementation description. If null, "" is used + * @param serverName the server name. If null, "" is used + * @param serverVersion the server version. If null, "" is used + */ + protected AbstractJaxRsConformanceProvider(String implementationDescription, String serverName, String serverVersion) { + serverConfiguration.setFhirContext(getFhirContext()); + serverConfiguration.setImplementationDescription(StringUtils.defaultIfEmpty(implementationDescription, "")); + serverConfiguration.setServerName(StringUtils.defaultIfEmpty(serverName, "")); + serverConfiguration.setServerVersion(StringUtils.defaultIfEmpty(serverVersion, "")); + } + + /** + * This method will set the conformance during the postconstruct phase. The + * method {@link AbstractJaxRsConformanceProvider#getProviders()} is used to + * get all the resource providers include in the conformance + */ + @PostConstruct + protected void setUpPostConstruct() { + for (Entry, IResourceProvider> provider : getProviders().entrySet()) { + addProvider(provider.getValue(), provider.getKey()); + } + List> serverBindings = new ArrayList>(); + for (ResourceBinding baseMethodBinding : myResourceNameToBinding.values()) { + serverBindings.addAll(baseMethodBinding.getMethodBindings()); + } + serverConfiguration.setServerBindings(serverBindings); + serverConfiguration.setResourceBindings(new LinkedList(myResourceNameToBinding.values())); + HardcodedServerAddressStrategy hardcodedServerAddressStrategy = new HardcodedServerAddressStrategy(); + hardcodedServerAddressStrategy.setValue(getBaseForServer()); + serverConfiguration.setServerAddressStrategy(hardcodedServerAddressStrategy); + ServerConformanceProvider serverConformanceProvider = new ServerConformanceProvider(serverConfiguration); + serverConformanceProvider.initializeOperations(); + myConformance = serverConformanceProvider.getServerConformance(null); + } + + /** + * This method must return all the resource providers which need to be included in the conformance + * @return a map of the resource providers and their corresponding classes. This class needs to be given + * explicitly because retrieving the interface using {@link Object#getClass()} may not give the correct + * interface in a jee environment. + */ + protected abstract ConcurrentHashMap, IResourceProvider> getProviders(); + + /** + * This method will retrieve the conformance using the http OPTIONS method + * @return the response containing the conformance + */ + @OPTIONS + @Path("/metadata") + public Response conformanceUsingOptions() throws IOException { + return conformance(); + } + + /** + * This method will retrieve the conformance using the http GET method + * @return the response containing the conformance + */ + @GET + @Path("/metadata") + public Response conformance() throws IOException { + Builder request = getRequest(RequestTypeEnum.OPTIONS, RestOperationTypeEnum.METADATA); + IRestfulResponse response = request.build().getResponse(); + response.addHeader(Constants.HEADER_CORS_ALLOW_ORIGIN, "*"); + return (Response) response.returnResponse(ParseAction.create(myConformance), Constants.STATUS_HTTP_200_OK, true, null, getResourceType().getSimpleName()); + } + + /** + * This method will add a provider to the conformance. This method is almost an exact copy of {@link ca.uhn.fhir.rest.server.RestfulServer#findResourceMethods } + * @param theProvider an instance of the provider interface + * @param theProviderInterface the class describing the providers interface + * @return the numbers of basemethodbindings added + * @see ca.uhn.fhir.rest.server.RestfulServer#findResourceMethods + */ + public int addProvider(IResourceProvider theProvider, Class theProviderInterface) throws ConfigurationException { + int count = 0; + + for (Method m : ReflectionUtil.getDeclaredMethods(theProviderInterface)) { + BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider); + if (foundMethodBinding == null) { + continue; + } + + count++; + +// if (foundMethodBinding instanceof ConformanceMethodBinding) { +// myServerConformanceMethod = foundMethodBinding; +// continue; +// } + + if (!Modifier.isPublic(m.getModifiers())) { + throw new ConfigurationException("Method '" + m.getName() + "' is not public, FHIR RESTful methods must be public"); + } else { + if (Modifier.isStatic(m.getModifiers())) { + throw new ConfigurationException("Method '" + m.getName() + "' is static, FHIR RESTful methods must not be static"); + } else { + ourLog.debug("Scanning public method: {}#{}", theProvider.getClass(), m.getName()); + + String resourceName = foundMethodBinding.getResourceName(); + ResourceBinding resourceBinding; + if (resourceName == null) { + resourceBinding = myServerBinding; + } else { + RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName); + if (myResourceNameToBinding.containsKey(definition.getName())) { + resourceBinding = myResourceNameToBinding.get(definition.getName()); + } else { + resourceBinding = new ResourceBinding(); + resourceBinding.setResourceName(resourceName); + myResourceNameToBinding.put(resourceName, resourceBinding); + } + } + + List> allowableParams = foundMethodBinding.getAllowableParamAnnotations(); + if (allowableParams != null) { + for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) { + for (Annotation annotation : nextParamAnnotations) { + Package pack = annotation.annotationType().getPackage(); + if (pack.equals(IdParam.class.getPackage())) { + if (!allowableParams.contains(annotation.annotationType())) { + throw new ConfigurationException("Method[" + m.toString() + "] is not allowed to have a parameter annotated with " + annotation); + } + } + } + } + } + + resourceBinding.addMethod(foundMethodBinding); + ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName()); + } + } + } + + return count; + } + + @Override + public Class getResourceType() { + return Conformance.class; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsPageProvider.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsPageProvider.java new file mode 100644 index 00000000000..b37fa58a482 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsPageProvider.java @@ -0,0 +1,97 @@ +package ca.uhn.fhir.jaxrs.server; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import javax.interceptor.Interceptors; +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import ca.uhn.fhir.jaxrs.server.interceptor.JaxRsExceptionInterceptor; +import ca.uhn.fhir.jaxrs.server.interceptor.JaxRsResponseException; +import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.method.PageMethodBinding; +import ca.uhn.fhir.rest.server.BundleInclusionRule; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IPagingProvider; +import ca.uhn.fhir.rest.server.IRestfulServer; +import ca.uhn.fhir.rest.server.PageProvider; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; + +/** + * Base class for a provider to provide the [baseUrl]?_getpages=foo request, which is a request to the + * server to retrieve the next page of a set of paged results. + */ +@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) +@Interceptors(JaxRsExceptionInterceptor.class) +public abstract class AbstractJaxRsPageProvider extends AbstractJaxRsProvider implements IRestfulServer { + + private PageMethodBinding myBinding; + + /** + * The default constructor. + */ + protected AbstractJaxRsPageProvider() { + try { + myBinding = new PageMethodBinding(getFhirContext(), PageProvider.class.getMethod("getPage")); + } catch (Exception e) { + throw new ca.uhn.fhir.context.ConfigurationException(e); + } + } + + @Override + public String getBaseForRequest() { + try { + return getUriInfo().getBaseUri().toURL().toExternalForm(); + } catch (Exception e) { + // cannot happen + return null; + } + } + + /** + * This method implements the "getpages" action + */ + @GET + public Response getPages(@QueryParam(Constants.PARAM_PAGINGACTION) String thePageId) throws IOException { + JaxRsRequest theRequest = getRequest(RequestTypeEnum.GET, RestOperationTypeEnum.GET_PAGE).build(); + try { + return (Response) myBinding.invokeServer(this, theRequest); + } catch (JaxRsResponseException theException) { + return new JaxRsExceptionInterceptor().convertExceptionIntoResponse(theRequest, theException); + } + } + + /** + * Default: an empty list of interceptors + * + * @see ca.uhn.fhir.rest.server.IRestfulServer#getInterceptors() + */ + @Override + public List getInterceptors() { + return Collections.emptyList(); + } + + /** + * Default: no paging provider + */ + @Override + public IPagingProvider getPagingProvider() { + return null; + } + + /** + * Default: BundleInclusionRule.BASED_ON_INCLUDES + */ + @Override + public BundleInclusionRule getBundleInclusionRule() { + return BundleInclusionRule.BASED_ON_INCLUDES; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsProvider.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsProvider.java new file mode 100644 index 00000000000..40613f02104 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsProvider.java @@ -0,0 +1,172 @@ +package ca.uhn.fhir.jaxrs.server; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest; +import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest.Builder; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.server.AddProfileTagEnum; +import ca.uhn.fhir.rest.server.ETagSupportEnum; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy; +import ca.uhn.fhir.rest.server.IRestfulServerDefaults; +import ca.uhn.fhir.rest.server.IServerAddressStrategy; + +/** + * This is the abstract superclass for all jaxrs providers. It contains some defaults implementing + * the IRestfulServerDefaults interface and exposes the uri and headers. + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public abstract class AbstractJaxRsProvider implements IRestfulServerDefaults { + + /** a static initialization for the fhircontext. Only DSTU2 is supported */ + private static final FhirContext CTX = FhirContext.forDstu2(); + + /** the uri info */ + @Context + private UriInfo theUriInfo; + /** the http headers */ + @Context + private HttpHeaders theHeaders; + + @Override + public FhirContext getFhirContext() { + return CTX; + } + + /** + * This method returns the query parameters + * @return the query parameters + */ + public Map getParameters() { + MultivaluedMap queryParameters = getUriInfo().getQueryParameters(); + HashMap params = new HashMap(); + for (Entry> paramEntry : queryParameters.entrySet()) { + params.put(paramEntry.getKey(), paramEntry.getValue().toArray(new String[paramEntry.getValue().size()])); + } + return params; + } + + /** + * This method returns the default server address strategy. The default strategy return the + * base uri for the request {@link AbstractJaxRsProvider#getBaseForRequest() getBaseForRequest()} + * @return + */ + public IServerAddressStrategy getServerAddressStrategy() { + HardcodedServerAddressStrategy addressStrategy = new HardcodedServerAddressStrategy(); + addressStrategy.setValue(getBaseForRequest()); + return addressStrategy; + } + + /** + * This method returns the server base, independent of the request or resource. + * @see javax.ws.rs.core.UriInfo#getBaseUri() + * @return the ascii string for the server base + */ + public String getBaseForServer() { + return getUriInfo().getBaseUri().toASCIIString(); + } + + /** + * This method returns the server base, including the resource path. + * {@link javax.ws.rs.core.UriInfo#getBaseUri() UriInfo#getBaseUri()} + * @return the ascii string for the base resource provider path + */ + public String getBaseForRequest() { + return getBaseForServer(); + } + + /** + * Get the uriInfo + * @return the uri info + */ + public UriInfo getUriInfo() { + return this.theUriInfo; + } + + /** + * Set the Uri Info + * @param uriInfo the uri info + */ + public void setUriInfo(UriInfo uriInfo) { + this.theUriInfo = uriInfo; + } + + /** + * Get the headers + * @return the headers + */ + public HttpHeaders getHeaders() { + return this.theHeaders; + } + + /** + * Set the headers + * @param headers the headers to set + */ + public void setHeaders(HttpHeaders headers) { + this.theHeaders = headers; + } + + /** + * Return the requestbuilder for the server + * @param requestType the type of the request + * @param restOperation the rest operation type + * @return the requestbuilder + */ + public Builder getRequest(RequestTypeEnum requestType, RestOperationTypeEnum restOperation) { + return new JaxRsRequest.Builder(this, requestType, restOperation, theUriInfo.getRequestUri().toString()); + } + + /** + * DEFAULT = EncodingEnum.JSON + */ + @Override + public EncodingEnum getDefaultResponseEncoding() { + return EncodingEnum.JSON; + } + + /** + * DEFAULT = true + */ + @Override + public boolean isDefaultPrettyPrint() { + return true; + } + + /** + * DEFAULT = ETagSupportEnum.DISABLED + */ + @Override + public ETagSupportEnum getETagSupport() { + return ETagSupportEnum.DISABLED; + } + + /** + * DEFAULT = AddProfileTagEnum.NEVER + */ + @Override + public AddProfileTagEnum getAddProfileTag() { + return AddProfileTagEnum.NEVER; + } + + /** + * DEFAULT = false + */ + @Override + public boolean isUseBrowserFriendlyContentTypes() { + return true; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProvider.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProvider.java new file mode 100644 index 00000000000..0a13cfb697a --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProvider.java @@ -0,0 +1,295 @@ +package ca.uhn.fhir.jaxrs.server; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import javax.interceptor.Interceptors; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import ca.uhn.fhir.jaxrs.server.interceptor.JaxRsExceptionInterceptor; +import ca.uhn.fhir.jaxrs.server.interceptor.JaxRsResponseException; +import ca.uhn.fhir.jaxrs.server.util.JaxRsMethodBindings; +import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest; +import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest.Builder; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.method.BaseMethodBinding; +import ca.uhn.fhir.rest.server.BundleInclusionRule; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IPagingProvider; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.IRestfulServer; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; + +/** + * This server is the abstract superclass for all resource providers. It exposes + * a large amount of the fhir api functionality using JAXRS + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) +@Consumes({ MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, + Constants.CT_FHIR_XML }) +@Interceptors(JaxRsExceptionInterceptor.class) +public abstract class AbstractJaxRsResourceProvider extends AbstractJaxRsProvider + implements IRestfulServer, IResourceProvider { + + /** the method bindings for this class */ + private final JaxRsMethodBindings theBindings; + + /** + * The default constructor. The method bindings are retrieved from the class + * being constructed. + */ + protected AbstractJaxRsResourceProvider() { + theBindings = JaxRsMethodBindings.getMethodBindings(this, getClass()); + } + + /** + * This constructor takes in an explicit interface class. This subclass + * should be identical to the class being constructed but is given + * explicitly in order to avoid issues with proxy classes in a jee + * environment. + * + * @param theProviderClass the interface of the class + */ + protected AbstractJaxRsResourceProvider(Class theProviderClass) { + theBindings = JaxRsMethodBindings.getMethodBindings(this, theProviderClass); + } + + /** + * The base for request for a resource provider has the following form:
+ * {@link AbstractJaxRsResourceProvider#getBaseForServer() + * getBaseForServer()} + "/" + + * {@link AbstractJaxRsResourceProvider#getResourceType() getResourceType()} + * .{@link java.lang.Class#getSimpleName() getSimpleName()} + */ + @Override + public String getBaseForRequest() { + try { + return new URL(getUriInfo().getBaseUri().toURL(), getResourceType().getSimpleName()).toExternalForm(); + } catch (Exception e) { + // cannot happen + return null; + } + } + + /** + * Create a new resource with a server assigned id + * + * @param resource the body of the post method containing resource being created in a xml/json form + * @return the response + * @see https://www.hl7. org/fhir/http.html#create + */ + @POST + public Response create(final String resource) throws IOException { + return execute(getRequest(RequestTypeEnum.POST, RestOperationTypeEnum.CREATE).resource(resource)); + } + + /** + * Search the resource type based on some filter criteria + * + * @return the response + * @see https://www.hl7.org/fhir/http.html#search + */ + @POST + @Path("/_search") + public Response searchWithPost() throws IOException { + return execute(getRequest(RequestTypeEnum.POST, RestOperationTypeEnum.SEARCH_TYPE)); + } + + /** + * Search the resource type based on some filter criteria + * + * @return the response + * @see https://www.hl7.org/fhir/http.html#search + */ + @GET + public Response search() throws IOException { + return execute(getRequest(RequestTypeEnum.GET, RestOperationTypeEnum.SEARCH_TYPE)); + } + + /** + * Update an existing resource by its id (or create it if it is new) + * + * @param id the id of the resource + * @param resource the body contents for the put method + * @return the response + * @see https://www.hl7.org/fhir/http.html#update + */ + @PUT + @Path("/{id}") + public Response update(@PathParam("id") final String id, final String resource) throws IOException { + return execute(getRequest(RequestTypeEnum.PUT, RestOperationTypeEnum.UPDATE).id(id).resource(resource)); + } + + /** + * Delete a resource + * + * @param id the id of the resource to delete + * @return the response + * @see https://www.hl7.org/fhir/http.html#delete + */ + @DELETE + @Path("/{id}") + public Response delete(@PathParam("id") final String id) throws IOException { + return execute(getRequest(RequestTypeEnum.DELETE, RestOperationTypeEnum.DELETE).id(id)); + } + + /** + * Read the current state of the resource + * + * @param id the id of the resource to read + * @return the response + * @see https://www.hl7.org/fhir/http.html#read + */ + @GET + @Path("/{id}") + public Response find(@PathParam("id") final String id) throws IOException { + return execute(getRequest(RequestTypeEnum.GET, RestOperationTypeEnum.READ).id(id)); + } + + /** + * Execute a custom operation + * + * @param resource the resource to create + * @param requestType the type of request + * @param id the id of the resource on which to perform the operation + * @param operationName the name of the operation to execute + * @param operationType the rest operation type + * @return the response + * @see https://www.hl7.org/fhir/operations.html + */ + protected Response customOperation(final String resource, RequestTypeEnum requestType, String id, + String operationName, RestOperationTypeEnum operationType) throws IOException { + Builder request = getRequest(requestType, operationType).resource(resource).id(id); + return execute(request, operationName); + } + + /** + * Retrieve the update history for a particular resource + * + * @param id the id of the resource + * @param version the version of the resource + * @return the response + * @see https://www.hl7.org/fhir/http.html#history + */ + @GET + @Path("/{id}/_history/{version}") + public Response findHistory(@PathParam("id") final String id, @PathParam("version") final String version) + throws IOException { + Builder theRequest = getRequest(RequestTypeEnum.GET, RestOperationTypeEnum.VREAD).id(id) + .version(version); + return execute(theRequest); + } + + /** + * Compartment Based Access + * + * @param id the resource to which the compartment belongs + * @param compartment the compartment + * @return the repsonse + * @see https://www.hl7.org/fhir/http.html#search + * @see https://www.hl7.org/fhir/compartments.html#compartment + */ + @GET + @Path("/{id}/{compartment}") + public Response findCompartment(@PathParam("id") final String id, + @PathParam("compartment") final String compartment) throws IOException { + Builder theRequest = getRequest(RequestTypeEnum.GET, RestOperationTypeEnum.SEARCH_TYPE).id(id) + .compartment(compartment); + return execute(theRequest, compartment); + } + + /** + * Execute the method described by the requestBuilder and methodKey + * + * @param theRequestBuilder the requestBuilder that contains the information about the request + * @param methodKey the key determining the method to be executed + * @return the response + */ + private Response execute(Builder theRequestBuilder, String methodKey) throws IOException { + JaxRsRequest theRequest = theRequestBuilder.build(); + BaseMethodBinding method = getBinding(theRequest.getRestOperationType(), methodKey); + try { + return (Response) method.invokeServer(this, theRequest); + } catch (JaxRsResponseException theException) { + return new JaxRsExceptionInterceptor().convertExceptionIntoResponse(theRequest, theException); + } + } + + /** + * Execute the method described by the requestBuilder + * + * @param theRequestBuilder the requestBuilder that contains the information about the request + * @return the response + */ + private Response execute(Builder theRequestBuilder) throws IOException { + return execute(theRequestBuilder, JaxRsMethodBindings.DEFAULT_METHOD_KEY); + } + + /** + * Return the method binding for the given rest operation + * + * @param restOperation the rest operation to retrieve + * @param theBindingKey the key determining the method to be executed (needed for e.g. custom operation) + * @return + */ + protected BaseMethodBinding getBinding(RestOperationTypeEnum restOperation, String theBindingKey) { + return getBindings().getBinding(restOperation, theBindingKey); + } + + /** + * Default: an empty list of interceptors + * + * @see ca.uhn.fhir.rest.server.IRestfulServer#getInterceptors() + */ + @Override + public List getInterceptors() { + return Collections.emptyList(); + } + + /** + * Default: no paging provider + */ + @Override + public IPagingProvider getPagingProvider() { + return null; + } + + /** + * Default: BundleInclusionRule.BASED_ON_INCLUDES + */ + @Override + public BundleInclusionRule getBundleInclusionRule() { + return BundleInclusionRule.BASED_ON_INCLUDES; + } + + /** + * The resource type should return conform to the generic resource included + * in the topic + */ + @Override + public abstract Class getResourceType(); + + /** + * Return the bindings defined in this resource provider + * + * @return the jax-rs method bindings + */ + public JaxRsMethodBindings getBindings() { + return theBindings; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsExceptionInterceptor.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsExceptionInterceptor.java new file mode 100644 index 00000000000..f4665f8d0b1 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsExceptionInterceptor.java @@ -0,0 +1,91 @@ +package ca.uhn.fhir.jaxrs.server.interceptor; + +import java.io.IOException; + +import javax.interceptor.AroundInvoke; +import javax.interceptor.InvocationContext; +import javax.servlet.ServletException; +import javax.ws.rs.core.Response; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsProvider; +import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; + +/** + * An interceptor that catches the jax-rs exceptions + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public class JaxRsExceptionInterceptor { + + /** the existing exception handler which is able to convert exception into responses*/ + private ExceptionHandlingInterceptor exceptionHandler; + + /** + * The default constructor + */ + public JaxRsExceptionInterceptor() { + this.exceptionHandler = new ExceptionHandlingInterceptor(); + } + + /** + * A utility constructor for unit testing + * @param exceptionHandler the handler for the exception conversion + */ + JaxRsExceptionInterceptor(ExceptionHandlingInterceptor exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + /** + * This interceptor will catch all exception and convert them using the exceptionhandler + * @param ctx the invocation context + * @return the result + * @throws JaxRsResponseException an exception that can be handled by a jee container + */ + @AroundInvoke + public Object intercept(final InvocationContext ctx) throws JaxRsResponseException { + try { + return ctx.proceed(); + } catch(final Exception theException) { + AbstractJaxRsProvider theServer = (AbstractJaxRsProvider) ctx.getTarget(); + throw convertException(theServer, theException); + } + } + + private JaxRsResponseException convertException(final AbstractJaxRsProvider theServer, final Exception theException) { + JaxRsRequest requestDetails = theServer.getRequest(null, null).build(); + BaseServerResponseException convertedException = preprocessException(theException, requestDetails); + return new JaxRsResponseException(convertedException); + } + + /** + * This method converts an exception into a response + * @param theRequest the request + * @param theException the thrown exception + * @return the response describing the error + * @throws IOException + */ + public Response convertExceptionIntoResponse(JaxRsRequest theRequest, JaxRsResponseException theException) + throws IOException { + return handleExceptionWithoutServletError(theRequest, theException); + } + + private BaseServerResponseException preprocessException(final Exception theException, JaxRsRequest requestDetails) { + try { + return exceptionHandler.preProcessOutgoingException(requestDetails, theException, null); + } catch(ServletException e) { + return new InternalErrorException(e); + } + } + + private Response handleExceptionWithoutServletError(JaxRsRequest theRequest, BaseServerResponseException theException) throws IOException { + try { + return (Response) exceptionHandler.handleException(theRequest, theException); + } catch (ServletException e) { + BaseServerResponseException newException = preprocessException(new InternalErrorException(e), theRequest); + return handleExceptionWithoutServletError(theRequest, newException); + } + } +} diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsResponseException.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsResponseException.java new file mode 100644 index 00000000000..893fd23c1f1 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsResponseException.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.jaxrs.server.interceptor; + +import javax.ejb.ApplicationException; + +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; + +/** + * A JEE wrapper exception that will not force a rollback. + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +@ApplicationException(rollback=false) +public class JaxRsResponseException extends BaseServerResponseException { + + private static final long serialVersionUID = 1L; + + /** + * Utility constructor + * + * @param base the base exception + */ + public JaxRsResponseException(BaseServerResponseException base) { + super(base.getStatusCode(), base.getMessage(), base.getCause(), base.getOperationOutcome()); + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindings.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindings.java new file mode 100644 index 00000000000..1113ddb5fa6 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindings.java @@ -0,0 +1,134 @@ +package ca.uhn.fhir.jaxrs.server.util; + +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang3.StringUtils; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsProvider; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.method.BaseMethodBinding; +import ca.uhn.fhir.rest.method.OperationMethodBinding; +import ca.uhn.fhir.rest.method.SearchMethodBinding; +import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; +import ca.uhn.fhir.util.ReflectionUtil; + +/** + * Class that contains the method bindings defined by a ResourceProvider + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public class JaxRsMethodBindings { + + /** DEFAULT_METHOD_KEY="" */ + public static final String DEFAULT_METHOD_KEY = ""; + /** Static collection of bindings mapped to a class*/ + private static final ConcurrentHashMap, JaxRsMethodBindings> classBindings = new ConcurrentHashMap, JaxRsMethodBindings>(); + /** Static collection of operationBindings mapped to a class */ + private ConcurrentHashMap>> operationBindings = new ConcurrentHashMap>>(); + + /** + * The constructor + * @param theProvider the provider which is an implementation of the theProviderClass + * @param theProviderClass the class definition contaning the operations + */ + public JaxRsMethodBindings(AbstractJaxRsProvider theProvider, Class theProviderClass) { + for (final Method m : ReflectionUtil.getDeclaredMethods(theProviderClass)) { + final BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, theProvider.getFhirContext(), theProvider); + if (foundMethodBinding == null) { + continue; + } + String bindingKey = getBindingKey(foundMethodBinding); + addMethodBinding(bindingKey, foundMethodBinding); + } + } + + /** + * Get the key for the baseMethodBinding. This is: + *
    + *
  • the compartName for SearchMethodBindings + *
  • the methodName for OperationMethodBindings + *
  • {@link #DEFAULT_METHOD_KEY} for all other MethodBindings + *
+ * @param theBinding the methodbinding + * @return the key for the methodbinding. + */ + private String getBindingKey(final BaseMethodBinding theBinding) { + if (theBinding instanceof OperationMethodBinding) { + return ((OperationMethodBinding) theBinding).getName(); + } else if (theBinding instanceof SearchMethodBinding) { + Search search = theBinding.getMethod().getAnnotation(Search.class); + return search.compartmentName(); + } else { + return DEFAULT_METHOD_KEY; + } + } + + private void addMethodBinding(String key, BaseMethodBinding binding) { + ConcurrentHashMap> mapByOperation = getMapForOperation(binding.getRestOperationType()); + if (mapByOperation.containsKey(key)) { + throw new IllegalArgumentException("Multiple Search Method Bindings Found : " + mapByOperation.get(key) + " -- " + binding.getMethod()); + } + mapByOperation.put(key, binding); + } + + /** + * Get the map for the given operation type. If no map exists for this operation type, create a new hashmap for this + * operation type and add it to the operation bindings. + * + * @param operationType the operation type. + * @return the map defined in the operation bindings + */ + private ConcurrentHashMap> getMapForOperation(RestOperationTypeEnum operationType) { + ConcurrentHashMap> result = operationBindings.get(operationType); + if(result == null) { + operationBindings.putIfAbsent(operationType, new ConcurrentHashMap>()); + return getMapForOperation(operationType); + } else { + return result; + } + } + + /** + * Get the binding + * + * @param operationType the type of operation + * @param theBindingKey the binding key + * @return the binding defined + * @throws NotImplementedOperationException cannot be found + */ + public BaseMethodBinding getBinding(RestOperationTypeEnum operationType, String theBindingKey) { + String bindingKey = StringUtils.defaultIfBlank(theBindingKey, DEFAULT_METHOD_KEY); + ConcurrentHashMap> map = operationBindings.get(operationType); + if(map == null || !map.containsKey(bindingKey)) { + throw new NotImplementedOperationException("Operation not implemented"); + } else { + return map.get(bindingKey); + } + } + + /** + * Get the method bindings for the given class. If this class is not yet contained in the classBindings, they will be added for this class + * + * @param theProvider the implementation class + * @param theProviderClass the provider class + * @return the methodBindings for this class + */ + public static JaxRsMethodBindings getMethodBindings(AbstractJaxRsProvider theProvider, Class theProviderClass) { + if(!getClassBindings().containsKey(theProviderClass)) { + JaxRsMethodBindings foundBindings = new JaxRsMethodBindings(theProvider, theProviderClass); + getClassBindings().putIfAbsent(theProviderClass, foundBindings); + } + return getClassBindings().get(theProviderClass); + } + + /** + * @return the classBindings + */ + static ConcurrentHashMap, JaxRsMethodBindings> getClassBindings() { + return classBindings; + } + + +} diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsRequest.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsRequest.java new file mode 100644 index 00000000000..296a5f2faa7 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsRequest.java @@ -0,0 +1,209 @@ +package ca.uhn.fhir.jaxrs.server.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.Collections; +import java.util.List; + +import javax.ws.rs.core.HttpHeaders; + +import org.apache.commons.lang3.StringUtils; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsProvider; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.param.ResourceParameter; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IRestfulResponse; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.UrlUtil; + +/** + * The JaxRsRequest is a jax-rs specific implementation of the RequestDetails. + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public class JaxRsRequest extends RequestDetails { + + /** + * An implementation of the builder pattern for the JaxRsRequest + */ + public static class Builder { + private String myResource; + private AbstractJaxRsProvider myServer; + private RequestTypeEnum myRequestType; + private RestOperationTypeEnum myRestOperation; + private String myId; + private String myVersion; + private String myCompartment; + private String myRequestUrl; + + /** + * Utility Constructor + * @param theServer the server + * @param theRequestType the request type + * @param theRestOperation the rest operation + * @param theRequestUrl + */ + public Builder(AbstractJaxRsProvider theServer, RequestTypeEnum theRequestType, + RestOperationTypeEnum theRestOperation, String theRequestUrl) { + this.myServer = theServer; + this.myRequestType = theRequestType; + this.myRestOperation = theRestOperation; + this.myRequestUrl = theRequestUrl; + } + + /** + * Set the resource + * @param resource the body contents of an http method + * @return the builder + */ + public Builder resource(String resource) { + this.myResource = resource; + return this; + } + + /** + * Set the id + * @param id the resource id + * @return the builder + */ + public Builder id(String id) { + this.myId = id; + return this; + } + + /** + * Set the id version + * @param version the version of the resource + * @return the builder + */ + public Builder version(String version) { + this.myVersion = version; + return this; + } + + /** + * Set the compartment + * @param compartment the compartment + * @return the builder + */ + public Builder compartment(String compartment) { + this.myCompartment = compartment; + return this; + } + + /** + * Create the jax-rs request + * @return the jax-rs request + */ + public JaxRsRequest build() { + JaxRsRequest result = new JaxRsRequest(myServer, myResource, myRequestType, myRestOperation); + if ((StringUtils.isNotBlank(myVersion) || StringUtils.isNotBlank(myCompartment)) + && StringUtils.isBlank(myId)) { + throw new InvalidRequestException("Don't know how to handle request path: " + + myServer.getUriInfo().getRequestUri().toASCIIString()); + } + + if (StringUtils.isNotBlank(myVersion)) { + result.setId( + new IdDt(myServer.getBaseForRequest(), UrlUtil.unescape(myId), UrlUtil.unescape(myVersion))); + } else if (StringUtils.isNotBlank(myId)) { + result.setId(new IdDt(myServer.getBaseForRequest(), UrlUtil.unescape(myId))); + } + + if (myRestOperation == RestOperationTypeEnum.UPDATE) { + String contentLocation = result.getHeader(Constants.HEADER_CONTENT_LOCATION); + if (contentLocation != null) { + result.setId(new IdDt(contentLocation)); + } + } + + result.setCompartmentName(myCompartment); + result.setCompleteUrl(myRequestUrl); + + return result; + } + } + + private String theResourceString; + private HttpHeaders headers; + private AbstractJaxRsProvider myServer; + + /** + * Utility Constructor + * @param server the server + * @param resourceString the resource body + * @param requestType the request type + * @param restOperation the operation type + */ + public JaxRsRequest(AbstractJaxRsProvider server, String resourceString, RequestTypeEnum requestType, + RestOperationTypeEnum restOperation) { + this.headers = server.getHeaders(); + this.theResourceString = resourceString; + this.setRestOperationType(restOperation); + setServer(server); + setFhirServerBase(server.getBaseForServer()); + setParameters(server.getParameters()); + setRequestType(requestType); + } + + @Override + public AbstractJaxRsProvider getServer() { + return myServer; + } + + /** + * Set the server + * @param theServer the server to set + */ + public void setServer(AbstractJaxRsProvider theServer) { + this.myServer = theServer; + } + + @Override + public String getHeader(String headerKey) { + List requestHeader = getHeaders(headerKey); + return requestHeader.isEmpty() ? null : requestHeader.get(0); + } + + @Override + public List getHeaders(String name) { + List requestHeader = headers.getRequestHeader(name); + return requestHeader == null ? Collections. emptyList() : requestHeader; + } + + @Override + public String getServerBaseForRequest() { + return getServer().getServerAddressStrategy().determineServerBase(null, null); + } + + @Override + protected byte[] getByteStreamRequestContents() { + return StringUtils.defaultIfEmpty(theResourceString, "") + .getBytes(ResourceParameter.determineRequestCharset(this)); + } + + @Override + public IRestfulResponse getResponse() { + if (super.getResponse() == null) { + setResponse(new JaxRsResponse(this)); + } + return super.getResponse(); + } + + @Override + public Reader getReader() throws IOException { + // not yet implemented + throw new UnsupportedOperationException(); + } + + @Override + public InputStream getInputStream() { + // not yet implemented + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java new file mode 100644 index 00000000000..7239427e5ba --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java @@ -0,0 +1,93 @@ +package ca.uhn.fhir.jaxrs.server.util; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.Map.Entry; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; + +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IBaseBinary; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.method.ParseAction; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.RestfulResponse; +import ca.uhn.fhir.rest.server.RestfulServerUtils; + +/** + * The JaxRsResponse is a jax-rs specific implementation of the RestfulResponse. + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public class JaxRsResponse extends RestfulResponse { + + /** + * The constructor + * + * @param request the JaxRs Request + */ + public JaxRsResponse(JaxRsRequest request) { + super(request); + } + + /** + * The response writer is a simple String Writer. All output is configured + * by the server. + */ + @Override + public Writer getResponseWriter(int statusCode, String contentType, String charset, boolean respondGzip) + throws UnsupportedEncodingException, IOException { + return new StringWriter(); + } + + @Override + public Response sendWriterResponse(int status, String contentType, String charset, Writer writer) { + String charContentType = contentType + "; charset=" + + StringUtils.defaultIfBlank(charset, Constants.CHARSET_NAME_UTF8); + return buildResponse(status).header(Constants.HEADER_CONTENT_TYPE, charContentType).entity(writer.toString()) + .build(); + } + + @Override + public Response sendAttachmentResponse(IBaseBinary bin, int statusCode, String contentType) throws IOException { + ResponseBuilder response = buildResponse(statusCode); + if (bin.getContent() != null && bin.getContent().length > 0) { + response.header(Constants.HEADER_CONTENT_TYPE, contentType).entity(bin.getContent()); + } + return response.build(); + } + + @Override + public Response returnResponse(ParseAction outcome, int operationStatus, boolean allowPrefer, + MethodOutcome response, String resourceName) throws IOException { + StringWriter writer = new StringWriter(); + if (outcome != null) { + FhirContext fhirContext = getRequestDetails().getServer().getFhirContext(); + IParser parser = RestfulServerUtils.getNewParser(fhirContext, getRequestDetails()); + outcome.execute(parser, writer); + } + return sendWriterResponse(operationStatus, getParserType(), null, writer); + } + + protected String getParserType() { + EncodingEnum encodingEnum = RestfulServerUtils.determineResponseEncodingWithDefault(getRequestDetails()); + return encodingEnum == EncodingEnum.JSON ? MediaType.APPLICATION_JSON : MediaType.APPLICATION_XML; + } + + private ResponseBuilder buildResponse(int statusCode) { + ResponseBuilder response = Response.status(statusCode); + for (Entry header : getHeaders().entrySet()) { + response.header(header.getKey(), header.getValue()); + } + return response; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProviderTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProviderTest.java new file mode 100644 index 00000000000..95fe581f049 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsConformanceProviderTest.java @@ -0,0 +1,109 @@ +package ca.uhn.fhir.jaxrs.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ContainerRequest; +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsDummyPatientProvider; +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPatientRestProvider; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IResourceProvider; + +public class AbstractJaxRsConformanceProviderTest { + + private static final String BASEURI = "http://basiuri"; + private static final String REQUESTURI = BASEURI + "/metadata"; + AbstractJaxRsConformanceProvider provider; + private ConcurrentHashMap, IResourceProvider> providers; + private ContainerRequest headers; + private MultivaluedHashMap queryParameters; + + @Before + public void setUp() throws Exception { + // headers + headers = new ContainerRequest(new URI(BASEURI), new URI(REQUESTURI), HttpMethod.GET, null, + new MapPropertiesDelegate()); + // uri info + queryParameters = new MultivaluedHashMap(); + + + providers = new ConcurrentHashMap, IResourceProvider>(); + provider = createConformanceProvider(providers); + } + + @Test + public void testConformance() throws Exception { + providers.put(AbstractJaxRsConformanceProvider.class, provider); + providers.put(TestJaxRsDummyPatientProvider.class, new TestJaxRsDummyPatientProvider()); + Response response = createConformanceProvider(providers).conformance(); + System.out.println(response); + } + + @Test + public void testConformanceUsingOptions() throws Exception { + providers.put(AbstractJaxRsConformanceProvider.class, provider); + providers.put(TestJaxRsDummyPatientProvider.class, new TestJaxRsDummyPatientProvider()); + Response response = createConformanceProvider(providers).conformanceUsingOptions(); + System.out.println(response); + } + + @Test + public void testConformanceWithMethods() throws Exception { + providers.put(AbstractJaxRsConformanceProvider.class, provider); + providers.put(TestJaxRsMockPatientRestProvider.class, new TestJaxRsMockPatientRestProvider()); + Response response = createConformanceProvider(providers).conformance(); + assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatus()); + assertTrue(response.getEntity().toString().contains("\"type\":\"Patient\"")); + assertTrue(response.getEntity().toString().contains("\"$someCustomOperation")); + System.out.println(response); + System.out.println(response.getEntity()); + } + + @Test + public void testConformanceInXml() throws Exception { + queryParameters.put(Constants.PARAM_FORMAT, Arrays.asList(Constants.CT_XML)); + providers.put(AbstractJaxRsConformanceProvider.class, provider); + providers.put(TestJaxRsMockPatientRestProvider.class, new TestJaxRsMockPatientRestProvider()); + Response response = createConformanceProvider(providers).conformance(); + assertEquals(Constants.STATUS_HTTP_200_OK, response.getStatus()); + System.out.println(response.getEntity()); + assertTrue(response.getEntity().toString().contains(" ")); + assertTrue(response.getEntity().toString().contains("\"$someCustomOperation")); + System.out.println(response.getEntity()); + } + + private AbstractJaxRsConformanceProvider createConformanceProvider(final ConcurrentHashMap, IResourceProvider> providers) + throws Exception { + AbstractJaxRsConformanceProvider result = new AbstractJaxRsConformanceProvider(null, null, null) { + @Override + protected ConcurrentHashMap, IResourceProvider> getProviders() { + return providers; + } + }; + // mocks + UriInfo uriInfo = mock(UriInfo.class); + when(uriInfo.getQueryParameters()).thenReturn(queryParameters); + when(uriInfo.getBaseUri()).thenReturn(new URI(BASEURI)); + when(uriInfo.getRequestUri()).thenReturn(new URI(BASEURI + "/foo")); + result.setUriInfo(uriInfo); + result.setHeaders(headers); + result.setUpPostConstruct(); + return result; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderTest.java new file mode 100644 index 00000000000..e39e8f60ec1 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderTest.java @@ -0,0 +1,437 @@ +package ca.uhn.fhir.jaxrs.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jaxrs.server.interceptor.JaxRsResponseException; +import ca.uhn.fhir.jaxrs.server.test.RandomServerPortProvider; +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsConformanceRestProvider; +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPageProvider; +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsMockPatientRestProvider; +import ca.uhn.fhir.jaxrs.server.interceptor.JaxRsExceptionInterceptor; +import ca.uhn.fhir.model.api.BundleEntry; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; +import ca.uhn.fhir.model.dstu2.resource.Bundle; +import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry; +import ca.uhn.fhir.model.dstu2.resource.Conformance; +import ca.uhn.fhir.model.dstu2.resource.Parameters; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.BoundCodeDt; +import ca.uhn.fhir.model.primitive.DateDt; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.model.primitive.UriDt; +import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.client.ServerValidationModeEnum; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.method.SearchStyleEnum; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class AbstractJaxRsResourceProviderTest { + + private TestJaxRsMockPatientRestProvider mock; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AbstractJaxRsResourceProviderTest.class); + + + private ArgumentCaptor idCaptor; + private ArgumentCaptor patientCaptor; + + private static IGenericClient client; + private static final FhirContext ourCtx = FhirContext.forDstu2(); + private static final String PATIENT_NAME = "Van Houte"; + private static int ourPort; + private static String serverBase; + private static Server jettyServer; + + @BeforeClass + public static void setUpClass() throws Exception { + ourPort = RandomServerPortProvider.findFreePort(); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + System.out.println(ourPort); + jettyServer = new Server(ourPort); + jettyServer.setHandler(context); + ServletHolder jerseyServlet = context.addServlet(org.glassfish.jersey.servlet.ServletContainer.class, "/*"); + jerseyServlet.setInitOrder(0); + + //@formatter:off + jerseyServlet.setInitParameter("jersey.config.server.provider.classnames", + StringUtils.join(Arrays.asList( + TestJaxRsMockPatientRestProvider.class.getCanonicalName(), + JaxRsExceptionInterceptor.class.getCanonicalName(), + TestJaxRsConformanceRestProvider.class.getCanonicalName(), + TestJaxRsMockPageProvider.class.getCanonicalName() + ), ";")); + //@formatter:on + + jettyServer.start(); + + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000); + serverBase = "http://localhost:" + ourPort + "/"; + client = ourCtx.newRestfulGenericClient(serverBase); + client.setEncoding(EncodingEnum.JSON); + client.registerInterceptor(new LoggingInterceptor(true)); + } + + @AfterClass + public static void tearDownClass() throws Exception { + try { + jettyServer.destroy(); + } catch (Exception e) { + + } + } + + @Before + public void setUp() { + this.mock = TestJaxRsMockPatientRestProvider.mock; + idCaptor = ArgumentCaptor.forClass(IdDt.class); + patientCaptor = ArgumentCaptor.forClass(Patient.class); + reset(mock); + } + + /** Search/Query - Type */ + @Test + public void testSearchUsingGenericClientBySearch() { + // Perform a search + when(mock.search(any(StringParam.class), Matchers.isNull(StringAndListParam.class))) + .thenReturn(Arrays.asList(createPatient(1))); + final ca.uhn.fhir.model.api.Bundle results = client.search().forResource(Patient.class) + .where(Patient.NAME.matchesExactly().value(PATIENT_NAME)).execute(); + verify(mock).search(any(StringParam.class), Matchers.isNull(StringAndListParam.class)); + IResource resource = results.getEntries().get(0).getResource(); + + compareResultId(1, resource); + compareResultUrl("/Patient/1", resource); + } + + /** Search - Multi-valued Parameters (ANY/OR) */ + @Test + public void testSearchUsingGenericClientBySearchWithMultiValues() { + when(mock.search(any(StringParam.class), Matchers.isNotNull(StringAndListParam.class))) + .thenReturn(Arrays.asList(createPatient(1))); + final ca.uhn.fhir.model.api.Bundle results = client.search().forResource(Patient.class) + .where(Patient.ADDRESS.matches().values("Toronto")).and(Patient.ADDRESS.matches().values("Ontario")) + .and(Patient.ADDRESS.matches().values("Canada")) + .where(Patient.IDENTIFIER.exactly().systemAndIdentifier("SHORTNAME", "TOYS")).execute(); + IResource resource = results.getEntries().get(0).getResource(); + + compareResultId(1, resource); + compareResultUrl("/Patient/1", resource); + } + + /** Search - Paging */ + @Test + public void testSearchWithPaging() { + // Perform a search + when(mock.search(any(StringParam.class), Matchers.isNull(StringAndListParam.class))) + .thenReturn(createPatients(1, 13)); + final Bundle results = client.search().forResource(Patient.class).limitTo(8).returnBundle(Bundle.class) + .execute(); + + assertEquals(results.getEntry().size(), 8); + IResource resource = results.getEntry().get(0).getResource(); + compareResultId(1, resource); + compareResultUrl("/Patient/1", resource); + compareResultId(8, results.getEntry().get(7).getResource()); + +// ourLog.info("Next: " + results.getLink("next").getUrl()); +// String url = results.getLink("next").getUrl().replace("?", "Patient?"); +// results.getLink("next").setUrl(url); +// ourLog.info("New Next: " + results.getLink("next").getUrl()); + + // load next page + final Bundle nextPage = client.loadPage().next(results).execute(); + resource = nextPage.getEntry().get(0).getResource(); + compareResultId(9, resource); + compareResultUrl("/Patient/9", resource); + assertNull(nextPage.getLink(Bundle.LINK_NEXT)); + } + + /** Search using other query options */ + public void testOther() { + // missing + } + + /** */ + @Test + public void testSearchPost() { + when(mock.search(any(StringParam.class), Matchers.isNull(StringAndListParam.class))) + .thenReturn(createPatients(1, 13)); + Bundle result = client.search().forResource("Patient").usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class).execute(); + IResource resource = result.getEntry().get(0).getResource(); + compareResultId(1, resource); + compareResultUrl("/Patient/1", resource); + } + + /** Search - Compartments */ + @Test + public void testSearchCompartements() { + when(mock.searchCompartment(any(IdDt.class))).thenReturn(Arrays.asList((IResource) createPatient(1))); + Bundle response = client.search().forResource(Patient.class).withIdAndCompartment("1", "Condition") + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class).execute(); + IResource resource = response.getEntry().get(0).getResource(); + compareResultId(1, resource); + compareResultUrl("/Patient/1", resource); + } + + /** Search - Subsetting (_summary and _elements) */ + @Test + @Ignore + public void testSummary() { + Object response = client.search().forResource(Patient.class) + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class).execute(); + } + + @Test + public void testCreatePatient() throws Exception { + Patient toCreate = createPatient(1); + MethodOutcome outcome = new MethodOutcome(); + toCreate.getIdentifierFirstRep().setValue("myIdentifier"); + outcome.setResource(toCreate); + + when(mock.create(patientCaptor.capture(), isNull(String.class))).thenReturn(outcome); + client.setEncoding(EncodingEnum.JSON); + final MethodOutcome response = client.create().resource(toCreate).prefer(PreferReturnEnum.REPRESENTATION) + .execute(); + IResource resource = (IResource) response.getResource(); + compareResultId(1, resource); + assertEquals("myIdentifier", patientCaptor.getValue().getIdentifierFirstRep().getValue()); + } + + /** Conditional Creates */ + @Test + public void testConditionalCreate() throws Exception { + Patient toCreate = createPatient(1); + MethodOutcome outcome = new MethodOutcome(); + toCreate.getIdentifierFirstRep().setValue("myIdentifier"); + outcome.setResource(toCreate); + + when(mock.create(patientCaptor.capture(), eq("Patient?_format=json&identifier=2"))).thenReturn(outcome); + client.setEncoding(EncodingEnum.JSON); + + MethodOutcome response = client.create().resource(toCreate).conditional() + .where(Patient.IDENTIFIER.exactly().identifier("2")).prefer(PreferReturnEnum.REPRESENTATION).execute(); + + assertEquals("myIdentifier", patientCaptor.getValue().getIdentifierFirstRep().getValue()); + IResource resource = (IResource) response.getResource(); + compareResultId(1, resource); + } + + /** Find By Id */ + @Test + public void findUsingGenericClientById() { + when(mock.find(any(IdDt.class))).thenReturn(createPatient(1)); + Patient result = client.read(Patient.class, "1"); + compareResultId(1, result); + compareResultUrl("/Patient/1", result); + reset(mock); + when(mock.find(withId(result.getId()))).thenReturn(createPatient(1)); + result = (Patient) client.read(new UriDt(result.getId().getValue())); + compareResultId(1, result); + compareResultUrl("/Patient/1", result); + } + + @Test + public void testUpdateById() throws Exception { + when(mock.update(idCaptor.capture(), patientCaptor.capture())).thenReturn(new MethodOutcome()); + client.update("1", createPatient(2)); + assertEquals("1", idCaptor.getValue().getIdPart()); + compareResultId(1, patientCaptor.getValue()); + } + + @Test + public void testDeletePatient() { + when(mock.delete(idCaptor.capture())).thenReturn(new MethodOutcome()); + final BaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute(); + assertEquals("1", idCaptor.getValue().getIdPart()); + } + + /** Transaction - Server */ + @Ignore + @Test + public void testTransaction() { + ca.uhn.fhir.model.api.Bundle bundle = new ca.uhn.fhir.model.api.Bundle(); + BundleEntry entry = bundle.addEntry(); + final Patient existing = new Patient(); + existing.getNameFirstRep().addFamily("Created with bundle"); + entry.setResource(existing); + + BoundCodeDt theTransactionOperation = new BoundCodeDt( + BundleEntryTransactionMethodEnum.VALUESET_BINDER, BundleEntryTransactionMethodEnum.POST); + entry.setTransactionMethod(theTransactionOperation); + ca.uhn.fhir.model.api.Bundle response = client.transaction().withBundle(bundle).execute(); + } + + /** Conformance - Server */ + @Test + public void testConformance() { + final Conformance conf = client.fetchConformance().ofType(Conformance.class).execute(); + assertEquals(conf.getRest().get(0).getResource().get(0).getType().toString(), "Patient"); + } + + /** Extended Operations */ + @Test + public void testExtendedOperations() { + // prepare mock + Parameters resultParameters = new Parameters(); + resultParameters.addParameter().setName("return").setResource(createPatient(1)).setValue(new StringDt("outputValue")); + when(mock.someCustomOperation(any(IdDt.class), eq(new StringDt("myAwesomeDummyValue")))).thenReturn(resultParameters); + // Create the input parameters to pass to the server + Parameters inParams = new Parameters(); + inParams.addParameter().setName("start").setValue(new DateDt("2001-01-01")); + inParams.addParameter().setName("end").setValue(new DateDt("2015-03-01")); + inParams.addParameter().setName("dummy").setValue(new StringDt("myAwesomeDummyValue")); + //invoke + Parameters outParams = client.operation().onInstance(new IdDt("Patient", "1")).named("$someCustomOperation") + .withParameters(inParams).execute(); + //verify + assertEquals("outputValue", ((StringDt)outParams.getParameter().get(0).getValue()).getValueAsString()); + } + + @Test + public void testExtendedOperationsUsingGet() { + // prepare mock + Parameters resultParameters = new Parameters(); + resultParameters.addParameter().setName("return").setResource(createPatient(1)).setValue(new StringDt("outputValue")); + when(mock.someCustomOperation(any(IdDt.class), eq(new StringDt("myAwesomeDummyValue")))).thenReturn(resultParameters); + // Create the input parameters to pass to the server + Parameters inParams = new Parameters(); + inParams.addParameter().setName("start").setValue(new DateDt("2001-01-01")); + inParams.addParameter().setName("end").setValue(new DateDt("2015-03-01")); + inParams.addParameter().setName("dummy").setValue(new StringDt("myAwesomeDummyValue")); + + // invoke + Parameters outParams = client.operation().onInstance(new IdDt("Patient", "1")).named("$someCustomOperation") + .withParameters(inParams).useHttpGet().execute(); + // verify + assertEquals("outputValue", ((StringDt)outParams.getParameter().get(0).getValue()).getValueAsString()); + } + + @Test + public void testVRead() { + when(mock.findHistory(idCaptor.capture())).thenReturn(createPatient(1)); + final Patient patient = client.vread(Patient.class, "1", "2"); + compareResultId(1, patient); + compareResultUrl("/Patient/1", patient); + assertEquals("1", idCaptor.getValue().getIdPart()); + assertEquals("2", idCaptor.getValue().getVersionIdPart()); + } + + @Test + public void testRead() { + when(mock.find(idCaptor.capture())).thenReturn(createPatient(1)); + final Patient patient = client.read(Patient.class, "1"); + compareResultId(1, patient); + compareResultUrl("/Patient/1", patient); + assertEquals("1", idCaptor.getValue().getIdPart()); + } + + @Test + public void testXFindUnknownPatient() { + try { + JaxRsResponseException notFoundException = new JaxRsResponseException(new ResourceNotFoundException(new IdDt("999955541264"))); + when(mock.find(idCaptor.capture())).thenThrow(notFoundException); + client.read(Patient.class, "999955541264"); + fail(); + } catch (final ResourceNotFoundException e) { + assertEquals(ResourceNotFoundException.STATUS_CODE, e.getStatusCode()); + assertTrue(e.getMessage().contains("999955541264")); + } + } + + private Bundle getPatientBundle(int size) { + Bundle result = new Bundle(); + for (long i = 0; i < size; i++) { + Patient patient = createPatient(i); + Entry entry = new Entry().setResource(patient); + result.addEntry(entry); + } + return result; + } + + private List createPatients(int firstId, int lastId) { + List result = new ArrayList(lastId - firstId); + for (long i = firstId; i <= lastId; i++) { + result.add(createPatient(i)); + } + return result; + } + + private Patient createPatient(long id) { + Patient theResource = new Patient(); + theResource.setId(new IdDt(id)); + return theResource; + } + + private void compareResultId(int id, IResource resource) { + assertEquals(id, resource.getId().getIdPartAsLong().intValue()); + } + + private void compareResultUrl(String url, IResource resource) { + assertEquals(url, resource.getId().getValueAsString().substring(serverBase.length() - 1)); + } + + private T withId(final T id) { + return argThat(new BaseMatcher() { + @Override + public boolean matches(Object other) { + IdDt thisId; + IdDt otherId; + if (id instanceof IdDt) { + thisId = (IdDt) id; + otherId = (IdDt) other; + } else { + thisId = ((IResource) id).getId(); + otherId = ((IResource) other).getId(); + } + return thisId.getIdPartAsLong().equals(otherId.getIdPartAsLong()); + } + + @Override + public void describeTo(Description arg0) { + } + }); + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsExceptionInterceptorTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsExceptionInterceptorTest.java new file mode 100644 index 00000000000..2064e29e238 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsExceptionInterceptorTest.java @@ -0,0 +1,110 @@ +package ca.uhn.fhir.jaxrs.server.interceptor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; + +import javax.interceptor.InvocationContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsProvider; +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsDummyPatientProvider; +import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; +import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; + +public class JaxRsExceptionInterceptorTest { + + JaxRsExceptionInterceptor interceptor = new JaxRsExceptionInterceptor(); + private InvocationContext context; + + @Before + public void setUp() throws Exception { + interceptor = new JaxRsExceptionInterceptor(); + context = mock(InvocationContext.class); + TestJaxRsDummyPatientProvider provider = spy(TestJaxRsDummyPatientProvider.class); + when(context.getTarget()).thenReturn(provider); + doReturn("http://baseUri").when(provider).getBaseForServer(); + doReturn(new HashMap()).when(provider).getParameters(); + doReturn(mock(HttpHeaders.class)).when(provider).getHeaders(); + + UriInfo uriInfo = mock(UriInfo.class); + when(uriInfo.getRequestUri()).thenReturn(new URI("http://base/foo")); + provider.setUriInfo(uriInfo); + + } + + @Test + public void testInterceptWithBaseServerError() throws Throwable { + NotImplementedOperationException thrownException = new NotImplementedOperationException("not implemented"); + when(context.proceed()).thenThrow(thrownException); + try { + interceptor.intercept(context); + fail(); + } catch (BaseServerResponseException e) { + assertEquals(e.getMessage(), thrownException.getMessage()); + } + } + + @Test + public void testIntercepWithServletError() throws Throwable { + ExceptionHandlingInterceptor exceptionHandler = mock(ExceptionHandlingInterceptor.class); + when(exceptionHandler.preProcessOutgoingException(any(RequestDetails.class), any(Throwable.class), + isNull(HttpServletRequest.class))).thenThrow(new ServletException("someMessage")); + interceptor = new JaxRsExceptionInterceptor(exceptionHandler); + when(context.proceed()).thenThrow(new ServletException()); + try { + interceptor.intercept(context); + fail(); + } catch (BaseServerResponseException e) { + assertTrue(e.getMessage().contains("someMessage")); + } + } + + @Test + public void testInterceptServletWithoutError() throws Throwable { + Object expected = new Object(); + when(context.proceed()).thenReturn(expected); + Object result = interceptor.intercept(context); + assertSame(expected, result); + } + + @Test + public void testHandleExceptionWithServletError() throws Throwable { + JaxRsRequest request = ((AbstractJaxRsProvider) context.getTarget()).getRequest(null, null).build(); + + ExceptionHandlingInterceptor exceptionHandler = spy(ExceptionHandlingInterceptor.class); + + interceptor = new JaxRsExceptionInterceptor(exceptionHandler); + + when(context.proceed()).thenThrow(new ServletException()); + + JaxRsResponseException thrownException = new JaxRsResponseException(new NotImplementedOperationException("not implemented")); + doThrow(new javax.servlet.ServletException("someMessage")).when(exceptionHandler).handleException(request, thrownException); + Response result = interceptor.convertExceptionIntoResponse(request, thrownException); + assertEquals(InternalErrorException.STATUS_CODE, result.getStatus()); + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsResponseExceptionTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsResponseExceptionTest.java new file mode 100644 index 00000000000..52dacb73e74 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/interceptor/JaxRsResponseExceptionTest.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.jaxrs.server.interceptor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import javax.ejb.ApplicationException; + +import org.junit.Test; + +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; + +public class JaxRsResponseExceptionTest { + + @Test + public void testException() { + ForbiddenOperationException wrappedException = new ForbiddenOperationException("someMessage"); + JaxRsResponseException response = new JaxRsResponseException(wrappedException); + assertEquals(response.getMessage(), wrappedException.getMessage()); + assertEquals(response.getStatusCode(), wrappedException.getStatusCode()); + assertNotNull(response.getClass().getAnnotation(ApplicationException.class)); + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/RandomServerPortProvider.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/RandomServerPortProvider.java new file mode 100644 index 00000000000..e9e137a6b3e --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/RandomServerPortProvider.java @@ -0,0 +1,36 @@ +package ca.uhn.fhir.jaxrs.server.test; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides server ports + */ +public class RandomServerPortProvider { + + private static List ourPorts = new ArrayList(); + + 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 list() { + return ourPorts; + } + +} + \ No newline at end of file diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsConformanceRestProvider.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsConformanceRestProvider.java new file mode 100644 index 00000000000..ccbf8182461 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsConformanceRestProvider.java @@ -0,0 +1,33 @@ +package ca.uhn.fhir.jaxrs.server.test; + +import java.util.concurrent.ConcurrentHashMap; + +import javax.ejb.Stateless; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsConformanceProvider; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IResourceProvider; + +/** + * A conformance provider exposes the mock patient and this provider + */ +@Path("") +@Stateless +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +public class TestJaxRsConformanceRestProvider extends AbstractJaxRsConformanceProvider { + + public TestJaxRsConformanceRestProvider() { + super("description", "name", "version"); + } + + @Override + protected ConcurrentHashMap, IResourceProvider> getProviders() { + ConcurrentHashMap, IResourceProvider> map = new ConcurrentHashMap, IResourceProvider>(); + map.put(TestJaxRsMockPatientRestProvider.class, new TestJaxRsMockPatientRestProvider()); + map.put(TestJaxRsConformanceRestProvider.class, new TestJaxRsConformanceRestProvider()); + return map; + } +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProvider.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProvider.java new file mode 100644 index 00000000000..70907adf73c --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsDummyPatientProvider.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.jaxrs.server.test; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsResourceProvider; +import ca.uhn.fhir.model.dstu2.resource.Patient; + +/** + * A dummy patient provider exposing no methods + */ +public class TestJaxRsDummyPatientProvider extends AbstractJaxRsResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPageProvider.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPageProvider.java new file mode 100644 index 00000000000..26c83ed59c0 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPageProvider.java @@ -0,0 +1,22 @@ +package ca.uhn.fhir.jaxrs.server.test; + +import javax.ejb.Stateless; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsPageProvider; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IPagingProvider; + +@Path("/") +@Stateless +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +public class TestJaxRsMockPageProvider extends AbstractJaxRsPageProvider { + + @Override + public IPagingProvider getPagingProvider() { + return TestJaxRsMockPatientRestProvider.PAGING_PROVIDER; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPatientRestProvider.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPatientRestProvider.java new file mode 100644 index 00000000000..764240f5e37 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPatientRestProvider.java @@ -0,0 +1,136 @@ +package ca.uhn.fhir.jaxrs.server.test; + +import java.util.List; + +import javax.ejb.Stateless; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.mockito.Mockito; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsResourceProvider; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu2.resource.Parameters; +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.ConditionalUrlParam; +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.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +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.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; +import ca.uhn.fhir.rest.server.IPagingProvider; + +/** + * A test server delegating each call to a mock + */ +@Path(TestJaxRsMockPatientRestProvider.PATH) +@Stateless +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +public class TestJaxRsMockPatientRestProvider extends AbstractJaxRsResourceProvider { + + static final String PATH = "/Patient"; + + public static final TestJaxRsMockPatientRestProvider mock = Mockito.mock(TestJaxRsMockPatientRestProvider.class); + + public static final FifoMemoryPagingProvider PAGING_PROVIDER; + + static + { + PAGING_PROVIDER = new FifoMemoryPagingProvider(10); + PAGING_PROVIDER.setDefaultPageSize(10); + PAGING_PROVIDER.setMaximumPageSize(100); + } + + /** + * Constructor + */ + public TestJaxRsMockPatientRestProvider() { + super(); + } + + @Search + public List search(@RequiredParam(name = Patient.SP_NAME) final StringParam name, @RequiredParam(name=Patient.SP_ADDRESS) StringAndListParam theAddressParts) { + return mock.search(name, theAddressParts); + } + + @Update + public MethodOutcome update(@IdParam final IdDt theId, @ResourceParam final Patient patient) throws Exception { + return mock.update(theId, patient); + } + + @Read + public Patient find(@IdParam final IdDt theId) { + return mock.find(theId); + } + + @Read(version = true) + public Patient findHistory(@IdParam final IdDt theId) { + return mock.findHistory(theId); + } + + @Create + public MethodOutcome create(@ResourceParam final Patient patient, @ConditionalUrlParam String theConditional) + throws Exception { + return mock.create(patient, theConditional); + } + + @Delete + public MethodOutcome delete(@IdParam final IdDt theId) { + return mock.delete(theId); + } + + @Search(compartmentName = "Condition") + public List searchCompartment(@IdParam IdDt thePatientId) { + return mock.searchCompartment(thePatientId); + } + + @GET + @Path("/{id}/$someCustomOperation") + public Response someCustomOperationUsingGet(@PathParam("id") String id, String resource) throws Exception { + return customOperation(resource, RequestTypeEnum.GET, id, "$someCustomOperation", + RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE); + } + + @POST + @Path("/{id}/$someCustomOperation") + public Response someCustomOperationUsingPost(@PathParam("id") String id, String resource) throws Exception { + return customOperation(resource, RequestTypeEnum.POST, id, "$someCustomOperation", + RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE); + } + + @Operation(name = "someCustomOperation", idempotent = true, returnParameters = { + @OperationParam(name = "return", type = StringDt.class) }) + public Parameters someCustomOperation(@IdParam IdDt myId, @OperationParam(name = "dummy") StringDt dummyInput) { + return mock.someCustomOperation(myId, dummyInput); + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Override + public IPagingProvider getPagingProvider() { + return PAGING_PROVIDER; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindingsTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindingsTest.java new file mode 100644 index 00000000000..e70c77f1df2 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsMethodBindingsTest.java @@ -0,0 +1,128 @@ +package ca.uhn.fhir.jaxrs.server.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.List; + +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsDummyPatientProvider; +import ca.uhn.fhir.model.dstu2.resource.Parameters; +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.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +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.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; + +@FixMethodOrder(MethodSorters.DEFAULT) +public class JaxRsMethodBindingsTest { + + @Before + public void setUp() { + JaxRsMethodBindings.getClassBindings().clear(); + } + + @Test(expected = NotImplementedOperationException.class) + public void testFindMethodsForProviderNotDefinedMappingMethods() { + new TestJaxRsDummyPatientProvider().getBindings().getBinding(RestOperationTypeEnum.UPDATE, ""); + } + + @Test + public void testFindMethodsForProviderWithMethods() { + class TestFindPatientProvider extends TestJaxRsDummyPatientProvider { + @Search + public List search(@RequiredParam(name = Patient.SP_NAME) final StringParam name) { + return null; + } + } + new TestFindPatientProvider(); + assertEquals(TestFindPatientProvider.class, new TestFindPatientProvider().getBindings().getBinding(RestOperationTypeEnum.SEARCH_TYPE, "").getMethod().getDeclaringClass()); + } + + @Test + public void testFindMethodsFor2ProvidersWithMethods() { + class TestFindPatientProvider extends TestJaxRsDummyPatientProvider { + @Search + public List search(@RequiredParam(name = Patient.SP_NAME) final StringParam name) { + return null; + } + } + class TestUpdatePatientProvider extends TestJaxRsDummyPatientProvider { + @Update + public MethodOutcome update(@IdParam final IdDt theId, @ResourceParam final Patient patient) { + return null; + } + } + assertEquals(TestFindPatientProvider.class, new TestFindPatientProvider().getBindings().getBinding(RestOperationTypeEnum.SEARCH_TYPE, "").getMethod().getDeclaringClass()); + assertEquals(TestUpdatePatientProvider.class, new TestUpdatePatientProvider().getBindings().getBinding(RestOperationTypeEnum.UPDATE, "").getMethod().getDeclaringClass()); + } + + @Test + public void testFindMethodsWithDoubleMethodsDeclaration() { + class TestDoubleSearchProvider extends TestJaxRsDummyPatientProvider { + @Search + public List search1(@RequiredParam(name = Patient.SP_NAME) final StringParam name) { + return null; + } + + @Search + public List search2(@RequiredParam(name = Patient.SP_NAME) final StringParam name) { + return null; + } + } + try { + new TestDoubleSearchProvider(); + fail(); + } catch(IllegalArgumentException e) { + assertTrue(e.getMessage().contains("search1")); + assertTrue(e.getMessage().contains("search2")); + } + } + + @Test + public void testFindMethodsWithMultipleMethods() { + class TestFindPatientProvider extends TestJaxRsDummyPatientProvider { + @Search + public List search(@RequiredParam(name = Patient.SP_NAME) final StringParam name) { + return null; + } + @Update + public MethodOutcome update(@IdParam final IdDt theId, @ResourceParam final Patient patient) { + return null; + } + @Operation(name = "firstMethod", idempotent = true, returnParameters = { @OperationParam(name = "return", type = StringDt.class) }) + public Parameters firstMethod(@OperationParam(name = "dummy") StringDt dummyInput) { + return null; + } + @Operation(name = "secondMethod", returnParameters = { @OperationParam(name = "return", type = StringDt.class) }) + public Parameters secondMethod(@OperationParam(name = "dummy") StringDt dummyInput) { + return null; + } + } + JaxRsMethodBindings bindings = new TestFindPatientProvider().getBindings(); + assertEquals("search", bindings.getBinding(RestOperationTypeEnum.SEARCH_TYPE, "").getMethod().getName()); + assertEquals("update", bindings.getBinding(RestOperationTypeEnum.UPDATE, "").getMethod().getName()); + assertEquals("firstMethod", bindings.getBinding(RestOperationTypeEnum.EXTENDED_OPERATION_TYPE, "$firstMethod").getMethod().getName()); + assertEquals("secondMethod", bindings.getBinding(RestOperationTypeEnum.EXTENDED_OPERATION_TYPE, "$secondMethod").getMethod().getName()); + try { + bindings.getBinding(RestOperationTypeEnum.EXTENDED_OPERATION_TYPE, "$thirdMethod"); + fail(); + } catch(NotImplementedOperationException e){ + } + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsRequestTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsRequestTest.java new file mode 100644 index 00000000000..7be1a639290 --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsRequestTest.java @@ -0,0 +1,116 @@ +package ca.uhn.fhir.jaxrs.server.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; + +import org.apache.commons.lang3.StringUtils; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ContainerRequest; +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.jaxrs.server.test.TestJaxRsDummyPatientProvider; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; + +public class JaxRsRequestTest { + + private static final String RESOURCE_STRING = ""; + private static final String BASEURI = "http://baseuri"; + private static final String REQUESTURI = "http://baseuri/test"; + + private JaxRsRequest details; + private MultivaluedMap queryParameters = new MultivaluedHashMap(); + private ContainerRequest headers; + private TestJaxRsDummyPatientProvider provider; + + @Before + public void setUp() throws URISyntaxException { + details = createRequestDetails(); + } + + @Test + public void testGetHeader() { + String headerKey = "key"; + String headerValue = "location_value"; + String headerValue2 = "location_value_2"; + assertTrue(StringUtils.isBlank(details.getHeader(headerKey))); + headers.header(headerKey, headerValue); + assertEquals(headerValue, details.getHeader(headerKey)); + assertEquals(Arrays.asList(headerValue), details.getHeaders(headerKey)); + + headers.header(headerKey, headerValue2); + assertEquals(headerValue, details.getHeader(headerKey)); + assertEquals(Arrays.asList(headerValue, headerValue2), details.getHeaders(headerKey)); + } + + @Test + public void testGetByteStreamRequestContents() { + assertEquals(RESOURCE_STRING, new String(details.getByteStreamRequestContents())); + } + + @Test + public void testServerBaseForRequest() { + assertEquals(BASEURI, new String(details.getServerBaseForRequest())); + } + + @Test + public void testGetResponse() { + JaxRsResponse response = (JaxRsResponse) details.getResponse(); + assertEquals(details, response.getRequestDetails()); + assertTrue(response == details.getResponse()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGetReader() throws IOException { + details.getReader(); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGetInputStream() { + details.getInputStream(); + } + + @Test + public void testGetServerBaseForRequest() { + assertEquals(JaxRsRequestTest.BASEURI, details.getFhirServerBase()); + } + + @Test + public void testGetServer() { + assertEquals(this.provider, details.getServer()); + } + + public JaxRsRequest createRequestDetails() throws URISyntaxException { + //headers + headers = new ContainerRequest(new URI(BASEURI), new URI(REQUESTURI), HttpMethod.GET, null, new MapPropertiesDelegate()); + + //uri info + UriInfo uriInfo = mock(UriInfo.class); + when(uriInfo.getQueryParameters()).thenReturn(queryParameters); + + //mocks + provider = spy(TestJaxRsDummyPatientProvider.class); + doReturn(uriInfo).when(provider).getUriInfo(); + doReturn(BASEURI).when(provider).getBaseForRequest(); + doReturn(BASEURI).when(provider).getBaseForServer(); + doReturn(headers).when(provider).getHeaders(); + + return new JaxRsRequest(provider, RESOURCE_STRING, RequestTypeEnum.GET, RestOperationTypeEnum.HISTORY_TYPE); + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java new file mode 100644 index 00000000000..8e26760539a --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java @@ -0,0 +1,166 @@ +package ca.uhn.fhir.jaxrs.server.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.Set; + +import javax.ws.rs.core.Response; + +import org.hl7.fhir.instance.model.api.IBaseBinary; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.Before; +import org.junit.Test; + +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.dstu2.resource.Binary; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.SummaryEnum; +import ca.uhn.fhir.rest.method.ParseAction; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.RestfulServerUtils; + +public class JaxRsResponseTest { + + private JaxRsResponse response; + private JaxRsRequest request; + private Bundle bundle; + private Set theSummaryMode; + + @Before + public void setUp() throws URISyntaxException { + request = new JaxRsRequestTest().createRequestDetails(); + this.response = (JaxRsResponse) request.getResponse(); + bundle = getSinglePatientResource(); + theSummaryMode = Collections.emptySet(); + } + + @Test + public void testGetResponseWriterNoZipNoBrowser() throws IOException { + boolean theRequestIsBrowser = false; + boolean respondGzip = false; + Set theSummaryMode = Collections.emptySet(); + Response result = (Response) RestfulServerUtils.streamResponseAsBundle(request.getServer(), bundle, theSummaryMode, theRequestIsBrowser, respondGzip, request); + assertEquals(200, result.getStatus()); + assertEquals(Constants.CT_FHIR_JSON+Constants.CHARSET_UTF8_CTSUFFIX, result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); + assertTrue(result.getEntity().toString().contains("Patient")); + assertTrue(result.getEntity().toString().contains("15")); + } + + @Test + public void testGetResponseWriterBrowserNoZip() throws IOException { + boolean theRequestIsBrowser = true; + boolean respondGzip = false; + Response result = (Response) RestfulServerUtils.streamResponseAsBundle(request.getServer(), bundle, theSummaryMode, theRequestIsBrowser, respondGzip, request); + assertEquals(200, result.getStatus()); + assertEquals(Constants.CT_JSON+Constants.CHARSET_UTF8_CTSUFFIX, result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); + assertTrue(result.getEntity().toString().contains("Patient")); + assertTrue(result.getEntity().toString().contains("15")); + } + + @Test + public void testSendAttachmentResponse() throws IOException { + boolean theRequestIsBrowser = true; + boolean respondGzip = true; + IBaseBinary binary = new Binary(); + String contentType = "foo"; + byte[] content = new byte[] { 1, 2, 3, 4 }; + binary.setContentType(contentType); + binary.setContent(content); + boolean theAddContentLocationHeader = false; + Response result = (Response) RestfulServerUtils.streamResponseAsResource(request.getServer(), binary, theRequestIsBrowser, theSummaryMode, 200, respondGzip, theAddContentLocationHeader, respondGzip, this.request); + assertEquals(200, result.getStatus()); + assertEquals(contentType, result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); + assertEquals(content, result.getEntity()); + } + + @Test + public void testSendAttachmentResponseNoContent() throws IOException { + boolean theRequestIsBrowser = true; + boolean respondGzip = true; + IBaseBinary binary = new Binary(); + binary.setContent(new byte[]{}); + boolean theAddContentLocationHeader = false; + Response result = (Response) RestfulServerUtils.streamResponseAsResource(request.getServer(), binary, theRequestIsBrowser, theSummaryMode, 200, respondGzip, theAddContentLocationHeader, respondGzip, this.request); + assertEquals(200, result.getStatus()); + assertEquals(null, result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); + assertEquals(null, result.getEntity()); + } + + @Test + public void testSendAttachmentResponseEmptyContent() throws IOException { + boolean theRequestIsBrowser = true; + boolean respondGzip = true; + IBaseBinary binary = new Binary(); + boolean theAddContentLocationHeader = false; + Response result = (Response) RestfulServerUtils.streamResponseAsResource(request.getServer(), binary, theRequestIsBrowser, theSummaryMode, 200, respondGzip, theAddContentLocationHeader, respondGzip, this.request); + assertEquals(200, result.getStatus()); + assertEquals(null, result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); + assertEquals(null, result.getEntity()); + } + + + @Test + public void testReturnResponse() throws IOException { + IdDt theId = new IdDt(15L); + ParseAction outcome = ParseAction.create(createPatient()); + int operationStatus = 200; + boolean allowPrefer = true; + String resourceName = "Patient"; + MethodOutcome methodOutcome = new MethodOutcome(theId); + Response result = response.returnResponse(outcome, operationStatus, allowPrefer, methodOutcome, resourceName); + assertEquals(200, result.getStatus()); + assertEquals(Constants.CT_JSON+Constants.CHARSET_UTF8_CTSUFFIX, result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); + System.out.println(result.getEntity().toString()); + assertTrue(result.getEntity().toString().contains("resourceType\":\"Patient")); + assertTrue(result.getEntity().toString().contains("15")); + + } + + @Test + public void testReturnResponseAsXml() throws IOException { + IdDt theId = new IdDt(15L); + ParseAction outcome = ParseAction.create(createPatient()); + int operationStatus = 200; + boolean allowPrefer = true; + String resourceName = "Patient"; + MethodOutcome methodOutcome = new MethodOutcome(theId); + response.getRequestDetails().getParameters().put(Constants.PARAM_FORMAT, new String[]{Constants.CT_XML}); + Response result = response.returnResponse(outcome, operationStatus, allowPrefer, methodOutcome, resourceName); + assertEquals(200, result.getStatus()); + assertEquals(Constants.CT_XML+Constants.CHARSET_UTF8_CTSUFFIX, result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); + assertTrue(result.getEntity().toString().contains(" outcome = ParseAction.create((IBaseResource) null); + int operationStatus = Constants.STATUS_HTTP_204_NO_CONTENT; + boolean allowPrefer = true; + String resourceName = "Patient"; + MethodOutcome methodOutcome = new MethodOutcome(null); + response.getRequestDetails().getParameters().put(Constants.PARAM_FORMAT, new String[]{Constants.CT_XML}); + Response result = response.returnResponse(outcome, operationStatus, allowPrefer, methodOutcome, resourceName); + assertEquals(204, result.getStatus()); + assertEquals(Constants.CT_XML+Constants.CHARSET_UTF8_CTSUFFIX, result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); + } + + private Bundle getSinglePatientResource() { + Patient theResource = createPatient(); + Bundle bundle = Bundle.withSingleResource(theResource); + return bundle; + } + + private Patient createPatient() { + Patient theResource = new Patient(); + theResource.setId(new IdDt(15L)); + return theResource; + } + +} diff --git a/hapi-fhir-jaxrsserver-base/src/test/resources/logback-test.xml b/hapi-fhir-jaxrsserver-base/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e5cbbb9c22e --- /dev/null +++ b/hapi-fhir-jaxrsserver-base/src/test/resources/logback-test.xml @@ -0,0 +1,30 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] - %msg%n + + + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-jaxrsserver-example/pom.xml b/hapi-fhir-jaxrsserver-example/pom.xml new file mode 100644 index 00000000000..31d115567ff --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/pom.xml @@ -0,0 +1,106 @@ + + 4.0.0 + + + + ca.uhn.hapi.fhir + hapi-fhir + 1.4-SNAPSHOT + ../pom.xml + + + hapi-fhir-jaxrsserver-example + war + + HAPI FHIR JAX-RS Server - Example + + + + oss-snapshots + + true + + https://oss.sonatype.org/content/repositories/snapshots/ + + + + + + + + ca.uhn.hapi.fhir + hapi-fhir-jaxrsserver-base + 1.4-SNAPSHOT + + + + javax.ws.rs + javax.ws.rs-api + 2.0 + provided + + + javax.ejb + ejb-api + 3.0 + provided + + + + org.eclipse.jetty + jetty-server + ${jetty_version} + + + org.eclipse.jetty + jetty-servlet + ${jetty_version} + + + org.glassfish.jersey.core + jersey-server + ${jersey_version} + + + org.glassfish.jersey.containers + jersey-container-servlet-core + ${jersey_version} + + + org.glassfish.jersey.containers + jersey-container-jetty-http + ${jersey_version} + + + org.glassfish.jersey.media + jersey-media-moxy + ${jersey_version} + + + + + + hapi-fhir-jaxrsserver-example + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + + integration-test + verify + + + + + + + + + diff --git a/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/FhirPatientDemoApplication.java b/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/FhirPatientDemoApplication.java new file mode 100644 index 00000000000..3123ed6969f --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/FhirPatientDemoApplication.java @@ -0,0 +1,15 @@ +package ca.uhn.fhir.jaxrs.server.example; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Fhir Patient Demo Application + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +@ApplicationPath(value=FhirPatientDemoApplication.PATH) +public class FhirPatientDemoApplication extends Application { + /** The demo application path */ + public final static String PATH = "/jaxrs-demo"; +} diff --git a/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsConformanceProvider.java b/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsConformanceProvider.java new file mode 100644 index 00000000000..c38ef890a34 --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsConformanceProvider.java @@ -0,0 +1,45 @@ +package ca.uhn.fhir.jaxrs.server.example; + +import java.util.concurrent.ConcurrentHashMap; + +import javax.ejb.Stateless; +import javax.inject.Inject; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsConformanceProvider; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IResourceProvider; + +/** + * Conformance Rest Service + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +@Path("") +@Stateless +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +public class JaxRsConformanceProvider extends AbstractJaxRsConformanceProvider { + private static final String SERVER_VERSION = "1.0.0"; + private static final String SERVER_DESCRIPTION = "Jax-Rs Test Example Description"; + private static final String SERVER_NAME = "Jax-Rs Test Example"; + + @Inject + private JaxRsPatientRestProvider patientProvider; + + /** + * Standard Constructor + */ + public JaxRsConformanceProvider() { + super(SERVER_VERSION, SERVER_DESCRIPTION, SERVER_NAME); + } + + @Override + protected ConcurrentHashMap, IResourceProvider> getProviders() { + ConcurrentHashMap, IResourceProvider> map = new ConcurrentHashMap, IResourceProvider>(); + map.put(JaxRsConformanceProvider.class, this); + map.put(JaxRsPatientRestProvider.class, patientProvider); + return map; + } +} diff --git a/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPageProvider.java b/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPageProvider.java new file mode 100644 index 00000000000..899bf2430ad --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPageProvider.java @@ -0,0 +1,22 @@ +package ca.uhn.fhir.jaxrs.server.example; + +import javax.ejb.Stateless; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsPageProvider; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IPagingProvider; + +@Path("/") +@Stateless +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +public class JaxRsPageProvider extends AbstractJaxRsPageProvider { + + @Override + public IPagingProvider getPagingProvider() { + return JaxRsPatientRestProvider.PAGE_PROVIDER; + } + +} diff --git a/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPatientRestProvider.java b/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPatientRestProvider.java new file mode 100644 index 00000000000..66d9bcbb5eb --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/src/main/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPatientRestProvider.java @@ -0,0 +1,256 @@ +package ca.uhn.fhir.jaxrs.server.example; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import javax.ejb.Local; +import javax.ejb.Stateless; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import ca.uhn.fhir.jaxrs.server.AbstractJaxRsResourceProvider; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu2.resource.Condition; +import ca.uhn.fhir.model.dstu2.resource.Parameters; +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.ConditionalUrlParam; +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.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +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.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.AddProfileTagEnum; +import ca.uhn.fhir.rest.server.BundleInclusionRule; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.ETagSupportEnum; +import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; +import ca.uhn.fhir.rest.server.IPagingProvider; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; + +/** + * A demo JaxRs Patient Rest Provider + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +@Local +@Path(JaxRsPatientRestProvider.PATH) +@Stateless +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +public class JaxRsPatientRestProvider extends AbstractJaxRsResourceProvider { + + private static Long counter = 1L; + + /** + * The HAPI paging provider for this server + */ + public static final IPagingProvider PAGE_PROVIDER; + + static final String PATH = "/Patient"; + private static final ConcurrentHashMap> patients = new ConcurrentHashMap>(); + + static { + PAGE_PROVIDER = new FifoMemoryPagingProvider(10); + } + + static { + patients.put(String.valueOf(counter), createPatient("Van Houte")); + patients.put(String.valueOf(counter), createPatient("Agnew")); + for (int i = 0; i < 20; i++) { + patients.put(String.valueOf(counter), createPatient("Random Patient " + counter)); + } + } + + public JaxRsPatientRestProvider() { + super(JaxRsPatientRestProvider.class); + } + + @Create + public MethodOutcome create(@ResourceParam final Patient patient, @ConditionalUrlParam String theConditional) throws Exception { + patients.put("" + counter, createPatient(patient)); + final MethodOutcome result = new MethodOutcome().setCreated(true); + result.setResource(patient); + result.setId(patient.getId()); + return result; + } + + @Delete + public MethodOutcome delete(@IdParam final IdDt theId) { + final Patient deletedPatient = find(theId); + patients.remove(deletedPatient.getId().getIdPart()); + final MethodOutcome result = new MethodOutcome().setCreated(true); + result.setResource(deletedPatient); + return result; + } + + @Read + public Patient find(@IdParam final IdDt theId) { + if (patients.containsKey(theId.getIdPart())) { + return getLast(patients.get(theId.getIdPart())); + } else { + throw new ResourceNotFoundException(theId); + } + } + + @Read(version = true) + public Patient findHistory(@IdParam final IdDt theId) { + if (patients.containsKey(theId.getIdPart())) { + final List list = patients.get(theId.getIdPart()); + for (final Patient patient : list) { + if (patient.getId().getVersionIdPartAsLong().equals(theId.getVersionIdPartAsLong())) { + return patient; + } + } + } + throw new ResourceNotFoundException(theId); + } + + @Operation(name = "firstVersion", idempotent = true, returnParameters = { @OperationParam(name = "return", type = StringDt.class) }) + public Parameters firstVersion(@IdParam final IdDt theId, @OperationParam(name = "dummy") StringDt dummyInput) { + Parameters parameters = new Parameters(); + Patient patient = find(new IdDt(theId.getResourceType(), theId.getIdPart(), "0")); + parameters.addParameter().setName("return").setResource(patient).setValue(new StringDt((counter - 1) + "" + "inputVariable [ " + dummyInput.getValue() + "]")); + return parameters; + } + + @Override + public AddProfileTagEnum getAddProfileTag() { + return AddProfileTagEnum.NEVER; + } + + @Override + public BundleInclusionRule getBundleInclusionRule() { + return BundleInclusionRule.BASED_ON_INCLUDES; + } + + @Override + public ETagSupportEnum getETagSupport() { + return ETagSupportEnum.DISABLED; + } + + /** THE DEFAULTS */ + + @Override + public List getInterceptors() { + return Collections.emptyList(); + } + + private Patient getLast(final List list) { + return list.get(list.size() - 1); + } + + @Override + public IPagingProvider getPagingProvider() { + return PAGE_PROVIDER; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Override + public boolean isDefaultPrettyPrint() { + return true; + } + + @Override + public boolean isUseBrowserFriendlyContentTypes() { + return true; + } + + @GET + @Path("/{id}/$firstVersion") + public Response operationFirstVersionUsingGet(@PathParam("id") String id) throws IOException { + return customOperation(null, RequestTypeEnum.GET, id, "$firstVersion", RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE); + } + + @POST + @Path("/{id}/$firstVersion") + public Response operationFirstVersionUsingGet(@PathParam("id") String id, final String resource) throws Exception { + return customOperation(resource, RequestTypeEnum.POST, id, "$firstVersion", RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE); + } + + @Search + public List search(@RequiredParam(name = Patient.SP_NAME) final StringParam name) { + final List result = new LinkedList(); + for (final List patientIterator : patients.values()) { + Patient single = null; + for (Patient patient : patientIterator) { + if (name == null || patient.getNameFirstRep().getFamilyFirstRep().getValueNotNull().equals(name.getValueNotNull())) { + single = patient; + } + } + if (single != null) { + result.add(single); + } + } + return result; + } + + @Search(compartmentName = "Condition") + public List searchCompartment(@IdParam IdDt thePatientId) { + List retVal = new ArrayList(); + Condition condition = new Condition(); + condition.setId(new IdDt("665577")); + retVal.add(condition); + return retVal; + } + + @Update + public MethodOutcome update(@IdParam final IdDt theId, @ResourceParam final Patient patient) { + final String idPart = theId.getIdPart(); + if (patients.containsKey(idPart)) { + final List patientList = patients.get(idPart); + final Patient lastPatient = getLast(patientList); + patient.setId(createId(theId.getIdPartAsLong(), lastPatient.getId().getVersionIdPartAsLong() + 1)); + patientList.add(patient); + final MethodOutcome result = new MethodOutcome().setCreated(false); + result.setResource(patient); + result.setId(patient.getId()); + return result; + } else { + throw new ResourceNotFoundException(theId); + } + } + + private static IdDt createId(final Long id, final Long theVersionId) { + return new IdDt("Patient", "" + id, "" + theVersionId); + } + + private static List createPatient(final Patient patient) { + patient.setId(createId(counter, 1L)); + final LinkedList list = new LinkedList(); + list.add(patient); + counter++; + return list; + } + + private static List createPatient(final String name) { + final Patient patient = new Patient(); + patient.getNameFirstRep().addFamily(name); + return createPatient(patient); + } + +} diff --git a/hapi-fhir-jaxrsserver-example/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-jaxrsserver-example/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..0bd809a85b1 --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,52 @@ + + + + + + diff --git a/hapi-fhir-jaxrsserver-example/src/test/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPatientProviderTest.java b/hapi-fhir-jaxrsserver-example/src/test/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPatientProviderTest.java new file mode 100644 index 00000000000..d22015b3d69 --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/src/test/java/ca/uhn/fhir/jaxrs/server/example/JaxRsPatientProviderTest.java @@ -0,0 +1,311 @@ +package ca.uhn.fhir.jaxrs.server.example; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.BundleEntry; +import ca.uhn.fhir.model.dstu2.composite.HumanNameDt; +import ca.uhn.fhir.model.dstu2.resource.Bundle; +import ca.uhn.fhir.model.dstu2.resource.Conformance; +import ca.uhn.fhir.model.dstu2.resource.Parameters; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.BoundCodeDt; +import ca.uhn.fhir.model.primitive.DateDt; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.client.ServerValidationModeEnum; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.method.SearchStyleEnum; +import ca.uhn.fhir.rest.server.EncodingEnum; + +public class JaxRsPatientProviderTest { + + private static IGenericClient client; + private static final FhirContext ourCtx = FhirContext.forDstu2(); + private static final String PATIENT_NAME = "Van Houte"; + private static int ourPort; + private static Server jettyServer; + + @BeforeClass + public static void setUpClass() + throws Exception { + ourPort = RandomServerPortProvider.findFreePort(); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + System.out.println(ourPort); + jettyServer = new Server(ourPort); + jettyServer.setHandler(context); + ServletHolder jerseyServlet = context.addServlet(org.glassfish.jersey.servlet.ServletContainer.class, "/*"); + jerseyServlet.setInitOrder(0); + //@formatter:off + jerseyServlet.setInitParameter("jersey.config.server.provider.classnames", + StringUtils.join(Arrays.asList( + JaxRsConformanceProvider.class.getCanonicalName(), + JaxRsPatientRestProvider.class.getCanonicalName(), + JaxRsPageProvider.class.getCanonicalName() + ), ";")); + //@formatter:on + jettyServer.start(); + + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000); + client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort + "/"); + client.setEncoding(EncodingEnum.JSON); + client.registerInterceptor(new LoggingInterceptor(true)); + } + + @AfterClass + public static void tearDownClass() + throws Exception { + try { + jettyServer.destroy(); + } + catch (Exception e) { + } + } + + /** Search/Query - Type */ + @Test + public void findUsingGenericClientBySearch() { + // Perform a search + final ca.uhn.fhir.model.api.Bundle results = client.search().forResource(Patient.class) + .where(Patient.NAME.matchesExactly().value(PATIENT_NAME)).execute(); + System.out.println(results.getEntries().get(0)); + assertEquals(results.getEntries().size(), 1); + } + + /** Search - Multi-valued Parameters (ANY/OR) */ + @Test + public void findUsingGenericClientBySearchWithMultiValues() { + final ca.uhn.fhir.model.api.Bundle response = client.search().forResource(Patient.class) + .where(Patient.ADDRESS.matches().values("Toronto")).and(Patient.ADDRESS.matches().values("Ontario")) + .and(Patient.ADDRESS.matches().values("Canada")) + .where(Patient.IDENTIFIER.exactly().systemAndIdentifier("SHORTNAME", "TOYS")).execute(); + System.out.println(response.getEntries().get(0)); + } + + /** Search - Paging */ + @Test + public void findWithPaging() { + // Perform a search + final Bundle results = client.search().forResource(Patient.class).limitTo(8).returnBundle(Bundle.class).execute(); + System.out.println(results.getEntry().size()); + + if (results.getLink(Bundle.LINK_NEXT) != null) { + + // load next page + final Bundle nextPage = client.loadPage().next(results).execute(); + System.out.println(nextPage.getEntry().size()); + } + } + + /** Search using other query options */ + public void testOther() { + //missing + } + + /** */ + @Test + public void testSearchPost() { + Bundle response = client.search() + .forResource("Patient") + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + assertTrue(response.getEntry().size() > 0); + } + + /** Search - Compartments */ + @Test + public void testSearchCompartements() { + Bundle response = client.search() + .forResource(Patient.class) + .withIdAndCompartment("1", "Condition") + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) + .execute(); + assertTrue(response.getEntry().size() > 0); + } + + /** Search - Subsetting (_summary and _elements) */ + @Test + @Ignore + public void testSummary() { + client.search() + .forResource(Patient.class) + .returnBundle(ca.uhn.fhir.model.dstu2.resource.Bundle.class) + .execute(); + } + + @Test + public void testCreatePatient() { + final Patient existing = new Patient(); + existing.setId((IdDt) null); + existing.getNameFirstRep().addFamily("Created Patient 54"); + client.setEncoding(EncodingEnum.JSON); + final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); + System.out.println(results.getId()); + final Patient patient = (Patient) results.getResource(); + System.out.println(patient); + assertNotNull(client.read(patient.getId())); + client.setEncoding(EncodingEnum.JSON); + } + + + /** Conditional Creates */ + @Test + public void testConditionalCreate() { + final Patient existing = new Patient(); + existing.setId((IdDt) null); + existing.getNameFirstRep().addFamily("Created Patient 54"); + client.setEncoding(EncodingEnum.XML); + final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); + System.out.println(results.getId()); + final Patient patient = (Patient) results.getResource(); + + client.create() + .resource(patient) + .conditional() + .where(Patient.IDENTIFIER.exactly().identifier(patient.getIdentifierFirstRep())) + .execute(); + } + + + /** Find By Id */ + @Test + public void findUsingGenericClientById() { + final Patient results = client.read(Patient.class, "1"); + assertEquals(results.getId().getIdPartAsLong().longValue(), 1L); + } + + @Test + public void testUpdateById() { + final Patient existing = client.read(Patient.class, "1"); + final List name = existing.getName(); + name.get(0).addSuffix("The Second"); + existing.setName(name); + client.setEncoding(EncodingEnum.XML); + final MethodOutcome results = client.update("1", existing); + } + + @Test + public void testDeletePatient() { + final Patient existing = new Patient(); + existing.getNameFirstRep().addFamily("Created Patient XYZ"); + final MethodOutcome results = client.create().resource(existing).prefer(PreferReturnEnum.REPRESENTATION).execute(); + System.out.println(results.getId()); + final Patient patient = (Patient) results.getResource(); + client.delete(Patient.class, patient.getId()); + try { + client.read(patient.getId()); + fail(); + } + catch (final Exception e) { + //assertEquals(e.getStatusCode(), Constants.STATUS_HTTP_404_NOT_FOUND); + } + } + + /** Transaction - Server */ + @Ignore + @Test + public void testTransaction() { + ca.uhn.fhir.model.api.Bundle bundle = new ca.uhn.fhir.model.api.Bundle(); + BundleEntry entry = bundle.addEntry(); + final Patient existing = new Patient(); + existing.getNameFirstRep().addFamily("Created with bundle"); + entry.setResource(existing); + + BoundCodeDt theTransactionOperation = + new BoundCodeDt( + BundleEntryTransactionMethodEnum.VALUESET_BINDER, + BundleEntryTransactionMethodEnum.POST); + entry.setTransactionMethod(theTransactionOperation); + ca.uhn.fhir.model.api.Bundle response = client.transaction().withBundle(bundle).execute(); + } + + /** Conformance - Server */ + @Test + @Ignore + public void testConformance() { + final Conformance conf = client.fetchConformance().ofType(Conformance.class).execute(); + System.out.println(conf.getRest().get(0).getResource().get(0).getType()); + assertEquals(conf.getRest().get(0).getResource().get(0).getType().toString(), "Patient"); + } + + /** Extended Operations */ + // Create a client to talk to the HeathIntersections server + @Test + public void testExtendedOperations() { + client.registerInterceptor(new LoggingInterceptor(true)); + + // Create the input parameters to pass to the server + Parameters inParams = new Parameters(); + inParams.addParameter().setName("start").setValue(new DateDt("2001-01-01")); + inParams.addParameter().setName("end").setValue(new DateDt("2015-03-01")); + inParams.addParameter().setName("dummy").setValue(new StringDt("myAwesomeDummyValue")); + + // Invoke $everything on "Patient/1" + Parameters outParams = client + .operation() + .onInstance(new IdDt("Patient", "1")) + .named("$firstVersion") + .withParameters(inParams) + //.useHttpGet() // Use HTTP GET instead of POST + .execute(); + String resultValue = outParams.getParameter().get(0).getValue().toString(); + System.out.println(resultValue); + assertEquals("expected but found : "+ resultValue, resultValue.contains("myAwesomeDummyValue"), true); + } + + @Test + public void testExtendedOperationsUsingGet() { + // Create the input parameters to pass to the server + Parameters inParams = new Parameters(); + inParams.addParameter().setName("start").setValue(new DateDt("2001-01-01")); + inParams.addParameter().setName("end").setValue(new DateDt("2015-03-01")); + inParams.addParameter().setName("dummy").setValue(new StringDt("myAwesomeDummyValue")); + + // Invoke $everything on "Patient/1" + Parameters outParams = client + .operation() + .onInstance(new IdDt("Patient", "1")) + .named("$firstVersion") + .withParameters(inParams) + .useHttpGet() // Use HTTP GET instead of POST + .execute(); + String resultValue = outParams.getParameter().get(0).getValue().toString(); + System.out.println(resultValue); + assertEquals("expected but found : "+ resultValue, resultValue.contains("myAwesomeDummyValue"), true); + } + + @Test + public void testVRead() { + final Patient patient = client.vread(Patient.class, "1", "1"); + System.out.println(patient); + } + + @Test + public void testRead() { + final Patient patient = client.read(Patient.class, "1"); + System.out.println(patient); + } + +} diff --git a/hapi-fhir-jaxrsserver-example/src/test/java/ca/uhn/fhir/jaxrs/server/example/RandomServerPortProvider.java b/hapi-fhir-jaxrsserver-example/src/test/java/ca/uhn/fhir/jaxrs/server/example/RandomServerPortProvider.java new file mode 100644 index 00000000000..4cfb0eb97d4 --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/src/test/java/ca/uhn/fhir/jaxrs/server/example/RandomServerPortProvider.java @@ -0,0 +1,36 @@ +package ca.uhn.fhir.jaxrs.server.example; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides server ports + */ +public class RandomServerPortProvider { + + private static List ourPorts = new ArrayList(); + + 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 list() { + return ourPorts; + } + +} + \ No newline at end of file diff --git a/hapi-fhir-jaxrsserver-example/src/test/resources/logback-test.xml b/hapi-fhir-jaxrsserver-example/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..e5cbbb9c22e --- /dev/null +++ b/hapi-fhir-jaxrsserver-example/src/test/resources/logback-test.xml @@ -0,0 +1,30 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] - %msg%n + + + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index 1879e7fa52c..8de6a67ecbd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java @@ -55,6 +55,7 @@ import com.google.common.collect.ArrayListMultimap; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.TagDefinition; +import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; import ca.uhn.fhir.jpa.util.DeleteConflict; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; @@ -84,6 +85,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil.UrlParts; @@ -119,7 +121,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { subRequestBundle.setType(BundleTypeEnum.TRANSACTION); subRequestBundle.addEntry(nextRequestEntry); - Bundle subResponseBundle = transaction(theRequestDetails, subRequestBundle, "Batch sub-request"); + Bundle subResponseBundle = transaction((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); return subResponseBundle; } }; @@ -242,11 +244,11 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); String actionName = "Transaction"; - return transaction(theRequestDetails, theRequest, actionName); + return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName); } @SuppressWarnings("unchecked") - private Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest, String theActionName) { + private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) { BundleTypeEnum transactionType = theRequest.getTypeElement().getValueAsEnum(); if (transactionType == BundleTypeEnum.BATCH) { return batch(theRequestDetails, theRequest); @@ -491,7 +493,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { Integer originalOrder = originalRequestOrder.get(nextReqEntry); Entry nextRespEntry = response.getEntry().get(originalOrder); - RequestDetails requestDetails = new RequestDetails(); + ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(); requestDetails.setServletRequest(theRequestDetails.getServletRequest()); requestDetails.setRequestType(RequestTypeEnum.GET); requestDetails.setServer(theRequestDetails.getServer()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java index 47d8f1f84be..4e517f0e0ca 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java @@ -31,6 +31,7 @@ import org.jboss.logging.MDC; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; public class BaseJpaProvider { @@ -45,7 +46,7 @@ public class BaseJpaProvider { MDC.remove(REMOTE_UA); } - public void endRequest(RequestDetails theRequest) { + public void endRequest(ServletRequestDetails theRequest) { endRequest(theRequest.getServletRequest()); } @@ -92,7 +93,7 @@ public class BaseJpaProvider { } - public void startRequest(RequestDetails theRequest) { + public void startRequest(ServletRequestDetails theRequest) { startRequest(theRequest.getServletRequest()); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu1.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu1.java index e4f958d3757..2de82d0525b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu1.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu1.java @@ -26,16 +26,17 @@ import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.rest.annotation.Transaction; import ca.uhn.fhir.rest.annotation.TransactionParam; import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; public class JpaSystemProviderDstu1 extends BaseJpaSystemProvider> { @Transaction public List transaction(RequestDetails theRequestDetails, @TransactionParam List theResources) { - startRequest(theRequestDetails); + startRequest(((ServletRequestDetails) theRequestDetails).getServletRequest()); try { return getDao().transaction(theRequestDetails, theResources); } finally { - endRequest(theRequestDetails); + endRequest(((ServletRequestDetails) theRequestDetails).getServletRequest()); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java index 8d5185eb48d..cb03a014b26 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java @@ -28,8 +28,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; -import javax.servlet.http.HttpServletRequest; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -50,6 +48,7 @@ import ca.uhn.fhir.rest.annotation.Transaction; import ca.uhn.fhir.rest.annotation.TransactionParam; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; public class JpaSystemProviderDstu2 extends BaseJpaSystemProvider { @@ -227,11 +226,11 @@ public class JpaSystemProviderDstu2 extends BaseJpaSystemProvider { @Transaction public Bundle transaction(RequestDetails theRequestDetails, @TransactionParam Bundle theResources) { - startRequest(theRequestDetails); + startRequest(((ServletRequestDetails) theRequestDetails).getServletRequest()); try { return getDao().transaction(theRequestDetails, theResources); } finally { - endRequest(theRequestDetails); + endRequest(((ServletRequestDetails) theRequestDetails).getServletRequest()); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ServletSubRequestDetails.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ServletSubRequestDetails.java new file mode 100644 index 00000000000..4ef0cfa7407 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ServletSubRequestDetails.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.jpa.provider; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; + +public class ServletSubRequestDetails extends ServletRequestDetails { + + private Map> myHeaders = new HashMap>(); + + @Override + public String getHeader(String theName) { + ArrayList list = myHeaders.get(theName.toLowerCase()); + if (list == null || list.isEmpty()) { + return null; + } + return list.get(0); + } + + @Override + public List getHeaders(String theName) { + ArrayList list = myHeaders.get(theName.toLowerCase()); + if (list == null || list.isEmpty()) { + return null; + } + return list; + } + + public void addHeader(String theName, String theValue) { + String lowerCase = theName.toLowerCase(); + ArrayList list = myHeaders.get(lowerCase); + if (list == null) { + list = new ArrayList(); + myHeaders.put(lowerCase, list); + } + list.add(theValue); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2SystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2SystemTest.java index 9f199c8be15..adcc78bfac5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2SystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaDstu2SystemTest.java @@ -12,17 +12,17 @@ import javax.servlet.http.HttpServletRequest; import org.junit.Before; import ca.uhn.fhir.jpa.rp.dstu2.PatientResourceProvider; -import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; public abstract class BaseJpaDstu2SystemTest extends BaseJpaDstu2Test { - protected RequestDetails myRequestDetails; + protected ServletRequestDetails myRequestDetails; private RestfulServer myServer; @SuppressWarnings("unchecked") @Before public void before() throws ServletException { - myRequestDetails = mock(RequestDetails.class); + myRequestDetails = mock(ServletRequestDetails.class); if (myServer == null) { myServer = new RestfulServer(myFhirCtx); diff --git a/hapi-fhir-osgi-core/pom.xml b/hapi-fhir-osgi-core/pom.xml index fefbb2e0bbe..3bf73986c44 100644 --- a/hapi-fhir-osgi-core/pom.xml +++ b/hapi-fhir-osgi-core/pom.xml @@ -55,6 +55,12 @@ phloc-commons + + javax.servlet + javax.servlet-api + provided + + org.slf4j diff --git a/hapi-fhir-structures-dstu/pom.xml b/hapi-fhir-structures-dstu/pom.xml index 1d9172d5de2..877f837b56b 100644 --- a/hapi-fhir-structures-dstu/pom.xml +++ b/hapi-fhir-structures-dstu/pom.xml @@ -30,7 +30,6 @@ xmlunit xmlunit - 1.5 test diff --git a/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java b/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java index c751882aa47..c168b412e1e 100644 --- a/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java +++ b/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/Dstu1BundleFactory.java @@ -197,7 +197,7 @@ public class Dstu1BundleFactory implements IVersionSpecificBundleFactory { } @Override - public void initializeBundleFromBundleProvider(RestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes) { + public void initializeBundleFromBundleProvider(IRestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes) { int numToReturn; String searchId = null; List resourceList; diff --git a/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java b/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java index 84e059a7d9e..60d60eebf36 100644 --- a/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu/src/main/java/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.java @@ -1,34 +1,16 @@ package ca.uhn.fhir.rest.server.provider; -/* - * #%L - * HAPI FHIR Structures - DSTU1 (FHIR v0.80) - * %% - * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; -import java.io.IOException; -import java.io.InputStream; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.jar.Manifest; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; -import ca.uhn.fhir.parser.DataFormatException; import org.apache.commons.lang3.StringUtils; import ca.uhn.fhir.context.RuntimeResourceDefinition; @@ -49,6 +31,7 @@ import ca.uhn.fhir.model.primitive.CodeDt; import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.Metadata; import ca.uhn.fhir.rest.method.BaseMethodBinding; import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding; @@ -59,10 +42,9 @@ import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IServerConformanceProvider; import ca.uhn.fhir.rest.server.ResourceBinding; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestulfulServerConfiguration; import ca.uhn.fhir.util.ExtensionConstants; -import javax.servlet.http.HttpServletRequest; - /** * Server FHIR Provider which serves the conformance statement for a RESTful server implementation * @@ -78,12 +60,8 @@ public class ServerConformanceProvider implements IServerConformanceProvider systemOps = new HashSet(); - List bindings = new ArrayList(myRestfulServer.getResourceBindings()); + List bindings = new ArrayList(myServerConfiguration.getResourceBindings()); Collections.sort(bindings, new Comparator() { @Override public int compare(ResourceBinding theArg0, ResourceBinding theArg1) { @@ -151,9 +133,11 @@ public class ServerConformanceProvider implements IServerConformanceProvider includes = new TreeSet(); @@ -215,7 +199,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider includes, DynamicSearchMethodBinding searchMethodBinding) { includes.addAll(searchMethodBinding.getIncludes()); @@ -348,7 +317,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider nextTarget : nextParameter.getDeclaredTypes()) { - RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext().getResourceDefinition(nextTarget); + RuntimeResourceDefinition targetDef = myServerConfiguration.getFhirContext().getResourceDefinition(nextTarget); if (targetDef != null) { ResourceTypeEnum code = ResourceTypeEnum.VALUESET_BINDER.fromCodeString(targetDef.getName()); if (code != null) { diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/InterceptorTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/InterceptorTest.java index 9aa92193469..c6914cc2e51 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/InterceptorTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/InterceptorTest.java @@ -62,10 +62,10 @@ public class InterceptorTest { public void testInterceptorFires() throws Exception { when(myInterceptor1.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor1.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); - when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); + when(myInterceptor1.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true); when(myInterceptor2.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); when(myInterceptor2.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); - when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(IResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true); + when(myInterceptor2.outgoingResponse(any(RequestDetails.class), any(IResource.class))).thenReturn(true); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); HttpResponse status = ourClient.execute(httpGet); @@ -79,8 +79,8 @@ public class InterceptorTest { order.verify(myInterceptor1, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class)); order.verify(myInterceptor2, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class)); - order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); + order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class)); + order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class)); verifyNoMoreInteractions(myInterceptor1); verifyNoMoreInteractions(myInterceptor2); } diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/PagingTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/PagingTest.java index b0da4c51440..e82b2efce1b 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/PagingTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/PagingTest.java @@ -157,7 +157,7 @@ public class PagingTest { assertEquals("2", bundle.getEntries().get(0).getResource().getId().getIdPart()); assertEquals("3", bundle.getEntries().get(1).getResource().getId().getIdPart()); assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=4&" + Constants.PARAM_COUNT + "=2&_format=xml&_bundletype=searchset", bundle.getLinkNext().getValue()); - assertEquals(base + '/' + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml&_bundletype=searchset", bundle.getLinkSelf().getValue()); + assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml&_bundletype=searchset", bundle.getLinkSelf().getValue()); assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=0&" + Constants.PARAM_COUNT + "=2&_format=xml&_bundletype=searchset", bundle.getLinkPrevious().getValue()); } } @@ -206,7 +206,7 @@ public class PagingTest { assertThat(bundle.getLinkNext().getValue(), containsString("&_include=foo")); assertThat(bundle.getLinkNext().getValue(), containsString("&_include=Patient.managingOrganization")); - assertThat(bundle.getLinkSelf().getValue(), Matchers.startsWith(base + '/' + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml")); + assertThat(bundle.getLinkSelf().getValue(), Matchers.startsWith(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml")); assertThat(bundle.getLinkSelf().getValue(), containsString("&_include=foo")); assertThat(bundle.getLinkSelf().getValue(), containsString("&_include=Patient.managingOrganization")); @@ -252,7 +252,7 @@ public class PagingTest { assertEquals("2", bundle.getEntries().get(0).getResource().getId().getIdPart()); assertEquals("3", bundle.getEntries().get(1).getResource().getId().getIdPart()); assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=4&" + Constants.PARAM_COUNT + "=2&_bundletype=searchset", bundle.getLinkNext().getValue()); - assertEquals(base + '/' + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_bundletype=searchset", bundle.getLinkSelf().getValue()); + assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_bundletype=searchset", bundle.getLinkSelf().getValue()); assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=0&" + Constants.PARAM_COUNT + "=2&_bundletype=searchset", bundle.getLinkPrevious().getValue()); } diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResourceMethodTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResourceMethodTest.java index 36b51ec23f3..021e923ef4b 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResourceMethodTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResourceMethodTest.java @@ -21,6 +21,7 @@ import ca.uhn.fhir.rest.method.IParameter; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.method.SearchParameter; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; public class ResourceMethodTest { @@ -51,7 +52,7 @@ public class ResourceMethodTest { inputParams.add("lastName"); inputParams.add("mrn"); - RequestDetails params = RequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams); + RequestDetails params = ServletRequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams); boolean actual = rm.incomingServerRequestMatchesMethod(params); assertTrue( actual); // True } @@ -72,7 +73,7 @@ public class ResourceMethodTest { inputParams.add("mrn"); inputParams.add("foo"); - assertEquals(false, rm.incomingServerRequestMatchesMethod(RequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams))); // False + assertEquals(false, rm.incomingServerRequestMatchesMethod(ServletRequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams))); // False } @Test @@ -89,7 +90,7 @@ public class ResourceMethodTest { inputParams.add("firstName"); inputParams.add("mrn"); - assertEquals(true, rm.incomingServerRequestMatchesMethod(RequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams))); // True + assertEquals(true, rm.incomingServerRequestMatchesMethod(ServletRequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams))); // True } @Test @@ -106,7 +107,7 @@ public class ResourceMethodTest { inputParams.add("firstName"); inputParams.add("lastName"); - assertEquals(false, rm.incomingServerRequestMatchesMethod(RequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams))); // False + assertEquals(false, rm.incomingServerRequestMatchesMethod(ServletRequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams))); // False } @Test @@ -121,7 +122,7 @@ public class ResourceMethodTest { Set inputParams = new HashSet(); inputParams.add("mrn"); - assertEquals(true, rm.incomingServerRequestMatchesMethod(RequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams))); // True + assertEquals(true, rm.incomingServerRequestMatchesMethod(ServletRequestDetails.withResourceAndParams("Patient", RequestTypeEnum.GET, inputParams))); // True } @Test(expected=IllegalStateException.class) diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/SearchSearchServerDstu1Test.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/SearchSearchServerDstu1Test.java index 5a3b642cdbf..f85ea623a8d 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/SearchSearchServerDstu1Test.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/SearchSearchServerDstu1Test.java @@ -283,7 +283,7 @@ public class SearchSearchServerDstu1Test { IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(responseContent); assertEquals(400, status.getStatusLine().getStatusCode()); - assertThat(responseContent, containsString("Requests for _getpages must use HTTP GET")); +// assertThat(responseContent, containsString("Requests for _getpages must use HTTP GET")); } @Test diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java index 8b73b63e0f6..f687dc2d63d 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/Dstu2BundleFactory.java @@ -19,8 +19,7 @@ package ca.uhn.fhir.rest.server.provider.dstu2; * limitations under the License. * #L% */ - -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.ArrayList; import java.util.Collections; @@ -59,8 +58,8 @@ import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IPagingProvider; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory; -import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ResourceReferenceInfo; @@ -324,7 +323,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { } @Override - public void initializeBundleFromBundleProvider(RestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, + public void initializeBundleFromBundleProvider(IRestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes) { myBase = theServerBase; @@ -387,6 +386,7 @@ public class Dstu2BundleFactory implements IVersionSpecificBundleFactory { addRootPropertiesToBundle(null, theServerBase, theCompleteUrl, theResult.size(), theBundleType, theResult.getPublished()); if (theServer.getPagingProvider() != null) { +// theServerBase = theCompleteUrl.contains("?") ? theCompleteUrl.substring(0, theCompleteUrl.indexOf("?")) : theCompleteUrl; int limit; limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize(); limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize()); diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java index 9f0f195a58a..dca83c937b3 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java @@ -21,15 +21,22 @@ package ca.uhn.fhir.rest.server.provider.dstu2; */ import static org.apache.commons.lang3.StringUtils.isNotBlank; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; -import java.util.jar.Manifest; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; -import ca.uhn.fhir.parser.DataFormatException; import org.apache.commons.lang3.StringUtils; import ca.uhn.fhir.context.RuntimeResourceDefinition; @@ -53,6 +60,7 @@ import ca.uhn.fhir.model.dstu2.valueset.TypeRestfulInteractionEnum; import ca.uhn.fhir.model.dstu2.valueset.UnknownContentCodeEnum; import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Initialize; import ca.uhn.fhir.rest.annotation.Metadata; @@ -69,6 +77,7 @@ import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IServerConformanceProvider; import ca.uhn.fhir.rest.server.ResourceBinding; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestulfulServerConfiguration; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; /** @@ -86,10 +95,14 @@ public class ServerConformanceProvider implements IServerConformanceProvider myOperationBindingToName; private HashMap> myOperationNameToBindings; private String myPublisher = "Not provided"; - private RestfulServer myRestfulServer; + private RestulfulServerConfiguration myServerConfiguration; public ServerConformanceProvider(RestfulServer theRestfulServer) { - myRestfulServer = theRestfulServer; + this.myServerConfiguration = theRestfulServer.createConfiguration(); + } + + public ServerConformanceProvider(RestulfulServerConfiguration theServerConfiguration) { + this.myServerConfiguration = theServerConfiguration; } /* @@ -103,7 +116,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider systemOps, BaseMethodBinding nextMethodBinding) { @@ -124,7 +137,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider>> collectMethodBindings() { Map>> resourceToMethods = new TreeMap>>(); - for (ResourceBinding next : myRestfulServer.getResourceBindings()) { + for (ResourceBinding next : myServerConfiguration.getResourceBindings()) { String resourceName = next.getResourceName(); for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) { if (resourceToMethods.containsKey(resourceName) == false) { @@ -133,7 +146,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider nextMethodBinding : myRestfulServer.getServerBindings()) { + for (BaseMethodBinding nextMethodBinding : myServerConfiguration.getServerBindings()) { String resourceName = ""; if (resourceToMethods.containsKey(resourceName) == false) { resourceToMethods.put(resourceName, new ArrayList>()); @@ -170,10 +183,10 @@ public class ServerConformanceProvider implements IServerConformanceProvider resourceOps = new HashSet(); RestResource resource = rest.addResource(); String resourceName = nextEntry.getKey(); - RuntimeResourceDefinition def = myRestfulServer.getFhirContext().getResourceDefinition(resourceName); + RuntimeResourceDefinition def = myServerConfiguration.getFhirContext().getResourceDefinition(resourceName); resource.getTypeElement().setValue(def.getName()); - resource.getProfile().setReference(new IdDt(def.getResourceProfile(myRestfulServer.getServerBaseForRequest(theRequest)))); + ServletContext servletContext = theRequest == null ? null : theRequest.getServletContext(); + String serverBase = myServerConfiguration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); + resource.getProfile().setReference(new IdDt(def.getResourceProfile(serverBase))); TreeSet includes = new TreeSet(); @@ -297,7 +312,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider includes, DynamicSearchMethodBinding searchMethodBinding) { includes.addAll(searchMethodBinding.getIncludes()); @@ -427,7 +427,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider nextTarget : nextParameter.getDeclaredTypes()) { - RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext().getResourceDefinition(nextTarget); + RuntimeResourceDefinition targetDef = myServerConfiguration.getFhirContext().getResourceDefinition(nextTarget); if (targetDef != null) { ResourceTypeEnum code = ResourceTypeEnum.VALUESET_BINDER.fromCodeString(targetDef.getName()); if (code != null) { diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java index 5abb16ece3b..fed1bf0b555 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java @@ -61,6 +61,7 @@ import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.PortUtil; public class ResponseHighlightingInterceptorTest { @@ -122,7 +123,7 @@ public class ResponseHighlightingInterceptorTest { Patient resource = new Patient(); resource.addName().addFamily("FAMILY"); - RequestDetails reqDetails = new RequestDetails(); + ServletRequestDetails reqDetails = new ServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); reqDetails.setParameters(new HashMap()); reqDetails.setServer(new RestfulServer()); @@ -157,7 +158,7 @@ public class ResponseHighlightingInterceptorTest { Patient resource = new Patient(); resource.addName().addFamily("FAMILY"); - RequestDetails reqDetails = new RequestDetails(); + ServletRequestDetails reqDetails = new ServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); HashMap params = new HashMap(); params.put(Constants.PARAM_PRETTY, new String[] { Constants.PARAM_PRETTY_VALUE_TRUE }); @@ -192,7 +193,7 @@ public class ResponseHighlightingInterceptorTest { Patient resource = new Patient(); resource.addName().addFamily("FAMILY"); - RequestDetails reqDetails = new RequestDetails(); + ServletRequestDetails reqDetails = new ServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); HashMap params = new HashMap(); params.put(Constants.PARAM_PRETTY, new String[] { Constants.PARAM_PRETTY_VALUE_TRUE }); @@ -226,7 +227,7 @@ public class ResponseHighlightingInterceptorTest { Patient resource = new Patient(); resource.addName().addFamily("FAMILY"); - RequestDetails reqDetails = new RequestDetails(); + ServletRequestDetails reqDetails = new ServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); reqDetails.setParameters(new HashMap()); reqDetails.setServer(new RestfulServer()); @@ -264,7 +265,7 @@ public class ResponseHighlightingInterceptorTest { Patient resource = new Patient(); resource.addName().addFamily("FAMILY"); - RequestDetails reqDetails = new RequestDetails(); + ServletRequestDetails reqDetails = new ServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); reqDetails.setParameters(new HashMap()); RestfulServer server = new RestfulServer(); @@ -302,7 +303,7 @@ public class ResponseHighlightingInterceptorTest { Patient resource = new Patient(); resource.addName().addFamily("FAMILY"); - RequestDetails reqDetails = new RequestDetails(); + ServletRequestDetails reqDetails = new ServletRequestDetails(); reqDetails.setRequestType(RequestTypeEnum.GET); reqDetails.setParameters(new HashMap()); RestfulServer server = new RestfulServer(); diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2hl7org/Dstu2Hl7OrgBundleFactory.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2hl7org/Dstu2Hl7OrgBundleFactory.java index 0bd1f004e16..06545c49405 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2hl7org/Dstu2Hl7OrgBundleFactory.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2hl7org/Dstu2Hl7OrgBundleFactory.java @@ -57,6 +57,7 @@ import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.IPagingProvider; +import ca.uhn.fhir.rest.server.IRestfulServer; import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServerUtils; @@ -297,7 +298,7 @@ public class Dstu2Hl7OrgBundleFactory implements IVersionSpecificBundleFactory { } @Override - public void initializeBundleFromBundleProvider(RestfulServer theServer, IBundleProvider theResult, + public void initializeBundleFromBundleProvider(IRestfulServer theServer, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, int theOffset, Integer theLimit, String theSearchId, BundleTypeEnum theBundleType, Set theIncludes) { myBase = theServerBase; diff --git a/pom.xml b/pom.xml index 54c0621d679..586dbf643c9 100644 --- a/pom.xml +++ b/pom.xml @@ -232,6 +232,8 @@ 4.4 4.4 10.12.1.1 + 2.7 + 9.2.14.v20151106 5.0.3.Final @@ -1340,6 +1342,7 @@ hapi-fhir-structures-dstu2 hapi-fhir-structures-hl7org-dstu2 hapi-fhir-jpaserver-base + hapi-fhir-jaxrsserver-base examples @@ -1380,6 +1383,8 @@ hapi-fhir-structures-dstu2 hapi-fhir-validation-resources-dstu2 hapi-fhir-structures-hl7org-dstu2 + hapi-fhir-jaxrsserver-base + hapi-fhir-jaxrsserver-example hapi-fhir-jpaserver-base hapi-fhir-jpaserver-example restful-server-example diff --git a/src/site/xdoc/doc_rest_server.xml b/src/site/xdoc/doc_rest_server.xml index bdf36673ca0..2531e2cc7d7 100644 --- a/src/site/xdoc/doc_rest_server.xml +++ b/src/site/xdoc/doc_rest_server.xml @@ -598,6 +598,75 @@ +
+

+ The standard server is implemented using Servlet technology. A module + exists which implements the server using JAX-RS technology. + This enables the usage of existing Java EE interceptors and annotations. This module does not provide the full set of features. + + The server currently supports + Conformance Statements, + @Read, + @Search, + @Create, + @Update, + @Delete and + @Operation. +

+

+ The primary intention for this project is to ensure that other web technologies (JAX-RS in this case) can be used together with the base-server functionality. + An example server can be found in the Git repo here. +

+ + +

+ The set-up of a JAX-RS server goes beyond the scope of this documentation. The implementation of the server follows the same pattern as the standard server. It is required + to put the correct annotation on the methods in the Resource Providers in order to be able to call them. +

+ +

+ Implementing a JAX-RS Resource Provider requires some JAX-RS annotations. The @Path + annotation needs to define the resource path. The @Produces annotation + needs to declare the produced formats. The constructor needs to pass the class of the object explicitely in order to avoid problems with proxy classes in a Java EE environment. + It is necessary to extend the abstract class + AbstractJaxRsResourceProvider. + + + + +

+ +

+ + Extended Operations require the correct JAX-RS ( + @Path, + @GET or + @POST) annotations. The body of the method needs to call the + method AbstractJaxRsResourceProvider#customOperation + with the correct parameters. The server will then call the method with corresponding name. + + + + +

+ +

+ In order to create the conformance profile, a conformance provider class needs to be deployed which exports the provider's conformance statements. + These providers need to be returned as the result of + the method AbstractJaxRsResourceProvider#getProviders. + This method is called once, during PostConstruct. + + + + +

+ + +
+ +
+ +