
001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2025 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.jpa.repository; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 025import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 026import ca.uhn.fhir.model.api.IQueryParameterType; 027import ca.uhn.fhir.model.api.Include; 028import ca.uhn.fhir.model.valueset.BundleTypeEnum; 029import ca.uhn.fhir.repository.IRepository; 030import ca.uhn.fhir.rest.api.Constants; 031import ca.uhn.fhir.rest.api.MethodOutcome; 032import ca.uhn.fhir.rest.api.PatchTypeEnum; 033import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 034import ca.uhn.fhir.rest.api.server.IBundleProvider; 035import ca.uhn.fhir.rest.api.server.RequestDetails; 036import ca.uhn.fhir.rest.server.IPagingProvider; 037import ca.uhn.fhir.rest.server.RestfulServer; 038import ca.uhn.fhir.rest.server.RestfulServerUtils; 039import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 040import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 041import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; 042import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 043import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding; 044import ca.uhn.fhir.rest.server.method.PageMethodBinding; 045import ca.uhn.fhir.util.UrlUtil; 046import com.google.common.collect.Multimap; 047import jakarta.annotation.Nonnull; 048import org.hl7.fhir.instance.model.api.IBaseBundle; 049import org.hl7.fhir.instance.model.api.IBaseConformance; 050import org.hl7.fhir.instance.model.api.IBaseParameters; 051import org.hl7.fhir.instance.model.api.IBaseResource; 052import org.hl7.fhir.instance.model.api.IIdType; 053 054import java.io.IOException; 055import java.util.HashSet; 056import java.util.List; 057import java.util.Map; 058import java.util.Set; 059 060import static ca.uhn.fhir.jpa.repository.RequestDetailsCloner.startWith; 061import static org.apache.commons.lang3.StringUtils.isNotBlank; 062 063/** 064 * This class leverages DaoRegistry from Hapi-fhir to implement CRUD FHIR API operations constrained to provide only the operations necessary for the cql-evaluator modules to function. 065 **/ 066@SuppressWarnings("squid:S1135") 067public class HapiFhirRepository implements IRepository { 068 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HapiFhirRepository.class); 069 private final DaoRegistry myDaoRegistry; 070 private final RequestDetails myRequestDetails; 071 private final RestfulServer myRestfulServer; 072 073 public HapiFhirRepository( 074 DaoRegistry theDaoRegistry, RequestDetails theRequestDetails, RestfulServer theRestfulServer) { 075 myDaoRegistry = theDaoRegistry; 076 myRequestDetails = theRequestDetails; 077 myRestfulServer = theRestfulServer; 078 } 079 080 @Override 081 public <T extends IBaseResource, I extends IIdType> T read( 082 Class<T> theResourceType, I theId, Map<String, String> theHeaders) { 083 RequestDetails details = startWith(myRequestDetails) 084 .setAction(RestOperationTypeEnum.READ) 085 .addHeaders(theHeaders) 086 .create(); 087 return myDaoRegistry.getResourceDao(theResourceType).read(theId, details); 088 } 089 090 @Override 091 public <T extends IBaseResource> MethodOutcome create(T theResource, Map<String, String> theHeaders) { 092 RequestDetails details = startWith(myRequestDetails) 093 .setAction(RestOperationTypeEnum.CREATE) 094 .addHeaders(theHeaders) 095 .create(); 096 return myDaoRegistry.getResourceDao(theResource).create(theResource, details); 097 } 098 099 @Override 100 public <I extends IIdType, P extends IBaseParameters> MethodOutcome patch( 101 I theId, P thePatchParameters, Map<String, String> theHeaders) { 102 RequestDetails details = startWith(myRequestDetails) 103 .setAction(RestOperationTypeEnum.PATCH) 104 .addHeaders(theHeaders) 105 .create(); 106 107 return myDaoRegistry 108 .getResourceDao(theId.getResourceType()) 109 .patch(theId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, thePatchParameters, details); 110 } 111 112 @Override 113 public <T extends IBaseResource> MethodOutcome update(T theResource, Map<String, String> theHeaders) { 114 RequestDetails details = startWith(myRequestDetails) 115 .setAction(RestOperationTypeEnum.UPDATE) 116 .addHeaders(theHeaders) 117 .create(); 118 119 DaoMethodOutcome update = myDaoRegistry.getResourceDao(theResource).update(theResource, details); 120 boolean created = update.getCreated() != null && update.getCreated(); 121 if (created) { 122 update.setResponseStatusCode(Constants.STATUS_HTTP_201_CREATED); 123 } else { 124 update.setResponseStatusCode(Constants.STATUS_HTTP_200_OK); 125 } 126 return update; 127 } 128 129 @Override 130 public <T extends IBaseResource, I extends IIdType> MethodOutcome delete( 131 Class<T> theResourceType, I theId, Map<String, String> theHeaders) { 132 RequestDetails details = startWith(myRequestDetails) 133 .setAction(RestOperationTypeEnum.DELETE) 134 .addHeaders(theHeaders) 135 .create(); 136 137 return myDaoRegistry.getResourceDao(theResourceType).delete(theId, details); 138 } 139 140 @Override 141 public <B extends IBaseBundle, T extends IBaseResource> B search( 142 Class<B> theBundleType, 143 Class<T> theResourceType, 144 Multimap<String, List<IQueryParameterType>> theSearchParameters, 145 Map<String, String> theHeaders) { 146 RequestDetails details = startWith(myRequestDetails) 147 .setAction(RestOperationTypeEnum.SEARCH_TYPE) 148 .addHeaders(theHeaders) 149 .create(); 150 SearchConverter converter = new SearchConverter(); 151 converter.convertParameters(theSearchParameters, fhirContext()); 152 details.setParameters(converter.myResultParameters); 153 details.setResourceName(myDaoRegistry.getFhirContext().getResourceType(theResourceType)); 154 IBundleProvider bundleProvider = 155 myDaoRegistry.getResourceDao(theResourceType).search(converter.mySearchParameterMap, details); 156 157 if (bundleProvider == null) { 158 return null; 159 } 160 161 return createBundle(details, bundleProvider, null); 162 } 163 164 private <B extends IBaseBundle> B createBundle( 165 RequestDetails theRequestDetails, @Nonnull IBundleProvider theBundleProvider, String thePagingAction) { 166 Integer count = RestfulServerUtils.extractCountParameter(theRequestDetails); 167 String linkSelf = RestfulServerUtils.createLinkSelf(theRequestDetails.getFhirServerBase(), theRequestDetails); 168 169 Set<Include> includes = new HashSet<>(); 170 String[] reqIncludes = theRequestDetails.getParameters().get(Constants.PARAM_INCLUDE); 171 if (reqIncludes != null) { 172 for (String nextInclude : reqIncludes) { 173 includes.add(new Include(nextInclude)); 174 } 175 } 176 177 Integer offset = RestfulServerUtils.tryToExtractNamedParameter(theRequestDetails, Constants.PARAM_PAGINGOFFSET); 178 if (offset == null || offset < 0) { 179 offset = 0; 180 } 181 int start = offset; 182 Integer size = theBundleProvider.size(); 183 if (size != null) { 184 start = Math.max(0, Math.min(offset, size)); 185 } 186 187 BundleTypeEnum bundleType; 188 String[] bundleTypeValues = theRequestDetails.getParameters().get(Constants.PARAM_BUNDLETYPE); 189 if (bundleTypeValues != null) { 190 bundleType = BundleTypeEnum.VALUESET_BINDER.fromCodeString(bundleTypeValues[0]); 191 } else { 192 bundleType = BundleTypeEnum.SEARCHSET; 193 } 194 195 return unsafeCast(BundleProviderUtil.createBundleFromBundleProvider( 196 myRestfulServer, 197 theRequestDetails, 198 count, 199 linkSelf, 200 includes, 201 theBundleProvider, 202 start, 203 bundleType, 204 thePagingAction)); 205 } 206 207 // TODO: The main use case for this is paging through Bundles, but I suppose that technically 208 // we ought to handle any old link. Maybe this is also an escape hatch for "custom non-FHIR 209 // repository action"? 210 @Override 211 public <B extends IBaseBundle> B link(Class<B> theBundleType, String theUrl, Map<String, String> theHeaders) { 212 RequestDetails details = startWith(myRequestDetails) 213 .setAction(RestOperationTypeEnum.GET_PAGE) 214 .addHeaders(theHeaders) 215 .create(); 216 UrlUtil.UrlParts urlParts = UrlUtil.parseUrl(theUrl); 217 details.setCompleteUrl(theUrl); 218 details.setParameters(UrlUtil.parseQueryStrings(urlParts.getParams())); 219 220 IPagingProvider pagingProvider = myRestfulServer.getPagingProvider(); 221 if (pagingProvider == null) { 222 throw new InvalidRequestException(Msg.code(2638) + "This server does not support paging"); 223 } 224 225 String pagingAction = details.getParameters().get(Constants.PARAM_PAGINGACTION)[0]; 226 227 IBundleProvider bundleProvider; 228 229 String pageId = null; 230 String[] pageIdParams = details.getParameters().get(Constants.PARAM_PAGEID); 231 if (pageIdParams != null && pageIdParams.length > 0 && isNotBlank(pageIdParams[0])) { 232 pageId = pageIdParams[0]; 233 } 234 235 if (pageId != null) { 236 // This is a page request by Search ID and Page ID 237 bundleProvider = pagingProvider.retrieveResultList(details, pagingAction, pageId); 238 validateHaveBundleProvider(pagingAction, bundleProvider); 239 } else { 240 // This is a page request by Search ID and Offset 241 bundleProvider = pagingProvider.retrieveResultList(details, pagingAction); 242 validateHaveBundleProvider(pagingAction, bundleProvider); 243 } 244 245 return createBundle(details, bundleProvider, pagingAction); 246 } 247 248 private void validateHaveBundleProvider(String thePagingAction, IBundleProvider theBundleProvider) { 249 // Return an HTTP 410 if the search is not known 250 if (theBundleProvider == null) { 251 ourLog.info("Client requested unknown paging ID[{}]", thePagingAction); 252 String msg = fhirContext() 253 .getLocalizer() 254 .getMessage(PageMethodBinding.class, "unknownSearchId", thePagingAction); 255 throw new ResourceGoneException(Msg.code(2639) + msg); 256 } 257 } 258 259 @Override 260 public <C extends IBaseConformance> C capabilities( 261 Class<C> theCapabilityStatementType, Map<String, String> theHeaders) { 262 ConformanceMethodBinding method = myRestfulServer.getServerConformanceMethod(); 263 if (method == null) { 264 return null; 265 } 266 RequestDetails details = startWith(myRequestDetails) 267 .setAction(RestOperationTypeEnum.METADATA) 268 .addHeaders(theHeaders) 269 .create(); 270 return unsafeCast(method.provideCapabilityStatement(myRestfulServer, details)); 271 } 272 273 @Override 274 @SuppressWarnings("unchecked") 275 public <B extends IBaseBundle> B transaction(B theBundle, Map<String, String> theHeaders) { 276 RequestDetails details = startWith(myRequestDetails) 277 .setAction(RestOperationTypeEnum.TRANSACTION) 278 .addHeaders(theHeaders) 279 .create(); 280 return unsafeCast(myDaoRegistry.getSystemDao().transaction(details, theBundle)); 281 } 282 283 @Override 284 public <R extends IBaseResource, P extends IBaseParameters> R invoke( 285 String theName, P theParameters, Class<R> theReturnType, Map<String, String> theHeaders) { 286 RequestDetails details = startWith(myRequestDetails) 287 .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER) 288 .addHeaders(theHeaders) 289 .setOperation(theName) 290 .setParameters(theParameters) 291 .create(); 292 293 return invoke(details); 294 } 295 296 @Override 297 public <P extends IBaseParameters> MethodOutcome invoke( 298 String theName, P theParameters, Map<String, String> theHeaders) { 299 RequestDetails details = startWith(myRequestDetails) 300 .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER) 301 .addHeaders(theHeaders) 302 .setOperation(theName) 303 .setParameters(theParameters) 304 .create(); 305 306 return invoke(details); 307 } 308 309 @Override 310 public <R extends IBaseResource, P extends IBaseParameters, T extends IBaseResource> R invoke( 311 Class<T> theResourceType, 312 String theName, 313 P theParameters, 314 Class<R> theReturnType, 315 Map<String, String> theHeaders) { 316 RequestDetails details = startWith(myRequestDetails) 317 .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER) 318 .addHeaders(theHeaders) 319 .setOperation(theName) 320 .setResourceType(theResourceType.getSimpleName()) 321 .setParameters(theParameters) 322 .create(); 323 324 return invoke(details); 325 } 326 327 @Override 328 public <P extends IBaseParameters, T extends IBaseResource> MethodOutcome invoke( 329 Class<T> theResourceType, String theName, P theParameters, Map<String, String> theHeaders) { 330 RequestDetails details = startWith(myRequestDetails) 331 .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER) 332 .addHeaders(theHeaders) 333 .setOperation(theName) 334 .setResourceType(theResourceType.getSimpleName()) 335 .setParameters(theParameters) 336 .create(); 337 338 return invoke(details); 339 } 340 341 @Override 342 public <R extends IBaseResource, P extends IBaseParameters, I extends IIdType> R invoke( 343 I theId, String theName, P theParameters, Class<R> theReturnType, Map<String, String> theHeaders) { 344 RequestDetails details = startWith(myRequestDetails) 345 .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER) 346 .addHeaders(theHeaders) 347 .setOperation(theName) 348 .setResourceType(theId.getResourceType()) 349 .setId(theId) 350 .setParameters(theParameters) 351 .create(); 352 353 return invoke(details); 354 } 355 356 @Override 357 public <P extends IBaseParameters, I extends IIdType> MethodOutcome invoke( 358 I theId, String theName, P theParameters, Map<String, String> theHeaders) { 359 RequestDetails details = startWith(myRequestDetails) 360 .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER) 361 .addHeaders(theHeaders) 362 .setOperation(theName) 363 .setResourceType(theId.getResourceType()) 364 .setId(theId) 365 .setParameters(theParameters) 366 .create(); 367 368 return invoke(details); 369 } 370 371 private void notImplemented() { 372 throw new NotImplementedOperationException(Msg.code(2640) + "history not yet implemented"); 373 } 374 375 @Override 376 public <B extends IBaseBundle, P extends IBaseParameters> B history( 377 P theParameters, Class<B> theBundleType, Map<String, String> theHeaders) { 378 notImplemented(); 379 380 return null; 381 } 382 383 @Override 384 public <B extends IBaseBundle, P extends IBaseParameters, T extends IBaseResource> B history( 385 Class<T> theResourceType, P theParameters, Class<B> theBundleType, Map<String, String> theHeaders) { 386 notImplemented(); 387 388 return null; 389 } 390 391 @Override 392 public <B extends IBaseBundle, P extends IBaseParameters, I extends IIdType> B history( 393 I theId, P theParameters, Class<B> theBundleType, Map<String, String> theHeaders) { 394 notImplemented(); 395 396 return null; 397 } 398 399 @Override 400 public @Nonnull FhirContext fhirContext() { 401 return myDaoRegistry.getFhirContext(); 402 } 403 404 protected <R> R invoke(RequestDetails theDetails) { 405 try { 406 return unsafeCast(myRestfulServer 407 .determineResourceMethod(theDetails, null) 408 .invokeServer(myRestfulServer, theDetails)); 409 } catch (IOException exception) { 410 throw new InternalErrorException(Msg.code(2641) + exception); 411 } 412 } 413 414 @SuppressWarnings("unchecked") 415 private static <T> T unsafeCast(Object theObject) { 416 return (T) theObject; 417 } 418}