/*
* Copyright (c) 2008-2014 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.EventListener;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import org.cometd.bayeux.MarkedReference;
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(Object[])} and {@link #removeAndShare(Object[])}
* to broadcast changes related to elements, as well as {@link #setAndShare(Object)} 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)}
* 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 OortObject<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);
}
/**
* 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>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>
*
* @param elements the elements to add
* @return whether at least one of the elements was added to the local entity list
*/
public boolean addAndShare(E... elements)
{
Data<Boolean> data = new Data<Boolean>(6);
data.put(Info.VERSION_FIELD, nextVersion());
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);
return data.getResult();
}
/**
* <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>
*
* @param elements the elements to remove
* @return whether at least one of the elements was removed from the local entity list
*/
public boolean removeAndShare(E... elements)
{
Data<Boolean> data = new Data<Boolean>(6);
data.put(Info.VERSION_FIELD, nextVersion());
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);
return data.getResult();
}
@Override
protected void onObject(Map<String, Object> data)
{
if (TYPE_FIELD_ELEMENT_VALUE.equals(data.get(Info.TYPE_FIELD)))
{
String action = (String)data.get(Info.ACTION_FIELD);
final boolean remove = ACTION_FIELD_REMOVE_VALUE.equals(action);
if (!ACTION_FIELD_ADD_VALUE.equals(action) && !remove)
throw new IllegalArgumentException(action);
String oortURL = (String)data.get(Info.OORT_URL_FIELD);
Info<List<E>> info = getInfo(oortURL);
if (info != null)
{
// Retrieve elements
Object object = data.get(Info.OBJECT_FIELD);
if (object instanceof Object[])
object = Arrays.asList((Object[])object);
@SuppressWarnings("unchecked")
final List<E> elements = (List<E>)object;
// Set the new Info
Info<List<E>> newInfo = new Info<>(getOort().getURL(), data);
final List<E> list = info.getObject();
newInfo.put(Info.OBJECT_FIELD, list);
final AtomicBoolean result = new AtomicBoolean();
MarkedReference<Info<List<E>>> old = setInfo(newInfo, new Runnable()
{
public void run()
{
if (remove)
result.set(list.removeAll(elements));
else
result.set(list.addAll(elements));
}
});
if (logger.isDebugEnabled())
logger.debug("{} {} list {} of {}",
old.isMarked() ? "Performed" : "Skipped",
newInfo.isLocal() ? "local" : "remote",
remove ? "remove" : "add",
elements);
if (old.isMarked())
{
if (remove)
notifyElementsRemoved(info, elements);
else
notifyElementsAdded(info, elements);
}
if (data instanceof Data)
((Data<Boolean>)data).setResult(result.get());
}
else
{
if (logger.isDebugEnabled())
logger.debug("No info for {}", oortURL);
}
}
else
{
super.onObject(data);
}
}
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>
{
public void onAdded(Info<List<E>> info, List<E> elements)
{
}
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;
}
public void onUpdated(Info<List<E>> oldInfo, Info<List<E>> newInfo)
{
List<E> added = new ArrayList<>(newInfo.getObject());
added.removeAll(oldInfo.getObject());
List<E> removed = new ArrayList<>(oldInfo.getObject());
removed.removeAll(newInfo.getObject());
if (!added.isEmpty())
oortList.notifyElementsAdded(newInfo, added);
if (!removed.isEmpty())
oortList.notifyElementsRemoved(newInfo, removed);
}
public void onRemoved(Info<List<E>> info)
{
oortList.notifyElementsRemoved(info, info.getObject());
}
}
}