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.model.api.IResource; 028import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 029import ca.uhn.fhir.rest.annotation.*; 030import ca.uhn.fhir.rest.api.MethodOutcome; 031import ca.uhn.fhir.rest.param.TokenAndListParam; 032import ca.uhn.fhir.rest.param.TokenOrListParam; 033import ca.uhn.fhir.rest.param.TokenParam; 034import ca.uhn.fhir.rest.server.IResourceProvider; 035import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 036import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 037import ca.uhn.fhir.util.ValidateUtil; 038import org.hl7.fhir.instance.model.api.IBase; 039import org.hl7.fhir.instance.model.api.IBaseResource; 040import org.hl7.fhir.instance.model.api.IIdType; 041import org.hl7.fhir.instance.model.api.IPrimitiveType; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044 045import java.util.*; 046import java.util.concurrent.atomic.AtomicLong; 047 048import static org.apache.commons.lang3.StringUtils.isBlank; 049 050/** 051 * This class is a simple implementation of the resource provider 052 * interface that uses a HashMap to store all resources in memory. 053 * <p> 054 * This class currently supports the following FHIR operations: 055 * </p> 056 * <ul> 057 * <li>Create</li> 058 * <li>Update existing resource</li> 059 * <li>Update non-existing resource (e.g. create with client-supplied ID)</li> 060 * <li>Delete</li> 061 * <li>Search by resource type with no parameters</li> 062 * </ul> 063 * 064 * @param <T> The resource type to support 065 */ 066public class HashMapResourceProvider<T extends IBaseResource> implements IResourceProvider { 067 private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProvider.class); 068 private final Class<T> myResourceType; 069 private final FhirContext myFhirContext; 070 private final String myResourceName; 071 protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = Collections.synchronizedMap(new LinkedHashMap<>()); 072 protected Map<String, LinkedList<T>> myIdToHistory = Collections.synchronizedMap(new LinkedHashMap<>()); 073 protected LinkedList<T> myTypeHistory = new LinkedList<>(); 074 private long myNextId; 075 private AtomicLong myDeleteCount = new AtomicLong(0); 076 private AtomicLong mySearchCount = new AtomicLong(0); 077 private AtomicLong myUpdateCount = new AtomicLong(0); 078 private AtomicLong myCreateCount = new AtomicLong(0); 079 private AtomicLong myReadCount = new AtomicLong(0); 080 081 /** 082 * Constructor 083 * 084 * @param theFhirContext The FHIR context 085 * @param theResourceType The resource type to support 086 */ 087 @SuppressWarnings("WeakerAccess") 088 public HashMapResourceProvider(FhirContext theFhirContext, Class<T> theResourceType) { 089 myFhirContext = theFhirContext; 090 myResourceType = theResourceType; 091 myResourceName = myFhirContext.getResourceDefinition(theResourceType).getName(); 092 clear(); 093 } 094 095 /** 096 * Clear all data held in this resource provider 097 */ 098 public void clear() { 099 myNextId = 1; 100 myIdToVersionToResourceMap.clear(); 101 myIdToHistory.clear(); 102 myTypeHistory.clear(); 103 } 104 105 /** 106 * Clear the counts used by {@link #getCountRead()} and other count methods 107 */ 108 public void clearCounts() { 109 myReadCount.set(0L); 110 myUpdateCount.set(0L); 111 myCreateCount.set(0L); 112 myDeleteCount.set(0L); 113 mySearchCount.set(0L); 114 } 115 116 @Create 117 public MethodOutcome create(@ResourceParam T theResource) { 118 long idPart = myNextId++; 119 String idPartAsString = Long.toString(idPart); 120 Long versionIdPart = 1L; 121 122 IIdType id = store(theResource, idPartAsString, versionIdPart); 123 124 myCreateCount.incrementAndGet(); 125 126 return new MethodOutcome() 127 .setCreated(true) 128 .setId(id); 129 } 130 131 @Delete 132 public MethodOutcome delete(@IdParam IIdType theId) { 133 TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart()); 134 if (versions == null || versions.isEmpty()) { 135 throw new ResourceNotFoundException(theId); 136 } 137 138 139 long nextVersion = versions.lastEntry().getKey() + 1L; 140 IIdType id = store(null, theId.getIdPart(), nextVersion); 141 142 myDeleteCount.incrementAndGet(); 143 144 return new MethodOutcome() 145 .setId(id); 146 } 147 148 /** 149 * This method returns a simple operation count. This is mostly 150 * useful for testing purposes. 151 */ 152 public long getCountCreate() { 153 return myCreateCount.get(); 154 } 155 156 /** 157 * This method returns a simple operation count. This is mostly 158 * useful for testing purposes. 159 */ 160 public long getCountDelete() { 161 return myDeleteCount.get(); 162 } 163 164 /** 165 * This method returns a simple operation count. This is mostly 166 * useful for testing purposes. 167 */ 168 public long getCountRead() { 169 return myReadCount.get(); 170 } 171 172 /** 173 * This method returns a simple operation count. This is mostly 174 * useful for testing purposes. 175 */ 176 public long getCountSearch() { 177 return mySearchCount.get(); 178 } 179 180 /** 181 * This method returns a simple operation count. This is mostly 182 * useful for testing purposes. 183 */ 184 public long getCountUpdate() { 185 return myUpdateCount.get(); 186 } 187 188 @Override 189 public Class<T> getResourceType() { 190 return myResourceType; 191 } 192 193 private synchronized TreeMap<Long, T> getVersionToResource(String theIdPart) { 194 myIdToVersionToResourceMap.computeIfAbsent(theIdPart, t -> new TreeMap<>()); 195 return myIdToVersionToResourceMap.get(theIdPart); 196 } 197 198 @History 199 public List<T> historyInstance(@IdParam IIdType theId) { 200 LinkedList<T> retVal = myIdToHistory.get(theId.getIdPart()); 201 if (retVal == null) { 202 throw new ResourceNotFoundException(theId); 203 } 204 205 return retVal; 206 } 207 208 @History 209 public List<T> historyType() { 210 return myTypeHistory; 211 } 212 213 @Read(version = true) 214 public T read(@IdParam IIdType theId) { 215 TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart()); 216 if (versions == null || versions.isEmpty()) { 217 throw new ResourceNotFoundException(theId); 218 } 219 220 T retVal; 221 if (theId.hasVersionIdPart()) { 222 Long versionId = theId.getVersionIdPartAsLong(); 223 if (!versions.containsKey(versionId)) { 224 throw new ResourceNotFoundException(theId); 225 } else { 226 T resource = versions.get(versionId); 227 if (resource == null) { 228 throw new ResourceGoneException(theId); 229 } 230 retVal = resource; 231 } 232 233 } else { 234 retVal = versions.lastEntry().getValue(); 235 } 236 237 myReadCount.incrementAndGet(); 238 239 return retVal; 240 } 241 242 @Search 243 public List<T> searchAll() { 244 List<T> retVal = new ArrayList<>(); 245 246 for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) { 247 if (next.isEmpty() == false) { 248 T nextResource = next.lastEntry().getValue(); 249 retVal.add(nextResource); 250 } 251 } 252 253 mySearchCount.incrementAndGet(); 254 return retVal; 255 } 256 257 @Search 258 public List<T> searchById( 259 @RequiredParam(name = "_id") TokenAndListParam theIds) { 260 261 List<T> retVal = new ArrayList<>(); 262 263 for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) { 264 if (next.isEmpty() == false) { 265 T nextResource = next.lastEntry().getValue(); 266 267 boolean matches = true; 268 if (theIds != null && theIds.getValuesAsQueryTokens().size() > 0) { 269 for (TokenOrListParam nextIdAnd : theIds.getValuesAsQueryTokens()) { 270 matches = false; 271 for (TokenParam nextOr : nextIdAnd.getValuesAsQueryTokens()) { 272 if (nextOr.getValue().equals(nextResource.getIdElement().getIdPart())) { 273 matches = true; 274 } 275 } 276 if (!matches) { 277 break; 278 } 279 } 280 } 281 282 if (!matches) { 283 continue; 284 } 285 286 retVal.add(nextResource); 287 } 288 } 289 290 mySearchCount.incrementAndGet(); 291 292 return retVal; 293 } 294 295 private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart) { 296 IIdType id = myFhirContext.getVersion().newIdType(); 297 String versionIdPart = Long.toString(theVersionIdPart); 298 id.setParts(null, myResourceName, theIdPart, versionIdPart); 299 if (theResource != null) { 300 theResource.setId(id); 301 } 302 303 /* 304 * This is a bit of magic to make sure that the versionId attribute 305 * in the resource being stored accurately represents the version 306 * that was assigned by this provider 307 */ 308 if (theResource != null) { 309 if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) { 310 ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, versionIdPart); 311 } else { 312 BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta"); 313 List<IBase> metaValues = metaChild.getAccessor().getValues(theResource); 314 if (metaValues.size() > 0) { 315 IBase meta = metaValues.get(0); 316 BaseRuntimeElementCompositeDefinition<?> metaDef = (BaseRuntimeElementCompositeDefinition<?>) myFhirContext.getElementDefinition(meta.getClass()); 317 BaseRuntimeChildDefinition versionIdDef = metaDef.getChildByName("versionId"); 318 List<IBase> versionIdValues = versionIdDef.getAccessor().getValues(meta); 319 if (versionIdValues.size() > 0) { 320 IPrimitiveType<?> versionId = (IPrimitiveType<?>) versionIdValues.get(0); 321 versionId.setValueAsString(versionIdPart); 322 } 323 } 324 } 325 } 326 327 ourLog.info("Storing resource with ID: {}", id.getValue()); 328 329 // Store to ID->version->resource map 330 TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart); 331 versionToResource.put(theVersionIdPart, theResource); 332 333 // Store to type history map 334 myTypeHistory.addFirst(theResource); 335 336 // Store to ID history map 337 myIdToHistory.computeIfAbsent(theIdPart, t -> new LinkedList<>()); 338 myIdToHistory.get(theIdPart).addFirst(theResource); 339 340 // Return the newly assigned ID including the version ID 341 return id; 342 } 343 344 /** 345 * @param theConditional This is provided only so that subclasses can implement if they want 346 */ 347 @Update 348 public MethodOutcome update( 349 @ResourceParam T theResource, 350 @ConditionalUrlParam String theConditional) { 351 352 ValidateUtil.isTrueOrThrowInvalidRequest(isBlank(theConditional), "This server doesn't support conditional update"); 353 354 String idPartAsString = theResource.getIdElement().getIdPart(); 355 TreeMap<Long, T> versionToResource = getVersionToResource(idPartAsString); 356 357 Long versionIdPart; 358 boolean created; 359 if (versionToResource.isEmpty()) { 360 versionIdPart = 1L; 361 created = true; 362 } else { 363 versionIdPart = versionToResource.lastKey() + 1L; 364 created = false; 365 } 366 367 IIdType id = store(theResource, idPartAsString, versionIdPart); 368 369 myUpdateCount.incrementAndGet(); 370 371 return new MethodOutcome() 372 .setCreated(created) 373 .setId(id); 374 } 375 376}