001package ca.uhn.fhir.rest.server.provider; 002 003/*- 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 025import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 026import ca.uhn.fhir.context.FhirContext; 027import ca.uhn.fhir.context.FhirVersionEnum; 028import ca.uhn.fhir.interceptor.api.HookParams; 029import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 030import ca.uhn.fhir.interceptor.api.Pointcut; 031import ca.uhn.fhir.model.api.IResource; 032import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 033import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; 034import ca.uhn.fhir.rest.annotation.Create; 035import ca.uhn.fhir.rest.annotation.Delete; 036import ca.uhn.fhir.rest.annotation.History; 037import ca.uhn.fhir.rest.annotation.IdParam; 038import ca.uhn.fhir.rest.annotation.Read; 039import ca.uhn.fhir.rest.annotation.RequiredParam; 040import ca.uhn.fhir.rest.annotation.ResourceParam; 041import ca.uhn.fhir.rest.annotation.Search; 042import ca.uhn.fhir.rest.annotation.Update; 043import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum; 044import ca.uhn.fhir.rest.api.MethodOutcome; 045import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 046import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 047import ca.uhn.fhir.rest.api.server.RequestDetails; 048import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; 049import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 050import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 051import ca.uhn.fhir.rest.param.TokenAndListParam; 052import ca.uhn.fhir.rest.param.TokenOrListParam; 053import ca.uhn.fhir.rest.param.TokenParam; 054import ca.uhn.fhir.rest.server.IResourceProvider; 055import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 056import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 057import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 058import ca.uhn.fhir.util.ValidateUtil; 059import com.google.common.collect.Lists; 060import org.hl7.fhir.instance.model.api.IBase; 061import org.hl7.fhir.instance.model.api.IBaseResource; 062import org.hl7.fhir.instance.model.api.IIdType; 063import org.hl7.fhir.instance.model.api.IPrimitiveType; 064import org.slf4j.Logger; 065import org.slf4j.LoggerFactory; 066 067import javax.annotation.Nonnull; 068import java.util.ArrayList; 069import java.util.Collections; 070import java.util.LinkedHashMap; 071import java.util.LinkedList; 072import java.util.List; 073import java.util.Map; 074import java.util.TreeMap; 075import java.util.concurrent.atomic.AtomicLong; 076 077import static org.apache.commons.lang3.StringUtils.isBlank; 078 079/** 080 * This class is a simple implementation of the resource provider 081 * interface that uses a HashMap to store all resources in memory. 082 * <p> 083 * This class currently supports the following FHIR operations: 084 * </p> 085 * <ul> 086 * <li>Create</li> 087 * <li>Update existing resource</li> 088 * <li>Update non-existing resource (e.g. create with client-supplied ID)</li> 089 * <li>Delete</li> 090 * <li>Search by resource type with no parameters</li> 091 * </ul> 092 * 093 * @param <T> The resource type to support 094 */ 095public class HashMapResourceProvider<T extends IBaseResource> implements IResourceProvider { 096 private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProvider.class); 097 private final Class<T> myResourceType; 098 private final FhirContext myFhirContext; 099 private final String myResourceName; 100 protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new LinkedHashMap<>(); 101 protected Map<String, LinkedList<T>> myIdToHistory = new LinkedHashMap<>(); 102 protected LinkedList<T> myTypeHistory = new LinkedList<>(); 103 protected AtomicLong mySearchCount = new AtomicLong(0); 104 private long myNextId; 105 private AtomicLong myDeleteCount = new AtomicLong(0); 106 private AtomicLong myUpdateCount = new AtomicLong(0); 107 private AtomicLong myCreateCount = new AtomicLong(0); 108 private AtomicLong myReadCount = new AtomicLong(0); 109 110 /** 111 * Constructor 112 * 113 * @param theFhirContext The FHIR context 114 * @param theResourceType The resource type to support 115 */ 116 public HashMapResourceProvider(FhirContext theFhirContext, Class<T> theResourceType) { 117 myFhirContext = theFhirContext; 118 myResourceType = theResourceType; 119 myResourceName = myFhirContext.getResourceType(theResourceType); 120 clear(); 121 } 122 123 /** 124 * Clear all data held in this resource provider 125 */ 126 public synchronized void clear() { 127 myNextId = 1; 128 myIdToVersionToResourceMap.clear(); 129 myIdToHistory.clear(); 130 myTypeHistory.clear(); 131 } 132 133 /** 134 * Clear the counts used by {@link #getCountRead()} and other count methods 135 */ 136 public synchronized void clearCounts() { 137 myReadCount.set(0L); 138 myUpdateCount.set(0L); 139 myCreateCount.set(0L); 140 myDeleteCount.set(0L); 141 mySearchCount.set(0L); 142 } 143 144 @Create 145 public synchronized MethodOutcome create(@ResourceParam T theResource, RequestDetails theRequestDetails) { 146 TransactionDetails transactionDetails = new TransactionDetails(); 147 148 createInternal(theResource, theRequestDetails, transactionDetails); 149 150 myCreateCount.incrementAndGet(); 151 152 return new MethodOutcome() 153 .setCreated(true) 154 .setResource(theResource) 155 .setId(theResource.getIdElement()); 156 } 157 158 private void createInternal(@ResourceParam T theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 159 long idPart = myNextId++; 160 String idPartAsString = Long.toString(idPart); 161 Long versionIdPart = 1L; 162 163 assert !myIdToVersionToResourceMap.containsKey(idPartAsString); 164 165 IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails); 166 theResource.setId(id); 167 } 168 169 @Delete 170 public synchronized MethodOutcome delete(@IdParam IIdType theId, RequestDetails theRequestDetails) { 171 TransactionDetails transactionDetails = new TransactionDetails(); 172 173 TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart()); 174 if (versions == null || versions.isEmpty()) { 175 throw new ResourceNotFoundException(Msg.code(1979) + theId); 176 } 177 178 179 long nextVersion = versions.lastEntry().getKey() + 1L; 180 IIdType id = store(null, theId.getIdPart(), nextVersion, theRequestDetails, transactionDetails); 181 182 myDeleteCount.incrementAndGet(); 183 184 return new MethodOutcome() 185 .setId(id); 186 } 187 188 /** 189 * This method returns a simple operation count. This is mostly 190 * useful for testing purposes. 191 */ 192 public synchronized long getCountCreate() { 193 return myCreateCount.get(); 194 } 195 196 /** 197 * This method returns a simple operation count. This is mostly 198 * useful for testing purposes. 199 */ 200 public synchronized long getCountDelete() { 201 return myDeleteCount.get(); 202 } 203 204 /** 205 * This method returns a simple operation count. This is mostly 206 * useful for testing purposes. 207 */ 208 public synchronized long getCountRead() { 209 return myReadCount.get(); 210 } 211 212 /** 213 * This method returns a simple operation count. This is mostly 214 * useful for testing purposes. 215 */ 216 public synchronized long getCountSearch() { 217 return mySearchCount.get(); 218 } 219 220 /** 221 * This method returns a simple operation count. This is mostly 222 * useful for testing purposes. 223 */ 224 public synchronized long getCountUpdate() { 225 return myUpdateCount.get(); 226 } 227 228 @Override 229 public Class<T> getResourceType() { 230 return myResourceType; 231 } 232 233 private TreeMap<Long, T> getVersionToResource(String theIdPart) { 234 myIdToVersionToResourceMap.computeIfAbsent(theIdPart, t -> new TreeMap<>()); 235 return myIdToVersionToResourceMap.get(theIdPart); 236 } 237 238 @History 239 public synchronized List<IBaseResource> historyInstance(@IdParam IIdType theId, RequestDetails theRequestDetails) { 240 LinkedList<T> retVal = myIdToHistory.get(theId.getIdPart()); 241 if (retVal == null) { 242 throw new ResourceNotFoundException(Msg.code(1980) + theId); 243 } 244 245 return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); 246 } 247 248 @History 249 public List<T> historyType() { 250 return myTypeHistory; 251 } 252 253 @Read(version = true) 254 public synchronized T read(@IdParam IIdType theId, RequestDetails theRequestDetails) { 255 TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart()); 256 if (versions == null || versions.isEmpty()) { 257 throw new ResourceNotFoundException(Msg.code(1981) + theId); 258 } 259 260 T retVal; 261 if (theId.hasVersionIdPart()) { 262 Long versionId = theId.getVersionIdPartAsLong(); 263 if (!versions.containsKey(versionId)) { 264 throw new ResourceNotFoundException(Msg.code(1982) + theId); 265 } else { 266 T resource = versions.get(versionId); 267 if (resource == null) { 268 throw new ResourceGoneException(Msg.code(1983) + theId); 269 } 270 retVal = resource; 271 } 272 273 } else { 274 retVal = versions.lastEntry().getValue(); 275 } 276 277 myReadCount.incrementAndGet(); 278 279 retVal = fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); 280 if (retVal == null) { 281 throw new ResourceNotFoundException(Msg.code(1984) + theId); 282 } 283 return retVal; 284 } 285 286 @Search 287 public synchronized List<IBaseResource> searchAll(RequestDetails theRequestDetails) { 288 mySearchCount.incrementAndGet(); 289 List<T> retVal = getAllResources(); 290 return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); 291 } 292 293 @Nonnull 294 protected synchronized List<T> getAllResources() { 295 List<T> retVal = new ArrayList<>(); 296 297 for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) { 298 if (next.isEmpty() == false) { 299 T nextResource = next.lastEntry().getValue(); 300 if (nextResource != null) { 301 retVal.add(nextResource); 302 } 303 } 304 } 305 306 return retVal; 307 } 308 309 @Search 310 public synchronized List<IBaseResource> searchById( 311 @RequiredParam(name = "_id") TokenAndListParam theIds, RequestDetails theRequestDetails) { 312 313 List<T> retVal = new ArrayList<>(); 314 315 for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) { 316 if (next.isEmpty() == false) { 317 T nextResource = next.lastEntry().getValue(); 318 319 boolean matches = true; 320 if (theIds != null && theIds.getValuesAsQueryTokens().size() > 0) { 321 for (TokenOrListParam nextIdAnd : theIds.getValuesAsQueryTokens()) { 322 matches = false; 323 for (TokenParam nextOr : nextIdAnd.getValuesAsQueryTokens()) { 324 if (nextOr.getValue().equals(nextResource.getIdElement().getIdPart())) { 325 matches = true; 326 } 327 } 328 if (!matches) { 329 break; 330 } 331 } 332 } 333 334 if (!matches) { 335 continue; 336 } 337 338 retVal.add(nextResource); 339 } 340 } 341 342 mySearchCount.incrementAndGet(); 343 344 return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); 345 } 346 347 private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 348 IIdType id = myFhirContext.getVersion().newIdType(); 349 String versionIdPart = Long.toString(theVersionIdPart); 350 id.setParts(null, myResourceName, theIdPart, versionIdPart); 351 if (theResource != null) { 352 theResource.setId(id); 353 } 354 355 /* 356 * This is a bit of magic to make sure that the versionId attribute 357 * in the resource being stored accurately represents the version 358 * that was assigned by this provider 359 */ 360 if (theResource != null) { 361 if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) { 362 ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, versionIdPart); 363 } else { 364 BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta"); 365 List<IBase> metaValues = metaChild.getAccessor().getValues(theResource); 366 if (metaValues.size() > 0) { 367 IBase meta = metaValues.get(0); 368 BaseRuntimeElementCompositeDefinition<?> metaDef = (BaseRuntimeElementCompositeDefinition<?>) myFhirContext.getElementDefinition(meta.getClass()); 369 BaseRuntimeChildDefinition versionIdDef = metaDef.getChildByName("versionId"); 370 List<IBase> versionIdValues = versionIdDef.getAccessor().getValues(meta); 371 if (versionIdValues.size() > 0) { 372 IPrimitiveType<?> versionId = (IPrimitiveType<?>) versionIdValues.get(0); 373 versionId.setValueAsString(versionIdPart); 374 } 375 } 376 } 377 } 378 379 ourLog.info("Storing resource with ID: {}", id.getValue()); 380 381 // Store to ID->version->resource map 382 TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart); 383 versionToResource.put(theVersionIdPart, theResource); 384 385 if (theRequestDetails != null) { 386 IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); 387 388 if (theResource != null) { 389 if (!myIdToHistory.containsKey(theIdPart)) { 390 391 // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED 392 HookParams preStorageParams = new HookParams() 393 .add(RequestDetails.class, theRequestDetails) 394 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 395 .add(IBaseResource.class, theResource) 396 .add(TransactionDetails.class, theTransactionDetails); 397 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, preStorageParams); 398 399 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_CREATED 400 HookParams preCommitParams = new HookParams() 401 .add(RequestDetails.class, theRequestDetails) 402 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 403 .add(IBaseResource.class, theResource) 404 .add(TransactionDetails.class, theTransactionDetails) 405 .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 406 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, preCommitParams); 407 408 } else { 409 410 // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_UPDATED 411 HookParams preStorageParams = new HookParams() 412 .add(RequestDetails.class, theRequestDetails) 413 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 414 .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) 415 .add(IBaseResource.class, theResource) 416 .add(TransactionDetails.class, theTransactionDetails); 417 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); 418 419 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 420 HookParams preCommitParams = new HookParams() 421 .add(RequestDetails.class, theRequestDetails) 422 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 423 .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) 424 .add(IBaseResource.class, theResource) 425 .add(TransactionDetails.class, theTransactionDetails) 426 .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 427 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); 428 429 } 430 } 431 } 432 433 // Store to type history map 434 myTypeHistory.addFirst(theResource); 435 436 // Store to ID history map 437 myIdToHistory.computeIfAbsent(theIdPart, t -> new LinkedList<>()); 438 myIdToHistory.get(theIdPart).addFirst(theResource); 439 440 // Return the newly assigned ID including the version ID 441 return id; 442 } 443 444 /** 445 * @param theConditional This is provided only so that subclasses can implement if they want 446 */ 447 @Update 448 public synchronized MethodOutcome update( 449 @ResourceParam T theResource, 450 @ConditionalUrlParam String theConditional, 451 RequestDetails theRequestDetails) { 452 TransactionDetails transactionDetails = new TransactionDetails(); 453 454 ValidateUtil.isTrueOrThrowInvalidRequest(isBlank(theConditional), "This server doesn't support conditional update"); 455 456 boolean created = updateInternal(theResource, theRequestDetails, transactionDetails); 457 myUpdateCount.incrementAndGet(); 458 459 return new MethodOutcome() 460 .setCreated(created) 461 .setResource(theResource) 462 .setId(theResource.getIdElement()); 463 } 464 465 private boolean updateInternal(@ResourceParam T theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 466 String idPartAsString = theResource.getIdElement().getIdPart(); 467 TreeMap<Long, T> versionToResource = getVersionToResource(idPartAsString); 468 469 Long versionIdPart; 470 boolean created; 471 if (versionToResource.isEmpty()) { 472 versionIdPart = 1L; 473 created = true; 474 } else { 475 versionIdPart = versionToResource.lastKey() + 1L; 476 created = false; 477 } 478 479 IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails); 480 theResource.setId(id); 481 return created; 482 } 483 484 public FhirContext getFhirContext() { 485 return myFhirContext; 486 } 487 488 /** 489 * This is a utility method that can be used to store a resource without 490 * having to use the outside API. In this case, the storage happens without 491 * any interaction with interceptors, etc. 492 * 493 * @param theResource The resource to store. If the resource has an ID, that ID is updated. 494 * @return Return the ID assigned to the stored resource 495 */ 496 public synchronized IIdType store(T theResource) { 497 if (theResource.getIdElement().hasIdPart()) { 498 updateInternal(theResource, null, new TransactionDetails()); 499 } else { 500 createInternal(theResource, null, new TransactionDetails()); 501 } 502 return theResource.getIdElement(); 503 } 504 505 /** 506 * Returns an unmodifiable list containing the current version of all resources stored in this provider 507 * 508 * @since 4.1.0 509 */ 510 public synchronized List<T> getStoredResources() { 511 List<T> retVal = new ArrayList<>(); 512 for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) { 513 retVal.add(next.lastEntry().getValue()); 514 } 515 return Collections.unmodifiableList(retVal); 516 } 517 518 private static <T extends IBaseResource> T fireInterceptorsAndFilterAsNeeded(T theResource, RequestDetails theRequestDetails) { 519 List<IBaseResource> output = fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails); 520 if (output.size() == 1) { 521 return theResource; 522 } else { 523 return null; 524 } 525 } 526 527 protected static <T extends IBaseResource> List<IBaseResource> fireInterceptorsAndFilterAsNeeded(List<T> theResources, RequestDetails theRequestDetails) { 528 List<IBaseResource> resourcesToReturn = new ArrayList<>(theResources); 529 530 if (theRequestDetails != null) { 531 IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); 532 533 // Call the STORAGE_PREACCESS_RESOURCES pointcut (used for consent/auth interceptors) 534 SimplePreResourceAccessDetails preResourceAccessDetails = new SimplePreResourceAccessDetails(resourcesToReturn); 535 HookParams params = new HookParams() 536 .add(RequestDetails.class, theRequestDetails) 537 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 538 .add(IPreResourceAccessDetails.class, preResourceAccessDetails); 539 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 540 preResourceAccessDetails.applyFilterToList(); 541 542 // Call the STORAGE_PREACCESS_RESOURCES pointcut (used for consent/auth interceptors) 543 SimplePreResourceShowDetails preResourceShowDetails = new SimplePreResourceShowDetails(resourcesToReturn); 544 HookParams preShowParams = new HookParams() 545 .add(RequestDetails.class, theRequestDetails) 546 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 547 .add(IPreResourceShowDetails.class, preResourceShowDetails); 548 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, preShowParams); 549 resourcesToReturn = preResourceShowDetails.toList(); 550 551 } 552 553 return resourcesToReturn; 554 } 555}