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}