/* * #%L * Nazgul Project: nazgul-core-cache-impl-hazelcast * %% * Copyright (C) 2010 - 2017 jGuru Europe AB * %% * Licensed under the jGuru Europe AB license (the "License"), based * on Apache License, Version 2.0; you may not use this file except * in compliance with the License. * * You may obtain a copy of the License at * * http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% * */ package se.jguru.nazgul.core.cache.impl.hazelcast; import com.hazelcast.core.DistributedObject; import com.hazelcast.core.ICollection; import com.hazelcast.core.IMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.jguru.nazgul.core.cache.api.CacheListener; import se.jguru.nazgul.core.cache.api.distributed.async.DestinationProvider; import se.jguru.nazgul.core.cache.api.transaction.AbstractTransactedAction; import se.jguru.nazgul.core.cache.impl.hazelcast.grid.AdminMessage; import se.jguru.nazgul.core.cache.impl.hazelcast.grid.GridOperations; import se.jguru.nazgul.core.clustering.api.AbstractClusterable; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; /** * Abstract identifiable handling local registration and de-registration of * HazelcastCacheListenerAdapters to cached objects. The AbstractHazelcastCacheListenerManager contains a local * (in-memory) listener Map, since Hazelcast (version 2) requires that the actual listener object is used to * de-register the listener. * * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB */ @SuppressWarnings({"rawtypes", "unchecked", "serial"}) public abstract class AbstractHazelcastCacheListenerManager extends AbstractClusterable implements GridOperations, DestinationProvider<String, Object> { // Our log private static final Logger log = LoggerFactory.getLogger(AbstractHazelcastCacheListenerManager.class); // Internal state private Map<String, AbstractHazelcastCacheListenerAdapter> locallyRegisteredListeners = new TreeMap<>(); protected final Object lock = new Object(); /** * Creates a new AbstractHazelcastCacheListenerManager and assigns the internal ID state. * * @param id The identifier of this AbstractHazelcastCacheListenerManager. */ protected AbstractHazelcastCacheListenerManager(final String id) { super(id, false); } /** * Adds a listener to events on this cache, unless another existing CacheListener * is found with the same id as the provided CacheListener. * * @param listener The listener to add. * @return true if the cacheListener was added, and false otherwise. */ @Override public final boolean addListener(final CacheListener<String, Object> listener) { return addListenerFor(getSharedMap(), listener); } /** * Removes and returns the CacheListener with the given key. * <strong>This operation is asynchronous.</strong> * * @param key The unique identifier for the given CacheListener to remove from operating on this Cache. */ @Override public final void removeListener(final String key) { removeListenerFor(getSharedMap(), key); } /** * Acquires the list of all active Listeners of this Cache instance. Note that this does not include CacheListener * instances wired to distributed objects, nor CacheListener instances wired to other members within a distributed * cache. * * @return a List holding all IDs of the active Listeners onto this (local member) Cache. Note that this does not * include CacheListener instances wired to distributed objects, nor nor CacheListener instances wired to * other members within a distributed cache. */ @Override public final List<String> getListenerIds() { final Set<String> tmp = new TreeSet<String>(); final IMap<String, TreeSet<String>> listenersIdMap = getCacheListenersIDMap(); for (final Map.Entry<String, TreeSet<String>> current : listenersIdMap.entrySet()) { for (final String currentListenerID : current.getValue()) { tmp.add(currentListenerID); } } // Return an unmodifiable copy. return Collections.unmodifiableList(new ArrayList<>(tmp)); } /** * Adds a CacheListener to the distributed object. The cacheListener will * be invoked when the properties/items/key-value pairs of the distributed * object are altered. * <strong>This operation may be an asynchronous operation depending * on the underlying cache implementation.</strong> * * @param distributedObject The distributed object from which we should * listen to changes. * @param listener The CacheListener to register to the given distributed object. * @return <code>true</code> if the CacheListener was successfully registered, * and <code>false</code> otherwise. * @throws IllegalArgumentException If the distributedObject was not appropriate for * registering a CacheListener (i.e. incorrect type * for the underlying cache implementation). */ @Override public final boolean addListenerFor(final Object distributedObject, final CacheListener<String, Object> listener) throws IllegalArgumentException { final DistributedObject distObject = cast(distributedObject); // Is the listener already registered onto the shared map? final TreeSet<String> knownListenerIDs = getCacheListenersIDMap().get("" + distObject.getName()); if (knownListenerIDs != null && knownListenerIDs.contains(listener.getClusterId())) { if (log.isWarnEnabled()) { log.warn("(CacheID: " + getClusterId() + "): CacheListener [" + listener.getClusterId() + "] was already registered to the Instance [" + distObject.getName() + "]. Aborting registration."); } // Bail out. return false; } // Do we have a mismatch between the knownListenerIDs and our locallyRegisteredListeners map? if (locallyRegisteredListeners.containsKey(listener.getClusterId())) { if (log.isWarnEnabled()) { final AbstractHazelcastCacheListenerAdapter alreadyRegistered = locallyRegisteredListeners.get(listener.getClusterId()); log.warn("Already registered listener [" + alreadyRegistered.getId() + "] holding CacheListener of type [" + alreadyRegistered.getCacheListener().getClass().getName() + "]. Aborting registration."); } // We should not re-register the listener. return false; } // Wrap the CacheListener inside a HazelcastCacheListenerAdapter. final AbstractHazelcastCacheListenerAdapter<String, Object> toAdd = new AbstractHazelcastCacheListenerAdapter(listener) { @Override protected Object convertFrom(final String distributedObjectId) { return "" + distributedObjectId; } @Override protected Object createFrom(final Object source) { return source; } }; synchronized (lock) { performTransactedAction(new AbstractTransactedAction("Could not add listener [" + listener.getClusterId() + "] of type [" + listener.getClass().getName() + "]") { /** * Defines a method invoked within a Transactional * boundary, using the Cache as context. * * @throws RuntimeException if the implementation needs to signal * a rollback to the Cache Transaction manager. */ @Override public void doInTransaction() throws RuntimeException { // Add the listener id to the CLUSTERWIDE_LISTENERID_MAP addListenerIdFor(distObject, toAdd.getId()); // Add the listener internally, to enable unregistering later on. locallyRegisteredListeners.put(toAdd.getId(), toAdd); // ... and add the listener to the instance it should listen to... if (distObject instanceof IMap) { ((IMap) distObject).addEntryListener(toAdd, true); } else if (distObject instanceof ICollection) { ((ICollection) distObject).addItemListener(toAdd, true); } else { // We can't handle this type of distObject... final Class<?>[] handleableTypes = {IMap.class, ICollection.class}; final StringBuilder permitted = new StringBuilder("["); for (final Class<?> current : handleableTypes) { permitted.append(current.getName()).append(", "); } throw new IllegalArgumentException("Will not add listener to an instance of type [" + distObject.getClass().getName() + "]. Supported types are " + permitted.substring(0, permitted.length() - 2) + "]."); } /* switch (distObject.getInstanceType()) { case MAP: ((IMap) distObject).addEntryListener(toAdd, true); break; case LIST: case SET: case QUEUE: ((ICollection) distObject).addItemListener(toAdd, true); break; default: final Instance.InstanceType[] permittedTypes = {Instance.InstanceType.LIST, Instance.InstanceType.SET, Instance.InstanceType.QUEUE, Instance.InstanceType.MAP}; final StringBuffer permitted = new StringBuffer("["); for (final Instance.InstanceType current : permittedTypes) { permitted.append(current.name()).append(", "); } throw new IllegalArgumentException("Will not add listener to an instance of type [" + distObject.getInstanceType().name() + "]. Supported types are " + permitted.substring(0, permitted.length() - 2) + "]."); } */ } }); } // All done. return true; } /** * Removes the given CacheListener from the distributed object. * <strong>This operation may be an asynchronous operation depending * on the underlying cache implementation.</strong> * * @param distributedObject The distributed object from which we should remove the CacheListener. * @param cacheListenerId The ID of the CacheListener to remove from the given distributed object. * @throws IllegalArgumentException If the distributedObject was not appropriate for * removing a CacheListener (i.e. incorrect type for the * underlying cache implementation). */ @Override public final void removeListenerFor(final Object distributedObject, final String cacheListenerId) throws IllegalArgumentException { final DistributedObject distObject = cast(distributedObject); // Is the cacheListenerId registered? final TreeSet<String> listenerIDs = getCacheListenersIDMap().get("" + distObject.getName()); if (listenerIDs == null || !listenerIDs.contains(cacheListenerId)) { throw new IllegalStateException("(CacheID: " + getClusterId() + "): Listener [" + cacheListenerId + "] not registered for instance [" + distObject.getName() + "] in Hazelcast."); } // All seems sane. // Send the message that removes the listener. sendAdminMessage(AdminMessage.createRemoveListenerMessage("" + distObject.getName(), cacheListenerId)); } /** * Retrieves all IDs of the CacheListeners bound to the given distributedObject. * * @param distributedObject The distributedObject whose CacheListener IDs we should retrieve. * @return The IDs of all CacheListener instances registered to the provided distributedObject. */ @Override public final List<String> getListenersIDsFor(final Object distributedObject) { // Cast, and acquire our listeners. final DistributedObject instance = cast(distributedObject); // Wrap and return return Collections.unmodifiableList(new ArrayList<String>(getListenerIDsFor(instance))); } // // Helpers // /** * Checks if the provided listener is locally registered within this AbstractHazelcastCacheListenerManager. * * @param listener The CacheListener to check. * @return <code>true</code> if the CacheListener is registered within this AbstractHazelcastCacheListenerManager. */ protected final boolean isLocallyRegistered(final CacheListener<String, Object> listener) { return listener != null && locallyRegisteredListeners.containsKey(listener.getClusterId()); } /** * @return The Map of locally registered listeners. */ protected Map<String, AbstractHazelcastCacheListenerAdapter> getLocallyRegisteredListeners() { return locallyRegisteredListeners; } /** * Validates that the provided distributedObject is a Hazelcast Instance. * * @param distributedObject The object to validate. * @return The distributedObject, type cast to a Hazelcast Instance. * @throws IllegalArgumentException if the distributedObject was not a Hazelcast Instance. */ @Override public DistributedObject cast(final Object distributedObject) throws IllegalArgumentException { if (!(distributedObject instanceof DistributedObject)) { throw new IllegalArgumentException("(CacheID: " + getClusterId() + "): Only DistributedObject objects can be " + "distributed in Hazelcast. Class [" + distributedObject.getClass().getName() + "] is not an instance."); } // Cast the distributedObject for type switching. return (DistributedObject) distributedObject; } /** * Retrieves all IDs of the CacheListeners bound to the given distributedObject Instance. * * @param distributedObject The distributedObject Instance whose CacheListener IDs we should retrieve. * @return The IDs of all CacheListener instances registered to the provided distributedObject. */ protected final Set<String> getListenerIDsFor(final DistributedObject distributedObject) { final TreeSet<String> listenerIDs = getCacheListenersIDMap().get("" + distributedObject.getName()); if (listenerIDs == null) { // This is not a distributed/replicated structure, // implying that it can only be returned READ-ONLY // to the calling API client. return new HashSet<String>(); } return listenerIDs; } }