001/*-
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2024 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 jakarta.annotation.Nonnull;
030import org.hl7.fhir.instance.model.api.IBaseResource;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033import org.springframework.stereotype.Component;
034
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(
057                        FhirContext theFhirContext,
058                        ResourceChangeListenerCacheFactory theResourceChangeListenerCacheFactory,
059                        InMemoryResourceMatcher theInMemoryResourceMatcher) {
060                myFhirContext = theFhirContext;
061                myResourceChangeListenerCacheFactory = theResourceChangeListenerCacheFactory;
062                myInMemoryResourceMatcher = theInMemoryResourceMatcher;
063        }
064
065        /**
066         * Register a listener in order to be notified whenever a resource matching the provided SearchParameterMap
067         * changes in any way.  If the change happened on the same jvm process where this registry resides, then the listener will be called
068         * within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS} of the change happening.  If the change happened
069         * on a different jvm process, then the listener will be called within theRemoteRefreshIntervalMs.
070         *
071         * @param theResourceName            the type of the resource the listener should be notified about (e.g. "Subscription" or "SearchParameter")
072         * @param theSearchParameterMap      the listener will only be notified of changes to resources that match this map
073         * @param theResourceChangeListener  the listener that will be called whenever resource changes are detected
074         * @param theRemoteRefreshIntervalMs the number of milliseconds between checking the database for changed resources that match the search parameter map
075         * @return RegisteredResourceChangeListener that stores the resource id cache, and the next refresh time
076         * @throws ca.uhn.fhir.parser.DataFormatException if theResourceName is not a valid resource type in our FhirContext
077         * @throws IllegalArgumentException               if theSearchParamMap cannot be evaluated in-memory
078         */
079        @Override
080        public IResourceChangeListenerCache registerResourceResourceChangeListener(
081                        String theResourceName,
082                        SearchParameterMap theSearchParameterMap,
083                        IResourceChangeListener theResourceChangeListener,
084                        long theRemoteRefreshIntervalMs) {
085                // Clone searchparameter map
086                RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceName);
087                InMemoryMatchResult inMemoryMatchResult =
088                                myInMemoryResourceMatcher.canBeEvaluatedInMemory(theSearchParameterMap, resourceDef);
089                if (!inMemoryMatchResult.supported()) {
090                        throw new IllegalArgumentException(Msg.code(482) + "SearchParameterMap " + theSearchParameterMap
091                                        + " cannot be evaluated in-memory: " + inMemoryMatchResult.getUnsupportedReason()
092                                        + ".  Only search parameter maps that can be evaluated in-memory may be registered.");
093                }
094                return add(theResourceName, theResourceChangeListener, theSearchParameterMap, theRemoteRefreshIntervalMs);
095        }
096
097        /**
098         * Unregister a listener from this service
099         *
100         * @param theResourceChangeListener
101         */
102        @Override
103        public void unregisterResourceResourceChangeListener(IResourceChangeListener theResourceChangeListener) {
104                myListenerEntries.removeIf(l -> l.getResourceChangeListener().equals(theResourceChangeListener));
105        }
106
107        @Override
108        public void unregisterResourceResourceChangeListener(IResourceChangeListenerCache theResourceChangeListenerCache) {
109                myListenerEntries.remove(theResourceChangeListenerCache);
110        }
111
112        private IResourceChangeListenerCache add(
113                        String theResourceName,
114                        IResourceChangeListener theResourceChangeListener,
115                        SearchParameterMap theMap,
116                        long theRemoteRefreshIntervalMs) {
117                ResourceChangeListenerCache retval = myResourceChangeListenerCacheFactory.newResourceChangeListenerCache(
118                                theResourceName, theMap, theResourceChangeListener, theRemoteRefreshIntervalMs);
119                myListenerEntries.add(retval);
120                return retval;
121        }
122
123        @Nonnull
124        public Iterator<ResourceChangeListenerCache> iterator() {
125                return myListenerEntries.iterator();
126        }
127
128        public int size() {
129                return myListenerEntries.size();
130        }
131
132        @VisibleForTesting
133        public void clearCachesForUnitTest() {
134                myListenerEntries.forEach(ResourceChangeListenerCache::clearForUnitTest);
135        }
136
137        @Override
138        public boolean contains(IResourceChangeListenerCache theCache) {
139                return myListenerEntries.contains(theCache);
140        }
141
142        @VisibleForTesting
143        public int getResourceVersionCacheSizeForUnitTest() {
144                int retval = 0;
145                for (ResourceChangeListenerCache entry : myListenerEntries) {
146                        retval += entry.getResourceVersionCache().size();
147                }
148                return retval;
149        }
150
151        @Override
152        public void requestRefreshIfWatching(IBaseResource theResource) {
153                String resourceName = myFhirContext.getResourceType(theResource);
154                for (ResourceChangeListenerCache entry : myListenerEntries) {
155                        if (resourceName.equals(entry.getResourceName())) {
156                                entry.requestRefreshIfWatching(theResource);
157                        }
158                }
159        }
160
161        @Override
162        public Set<String> getWatchedResourceNames() {
163                return myListenerEntries.stream()
164                                .map(ResourceChangeListenerCache::getResourceName)
165                                .collect(Collectors.toSet());
166        }
167
168        @Override
169        @VisibleForTesting
170        public void clearListenersForUnitTest() {
171                myListenerEntries.clear();
172        }
173}