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}