/* * Created on May 12, 2006 */ package ecologylab.oodss.distributed.client; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.BindException; import java.net.InetSocketAddress; import java.net.PortUnreachableException; import java.net.Socket; import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.ClosedChannelException; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import ecologylab.collections.Scope; import ecologylab.generic.Debug; import ecologylab.generic.Generic; import ecologylab.generic.StringTools; import ecologylab.oodss.distributed.common.ClientConstants; import ecologylab.oodss.distributed.common.LimitedInputStream; import ecologylab.oodss.distributed.exception.MessageTooLargeException; import ecologylab.oodss.distributed.impl.MessageWithMetadata; import ecologylab.oodss.distributed.impl.MessageWithMetadataPool; import ecologylab.oodss.distributed.impl.PreppedRequest; import ecologylab.oodss.distributed.impl.PreppedRequestPool; import ecologylab.oodss.exceptions.BadClientException; import ecologylab.oodss.messages.DisconnectRequest; import ecologylab.oodss.messages.InitConnectionRequest; import ecologylab.oodss.messages.InitConnectionResponse; import ecologylab.oodss.messages.RequestMessage; import ecologylab.oodss.messages.ResponseMessage; import ecologylab.oodss.messages.SendableRequest; import ecologylab.oodss.messages.ServiceMessage; import ecologylab.oodss.messages.UpdateMessage; import ecologylab.serialization.SIMPLTranslationException; import ecologylab.serialization.SimplTypesScope; import ecologylab.serialization.formatenums.Format; import ecologylab.serialization.formatenums.StringFormat; /** * Services Client using NIO; a major difference with the NIO version is state tracking. Since the * sending methods do not wait for the server to return. * * This object will listen for incoming messages from the server, and will send any messages that it * receives on its end. * * Since the underlying implementation is TCP/IP, messages sent should be sent in order, and the * responses should match that order. * * Another major difference between this and the non-NIO version of ServicesClient is that it is * StartAndStoppable. * * @author Zachary O. Dugas Toups (zach@ecologylab.net) */ public class NIOClient<S extends Scope> extends Debug implements ClientConstants { protected String serverAddress; /** * stores the sequence of characters read from the header of an incoming message, may need to * persist across read calls, as the entire header may not be sent at once. */ private final StringBuilder currentHeaderSequence = new StringBuilder(); /** * stores the sequence of characters read from the header of an incoming message and identified as * being a key for a header entry; may need to persist across read calls. */ private final StringBuilder currentKeyHeaderSequence = new StringBuilder(); private volatile boolean blockingRequestPending = false; private final Queue<MessageWithMetadata<ServiceMessage, Object>> blockingResponsesQueue = new LinkedBlockingQueue<MessageWithMetadata<ServiceMessage, Object>>(); protected final LinkedBlockingQueue<PreppedRequest> requestsQueue = new LinkedBlockingQueue<PreppedRequest>(); /** * A map that stores all the requests that have not yet gotten responses. Maps UID to * RequestMessage. */ protected final Map<Long, PreppedRequest> unfulfilledRequests = new HashMap<Long, PreppedRequest>(); /** * The number of times a call to reconnect() should attempt to contact the server before giving up * and calling stop(). */ protected int reconnectAttempts = RECONNECT_ATTEMPTS; /** The number of milliseconds to wait between reconnect attempts. */ protected int waitBetweenReconnectAttempts = WAIT_BEWTEEN_RECONNECT_ATTEMPTS; private String sessionId = null; protected ReconnectBlocker blocker = null; /** * selectInterval is passed to select() when it is called in the run loop. It is set to 0 * indicating that the loop should block until the selector picks up something interesting. * However, if this class is subclassed, it is possible to modify this value so that the select() * will only block for the number of ms supplied by this field. Thus, it is possible (by also * subclassing the sendData() method) to have this send data on an interval, and then select. */ protected long selectInterval = 0; protected boolean isSending = false; /** * Contains the unique identifier for the next message that the client will send. */ private long uidIndex = 1; private int endOfFirstHeader = -1; protected boolean maybeEndSequence = false; private int uidOfCurrentMessage = -1; /** * Counts how many characters still need to be extracted from the incomingMessageBuffer before * they can be turned into a message (based upon the HTTP header). A value of -1 means that there * is not yet a complete header, so no length has been determined (yet). */ private int contentLengthRemaining = -1; /** * Whether or not to allow server to reply using compression */ private boolean allowCompression = false; /** * Whether or not to send compressed requests. */ private boolean sendCompressed = false; /** * Stores the key-value pairings from a parsed HTTP-like header on an incoming message. */ protected final HashMap<String, String> headerMap = new HashMap<String, String>(); protected Socket thisSocket = null; protected final PreppedRequestPool pRequestPool; protected final MessageWithMetadataPool<ServiceMessage, Object> responsePool = new MessageWithMetadataPool<ServiceMessage, Object>( 2, 4); private String contentEncoding; private List<ClientStatusListener> clientStatusListeners = null; private byte[] readBuffer = new byte[1024]; private ByteBuffer firstByteReadBuffer = ByteBuffer.wrap(readBuffer, 0, 1); private CharBuffer firstCharBuffer = CharBuffer .allocate(1); private OutputStream socketOutputStream; private OutputStreamWriter socketWriter; private boolean maybeLineEnd = false; private int portNumber; private InputStream socketInputStream; private boolean running = false; private Thread reader; private Thread writer; protected S objectRegistry; private SimplTypesScope translationScope; protected CharsetDecoder decoder = CHARSET .newDecoder(); public NIOClient(String serverAddress, int portNumber, SimplTypesScope messageSpace, S objectRegistry) throws IOException { super(); this.portNumber = portNumber; this.objectRegistry = objectRegistry; this.translationScope = messageSpace; pRequestPool = new PreppedRequestPool(2, 4, 1024); this.serverAddress = serverAddress; } @Deprecated public NIOClient(String serverAddress, int portNumber, SimplTypesScope messageSpace, S objectRegistry, int maxMessageLengthChars) throws IOException { this(serverAddress, portNumber, messageSpace, objectRegistry); } /** * If this client is not already connected, connects to the specified serverAddress on the * specified portNumber, then calls start() to begin listening for server responses and processing * them, then sends handshake data and establishes the session id. * * @see ecologylab.oodss.distributed.legacy.ServicesClientBase#connect() */ public boolean connect(int timeoutMilli) { debug(5, "initializing connection..."); if (this.connectImpl()) { debug(5, "starting listener thread..."); this.start(); // now send first handshake message ResponseMessage initResponse = null; try { initResponse = this.sendMessage(new InitConnectionRequest(this.sessionId), timeoutMilli); } catch (MessageTooLargeException e) { // this shouldn't be able to happen e.printStackTrace(); } if (initResponse instanceof InitConnectionResponse) { if (this.sessionId == null) { this.sessionId = ((InitConnectionResponse) initResponse).getSessionId(); debug(3, "new session: " + this.sessionId); } else if (this.sessionId == ((InitConnectionResponse) initResponse).getSessionId()) { debug(3, "reconnected and restored previous connection: " + this.sessionId); } else { String newId = ((InitConnectionResponse) initResponse).getSessionId(); debug("unable to restore previous session, " + this.sessionId + "; new session: " + newId); this.unableToRestorePreviousConnection(this.sessionId, newId); this.sessionId = newId; } // this.thisSocket.keyFor(this.selector).attach(this.sessionId); } } this.notifyOfStatusChange(this.connected()); debug(5, "connected? " + this.connected()); return connected(); } public boolean connect() { return connect(-1); } /** * Connect to the server (if not already connected). Return connection status. * * @return True if connected, false if not. */ private boolean connectImpl() { return connected() ? true : createConnection(); } /** * Sets the UID for request (if necessary), enqueues it then registers write interest for the * NIOClient's selection key and calls wakeup() on the selector. * * @param request * @throws SIMPLTranslationException */ protected PreppedRequest prepareAndEnqueueRequestForSending(SendableRequest request) throws SIMPLTranslationException, MessageTooLargeException { long uid = this.generateUid(); PreppedRequest pReq = null; pReq = this.pRequestPool.acquire(); // fill requestBuffer SimplTypesScope.serialize(request, pReq.getRequest(), StringFormat.XML); pReq.setUid(uid); pReq.setDisposable(request.isDisposable()); if (pReq != null) enqueueRequestForSending(pReq); return pReq; } protected void enqueueRequestForSending(PreppedRequest request) { synchronized (requestsQueue) { requestsQueue.add(request); } } public void disconnect(boolean waitForResponses) { int attemptsCounter = 0; while (this.requestsRemaining() > 0 && this.connected() && waitForResponses && attemptsCounter++ < 10) { debug(5, "******************* Request queue not empty, finishing " + requestsRemaining() + " messages before disconnecting (attempt " + attemptsCounter + ")..."); synchronized (this) { try { wait(100); } catch (InterruptedException e) { e.printStackTrace(); } } } debug(5, "* starting disconnect process..."); try { if (connected()) { debug(5, "** currently connected..."); while (waitForResponses && connected() && !this.shutdownOK() && attemptsCounter-- > 0) { debug(5, "*** " + this.unfulfilledRequests.size() + " requests still pending response from server (attempt " + attemptsCounter + ")."); debug(5, "*** connected: " + connected()); synchronized (this) { try { wait(100); } catch (InterruptedException e) { e.printStackTrace(); } } } // disconnect properly... if (this.sessionId != null) { debug(5, "**** session still active; handling disconnecting messages..."); this.handleDisconnectingMessages(); this.sessionId = null; } } } finally { stop(); nullOut(); } } /** * Hook method for subclasses to provide specific disconnect messages. For example, authenticating * clients will want to log out. */ protected void handleDisconnectingMessages() { debug(5, "************** sending disconnect request"); try { this.sendMessage(DisconnectRequest.REUSABLE_INSTANCE, 10000); } catch (MessageTooLargeException e) { // this shouldn't be able to happen, unless the maximum request size // gets set below the size of a DisconnectRequest e.printStackTrace(); } } /** * @return */ protected boolean shutdownOK() { return !(this.unfulfilledRequests.size() > 0); } protected void nullOut() { if (thisSocket != null) { synchronized (thisSocket) { thisSocket = null; } } } public boolean connected() { return (thisSocket != null) && thisSocket.isConnected(); } /** * Side effect of calling start(). */ protected boolean createConnection() { if(!connected()) { try { // create the channel and connect it to the server debug(5, "creating socket!"); thisSocket = new Socket(); thisSocket.connect(new InetSocketAddress(serverAddress, portNumber)); socketOutputStream = new BufferedOutputStream(thisSocket.getOutputStream()); socketWriter = new OutputStreamWriter(socketOutputStream, CHARSET); socketInputStream = new BufferedInputStream(thisSocket.getInputStream()); } catch (BindException e) { debug(5, "Couldnt create socket connection to server - " + serverAddress + ":" + portNumber + " - " + e); nullOut(); } catch (PortUnreachableException e) { debug(5, "Server is alive, but has no daemon on portNumber " + portNumber + ": " + e); nullOut(); } catch (SocketException e) { debug(5, "Server '" + serverAddress + "' unreachable: " + e); nullOut(); } catch (IOException e) { debug(5, "Bad response from server: " + e); nullOut(); } } return connected(); } /** * Hook method to allow subclasses to deal with a failed restore after disconnect. This should be * a rare occurance, but some sublcasses may need to deal with this case specifically. * * @param oldId * - the previous session id. * @param newId * - the new session id given by the server after reconnect. */ protected void unableToRestorePreviousConnection(String oldId, String newId) { } /** * Sends request, but does not wait for the response. The response gets processed later in a * non-stateful way by the run method. * * @param request * the request to send to the server. * * @return the UID of request. */ public PreppedRequest nonBlockingSendMessage(RequestMessage request) throws IOException, MessageTooLargeException { if (connected()) { try { return this.prepareAndEnqueueRequestForSending(request); } catch (SIMPLTranslationException e) { error("error translating message; returning null"); e.printStackTrace(); return null; } } else { throw new IOException("Not connected to server."); } } /** * Blocking send. Sends the request and waits infinitely for the response, which it returns. * * @throws MessageTooLargeException * * @see ecologylab.oodss.distributed.legacy.ServicesClientBase#sendMessage(ecologylab.oodss.messages.RequestMessage) */ public synchronized ResponseMessage sendMessage(RequestMessage request) throws MessageTooLargeException { return this.sendMessage(request, -1); } /** * Blocking send with timeout. Sends the request and waits timeOutMillis milliseconds for the * response, which it returns. sendMessage(RequestMessage, int) will return null if no message was * received in time. * * @param request * @param timeOutMillis * @return * @throws MessageTooLargeException */ public synchronized ResponseMessage sendMessage(SendableRequest request, int timeOutMillis) throws MessageTooLargeException { MessageWithMetadata<ServiceMessage, Object> responseMessage = null; // notify the connection thread that we are waiting on a response blockingRequestPending = true; long currentMessageUid; boolean blockingRequestFailed = false; long startTime = System.currentTimeMillis(); int timeCounter = 0; try { currentMessageUid = this.prepareAndEnqueueRequestForSending(request).getUid(); } catch (SIMPLTranslationException e1) { error("error translating to XML; returning null"); e1.printStackTrace(); return null; } catch (MessageTooLargeException e) { blockingRequestPending = false; error("message too large to send"); e.printStackTrace(); throw e; } // wait to be notified that the response has arrived while (blockingRequestPending && !blockingRequestFailed) { if (timeOutMillis <= -1) { debug(5, "waiting on blocking request"); } try { if (timeOutMillis > -1) { wait(timeOutMillis); } else { wait(); } } catch (InterruptedException e) { e.printStackTrace(); Thread.interrupted(); } debug(5, "waking"); timeCounter += System.currentTimeMillis() - startTime; startTime = System.currentTimeMillis(); while ((blockingRequestPending) && (!blockingResponsesQueue.isEmpty())) { responseMessage = blockingResponsesQueue.poll(); if (responseMessage.getUid() == currentMessageUid) { debug(5, "got the right response: " + currentMessageUid); blockingRequestPending = false; blockingResponsesQueue.clear(); ResponseMessage respMsg = (ResponseMessage) responseMessage.getMessage(); // try // { // debug("response: " + respMsg.serialize().toString()); // } // catch (SIMPLTranslationException e) // { // e.printStackTrace(); // } responseMessage = responsePool.release(responseMessage); return respMsg; } responseMessage = responsePool.release(responseMessage); } if ((timeOutMillis > -1) && (timeCounter >= timeOutMillis) && (blockingRequestPending)) { blockingRequestFailed = true; } } if (blockingRequestFailed) { debug(5, "Request failed due to timeout!"); } return null; } private class SocketReader implements Runnable { @Override public void run() { while (running) { try { processReadData(socketInputStream); } catch (BadClientException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } private class SocketWriter implements Runnable { @Override public void run() { while (running) { try { createPacketFromMessageAndSend(requestsQueue.take()); } catch (InterruptedException e) { if (!running) { break; } } } } } public void start() { if (connected()) { running = true; reader = new Thread(new SocketReader()); reader.start(); writer = new Thread(new SocketWriter()); writer.start(); } } public void stop() { debug(5, "shutting down client listening thread."); running = false; try { if(thisSocket != null) thisSocket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } reader.interrupt(); writer.interrupt(); } /** * Returns the next request in the request queue and removes it from that queue. Sublcasses that * override the queue functionality will need to override this method. * * @return the next request in the request queue. */ protected PreppedRequest dequeueRequest() { return this.requestsQueue.poll(); } /** * Returns the number of requests remaining in the requests queue. Subclasses that override the * queue functionality will need to change this method accordingly. * * @return the size of the request queue. */ protected int requestsRemaining() { return this.requestsQueue.size(); } /** * Attempts to reconnect this client if it has been disconnected. After reconnecting, re-queues * all requests still in the unfulfilledRequests map. * * If the attempt to reconnect fails, reconnect() will attempt a number of times equal to * reconnectAttempts, waiting waitBetweenReconnectAttempts milliseconds between attempts. If all * such attempts fail, calls stop() on this to shut down the client. The client will then need to * be re-started manually. * */ protected void reconnect() { debug(5, "attempting to reconnect..."); int reconnectsRemaining = this.reconnectAttempts; if (reconnectsRemaining < 0) { reconnectsRemaining = 1; } while (!connected() && reconnectsRemaining > 0) { this.nullOut(); // attempt to connect, if failed, wait if (!this.connect() && --reconnectsRemaining > 0) { try { this.wait(this.waitBetweenReconnectAttempts); } catch (InterruptedException e) { e.printStackTrace(); } } } if (connected()) { synchronized (unfulfilledRequests) { List<PreppedRequest> rerequests = new LinkedList<PreppedRequest>( this.unfulfilledRequests.values()); Collections.sort(rerequests); for (PreppedRequest req : rerequests) { this.enqueueRequestForSending(req); } } } else { this.stop(); } } /** * Hook method to allow subclasses to deal with unfulfilled requests in their own way. * * Adds req to the unfulfilled requests map. * * @param req */ protected void addUnfulfilledRequest(PreppedRequest req) { synchronized (unfulfilledRequests) { this.unfulfilledRequests.put(req.getUid(), req); } } /** * Stores the request in the unfulfilledRequests map according to its UID, converts it to XML, * prepends the HTTP-like header, then writes it out to the channel. Then re-registers key for * reading. * * @param pReq */ private void createPacketFromMessageAndSend(PreppedRequest pReq) { StringBuilder outgoingReq = pReq.getRequest(); this.addUnfulfilledRequest(pReq); byte[] messageBytes = null; try { if (this.sendCompressed) { messageBytes = this.compress(outgoingReq); } else { messageBytes = this.encode(outgoingReq); } socketWriter.append(CONTENT_LENGTH_STRING); socketWriter.append(':'); socketWriter.append("" + messageBytes.length); socketWriter.append(HTTP_HEADER_LINE_DELIMITER); socketWriter.append(UNIQUE_IDENTIFIER_STRING); socketWriter.append(':'); socketWriter.append("" + pReq.getUid()); if (allowCompression) { socketWriter.append(HTTP_HEADER_LINE_DELIMITER); socketWriter.append(HTTP_ACCEPTED_ENCODINGS); } if (this.sendCompressed) { socketWriter.append(HTTP_HEADER_LINE_DELIMITER); socketWriter.append(HTTP_CONTENT_CODING); socketWriter.append(":"); socketWriter.append(HTTP_DEFLATE_ENCODING); } socketWriter.append(HTTP_HEADER_TERMINATOR); socketWriter.flush(); socketOutputStream.write(messageBytes); socketOutputStream.flush(); } catch (ClosedChannelException e) { debug(5, "connection severed; disconnecting and storing requests..."); } catch (NullPointerException e) { e.printStackTrace(); System.out.println("recovering."); } catch (CharacterCodingException e) { e.printStackTrace(); } catch (IOException e) { debug("connection severed; disconnecting..."); this.disconnect(false); } } private byte[] compress(StringBuilder src) throws IOException { ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(1024); DeflaterOutputStream zipStream = new DeflaterOutputStream(byteArrayStream); OutputStreamWriter encodingStream = new OutputStreamWriter(zipStream, CHARSET); encodingStream.append(src); encodingStream.flush(); zipStream.flush(); zipStream.finish(); byteArrayStream.flush(); return byteArrayStream.toByteArray(); } private byte[] encode(StringBuilder src) throws IOException { ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(1024); OutputStreamWriter encodingStream = new OutputStreamWriter(byteArrayStream, CHARSET); encodingStream.append(src); encodingStream.flush(); byteArrayStream.flush(); return byteArrayStream.toByteArray(); } private void processUpdate(UpdateMessage message) { message.processUpdate(objectRegistry); } /** * Converts incomingMessage to a ResponseMessage, then processes the response and removes its UID * from the unfulfilledRequests map. * * @param incomingMessage * @return * @throws SIMPLTranslationException */ private MessageWithMetadata<ServiceMessage, Object> processString( InputStream incomingMessageStream, int incomingUid) throws SIMPLTranslationException { /* * if (show(5)) debug("incoming message: " + incomingMessage); */ MessageWithMetadata<ServiceMessage, Object> response = translateXMLStringToServiceMessage( incomingMessageStream, incomingUid); if (response == null) { debug("ERROR: translation failed: "); } else { if (response.getMessage() instanceof ResponseMessage) { // perform the service being requested processResponse((ResponseMessage) response.getMessage()); synchronized (unfulfilledRequests) { PreppedRequest finishedReq = unfulfilledRequests.remove(response.getUid()); if (finishedReq != null) { // subclasses might choose not to use unfulfilledRequests; this // avoids problems with releasing resources; // NOTE -- it may be necessary to release elsewhere in this case. finishedReq = this.pRequestPool.release(finishedReq); } } } else if (response.getMessage() instanceof UpdateMessage) { processUpdate((UpdateMessage) response.getMessage()); } } return response; } public void disconnect() { disconnect(true); } /** * @param reconnectAttempts * the reconnectAttempts to set */ public void setReconnectAttempts(int reconnectAttempts) { this.reconnectAttempts = reconnectAttempts; } /** * @param waitBetweenReconnectAttempts * the waitBetweenReconnectAttempts to set */ public void setWaitBetweenReconnectAttempts(int waitBetweenReconnectAttempts) { this.waitBetweenReconnectAttempts = waitBetweenReconnectAttempts; } protected void clearSessionId() { this.sessionId = null; } /** * @see ecologylab.oodss.distributed.impl.NIONetworking#processReadData(java.lang.Object, * java.nio.channels.SocketChannel, byte[], int) */ protected void processReadData(InputStream inputStream) throws BadClientException { try { // look for HTTP header if (endOfFirstHeader == -1) { endOfFirstHeader = this.parseHeader(inputStream); } if (endOfFirstHeader == -1) { /* * no header yet; if it's too large, bad client; if it's not too large yet, just exit, it'll * get checked again when more data comes down the pipe */ return; } else { // we've read all of the header, and have it loaded into the // map; now we can use it if (contentLengthRemaining == -1) { try { // handle all header information here; delete it when // done here contentLengthRemaining = Integer.parseInt(this.headerMap.get(CONTENT_LENGTH_STRING)); if (headerMap.containsKey(UNIQUE_IDENTIFIER_STRING)) { uidOfCurrentMessage = Integer.parseInt(this.headerMap.get(UNIQUE_IDENTIFIER_STRING)); } contentEncoding = this.headerMap.get(HTTP_CONTENT_CODING); this.headerMap.clear(); } catch (NumberFormatException e) { e.printStackTrace(); contentLengthRemaining = -1; } } } /* * we have the end of the first header (otherwise we would have broken out earlier). If we * don't have the content length, something bad happened, because it should have been read. */ if (contentLengthRemaining == -1) { /* * if we still don't have the remaining length, then there was a problem */ return; } try { // buffer all incoming bytes from stream if (contentLengthRemaining > 0) { LimitedInputStream limitStream = new LimitedInputStream(inputStream, contentLengthRemaining); InflaterInputStream inflateStream = null; InputStream messageStream = null; if (contentEncoding != null && contentEncoding.equals(HTTP_DEFLATE_ENCODING)) { inflateStream = new InflaterInputStream(limitStream); messageStream = inflateStream; } else { messageStream = limitStream; } try { if (!this.blockingRequestPending) { // we process the read data into a response message, let it // perform its response, then dispose of // the // resulting MessageWithMetadata object this.responsePool.release(processString(messageStream, uidOfCurrentMessage)); } else { blockingResponsesQueue.add(processString(messageStream, uidOfCurrentMessage)); synchronized (this) { notify(); } } } catch (SIMPLTranslationException e) { e.printStackTrace(); } finally { while(limitStream.available() > 0) { limitStream.read(readBuffer); } contentLengthRemaining = -1; endOfFirstHeader = -1; } } } catch (NullPointerException e) { e.printStackTrace(); } } catch (CharacterCodingException e1) { e1.printStackTrace(); } catch (SocketException se) { this.stop(); } catch (IOException e) { e.printStackTrace(); } } /** * Increments the internal tracker of the next UID, and returns the current one. * * @return the current uidIndex. */ public synchronized long generateUid() { // return the current value of uidIndex, then increment. return uidIndex++; } /** * Use the ServicesClient and its NameSpace to do the translation. Can be overridden to provide * special functionalities * * @param messageString * @return * @throws XMLTranslationException */ protected MessageWithMetadata<ServiceMessage, Object> translateXMLStringToServiceMessage( InputStream inputStream, int incomingUid) throws SIMPLTranslationException { ServiceMessage resp = (ServiceMessage) this.translationScope.deserialize(inputStream, Format.XML, CHARSET); if (resp == null) { return null; } MessageWithMetadata<ServiceMessage, Object> retVal = this.responsePool.acquire(); retVal.setMessage(resp); retVal.setUid(incomingUid); return retVal; } /** * Process a ResponseMessage received from the server in response to a previously-sent * RequestMessage. * * @param responseMessageToProcess */ protected void processResponse(ResponseMessage responseMessageToProcess) { responseMessageToProcess.processResponse(objectRegistry); } public String getServer() { return this.serverAddress; } public void setServer(String serverAddress) { this.serverAddress = serverAddress; } /** * Returns the most recently used UID. * * @return the current uidIndex. */ public long getUidNoIncrement() { // return the current value of uidIndex return uidIndex; } /** * Check to see if the server is running. * * @return true if the server is running, false otherwise. */ public boolean isServerRunning() { debug(5, "checking availability of server"); boolean serverIsRunning = createConnection(); // we're just checking, don't keep the connection if (connected()) { debug(5, "server is running; disconnecting"); disconnect(); } return serverIsRunning; } /** * Try and connect to the server. If we fail, wait CONNECTION_RETRY_SLEEP_INTERVAL and try again. * Repeat ad nauseum. */ public void waitForConnect() { System.out.println("Waiting for a server on port " + portNumber); while (!connect()) { // try again soon Generic.sleep(WAIT_BEWTEEN_RECONNECT_ATTEMPTS); } } /** * Parses the header of an incoming set of characters (i.e. a message from a client to a server), * loading all of the HTTP-like headers into the given headerMap. * * If headerMap is null, this method will throw a null pointer exception. * * @param allIncomingChars * - the characters read from an incoming stream. * @param headerMap * - the map into which all of the parsed headers will be placed. * @return the length of the parsed header, or -1 if it was not yet found. * @throws IOException */ protected int parseHeader(InputStream incomingStream) throws IOException { // indicates that we might be at the end of the header char currentChar; synchronized (currentHeaderSequence) { StringTools.clear(currentHeaderSequence); StringTools.clear(currentKeyHeaderSequence); while (true) { int ret = incomingStream.read(readBuffer, 0, 1); if(ret != 1) { return -1; } firstByteReadBuffer.position(0); firstByteReadBuffer.limit(1); firstCharBuffer.clear(); decoder.decode(firstByteReadBuffer, firstCharBuffer, true); firstCharBuffer.flip(); currentChar = firstCharBuffer.charAt(0); switch (currentChar) { case (':'): /* * we have the end of a key; move the currentHeaderSequence into the * currentKeyHeaderSequence and clear it */ currentKeyHeaderSequence.append(currentHeaderSequence); StringTools.clear(currentHeaderSequence); maybeLineEnd = false; break; case ('\r'): /* * we have the end of a line; if there's a CRLF, then we have the end of the value * sequence or the end of the header. */ maybeLineEnd = true; break; case ('\n'): if (maybeLineEnd) { if (!maybeEndSequence) {// load the key/value pair headerMap.put(currentKeyHeaderSequence.toString().toLowerCase(), currentHeaderSequence.toString()); StringTools.clear(currentKeyHeaderSequence); StringTools.clear(currentHeaderSequence); } else { // end of the header maybeEndSequence = false; return 0; } maybeEndSequence = true; } maybeLineEnd = false; break; default: currentHeaderSequence.append(currentChar); maybeEndSequence = false; maybeLineEnd = false; break; } } } } public void setReconnectBlocker(ReconnectBlocker blocker) { this.blocker = blocker; } public class Reconnecter implements Runnable { @Override public void run() { if (blocker != null) blocker.reconnectBlock(); debug("Reconnecting in reconnector thread!"); reconnect(); } } public boolean allowsCompression() { return this.allowCompression; } public void allowCompression(boolean useCompression) { this.allowCompression = useCompression; } /** * Specifies whether or not to use request compression, most web servers don't allow receiving * compressed requests (i.e. Apache) * * @param useRequestCompression * whether or not to use compression when sending requests */ public void useRequestCompression(boolean useRequestCompression) { this.sendCompressed = useRequestCompression; } public boolean usesRequestCompression() { return this.sendCompressed; } public void addClientStatusListener(ClientStatusListener csl) { this.clientStatusListeners().add(csl); } public void removeClientStatusListener(ClientStatusListener csl) { this.clientStatusListeners().remove(csl); } private void notifyOfStatusChange(boolean newStatus) { if (this.clientStatusListeners != null) { for (ClientStatusListener csl : this.clientStatusListeners()) { csl.clientConnectionStatusChanged(newStatus); } } } private List<ClientStatusListener> clientStatusListeners() { if (this.clientStatusListeners == null) { synchronized (this) { if (this.clientStatusListeners == null) { this.clientStatusListeners = new LinkedList<ClientStatusListener>(); } } } return this.clientStatusListeners; } public void setPriority(int priority) { if (reader != null) { reader.setPriority(priority); } if (writer != null) { writer.setPriority(priority); } } public S getScope() { return objectRegistry; } }