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