
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}