All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tangosol.net.cache.ContinuousQueryCache Maven / Gradle / Ivy

There is a newer version: 24.09
Show newest version
/*
 * Copyright (c) 2000, 2023, Oracle and/or its affiliates.
 *
 * Licensed under the Universal Permissive License v 1.0 as shown at
 * https://oss.oracle.com/licenses/upl.
 */
package com.tangosol.net.cache;

import com.oracle.coherence.common.base.Continuation;

import com.tangosol.internal.net.NamedCacheDeactivationListener;

import com.tangosol.internal.util.invoke.Lambdas;

import com.tangosol.io.ClassLoaderAware;
import com.tangosol.io.Serializer;
import com.tangosol.io.SerializerFactory;

import com.tangosol.net.CacheFactory;
import com.tangosol.net.CacheService;
import com.tangosol.net.FlowControl;
import com.tangosol.net.MemberEvent;
import com.tangosol.net.MemberListener;
import com.tangosol.net.NamedCache;

import com.tangosol.util.AbstractKeySetBasedMap;
import com.tangosol.util.AbstractMapListener;
import com.tangosol.util.Binary;
import com.tangosol.util.Converter;
import com.tangosol.util.ConverterCollections;
import com.tangosol.util.ExternalizableHelper;
import com.tangosol.util.Filter;
import com.tangosol.util.FilterEnumerator;
import com.tangosol.util.ImmutableArrayList;
import com.tangosol.util.InvocableMap;
import com.tangosol.util.InvocableMapHelper;
import com.tangosol.util.ListMap;
import com.tangosol.util.LiteMap;
import com.tangosol.util.MapEvent;
import com.tangosol.util.MapListener;
import com.tangosol.util.MapListenerSupport;
import com.tangosol.util.MapListenerSupport.FilterEvent;
import com.tangosol.util.MapTriggerListener;
import com.tangosol.util.MultiplexingMapListener;
import com.tangosol.util.NullImplementation;
import com.tangosol.util.ObservableHashMap;
import com.tangosol.util.ObservableMap;
import com.tangosol.util.SafeHashMap;
import com.tangosol.util.TaskDaemon;
import com.tangosol.util.ValueExtractor;

import com.tangosol.util.processor.AsynchronousProcessor;
import com.tangosol.util.processor.ExtractorProcessor;

import com.tangosol.util.transformer.ExtractorEventTransformer;
import com.tangosol.util.transformer.SemiLiteEventTransformer;

import com.tangosol.util.filter.AlwaysFilter;
import com.tangosol.util.filter.AndFilter;
import com.tangosol.util.filter.KeyAssociatedFilter;
import com.tangosol.util.filter.LimitFilter;
import com.tangosol.util.filter.MapEventFilter;
import com.tangosol.util.filter.MapEventTransformerFilter;
import com.tangosol.util.filter.NotFilter;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import java.util.function.Supplier;

