Java tutorial
/* * BungeeTabListPlus - a BungeeCord plugin to customize the tablist * * Copyright (C) 2014 - 2015 Florian Stober * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package codecrafter47.bungeetablistplus.managers; import codecrafter47.bungeetablistplus.BungeeTabListPlus; import codecrafter47.bungeetablistplus.common.BTLPDataKeys; import codecrafter47.bungeetablistplus.common.network.DataStreamUtils; import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry; import codecrafter47.bungeetablistplus.data.BTLPBungeeDataKeys; import codecrafter47.bungeetablistplus.data.BTLPDataTypes; import codecrafter47.bungeetablistplus.player.ConnectedPlayer; import codecrafter47.bungeetablistplus.player.IPlayerProvider; import codecrafter47.bungeetablistplus.player.RedisPlayer; import com.google.common.collect.Sets; import com.google.common.io.ByteArrayDataInput; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import com.imaginarycode.minecraft.redisbungee.RedisBungee; import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; import de.codecrafter47.data.api.DataCache; import de.codecrafter47.data.api.DataHolder; import de.codecrafter47.data.api.DataKey; import de.codecrafter47.data.api.DataKeyRegistry; import de.codecrafter47.data.bukkit.api.BukkitData; import de.codecrafter47.data.bungee.api.BungeeData; import de.codecrafter47.data.minecraft.api.MinecraftData; import de.codecrafter47.data.sponge.api.SpongeData; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; public class RedisPlayerManager implements IPlayerProvider, Listener { private static final TypeAdapterRegistry typeRegistry = TypeAdapterRegistry .of(TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS, BTLPDataTypes.REGISTRY); private static final DataKeyRegistry keyRegistry = DataKeyRegistry.of(MinecraftData.class, BukkitData.class, SpongeData.class, BungeeData.class, BTLPDataKeys.class, BTLPBungeeDataKeys.class); private static String CHANNEL_REQUEST_DATA_OLD = "btlp-data-request"; private static String CHANNEL_DATA_OLD = "btlp-data"; private static String CHANNEL_DATA_REQUEST = "btlp-data-req"; private static String CHANNEL_DATA_UPDATE = "btlp-data-upd"; private List<RedisPlayer> playerList = Collections.emptyList(); private final Map<UUID, RedisPlayer> byUUID = new ConcurrentHashMap<>(); private final ConnectedPlayerManager connectedPlayerManager; private final BungeeTabListPlus plugin; private final Logger logger; private final Consumer<String> missingDataKeyLogger = new Consumer<String>() { private final Set<String> missingKeys = Sets.newConcurrentHashSet(); @Override public void accept(String id) { if (missingKeys.add(id)) { logger.warning("Missing data key with id " + id + ". Is the plugin up-to-date?"); } } }; public RedisPlayerManager(ConnectedPlayerManager connectedPlayerManager, BungeeTabListPlus plugin, Logger logger) { this.connectedPlayerManager = connectedPlayerManager; this.plugin = plugin; this.logger = logger; RedisBungee.getApi().registerPubSubChannels(CHANNEL_REQUEST_DATA_OLD, CHANNEL_DATA_OLD); RedisBungee.getApi().registerPubSubChannels(CHANNEL_DATA_REQUEST, CHANNEL_DATA_UPDATE); ProxyServer.getInstance().getScheduler().schedule(BungeeTabListPlus.getInstance().getPlugin(), this::updatePlayers, 1, 1, TimeUnit.SECONDS); ProxyServer.getInstance().getPluginManager().registerListener(BungeeTabListPlus.getInstance().getPlugin(), this); } @Override public Collection<RedisPlayer> getPlayers() { return playerList; } @EventHandler @SuppressWarnings("unchecked") public void onRedisMessage(PubSubMessageEvent event) { String channel = event.getChannel(); if (channel.equals(CHANNEL_DATA_REQUEST)) { ByteArrayDataInput input = ByteStreams.newDataInput(Base64.getDecoder().decode(event.getMessage())); try { UUID uuid = DataStreamUtils.readUUID(input); ConnectedPlayer player = connectedPlayerManager.getPlayerIfPresent(uuid); if (player != null) { DataKey<?> key = DataStreamUtils.readDataKey(input, keyRegistry, missingDataKeyLogger); if (key != null) { player.addDataChangeListener((DataKey<Object>) key, new DataChangeListener(uuid, (DataKey<Object>) key)); updateData(uuid, (DataKey<Object>) key, player.get(key)); } } } catch (IOException ex) { logger.log(Level.SEVERE, "Unexpected error reading redis message", ex); } } else if (channel.equals(CHANNEL_DATA_UPDATE)) { ByteArrayDataInput input = ByteStreams.newDataInput(Base64.getDecoder().decode(event.getMessage())); try { UUID uuid = DataStreamUtils.readUUID(input); RedisPlayer player = byUUID.get(uuid); if (player != null) { DataCache cache = player.getData(); DataKey<?> key = DataStreamUtils.readDataKey(input, keyRegistry, missingDataKeyLogger); if (key != null) { boolean removed = input.readBoolean(); if (removed) { plugin.runInMainThread(() -> cache.updateValue(key, null)); } else { Object value = typeRegistry.getTypeAdapter(key.getType()).read(input); plugin.runInMainThread(() -> cache.updateValue((DataKey<Object>) key, value)); } } } } catch (IOException ex) { logger.log(Level.SEVERE, "Unexpected error reading redis message", ex); } } else if (channel.equals(CHANNEL_DATA_OLD) || channel.equals(CHANNEL_REQUEST_DATA_OLD)) { logger.warning("BungeeTabListPlus on at least one proxy in your network is outdated."); } } private void updatePlayers() { Set<UUID> playersOnline = RedisBungee.getApi().getPlayersOnline(); // remove players which have gone offline for (Iterator<UUID> iterator = byUUID.keySet().iterator(); iterator.hasNext();) { UUID uuid = iterator.next(); if (!playersOnline.contains(uuid) || connectedPlayerManager.getPlayerIfPresent(uuid) != null) { iterator.remove(); } } // add new players for (UUID uuid : playersOnline) { if (!byUUID.containsKey(uuid) && connectedPlayerManager.getPlayerIfPresent(uuid) == null) { byUUID.put(uuid, new RedisPlayer(uuid)); } } // update list playerList = byUUID.values().stream().filter(RedisPlayer::hasName).collect(Collectors.toList()); } public <T> void request(UUID uuid, DataKey<T> key) { try { ByteArrayDataOutput data = ByteStreams.newDataOutput(); DataStreamUtils.writeUUID(data, uuid); DataStreamUtils.writeDataKey(data, key); RedisBungee.getApi().sendChannelMessage(CHANNEL_DATA_REQUEST, Base64.getEncoder().encodeToString(data.toByteArray())); } catch (RuntimeException ex) { BungeeTabListPlus.getInstance().getLogger().log(Level.WARNING, "RedisBungee Error", ex); } catch (Throwable th) { BungeeTabListPlus.getInstance().getLogger().log(Level.SEVERE, "Failed to request data", th); } } private <T> void updateData(UUID uuid, DataKey<T> key, T value) { try { ByteArrayDataOutput data = ByteStreams.newDataOutput(); DataStreamUtils.writeUUID(data, uuid); DataStreamUtils.writeDataKey(data, key); data.writeBoolean(value == null); if (value != null) { typeRegistry.getTypeAdapter(key.getType()).write(data, value); } RedisBungee.getApi().sendChannelMessage(CHANNEL_DATA_UPDATE, Base64.getEncoder().encodeToString(data.toByteArray())); } catch (RuntimeException ex) { BungeeTabListPlus.getInstance().getLogger().log(Level.WARNING, "RedisBungee Error", ex); } catch (Throwable th) { BungeeTabListPlus.getInstance().getLogger().log(Level.SEVERE, "Failed to send data", th); } } private class DataChangeListener implements DataHolder.DataChangeListener<Object> { private final UUID uuid; private final DataKey<Object> dataKey; private DataChangeListener(UUID uuid, DataKey<Object> dataKey) { this.uuid = uuid; this.dataKey = dataKey; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DataChangeListener that = (DataChangeListener) o; return uuid.equals(that.uuid) && dataKey.equals(that.dataKey); } @Override public int hashCode() { int result = uuid.hashCode(); result = 31 * result + dataKey.hashCode(); return result; } @Override public void onChange(Object value) { RedisPlayerManager.this.updateData(uuid, dataKey, value); } } }