/* * Copyright (c) 2008-2017 the original author or authors. * * 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 org.cometd.oort; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EventListener; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import org.cometd.bayeux.server.BayeuxServer; /** * <p>A specialized oort object whose entity is a {@link List}.</p> * <p>{@link OortList} specializes {@code OortObject} and allows optimized replication of elements * across the cluster: instead of replicating the whole list, that may be contain a lot of elements, * only elements that are added or removed are replicated.</p> * <p>Applications can use {@link #addAndShare(Result, Object[])} and {@link #removeAndShare(Result, Object[])} * to broadcast changes related to elements, as well as {@link #setAndShare(Object, Result)} to * change the whole list.</p> * <p>When one or more elements are changed, {@link ElementListener}s are notified. * {@link DeltaListener} converts whole list updates triggered by {@link #setAndShare(Object, Result)} * into events for {@link ElementListener}s, giving applications a single listener type to implement * their business logic.</p> * * @param <E> the element type */ public class OortList<E> extends OortContainer<List<E>> { private static final String TYPE_FIELD_ELEMENT_VALUE = "oort.list.element"; private static final String ACTION_FIELD_ADD_VALUE = "oort.list.add"; private static final String ACTION_FIELD_REMOVE_VALUE = "oort.list.remove"; private final List<ElementListener<E>> listeners = new CopyOnWriteArrayList<>(); public OortList(Oort oort, String name, Factory<List<E>> factory) { super(oort, name, factory); } public void addElementListener(ElementListener<E> listener) { listeners.add(listener); } public void removeElementListener(ElementListener<E> listener) { listeners.remove(listener); } public void removeElementListeners() { listeners.clear(); } /** * Returns whether the given {@code element} is present in the local entity list of this node. * Differently from {@link #isPresent(Object)}, only the local entity list is scanned. * * @param element the element to test for presence * @return true if the {@code element} is contained in the local entity list, false otherwise */ public boolean contains(E element) { return getInfo(getOort().getURL()).getObject().contains(element); } /** * Returns whether the given {@code element} is present in one of the entity lists of all nodes. * Differently from {@link #contains(Object)} entity lists of all nodes are scanned. * * @param element the element to test for presence * @return true if the {@code element} is contained in one of the entity lists of all nodes, false otherwise */ public boolean isPresent(E element) { for (Info<List<E>> info : this) { if (info.getObject().contains(element)) { return true; } } return false; } /** * <p>Blocking version of {@link #addAndShare(Result, Object[])}, but deprecated.</p> * <p>This method will be removed in a future release.</p> * * @param elements the elements to add * @return whether at least one of the elements was added to the local entity list * @deprecated use {@link #addAndShare(Result, Object[])} instead */ @Deprecated public boolean addAndShare(E... elements) { Result.Deferred<Boolean> result = new Result.Deferred<>(); addAndShare(result, elements); return result.get(); } /** * <p>Adds the given {@code elements} to the local entity list, * and then broadcasts the addition to all nodes in the cluster.</p> * <p>Calling this method triggers notifications {@link ElementListener}s, * both on this node and on remote nodes.</p> * <p>The element is guaranteed to be added not when this method returns, * but when the {@link Result} parameter is notified.</p> * * @param callback the callback invoked with whether at least one of the elements was added to the local entity list * or {@code null} if there is no interest in knowing whether elements were added * @param elements the elements to add */ public void addAndShare(Result<Boolean> callback, E... elements) { Data<Boolean> data = new Data<>(6, callback); data.put(Info.OORT_URL_FIELD, getOort().getURL()); data.put(Info.NAME_FIELD, getName()); data.put(Info.OBJECT_FIELD, elements); data.put(Info.TYPE_FIELD, TYPE_FIELD_ELEMENT_VALUE); data.put(Info.ACTION_FIELD, ACTION_FIELD_ADD_VALUE); if (logger.isDebugEnabled()) { logger.debug("Sharing list add {}", data); } BayeuxServer bayeuxServer = getOort().getBayeuxServer(); bayeuxServer.getChannel(getChannelName()).publish(getLocalSession(), data); } /** * <p>Blocking version of {@link #removeAndShare(Result, Object[])}, but deprecated.</p> * <p>This method will be removed in a future release.</p> * * @param elements the elements to remove * @return whether at least one of the elements was removed from the local entity list * @deprecated use {@link #removeAndShare(Result, Object[])} instead */ @Deprecated public boolean removeAndShare(E... elements) { Result.Deferred<Boolean> result = new Result.Deferred<>(); removeAndShare(result, elements); return result.get(); } /** * <p>Removes the given {@code elements} to the local entity list, * and then broadcasts the removal to all nodes in the cluster.</p> * <p>Calling this method triggers notifications {@link ElementListener}s, * both on this node and on remote nodes.</p> * <p>The element is guaranteed to be removed not when this method returns, * but when the {@link Result} parameter is notified.</p> * * @param callback the callback invoked with whether at least one of the elements was removed to the local entity list * or {@code null} if there is no interest in knowing whether elements were removed * @param elements the elements to remove */ public void removeAndShare(Result<Boolean> callback, E... elements) { Data<Boolean> data = new Data<>(6, callback); data.put(Info.OORT_URL_FIELD, getOort().getURL()); data.put(Info.NAME_FIELD, getName()); data.put(Info.OBJECT_FIELD, elements); data.put(Info.TYPE_FIELD, TYPE_FIELD_ELEMENT_VALUE); data.put(Info.ACTION_FIELD, ACTION_FIELD_REMOVE_VALUE); if (logger.isDebugEnabled()) { logger.debug("Sharing list remove {}", data); } BayeuxServer bayeuxServer = getOort().getBayeuxServer(); bayeuxServer.getChannel(getChannelName()).publish(getLocalSession(), data); } @Override protected boolean isItemUpdate(Map<String, Object> data) { return TYPE_FIELD_ELEMENT_VALUE.equals(data.get(Info.TYPE_FIELD)); } @Override protected void onItem(Info<List<E>> info, Map<String, Object> data) { // Retrieve elements. Object object = data.get(Info.OBJECT_FIELD); if (object instanceof Object[]) { object = Arrays.asList((Object[])object); } @SuppressWarnings("unchecked") List<E> elements = (List<E>)object; // Perform the action. List<E> list = info.getObject(); boolean result; String action = (String)data.get(Info.ACTION_FIELD); switch (action) { case ACTION_FIELD_ADD_VALUE: result = list.addAll(elements); break; case ACTION_FIELD_REMOVE_VALUE: result = list.removeAll(elements); break; default: throw new IllegalArgumentException(action); } // Update the version. info.put(Info.VERSION_FIELD, data.get(Info.VERSION_FIELD)); // Notify. if (logger.isDebugEnabled()) { logger.debug("{} list {} of {}", info.isLocal() ? "Local" : "Remote", action, elements); } switch (action) { case ACTION_FIELD_ADD_VALUE: notifyElementsAdded(info, elements); break; case ACTION_FIELD_REMOVE_VALUE: notifyElementsRemoved(info, elements); break; } if (data instanceof Data) { ((Data<Boolean>)data).setResult(result); } } private void notifyElementsAdded(Info<List<E>> info, List<E> elements) { for (ElementListener<E> listener : listeners) { try { listener.onAdded(info, elements); } catch (Throwable x) { logger.info("Exception while invoking listener " + listener, x); } } } private void notifyElementsRemoved(Info<List<E>> info, List<E> elements) { for (ElementListener<E> listener : listeners) { try { listener.onRemoved(info, elements); } catch (Throwable x) { logger.info("Exception while invoking listener " + listener, x); } } } /** * Listener for element events that update the entity list, either locally or remotely. * * @param <E> the element type */ public interface ElementListener<E> extends EventListener { /** * Callback method invoked when elements are added to the entity list. * * @param info the {@link Info} that was changed by the addition * @param elements the elements added */ public void onAdded(Info<List<E>> info, List<E> elements); /** * Callback method invoked when elements are removed from the entity list. * * @param info the {@link Info} that was changed by the removal * @param elements the elements removed */ public void onRemoved(Info<List<E>> info, List<E> elements); /** * Empty implementation of {@link ElementListener}. * * @param <E> the element type */ public static class Adapter<E> implements ElementListener<E> { @Override public void onAdded(Info<List<E>> info, List<E> elements) { } @Override public void onRemoved(Info<List<E>> info, List<E> elements) { } } } /** * <p>An implementation of {@link Listener} that converts whole list events into {@link ElementListener} events.</p> * <p>For example, if an entity list:</p> * <pre> * [A, B] * </pre> * <p>is replaced by a list:</p> * <pre> * [A, C, D] * </pre> * <p>then this listener generates two "add" events for {@code C} and {@code D} * and one "remove" event for {@code B}.</p> * * @param <E> the element type */ public static class DeltaListener<E> implements Listener<List<E>> { private final OortList<E> oortList; public DeltaListener(OortList<E> oortList) { this.oortList = oortList; } @Override public void onUpdated(Info<List<E>> oldInfo, Info<List<E>> newInfo) { List<E> oldList = oldInfo == null ? Collections.<E>emptyList() : oldInfo.getObject(); List<E> newList = newInfo.getObject(); List<E> added = new ArrayList<>(newList); added.removeAll(oldList); List<E> removed = new ArrayList<>(oldList); removed.removeAll(newList); if (!added.isEmpty()) { oortList.notifyElementsAdded(newInfo, added); } if (!removed.isEmpty()) { oortList.notifyElementsRemoved(newInfo, removed); } } @Override public void onRemoved(Info<List<E>> info) { oortList.notifyElementsRemoved(info, info.getObject()); } } }