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}