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