From 88dcf0cb324f68666778a794b76d6b60acfcdef6 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Fri, 14 Sep 2012 19:12:08 +0200 Subject: [PATCH] Improved API and performance. API changes: * The PacketListener now uses a "ListeningWhitelist" class to report which packet ids it wishes to listen in on for either the client or the server. This makes it possible to use your plugin class as the listener more easily. * Added a priority system similar to Bukkit events. Performance changes: * Create and maintain a separate list of listeners for each packet ID. This uses slightly more memory, but is far more efficient. Especially in light of the priority system. In addition, the listener lists are (hopefully) concurrent. They're optimized for read-access, however. Adding or removing a listener is a O(n) operation. --- .../comphenix/protocol/ProtocolManager.java | 14 +- .../protocol/events/ListenerPriority.java | 17 +++ .../protocol/events/ListeningWhitelist.java | 110 +++++++++++++++ .../protocol/events/PacketAdapter.java | 47 ++++--- .../protocol/events/PacketListener.java | 20 +-- .../injector/ConcurrentListenerMultimap.java | 125 +++++++----------- .../injector/PacketFilterManager.java | 73 ++++++---- .../injector/SortedCopyOnWriteArray.java | 73 ++++++++++ .../injector/SortedCopyOnWriteArrayTest.java | 76 +++++++++++ 9 files changed, 413 insertions(+), 142 deletions(-) create mode 100644 ProtocolLib/src/com/comphenix/protocol/events/ListeningWhitelist.java create mode 100644 ProtocolLib/src/com/comphenix/protocol/injector/SortedCopyOnWriteArray.java create mode 100644 ProtocolLib/test/com/comphenix/protocol/injector/SortedCopyOnWriteArrayTest.java diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java index 9f941bc7..f2735874 100644 --- a/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java @@ -119,11 +119,17 @@ public interface ProtocolManager { public PacketContainer createPacket(int id, boolean skipDefaults); /** - * Retieves a set of every enabled packet. - * @return Every packet filter. + * Retrieves a immutable set containing the ID of the sent server packets that will be observed by listeners. + * @return Every filtered server packet. */ - public Set getPacketFilters(); - + public Set getSendingFilters(); + + /** + * Retrieves a immutable set containing the ID of the recieved client packets that will be observed by listeners. + * @return Every filtered client packet. + */ + public Set getReceivingFilters(); + /** * Determines whether or not this protocol mananger has been disabled. * @return TRUE if it has, FALSE otherwise. diff --git a/ProtocolLib/src/com/comphenix/protocol/events/ListenerPriority.java b/ProtocolLib/src/com/comphenix/protocol/events/ListenerPriority.java index f3d27dfd..ba8d77c4 100644 --- a/ProtocolLib/src/com/comphenix/protocol/events/ListenerPriority.java +++ b/ProtocolLib/src/com/comphenix/protocol/events/ListenerPriority.java @@ -1,3 +1,20 @@ +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + package com.comphenix.protocol.events; /** diff --git a/ProtocolLib/src/com/comphenix/protocol/events/ListeningWhitelist.java b/ProtocolLib/src/com/comphenix/protocol/events/ListeningWhitelist.java new file mode 100644 index 00000000..ee1a3870 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/events/ListeningWhitelist.java @@ -0,0 +1,110 @@ +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +package com.comphenix.protocol.events; + +import java.util.Set; + +import com.google.common.base.Objects; +import com.google.common.collect.Sets; + +/** + * Determines which packets will be observed by a listener, and with what priority. + + * @author Kristian + */ +public class ListeningWhitelist { + + /** + * A whitelist with no packets - indicates that the listener shouldn't observe any packets. + */ + public static ListeningWhitelist EMPTY_WHITELIST = new ListeningWhitelist(ListenerPriority.LOW); + + private ListenerPriority priority; + private Set whitelist; + + /** + * Creates a packet whitelist for a given priority with a set of packet IDs. + * @param priority - the listener priority. + * @param whitelist - set of IDs to observe/enable. + */ + public ListeningWhitelist(ListenerPriority priority, Set whitelist) { + this.priority = priority; + this.whitelist = whitelist; + } + + /** + * Creates a packet whitelist of a given priority for a list of packets. + * @param priority - the listener priority. + * @param whitelist - list of packet IDs to observe/enable. + */ + public ListeningWhitelist(ListenerPriority priority, Integer... whitelist) { + this.priority = priority; + this.whitelist = Sets.newHashSet(whitelist); + } + + /** + * Whether or not this whitelist has any enabled packets. + * @return TRUE if there are any packets, FALSE otherwise. + */ + public boolean isEnabled() { + return whitelist != null || whitelist.size() > 0; + } + + /** + * Retrieve the priority in the execution order of the packet listener. Highest priority will be executed last. + * @return Execution order in terms of priority. + */ + public ListenerPriority getPriority() { + return priority; + } + + /** + * Retrieves the list of packets that will be observed by the listeners. + * @return Packet whitelist. + */ + public Set getWhitelist() { + return whitelist; + } + + @Override + public int hashCode(){ + return Objects.hashCode(priority, whitelist); + } + + @Override + public boolean equals(final Object obj){ + if(obj instanceof ListeningWhitelist){ + final ListeningWhitelist other = (ListeningWhitelist) obj; + return Objects.equal(priority, other.priority) + && Objects.equal(whitelist, other.whitelist); + } else{ + return false; + } + } + + @Override + public String toString() { + if (this == EMPTY_WHITELIST) + return "EMPTY_WHITELIST"; + else + return Objects.toStringHelper(this) + .add("priority", priority) + .add("packets", whitelist).toString(); + } + +} diff --git a/ProtocolLib/src/com/comphenix/protocol/events/PacketAdapter.java b/ProtocolLib/src/com/comphenix/protocol/events/PacketAdapter.java index d5aea595..9a1dc40d 100644 --- a/ProtocolLib/src/com/comphenix/protocol/events/PacketAdapter.java +++ b/ProtocolLib/src/com/comphenix/protocol/events/PacketAdapter.java @@ -17,12 +17,8 @@ package com.comphenix.protocol.events; -import java.util.Set; - import org.bukkit.plugin.Plugin; -import com.google.common.base.Joiner; -import com.google.common.collect.Sets; /** * Represents a packet listener with useful constructors. @@ -32,10 +28,10 @@ import com.google.common.collect.Sets; public abstract class PacketAdapter implements PacketListener { protected Plugin plugin; - protected Set packetsID; protected ConnectionSide connectionSide; - protected ListenerPriority listenerPriority; - + protected ListeningWhitelist receivingWhitelist = ListeningWhitelist.EMPTY_WHITELIST; + protected ListeningWhitelist sendingWhitelist = ListeningWhitelist.EMPTY_WHITELIST; + /** * Initialize a packet listener. * @param plugin - the plugin that spawned this listener. @@ -54,10 +50,23 @@ public abstract class PacketAdapter implements PacketListener { * @param packets - the packet IDs the listener is looking for. */ public PacketAdapter(Plugin plugin, ConnectionSide connectionSide, ListenerPriority listenerPriority, Integer... packets) { + if (plugin == null) + throw new IllegalArgumentException("plugin cannot be null"); + if (connectionSide == null) + throw new IllegalArgumentException("connectionSide cannot be null"); + if (listenerPriority == null) + throw new IllegalArgumentException("listenerPriority cannot be null"); + if (packets == null) + throw new IllegalArgumentException("packets cannot be null"); + + // Add whitelists + if (connectionSide.isForServer()) + sendingWhitelist = new ListeningWhitelist(listenerPriority, packets); + if (connectionSide.isForClient()) + receivingWhitelist = new ListeningWhitelist(listenerPriority, packets); + this.plugin = plugin; this.connectionSide = connectionSide; - this.packetsID = Sets.newHashSet(packets); - this.listenerPriority = listenerPriority; } @Override @@ -71,13 +80,13 @@ public abstract class PacketAdapter implements PacketListener { } @Override - public ConnectionSide getConnectionSide() { - return connectionSide; + public ListeningWhitelist getReceivingWhitelist() { + return receivingWhitelist; } @Override - public Set getPacketsID() { - return packetsID; + public ListeningWhitelist getSendingWhitelist() { + return sendingWhitelist; } @Override @@ -85,11 +94,6 @@ public abstract class PacketAdapter implements PacketListener { return plugin; } - @Override - public ListenerPriority getListenerPriority() { - return listenerPriority; - } - /** * Retrieves the name of the plugin that has been associated with the listener. * @return Name of the associated plugin. @@ -113,8 +117,9 @@ public abstract class PacketAdapter implements PacketListener { @Override public String toString() { // This is used by the error reporter - return String.format("PacketAdapter[plugin=%s, side=%s, packets=%s]", - getPluginName(this), getConnectionSide().name(), - Joiner.on(", ").join(packetsID)); + return String.format("PacketAdapter[plugin=%s, sending=%s, receiving=%s]", + getPluginName(this), + sendingWhitelist, + receivingWhitelist); } } diff --git a/ProtocolLib/src/com/comphenix/protocol/events/PacketListener.java b/ProtocolLib/src/com/comphenix/protocol/events/PacketListener.java index 682b3780..aab19240 100644 --- a/ProtocolLib/src/com/comphenix/protocol/events/PacketListener.java +++ b/ProtocolLib/src/com/comphenix/protocol/events/PacketListener.java @@ -17,8 +17,6 @@ package com.comphenix.protocol.events; -import java.util.Set; - import org.bukkit.plugin.Plugin; /** @@ -43,22 +41,16 @@ public interface PacketListener { public void onPacketReceiving(PacketEvent event); /** - * Retrieve whether or not we're listening for client or server packets. - * @return The type of packets we expect. + * Retrieve which packets sent by the server this listener will observe. + * @return List of server packets to observe, along with the priority. */ - public ConnectionSide getConnectionSide(); + public ListeningWhitelist getSendingWhitelist(); /** - * Retrieve the priority in the execution order of this packet listener. Highest priority will be executed last. - * @return Execution order in terms of priority. + * Retrieve which packets sent by the client this listener will observe. + * @return List of server packets to observe, along with the priority. */ - public ListenerPriority getListenerPriority(); - - /** - * Set of packet ids we expect to recieve. - * @return Packets IDs. - */ - public Set getPacketsID(); + public ListeningWhitelist getReceivingWhitelist(); /** * Retrieve the plugin that created list packet listener. diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/ConcurrentListenerMultimap.java b/ProtocolLib/src/com/comphenix/protocol/injector/ConcurrentListenerMultimap.java index 9fac1868..d12faec2 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/ConcurrentListenerMultimap.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/ConcurrentListenerMultimap.java @@ -1,7 +1,6 @@ package com.comphenix.protocol.injector; import java.util.ArrayList; -import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -9,10 +8,11 @@ import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; import java.util.logging.Logger; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; -import com.google.common.base.Objects; import com.google.common.primitives.Ints; /** @@ -23,36 +23,32 @@ import com.google.common.primitives.Ints; public class ConcurrentListenerMultimap { // The core of our map - protected ConcurrentMap> listeners = - new ConcurrentHashMap>(); + protected ConcurrentMap> listeners = + new ConcurrentHashMap>(); /** * Adds a listener to its requested list of packet recievers. * @param listener - listener with a list of packets to recieve notifcations for. */ - public void addListener(PacketListener listener) { - for (Integer packetID : listener.getPacketsID()) { - addListener(packetID, listener); + public void addListener(PacketListener listener, ListeningWhitelist whitelist) { + + PrioritizedListener prioritized = new PrioritizedListener(listener, whitelist.getPriority()); + + for (Integer packetID : whitelist.getWhitelist()) { + addListener(packetID, prioritized); } } // Add the listener to a specific packet notifcation list - private void addListener(Integer packetID, PacketListener listener) { + private void addListener(Integer packetID, PrioritizedListener listener) { - SortedArrayList list = listeners.get(packetID); + SortedCopyOnWriteArray list = listeners.get(packetID); // We don't want to create this for every lookup if (list == null) { // It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order, // which is a essential feature for our purposes. - final SortedArrayList value = new SortedArrayList(new Comparator() { - @Override - public int compare(PacketListener o1, PacketListener o2) { - // This ensures that lower priority listeners are executed first - return Ints.compare(o1.getListenerPriority().getSlot(), - o2.getListenerPriority().getSlot()); - } - }); + final SortedCopyOnWriteArray value = new SortedCopyOnWriteArray(); list = listeners.putIfAbsent(packetID, value); @@ -74,21 +70,26 @@ public class ConcurrentListenerMultimap { * @param listener - listener to remove. * @return Every packet ID that was removed due to no listeners. */ - public List removeListener(PacketListener listener) { + public List removeListener(PacketListener listener, ListeningWhitelist whitelist) { List removedPackets = new ArrayList(); // Again, not terribly efficient. But adding or removing listeners should be a rare event. - for (Integer packetID : listener.getPacketsID()) { + for (Integer packetID : whitelist.getWhitelist()) { - SortedArrayList list = listeners.get(packetID); + SortedCopyOnWriteArray list = listeners.get(packetID); // Remove any listeners if (list != null) { synchronized(list) { // Don't remove from newly created lists if (list.size() > 0) { - list.removeAll(listener); + // Remove this listener + for (Iterator it = list.iterator(); it.hasNext(); ) { + if (it.next().getListener().equals(list)) { + it.remove(); + } + } if (list.size() == 0) { listeners.remove(packetID); @@ -110,20 +111,21 @@ public class ConcurrentListenerMultimap { * @param event - the packet event to invoke. */ public void invokePacketRecieving(Logger logger, PacketEvent event) { - SortedArrayList list = listeners.get(event.getPacketID()); + SortedCopyOnWriteArray list = listeners.get(event.getPacketID()); if (list == null) return; // We have to be careful. Cannot modify the underlying list when sending notifications. synchronized (list) { - for (PacketListener listener : list) { + for (PrioritizedListener element : list) { try { - listener.onPacketReceiving(event); + element.getListener().onPacketReceiving(event); } catch (Exception e) { // Minecraft doesn't want your Exception. logger.log(Level.SEVERE, - "Exception occured in onPacketReceiving() for " + PacketAdapter.getPluginName(listener), e); + "Exception occured in onPacketReceiving() for " + + PacketAdapter.getPluginName(element.getListener()), e); } } } @@ -135,75 +137,50 @@ public class ConcurrentListenerMultimap { * @param event - the packet event to invoke. */ public void invokePacketSending(Logger logger, PacketEvent event) { - SortedArrayList list = listeners.get(event.getPacketID()); + SortedCopyOnWriteArray list = listeners.get(event.getPacketID()); if (list == null) return; synchronized (list) { - for (PacketListener listener : list) { + for (PrioritizedListener element : list) { try { - listener.onPacketSending(event); + element.getListener().onPacketSending(event); } catch (Exception e) { // Minecraft doesn't want your Exception. logger.log(Level.SEVERE, - "Exception occured in onPacketReceiving() for " + PacketAdapter.getPluginName(listener), e); + "Exception occured in onPacketReceiving() for " + + PacketAdapter.getPluginName(element.getListener()), e); } } } } /** - * An implicitly sorted array list that preserves insertion order and maintains duplicates. - * - * Note that only the {@link insertSorted} method will update the list correctly, - * @param - type of the sorted list. + * A listener with an associated priority. */ - private class SortedArrayList implements Iterable { - - private Comparator comparator; - private List list = new ArrayList(); + private class PrioritizedListener implements Comparable { + private PacketListener listener; + private ListenerPriority priority; - public SortedArrayList(Comparator comparator) { - this.comparator = comparator; + public PrioritizedListener(PacketListener listener, ListenerPriority priority) { + this.listener = listener; + this.priority = priority; } - - /** - * Inserts the given element in the proper location. - * @param value - element to insert. - */ - public void insertSorted(T value) { - list.add(value); - for (int i = list.size() - 1; i > 0 && comparator.compare(value, list.get(i-1)) < 0; i--) { - T tmp = list.get(i); - list.set(i, list.get(i-1)); - list.set(i-1, tmp); - } - } - - /** - * Removes every instance of the given element. - * @param element - element to remove. - */ - public void removeAll(T element) { - for (Iterator it = list.iterator(); it.hasNext(); ) { - if (Objects.equal(it.next(), element)) { - it.remove(); - } - } - } - - /** - * Retrieve the size of the list. - * @return Size of the list. - */ - public int size() { - return list.size(); - } @Override - public Iterator iterator() { - return list.iterator(); + public int compareTo(PrioritizedListener other) { + // This ensures that lower priority listeners are executed first + return Ints.compare(this.getPriority().getSlot(), + other.getPriority().getSlot()); + } + + public PacketListener getListener() { + return listener; + } + + public ListenerPriority getPriority() { + return priority; } } } diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java index e9099f81..f79a88d3 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.Logger; @@ -50,11 +49,12 @@ import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FuzzyReflection; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; public final class PacketFilterManager implements ProtocolManager { - private Set packetListeners = new CopyOnWriteArraySet(); + // Create a concurrent set + private Set packetListeners = + Collections.newSetFromMap(new ConcurrentHashMap()); // Player injection private Map connectionLookup = new ConcurrentHashMap(); @@ -111,37 +111,52 @@ public final class PacketFilterManager implements ProtocolManager { public void addPacketListener(PacketListener listener) { if (listener == null) throw new IllegalArgumentException("listener cannot be NULL."); - - ConnectionSide side = listener.getConnectionSide(); - packetListeners.add(listener); - // Add listeners - if (side.isForServer()) - sendingListeners.addListener(listener); - if (side.isForClient()) - recievedListeners.addListener(listener); + // A listener can only be added once + if (packetListeners.contains(listener)) + return; - // Inform our injected hooks - enablePacketFilters(side, listener.getPacketsID()); + ListeningWhitelist sending = listener.getSendingWhitelist(); + ListeningWhitelist receiving = listener.getReceivingWhitelist(); + boolean hasSending = sending != null && sending.isEnabled(); + boolean hasReceiving = receiving != null && receiving.isEnabled(); + + if (hasSending || hasReceiving) { + // Add listeners and hooks + if (hasSending) { + sendingListeners.addListener(listener, sending); + enablePacketFilters(ConnectionSide.SERVER_SIDE, sending.getWhitelist()); + } + if (hasReceiving) { + recievedListeners.addListener(listener, receiving); + enablePacketFilters(ConnectionSide.CLIENT_SIDE, receiving.getWhitelist()); + } + + // Inform our injected hooks + packetListeners.add(listener); + } } @Override public void removePacketListener(PacketListener listener) { if (listener == null) throw new IllegalArgumentException("listener cannot be NULL"); - - ConnectionSide side = listener.getConnectionSide(); + List sendingRemoved = null; List receivingRemoved = null; + ListeningWhitelist sending = listener.getSendingWhitelist(); + ListeningWhitelist receiving = listener.getReceivingWhitelist(); + // Remove from the overal list of listeners - packetListeners.remove(listener); + if (!packetListeners.remove(listener)) + return; // Add listeners - if (side.isForServer()) - sendingRemoved = sendingListeners.removeListener(listener); - if (side.isForClient()) - receivingRemoved = recievedListeners.removeListener(listener); + if (sending != null && sending.isEnabled()) + sendingRemoved = sendingListeners.removeListener(listener, sending); + if (receiving != null && receiving.isEnabled()) + receivingRemoved = recievedListeners.removeListener(listener, receiving); // Remove hooks, if needed if (sendingRemoved != null && sendingRemoved.size() > 0) @@ -154,9 +169,7 @@ public final class PacketFilterManager implements ProtocolManager { public void removePacketListeners(Plugin plugin) { // Iterate through every packet listener - for (Object element : packetListeners.toArray()) { - PacketListener listener = (PacketListener) element; - + for (PacketListener listener : packetListeners) { // Remove the listener if (Objects.equal(listener.getPlugin(), plugin)) { removePacketListener(listener); @@ -277,11 +290,13 @@ public final class PacketFilterManager implements ProtocolManager { } @Override - public Set getPacketFilters() { - if (packetInjector != null) - return Sets.union(sendingFilters, packetInjector.getPacketHandlers()); - else - return sendingFilters; + public Set getSendingFilters() { + return ImmutableSet.copyOf(sendingFilters); + } + + @Override + public Set getReceivingFilters() { + return ImmutableSet.copyOf(packetInjector.getPacketHandlers()); } /** @@ -362,7 +377,7 @@ public final class PacketFilterManager implements ProtocolManager { // Find the register event method Method registerEvent = FuzzyReflection.fromObject(manager).getMethodByParameters("registerEvent", eventTypes, Listener.class, eventPriority, Plugin.class); - + Enhancer ex = new Enhancer(); ex.setSuperclass(playerListener); ex.setClassLoader(classLoader); diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/SortedCopyOnWriteArray.java b/ProtocolLib/src/com/comphenix/protocol/injector/SortedCopyOnWriteArray.java new file mode 100644 index 00000000..e0208ddc --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/SortedCopyOnWriteArray.java @@ -0,0 +1,73 @@ +package com.comphenix.protocol.injector; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * An implicitly sorted array list that preserves insertion order and maintains duplicates. + * + * Note that only the {@link insertSorted} method will update the list correctly, + * @param - type of the sorted list. + */ +class SortedCopyOnWriteArray implements Iterable { + // Prevent reordering + private volatile List list; + + public SortedCopyOnWriteArray() { + this(new ArrayList()); + } + + public SortedCopyOnWriteArray(List wrapped) { + this.list = wrapped; + } + + /** + * Inserts the given element in the proper location. + * @param value - element to insert. + */ + @SuppressWarnings("unchecked") + public synchronized void insertSorted(T value) { + + // We use NULL as a special marker, so we don't allow it + if (value == null) + throw new IllegalArgumentException("value cannot be NULL"); + + List copy = new ArrayList(); + Comparable compare = (Comparable) value; + + for (T element : list) { + // If the value is now greater than the current element, it should be placed right before it + if (value != null && compare.compareTo(element) < 0) { + copy.add(value); + value = null; + } + copy.add(element); + } + + // Don't forget to add it + if (value != null) + copy.add(value); + + list = copy; + } + + public T get(int index) { + return list.get(index); + } + + /** + * Retrieve the size of the list. + * @return Size of the list. + */ + public int size() { + return list.size(); + } + + /** + * Retrieves an iterator over the elements in the given list. + */ + public Iterator iterator() { + return list.iterator(); + } +} diff --git a/ProtocolLib/test/com/comphenix/protocol/injector/SortedCopyOnWriteArrayTest.java b/ProtocolLib/test/com/comphenix/protocol/injector/SortedCopyOnWriteArrayTest.java new file mode 100644 index 00000000..9a9bcd9c --- /dev/null +++ b/ProtocolLib/test/com/comphenix/protocol/injector/SortedCopyOnWriteArrayTest.java @@ -0,0 +1,76 @@ +package com.comphenix.protocol.injector; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import com.comphenix.protocol.events.ListenerPriority; +import com.google.common.primitives.Ints; + +public class SortedCopyOnWriteArrayTest { + + @Test + public void testInsertion() { + + final int MAX_NUMBER = 50; + + SortedCopyOnWriteArray test = new SortedCopyOnWriteArray(); + + // Generate some numbers + List numbers = new ArrayList(); + + for (int i = 0; i < MAX_NUMBER; i++) { + numbers.add(i); + } + + // Random insertion to test it all + Collections.shuffle(numbers); + + // O(n^2) of course, so don't use too large numbers + for (int i = 0; i < MAX_NUMBER; i++) { + test.insertSorted(numbers.get(i)); + } + + // Check that everything is correct + for (int i = 0; i < MAX_NUMBER; i++) { + assertEquals((Integer) i, test.get(i)); + } + } + + @Test + public void testOrder() { + PriorityStuff a = new PriorityStuff(ListenerPriority.HIGH, 1); + PriorityStuff b = new PriorityStuff(ListenerPriority.NORMAL, 2); + PriorityStuff c = new PriorityStuff(ListenerPriority.NORMAL, 3); + SortedCopyOnWriteArray test = new SortedCopyOnWriteArray(); + + test.insertSorted(a); + test.insertSorted(b); + test.insertSorted(c); + + // Make sure the normal's are in the right order + assertEquals(2, test.get(0).id); + assertEquals(3, test.get(1).id); + } + + private class PriorityStuff implements Comparable { + public ListenerPriority priority; + public int id; + + public PriorityStuff(ListenerPriority priority, int id) { + this.priority = priority; + this.id = id; + } + + @Override + public int compareTo(PriorityStuff other) { + // This ensures that lower priority listeners are executed first + return Ints.compare(priority.getSlot(), + other.priority.getSlot()); + } + } +}