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}