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}