/* * Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * 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. */ package com.hazelcast.map.impl.event; import com.hazelcast.core.EntryEventType; import com.hazelcast.map.impl.EntryEventFilter; import com.hazelcast.map.impl.EventListenerFilter; import com.hazelcast.map.impl.MapPartitionLostEventFilter; import com.hazelcast.map.impl.MapServiceContext; import com.hazelcast.map.impl.nearcache.invalidation.UuidFilter; import com.hazelcast.map.impl.query.QueryEventFilter; import com.hazelcast.nio.Address; import com.hazelcast.nio.serialization.Data; import com.hazelcast.spi.EventFilter; import com.hazelcast.spi.impl.eventservice.impl.TrueEventFilter; import com.hazelcast.spi.serialization.SerializationService; import com.hazelcast.util.collection.Int2ObjectHashMap; import java.util.Collection; import java.util.Map; import static com.hazelcast.core.EntryEventType.ADDED; import static com.hazelcast.core.EntryEventType.EVICTED; import static com.hazelcast.core.EntryEventType.EXPIRED; import static com.hazelcast.core.EntryEventType.INVALIDATION; import static com.hazelcast.core.EntryEventType.REMOVED; import static com.hazelcast.core.EntryEventType.UPDATED; /** * A filtering strategy that preserves the default behavior in most cases, but processes entry events for listeners with * predicates to fit with "query cache" concept. For example when the original event indicates an update from an old value that * does not match the predicate to a new value that does match, then the entry listener will be notified with an ADDED event. * This affects only map listeners with predicates and the way entry updates are handled. Put/remove operations are not * affected, neither are listeners without predicates. * This filtering strategy is used when Hazelcast property {@code hazelcast.map.entry.filtering.natural.event.types} is set * to {@code true}. The complete decision matrix for event types published with this filtering strategy. * <p> * <table> * <tr> * <td>Old value</td> * <td>New value</td> * <td>Published event type</td> * </tr> * <tr> * <td>Match</td> * <td>Mismatch</td> * <td>REMOVED</td> * </tr> * <tr> * <td>Match</td> * <td>Match</td> * <td>UPDATED</td> * </tr> * <tr> * <td>Mismatch</td> * <td>Mismatch</td> * <td>NO MATCH (no event is triggered)</td> * </tr> * <tr> * <td>Mismatch</td> * <td>Match</td> * <td>ADDED</td> * </tr> * </table> */ public class QueryCacheNaturalFilteringStrategy extends AbstractFilteringStrategy { // Default capacity of event-type > EventData map. private static final int EVENT_DATA_MAP_CAPACITY = 4; public QueryCacheNaturalFilteringStrategy(SerializationService serializationService, MapServiceContext mapServiceContext) { super(serializationService, mapServiceContext); } @SuppressWarnings({"checkstyle:npathcomplexity", "checkstyle:cyclomaticcomplexity"}) @Override public int doFilter(EventFilter filter, Data dataKey, Object dataOldValue, Object dataValue, EntryEventType eventType, String mapNameOrNull) { if (filter instanceof MapPartitionLostEventFilter) { return FILTER_DOES_NOT_MATCH; } // Since the event type may change if we have a QueryEventFilter to evaluate, // the effective event type may change after execution of QueryEventFilter.eval EventListenerFilter filterAsEventListenerFilter = null; boolean originalFilterEventTypeMatches = true; if (filter instanceof EventListenerFilter) { int type = eventType.getType(); if (type == INVALIDATION.getType()) { return FILTER_DOES_NOT_MATCH; } // evaluate whether the filter matches the original event type originalFilterEventTypeMatches = filter.eval(type); // hold a reference to the original event filter; this may be used later, in case there is a query event filter // and it alters the event type to be published filterAsEventListenerFilter = ((EventListenerFilter) filter); filter = ((EventListenerFilter) filter).getEventFilter(); if (filter instanceof UuidFilter) { return FILTER_DOES_NOT_MATCH; } } if (originalFilterEventTypeMatches && filter instanceof TrueEventFilter) { return eventType.getType(); } if (filter instanceof QueryEventFilter) { int effectiveEventType = processQueryEventFilterWithAlternativeEventType(filter, eventType, dataKey, dataOldValue, dataValue, mapNameOrNull); if (effectiveEventType == FILTER_DOES_NOT_MATCH) { return FILTER_DOES_NOT_MATCH; } else { // query matches, also need to verify that the effective event type is one that the EventListenerFilter // wants to listen for (if effective event type != original event type if (filterAsEventListenerFilter != null && effectiveEventType != eventType.getType()) { return filterAsEventListenerFilter.eval(effectiveEventType) ? effectiveEventType : FILTER_DOES_NOT_MATCH; } else { return effectiveEventType; } } } if (filter instanceof EntryEventFilter) { return (originalFilterEventTypeMatches && processEntryEventFilter(filter, dataKey)) ? eventType.getType() : FILTER_DOES_NOT_MATCH; } throw new IllegalArgumentException("Unknown EventFilter type = [" + filter.getClass().getCanonicalName() + "]"); } @Override public EntryEventDataCache getEntryEventDataCache() { return new EntryEventDataPerEventTypeCache(); } @Override public String toString() { return "QueryCacheNaturalFilteringStrategy"; } private int processQueryEventFilterWithAlternativeEventType(EventFilter filter, EntryEventType eventType, Data dataKey, Object dataOldValue, Object dataValue, String mapNameOrNull) { if (eventType == UPDATED) { // need to evaluate the filter on both old and new value and morph accordingly the event type boolean newValueMatches = evaluateQueryEventFilter(filter, dataKey, dataValue, mapNameOrNull); boolean oldValueMatches = evaluateQueryEventFilter(filter, dataKey, dataOldValue, mapNameOrNull); // decision matrix: // +-----------+-----------+---------+ // | Old value | New value | Result | // +-----------+-----------+---------+ // | Match | Mismatch | REMOVED | // | Match | Match | UPDATED | // | Mismatch | Mismatch | NO MATCH| // | Mismatch | Match | ADDED | // +-----------+-----------+---------+ if (oldValueMatches) { return newValueMatches ? UPDATED.getType() : REMOVED.getType(); } else { return newValueMatches ? ADDED.getType() : FILTER_DOES_NOT_MATCH; } } else { Object testValue; if (eventType == REMOVED || eventType == EVICTED || eventType == EXPIRED) { testValue = dataOldValue; } else { testValue = dataValue; } return evaluateQueryEventFilter(filter, dataKey, testValue, mapNameOrNull) ? eventType.getType() : FILTER_DOES_NOT_MATCH; } } /** * Caches 2 different {@link EntryEventData} objects for each event type - one for * including values and one for excluding values. */ private class EntryEventDataPerEventTypeCache implements EntryEventDataCache { Map<Integer, EntryEventData> eventDataIncludingValues; Map<Integer, EntryEventData> eventDataExcludingValues; boolean empty = true; @Override public EntryEventData getOrCreateEventData(String mapName, Address caller, Data dataKey, Object newValue, Object oldValue, Object mergingValue, int eventType, boolean includingValues) { if (includingValues) { if (eventDataIncludingValues == null) { eventDataIncludingValues = new Int2ObjectHashMap<EntryEventData>(EVENT_DATA_MAP_CAPACITY); } return getOrCreateEventData(eventDataIncludingValues, mapName, caller, dataKey, newValue, oldValue, mergingValue, eventType); } else { if (eventDataExcludingValues == null) { eventDataExcludingValues = new Int2ObjectHashMap<EntryEventData>(EVENT_DATA_MAP_CAPACITY); } return getOrCreateEventData(eventDataExcludingValues, mapName, caller, dataKey, null, null, null, eventType); } } @Override public boolean isEmpty() { return empty; } @Override public Collection<EntryEventData> eventDataIncludingValues() { return eventDataIncludingValues == null ? null : eventDataIncludingValues.values(); } @Override public Collection<EntryEventData> eventDataExcludingValues() { return eventDataExcludingValues == null ? null : eventDataExcludingValues.values(); } /** * If an {@code EntryEventData} is already mapped to the given {@code eventType} in {@code Map eventDataPerEventType}, * then return the mapped value, otherwise create the {@code EntryEventData}, put it in {@code Map eventDataPerEventType} * and return it. * * @param eventDataPerEventType map from event type to cached event data * @param mapName name of map * @param caller the address of the caller that caused the event * @param dataKey the key of the event map entry * @param newValue the new value of the map entry * @param oldValue the old value of the map entry * @param mergingValue the value used when performing a merge operation in case of * a {@link EntryEventType#MERGED} event. This value together with the old value * produced the new value. * @param eventType the event type * @return {@code EntryEventData} already cached in {@code Map eventDataPerEventType} for the given {@code eventType} or * if not already cached, a new {@code EntryEventData} object. */ private EntryEventData getOrCreateEventData(Map<Integer, EntryEventData> eventDataPerEventType, String mapName, Address caller, Data dataKey, Object newValue, Object oldValue, Object mergingValue, int eventType) { if (eventDataPerEventType.containsKey(eventType)) { return eventDataPerEventType.get(eventType); } else { Data dataOldValue = oldValue == null ? null : mapServiceContext.toData(oldValue); Data dataNewValue = newValue == null ? null : mapServiceContext.toData(newValue); Data dataMergingValue = mergingValue == null ? null : mapServiceContext.toData(mergingValue); EntryEventData entryEventData = new EntryEventData(getThisNodesAddress(), mapName, caller, dataKey, dataNewValue, dataOldValue, dataMergingValue, eventType); eventDataPerEventType.put(eventType, entryEventData); if (empty) { empty = false; } return entryEventData; } } } }