/**
 * Create a materialized view of a {@link NamedCache} using the {@code Coherence}
 * Continuous Query capability.
 * 

* In addition to providing an up-to-date view of the backing {@link NamedCache}, this * class supports server-side transformation of cache values. For example, you * could create a client-side view of a cache containing {@code Portfolio} * instances that only contains the value of each portfolio: *

{@code
 *   NamedCache cache =
 *         CacheFactory.getTypedCache("portfolio", TypeAssertion.withoutTypeChecking());
 *   NamedCache    cqc   = new ContinuousQueryCache<>(cache,
 *                                                                    AlwaysFilter.INSTANCE,
 *                                                                    Portfolio::getValue);
 * }
* * @param the type of the cache entry keys * @param the type of the entry values in the back cache that is used * as the source for this {@code ContinuousQueryCache} * @param the type of the entry values in this {@code ContinuousQueryCache}, which * will be the same as {@code V_BACK}, unless a {@code transformer} is specified * when creating this {@code ContinuousQueryCache} * * @author cp 2006.01.19 * @since Coherence 3.1 */ @SuppressWarnings("unchecked") public class ContinuousQueryCache extends AbstractKeySetBasedMap implements NamedCache { // ----- constructors --------------------------------------------------- /** * Create a locally materialized view of a {@link NamedCache} using a {@link Filter}. A * materialized view is an implementation of Continuous Query exposed * through the standard {@link NamedCache} API. *

* This constructor will result in a {@code ContinuousQueryCache} that caches both * its keys and values locally. * * @param cache the {@link NamedCache} with which the view will be created * * @since 12.2.1.4 */ public ContinuousQueryCache(NamedCache cache) { this(cache, AlwaysFilter.INSTANCE(), true, null, null); } /** * Create a locally materialized view of a {@link NamedCache} using a {@link Filter}. A * materialized view is an implementation of Continuous Query * exposed through the standard {@link NamedCache} API. *

* This constructor will result in a {@code ContinuousQueryCache} that caches both * its keys and values locally. * * @param supplierCache a {@link Supplier} that returns a {@link NamedCache} * with which the {@code ContinuousQueryCache} will be created * The Supplier must return a new instance each time * {@link Supplier#get()} is called * * @since 12.2.1.4 */ public ContinuousQueryCache(Supplier> supplierCache) { this(supplierCache, AlwaysFilter.INSTANCE(), true, null, null, null); } /** * Create a locally materialized view of a {@link NamedCache} using a Filter. A * materialized view is an implementation of Continuous Query * exposed through the standard {@link NamedCache} API. *

* This constructor will result in a {@code ContinuousQueryCache} that caches both * its keys and values locally. * * @param cache the {@link NamedCache} to create a view of * @param filter the filter that defines the view */ public ContinuousQueryCache(NamedCache cache, Filter filter) { this(cache, filter, true, null, null); } /** * Create a locally materialized view of a {@link NamedCache} using a {@link Filter}. A * materialized view is an implementation of Continuous Query * exposed through the standard {@link NamedCache} API. *

* This constructor will result in a {@code ContinuousQueryCache} that caches both * its keys and values locally. * * @param supplierCache a {@link Supplier} that returns a {@link NamedCache} * with which the {@code ContinuousQueryCache} will be created * The Supplier must return a new instance each time * {@link Supplier#get()} is called * @param filter the {@link Filter} that defines the view * * @since 12.2.1.4 */ public ContinuousQueryCache(Supplier> supplierCache, Filter filter) { this(supplierCache, filter, true, null, null, null); } /** * Create a locally materialized view of a {@link NamedCache} using a {@link Filter} and * a transformer. A materialized view is an implementation of * Continuous Query exposed through the standard {@link NamedCache} API. *

* This constructor will result in a read-only ContinuousQueryCache * that caches both its keys and transformed values locally. * * @param cache the {@link NamedCache} to create a view of * @param filter the {@link Filter} that defines the view * @param transformer the {@link ValueExtractor} that should be used to transform * values retrieved from the underlying {@link NamedCache} * before storing them locally */ public ContinuousQueryCache(NamedCache cache, Filter filter, ValueExtractor transformer) { this(cache, filter, true, null, transformer); } /** * Create a materialized view of a {@link NamedCache} using a Filter. A * materialized view is an implementation of Continuous Query * exposed through the standard {@link NamedCache} API. * * @param cache the {@link NamedCache} to create a view of * @param filter the {@link Filter} that defines the view * @param fCacheValues pass {@code true} to cache both the keys and values of the * materialized view locally, or {@code false} to only cache * the keys */ public ContinuousQueryCache(NamedCache cache, Filter filter, boolean fCacheValues) { this(cache, filter, fCacheValues, null, null); } /** * Create a materialized view of a {@link NamedCache} using a Filter. A * materialized view is an implementation of Continuous Query * exposed through the standard {@link NamedCache} API. This constructor allows * a client to receive all events, including those that result from the * initial population of the {@code ContinuousQueryCache}. In other words, all * contents of the {@code ContinuousQueryCache} will be delivered to the listener * as a sequence of events, including those items that already exist in * the underlying (unfiltered) cache. * * @param cache the {@link NamedCache} to create a view of * @param filter the {@link Filter} that defines the view * @param listener a {@link MapListener} that will receive all the events from * the {@code ContinuousQueryCache}, including those corresponding * to its initial population */ public ContinuousQueryCache(NamedCache cache, Filter filter, MapListener listener) { this(cache, filter, false, listener, null); } /** * Create a materialized view of a {@link NamedCache} using a {@link Filter}. A * materialized view is an implementation of Continuous Query * exposed through the standard {@link NamedCache} API. *

* This constructor will result in a read-only {@code ContinuousQueryCache} * that caches both its keys and transformed values locally. It will also allow * a client to receive all events, including those that result from the * initial population of the {@code ContinuousQueryCache}. In other words, all * contents of the {@code ContinuousQueryCache} will be delivered to the listener * as a sequence of events, including those items that already exist in * the underlying (unfiltered) cache. * * @param cache the {@link NamedCache} to create a view of * @param filter the {@link Filter} that defines the view * @param listener a {@link MapListener} that will receive all the events from the * {@code ContinuousQueryCache}, including those corresponding * to its initial population * @param transformer the {@link ValueExtractor} that should be used to transform * values retrieved from the underlying {@link NamedCache} * before storing them locally */ public ContinuousQueryCache(NamedCache cache, Filter filter, MapListener listener, ValueExtractor transformer) { this(cache, filter, true, listener, transformer); } /** * Construct the ContinuousQueryCache. * * @param cache the {@link NamedCache} to create a view of * @param filter the {@link Filter} that defines the view * @param fCacheValues pass true to cache both the keys and values of the * materialized view locally, or false to only cache * the keys * @param listener an optional {@link MapListener} that will receive all * events starting from the initialization of the {@code ContinuousQueryCache} * @param transformer an optional {@link ValueExtractor} that would be used to * transform values retrieved from the underlying cache * before storing them locally; if specified, this * {@code ContinuousQueryCache} will become "read-only" */ public ContinuousQueryCache(NamedCache cache, Filter filter, boolean fCacheValues, MapListener listener, ValueExtractor transformer) { this(() -> cache, filter, fCacheValues, listener, transformer, null); } /** * Create a materialized view of a {@link NamedCache} using a {@link Filter}. A * materialized view is an implementation of Continuous Query * exposed through the standard {@link NamedCache} API. *

* This constructor will result in a read-only {@code ContinuousQueryCache} * that caches both its keys and transformed values locally. It will also allow * a client to receive all events, including those that result from the * initial population of the {@code ContinuousQueryCache}. In other words, all * contents of the {@code ContinuousQueryCache} will be delivered to the listener * as a sequence of events, including those items that already exist in * the underlying (unfiltered) cache. * * @param supplierCache a {@link Supplier} that returns a {@link NamedCache} * with which the {@code ContinuousQueryCache} will be created. * The Supplier must return a new instance each * time {@link Supplier#get()} is called * @param filter the {@link Filter} that defines the view * @param fCacheValues pass {@code true} to cache both the keys and values of the * materialized view locally, or {@code false} to only cache * the keys * @param listener an optional {@link MapListener} that will receive all * events starting from the initialization of the * {@code ContinuousQueryCache} * @param transformer an optional {@link ValueExtractor} that would be used to * transform values retrieved from the underlying cache * before storing them locally; if specified, this * {@code ContinuousQueryCache} will become read-only * @param loader an optional {@link ClassLoader} * * @since 12.2.1.4 */ public ContinuousQueryCache(Supplier> supplierCache, Filter filter, boolean fCacheValues, MapListener listener, ValueExtractor transformer, ClassLoader loader) { NamedCache cache = supplierCache.get(); if (cache == null) { throw new IllegalArgumentException("NamedCache must be specified"); } if (filter == null) { throw new IllegalArgumentException("Filter must be specified"); } if (filter instanceof LimitFilter) { // FUTURE TODO: it would be nice to eventually be able to have a // cache of the "top ten" items, etc. throw new UnsupportedOperationException("LimitFilter may not be used"); } m_loader = loader; f_supplierCache = supplierCache; m_cache = ensureConverters(cache); m_filter = filter; m_fCacheValues = fCacheValues; m_transformer = Lambdas.ensureRemotable(transformer); m_fReadOnly = transformer != null; m_nState = STATE_DISCONNECTED; m_mapListener = listener; // by including information about the underlying cache, filter and // transformer, the resulting cache name is convoluted but extremely // helpful for tasks such as debugging m_sName = getDefaultName(cache.getCacheName(), filter, transformer); ensureInternalCache(); ensureSynchronized(false); } // ----- accessors ------------------------------------------------------ /** * Obtain the {@link NamedCache} that this {@code ContinuousQueryCache} is based on. * * @return the underlying {@link NamedCache} */ public NamedCache getCache() { NamedCache cache = m_cache; if (cache == null) { synchronized (this) { if (m_cache == null) { cache = f_supplierCache.get(); if (cache == null) { throw new IllegalStateException("NamedCache is not active"); } cache = m_cache = ensureConverters(cache); } } } return cache; } /** * Obtain the {@link Filter} that this {@code ContinuousQueryCache} is using to query the * underlying {@link NamedCache}. * * @return the {@link Filter} that this cache uses to select its contents * from the underlying {@link NamedCache} */ public Filter getFilter() { return m_filter; } /** * Obtain the transformer that this {@code ContinuousQueryCache} is using to transform the results from * the underlying cache prior to storing them locally. * * @return the {@link ValueExtractor} that this cache uses to transform entries from the underlying cache * * @since 12.2.1.4 */ public ValueExtractor getTransformer() { return m_transformer; } /** * Obtain the configured {@link MapListener} for this {@code ContinuousQueryCache}. * * @return the {@link MapListener} for this {@code ContinuousQueryCache} * * @since 12.2.1.4 */ public MapListener getMapListener() { return m_mapListener; } /** * Determine if this {@code ContinuousQueryCache} caches values locally. * * @return {@code true} if this object caches values locally, and {@code false} if it * relies on the underlying {@link NamedCache} */ public boolean isCacheValues() { return m_fCacheValues || isObserved(); } /** * Modify the local-caching option for the {@link ContinuousQueryCache}. By * changing this value from false to true, the * {@code ContinuousQueryCache} will fully realize its contents locally and * maintain them coherently in a manner analogous to the Coherence Near * Cache. By changing this value from true to false, * the {@code ContinuousQueryCache} will discard its locally cached data and * rely on the underlying NamedCache. *

* * @param fCacheValues pass {@code true} to enable local caching, or {@code false} * to disable it */ public synchronized void setCacheValues(boolean fCacheValues) { if (fCacheValues != m_fCacheValues) { boolean fDidCacheValues = isCacheValues(); // If we are no longer caching the values then we don't need the // local indexes. if (fDidCacheValues) { releaseIndexMap(); } m_fCacheValues = fCacheValues; if (isCacheValues() != fDidCacheValues) { configureSynchronization(false); } } } /** * Determine if this {@code ContinuousQueryCache} transforms values. * * @return {@code true} if this {@code ContinuousQueryCache} has been configured to transform * values */ public boolean isTransformed() { return m_transformer != null; } /** * Determine if this {@code ContinuousQueryCache} disallows data modification * operations. * * @return {@code true} if this {@code ContinuousQueryCache} has been configured as * read-only */ public boolean isReadOnly() { return m_fReadOnly; } /** * Modify the read-only option for the {@code ContinuousQueryCache}. Note that the * cache can be made read-only, but the opposite (making it mutable) is * explicitly disallowed. * * @param fReadOnly pass {@code true} to prohibit clients from making * modifications to this cache */ public synchronized void setReadOnly(boolean fReadOnly) { if (fReadOnly != isReadOnly()) { // once the cache is read-only, changing its read-only setting is // a mutating operation and thus is dis-allowed checkReadOnly(); m_fReadOnly = fReadOnly; } } /** * Create the internal cache used by the {@code ContinuousQueryCache}. * * @return a new {@link ObservableMap} that will represent the materialized view * of the {@code ContinuousQueryCache} */ protected ObservableMap instantiateInternalCache() { return new ObservableHashMap<>(); } /** * Create and initialize this {@code ContinuousQueryCache}'s (if not already present) internal cache. * This method is called by {@link #configureSynchronization(boolean)}, as such, it shouldn't be called * directly. Use {@link #getInternalCache()}. * * @return the {@link ObservableMap} functioning as this {@code ContinuousQueryCache}'s internal cache * * @since 12.2.1.4 */ protected ObservableMap ensureInternalCache() { if (m_mapLocal == null) { m_mapLocal = instantiateInternalCache(); MapListener mapListener = m_mapListener; if (mapListener != null) { // the initial listener has to hear the initial events ensureEventQueue(); ensureListenerSupport().addListener( instantiateEventRouter(mapListener, false), (Filter) null, false); m_fListeners = true; } } return m_mapLocal; } /** * Obtain a reference to the internal cache. The internal cache maintains * all of the keys in the {@code ContinuousQueryCache}, and if * {@link #isCacheValues()} is true, it also maintains the up-to-date * values corresponding to those keys. * * @return the internal cache that represents the materialized view of the * {@code ContinuousQueryCache} */ protected ObservableMap getInternalCache() { ensureSynchronized(true); return m_mapLocal; } /** * Determine if the {@code ContinuousQueryCache} has any listeners that cannot be * served by this Map listening to lite events. * * @return {@code true} iff there is at least one {@link MapListener listener} */ protected boolean isObserved() { return m_fListeners; } /** * Specify whether the {@code ContinuousQueryCache} has any listeners that cannot * be served by this Map listening to lite events. * * @param fObserved {@code true} iff there is at least one {@link MapListener listener} */ protected synchronized void setObserved(boolean fObserved) { if (fObserved != isObserved()) { boolean fDidCacheValues = isCacheValues(); m_fListeners = fObserved; if (isCacheValues() != fDidCacheValues) { configureSynchronization(false); } } } /** * Obtain the state of the {@code ContinuousQueryCache}. * * @return one of the {@code STATE_} enums */ public int getState() { return m_nState; } /** * Change the state of the {@code ContinuousQueryCache}. * * @param nState one of the {@code STATE_} enums */ protected void changeState(int nState) { switch (nState) { case STATE_DISCONNECTED: resetCacheRefs(); m_nState = STATE_DISCONNECTED; break; case STATE_CONFIGURING: synchronized (this) { int nStatePrev = m_nState; azzert(nStatePrev == STATE_DISCONNECTED || nStatePrev == STATE_SYNCHRONIZED); m_mapSyncReq = new SafeHashMap(); m_nState = STATE_CONFIGURING; } break; case STATE_CONFIGURED: synchronized (this) { if (m_nState == STATE_CONFIGURING) { m_nState = STATE_CONFIGURED; } else { throw new IllegalStateException(getCacheName() + " has been invalidated"); } } break; case STATE_SYNCHRONIZED: synchronized (this) { if (m_nState == STATE_CONFIGURED) { m_mapSyncReq = null; m_nState = STATE_SYNCHRONIZED; } else { throw new IllegalStateException(getCacheName() + " has been invalidated"); } } break; default: throw new IllegalArgumentException("unknown state: " + nState); } } /** * Reset cache references to null. */ protected void resetCacheRefs() { m_converterFromBinary = null; m_converterToBinary = null; m_cache = null; } /** * Return the reconnection interval (in milliseconds). This value indicates the period * in which re-synchronization with the underlying cache will be delayed in the case the * connection is severed. During this time period, local content can be accessed without * triggering re-synchronization of the local content. * * @return a reconnection interval (in milliseconds) * * @see #setReconnectInterval * @since Coherence 3.4 */ public long getReconnectInterval() { return m_cReconnectMillis; } /** * Specify the reconnection interval (in milliseconds). This value indicates the period * in which re-synchronization with the underlying cache will be delayed in the case the * connection is severed. During this time period, local content can be accessed without * triggering re-synchronization of the local content. * * @param cReconnectMillis reconnection interval (in milliseconds). A value of zero * or less means that the {@code ContinuousQueryCache} cannot * be used when not connected. * * @since Coherence 3.4 */ public void setReconnectInterval(long cReconnectMillis) { m_cReconnectMillis = cReconnectMillis; } /** * Set the cache name for this {@code ContinuousQueryCache} as returned * by {@link #getCacheName()}. *

* Note: setting the cache name to be consistent with the * {@link NamedCache#getCacheName() cache name} of * the {@link NamedCache} this CQC is backed by will * ensure data structures that cache {@code NamedCache} * instances based upon the reported cache name would * result in an appropriate cache hit. * * @param sCacheName the name this CQC should report as its * {@link NamedCache#getCacheName() cache name} * * @since 12.2.1.4 */ public void setCacheName(String sCacheName) { m_sName = sCacheName == null ? getDefaultName(getCache().getCacheName(), m_filter, m_transformer) : sCacheName; } // ----- Map interface -------------------------------------------------- @Override public void clear() { checkReadOnly(); getCache().keySet().removeAll(getInternalKeySet()); } @Override public V_FRONT get(Object oKey) { Object oResult = isCacheValues() ? getInternalCache().get(oKey) : containsKey(oKey) ? getInternal(oKey) : null; return ensureInflated(oKey, oResult); } @Override public V_FRONT put(K oKey, V_FRONT oValue) { V_FRONT oOrig; checkReadOnly(); checkEntry(oKey, oValue); // see if the putAll() optimization will work; this requires the // return value to be locally cached, or knowledge that the orig // value is null (because it is not present in the // ContinuousQueryCache) NamedCache cache = (NamedCache) getCache(); // safe (no transformer) boolean fLocalCache = isCacheValues(); boolean fPresent = containsKey(oKey); if (fLocalCache || !fPresent) { oOrig = fPresent ? getInternalCache().get(oKey) : null; cache.putAll(Collections.singletonMap(oKey, oValue)); } else { oOrig = cache.put(oKey, oValue); if (!InvocableMapHelper.evaluateEntry(getFilter(), oKey, oOrig)) { oOrig = null; } } return fromInternal(oOrig); } @Override public void putAll(Map map) { checkReadOnly(); for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) { checkEntry((Map.Entry) iter.next()); } ((NamedCache) getCache()).putAll(map); // safe (no transformer) } @Override public V_FRONT remove(Object oKey) { checkReadOnly(); V_FRONT oOrig = null; if (containsKey(oKey)) { NamedCache cache = (NamedCache) getCache(); // safe (no transformer) if (isCacheValues()) { oOrig = ensureInflated(oKey, /*oValue*/ null); removeBlind(oKey); } else { oOrig = cache.remove(oKey); } } return oOrig; } // ----- CacheMap interface --------------------------------------------- @Override public Map getAll(Collection colKeys) { Map mapResult; Map mapLocal = getInternalCache(); if (isCacheValues()) { mapResult = new ListMap(); for (Iterator iter = colKeys.iterator(); iter.hasNext(); ) { K oKey = iter.next(); V_FRONT oVal = ensureInflated(oKey, /*oValue*/ null); if (oVal != null || containsKey(oKey)) { mapResult.put(oKey, oVal); } } } else if (colKeys.size() <= 1) { // optimization: the requested set is either empty or the caller // is doing a combined "containsKey() and get()" mapResult = new ListMap(); for (Iterator iter = colKeys.iterator(); iter.hasNext(); ) { K oKey = iter.next(); if (mapLocal.containsKey(oKey)) { V_FRONT oValue = getInternal(oKey); if ((oValue != null || mapLocal.containsKey(oKey)) && InvocableMapHelper.evaluateEntry(getFilter(), oKey, oValue)) { mapResult.put(oKey, oValue); } } } } else { // since the values are not cached, delegate the processing to // the underlying NamedCache Collection collView = new HashSet<>(colKeys); collView.retainAll(mapLocal.keySet()); mapResult = getAllInternal(collView); // verify that the returned contents should all be in this // cache Filter filter = getFilter(); if (!mapResult.isEmpty() && new FilterEnumerator( mapResult.values().iterator(), new NotFilter<>(filter)).hasNext()) { Iterator> iter = mapResult.entrySet().iterator(); mapResult = new HashMap<>(); while (iter.hasNext()) { Map.Entry entry = iter.next(); if (InvocableMapHelper.evaluateEntry(filter, entry)) { mapResult.put(entry.getKey(), entry.getValue()); } } } } return mapResult; } @Override public V_FRONT put(K oKey, V_FRONT oValue, long cMillis) { if (cMillis == EXPIRY_DEFAULT) { return put(oKey, oValue); } else { checkReadOnly(); checkEntry(oKey, oValue); NamedCache cache = (NamedCache) getCache(); // safe (no transformer) V_FRONT oOrig = fromInternal(cache.put(oKey, oValue, cMillis)); return InvocableMapHelper.evaluateEntry(getFilter(), oKey, oOrig) ? oOrig : null; } } // ----- AbstractKeyBasedMap methods ------------------------------------ /** * Removes the mapping for this key from this map if present. This method * exists to allow sub-classes to optimize remove functionality for * situations in which the original value is not required. * * @param oKey key whose mapping is to be removed from the map * * @return {@code true} iff the {@code Map} changed as the result of this operation */ @Override protected boolean removeBlind(Object oKey) { checkReadOnly(); //noinspection SuspiciousMethodCalls return containsKey(oKey) && getCache().keySet().remove(oKey); } @Override protected Set getInternalKeySet() { return getInternalCache().keySet(); } // ----- ObservableMap interface ---------------------------------------- @Override public synchronized void addMapListener(MapListener listener) { addMapListener(listener, (Filter) null, false); } @Override public synchronized void removeMapListener(MapListener listener) { removeMapListener(listener, (Filter) null); } @Override public synchronized void addMapListener(MapListener listener, K oKey, boolean fLite) { azzert(listener != null); if (listener instanceof MapTriggerListener) { throw new IllegalArgumentException("ContinuousQueryCache does not support MapTriggerListeners"); } // need to cache values locally to provide standard (not lite) events if (!fLite) { setObserved(true); } ensureEventQueue(); ensureListenerSupport().addListener(instantiateEventRouter(listener, fLite), oKey, fLite); } @Override public synchronized void removeMapListener(MapListener listener, K oKey) { azzert(listener != null); MapListenerSupport listenerSupport = m_listenerSupport; if (listenerSupport != null) { listenerSupport.removeListener(instantiateEventRouter(listener, false), oKey); } } @Override public synchronized void addMapListener(MapListener listener, Filter filter, boolean fLite) { azzert(listener != null); if (listener instanceof MapTriggerListener) { throw new IllegalArgumentException("ContinuousQueryCache does not support MapTriggerListeners"); } // need to cache values locally to provide event filtering and to // provide standard (not lite) events if (filter != null || !fLite) { setObserved(true); } ensureEventQueue(); ensureListenerSupport().addListener(instantiateEventRouter(listener, fLite), filter, fLite); } @Override public synchronized void removeMapListener(MapListener listener, Filter filter) { azzert(listener != null); MapListenerSupport listenerSupport = m_listenerSupport; if (listenerSupport != null) { listenerSupport.removeListener(instantiateEventRouter(listener, false), filter); } } // ----- QueryMap interface --------------------------------------------- @Override public Set keySet(Filter filter) { return isCacheValues() ? InvocableMapHelper.query(this, getIndexMap(), filter, false, false, null) : getCache().keySet(mergeFilter(filter)); } @Override public Set> entrySet(Filter filter) { return isCacheValues() ? InvocableMapHelper.query(this, getIndexMap(), filter, true, false, null) : entrySetInternal(mergeFilter(filter)); } @Override public Set> entrySet(Filter filter, Comparator comparator) { return isCacheValues() ? InvocableMapHelper.query(this, getIndexMap(), filter, true, true, comparator) : entrySetInternal(mergeFilter(filter), comparator); } /** * If {@link #isCacheValues()} is {@code true}, the index will be created locally as well as * on the {@link NamedCache} this {@code ContinuousQueryCache} wraps, otherwise, the index will be * created on the wrapped {@link NamedCache} only. * * @throws IllegalArgumentException if {@code extractor} is an instance of * {@link com.tangosol.util.MapTriggerListener} * * @see com.tangosol.util.QueryMap#addIndex(ValueExtractor, boolean, Comparator) */ @Override public void addIndex(ValueExtractor extractor, boolean fOrdered, Comparator comparator) { // add the index locally if we are caching values synchronized (this) { if (isCacheValues()) { // Note: we pass 'this' rather than a direct reference to the internal map to intercept // MapListener registrations and deserialize the value if necessary InvocableMapHelper.addIndex(extractor, fOrdered, comparator, this, ensureIndexMap()); } } // addIndex is a no-op if many clients are trying to add the same one getCache().addIndex(extractor, fOrdered, comparator); } /** * If {@link #isCacheValues()} is {@code true}, the index will be removed locally, however, this call * will not cause the index on the {@link NamedCache} this {@code ContinuousQueryCache} wraps. * Developers must remove the index on the wrapped cache manually. * * @see com.tangosol.util.QueryMap#removeIndex(ValueExtractor) * @see #getCache() */ @Override public void removeIndex(ValueExtractor extractor) { // remove the index locally if we are caching values but do not // attempt to remove it from the underlying cache ... // removeIndex would kill all the other clients' performance if every // client balanced their add and remove index calls, so this cache // ignores the suggestion (since it cannot know if it was the cache // that originally added the index) synchronized (this) { if (isCacheValues()) { // Note: we pass 'this' rather than a direct reference to the internal map to intercept // MapListener registrations and deserialize the value if necessary InvocableMapHelper.removeIndex(extractor, this, ensureIndexMap()); } } } // ----- NamedCache interface ------------------------------------------- @Override public void truncate() { checkReadOnly(); getCache().truncate(); } // ----- InvocableMap interface ----------------------------------------- /** * {@inheritDoc} *

* In order to invoke an entry processor on a back cache in a type-safe * manner you must use {@link #getCache()}.{@link #invoke(Object, EntryProcessor) invoke()} * instead. */ @Override public R invoke(K key, EntryProcessor processor) { NamedCache cache = (NamedCache) getCache(); return (R) fromInternal(cache.invoke(key, ensureConverted(processor))); } /** * {@inheritDoc} *

* In order to invoke an entry processor on a back cache in a type-safe * manner you must use {@link #getCache()}.{@link #invokeAll(Collection, EntryProcessor) invokeAll()} * instead. */ @Override public Map invokeAll(Collection collKeys, EntryProcessor processor) { if (collKeys.isEmpty()) { return Collections.EMPTY_MAP; } NamedCache cache = (NamedCache) getCache(); return instantiateConverterMap(cache.invokeAll(collKeys, ensureConverted(processor))); } /** * {@inheritDoc} *

* In order to invoke an entry processor on a back cache in a type-safe * manner you must use {@link #getCache()}.{@link #invokeAll(Filter, EntryProcessor) invokeAll()} * instead. */ @Override public Map invokeAll(Filter filter, EntryProcessor processor) { NamedCache cache = (NamedCache) getCache(); return instantiateConverterMap(cache.invokeAll(mergeFilter(filter), ensureConverted(processor))); } @Override public R aggregate(Collection collKeys, EntryAggregator aggregator) { if (collKeys.isEmpty()) { return aggregator.aggregate(Collections.emptySet()); } if (isCacheValues()) { return aggregator.aggregate(InvocableMapHelper.makeEntrySet(this, collKeys, true)); } else if (isTransformed()) { throw new UnsupportedOperationException( "Aggregation cannot be performed on a transforming CQC that does not cache values locally"); } else { NamedCache cache = (NamedCache) getCache(); return cache.aggregate(collKeys, aggregator); } } @Override public R aggregate(Filter filter, EntryAggregator aggregator) { if (isCacheValues()) { return aggregate(keySet(filter), aggregator); } else if (isTransformed()) { throw new UnsupportedOperationException( "Aggregation cannot be performed on a transforming CQC that does not cache values locally"); } else { NamedCache cache = (NamedCache) getCache(); // safe (no transformer) return cache.aggregate(mergeFilter(filter), aggregator); } } // ----- ConcurrentMap interface ---------------------------------------- @Override public boolean lock(Object oKey, long cWait) { // locking is counted as a mutating operation checkReadOnly(); return getCache().lock(oKey, cWait); } @Override public boolean lock(Object oKey) { return lock(oKey, 0); } @Override public boolean unlock(Object oKey) { // we intentionally don't do the ReadOnly check as you must // hold the lock in order to release it return getCache().unlock(oKey); } // ----- NamedCache interface ------------------------------------------- @Override public String getCacheName() { return m_sName; } @Override public CacheService getCacheService() { return getCache().getCacheService(); } @Override public boolean isActive() { NamedCache cache = m_cache; return cache != null && cache.isActive(); } @Override public void release() { // shut down the event queue shutdownEventQueue(); synchronized (this) { releaseListeners(); resetCacheRefs(); m_mapLocal = null; m_nState = STATE_DISCONNECTED; } } @Override public void destroy() { // destroys the view but not the underlying cache release(); } @Override public boolean isDestroyed() { NamedCache cache = m_cache; return cache != null && cache.isDestroyed(); } @Override public boolean isReleased() { NamedCache cache = m_cache; return cache == null || cache.isReleased(); } // ----- internal ------------------------------------------------------- /** * Return the value from the back cache, transforming it in the process if * necessary. * * @param oKey the key to get the associated value for * * @return the value for the given key */ protected V_FRONT getInternal(Object oKey) { //noinspection SuspiciousMethodCalls V_BACK value = getCache().get(oKey); return m_transformer == null ? (V_FRONT) value : m_transformer.extract(value); } /** * Return multiple values from the back cache, transforming them in the * process if necessary. * * @param colKeys the keys to get the associated values for * * @return the value for the given key */ protected Map getAllInternal(Collection colKeys) { Map mapResults = getCache().getAll(colKeys); return m_transformer == null ? (Map) mapResults : transform(mapResults.entrySet(), m_transformer); } /** * Return multiple values from the back cache based on a filter, * transforming them in the process if necessary. * * @param filter the {@link Filter} to find the entries for * * @return the value for the given key */ protected Set> entrySetInternal(Filter filter) { Set> setResults = getCache().entrySet(filter); //noinspection RedundantCast return m_transformer == null ? (Set) setResults : transform(setResults, m_transformer).entrySet(); } /** * Return multiple values from the back cache based on a filter, * transforming them in the process if necessary. * * @param filter the {@link Filter} to find the entries for * @param comparator the {@link Comparator} * * @return the value for the given key */ protected Set> entrySetInternal(Filter filter, Comparator comparator) { Set> setResults = getCache().entrySet(filter, comparator); //noinspection RedundantCast return m_transformer == null ? (Set) setResults : transform(setResults, m_transformer).entrySet(); } /** * Transform a set of entries. * * @param setIn the set of entries to transform * @param transformer the {@link ValueExtractor transformer} to use * * @return a Map containing transformed entries */ protected Map transform(Set> setIn, ValueExtractor transformer) { Map mapOut = new LiteMap<>(); setIn.forEach(entry -> mapOut.put(entry.getKey(), transformer.extract(entry.getValue()))); return mapOut; } /** * Return a {@link Filter filter} which merges the {@code ContinuousQueueCache}'s {@link Filter filter} with the * supplied {@link Filter filter}. * * @param filter the {@link Filter filter} to merge with this cache's {@link Filter filter} * * @return the merged {@link Filter filter} */ protected Filter mergeFilter(Filter filter) { if (filter == null) { return m_filter; } Filter filterMerged; // strip off key association Filter filterCQC = getFilter(); boolean fKeyAssoc = false; Object oKeyAssoc = null; if (filterCQC instanceof KeyAssociatedFilter) { KeyAssociatedFilter filterAssoc = (KeyAssociatedFilter) filterCQC; oKeyAssoc = filterAssoc.getHostKey(); filterCQC = filterAssoc.getFilter(); fKeyAssoc = true; // if the passed filter is also key-associated, strip it off too if (filter instanceof KeyAssociatedFilter) { filter = ((KeyAssociatedFilter) filter).getFilter(); } } else if (filter instanceof KeyAssociatedFilter) { KeyAssociatedFilter filterAssoc = (KeyAssociatedFilter) filter; oKeyAssoc = filterAssoc.getHostKey(); filter = filterAssoc.getFilter(); fKeyAssoc = true; } if (filter instanceof LimitFilter) { // To merge a LimitFilter with the CQC Filter we cannot // simply And the two, we must And the CQC Filter with the // LimitFilter's internal Filter, and then apply the limit // on top of that LimitFilter filterNew; LimitFilter filterOrig = (LimitFilter) filter; int iPageSize = filterOrig.getPageSize(); Object oCookie = filterOrig.getCookie(); if (oCookie instanceof LimitFilter) { // apply the page size as it could have changed since the // wrapper was created filterNew = (LimitFilter) oCookie; filterNew.setPageSize(iPageSize); } else { // cookie either didn't exist, or was not our cookie // construct the wrapper and stick it in the cookie for // future re-use filterNew = new LimitFilter(new AndFilter(filterCQC, filterOrig.getFilter()), iPageSize); filterOrig.setCookie(filterNew); } // apply current page number; // all other properties are for use by the query processor // and only need to be maintained within the wrapper filterNew.setPage(filterOrig.getPage()); filterMerged = filterNew; } else { filterMerged = new AndFilter(filterCQC, filter); } // apply key association if (fKeyAssoc) { filterMerged = new KeyAssociatedFilter(filterMerged, oKeyAssoc); } return filterMerged; } /** * Check the read-only setting to verify that the cache is NOT read-only. * * @throws IllegalStateException if the {@code ContinuousQueryCache} is read-only */ protected void checkReadOnly() { if (isReadOnly()) { throw new IllegalStateException(getCacheName() + " is read-only"); } } /** * Check the passed value to verify that it does belong in this * ContinuousQueryCache. * * @param entry a key value pair to check. * * @throws IllegalArgumentException if the entry does not belong in this * {@code ContinuousQueryCache} (based on the cache's filter) */ protected void checkEntry(Map.Entry entry) { if (!InvocableMapHelper.evaluateEntry(getFilter(), entry)) { throw new IllegalArgumentException(getCacheName() + ": Attempted modification violates filter; key=\"" + entry.getKey() + "\", value=\"" + entry.getValue() + "\""); } } /** * Check the passed value to verify that it does belong in this * ContinuousQueryCache. * * @param oKey the key for the entry * @param oValue the value for the entry * * @throws IllegalArgumentException if the entry does not belong in this * {@code ContinuousQueryCache} (based on the cache's filter) */ protected void checkEntry(Object oKey, Object oValue) { if (!InvocableMapHelper.evaluateEntry(getFilter(), oKey, oValue)) { throw new IllegalArgumentException(getCacheName() + ": Attempted modification violates filter; key=\"" + oKey + "\", value=\"" + oValue + "\""); } } /** * Return a String description of the provided {@code STATE_*} variables. * * @param nState the state for which a description will be returned * * @return the state description * * @throws IllegalStateException if an unknown state is provided * * @since 12.2.1.4.5 */ protected String getStateString(int nState) { switch (nState) { case STATE_CONFIGURED: return "STATE_CONFIGURED"; case STATE_CONFIGURING: return "STATE_CONFIGURING"; case STATE_DISCONNECTED: return "STATE_DISCONNECTED"; case STATE_SYNCHRONIZED: return "STATE_SYNCHRONIZED"; default: throw new IllegalStateException("unknown state: " + nState); } } /** * Set up the listeners that keep the {@code ContinuousQueryCache} up-to-date. * * @param fReload pass {@code true} to force a data reload */ protected synchronized void configureSynchronization(boolean fReload) { ObservableMap mapLocal = null; try { changeState(STATE_CONFIGURING); m_ldtConnectionTimestamp = getSafeTimeMillis(); NamedCache cache = getCache(); Filter filter = getFilter(); boolean fCacheValues = isCacheValues(); // get the old filters and listeners MapEventFilter filterAddPrev = m_filterAdd; MapListener listenerAddPrev = m_listenerAdd; // determine if this is initial configuration boolean fFirstTime = filterAddPrev == null; if (fFirstTime) { // register for service restart notification registerServiceListener(); registerDeactivationListener(); // create the "remove listener" int nMask = MapEventFilter.E_UPDATED_LEFT | MapEventFilter.E_DELETED; MapEventFilter filterRemove = new MapEventFilter(nMask, filter); MapListener listenerRemove = instantiateRemoveListener(); cache.addMapListener(listenerRemove, filterRemove, true); m_filterRemove = filterRemove; m_listenerRemove = listenerRemove; } else { cache.addMapListener(m_listenerRemove, m_filterRemove, true); } // configure the "add listener" int nMask = MapEventFilter.E_INSERTED | MapEventFilter.E_UPDATED_ENTERED; if (fCacheValues) { nMask |= MapEventFilter.E_UPDATED_WITHIN; } if (fFirstTime || nMask != filterAddPrev.getEventMask()) { MapEventFilter filterAdd = new MapEventFilter(nMask, filter); MapListener listenerAdd = instantiateAddListener(); cache.addMapListener(listenerAdd, createTransformerFilter(filterAdd), !fCacheValues); m_filterAdd = filterAdd; m_listenerAdd = listenerAdd; if (listenerAddPrev != null) { azzert(filterAddPrev != null); cache.removeMapListener(listenerAddPrev, createTransformerFilter(filterAddPrev)); } } else { cache.addMapListener(listenerAddPrev, createTransformerFilter(filterAddPrev), !fCacheValues); } // update the local query image mapLocal = ensureInternalCache(); if (fFirstTime || fReload) { // populate the internal cache if (isCacheValues()) { Set set = m_transformer == null ? cache.entrySet(filter) : cache.invokeAll(filter, new ExtractorProcessor(m_transformer)).entrySet(); // first remove anything that is not in the query if (!mapLocal.isEmpty()) { HashSet setQueryKeys = new HashSet(); for (Iterator iter = set.iterator(); iter.hasNext(); ) { setQueryKeys.add(((Map.Entry) iter.next()).getKey()); } mapLocal.keySet().retainAll(setQueryKeys); } // next, populate the local cache for (Iterator iter = set.iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry) iter.next(); mapLocal.put(entry.getKey(), entry.getValue()); } } else { // first remove the keys that are not in the query Set setQueryKeys = cache.keySet(filter); if (!mapLocal.isEmpty()) { mapLocal.keySet().retainAll(setQueryKeys); } // next, populate the local cache with keys from the query for (Iterator iter = setQueryKeys.iterator(); iter.hasNext(); ) { mapLocal.put(iter.next(), null); } } } else { // not the first time; internal cache is already populated if (fCacheValues) { // used to cache only keys, now caching values too Object[] aoKey; synchronized (mapLocal) // COH-1418 { aoKey = mapLocal.keySet().toArray(); } Map mapValues = cache.getAll(new ImmutableArrayList(aoKey)); mapLocal.putAll(mapValues); } else { // used to cache values, now caching only keys for (Iterator iter = mapLocal.entrySet().iterator(); iter.hasNext(); ) { ((Map.Entry) iter.next()).setValue(null); } } } int nCurrentState = getState(); if (nCurrentState != STATE_CONFIGURING) { // This is possible if the service thread has set the state // to STATE_DISCONNECTED. In this case, throw and let the caller // handle retry logic throw createUnexpectedStateException(STATE_CONFIGURED, nCurrentState); } changeState(STATE_CONFIGURED); // resolve all changes that occurred during configuration Map mapSyncReq = m_mapSyncReq; if (!mapSyncReq.isEmpty()) { Object[] aoKey; synchronized (mapSyncReq) // COH-1418 { aoKey = mapSyncReq.keySet().toArray(); } Map mapSyncVals = cache.getAll(new ImmutableArrayList(aoKey)); synchronized (mapSyncReq) { for (Iterator iter = mapSyncReq.keySet().iterator(); iter.hasNext(); ) { Object oKey = iter.next(); Object oValue = mapSyncVals.get(oKey); boolean fExists = oValue != null || mapSyncVals.containsKey(oKey); // COH-3847 - an update event was received and deferred // while configuring the CQC, but we need to double-check // that the new value satisfies the filter if (fExists && InvocableMapHelper.evaluateEntry(filter, oKey, oValue)) { mapLocal.put(oKey, oValue); } else { mapLocal.remove(oKey); } } // notify other threads that there is nothing to resolve mapSyncReq.clear(); } } nCurrentState = getState(); if (nCurrentState != STATE_CONFIGURED) { // This is possible if the service thread has set the state // to STATE_DISCONNECTED. In this case, throw and let the caller // handle retry logic throw createUnexpectedStateException(STATE_CONFIGURED, nCurrentState); } changeState(STATE_SYNCHRONIZED); } catch (Throwable e) { if (mapLocal != null) { // exception during initial load (COH-2625) or reconciliation; // in either case we need to unregister listeners and // start from scratch releaseListeners(); } // mark as disconnected changeState(STATE_DISCONNECTED); throw ensureRuntimeException(e); } } /** * Simple helper to create an exception for communicating invalid state transitions. * * @param nExpectedState expected state * @param nActualState actual state * * @return a new {@link RuntimeException} with a description of the invalid state transition * * @since 12.2.1.4.5 */ protected RuntimeException createUnexpectedStateException(int nExpectedState, int nActualState) { String sMsg = "Unexpected synchronization state. Expected: %s, actual: %s"; return new IllegalStateException(String.format(sMsg, getStateString(nExpectedState), getStateString(nActualState))); } /** * Wrap specified {@link MapEventFilter} with a {@link MapEventTransformerFilter} that * will either transform cache value using transformer defined for this * {@code ContinuousQueryCache}, or remove the old value from the event using * {@link SemiLiteEventTransformer}, if no transformer is defined for this * {@code ContinuousQueryCache}. * * @param filterAdd add {@link MapEventFilter} to wrap * * @return {@link MapEventTransformerFilter} that wraps specified add {@link MapEventFilter} */ @SuppressWarnings("unchecked") protected Filter createTransformerFilter(MapEventFilter filterAdd) { return new MapEventTransformerFilter(filterAdd, m_transformer == null ? SemiLiteEventTransformer.INSTANCE : new ExtractorEventTransformer(null, m_transformer)); } /** * Ensure that the {@code ContinuousQueryCache} listeners have been registered * and its content synchronized with the underlying {@link NamedCache}. * * @param fReload the value to pass to the #configureSynchronization * method if the {@code ContinuousQueryCache} needs to be * configured and synchronized */ protected void ensureSynchronized(boolean fReload) { // configure and synchronize the ContinuousQueryCache, if necessary if (getState() != STATE_SYNCHRONIZED) { long cReconnectMillis = getReconnectInterval(); boolean fAllowDisconnect = cReconnectMillis > 0; if (fAllowDisconnect && getSafeTimeMillis() < m_ldtConnectionTimestamp + cReconnectMillis) { // don't try to re-connect just yet return; } Throwable eConfig = null; int cAttempts = fAllowDisconnect ? 1 : 3; for (int i = 0; i < cAttempts; ++i) { synchronized (this) { int nState = getState(); if (nState == STATE_DISCONNECTED) { try { configureSynchronization(fReload); return; } catch (Throwable e) { eConfig = e; } } else { azzert(nState == STATE_SYNCHRONIZED); return; } } } if (!fAllowDisconnect) { String sMsg = "This ContinuousQueryCache is disconnected. Retry the operation again."; if (CacheFactory.isLogEnabled(CacheFactory.LOG_MAX)) { throw new IllegalStateException(sMsg, eConfig); } else { throw new IllegalStateException(sMsg); } } } } /** * Called when an event has occurred. Allows the key to be logged as * requiring deferred synchronization if the event occurs during the * configuration or population of the {@code ContinuousQueryCache}. * * @param oKey the key that the event is related to * * @return {@code true} if the event processing has been deferred */ protected boolean isEventDeferred(Object oKey) { boolean fDeferred = false; Map mapSyncReq = m_mapSyncReq; if (mapSyncReq != null) { if (getState() <= STATE_CONFIGURING) { // handle a truncation event being received during configuration // clear any currently pending events. if (DeactivationListener.class.getName().equals(oKey)) { mapSyncReq.clear(); } else { // since the listeners are being configured and the local // cache is being populated, assume that the event is // being processed out-of-order and requires a subsequent // synchronization of the corresponding value mapSyncReq.put(oKey, null); } fDeferred = true; } else { // since an event has arrived after the configuration // completed, the event automatically resolves the sync // requirement mapSyncReq.keySet().remove(oKey); } } return fDeferred; } /** * Ensure that the map of indexes maintained by this cache exists. * * @return the map of indexes. */ protected Map ensureIndexMap() { synchronized (this) { if (m_mapIndex == null) { m_mapIndex = new SafeHashMap(); } return m_mapIndex; } } /** * Get the map of indexes maintained by this cache. * * @return the map of indexes. */ protected Map getIndexMap() { return m_mapIndex; } /** * Release the the entire index map. */ protected void releaseIndexMap() { Map mapIndex = getIndexMap(); if (mapIndex != null) { HashSet setExtractors = new HashSet(mapIndex.keySet()); for (Iterator iter = setExtractors.iterator(); iter.hasNext(); ) { removeIndex((ValueExtractor) iter.next()); } } } /** * Release the {@link MapListener listeners}. */ protected void releaseListeners() { NamedCache cache = m_cache; if (cache != null) { unregisterServiceListener(); unregisterDeactivationListener(); MapListener listenerAdd = m_listenerAdd; if (listenerAdd != null) { try { cache.removeMapListener(listenerAdd, createTransformerFilter(m_filterAdd)); } catch (Exception ignored) { } m_listenerAdd = null; } m_filterAdd = null; MapListener listenerRemove = m_listenerRemove; if (listenerRemove != null) { try { cache.removeMapListener(listenerRemove, m_filterRemove); } catch (Exception ignored) { } m_listenerRemove = null; } m_filterRemove = null; } m_listenerSupport = null; } // ----- inner class: AddListener --------------------------------------- /** * Factory Method: Instantiate a {@link MapListener} for adding items to the * {@code ContinuousQueryCache}, and (if there are listeners on the * {@code ContinuousQueryCache}) for dispatching inserts and updates. * * @return a new {@link MapListener} that will add items to and update items in * the {@code ContinuousQueryCache} */ protected MapListener instantiateAddListener() { return new AddListener(); } /** * A {@link MapListener} for adding items to the {@code ContinuousQueryCache}. */ public class AddListener extends MultiplexingMapListener implements MapListenerSupport.SynchronousListener { @Override protected void onMapEvent(MapEvent evt) { ContinuousQueryCache cqc = ContinuousQueryCache.this; K oKey = evt.getKey(); if (!cqc.isEventDeferred(oKey)) { // guard against possible NPE; one could theoretically occur // during construction or after release; one occurred during // testing of a deadlock issue (COH-1418) Map map = cqc.m_mapLocal; if (map != null) { map.put(oKey, cqc.isCacheValues() ? evt.getNewValue() : null); } } } /** * Produce a human-readable description of this object. * * @return a {@link String} describing this object */ @Override public String toString() { return "AddListener[" + ContinuousQueryCache.this.toString() + "]"; } } // ----- inner class: RemoveListener ------------------------------------ /** * Factory Method: Instantiate a {@link MapListener} for evicting items from the * {@code ContinuousQueryCache}. * * @return a new {@link MapListener} that will listen to all events that will * remove items from the {@code ContinuousQueryCache} */ protected MapListener instantiateRemoveListener() { return new RemoveListener(); } /** * A {@link MapListener} for evicting items from the {@code ContinuousQueryCache}. */ public class RemoveListener extends MultiplexingMapListener implements MapListenerSupport.SynchronousListener { @Override protected void onMapEvent(MapEvent evt) { ContinuousQueryCache cqc = ContinuousQueryCache.this; K oKey = evt.getKey(); if (!cqc.isEventDeferred(oKey)) { // guard against possible NPE; one could theoretically occur // during construction or after release; one occurred during // testing of a deadlock issue (COH-1418) Map map = cqc.m_mapLocal; if (map != null) { map.remove(oKey); } } } /** * Produce a human-readable description of this object. * * @return a {@link String} describing this object */ @Override public String toString() { return "RemoveListener[" + ContinuousQueryCache.this.toString() + "]"; } } // ----- inner class: Service Listener ---------------------------------- /** * Instantiate and register a {@link MemberListener} with the underlying cache * service. *

* The primary goal of that {@link MemberListener listener} is invalidation of the front map * in case of the service [automatic] restart. */ protected void registerServiceListener() { // automatic front map clean up (upon service restart) // requires a MemberListener implementation CacheService service = getCacheService(); if (service != null) { try { MemberListener listener = new ServiceListener(); service.addMemberListener(listener); m_listenerService = listener; } catch (UnsupportedOperationException ignored) { } } } /** * Unregister underlying cache service {@link MemberListener member listener}. */ protected void unregisterServiceListener() { try { getCacheService().removeMemberListener(m_listenerService); } catch (RuntimeException ignored) { } } /** * {@link MemberListener} for the underlying cache's service. *

* The primary goal of that listener is invalidation of the * {@code ContinuousQueryCache} in case of the corresponding CacheService * [automatic] restart. */ protected class ServiceListener implements MemberListener { @Override public void memberJoined(MemberEvent evt) { } @Override public void memberLeaving(MemberEvent evt) { } @Override public void memberLeft(MemberEvent evt) { if (evt.isLocal()) { changeState(STATE_DISCONNECTED); } } /** * Produce a human-readable description of this object. * * @return a String describing this object */ @Override public String toString() { return "ServiceListener[" + ContinuousQueryCache.this.toString() + "]"; } } /** * Instantiate and register a {@link NamedCacheDeactivationListener} with the underlying cache * service. *

* The primary goal of that {@link NamedCacheDeactivationListener listener} is invalidation of the named cache * in case the named caches is destroyed / truncated. * * @since 12.2.1.4 */ protected void registerDeactivationListener() { // automatic named cache clean up (upon cache destruction) // requires a NamedCacheDeactivationListener implementation CacheService service = getCacheService(); if (service != null) { try { NamedCacheDeactivationListener deactivationListener; deactivationListener = m_listenerDeactivation = new DeactivationListener(); m_cache.addMapListener(deactivationListener); } catch (UnsupportedOperationException ignored) { } } } /** * Unregister underlying cache service member listener. */ protected void unregisterDeactivationListener() { MapListener deactivationListener = m_listenerDeactivation; if (deactivationListener != null) { try { NamedCache cache = m_cache; if (cache != null) { cache.removeMapListener(deactivationListener); } } catch (RuntimeException ignored) { } } } // ----- inner class: InternalMapListener ------------------------------- /** * This listener allows interception of all events triggered by the the internal * {@link ObservableMap} of the {@code ContinuousQueryCache}. * * @since 12.2.1.4 */ protected class InternalMapListener extends MultiplexingMapListener { // ----- constructors ----------------------------------------------- /** * Construct the {@link MapListener} to be registered with the internal {@link ObservableMap}. * * @param listenerSupport the {@link MapListenerSupport} to dispatch events with * @param convKey the {@link Converter} for keys * @param convValue the {@link Converter} for values */ public InternalMapListener(MapListenerSupport listenerSupport, Converter convKey, Converter convValue) { f_listenerSupport = listenerSupport; f_convKey = convKey; f_convValue = convValue; } // ----- MultiplexingMapListener methods ---------------------------- /** * Dispatch events received from the internal map to the {@link MapListenerSupport}'s registered * {@link MapListener}s. * * @param evt the {@link MapEvent} carrying the insert, update or delete */ @Override protected void onMapEvent(MapEvent evt) { if (evt.getId() == MapEvent.ENTRY_UPDATED) { if (!(evt.getNewValue() instanceof Binary) && evt.getOldValue() instanceof Binary) { // suppress events caused due to lazy deserialization return; } } f_listenerSupport.fireEvent(ConverterCollections.getMapEvent( ContinuousQueryCache.this, evt, f_convKey, f_convValue), false); } // ----- data members --------------------------------------------------- /** * The {@link Converter} to be applied to keys. */ protected final Converter f_convKey; /** * The {@link Converter} to be applied to values. */ protected final Converter f_convValue; /** * The {@link MapListenerSupport} to dispatch events to. */ protected final MapListenerSupport f_listenerSupport; } // ----- inner class: EventRouter --------------------------------------- /** * Factory Method: Instantiate a listener on the internal map that will * direct events to the passed listener, either synchronously or * asynchronously as appropriate. * * @param listener the {@link MapListener listener} to route to * @param fLite {@code true} to indicate that the {@link MapEvent} objects do * not have to include the OldValue and NewValue * property values in order to allow optimizations * * @return a new {@link EventRouter} specific to the passed {@link MapListener listener} */ protected EventRouter instantiateEventRouter(MapListener listener, boolean fLite) { return new EventRouter<>(listener, fLite); } /** * An EventRouter routes events from the internal cache of the * {@code ContinuousQueryCache} to the client listeners, and it can do so * asynchronously when appropriate. * * @param the type parameter * @param the type parameter */ @SuppressWarnings("TypeParameterHidesVisibleType") protected class EventRouter extends MultiplexingMapListener { // ----- constructors ----------------------------------------------- /** * Construct an EventRouter to route events from the internal cache * of the {@code ContinuousQueryCache} to the client listeners. * * @param listener a client listener * @param fLite true to indicate that the {@link MapEvent} objects do * not have to include the OldValue and NewValue * property values in order to allow optimizations */ public EventRouter(MapListener listener, boolean fLite) { m_listener = listener; f_fLite = fLite; } // ----- MultiplexingMapListener methods ---------------------------- @Override protected void onMapEvent(MapEvent evt) { final MapListener listener = m_listener; MapEvent event = evt; if (f_fLite) { event = new MapEvent(ContinuousQueryCache.this, evt.getId(), evt.getKey(), null, null); event = evt instanceof FilterEvent ? new FilterEvent(event, ((((FilterEvent) evt).getFilter()))) : event; } final MapEvent eventRoute = event; if (listener instanceof MapListenerSupport.SynchronousListener) { try { eventRoute.dispatch(listener); } catch (RuntimeException e) { err(e); } } else { TaskDaemon eventQueue = getEventQueue(); // COH-2413 - guard against IllegalStateException after release() if (eventQueue != null) { Runnable task = () -> eventRoute.dispatch(listener); eventQueue.executeTask(task); } } } // ----- Object methods --------------------------------------------- /** * Determine a hash value for the EventRouter object according to the * general {@link Object#hashCode()} contract. * * @return an integer hash value for this EventRouter */ @Override public int hashCode() { return m_listener.hashCode(); } /** * Compare the EventRouter with another object to determine equality. * * @return {@code true} iff this {@link EventRouter} and the passed object are * equivalent listeners */ @Override public boolean equals(Object o) { return o instanceof EventRouter && this.m_listener.equals(((EventRouter) o).m_listener); } /** * Produce a human-readable description of this EventRouter. * * @return a String describing this EventRouter */ @Override public String toString() { return "EventRouter[" + m_listener + "]"; } // ----- data members ----------------------------------------------- /** * The MapListener to route to. */ protected MapListener m_listener; /** * Flag indicating {@link MapEvent} objects do not have to include the OldValue and NewValue * property values in order to allow optimizations. */ protected final boolean f_fLite; } // ----- inner class: EventQueue ---------------------------------------- /** * Create a self-processing event queue. * * @return a {@link TaskDaemon} onto which events can be placed in order to be * dispatched asynchronously */ protected TaskDaemon instantiateEventQueue() { return new TaskDaemon("EventQueue:" + getCacheName()); } /** * Obtain this {@code ContinuousQueryCache}'s event queue. * * @return the event queue that this {@code ContinuousQueryCache} uses to dispatch * its events to its non-synchronous listeners */ protected TaskDaemon getEventQueue() { return m_eventQueue; } /** * Obtain the existing event queue or create one if none exists. * * @return the event queue that this {@code ContinuousQueryCache} uses to dispatch its events to its * non-synchronous listeners */ @SuppressWarnings("UnusedReturnValue") protected synchronized TaskDaemon ensureEventQueue() { TaskDaemon queue = getEventQueue(); if (queue == null) { m_eventQueue = queue = instantiateEventQueue(); } return queue; } /** * Shut down running event queue. */ protected void shutdownEventQueue() { TaskDaemon eventQueue = getEventQueue(); if (eventQueue != null) { m_eventQueue = null; eventQueue.stop(false); } } // ----- inner class: DeactivationListener ------------------------------ /** * DeactivationListener for the underlying NamedCache. *

* The primary goal of that listener is invalidation of the named cache when * the named cache is destroyed or to truncate the local cache if the back cache has been truncated. * * @since 12.2.1.4 */ protected class DeactivationListener extends AbstractMapListener implements NamedCacheDeactivationListener { // ----- MapListener methods ---------------------------------------- @Override public void entryDeleted(MapEvent evt) { // destroy/disconnect event changeState(STATE_DISCONNECTED); } @Override public void entryUpdated(MapEvent evt) { // don't process if event should be deferred. Record // the event happening to re-trigger synchronization if (!isEventDeferred(DeactivationListener.class.getName())) { // process truncate Map local = m_mapLocal; if (local != null) { if (local instanceof ObservableHashMap) { ((ObservableHashMap) local).truncate(); } else { local.clear(); } } } } } // ----- inner class: ConverterAsynchronousProcessorWrapper ------------- /** * Wraps an {@link AsynchronousProcessor} to ensure the result of the EntryProcessor * execution is deserialized prior to passing to the provided AsynchronousProcessor. * * @since 12.2.1.4 */ protected class ConverterAsynchronousProcessor extends AsynchronousProcessor { // ----- constructors ----------------------------------------------- /** * Construct the processor to wrap the provided {@link AsynchronousProcessor} in order to * ensure results are properly converted prior to return. * * @param processor the processor to wrap */ public ConverterAsynchronousProcessor(AsynchronousProcessor processor) { super(processor); f_processor = processor; // save reference in case CQC is released before result is accessed f_convUp = ContinuousQueryCache.this.m_converterFromBinary; } // ----- AsynchronousProcessor methods ------------------------------ @Override public void onResult(final Map.Entry entry) { Converter convUp = f_convUp; Converter convDown = NullImplementation.getConverter(); f_processor.onResult(ConverterCollections.getEntry(entry, convUp, convUp, convDown)); } @Override public void onException(Throwable eReason) { f_processor.onException(eReason); } @Override public void onComplete() { f_processor.onComplete(); } @Override public int getUnitOfOrderId() { return f_processor.getUnitOfOrderId(); } @Override public EntryProcessor getProcessor() { return f_processor.getProcessor(); } @Override public Object process(InvocableMap.Entry entry) { return f_processor.process(entry); } @Override public Map processAll(Set setEntries) { return f_processor.processAll(setEntries); } // ----- AsynchronousAgent methods ---------------------------------- @Override public void bind(FlowControl control) { f_processor.bind(control); } @Override public void flush() { f_processor.flush(); } @Override public boolean checkBacklog(Continuation continueNormal) { return f_processor.checkBacklog(continueNormal); } @Override public long drainBacklog(long cMillis) { return f_processor.drainBacklog(cMillis); } @Override public boolean cancel(boolean mayInterruptIfRunning) { return f_processor.cancel(mayInterruptIfRunning); } @Override public boolean isCancelled() { return f_processor.isCancelled(); } @Override public boolean isDone() { return f_processor.isDone(); } @Override public Object get() throws InterruptedException, ExecutionException { return f_processor.get(); } @Override public Object get(long cTimeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return f_processor.get(cTimeout, unit); } @Override public Object getResult() { return f_processor.getResult(); } @Override public Throwable getException() { return f_processor.getException(); } @Override public boolean isCompletedExceptionally() { return f_processor.isCompletedExceptionally(); } @Override public CompletableFuture getCompletableFuture() { return f_processor.getCompletableFuture(); } // ----- data members ----------------------------------------------- /** * The delegate {@link AsynchronousProcessor}. */ protected final AsynchronousProcessor f_processor; /** * Converter to deserialize {@link Binary} values. */ protected final Converter f_convUp; } // ----- helper methods ------------------------------------------------- /** * Configure the local {@link MapListenerSupport} and register the intercepting * {@link MapListener} with the internal {@link ObservableMap}. * * @since 12.2.1.4 */ protected synchronized MapListenerSupport ensureListenerSupport() { MapListenerSupport listenerSupport = m_listenerSupport; if (listenerSupport == null) { listenerSupport = m_listenerSupport = new MapListenerSupport(); Converter convKey = NullImplementation.getConverter(); Converter convValue = m_converterFromBinary; m_mapLocal.addMapListener(new InternalMapListener(listenerSupport, convKey, convValue)); } return listenerSupport; } /** * If the internal cache value associated with the provided key is {@link Binary}, * deserialize the value and store it back to the internal cache in its deserialized form. * * @param oKey the key * @param oValue optional original value associated with the key. If not provided, there * will be a cost of an additional call to obtain the value currently * associated with the key * * @return the deserialized value * * @since 12.2.1.4 */ protected V_FRONT ensureInflated(Object oKey, Object oValue) { ObservableMap mapInternal = getInternalCache(); Object oInflated = oValue == null ? mapInternal.get(oKey) : oValue; if (oInflated instanceof Binary) { oInflated = fromInternal(oInflated); mapInternal.replace(oKey, oValue, oInflated); } return (V_FRONT) oInflated; } /** * Deserialize the provided {@link Binary} value. * * @param the type parameter * @param binValue the {@link Binary} value to deserialize * * @return the deserialized result * * @since 12.2.1.4 */ protected T fromInternal(Object binValue) { return (T) m_converterFromBinary.convert(binValue); } /** * Serialize the provided value into a {@link Binary}. * * @param oValue the object to serialize. * * @return the serialized result * * @since 12.2.1.4 */ protected Binary toInternal(Object oValue) { return (Binary) m_converterToBinary.convert(oValue); } /** * Provides out-bound conversion (i.e. conversion to values clients expect) of internal values. * * @param map the {@link Map} to wrap * * @return the {@link Map} that will be returned to the client * * @since 12.2.1.4 */ protected Map instantiateConverterMap(Map map) { if (isBinaryNamedCache()) { Converter convUp = m_converterFromBinary; Converter convDown = NullImplementation.getConverter(); return ConverterCollections.getMap(map, convUp, convDown, convUp, convDown); } return map; } /** * Wrap any {@link AsynchronousProcessor} instances with a custom wrapper to perform * conversion of result returned by the processor. * * @param processor the {@link EntryProcessor} * * @return the {@link EntryProcessor} to leverage when dispatching aggregation requests. * * @since 12.2.1.4 */ protected EntryProcessor ensureConverted(EntryProcessor processor) { return processor instanceof AsynchronousProcessor && isBinaryNamedCache() ? new ConverterAsynchronousProcessor((AsynchronousProcessor) processor) : processor; } /** * Returns {@code true} if provided cache is configured to use the * {@link NullImplementation#getClassLoader() NullImplementation classloader} which means * the values to/from from the cache will be {@link Binary}. * * @param cache the cache * * @return {@code true} if the cache is configured to use the * {@link NullImplementation#getClassLoader() NullImplementation classloader} * * @since 12.2.1.4 */ protected boolean isBinaryNamedCache(NamedCache cache) { ClassLoader loader = null; if (cache instanceof ClassLoaderAware) { loader = ((ClassLoaderAware) cache).getContextClassLoader(); } return loader == NullImplementation.getClassLoader(); } /** * Return {@code true} if the current back cache is configured to use {@link Binary} values. * * @return {@code true} if the back cache is configured to use {@link Binary} values * * @since 12.2.1.4 */ protected boolean isBinaryNamedCache() { azzert(m_converterFromBinary != null); return m_converterFromBinary != NullImplementation.getConverter(); } /** * Create a {@link Serializer} appropriate for the mode this cache is operating under * (i.e., binary vs non-binary). * * @return a new {@link Serializer} * * @since 12.2.1.4 */ protected Serializer instantiateSerializer() { CacheService service = getCacheService(); SerializerFactory factory = service.getDependencies().getSerializerFactory(); return factory == null ? ExternalizableHelper.ensureSerializer(m_loader) : factory.createSerializer(m_loader); } /** * Instantiate the {@link Converter converters} necessary to support the processing mode that this * {@code ContinuousQueryCache} will be operating under. * * @param cache the underlying cache * * @return the named cache * * @since 12.2.1.4 */ protected NamedCache ensureConverters(NamedCache cache) { Converter convDown = m_converterFromBinary; Converter convUp = m_converterToBinary; Converter convNull = NullImplementation.getConverter(); NamedCache cacheLocal = cache; if (convDown == null && convUp == null) { if (isBinaryNamedCache(cacheLocal)) { ClassLoader loader = m_loader; CacheService service = cacheLocal.getCacheService(); Serializer serializer = loader == null || loader == service.getContextClassLoader() ? service.getSerializer() : instantiateSerializer(); convDown = value -> ExternalizableHelper.toBinary(value, serializer); convUp = value -> value instanceof Binary ? ExternalizableHelper.fromBinary((Binary) value, serializer) : value; } else { convDown = convUp = NullImplementation.getConverter(); } // Note: value up converter is intentionally a no-op converter to avoid // deserialization until it needs to be surfaced to the client cacheLocal = ConverterCollections.getNamedCache(cacheLocal, convUp, convDown, convNull, convDown); m_converterFromBinary = convUp; m_converterToBinary = convDown; } return cacheLocal; } /** * Return the default name used by the CQC. * * @param sCacheName the cache name this CQC is backed by * @param filter the filter that reduces the set of entries of this CQC * @param transformer the {@link ValueExtractor transformer} to apply to the * raw entries * * @return the default name used by the CQC */ protected static String getDefaultName(String sCacheName, Filter filter, ValueExtractor transformer) { return String.format("ContinuousQueryCache{Cache=%s, Filter=%s, Transformer=%s}", sCacheName, filter, transformer); } // ----- constants ------------------------------------------------------ /** * State: Disconnected state. The content of the {@code ContinuousQueryCache} is not * fully synchronized with the underlying [clustered] cache. If the value of * the ReconnectInterval property is zero, it must be configured * (synchronized) before it can be used. * * @since Coherence 3.4 */ public static final int STATE_DISCONNECTED = 0; /** * State: The {@code ContinuousQueryCache} is configuring or re-configuring its * listeners and content. */ public static final int STATE_CONFIGURING = 1; /** * State: The {@code ContinuousQueryCache} has been configured. */ public static final int STATE_CONFIGURED = 2; /** * State: The {@code ContinuousQueryCache} has been configured and fully * synchronized. */ public static final int STATE_SYNCHRONIZED = 3; // ----- data members --------------------------------------------------- /** * The Supplier of the {@link NamedCache} to create a view of. * The {@link Supplier} must return a new instance every time the * {@link Supplier supplier's} {@link Supplier#get get()} method is called. * * @since 12.2.1.4 */ private Supplier> f_supplierCache; /** * The underlying {@link NamedCache} object. */ private volatile NamedCache m_cache; /** * The name of the underlying {@link NamedCache}. A copy is kept here because the * reference to the underlying {@link NamedCache} is discarded when this cache is * released. */ protected String m_sName; /** * The filter that represents the subset of information from the * underlying {@link NamedCache} that this {@code ContinuousQueryCache} represents. */ protected Filter m_filter; /** * The option of whether or not to locally cache values. */ protected boolean m_fCacheValues; /** * The transformer that should be used to convert values from the * underlying cache. */ protected ValueExtractor m_transformer; /** * The option to disallow modifications through this {@code ContinuousQueryCache} * interface. */ protected boolean m_fReadOnly; /** * The interval (in milliseconds) that indicates how often the * {@code ContinuousQueryCache} should attempt to synchronize its content with the * underlying cache in case the connection is severed. */ protected long m_cReconnectMillis; /** * The timestamp when the synchronization was last attempted. */ protected volatile long m_ldtConnectionTimestamp; /** * The keys that are in this {@code ContinuousQueryCache}, and (if * {@link #m_fCacheValues} is true) the corresponding values as well. */ protected ObservableMap m_mapLocal; /** * State of the {@code ContinuousQueryCache}. One of the {@code STATE_*} enums. */ protected volatile int m_nState; /** * While the {@code ContinuousQueryCache} is configuring or re-configuring its * listeners and content, any events that are received must be logged to * ensure that the corresponding content is in sync. */ protected volatile Map m_mapSyncReq; /** * The event queue for this {@code ContinuousQueryCache}. */ protected volatile TaskDaemon m_eventQueue; /** * Keeps track of whether the {@code ContinuousQueryCache} has listeners that * require this cache to cache values. */ protected boolean m_fListeners; /** * The {@link MapEventFilter} that uses the {@code ContinuousQueryCache's} filter to * select events that would add elements to this cache's contents. */ protected MapEventFilter m_filterAdd; /** * The {@link MapEventFilter} that uses the {@code ContinuousQueryCache's} filter to * select events that would remove elements from this cache's contents. */ protected MapEventFilter m_filterRemove; /** * The {@link MapListener listener} that gets information about what should be in this cache. */ protected MapListener m_listenerAdd; /** * The {@link MapListener listener} that gets information about what should be thrown out of * this cache. */ protected MapListener m_listenerRemove; /** * The cache service {@link MemberListener} for the underlying {@link NamedCache}. */ protected MemberListener m_listenerService; /** * The map of indexes maintained by this cache. The keys of the Map are * {@link ValueExtractor} objects, and for each key, the corresponding value * stored in the Map is a MapIndex object. */ protected Map m_mapIndex; /** * The {@link NamedCacheDeactivationListener}. * * @since 12.2.1.4 */ protected NamedCacheDeactivationListener m_listenerDeactivation; /** * The optional {@link MapListener} that may be provided during {@code ContinuousQueryCache} * construction. * * @since 12.2.1.4 */ protected MapListener m_mapListener; /** * {@link Converter} that will be used to convert values from {@link Binary binary}. * * @since 12.2.1.4 */ protected Converter m_converterFromBinary; /** * {@link Converter} that will be used to convert values to {@link Binary binary}. * * @since 12.2.1.4 */ protected Converter m_converterToBinary; /** * The {@link ClassLoader} to use when de-serializing/serializing keys and values. * * @since 12.2.1.4 */ protected ClassLoader m_loader; /** * Local {@link MapListenerSupport listener support} to allow the {@code ContinuousQueryCache} to intercept * all events dispatched by the internal {@link ObservableMap}. * * @since 12.2.1.4 */ protected MapListenerSupport m_listenerSupport; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy