Java tutorial
/* * The MIT License * * Copyright 2017 Fernando van Loenhout. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package me.ferrybig.javacoding.teamspeakconnector.internal; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Queue; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.IntFunction; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import me.ferrybig.javacoding.teamspeakconnector.ClientType; import me.ferrybig.javacoding.teamspeakconnector.File; import me.ferrybig.javacoding.teamspeakconnector.Server; import me.ferrybig.javacoding.teamspeakconnector.ServerStatus; import me.ferrybig.javacoding.teamspeakconnector.ShallowUser; import me.ferrybig.javacoding.teamspeakconnector.TeamspeakConnection; import me.ferrybig.javacoding.teamspeakconnector.TeamspeakException; import me.ferrybig.javacoding.teamspeakconnector.User; import me.ferrybig.javacoding.teamspeakconnector.internal.packets.ComplexRequest; import me.ferrybig.javacoding.teamspeakconnector.internal.packets.ComplexRequestBuilder; import me.ferrybig.javacoding.teamspeakconnector.internal.packets.ComplexResponse; import me.ferrybig.javacoding.teamspeakconnector.util.FutureUtil; /** * Internal object to the teamspeak connection, contains potentially usafe * methods, and it is not recommended to call methods of this class by yourself. * */ public class TeamspeakIO { private static final Logger LOG = Logger.getLogger(TeamspeakIO.class.getName()); private static final ByteBuf PING_PACKET = Unpooled.wrappedBuffer("\n".getBytes(StandardCharsets.UTF_8)); private final Queue<PendingPacket> incomingQueue = new LinkedList<>(); private final Channel channel; private boolean closed = false; private final Promise<ComplexResponse> closeFuture; private TeamspeakConnection con; private boolean started; private volatile Future<User> whoAmIPromise; public TeamspeakIO(Channel channel) { this.channel = Objects.requireNonNull(channel); this.closeFuture = channel.eventLoop().newPromise(); } public void registerConnection(TeamspeakConnection con) { if (this.started) { throw new IllegalStateException("Already started"); } this.con = con; } public Future<ComplexResponse> sendPacket(ComplexRequest req) { return sendPacket(req, SendBehaviour.NORMAL); } public Future<ComplexResponse> sendPacket(ComplexRequest req, SendBehaviour sendBehaviour) { if (closed) { if (sendBehaviour != SendBehaviour.NORMAL) { return this.closeFuture; } return channel.eventLoop().newFailedFuture(new TeamspeakException("Channel closed")); } Promise<ComplexResponse> prom = channel.eventLoop().newPromise(); ChannelFuture future; synchronized (incomingQueue) { if (closed) { if (sendBehaviour != SendBehaviour.NORMAL) { return this.closeFuture; } return prom.setFailure(new TeamspeakException("Channel closed")); } incomingQueue.offer(new PendingPacket(prom, req, sendBehaviour)); future = channel.writeAndFlush(req); } future.addListener(upstream -> { assert upstream == future; if (sendBehaviour == SendBehaviour.FORCE_CLOSE_CONNECTION) { channel.eventLoop().schedule(() -> { if (channel.isActive()) { LOG.fine("Closing channel by timeout"); channel.close(); } }, 10, TimeUnit.SECONDS); } if (!upstream.isSuccess()) { synchronized (incomingQueue) { if (incomingQueue.removeIf(prom::equals)) { prom.setFailure(new TeamspeakException("Exception during sending", upstream.cause())); } } } }); if (sendBehaviour == SendBehaviour.CLOSE_CONNECTION || sendBehaviour == SendBehaviour.FORCE_CLOSE_CONNECTION) { prom.addListener(upstream -> { assert upstream == prom; if (prom.isSuccess()) { synchronized (incomingQueue) { this.closed = true; } channel.close(); LOG.fine("Closing channel because sendmessage asked it"); } }); } return prom; } public <T, R> Future<R> chainFuture(Future<T> future, Function<T, R> mapping) { return FutureUtil.chainFuture(this.channel.eventLoop().newPromise(), future, mapping); } private void channeClosed(Throwable upstream) { synchronized (incomingQueue) { con = null; // Help the garbage collector closed = true; PendingPacket poll; TeamspeakException ex = new TeamspeakException("Channel closed"); if (upstream != null) { ex.initCause(upstream); } LOG.log(Level.FINE, "Marking {0} PendingPackets as closed", incomingQueue.size()); while ((poll = incomingQueue.poll()) != null) { poll.onChannelClose(upstream); } } } public void start() { if (this.con == null) { throw new IllegalStateException("No TeamspeakConnection registered"); } if (this.started) { throw new IllegalStateException("Already started"); } this.started = true; this.channel.pipeline().addLast(new SimpleChannelInboundHandler<ComplexResponse>() { private Throwable lastException = null; @Override protected void messageReceived(ChannelHandlerContext ctx, ComplexResponse msg) throws Exception { recievePacket(msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); lastException = cause; } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); channeClosed(lastException); } }); } private void recievePacket(ComplexResponse r) { LOG.log(Level.FINE, "Packet received with {0} commands", r.getCommands()); PendingPacket prom; synchronized (incomingQueue) { prom = incomingQueue.remove(); } prom.onResponseReceived(r); } public <T> Future<T> getCompletedFuture(T object) { return channel.eventLoop().newSucceededFuture(object); } public Channel getChannel() { return channel; } public InetAddress tryConvertAddress(String address) { try { return InetAddress.getByName(address); } catch (UnknownHostException ex) { LOG.log(Level.FINE, "Trying to convert address to ip failed", ex); return null; } } public Server mapServer(Map<String, String> data) { return new Server(con, Integer.parseInt(data.get("virtualserver_id")), Integer.parseInt(data.get("virtualserver_port")), resolveEnum(ServerStatus.class, data.get("virtualserver_status")), Integer.parseInt(data.getOrDefault("virtualserver_clientsonline", "0")), Integer.parseInt(data.getOrDefault("virtualserver_queryclientsonline", "0")), Integer.parseInt(data.getOrDefault("virtualserver_maxclients", "0")), Integer.parseInt(data.getOrDefault("virtualserver_uptime", "0")), data.get("virtualserver_name"), data.get("virtualserver_autostart").equals("1")); } public me.ferrybig.javacoding.teamspeakconnector.Channel mapChannel(Map<String, String> data) { return new me.ferrybig.javacoding.teamspeakconnector.Channel(con, Integer.parseInt(data.get("cid")), Integer.parseInt(data.get("channel_order")), con.getUnresolvedChannelById(Integer.parseInt(data.get("pid"))), data.get("channel_name"), data.get("channel_topic"), data.get("channel_flag_password").equals("1"), Integer.parseInt(data.getOrDefault("channel_needed_subscribe_power", "0")), // Not sure why this one is missing with `channelinfo` Integer.parseInt(data.get("channel_needed_talk_power")), data.get("channel_flag_default").equals("1"), data.get("channel_flag_permanent").equals("1"), Integer.parseInt(data.get("channel_icon_id")), Integer.parseInt(data.getOrDefault("total_clients_family", "0")), // These are missing with `channelinfo` Integer.parseInt(data.getOrDefault("channel_maxfamilyclients", "0")), Integer.parseInt(data.get("channel_maxclients")), Integer.parseInt(data.getOrDefault("total_clients", "0")), data.get("channel_flag_semi_permanent").equals("1"), Integer.parseInt(data.get("channel_codec")), Integer.parseInt(data.get("channel_codec_quality"))); } public User mapUser(Map<String, String> data) { return new User(con, Integer.parseInt(data.get("clid")), con.getUnresolvedChannelById(Integer.parseInt(data.get("cid"))), Integer.parseInt(data.get("client_database_id")), data.get("client_nickname"), ClientType.getById(Integer.parseInt(data.get("client_type"))), data.get("client_away").equals("1") ? data.get("client_away_message") : null, data.get("client_flag_talking").equals("1"), data.get("client_input_muted").equals("1"), data.get("client_output_muted").equals("1"), data.get("client_input_hardware").equals("1"), data.get("client_output_hardware").equals("1"), Integer.parseInt(data.get("client_talk_power")), data.get("client_is_talker").equals("1"), data.get("client_is_priority_speaker").equals("1"), data.get("client_is_recording").equals("1"), data.get("client_is_channel_commander").equals("1"), data.get("client_unique_identifier"), mapIntList(data.get("client_servergroups"), con::getUnresolvedGroupById), mapIntList(data.get("client_channel_group_id"), con::getUnresolvedChannelGroupById), con.getUnresolvedChannelById( Integer.parseInt(data.get("client_channel_group_inherited_channel_id"))), data.get("client_version"), data.get("client_platform"), Integer.parseInt(data.get("client_idle_time")), Long.parseLong(data.get("client_created")), Long.parseLong(data.get("client_lastconnected")), Integer.parseInt(data.get("client_icon_id")), data.get("client_country"), tryConvertAddress(data.get("connection_client_ip"))); } public ShallowUser mapShallowUser(Map<String, String> data) { return new ShallowUser(con, Integer.parseInt(data.get("clid")), data.get("client_unique_identifier"), Integer.parseInt(data.get("client_database_id")), con.getUnresolvedChannelById(Integer.parseInt(data.get("cid"))), data.get("client_nickname"), ClientType.getById(Integer.parseInt(data.get("client_type"))), data.get("client_away").equals("1") ? data.get("client_away_message") : null, data.get("client_input_muted").equals("1"), data.get("client_output_muted").equals("1"), data.get("client_input_hardware").equals("1"), data.get("client_output_hardware").equals("1"), Integer.parseInt(data.get("client_talk_power")), data.get("client_is_talker").equals("1"), data.get("client_is_priority_speaker").equals("1"), data.get("client_is_recording").equals("1"), data.get("client_is_channel_commander").equals("1"), mapIntList(data.get("client_servergroups"), con::getUnresolvedGroupById), mapIntList(data.get("client_channel_group_id"), con::getUnresolvedChannelGroupById), con.getUnresolvedChannelById( Integer.parseInt(data.get("client_channel_group_inherited_channel_id"))), Integer.parseInt(data.get("client_icon_id")), data.get("client_country")); } public User mapWhoAmI(Map<String, String> data) { return new User(con, Integer.parseInt(data.get("client_id")), con.getUnresolvedChannelById(Integer.parseInt(data.get("client_channel_id"))), Integer.parseInt(data.get("client_database_id")), data.get("client_nickname"), ClientType.QUERY, null, false, true, true, true, true, 0, false, false, false, false, data.get("client_unique_identifier"), Collections.emptyList(), Collections.emptyList(), con.getUnresolvedChannelById(Integer.parseInt(data.get("client_channel_id"))), "", // TODO program version "", // TODO program platform 0, 0, 0, 0, "", ((InetSocketAddress) channel.localAddress()).getAddress()); } public File mapFile(Map<String, String> data) { return null; // TODO } public <R> Future<R> mapComplexReponse(Future<ComplexResponse> in, Function<Map<String, String>, R> mapper) { return chainFuture(in, r -> { if (r.getCommands().size() != 1) { throw new IllegalArgumentException( "Cannot map a response of size " + r.getCommands().size() + " as a simple instance"); } return mapper.apply(r.getCommands().get(0)); }); } public <R> Future<List<R>> mapComplexReponseList(Future<ComplexResponse> in, Function<Map<String, String>, R> mapper) { return this.<R, R>mapComplexReponseList(in, mapper, Function.identity()); } public <R, I> Future<List<R>> mapComplexReponseList(Future<ComplexResponse> in, Function<Map<String, String>, I> mapper, Function<List<I>, List<R>> finalizer) { return chainFuture(in, r -> { return finalizer.apply(r.getCommands().stream().map(mapper).collect(Collectors.toList())); }); } public <R> List<R> mapIntList(String in, IntFunction<R> mapper) { return in.isEmpty() ? new ArrayList<>() : Arrays.stream(in.split(",")).mapToInt(Integer::parseInt).mapToObj(mapper) .collect(Collectors.toList()); } public <E extends Enum<E>> E resolveEnum(Class<E> enu, String var) { return Enum.valueOf(enu, var.toUpperCase().replace(' ', '_')); } private Map<Integer, me.ferrybig.javacoding.teamspeakconnector.Channel> mapChannelParents( Map<Integer, me.ferrybig.javacoding.teamspeakconnector.Channel> list) { for (me.ferrybig.javacoding.teamspeakconnector.Channel c : list.values()) { me.ferrybig.javacoding.teamspeakconnector.Channel parent = list.get(c.getParent().getId()); if (parent != null) { c.setParent(parent); } } return list; } /** * Pings the server, and returns a future stating when the ping was * delivered to the underlying channel * * @return */ public Future<?> ping() { return channel.writeAndFlush(PING_PACKET.retain()); } /** * Call this method after changing a server, so the api knows it should * refetch the information related to the server */ public void notifyServerChanged() { this.whoAmIPromise = null; whoAmI(); } public Future<User> whoAmI() { Future<User> whoami = this.whoAmIPromise; if (whoami != null) { return whoami; } synchronized (this) { whoami = this.whoAmIPromise; if (whoami != null) { return whoami; } whoami = this.mapComplexReponse(sendPacket(new ComplexRequestBuilder("whoami").build()), this::mapWhoAmI); this.whoAmIPromise = whoami; } return whoami; } }