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