001/*
002 * #%L
003 * HAPI FHIR JAX-RS Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jaxrs.server;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.api.BundleInclusionRule;
024import ca.uhn.fhir.jaxrs.server.interceptor.JaxRsExceptionInterceptor;
025import ca.uhn.fhir.jaxrs.server.util.JaxRsMethodBindings;
026import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest;
027import ca.uhn.fhir.jaxrs.server.util.JaxRsRequest.Builder;
028import ca.uhn.fhir.rest.api.server.IRestfulServer;
029import ca.uhn.fhir.rest.api.*;
030import ca.uhn.fhir.rest.server.IPagingProvider;
031import ca.uhn.fhir.rest.server.IResourceProvider;
032import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
033import jakarta.interceptor.Interceptors;
034import jakarta.ws.rs.Consumes;
035import jakarta.ws.rs.DELETE;
036import jakarta.ws.rs.GET;
037import jakarta.ws.rs.POST;
038import jakarta.ws.rs.PUT;
039import jakarta.ws.rs.Path;
040import jakarta.ws.rs.PathParam;
041import jakarta.ws.rs.Produces;
042import jakarta.ws.rs.core.MediaType;
043import jakarta.ws.rs.core.Response;
044import org.hl7.fhir.instance.model.api.IBaseResource;
045
046import java.io.IOException;
047import java.net.URL;
048
049/**
050 * This server is the abstract superclass for all resource providers. It exposes
051 * a large amount of the fhir api functionality using JAXRS
052 * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
053 */
054@Produces({
055        MediaType.APPLICATION_JSON,
056        MediaType.APPLICATION_XML,
057        MediaType.TEXT_PLAIN,
058        Constants.CT_FHIR_JSON,
059        Constants.CT_FHIR_XML
060})
061@Consumes({
062        MediaType.APPLICATION_FORM_URLENCODED,
063        MediaType.APPLICATION_JSON,
064        Constants.CT_FHIR_JSON,
065        Constants.CT_FHIR_XML,
066        Constants.CT_FHIR_JSON_NEW,
067        Constants.CT_FHIR_XML_NEW,
068        "application/octet-stream"
069})
070@Interceptors(JaxRsExceptionInterceptor.class)
071public abstract class AbstractJaxRsResourceProvider<R extends IBaseResource> extends AbstractJaxRsProvider
072                implements IRestfulServer<JaxRsRequest>, IResourceProvider {
073
074        /** the method bindings for this class */
075        private final JaxRsMethodBindings theBindings;
076
077        /**
078         * The default constructor. The method bindings are retrieved from the class
079         * being constructed.
080         */
081        protected AbstractJaxRsResourceProvider() {
082                super();
083                theBindings = JaxRsMethodBindings.getMethodBindings(this, getClass());
084        }
085
086        /**
087         * Provides the ability to specify the {@link FhirContext}.
088         * @param ctx the {@link FhirContext} instance.
089         */
090        protected AbstractJaxRsResourceProvider(final FhirContext ctx) {
091                super(ctx);
092                theBindings = JaxRsMethodBindings.getMethodBindings(this, getClass());
093        }
094
095        /**
096         * This constructor takes in an explicit interface class. This subclass
097         * should be identical to the class being constructed but is given
098         * explicitly in order to avoid issues with proxy classes in a jee
099         * environment.
100         *
101         * @param theProviderClass the interface of the class
102         */
103        protected AbstractJaxRsResourceProvider(final Class<? extends AbstractJaxRsProvider> theProviderClass) {
104                super();
105                theBindings = JaxRsMethodBindings.getMethodBindings(this, theProviderClass);
106        }
107
108        /**
109         * This constructor takes in an explicit interface class. This subclass
110         * should be identical to the class being constructed but is given
111         * explicitly in order to avoid issues with proxy classes in a jee
112         * environment.
113         *
114         * @param ctx the {@link FhirContext} instance.
115         * @param theProviderClass the interface of the class
116         */
117        protected AbstractJaxRsResourceProvider(
118                        final FhirContext ctx, final Class<? extends AbstractJaxRsProvider> theProviderClass) {
119                super(ctx);
120                theBindings = JaxRsMethodBindings.getMethodBindings(this, theProviderClass);
121        }
122
123        /**
124         * The base for request for a resource provider has the following form:</br>
125         * {@link AbstractJaxRsResourceProvider#getBaseForServer()
126         * getBaseForServer()} + "/" +
127         * {@link AbstractJaxRsResourceProvider#getResourceType() getResourceType()}
128         * .{@link java.lang.Class#getSimpleName() getSimpleName()}
129         */
130        @Override
131        public String getBaseForRequest() {
132                try {
133                        return new URL(getUriInfo().getBaseUri().toURL(), getResourceType().getSimpleName()).toExternalForm();
134                } catch (final Exception e) {
135                        // cannot happen
136                        return null;
137                }
138        }
139
140        /**
141         * Create a new resource with a server assigned id
142         *
143         * @param resource the body of the post method containing resource being created in a xml/json form
144         * @return the response
145         * @see <a href="https://www.hl7.org/fhir/http.html#create">https://www.hl7. org/fhir/http.html#create</a>
146         */
147        @POST
148        public Response create(final String resource) throws IOException {
149                return execute(getResourceRequest(RequestTypeEnum.POST, RestOperationTypeEnum.CREATE)
150                                .resource(resource));
151        }
152
153        /**
154         * Search the resource type based on some filter criteria
155         *
156         * @return the response
157         * @see <a href="https://www.hl7.org/fhir/http.html#search">https://www.hl7.org/fhir/http.html#search</a>
158         */
159        @POST
160        @Path("/_search")
161        public Response searchWithPost() throws IOException {
162                return execute(getResourceRequest(RequestTypeEnum.POST, RestOperationTypeEnum.SEARCH_TYPE));
163        }
164
165        /**
166         * Search the resource type based on some filter criteria
167         *
168         * @return the response
169         * @see <a href="https://www.hl7.org/fhir/http.html#search">https://www.hl7.org/fhir/http.html#search</a>
170         */
171        @GET
172        public Response search() throws IOException {
173                return execute(getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.SEARCH_TYPE));
174        }
175
176        /**
177         * Update an existing resource based on the given condition
178         * @param resource the body contents for the put method
179         * @return the response
180         * @see <a href="https://www.hl7.org/fhir/http.html#update">https://www.hl7.org/fhir/http.html#update</a>
181         */
182        @PUT
183        public Response conditionalUpdate(final String resource) throws IOException {
184                return execute(getResourceRequest(RequestTypeEnum.PUT, RestOperationTypeEnum.UPDATE)
185                                .resource(resource));
186        }
187
188        /**
189         * Update an existing resource by its id (or create it if it is new)
190         *
191         * @param id the id of the resource
192         * @param resource the body contents for the put method
193         * @return the response
194         * @see <a href="https://www.hl7.org/fhir/http.html#update">https://www.hl7.org/fhir/http.html#update</a>
195         */
196        @PUT
197        @Path("/{id}")
198        public Response update(@PathParam("id") final String id, final String resource) throws IOException {
199                return execute(getResourceRequest(RequestTypeEnum.PUT, RestOperationTypeEnum.UPDATE)
200                                .id(id)
201                                .resource(resource));
202        }
203
204        /**
205         * Delete a resource based on the given condition
206         *
207         * @return the response
208         * @see <a href="https://www.hl7.org/fhir/http.html#delete">https://www.hl7.org/fhir/http.html#delete</a>
209         */
210        @DELETE
211        public Response delete() throws IOException {
212                return execute(getResourceRequest(RequestTypeEnum.DELETE, RestOperationTypeEnum.DELETE));
213        }
214
215        /**
216         * Delete a resource
217         *
218         * @param id the id of the resource to delete
219         * @return the response
220         * @see <a href="https://www.hl7.org/fhir/http.html#delete">https://www.hl7.org/fhir/http.html#delete</a>
221         */
222        @DELETE
223        @Path("/{id}")
224        public Response delete(@PathParam("id") final String id) throws IOException {
225                return execute(getResourceRequest(RequestTypeEnum.DELETE, RestOperationTypeEnum.DELETE)
226                                .id(id));
227        }
228
229        /**
230         * Read the current state of the resource
231         *
232         * @param id the id of the resource to read
233         * @return the response
234         * @see <a href="https://www.hl7.org/fhir/http.html#read">https://www.hl7.org/fhir/http.html#read</a>
235         */
236        @GET
237        @Path("/{id : ((?!_history).)*}")
238        public Response find(@PathParam("id") final String id) throws IOException {
239                return execute(getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.READ)
240                                .id(id));
241        }
242
243        /**
244         * Execute a custom operation
245         *
246         * @param resource the resource to create
247         * @param requestType the type of request
248         * @param id the id of the resource on which to perform the operation
249         * @param operationName the name of the operation to execute
250         * @param operationType the rest operation type
251         * @return the response
252         * @see <a href="https://www.hl7.org/fhir/operations.html">https://www.hl7.org/fhir/operations.html</a>
253         */
254        protected Response customOperation(
255                        final String resource,
256                        final RequestTypeEnum requestType,
257                        final String id,
258                        final String operationName,
259                        final RestOperationTypeEnum operationType)
260                        throws IOException {
261                final Builder request = getResourceRequest(requestType, operationType)
262                                .resource(resource)
263                                .id(id);
264                return execute(request, operationName);
265        }
266
267        /**
268         * Retrieve a version of a resource
269         *
270         * @param id the id of the resource
271         * @param version the version of the resource
272         * @return the response
273         * @see <a href="https://www.hl7.org/fhir/http.html#history">https://www.hl7.org/fhir/http.html#history</a>
274         */
275        @GET
276        @Path("/{id}/_history/{version}")
277        public Response findVersion(@PathParam("id") final String id, @PathParam("version") final String version)
278                        throws IOException {
279                final Builder theRequest = getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.VREAD)
280                                .id(id)
281                                .version(version);
282                return execute(theRequest);
283        }
284
285        /**
286         * Retrieve the update history for a particular resource
287         *
288         * @param id the id of the resource
289         * @return the response
290         * @see <a href="https://www.hl7.org/fhir/http.html#history">https://www.hl7.org/fhir/http.html#history</a>
291         */
292        @GET
293        @Path("/{id}/_history")
294        public Response historyForInstance(@PathParam("id") final String id) throws IOException {
295                final Builder theRequest = getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.HISTORY_INSTANCE)
296                                .id(id);
297                return execute(theRequest);
298        }
299
300        /**
301         * Retrieve the update history for a particular type
302         *
303         * @return the response
304         * @see <a href="https://www.hl7.org/fhir/http.html#history">https://www.hl7.org/fhir/http.html#history</a>
305         */
306        @GET
307        @Path("/_history")
308        public Response historyForType() throws IOException {
309                final Builder theRequest = getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.HISTORY_TYPE);
310                return execute(theRequest);
311        }
312
313        /**
314         * Compartment Based Access
315         *
316         * @param id the resource to which the compartment belongs
317         * @param compartment the compartment
318         * @return the repsonse
319         * @see <a href="https://www.hl7.org/fhir/http.html#search">https://www.hl7.org/fhir/http.html#search</a>
320         * @see <a href="https://www.hl7.org/fhir/compartments.html#compartment">https://www.hl7.org/fhir/compartments.html#compartment</a>
321         */
322        @GET
323        @Path("/{id}/{compartment : ((?!_history).)*}")
324        public Response findCompartment(
325                        @PathParam("id") final String id, @PathParam("compartment") final String compartment) throws IOException {
326                final Builder theRequest = getResourceRequest(RequestTypeEnum.GET, RestOperationTypeEnum.SEARCH_TYPE)
327                                .id(id)
328                                .compartment(compartment);
329                return execute(theRequest, compartment);
330        }
331
332        @POST
333        @Path("/$validate")
334        public Response validate(final String resource) throws IOException {
335                return customOperation(
336                                resource, RequestTypeEnum.POST, null, "$validate", RestOperationTypeEnum.EXTENDED_OPERATION_TYPE);
337        }
338
339        /**
340         * Execute the method described by the requestBuilder and methodKey
341         *
342         * @param theRequestBuilder the requestBuilder that contains the information about the request
343         * @param methodKey the key determining the method to be executed
344         * @return the response
345         */
346        private Response execute(final Builder theRequestBuilder, final String methodKey) throws IOException {
347                final JaxRsRequest theRequest = theRequestBuilder.build();
348                final BaseMethodBinding method = getBinding(theRequest.getRestOperationType(), methodKey);
349                try {
350                        return (Response) method.invokeServer(this, theRequest);
351                } catch (final Throwable theException) {
352                        return handleException(theRequest, theException);
353                }
354        }
355
356        /**
357         * Execute the method described by the requestBuilder
358         *
359         * @param theRequestBuilder the requestBuilder that contains the information about the request
360         * @return the response
361         */
362        private Response execute(final Builder theRequestBuilder) throws IOException {
363                return execute(theRequestBuilder, JaxRsMethodBindings.DEFAULT_METHOD_KEY);
364        }
365
366        /**
367         * Return the method binding for the given rest operation
368         *
369         * @param restOperation the rest operation to retrieve
370         * @param theBindingKey the key determining the method to be executed (needed for e.g. custom operation)
371         * @return
372         */
373        protected BaseMethodBinding getBinding(final RestOperationTypeEnum restOperation, final String theBindingKey) {
374                return getBindings().getBinding(restOperation, theBindingKey);
375        }
376
377        /**
378         * Default: no paging provider
379         */
380        @Override
381        public IPagingProvider getPagingProvider() {
382                return null;
383        }
384
385        /**
386         * Default: BundleInclusionRule.BASED_ON_INCLUDES
387         */
388        @Override
389        public BundleInclusionRule getBundleInclusionRule() {
390                return BundleInclusionRule.BASED_ON_INCLUDES;
391        }
392
393        @Override
394        public PreferReturnEnum getDefaultPreferReturn() {
395                return PreferReturnEnum.REPRESENTATION;
396        }
397
398        /**
399         * The resource type should return conform to the generic resource included
400         * in the topic
401         */
402        @Override
403        public abstract Class<R> getResourceType();
404
405        /**
406         * Return the bindings defined in this resource provider
407         *
408         * @return the jax-rs method bindings
409         */
410        public JaxRsMethodBindings getBindings() {
411                return theBindings;
412        }
413
414        /**
415         * Return the request builder based on the resource name for the server
416         * @param requestType the type of the request
417         * @param restOperation the rest operation type
418         * @return the requestbuilder
419         */
420        private Builder getResourceRequest(final RequestTypeEnum requestType, final RestOperationTypeEnum restOperation) {
421                return getRequest(requestType, restOperation, getResourceType().getSimpleName());
422        }
423}