/******************************************************************************* * Copyright (c) 2004, 2010 BREDEX GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * BREDEX GmbH - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.jubula.communication.internal.connection; import java.io.BufferedReader; import java.io.IOException; import java.io.InterruptedIOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.apache.commons.lang.Validate; import org.eclipse.jubula.communication.internal.ConfigurableLogger; import org.eclipse.jubula.communication.internal.IExceptionHandler; import org.eclipse.jubula.communication.internal.listener.IErrorHandler; import org.eclipse.jubula.communication.internal.listener.IMessageHandler; import org.eclipse.jubula.communication.internal.message.MessageHeader; import org.eclipse.jubula.communication.internal.message.MessageHeader.InvalidHeaderVersionException; import org.eclipse.jubula.communication.internal.parser.MessageHeaderSerializer; import org.eclipse.jubula.communication.internal.writer.MessageWriter; import org.eclipse.jubula.tools.internal.constants.StringConstants; import org.eclipse.jubula.tools.internal.exception.SerialisationException; import org.eclipse.jubula.tools.internal.utils.IsAliveThread; import org.slf4j.LoggerFactory; /** * Class for sending and receiving messages (as strings) with a header which * contains the meta data. An instance is constructed with an open socket. * * This class is used by <code>Communicator</code> which is the class you * should use. * * <p> * How to use (for further description see the documentation at the methods) * <p> * * DefaultSocket socket = ... <br> * Connection con = new Connection(socket); <br> * <p> * con.addErrorHandler(...); <br> * con.addMessageHandler(...); <br> * <p> * start the reading thread con.startReading(); <br> * * MessageHeader header = ... ; see documentation there for settings <br> * header.set...(...); <br> * String message = ... ;<br> * con.sendMessage(header, message); <br> * ... * <p> * to close the connection use <br> * con.close(); <br> * * The connection will be closed, a new instance must be created. <br> * The reading thread is stopped immediately. <br> * <p> * * @author BREDEX GmbH * @created 09.07.2004 * */ public class Connection { /** * <code>IO_STREAM_ENCODING</code> * Encoding of Output-/InputStream. */ public static final String IO_STREAM_ENCODING = "UTF8"; //$NON-NLS-1$ /** the first number for the sequence numbers */ private static final long SEQUENCE_START = 1; /** the logger */ private ConfigurableLogger m_logger = new ConfigurableLogger( LoggerFactory.getLogger(Connection.class)); /** * the sequence number used by this connection to identify messages will be * increased after every send message */ private long m_sequenceNumber; /** the socket to use for this connection */ private Socket m_socket; /** reader for the input stream of the used socket */ private BufferedReader m_inputStreamReader; /** the output stream of the used socket */ private OutputStream m_outputStream; /** the set which contains the listeners for messages */ private Set<IMessageHandler> m_messageHandlers; /** the set which contains the listeners for error */ private Set<IErrorHandler> m_errorHandlers; /** the exception handler for reading from the network */ private IExceptionHandler m_exceptionHandler = null; /** the thread which reads from the socket */ private ReaderThread m_readerThread = null; /** boolean to avoid multiple notification of a shutdown */ private boolean m_shutDownFired; /** The (de)-serializer of message headers. */ private MessageHeaderSerializer m_headerSerializer; /** * constructor - initializes this connection, does not start with reading. * First register a message listener, then call startReading(). * @param socket the socket to use * @throws IllegalArgumentException if the socket is null or the socket has no assigned streams */ public Connection(DefaultClientSocket socket) throws IOException, IllegalArgumentException { this(socket, socket.getInputStreamReader()); } /** * constructor - initializes this connection, does not start with reading. * First register a message listener, then call startReading(). * @param socket the socket to use * @param socketInputStreamReader The input stream reader for the given * socket. This reader should always be used * rather than creating a new reader. * @throws IllegalArgumentException if the socket is null or the socket has no assigned streams */ public Connection(Socket socket, BufferedReader socketInputStreamReader) throws IllegalArgumentException { // check parameter socket Validate.notNull(socket, "socket must not be null"); //$NON-NLS-1$ try { m_socket = socket; m_outputStream = m_socket.getOutputStream(); m_inputStreamReader = socketInputStreamReader; } catch (IOException e) { throw new IllegalArgumentException("socket must be connected"); //$NON-NLS-1$ } // initialize member variables m_shutDownFired = false; m_sequenceNumber = SEQUENCE_START; // use HashSets to store the registered handlers // the set for the handlers must be a set supporting remove! // see remove*Handler() AND fire*- methods m_messageHandlers = new HashSet<IMessageHandler>(); m_errorHandlers = new HashSet<IErrorHandler>(); m_headerSerializer = new MessageHeaderSerializer(); } /** * synchronized method for retrieving a new sequence number from this * connection * @return an new sequenceNumber */ public synchronized String getNextSequenceNumber() { if (m_sequenceNumber == Long.MAX_VALUE) { m_sequenceNumber = SEQUENCE_START; } String result = String.valueOf(m_sequenceNumber); m_sequenceNumber++; return result; } /** * Starts reading from the input stream of the socket (in a separated thread). * @param id for debugging purposes: mark the reader thread */ public void startReading(String id) { ReaderThread readerThread = m_readerThread; if (readerThread == null) { readerThread = new ReaderThread("Connection.ReaderThread:" + id); //$NON-NLS-1$ readerThread.setDaemon(true); } synchronized (readerThread) { if (!readerThread.isAlive()) { readerThread.start(); } } m_readerThread = readerThread; } /** * closes the socket immediately */ public void close() { if (m_readerThread != null) { synchronized (m_readerThread) { if (m_readerThread.isAlive()) { m_readerThread.interrupt(); } } } try { m_socket.close(); } catch (IOException ioe) { getLogger().debug("io error closing a socket", ioe); //$NON-NLS-1$ } } /** * Adds a listener for received messages. An instance will not registered * twice. * @param messageHandler - the listener to register, null objects are ignored */ public void addMessageHandler(IMessageHandler messageHandler) { if (messageHandler != null) { synchronized (m_messageHandlers) { m_messageHandlers.add(messageHandler); } } } /** * Removes the given messageHandler. * @param messageHandler - the listener to remove, null objects are ignored */ public void removeMessageHandler(IMessageHandler messageHandler) { if (messageHandler != null) { synchronized (m_messageHandlers) { m_messageHandlers.remove(messageHandler); } } } /** * Adds an listener to notify in case of errors. An instance will not * registered twice. * @param errorHandler - the listener to register, null objects are ignored */ public void addErrorHandler(IErrorHandler errorHandler) { if (errorHandler != null) { synchronized (m_errorHandlers) { m_errorHandlers.add(errorHandler); } } } /** * Removes the given errorHandler. * @param errorHandler - the listener to remove, null objects are ignored */ public void removeErrorHandler(IErrorHandler errorHandler) { if (errorHandler != null) { synchronized (m_errorHandlers) { m_errorHandlers.remove(errorHandler); } } } /** * @return Returns the exceptionHandler. */ public synchronized IExceptionHandler getExceptionHandler() { return m_exceptionHandler; } /** * @param exceptionHandler The exceptionHandler to set. */ public synchronized void setExceptionHandler( IExceptionHandler exceptionHandler) { m_exceptionHandler = exceptionHandler; } /** * @return the remote IP address for this connection, or * <code>null</code> if not connected. */ public InetAddress getAddress() { return m_socket.getInetAddress(); } /** * A synchronized method for sending messages. If an IO error occurs, the * error handlers will be notified with sendFailed, shutDown AND an * IOException will be thrown. In case of a serialization error the error * handler will be notified with sendFailed(). The header is filled with the * message length * @param header - the header for the message, must not be null * @param message - the message to send, must not be null * @throws IOException - if the message could not send due to an IOException * @throws IllegalArgumentException - if the given message is null */ public synchronized void send(MessageHeader header, String message) throws IOException, IllegalArgumentException { // check parameter Validate.notNull(header, "Message header must not be null"); //$NON-NLS-1$ Validate.notNull(message, "Message must not be null"); //$NON-NLS-1$ try { header.setMessageLength(message.length()); String serializedHeader = m_headerSerializer.serialize(header); // create a buffered message writer MessageWriter writer = new MessageWriter(new OutputStreamWriter( m_outputStream, IO_STREAM_ENCODING)); // write header writer.write(MessageHeader.HEADER_START); writer.write(StringConstants.EMPTY + serializedHeader.length()); writer.newLine(); writer.write(serializedHeader); // write message writer.write(message); writer.flush(); if (getLogger().isInfoEnabled()) { getLogger().info("sent to " + m_socket.getRemoteSocketAddress() + " message with header: " + serializedHeader); //$NON-NLS-1$ //$NON-NLS-2$ } if (getLogger().isDebugEnabled()) { getLogger().debug("sent message: " + message); //$NON-NLS-1$ } } catch (IOException ioe) { getLogger().error("send failed", ioe); //$NON-NLS-1$ fireSendFailed(message, header); fireShutDown(); throw ioe; } catch (SerialisationException se) { getLogger().error("serialisation of " //$NON-NLS-1$ + header.toString() + "failed", se); //$NON-NLS-1$ fireSendFailed(message, header); } } /** * A synchronized method for notifying the error listeners with sendFailed() * @param message - the message which should be send * @param header - the header of the message */ private synchronized void fireSendFailed(String message, MessageHeader header) { if (getLogger().isDebugEnabled()) { getLogger().debug("firing send failed, message=" + message); //$NON-NLS-1$ } Iterator iter = ((HashSet)((HashSet)m_errorHandlers).clone()) .iterator(); while (iter.hasNext()) { try { ((IErrorHandler)iter.next()).sendFailed(header, message); } catch (Throwable t) { getLogger().error("Exception while calling listener", t); //$NON-NLS-1$ } } } /** * A synchronized method for notifying the error listeners with shutDown(). */ private synchronized void fireShutDown() { if (!m_shutDownFired) { getLogger().debug("firing shutdown"); //$NON-NLS-1$ Iterator iter = ((HashSet)((HashSet)m_errorHandlers).clone()) .iterator(); while (iter.hasNext()) { try { ((IErrorHandler)iter.next()).shutDown(); } catch (Throwable t) { getLogger().error("Exception while calling listener", t); //$NON-NLS-1$ } } // don't fire more than once m_shutDownFired = true; } else { getLogger().debug("shutdown already fired"); //$NON-NLS-1$ } } /** * A synchronized method for notifying the message handlers * @param header - the received message header * @param message - the received message */ private void fireMessageReceived(MessageHeader header, String message) { if (getLogger().isDebugEnabled()) { getLogger().debug("firing message received, message=" + message); //$NON-NLS-1$ } Iterator iter; synchronized (this) { iter = ((HashSet)((HashSet)m_messageHandlers).clone()).iterator(); } while (iter.hasNext()) { try { ((IMessageHandler)iter.next()).received(header, message); } catch (Throwable t) { getLogger().error("Exception while calling listener", t); //$NON-NLS-1$ } } } /** * A thread for reading from the inputStream of the socket. This thread * notifies the listeners. * @author BREDEX GmbH * @created 13.07.2004 * */ private class ReaderThread extends IsAliveThread { /** * default constructor * @param name identifies the thread */ public ReaderThread(String name) { super(name); } /** * {@inheritDoc} */ public void run() { while (!this.isInterrupted()) { String headerLengthToken = null; try { if (!waitForInput()) { return; } headerLengthToken = m_inputStreamReader.readLine(); int headerLength = Integer.parseInt(headerLengthToken); String headerString = readString(m_inputStreamReader, headerLength); if (getLogger().isInfoEnabled()) { getLogger().info("read header: " + headerString); //$NON-NLS-1$ } MessageHeader header = m_headerSerializer .deserialize(headerString); header.validateVersion(); String message = readString(m_inputStreamReader, header .getMessageLength()); // notify message handlers if (getLogger().isDebugEnabled()) { getLogger().debug("read message: " + message); //$NON-NLS-1$ } fireMessageReceived(header, message); } catch (IOException ioe) { // FIXME this is also used for stopping the AUT in a // regular way (pressing the AUT-Stop Button). // In a future release this should be handled in another way /* * exception while reading from input stream or writing to a * buffered StringWriter => m_logger the message and stop */ getLogger().debug("stopping reading either due to io exception or stopped AUT", ioe); //$NON-NLS-1$ fireShutDownAndFinish(); } catch (UnexpectedEofException e) { getLogger().error("unexpected end of file while reading message", e); //$NON-NLS-1$ close(); fireShutDownAndFinish(); } catch (NumberFormatException e) { getLogger().error("invalid header length token: " //$NON-NLS-1$ + headerLengthToken, e); } catch (InvalidHeaderVersionException ihve) { getLogger().error(ihve.getLocalizedMessage(), ihve); } catch (Throwable t) { getLogger().error("exception raised", t); //$NON-NLS-1$ final IExceptionHandler exceptionHandler = getExceptionHandler(); if (exceptionHandler != null) { if (!exceptionHandler.handle(t)) { // NOPMD by al on 3/19/07 1:44 PM // handling the exception returns false -> stop close(); fireShutDownAndFinish(); } } } } // expected shutdown fireShutDown(); } /** * Reads a string containing <code>length</code> characters from the * passed buffered reader. * @param reader The buffered socket reader * @param length The number of characters to read * @return The read string * @throws IOException If the read operation fails * @throws UnexpectedEofException If an end of file is encountered while reading length bytes of data. */ private String readString(BufferedReader reader, int length) throws IOException, UnexpectedEofException { if (getLogger().isDebugEnabled()) { getLogger().debug("readString len " + length); //$NON-NLS-1$ } char[] headerChars = new char[length]; int required = length; int filled = 0; while (required > 0) { int nread = reader.read(headerChars, filled, required); if (nread == -1) { getLogger().error("received message part before unexpected eof: " //$NON-NLS-1$ + String.valueOf(headerChars)); // this is really a (serious) error ! throw new UnexpectedEofException("after reading " + filled //$NON-NLS-1$ + " bytes of expected " //$NON-NLS-1$ + length + " bytes of string"); //$NON-NLS-1$ } filled += nread; required -= nread; } return String.valueOf(headerChars); } /** * waits for input, the sign MesageHeader.HEADER_START <br> * Partially transmitted messages are logged. * @return true if the reading process should continue, false otherwise * @throws IOException if an IO errors occurs while reading from the input stream */ private boolean waitForInput() throws IOException { int character = nextChar(); final boolean createLogMessage = getLogger().isDebugEnabled(); final StringWriter logMessage = new StringWriter(); // character must be the sign for a new message while (!this.isInterrupted() && (character != MessageHeader.HEADER_START)) { if (character == -1) { fireShutDownAndFinish(); if (this.isInterrupted()) { return false; } } character = nextChar(); if (createLogMessage) { logMessage.write(character); } } if (createLogMessage) { logMessage.flush(); getLogger().debug("received a portion of a message:" //$NON-NLS-1$ + logMessage.toString()); } return true; } /** * This method is a work around for some problems in the socket implementation. Sometimes * a broken connection is not detected. The timeout helps detecting some problems, but * there are still some left. * @return next char from socket * @throws SocketException in case of network error * @throws IOException in case of network error */ private int nextChar() throws SocketException, IOException { int oldTimeout = m_socket.getSoTimeout(); m_socket.setSoTimeout(5000); // loop every 5000 ms boolean loop = false; int character = -1; try { do { try { loop = false; character = m_inputStreamReader.read(); } catch (InterruptedIOException e) { loop = true; } } while (loop && !isInterrupted()); } finally { m_socket.setSoTimeout(oldTimeout); } return character; } /** * end of stream is reached, so stop this thread, */ private void fireShutDownAndFinish() { this.interrupt(); fireShutDown(); } } /** * Clears the list of error handlers and the list of message handlers. */ public void clearListeners() { m_errorHandlers.clear(); m_messageHandlers.clear(); } /** * @return the logger */ public ConfigurableLogger getLogger() { return m_logger; } }