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