001/*-
002 * #%L
003 * HAPI FHIR Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.cache;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
026import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
027import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
028import com.google.common.annotations.VisibleForTesting;
029import org.hl7.fhir.instance.model.api.IBaseResource;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032import org.springframework.stereotype.Component;
033
034import javax.annotation.Nonnull;
035import java.util.Iterator;
036import java.util.Queue;
037import java.util.Set;
038import java.util.concurrent.ConcurrentLinkedQueue;
039import java.util.stream.Collectors;
040
041/**
042 * This component holds an in-memory list of all registered {@link IResourceChangeListener} instances along
043 * with their caches and other details needed to maintain those caches.  Register an {@link IResourceChangeListener} instance
044 * with this service to be notified when resources you care about are changed.  This service quickly notifies listeners
045 * of changes that happened on the local process and also eventually notifies listeners of changes that were made by
046 * remote processes.
047 */
048@Component
049public class ResourceChangeListenerRegistryImpl implements IResourceChangeListenerRegistry {
050        private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerRegistryImpl.class);
051        private final Queue<ResourceChangeListenerCache> myListenerEntries = new ConcurrentLinkedQueue<>();
052        private final FhirContext myFhirContext;
053        private final ResourceChangeListenerCacheFactory myResourceChangeListenerCacheFactory;
054        private InMemoryResourceMatcher myInMemoryResourceMatcher;
055
056        public ResourceChangeListenerRegistryImpl(FhirContext theFhirContext, ResourceChangeListenerCacheFactory theResourceChangeListenerCacheFactory, InMemoryResourceMatcher theInMemoryResourceMatcher) {
057                myFhirContext = theFhirContext;
058                myResourceChangeListenerCacheFactory = theResourceChangeListenerCacheFactory;
059                myInMemoryResourceMatcher = theInMemoryResourceMatcher;
060        }
061
062        /**
063         * Register a listener in order to be notified whenever a resource matching the provided SearchParameterMap
064         * changes in any way.  If the change happened on the same jvm process where this registry resides, then the listener will be called
065         * within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS} of the change happening.  If the change happened
066         * on a different jvm process, then the listener will be called within theRemoteRefreshIntervalMs.
067         *
068         * @param theResourceName            the type of the resource the listener should be notified about (e.g. "Subscription" or "SearchParameter")
069         * @param theSearchParameterMap      the listener will only be notified of changes to resources that match this map
070         * @param theResourceChangeListener  the listener that will be called whenever resource changes are detected
071         * @param theRemoteRefreshIntervalMs the number of milliseconds between checking the database for changed resources that match the search parameter map
072         * @return RegisteredResourceChangeListener that stores the resource id cache, and the next refresh time
073         * @throws ca.uhn.fhir.parser.DataFormatException if theResourceName is not a valid resource type in our FhirContext
074         * @throws IllegalArgumentException               if theSearchParamMap cannot be evaluated in-memory
075         */
076        @Override
077        public IResourceChangeListenerCache registerResourceResourceChangeListener(String theResourceName, SearchParameterMap theSearchParameterMap, IResourceChangeListener theResourceChangeListener, long theRemoteRefreshIntervalMs) {
078                // Clone searchparameter map
079                RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceName);
080                InMemoryMatchResult inMemoryMatchResult = myInMemoryResourceMatcher.canBeEvaluatedInMemory(theSearchParameterMap, resourceDef);
081                if (!inMemoryMatchResult.supported()) {
082                        throw new IllegalArgumentException(Msg.code(482) + "SearchParameterMap " + theSearchParameterMap + " cannot be evaluated in-memory: " + inMemoryMatchResult.getUnsupportedReason() + ".  Only search parameter maps that can be evaluated in-memory may be registered.");
083                }
084                return add(theResourceName, theResourceChangeListener, theSearchParameterMap, theRemoteRefreshIntervalMs);
085        }
086
087        /**
088         * Unregister a listener from this service
089         *
090         * @param theResourceChangeListener
091         */
092        @Override
093        public void unregisterResourceResourceChangeListener(IResourceChangeListener theResourceChangeListener) {
094                myListenerEntries.removeIf(l -> l.getResourceChangeListener().equals(theResourceChangeListener));
095        }
096
097        @Override
098        public void unregisterResourceResourceChangeListener(IResourceChangeListenerCache theResourceChangeListenerCache) {
099                myListenerEntries.remove(theResourceChangeListenerCache);
100        }
101
102        private IResourceChangeListenerCache add(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theMap, long theRemoteRefreshIntervalMs) {
103                ResourceChangeListenerCache retval = myResourceChangeListenerCacheFactory.newResourceChangeListenerCache(theResourceName, theMap, theResourceChangeListener, theRemoteRefreshIntervalMs);
104                myListenerEntries.add(retval);
105                return retval;
106        }
107
108        @Nonnull
109        public Iterator<ResourceChangeListenerCache> iterator() {
110                return myListenerEntries.iterator();
111        }
112
113        public int size() {
114                return myListenerEntries.size();
115        }
116
117        @VisibleForTesting
118        public void clearCachesForUnitTest() {
119                myListenerEntries.forEach(ResourceChangeListenerCache::clearForUnitTest);
120        }
121
122        @Override
123        public boolean contains(IResourceChangeListenerCache theCache) {
124                return myListenerEntries.contains(theCache);
125        }
126
127        @VisibleForTesting
128        public int getResourceVersionCacheSizeForUnitTest() {
129                int retval = 0;
130                for (ResourceChangeListenerCache entry : myListenerEntries) {
131                        retval += entry.getResourceVersionCache().size();
132                }
133                return retval;
134        }
135
136        @Override
137        public void requestRefreshIfWatching(IBaseResource theResource) {
138                String resourceName = myFhirContext.getResourceType(theResource);
139                for (ResourceChangeListenerCache entry : myListenerEntries) {
140                        if (resourceName.equals(entry.getResourceName())) {
141                                entry.requestRefreshIfWatching(theResource);
142                        }
143                }
144        }
145
146        @Override
147        public Set<String> getWatchedResourceNames() {
148                return myListenerEntries.stream()
149                        .map(ResourceChangeListenerCache::getResourceName)
150                        .collect(Collectors.toSet());
151        }
152
153        @Override
154        @VisibleForTesting
155        public void clearListenersForUnitTest() {
156                myListenerEntries.clear();
157        }
158}