001package com.github.theholywaffle.teamspeak3;
002
003/*
004 * #%L
005 * TeamSpeak 3 Java API
006 * %%
007 * Copyright (C) 2014 Bert De Geyter
008 * %%
009 * Permission is hereby granted, free of charge, to any person obtaining a copy
010 * of this software and associated documentation files (the "Software"), to deal
011 * in the Software without restriction, including without limitation the rights
012 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
013 * copies of the Software, and to permit persons to whom the Software is
014 * furnished to do so, subject to the following conditions:
015 * 
016 * The above copyright notice and this permission notice shall be included in
017 * all copies or substantial portions of the Software.
018 * 
019 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
020 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
021 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
022 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
023 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
024 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
025 * THE SOFTWARE.
026 * #L%
027 */
028
029import com.github.theholywaffle.teamspeak3.api.CommandFuture;
030import com.github.theholywaffle.teamspeak3.api.exception.TS3CommandFailedException;
031import com.github.theholywaffle.teamspeak3.api.wrapper.QueryError;
032import com.github.theholywaffle.teamspeak3.commands.Command;
033import com.github.theholywaffle.teamspeak3.commands.response.DefaultArrayResponse;
034import com.github.theholywaffle.teamspeak3.commands.response.ResponseBuilder;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import java.io.BufferedReader;
039import java.io.IOException;
040import java.io.InputStreamReader;
041import java.net.SocketTimeoutException;
042import java.util.Queue;
043
044public class SocketReader extends Thread {
045
046        private static final Logger log = LoggerFactory.getLogger(SocketReader.class);
047
048        private final TS3Query ts3;
049        private final Queue<ResponseBuilder> receiveQueue;
050        private final BufferedReader in;
051        private final SocketWriter writer;
052        private final long commandTimeout;
053        private final boolean logComms;
054
055        private String lastEvent = "";
056
057        public SocketReader(QueryIO io, SocketWriter writer, TS3Query ts3Query, TS3Config config) throws IOException {
058                super("[TeamSpeak-3-Java-API] SocketReader");
059                this.receiveQueue = io.getReceiveQueue();
060                this.writer = writer;
061                this.commandTimeout = config.getCommandTimeout();
062                this.ts3 = ts3Query;
063                this.logComms = config.getEnableCommunicationsLogging();
064
065                // Connect
066                this.in = new BufferedReader(new InputStreamReader(io.getSocket().getInputStream(), "UTF-8"));
067                int i = 0;
068                while (i < 4 || in.ready()) {
069                        String welcomeMessage = in.readLine();
070                        if (logComms) log.debug("< {}", welcomeMessage);
071                        i++;
072                }
073        }
074
075        @Override
076        public void run() {
077                while (!isInterrupted()) {
078                        final String line;
079
080                        try {
081                                // Will block until a full line of text could be read.
082                                line = in.readLine();
083                        } catch (SocketTimeoutException socketTimeout) {
084                                // Really disconnected or just no data transferred for <commandTimeout> milliseconds?
085                                if (receiveQueue.isEmpty() || writer.getIdleTime() < commandTimeout) {
086                                        continue;
087                                } else {
088                                        log.error("Connection timed out.", socketTimeout);
089                                        break;
090                                }
091                        } catch (IOException io) {
092                                if (!isInterrupted()) {
093                                        log.error("Connection error occurred.", io);
094                                }
095                                break;
096                        }
097
098                        if (line == null) {
099                                // End of stream: connection terminated by server
100                                log.error("Connection closed by the server.");
101                                break;
102                        } else if (line.isEmpty()) {
103                                continue; // The server is sending garbage
104                        }
105
106                        if (line.startsWith("notify")) {
107                                handleEvent(line);
108                        } else {
109                                handleCommandResponse(line);
110                        }
111                }
112
113                try {
114                        in.close();
115                } catch (IOException ignored) {
116                        // Ignore
117                }
118
119                if (!isInterrupted()) {
120                        ts3.fireDisconnect();
121                }
122        }
123
124        private void handleEvent(final String event) {
125                if (logComms) log.debug("[event] < {}", event);
126
127                // Filter out duplicate events for join, quit and channel move events
128                if (!isDuplicate(event)) {
129                        final String arr[] = event.split(" ", 2);
130                        ts3.getEventManager().fireEvent(arr[0], arr[1]);
131                }
132        }
133
134        private void handleCommandResponse(final String response) {
135                final ResponseBuilder responseBuilder = receiveQueue.peek();
136                if (responseBuilder == null) {
137                        log.warn("[UNHANDLED] < {}", response);
138                        return;
139                }
140
141                if (logComms) log.debug("[{}] < {}", responseBuilder.getCommand().getName(), response);
142
143                if (response.startsWith("error ")) {
144                        handleCommandError(responseBuilder, response);
145                } else {
146                        responseBuilder.appendResponse(response);
147                }
148        }
149
150        private void handleCommandError(ResponseBuilder responseBuilder, final String error) {
151                final Command command = responseBuilder.getCommand();
152                if (command.getName().equals("quit")) {
153                        // Response to a quit command received, we're done
154                        interrupt();
155                }
156
157                receiveQueue.remove();
158
159                final QueryError queryError = DefaultArrayResponse.parseError(error);
160                final CommandFuture<DefaultArrayResponse> future = command.getFuture();
161
162                if (queryError.isSuccessful()) {
163                        final DefaultArrayResponse response = responseBuilder.buildResponse();
164
165                        ts3.submitUserTask("Future SuccessListener (" + command.getName() + ")", new Runnable() {
166                                @Override
167                                public void run() {
168                                        future.set(response);
169                                }
170                        });
171                } else {
172                        log.debug("TS3 command error: {}", queryError);
173
174                        ts3.submitUserTask("Future FailureListener (" + command.getName() + ")", new Runnable() {
175                                @Override
176                                public void run() {
177                                        future.fail(new TS3CommandFailedException(queryError));
178                                }
179                        });
180                }
181        }
182
183        private boolean isDuplicate(String eventMessage) {
184                if (!(eventMessage.startsWith("notifyclientmoved")
185                                || eventMessage.startsWith("notifycliententerview")
186                                || eventMessage.startsWith("notifyclientleftview"))) {
187
188                        // Event that will never cause duplicates
189                        return false;
190                }
191
192                if (eventMessage.equals(lastEvent)) {
193                        // Duplicate event!
194                        lastEvent = ""; // Let's only ever filter one duplicate
195                        return true;
196                }
197
198                lastEvent = eventMessage;
199                return false;
200        }
201}