geforkt von Mirrors/Velocity
Add initial adventure boss bar support
Dieser Commit ist enthalten in:
Ursprung
5285a3c155
Commit
1b8955a76b
@ -41,6 +41,7 @@ import com.velocitypowered.proxy.server.ServerMap;
|
|||||||
import com.velocitypowered.proxy.util.AddressUtil;
|
import com.velocitypowered.proxy.util.AddressUtil;
|
||||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||||
import com.velocitypowered.proxy.util.VelocityChannelRegistrar;
|
import com.velocitypowered.proxy.util.VelocityChannelRegistrar;
|
||||||
|
import com.velocitypowered.proxy.util.bossbar.BossBarManager;
|
||||||
import com.velocitypowered.proxy.util.bossbar.VelocityBossBar;
|
import com.velocitypowered.proxy.util.bossbar.VelocityBossBar;
|
||||||
import com.velocitypowered.proxy.util.ratelimit.Ratelimiter;
|
import com.velocitypowered.proxy.util.ratelimit.Ratelimiter;
|
||||||
import com.velocitypowered.proxy.util.ratelimit.Ratelimiters;
|
import com.velocitypowered.proxy.util.ratelimit.Ratelimiters;
|
||||||
@ -111,6 +112,7 @@ public class VelocityServer implements ProxyServer {
|
|||||||
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
|
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
|
||||||
private boolean shutdown = false;
|
private boolean shutdown = false;
|
||||||
private final VelocityPluginManager pluginManager;
|
private final VelocityPluginManager pluginManager;
|
||||||
|
private final BossBarManager bossBarManager;
|
||||||
|
|
||||||
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
||||||
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
||||||
@ -129,6 +131,7 @@ public class VelocityServer implements ProxyServer {
|
|||||||
cm = new ConnectionManager(this);
|
cm = new ConnectionManager(this);
|
||||||
servers = new ServerMap(this);
|
servers = new ServerMap(this);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
this.bossBarManager = new BossBarManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeyPair getServerKeyPair() {
|
public KeyPair getServerKeyPair() {
|
||||||
@ -501,9 +504,15 @@ public class VelocityServer implements ProxyServer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters the given player from the proxy.
|
||||||
|
*
|
||||||
|
* @param connection the connection to unregister
|
||||||
|
*/
|
||||||
public void unregisterConnection(ConnectedPlayer connection) {
|
public void unregisterConnection(ConnectedPlayer connection) {
|
||||||
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
|
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
|
||||||
connectionsByUuid.remove(connection.getUniqueId(), connection);
|
connectionsByUuid.remove(connection.getUniqueId(), connection);
|
||||||
|
bossBarManager.onDisconnect(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -615,6 +624,10 @@ public class VelocityServer implements ProxyServer {
|
|||||||
getAllPlayers().iterator());
|
getAllPlayers().iterator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BossBarManager getBossBarManager() {
|
||||||
|
return bossBarManager;
|
||||||
|
}
|
||||||
|
|
||||||
public static Gson getPingGsonInstance(ProtocolVersion version) {
|
public static Gson getPingGsonInstance(ProtocolVersion version) {
|
||||||
return version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0 ? POST_1_16_PING_SERIALIZER
|
return version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0 ? POST_1_16_PING_SERIALIZER
|
||||||
: PRE_1_16_PING_SERIALIZER;
|
: PRE_1_16_PING_SERIALIZER;
|
||||||
|
@ -64,6 +64,7 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.TextComponent;
|
import net.kyori.adventure.text.TextComponent;
|
||||||
import net.kyori.adventure.text.TranslatableComponent;
|
import net.kyori.adventure.text.TranslatableComponent;
|
||||||
@ -313,6 +314,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
connection.write(TitlePacket.resetForProtocolVersion(this.getProtocolVersion()));
|
connection.write(TitlePacket.resetForProtocolVersion(this.getProtocolVersion()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideBossBar(@NonNull BossBar bar) {
|
||||||
|
this.server.getBossBarManager().removeBossBar(this, bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showBossBar(@NonNull BossBar bar) {
|
||||||
|
this.server.getBossBarManager().addBossBar(this, bar);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ConnectionRequestBuilder createConnectionRequest(RegisteredServer server) {
|
public ConnectionRequestBuilder createConnectionRequest(RegisteredServer server) {
|
||||||
return new ConnectionRequestBuilderImpl(server);
|
return new ConnectionRequestBuilderImpl(server);
|
||||||
|
@ -173,4 +173,11 @@ public class BossBar implements MinecraftPacket {
|
|||||||
public boolean handle(MinecraftSessionHandler handler) {
|
public boolean handle(MinecraftSessionHandler handler) {
|
||||||
return handler.handle(this);
|
return handler.handle(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BossBar createRemovePacket(UUID id) {
|
||||||
|
BossBar packet = new BossBar();
|
||||||
|
packet.setUuid(id);
|
||||||
|
packet.setAction(REMOVE);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,261 @@
|
|||||||
|
package com.velocitypowered.proxy.util.bossbar;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
|
import com.velocitypowered.proxy.util.collect.Enum2IntMap;
|
||||||
|
import com.velocitypowered.proxy.util.collect.concurrent.SyncMap;
|
||||||
|
import com.velocitypowered.proxy.util.concurrent.Once;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Color;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Flag;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Overlay;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages all boss bars known to the proxy.
|
||||||
|
*/
|
||||||
|
public class BossBarManager implements BossBar.Listener {
|
||||||
|
private static final Enum2IntMap<Color> COLORS_TO_PROTOCOL =
|
||||||
|
new Enum2IntMap.Builder<>(Color.class)
|
||||||
|
.put(Color.PINK, 0)
|
||||||
|
.put(Color.BLUE, 1)
|
||||||
|
.put(Color.RED, 2)
|
||||||
|
.put(Color.GREEN, 3)
|
||||||
|
.put(Color.YELLOW, 4)
|
||||||
|
.put(Color.PURPLE, 5)
|
||||||
|
.put(Color.WHITE, 6)
|
||||||
|
.build();
|
||||||
|
private static final Enum2IntMap<Overlay> OVERLAY_TO_PROTOCOL =
|
||||||
|
new Enum2IntMap.Builder<>(Overlay.class)
|
||||||
|
.put(Overlay.PROGRESS, 0)
|
||||||
|
.put(Overlay.NOTCHED_6, 1)
|
||||||
|
.put(Overlay.NOTCHED_10, 2)
|
||||||
|
.put(Overlay.NOTCHED_12, 3)
|
||||||
|
.put(Overlay.NOTCHED_20, 4)
|
||||||
|
.build();
|
||||||
|
private static final Enum2IntMap<Flag> FLAG_BITS_TO_PROTOCOL =
|
||||||
|
new Enum2IntMap.Builder<>(Flag.class)
|
||||||
|
.put(Flag.DARKEN_SCREEN, 0x1)
|
||||||
|
.put(Flag.PLAY_BOSS_MUSIC, 0x2)
|
||||||
|
.put(Flag.CREATE_WORLD_FOG, 0x4)
|
||||||
|
.build();
|
||||||
|
private final SyncMap<BossBar, BossBarHolder> bars;
|
||||||
|
|
||||||
|
public BossBarManager() {
|
||||||
|
this.bars = SyncMap.of(WeakHashMap::new, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable BossBarHolder getHandler(BossBar bar) {
|
||||||
|
return this.bars.get(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BossBarHolder getOrCreateHandler(BossBar bar) {
|
||||||
|
BossBarHolder holder = this.bars.computeIfAbsent(bar, (k) -> new BossBarHolder(bar));
|
||||||
|
holder.register();
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a player disconnects from the proxy. Removes the player from any boss bar
|
||||||
|
* subscriptions.
|
||||||
|
*
|
||||||
|
* @param player the player to remove
|
||||||
|
*/
|
||||||
|
public void onDisconnect(ConnectedPlayer player) {
|
||||||
|
for (BossBarHolder holder : bars.values()) {
|
||||||
|
holder.subscribers.remove(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the specified player to the boss bar's viewers and spawns the boss bar, registering the
|
||||||
|
* boss bar if needed.
|
||||||
|
* @param player the intended viewer
|
||||||
|
* @param bar the boss bar to show
|
||||||
|
*/
|
||||||
|
public void addBossBar(ConnectedPlayer player, BossBar bar) {
|
||||||
|
BossBarHolder holder = this.getOrCreateHandler(bar);
|
||||||
|
if (holder.subscribers.add(player)) {
|
||||||
|
player.getConnection().write(holder.createAddPacket(player.getProtocolVersion()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the specified player to the boss bar's viewers and despawns the boss bar.
|
||||||
|
* @param player the intended viewer
|
||||||
|
* @param bar the boss bar to hide
|
||||||
|
*/
|
||||||
|
public void removeBossBar(ConnectedPlayer player, BossBar bar) {
|
||||||
|
BossBarHolder holder = this.getHandler(bar);
|
||||||
|
if (holder != null && holder.subscribers.remove(player)) {
|
||||||
|
player.getConnection().write(holder.createRemovePacket());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bossBarNameChanged(@NonNull BossBar bar, @NonNull Component oldName,
|
||||||
|
@NonNull Component newName) {
|
||||||
|
BossBarHolder holder = this.getHandler(bar);
|
||||||
|
if (holder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar pre116Packet = holder.createTitleUpdate(
|
||||||
|
newName, ProtocolVersion.MINECRAFT_1_15_2);
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar rgbPacket = holder.createTitleUpdate(
|
||||||
|
newName, ProtocolVersion.MINECRAFT_1_16);
|
||||||
|
for (ConnectedPlayer player : holder.subscribers) {
|
||||||
|
if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||||
|
player.getConnection().write(rgbPacket);
|
||||||
|
} else {
|
||||||
|
player.getConnection().write(pre116Packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bossBarPercentChanged(@NonNull BossBar bar, float oldPercent, float newPercent) {
|
||||||
|
BossBarHolder holder = this.getHandler(bar);
|
||||||
|
if (holder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = holder
|
||||||
|
.createPercentUpdate(newPercent);
|
||||||
|
for (ConnectedPlayer player : holder.subscribers) {
|
||||||
|
player.getConnection().write(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bossBarColorChanged(@NonNull BossBar bar, @NonNull Color oldColor,
|
||||||
|
@NonNull Color newColor) {
|
||||||
|
BossBarHolder holder = this.getHandler(bar);
|
||||||
|
if (holder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = holder.createColorUpdate(newColor);
|
||||||
|
for (ConnectedPlayer player : holder.subscribers) {
|
||||||
|
player.getConnection().write(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bossBarOverlayChanged(@NonNull BossBar bar, @NonNull Overlay oldOverlay,
|
||||||
|
@NonNull Overlay newOverlay) {
|
||||||
|
BossBarHolder holder = this.getHandler(bar);
|
||||||
|
if (holder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = holder
|
||||||
|
.createOverlayUpdate(newOverlay);
|
||||||
|
for (ConnectedPlayer player : holder.subscribers) {
|
||||||
|
player.getConnection().write(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bossBarFlagsChanged(@NonNull BossBar bar, @NonNull Set<Flag> oldFlags,
|
||||||
|
@NonNull Set<Flag> newFlags) {
|
||||||
|
BossBarHolder holder = this.getHandler(bar);
|
||||||
|
if (holder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = holder.createFlagsUpdate(newFlags);
|
||||||
|
for (ConnectedPlayer player : holder.subscribers) {
|
||||||
|
player.getConnection().write(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BossBarHolder {
|
||||||
|
private final UUID id = UUID.randomUUID();
|
||||||
|
private final BossBar bar;
|
||||||
|
private final Set<ConnectedPlayer> subscribers = SyncMap.setOf(WeakHashMap::new, 16);
|
||||||
|
private final Once registrationOnce = new Once();
|
||||||
|
|
||||||
|
BossBarHolder(BossBar bar) {
|
||||||
|
this.bar = bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
void register() {
|
||||||
|
registrationOnce.run(() -> this.bar.addListener(BossBarManager.this));
|
||||||
|
}
|
||||||
|
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar createRemovePacket() {
|
||||||
|
return com.velocitypowered.proxy.protocol.packet.BossBar.createRemovePacket(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar createAddPacket(ProtocolVersion version) {
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = new com.velocitypowered
|
||||||
|
.proxy.protocol.packet.BossBar();
|
||||||
|
packet.setUuid(this.id);
|
||||||
|
packet.setAction(com.velocitypowered.proxy.protocol.packet.BossBar.ADD);
|
||||||
|
packet.setName(ProtocolUtils.getJsonChatSerializer(version).serialize(bar.name()));
|
||||||
|
packet.setColor(COLORS_TO_PROTOCOL.get(bar.color()));
|
||||||
|
packet.setOverlay(bar.overlay().ordinal());
|
||||||
|
packet.setPercent(bar.percent());
|
||||||
|
packet.setFlags(serializeFlags(bar.flags()));
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar createPercentUpdate(float newPercent) {
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = new com.velocitypowered
|
||||||
|
.proxy.protocol.packet.BossBar();
|
||||||
|
packet.setUuid(this.id);
|
||||||
|
packet.setAction(com.velocitypowered.proxy.protocol.packet.BossBar.UPDATE_PERCENT);
|
||||||
|
packet.setPercent(newPercent);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar createColorUpdate(Color color) {
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = new com.velocitypowered
|
||||||
|
.proxy.protocol.packet.BossBar();
|
||||||
|
packet.setUuid(this.id);
|
||||||
|
packet.setAction(com.velocitypowered.proxy.protocol.packet.BossBar.UPDATE_NAME);
|
||||||
|
packet.setColor(COLORS_TO_PROTOCOL.get(color));
|
||||||
|
packet.setFlags(serializeFlags(bar.flags()));
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar createTitleUpdate(Component name,
|
||||||
|
ProtocolVersion version) {
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = new com.velocitypowered
|
||||||
|
.proxy.protocol.packet.BossBar();
|
||||||
|
packet.setUuid(this.id);
|
||||||
|
packet.setAction(com.velocitypowered.proxy.protocol.packet.BossBar.UPDATE_NAME);
|
||||||
|
packet.setName(ProtocolUtils.getJsonChatSerializer(version).serialize(name));
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar createFlagsUpdate(Set<Flag> newFlags) {
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = new com.velocitypowered
|
||||||
|
.proxy.protocol.packet.BossBar();
|
||||||
|
packet.setUuid(this.id);
|
||||||
|
packet.setAction(com.velocitypowered.proxy.protocol.packet.BossBar.UPDATE_PROPERTIES);
|
||||||
|
packet.setColor(COLORS_TO_PROTOCOL.get(this.bar.color()));
|
||||||
|
packet.setFlags(this.serializeFlags(newFlags));
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar createOverlayUpdate(Overlay overlay) {
|
||||||
|
com.velocitypowered.proxy.protocol.packet.BossBar packet = new com.velocitypowered
|
||||||
|
.proxy.protocol.packet.BossBar();
|
||||||
|
packet.setUuid(this.id);
|
||||||
|
packet.setAction(com.velocitypowered.proxy.protocol.packet.BossBar.UPDATE_PROPERTIES);
|
||||||
|
packet.setOverlay(OVERLAY_TO_PROTOCOL.get(overlay));
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte serializeFlags(Set<Flag> flags) {
|
||||||
|
byte val = 0x0;
|
||||||
|
for (Flag flag : flags) {
|
||||||
|
val |= FLAG_BITS_TO_PROTOCOL.get(flag);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package com.velocitypowered.proxy.util.collect;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
public class Enum2IntMap<E extends Enum<E>> {
|
||||||
|
private final int[] mappings;
|
||||||
|
|
||||||
|
private Enum2IntMap(int[] mappings) {
|
||||||
|
this.mappings = mappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int get(E key) {
|
||||||
|
return mappings[key.ordinal()];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder<E extends Enum<E>> {
|
||||||
|
private final int[] mappings;
|
||||||
|
private final EnumSet<E> populated;
|
||||||
|
private int defaultValue = -1;
|
||||||
|
|
||||||
|
public Builder(Class<E> klazz) {
|
||||||
|
this.mappings = new int[klazz.getEnumConstants().length];
|
||||||
|
this.populated = EnumSet.noneOf(klazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder<E> put(E key, int value) {
|
||||||
|
this.mappings[key.ordinal()] = value;
|
||||||
|
this.populated.add(key);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder<E> remove(E key, int value) {
|
||||||
|
this.populated.remove(key);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder<E> defaultValue(int defaultValue) {
|
||||||
|
this.defaultValue = defaultValue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int get(E key) {
|
||||||
|
if (this.populated.contains(key)) {
|
||||||
|
return this.mappings[key.ordinal()];
|
||||||
|
}
|
||||||
|
return this.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Enum2IntMap<E> build() {
|
||||||
|
for (E unpopulated : EnumSet.complementOf(this.populated)) {
|
||||||
|
this.mappings[unpopulated.ordinal()] = this.defaultValue;
|
||||||
|
}
|
||||||
|
return new Enum2IntMap<>(this.mappings.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,233 @@
|
|||||||
|
package com.velocitypowered.proxy.util.collect.concurrent;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A concurrent map, internally backed by a non-thread-safe map but carefully managed in a matter
|
||||||
|
* such that any changes are thread-safe. Internally, the map is split into a {@code read} and a
|
||||||
|
* {@code dirty} map. The read map only satisfies read requests, while the dirty map satisfies all
|
||||||
|
* other requests.
|
||||||
|
*
|
||||||
|
* <p>The map is optimized for two common use cases:</p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>The entry for the given map is only written once but read many
|
||||||
|
* times, as in a cache that only grows.</li>
|
||||||
|
*
|
||||||
|
* <li>Heavy concurrent modification of entries for a disjoint set of
|
||||||
|
* keys.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>In both cases, this map significantly reduces lock contention compared
|
||||||
|
* to a traditional map paired with a read and write lock, along with maps
|
||||||
|
* with an exclusive lock (such as with {@link Collections#synchronizedMap(Map)}.</p>
|
||||||
|
*
|
||||||
|
* <p>Null values are not accepted. Null keys are supported if the backing collection
|
||||||
|
* supports them.</p>
|
||||||
|
*
|
||||||
|
* <p>Based on: https://golang.org/src/sync/map.go</p>
|
||||||
|
*
|
||||||
|
* @param <K> the key type
|
||||||
|
* @param <V> the value type
|
||||||
|
*/
|
||||||
|
public interface SyncMap<K, V> extends ConcurrentMap<K, V> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a sync map, backed by a {@link HashMap}.
|
||||||
|
*
|
||||||
|
* @param <K> the key type
|
||||||
|
* @param <V> the value type
|
||||||
|
* @return a sync map
|
||||||
|
*/
|
||||||
|
static <K, V> SyncMap<K, V> hashmap() {
|
||||||
|
return of(HashMap<K, ExpungingValue<V>>::new, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a sync map, backed by a {@link HashMap} with a provided initial capacity.
|
||||||
|
*
|
||||||
|
* @param initialCapacity the initial capacity of the hash map
|
||||||
|
* @param <K> the key type
|
||||||
|
* @param <V> the value type
|
||||||
|
* @return a sync map
|
||||||
|
*/
|
||||||
|
static <K, V> SyncMap<K, V> hashmap(final int initialCapacity) {
|
||||||
|
return of(HashMap<K, ExpungingValue<V>>::new, initialCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mutable set view of a sync map, backed by a {@link HashMap}.
|
||||||
|
*
|
||||||
|
* @param <K> the key type
|
||||||
|
* @return a mutable set view of a sync map
|
||||||
|
*/
|
||||||
|
static <K> Set<K> hashset() {
|
||||||
|
return setOf(HashMap<K, ExpungingValue<Boolean>>::new, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mutable set view of a sync map, backed by a {@link HashMap} with a provided initial
|
||||||
|
* capacity.
|
||||||
|
*
|
||||||
|
* @param initialCapacity the initial capacity of the hash map
|
||||||
|
* @param <K> the key type
|
||||||
|
* @return a mutable set view of a sync map
|
||||||
|
*/
|
||||||
|
static <K> Set<K> hashset(final int initialCapacity) {
|
||||||
|
return setOf(HashMap<K, ExpungingValue<Boolean>>::new, initialCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a sync map, backed by the provided {@link Map} implementation with a provided initial
|
||||||
|
* capacity.
|
||||||
|
*
|
||||||
|
* @param function the map creation function
|
||||||
|
* @param initialCapacity the map initial capacity
|
||||||
|
* @param <K> the key type
|
||||||
|
* @param <V> the value type
|
||||||
|
* @return a sync map
|
||||||
|
*/
|
||||||
|
static <K, V> SyncMap<K, V> of(final Function<Integer, Map<K, ExpungingValue<V>>> function,
|
||||||
|
final int initialCapacity) {
|
||||||
|
return new SyncMapImpl<>(function, initialCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mutable set view of a sync map, backed by the provided {@link Map} implementation
|
||||||
|
* with a provided initial capacity.
|
||||||
|
*
|
||||||
|
* @param function the map creation function
|
||||||
|
* @param initialCapacity the map initial capacity
|
||||||
|
* @param <K> they key type
|
||||||
|
* @return a mutable set view of a sync map
|
||||||
|
*/
|
||||||
|
static <K> Set<K> setOf(final Function<Integer, Map<K, ExpungingValue<Boolean>>> function,
|
||||||
|
final int initialCapacity) {
|
||||||
|
return Collections.newSetFromMap(new SyncMapImpl<>(function, initialCapacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
* <p>Iterations over a sync map are thread-safe, and the keys iterated over will not change for a
|
||||||
|
* single iteration attempt, however they may not necessarily reflect the state of the map at the
|
||||||
|
* time the iterator was created.</p>
|
||||||
|
*
|
||||||
|
* <p>Performance note: if entries have been appended to the map, iterating over the entry set
|
||||||
|
* will automatically promote them to the read map.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
Set<Entry<K, V>> entrySet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
* <p>This implementation is {@code O(n)} in nature due to the need to check for any expunged
|
||||||
|
* entries. Likewise, as with other concurrent collections, the value obtained by this method may
|
||||||
|
* be out of date by the time this method returns.</p>
|
||||||
|
*
|
||||||
|
* @return the size of all the mappings contained in this map
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
int size();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
* <p>
|
||||||
|
* This method clears the map by resetting the internal state to a state similar to as if a new
|
||||||
|
* map had been created. If there are concurrent iterations in progress, they will reflect the
|
||||||
|
* state of the map prior to being cleared.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expunging value the backing map wraps for its values.
|
||||||
|
*
|
||||||
|
* @param <V> the backing value type
|
||||||
|
*/
|
||||||
|
interface ExpungingValue<V> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the backing element, which may be {@code null} if it has been expunged.
|
||||||
|
*
|
||||||
|
* @return the backing element if it has not been expunged
|
||||||
|
*/
|
||||||
|
V get();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to place the entry in the map if it is absent.
|
||||||
|
*
|
||||||
|
* @param value the value to place in the map
|
||||||
|
* @return a {@link Entry} with false key and null value if the value was expunged, a true key
|
||||||
|
* and the previous value in the map otherwise
|
||||||
|
*/
|
||||||
|
Entry<Boolean, V> putIfAbsent(V value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if this element has been expunged.
|
||||||
|
*
|
||||||
|
* @return whether or not this element has been expunged
|
||||||
|
*/
|
||||||
|
boolean isExpunged();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if this element has a value (it is neither expunged nor {@code null}.
|
||||||
|
*
|
||||||
|
* @return whether or not this element has a value
|
||||||
|
*/
|
||||||
|
boolean exists();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the backing element, which can be set to {@code null}.
|
||||||
|
*
|
||||||
|
* @param element the backing element
|
||||||
|
* @return the previous element stored, or {@code null} if the entry had been expunged
|
||||||
|
*/
|
||||||
|
V set(final V element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to replace the backing element, which can be set to {@code null}. This operation has no
|
||||||
|
* effect if the entry was expunged.
|
||||||
|
*
|
||||||
|
* @param expected the element to check for
|
||||||
|
* @param newElement the new element to be stored
|
||||||
|
* @return {@code true} if successful, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
boolean replace(final Object expected, final V newElement);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the entry stored in this value. Has no effect if {@code null} is stored in the map or
|
||||||
|
* the entry was expunged.
|
||||||
|
*/
|
||||||
|
V clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to set the backing element. If the entry is expunged, this operation will fail.
|
||||||
|
*
|
||||||
|
* @param element the new element
|
||||||
|
* @return {@code true} if the entry was not expunged, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
boolean trySet(final V element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to mark the item as expunged, if its value is {@code null}.
|
||||||
|
*
|
||||||
|
* @return whether or not the item has been expunged
|
||||||
|
*/
|
||||||
|
boolean tryMarkExpunged();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to set the backing element, which can be set to {@code null}, if the entry was
|
||||||
|
* expunged.
|
||||||
|
*
|
||||||
|
* @param element the new element
|
||||||
|
* @return {@code true} if the entry was unexpunged, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
boolean tryUnexpungeAndSet(final V element);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,562 @@
|
|||||||
|
package com.velocitypowered.proxy.util.collect.concurrent;
|
||||||
|
|
||||||
|
import java.util.AbstractMap;
|
||||||
|
import java.util.AbstractSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/* package */ final class SyncMapImpl<K, V> extends AbstractMap<K, V> implements SyncMap<K, V> {
|
||||||
|
|
||||||
|
private final Object lock = new Object();
|
||||||
|
private final Function<Integer, Map<K, ExpungingValue<V>>> function;
|
||||||
|
private volatile Map<K, ExpungingValue<V>> read;
|
||||||
|
private volatile boolean readAmended;
|
||||||
|
private int readMisses;
|
||||||
|
private Map<K, ExpungingValue<V>> dirty;
|
||||||
|
private EntrySet entrySet;
|
||||||
|
|
||||||
|
/* package */ SyncMapImpl(final Function<Integer, Map<K, ExpungingValue<V>>> function,
|
||||||
|
final int initialCapacity) {
|
||||||
|
this.function = function;
|
||||||
|
this.read = function.apply(initialCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
if (this.readAmended) {
|
||||||
|
synchronized (this.lock) {
|
||||||
|
if (this.readAmended) {
|
||||||
|
return this.getSize(this.dirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.getSize(this.read);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getSize(Map<K, ExpungingValue<V>> map) {
|
||||||
|
int size = 0;
|
||||||
|
for (ExpungingValue<V> value : map.values()) {
|
||||||
|
if (value.exists()) {
|
||||||
|
size++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExpungingValue<V> getValue(final Object key) {
|
||||||
|
ExpungingValue<V> entry = this.read.get(key);
|
||||||
|
if (entry == null && this.readAmended) {
|
||||||
|
entry = this.getValueReadMissed(key);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExpungingValue<V> getValueReadMissed(final Object key) {
|
||||||
|
ExpungingValue<V> entry = null;
|
||||||
|
synchronized (this.lock) {
|
||||||
|
if (this.readAmended && (entry = this.read.get(key)) == null && this.dirty != null) {
|
||||||
|
entry = this.dirty.get(key);
|
||||||
|
this.missLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsKey(final Object key) {
|
||||||
|
ExpungingValue<V> entry = this.getValue(key);
|
||||||
|
return entry != null && entry.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V get(final Object key) {
|
||||||
|
ExpungingValue<V> entry = this.getValue(key);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entry.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V put(final K key, final V value) {
|
||||||
|
Objects.requireNonNull(value, "value");
|
||||||
|
|
||||||
|
ExpungingValue<V> entry = this.read.get(key);
|
||||||
|
V previous = entry != null ? entry.get() : null;
|
||||||
|
if (entry != null && entry.trySet(value)) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
return this.putDirty(key, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private V putDirty(final K key, final V value, boolean onlyIfExists) {
|
||||||
|
ExpungingValue<V> entry;
|
||||||
|
V previous = null;
|
||||||
|
synchronized (this.lock) {
|
||||||
|
if (!onlyIfExists) {
|
||||||
|
entry = this.read.get(key);
|
||||||
|
if (entry != null && entry.tryUnexpungeAndSet(value)) {
|
||||||
|
// If we had an expunged entry, then this.dirty != null and we need to insert the entry there too.
|
||||||
|
this.dirty.put(key, entry);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dirty != null && (entry = this.dirty.get(key)) != null) {
|
||||||
|
previous = entry.set(value);
|
||||||
|
} else if (!onlyIfExists) {
|
||||||
|
if (!this.readAmended) {
|
||||||
|
this.dirtyLocked();
|
||||||
|
this.readAmended = true;
|
||||||
|
}
|
||||||
|
assert this.dirty != null;
|
||||||
|
this.dirty.put(key, new ExpungingValueImpl<>(value));
|
||||||
|
previous = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V remove(final Object key) {
|
||||||
|
ExpungingValue<V> entry = this.read.get(key);
|
||||||
|
if (entry == null && this.readAmended) {
|
||||||
|
synchronized (this.lock) {
|
||||||
|
if (this.readAmended && (entry = this.read.get(key)) == null && this.dirty != null) {
|
||||||
|
entry = this.dirty.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry != null ? entry.clear() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean remove(final Object key, final Object value) {
|
||||||
|
Objects.requireNonNull(value, "value");
|
||||||
|
|
||||||
|
ExpungingValue<V> entry = this.read.get(key);
|
||||||
|
boolean absent = entry == null;
|
||||||
|
if (absent && this.readAmended) {
|
||||||
|
synchronized (this.lock) {
|
||||||
|
if (this.readAmended && (absent = (entry = this.read.get(key)) == null)
|
||||||
|
&& this.dirty != null) {
|
||||||
|
absent = (entry = this.dirty.get(key)) == null;
|
||||||
|
if (!absent && entry.replace(value, null)) {
|
||||||
|
this.dirty.remove(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!absent) {
|
||||||
|
entry.replace(value, null);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V putIfAbsent(K key, V value) {
|
||||||
|
Objects.requireNonNull(value, "value");
|
||||||
|
|
||||||
|
// Go in for a clean hit if we can.
|
||||||
|
ExpungingValue<V> entry = this.read.get(key);
|
||||||
|
if (entry != null) {
|
||||||
|
Entry<Boolean, V> result = entry.putIfAbsent(value);
|
||||||
|
if (result.getKey() == Boolean.TRUE) {
|
||||||
|
return result.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (this.lock) {
|
||||||
|
entry = this.read.get(key);
|
||||||
|
if (entry != null && entry.tryUnexpungeAndSet(value)) {
|
||||||
|
this.dirty.put(key, entry);
|
||||||
|
return null;
|
||||||
|
} else if (this.dirty != null && (entry = this.dirty.get(key)) != null) {
|
||||||
|
Entry<Boolean, V> result = entry.putIfAbsent(value);
|
||||||
|
this.missLocked();
|
||||||
|
|
||||||
|
// The only time an entry would be expunged is if it were in the read map, and we've already checked for
|
||||||
|
// that earlier.
|
||||||
|
assert result.getKey() == Boolean.TRUE;
|
||||||
|
return result.getValue();
|
||||||
|
} else {
|
||||||
|
if (!this.readAmended) {
|
||||||
|
this.dirtyLocked();
|
||||||
|
this.readAmended = true;
|
||||||
|
}
|
||||||
|
assert this.dirty != null;
|
||||||
|
this.dirty.put(key, new ExpungingValueImpl<>(value));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V replace(K key, V value) {
|
||||||
|
Objects.requireNonNull(value, "value");
|
||||||
|
|
||||||
|
ExpungingValue<V> entry = this.read.get(key);
|
||||||
|
V previous = entry != null ? entry.get() : null;
|
||||||
|
if (entry != null && entry.trySet(value)) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
return this.putDirty(key, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean replace(K key, V oldValue, V newValue) {
|
||||||
|
Objects.requireNonNull(oldValue, "oldValue");
|
||||||
|
Objects.requireNonNull(newValue, "newValue");
|
||||||
|
|
||||||
|
// Try a clean hit
|
||||||
|
ExpungingValue<V> entry = this.read.get(key);
|
||||||
|
if (entry != null && entry.replace(oldValue, newValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed, go to the slow path. This is considerably simpler than the others that need to consider expunging.
|
||||||
|
synchronized (this.lock) {
|
||||||
|
if (this.dirty != null) {
|
||||||
|
entry = this.dirty.get(key);
|
||||||
|
if (entry.replace(oldValue, newValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
synchronized (this.lock) {
|
||||||
|
this.read = this.function.apply(16);
|
||||||
|
this.dirty = null;
|
||||||
|
this.readMisses = 0;
|
||||||
|
this.readAmended = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Entry<K, V>> entrySet() {
|
||||||
|
if (this.entrySet != null) {
|
||||||
|
return this.entrySet;
|
||||||
|
}
|
||||||
|
return this.entrySet = new EntrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void promoteIfNeeded() {
|
||||||
|
if (this.readAmended) {
|
||||||
|
synchronized (this.lock) {
|
||||||
|
if (this.readAmended && this.dirty != null) {
|
||||||
|
this.promoteLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void promoteLocked() {
|
||||||
|
if (this.dirty != null) {
|
||||||
|
this.read = this.dirty;
|
||||||
|
}
|
||||||
|
this.dirty = null;
|
||||||
|
this.readMisses = 0;
|
||||||
|
this.readAmended = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void missLocked() {
|
||||||
|
this.readMisses++;
|
||||||
|
int length = this.dirty != null ? this.dirty.size() : 0;
|
||||||
|
if (this.readMisses > length) {
|
||||||
|
this.promoteLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dirtyLocked() {
|
||||||
|
if (this.dirty == null) {
|
||||||
|
this.dirty = this.function.apply(this.read.size());
|
||||||
|
for (final Entry<K, ExpungingValue<V>> entry : this.read.entrySet()) {
|
||||||
|
if (!entry.getValue().tryMarkExpunged()) {
|
||||||
|
this.dirty.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ExpungingValueImpl<V> implements SyncMap.ExpungingValue<V> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A marker object used to indicate that the value in this map has been expunged.
|
||||||
|
*/
|
||||||
|
private static final Object EXPUNGED = new Object();
|
||||||
|
|
||||||
|
// The raw type is required here, which is sad, but type erasure has forced our hand in this
|
||||||
|
// regard. (Besides, using an Object type and casting to the desired value allows us to reuse
|
||||||
|
// this field to see if the value has been expunged.)
|
||||||
|
//
|
||||||
|
// Type-safety is ensured by ensuring the special EXPUNGED value is never returned and using
|
||||||
|
// generics on the set and constructor calls.
|
||||||
|
private static final AtomicReferenceFieldUpdater<ExpungingValueImpl, Object> valueUpdater =
|
||||||
|
AtomicReferenceFieldUpdater.newUpdater(ExpungingValueImpl.class, Object.class, "value");
|
||||||
|
|
||||||
|
private volatile Object value;
|
||||||
|
|
||||||
|
private ExpungingValueImpl(final V value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V get() {
|
||||||
|
Object value = valueUpdater.get(this);
|
||||||
|
if (value == EXPUNGED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (V) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Entry<Boolean, V> putIfAbsent(V value) {
|
||||||
|
for (; ; ) {
|
||||||
|
Object existingVal = valueUpdater.get(this);
|
||||||
|
if (existingVal == EXPUNGED) {
|
||||||
|
return new SimpleImmutableEntry<>(Boolean.FALSE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingVal != null) {
|
||||||
|
return new SimpleImmutableEntry<>(Boolean.TRUE, (V) existingVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueUpdater.compareAndSet(this, null, value)) {
|
||||||
|
return new SimpleImmutableEntry<>(Boolean.TRUE, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isExpunged() {
|
||||||
|
return valueUpdater.get(this) == EXPUNGED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists() {
|
||||||
|
Object val = valueUpdater.get(this);
|
||||||
|
return val != null && val != EXPUNGED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V set(final V value) {
|
||||||
|
Object oldValue = valueUpdater.getAndSet(this, value);
|
||||||
|
return oldValue == EXPUNGED ? null : (V) oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean trySet(final V newValue) {
|
||||||
|
for (; ; ) {
|
||||||
|
Object foundValue = valueUpdater.get(this);
|
||||||
|
if (foundValue == EXPUNGED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueUpdater.compareAndSet(this, foundValue, newValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean tryMarkExpunged() {
|
||||||
|
Object val = valueUpdater.get(this);
|
||||||
|
while (val == null) {
|
||||||
|
if (valueUpdater.compareAndSet(this, null, EXPUNGED)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
val = valueUpdater.get(this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean tryUnexpungeAndSet(V element) {
|
||||||
|
return valueUpdater.compareAndSet(this, EXPUNGED, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean replace(final Object expected, final V newValue) {
|
||||||
|
for (; ; ) {
|
||||||
|
Object val = valueUpdater.get(this);
|
||||||
|
if (val == EXPUNGED || !Objects.equals(val, expected)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueUpdater.compareAndSet(this, val, newValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V clear() {
|
||||||
|
for (; ; ) {
|
||||||
|
Object val = valueUpdater.get(this);
|
||||||
|
if (val == null || val == EXPUNGED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (valueUpdater.compareAndSet(this, val, null)) {
|
||||||
|
return (V) val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MapEntry implements Entry<K, V> {
|
||||||
|
|
||||||
|
private final K key;
|
||||||
|
|
||||||
|
private MapEntry(final Entry<K, ExpungingValue<V>> entry) {
|
||||||
|
this.key = entry.getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public K getKey() {
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V getValue() {
|
||||||
|
return SyncMapImpl.this.get(this.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V setValue(final V value) {
|
||||||
|
return SyncMapImpl.this.put(this.key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SyncMapImpl.MapEntry{key=" + this.getKey() + ", value=" + this.getValue() + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object other) {
|
||||||
|
if (this == other) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(other instanceof Map.Entry)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Entry<?, ?> that = (Entry<?, ?>) other;
|
||||||
|
return Objects.equals(this.getKey(), that.getKey())
|
||||||
|
&& Objects.equals(this.getValue(), that.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.getKey(), this.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EntrySet extends AbstractSet<Entry<K, V>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return SyncMapImpl.this.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(final Object entry) {
|
||||||
|
if (!(entry instanceof Map.Entry)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Entry<?, ?> mapEntry = (Entry<?, ?>) entry;
|
||||||
|
final V value = SyncMapImpl.this.get(mapEntry.getKey());
|
||||||
|
return value != null && Objects.equals(mapEntry.getValue(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean remove(final Object entry) {
|
||||||
|
if (!(entry instanceof Map.Entry)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Entry<?, ?> mapEntry = (Entry<?, ?>) entry;
|
||||||
|
return SyncMapImpl.this.remove(mapEntry.getKey()) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clear() {
|
||||||
|
SyncMapImpl.this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<Entry<K, V>> iterator() {
|
||||||
|
SyncMapImpl.this.promoteIfNeeded();
|
||||||
|
return new EntryIterator(SyncMapImpl.this.read.entrySet().iterator());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EntryIterator implements Iterator<Entry<K, V>> {
|
||||||
|
|
||||||
|
private final Iterator<Entry<K, ExpungingValue<V>>> backingIterator;
|
||||||
|
private Entry<K, V> next;
|
||||||
|
private Entry<K, V> current;
|
||||||
|
|
||||||
|
private EntryIterator(final Iterator<Entry<K, ExpungingValue<V>>> backingIterator) {
|
||||||
|
this.backingIterator = backingIterator;
|
||||||
|
Entry<K, ExpungingValue<V>> entry = this.getNextValue();
|
||||||
|
this.next = (entry != null ? new MapEntry(entry) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Entry<K, ExpungingValue<V>> getNextValue() {
|
||||||
|
Entry<K, ExpungingValue<V>> entry = null;
|
||||||
|
while (this.backingIterator.hasNext() && entry == null) {
|
||||||
|
final ExpungingValue<V> value = (entry = this.backingIterator.next()).getValue();
|
||||||
|
if (!value.exists()) {
|
||||||
|
entry = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.next != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Entry<K, V> next() {
|
||||||
|
this.current = this.next;
|
||||||
|
Entry<K, ExpungingValue<V>> entry = this.getNextValue();
|
||||||
|
this.next = (entry != null ? new MapEntry(entry) : null);
|
||||||
|
if (this.current == null) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
return this.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
if (this.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SyncMapImpl.this.remove(this.current.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void forEachRemaining(final Consumer<? super Entry<K, V>> action) {
|
||||||
|
if (this.next != null) {
|
||||||
|
action.accept(this.next);
|
||||||
|
}
|
||||||
|
this.backingIterator.forEachRemaining(entry -> {
|
||||||
|
if (entry.getValue().exists()) {
|
||||||
|
action.accept(new MapEntry(entry));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.velocitypowered.proxy.util.concurrent;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that guarantees that a given initialization shall only happen once. The implementation
|
||||||
|
* is (almost) a direct Java port of the Go {@code sync.Once} type (see the
|
||||||
|
* <a href="https://golang.org/pkg/sync/#Once">Go documentation</a>) and thus has similar
|
||||||
|
* semantics.
|
||||||
|
*/
|
||||||
|
public final class Once {
|
||||||
|
private static final AtomicIntegerFieldUpdater<Once> COMPLETED_UPDATER =
|
||||||
|
AtomicIntegerFieldUpdater.newUpdater(Once.class, "completed");
|
||||||
|
|
||||||
|
private static final int NOT_STARTED = 0;
|
||||||
|
private static final int COMPLETED = 1;
|
||||||
|
|
||||||
|
private volatile int completed = NOT_STARTED;
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls {@code runnable.run()} exactly once if this instance is being called for the first time,
|
||||||
|
* otherwise the invocation shall wait until {@code runnable.run()} completes. Future calls to
|
||||||
|
* this method once {@code runnable.run()} completes are no-ops - a new instance should be used
|
||||||
|
* instead.
|
||||||
|
*
|
||||||
|
* @param runnable the runnable to run
|
||||||
|
*/
|
||||||
|
public void run(Runnable runnable) {
|
||||||
|
if (COMPLETED_UPDATER.get(this) == NOT_STARTED) {
|
||||||
|
slowRun(runnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void slowRun(Runnable runnable) {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (this.completed == NOT_STARTED) {
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} finally {
|
||||||
|
COMPLETED_UPDATER.set(this, COMPLETED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,586 @@
|
|||||||
|
package com.velocitypowered.proxy.util.collect.concurrent;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.velocitypowered.proxy.util.collect.concurrent.SyncMap.ExpungingValue;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Spliterator;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class SyncMapTest {
|
||||||
|
@Test
|
||||||
|
public void testInitialization() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertEquals(0, map.size());
|
||||||
|
assertTrue(map.isEmpty());
|
||||||
|
assertFalse(map.containsKey("foo"));
|
||||||
|
assertFalse(map.containsValue("bar"));
|
||||||
|
assertNull(map.get("foo"));
|
||||||
|
assertNull(map.remove("foo"));
|
||||||
|
assertFalse(map.remove("foo", "bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPutDisallowsNullValues() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertThrows(NullPointerException.class, () -> map.put("test", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_put_get() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
assertEquals("bar", map.put("foo", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
assertEquals(1, map.size());
|
||||||
|
assertFalse(map.isEmpty());
|
||||||
|
assertTrue(map.containsKey("foo"));
|
||||||
|
assertTrue(map.containsValue("baz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_put_getAllowsNull() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put(null, "bar"));
|
||||||
|
assertEquals("bar", map.get(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_repeatedPut() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
|
||||||
|
// Make sure to promote the map
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("bar", map.put("foo", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_handlesDirtyAndReadPut() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
|
||||||
|
// Make sure to promote the dirty map
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new entry to the dirty map. The entry in the read must not be affected.
|
||||||
|
assertNull(map.put("abc", "123"));
|
||||||
|
assertEquals("123", map.get("abc"));
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_ensuresExpungedEntriesProperlyHandled() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
|
||||||
|
// Make sure to promote the dirty map
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the entry in the read map.
|
||||||
|
assertEquals("bar", map.remove("foo"));
|
||||||
|
assertEquals(0, map.size());
|
||||||
|
|
||||||
|
// Add a new entry to the dirty map. At this point, the entry should be expunged.
|
||||||
|
assertNull(map.put("abc", "123"));
|
||||||
|
|
||||||
|
// Make sure the entry still doesn't exist.
|
||||||
|
assertNull(map.get("foo"));
|
||||||
|
|
||||||
|
// Promote the dirty map one last time
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("123", map.get("abc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the entry really doesn't exist.
|
||||||
|
assertNull(map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_putExpungedProperlyHandled() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
|
||||||
|
// Make sure to promote the dirty map
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the entry in the read map.
|
||||||
|
assertEquals("bar", map.remove("foo"));
|
||||||
|
assertEquals(0, map.size());
|
||||||
|
|
||||||
|
// Add a new entry to the dirty map. At this point, the entry should be marked as expunged.
|
||||||
|
assertNull(map.put("abc", "123"));
|
||||||
|
|
||||||
|
// Bring the old entry back to life.
|
||||||
|
assertNull(map.put("foo", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
|
||||||
|
// Cause the dirty map to be promoted.
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("123", map.get("abc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the entries we added still exist (the dirty map was properly updated).
|
||||||
|
assertEquals("123", map.get("abc"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPutIfAbsentDisallowsNullValues() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertThrows(NullPointerException.class, () ->map.putIfAbsent("test", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_putIfAbsentBasic() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.putIfAbsent("foo", "bar"));
|
||||||
|
assertEquals("bar", map.putIfAbsent("foo", "baz"));
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
|
||||||
|
assertEquals("bar", map.remove("foo"));
|
||||||
|
assertNull(map.putIfAbsent("foo", "bar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_putIfAbsentPromoted() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.putIfAbsent("foo", "bar"));
|
||||||
|
|
||||||
|
// Force a promotion of the dirty map
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the entry in the read map.
|
||||||
|
assertEquals("bar", map.putIfAbsent("foo", "baz"));
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReplaceSingleDisallowsNullValues() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertThrows(NullPointerException.class, () -> map.replace("test", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_replaceSingleBasic() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.replace("foo", "bar"));
|
||||||
|
assertEquals(0, map.size());
|
||||||
|
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
assertEquals("bar", map.replace("foo", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_replaceSingleHandlesPromotion() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
// Force a promotion of the dirty map
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("bar", map.replace("foo", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReplaceSpecificDisallowsNullValues1() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertThrows(NullPointerException.class, () -> map.replace("test", null,
|
||||||
|
"abc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReplaceSpecificDisallowsNullValues2() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertThrows(NullPointerException.class, () -> map.replace("test", "abc",
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_replaceSpecificBasic() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
assertFalse(map.replace("foo", "baz", "bar"));
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
assertTrue(map.replace("foo", "bar", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_replaceSpecificPromoted() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
// Force a promotion of the dirty map
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(map.replace("foo", "baz", "bar"));
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
assertTrue(map.replace("foo", "bar", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_putIfAbsentHandleExpunged() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.putIfAbsent("foo", "bar"));
|
||||||
|
|
||||||
|
// Force a promotion of the dirty map
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
assertEquals("bar", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the entry in the read map.
|
||||||
|
assertEquals("bar", map.remove("foo"));
|
||||||
|
assertEquals(0, map.size());
|
||||||
|
|
||||||
|
// Add a new entry to the dirty map. At this point, the existing entry should be marked as expunged.
|
||||||
|
assertNull(map.put("abc", "123"));
|
||||||
|
|
||||||
|
// Bring the old entry back to life
|
||||||
|
assertNull(map.putIfAbsent("foo", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_putAll() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
final Map<String, String> test = Maps.newHashMap();
|
||||||
|
test.put("1", "2");
|
||||||
|
test.put("3", "4");
|
||||||
|
test.put("5", "6");
|
||||||
|
|
||||||
|
map.putAll(test);
|
||||||
|
assertEquals("2", map.get("1"));
|
||||||
|
assertEquals("4", map.get("3"));
|
||||||
|
assertEquals("6", map.get("5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_remove() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
map.put("foo", "bar");
|
||||||
|
map.put("abc", "123");
|
||||||
|
assertEquals(2, map.size());
|
||||||
|
assertEquals("bar", map.remove("foo"));
|
||||||
|
assertTrue(map.remove("abc", "123"));
|
||||||
|
assertNull(map.get("foo"));
|
||||||
|
assertNull(map.get("abc"));
|
||||||
|
assertEquals(0, map.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_unexpunge() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
assertNull(map.put("foo", "bar"));
|
||||||
|
assertEquals("bar", map.remove("foo"));
|
||||||
|
assertNull(map.put("foo", "baz"));
|
||||||
|
assertEquals("baz", map.get("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMutation_clear() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
map.put("example", "random");
|
||||||
|
map.clear();
|
||||||
|
assertNull(map.get("example"));
|
||||||
|
assertEquals(0, map.size());
|
||||||
|
assertTrue(map.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeySetPermittedMutations() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Set<String> keys = map.keySet();
|
||||||
|
assertEquals(3, keys.size());
|
||||||
|
assertFalse(keys.isEmpty());
|
||||||
|
assertTrue(keys.contains("1"));
|
||||||
|
assertFalse(keys.contains("2"));
|
||||||
|
assertTrue(keys.remove("1"));
|
||||||
|
assertFalse(keys.remove("2"));
|
||||||
|
assertFalse(keys.contains("1"));
|
||||||
|
assertEquals(2, keys.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeySetAdditionProhibited() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
final Set<String> keys = map.keySet();
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> keys.add("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeySetAddAllProhibited() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
final Set<String> keys = map.keySet();
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> keys.addAll(Lists
|
||||||
|
.newArrayList("bar", "baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeyMutation_iterator() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.of(
|
||||||
|
LinkedHashMap<String, ExpungingValue<String>>::new, 3);
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Set<String> keys = map.keySet();
|
||||||
|
final Iterator<String> keyIterator = keys.iterator();
|
||||||
|
assertTrue(keyIterator.hasNext());
|
||||||
|
assertEquals("1", keyIterator.next());
|
||||||
|
keyIterator.remove();
|
||||||
|
assertFalse(keys.contains("1"));
|
||||||
|
|
||||||
|
final String[] expected = {"3", "5"};
|
||||||
|
final List<String> remaining = new ArrayList<>();
|
||||||
|
keyIterator.forEachRemaining(remaining::add);
|
||||||
|
assertArrayEquals(expected, remaining.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testKeyMutation_spliterator() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.of(LinkedHashMap<String, SyncMap.ExpungingValue<String>>::new, 3);
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Set<String> keys = map.keySet();
|
||||||
|
final Spliterator<String> keySpliterator = keys.spliterator();
|
||||||
|
assertTrue(keySpliterator.tryAdvance(value -> assertEquals("1", value)));
|
||||||
|
|
||||||
|
final String[] expected = {"3", "5"};
|
||||||
|
final List<String> remaining = new ArrayList<>();
|
||||||
|
keySpliterator.forEachRemaining(remaining::add);
|
||||||
|
assertArrayEquals(expected, remaining.toArray());
|
||||||
|
|
||||||
|
assertEquals(3, keySpliterator.estimateSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValuesPermittedMutation() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Collection<String> values = map.values();
|
||||||
|
assertEquals(3, values.size());
|
||||||
|
assertFalse(values.isEmpty());
|
||||||
|
assertTrue(values.contains("2"));
|
||||||
|
assertFalse(values.contains("1"));
|
||||||
|
assertTrue(values.remove("2"));
|
||||||
|
assertFalse(values.remove("1"));
|
||||||
|
assertFalse(values.contains("2"));
|
||||||
|
assertEquals(2, values.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValuesAdditionProhibited() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
final Collection<String> values = map.values();
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> values.add("foo")); // Causes UOE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValuesAddAllProhibited() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
final Collection<String> values = map.values();
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> values.addAll(Lists
|
||||||
|
.newArrayList("bar", "baz"))); // Causes UOE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValueMutation_iterator() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.of(LinkedHashMap<String, SyncMap.ExpungingValue<String>>::new, 3);
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Collection<String> values = map.values();
|
||||||
|
final Iterator<String> valueIterator = values.iterator();
|
||||||
|
assertTrue(valueIterator.hasNext());
|
||||||
|
assertEquals("2", valueIterator.next());
|
||||||
|
valueIterator.remove();
|
||||||
|
assertFalse(values.contains("2"));
|
||||||
|
|
||||||
|
final String[] expected = {"4", "6"};
|
||||||
|
final List<String> remaining = new ArrayList<>();
|
||||||
|
valueIterator.forEachRemaining(remaining::add);
|
||||||
|
assertArrayEquals(expected, remaining.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValueMutation_spliterator() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.of(LinkedHashMap<String, SyncMap.ExpungingValue<String>>::new, 3);
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Collection<String> values = map.values();
|
||||||
|
final Spliterator<String> valueSpliterator = values.spliterator();
|
||||||
|
assertTrue(valueSpliterator.tryAdvance(value -> assertEquals("2", value)));
|
||||||
|
|
||||||
|
final String[] expected = {"4", "6"};
|
||||||
|
final List<String> remaining = new ArrayList<>();
|
||||||
|
valueSpliterator.forEachRemaining(remaining::add);
|
||||||
|
assertArrayEquals(expected, remaining.toArray());
|
||||||
|
|
||||||
|
assertEquals(3, valueSpliterator.estimateSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEntryMutation() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Map.Entry<String, String> goodEntry = this.exampleEntry("1", "2");
|
||||||
|
final Map.Entry<String, String> badEntry = this.exampleEntry("abc", "123");
|
||||||
|
|
||||||
|
final Set<Map.Entry<String, String>> entries = map.entrySet();
|
||||||
|
assertEquals(3, entries.size());
|
||||||
|
assertFalse(entries.isEmpty());
|
||||||
|
assertTrue(entries.contains(goodEntry));
|
||||||
|
assertFalse(entries.contains(badEntry));
|
||||||
|
assertTrue(entries.remove(goodEntry));
|
||||||
|
assertFalse(entries.remove(badEntry));
|
||||||
|
assertFalse(entries.contains(goodEntry));
|
||||||
|
assertEquals(2, entries.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEntryAddProhibited() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
final Map.Entry<String, String> badEntry = this.exampleEntry("abc", "123");
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> map.entrySet().add(badEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEntryAddAllProhibited() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.hashmap();
|
||||||
|
final Map.Entry<String, String> badEntry = this.exampleEntry("abc", "123");
|
||||||
|
final Map.Entry<String, String> badEntry2 = this.exampleEntry("1", "2");
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> map.entrySet().addAll(Lists
|
||||||
|
.newArrayList(badEntry, badEntry2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEntryMutation_iterator() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.of(LinkedHashMap<String, SyncMap.ExpungingValue<String>>::new, 3);
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Map.Entry<String, String> firstEntry = this.exampleEntry("1", "2");
|
||||||
|
final Map.Entry<String, String> secondEntry = this.exampleEntry("3", "4");
|
||||||
|
final Map.Entry<String, String> thirdEntry = this.exampleEntry("5", "6");
|
||||||
|
|
||||||
|
final Set<Map.Entry<String, String>> entries = map.entrySet();
|
||||||
|
final Iterator<Map.Entry<String, String>> entryIterator = entries.iterator();
|
||||||
|
assertTrue(entryIterator.hasNext());
|
||||||
|
assertEquals(entryIterator.next(), firstEntry);
|
||||||
|
entryIterator.remove();
|
||||||
|
assertFalse(entries.contains(firstEntry));
|
||||||
|
|
||||||
|
final List<Map.Entry<String, String>> remaining = new ArrayList<>();
|
||||||
|
entryIterator.forEachRemaining(remaining::add);
|
||||||
|
assertEquals(Lists.newArrayList(secondEntry, thirdEntry), remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEntryMutation_spliterator() {
|
||||||
|
final SyncMap<String, String> map = SyncMap.of(LinkedHashMap<String, SyncMap.ExpungingValue<String>>::new, 3);
|
||||||
|
map.put("1", "2");
|
||||||
|
map.put("3", "4");
|
||||||
|
map.put("5", "6");
|
||||||
|
|
||||||
|
final Map.Entry<String, String> firstEntry = this.exampleEntry("1", "2");
|
||||||
|
final Map.Entry<String, String> secondEntry = this.exampleEntry("3", "4");
|
||||||
|
final Map.Entry<String, String> thirdEntry = this.exampleEntry("5", "6");
|
||||||
|
|
||||||
|
final Set<Map.Entry<String, String>> entries = map.entrySet();
|
||||||
|
final Spliterator<Map.Entry<String, String>> entrySpliterator = entries.spliterator();
|
||||||
|
assertTrue(entrySpliterator.tryAdvance(value -> assertEquals(firstEntry, value)));
|
||||||
|
|
||||||
|
final List<Map.Entry<String, String>> remaining = new ArrayList<>();
|
||||||
|
entrySpliterator.forEachRemaining(remaining::add);
|
||||||
|
assertEquals(Lists.newArrayList(secondEntry, thirdEntry), remaining);
|
||||||
|
|
||||||
|
assertEquals(3, entrySpliterator.estimateSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map.Entry<String, String> exampleEntry(final String key, final String value) {
|
||||||
|
return new Map.Entry<String, String>() {
|
||||||
|
@Override
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String setValue(String value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SyncMapImpl.MapEntry{key=" + this.getKey() + ", value=" + this.getValue() + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object other) {
|
||||||
|
if(this == other) return true;
|
||||||
|
if(!(other instanceof Map.Entry)) return false;
|
||||||
|
final Map.Entry<?, ?> that = (Map.Entry<?, ?>) other;
|
||||||
|
return Objects.equals(this.getKey(), that.getKey())
|
||||||
|
&& Objects.equals(this.getValue(), that.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.getKey(), this.getValue());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren