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}