/* * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.shell.remoteui; import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message; import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Failure; import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.MessageType; import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Request; import com.google.gwt.dev.shell.remoteui.RemoteMessageProto.Message.Response; import com.google.gwt.dev.util.Callback; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Responsible for exchanging requests and responses between services. */ public class MessageTransport { /** * A callback that is invoked when errors occur at the transport level. */ public interface ErrorCallback { /** * Called when an exception occurs when attempting to send a response * message. */ void onResponseException(Exception e); /** * Called when the transport terminates due to an exception. * * @param e The exception that led to the termination */ void onTermination(Exception e); } /** * An exception that is generated by the transport when it receives a failure * message from the server in response to executing a client request. */ @SuppressWarnings("serial") public class RequestException extends Exception { private final Failure failureMessage; /** * Create a new instance with the given failure message. The exception's * message will be set to {@link Failure#getMessage()}. * * @param failureMessage The failure message returned by the server */ public RequestException(Failure failureMessage) { super(failureMessage.getMessage()); this.failureMessage = failureMessage; } /** * Gets the failure message returned by the server. */ public Failure getFailureMessage() { return failureMessage; } } class PendingRequest extends PendingSend { private final Callback<Response> callback; private final Message message; PendingRequest(Message message, Callback<Response> callback) { this.message = message; this.callback = callback; } @Override void failed(Exception e) { assert e != null; pendingRequestMap.remove(message.getMessageId()); callback.onError(e); } @Override void send(OutputStream outputStream) throws IOException { int messageId = message.getMessageId(); pendingRequestMap.put(messageId, this); message.writeDelimitedTo(outputStream); } /** * Sets the response that was received from the server, and signals the * thread that is waiting on the response. * * @param responseMessage the server's response * @throws InterruptedException */ void setResponse(Response responseMessage) { assert (responseMessage != null); callback.onDone(responseMessage); } } static class PendingRequestMap { private final Lock mapLock = new ReentrantLock(); private boolean noMoreAdds; private final Map<Integer, PendingRequest> requestIdToPendingServerRequest = new HashMap<Integer, PendingRequest>(); public void blockAdds(Exception e) { mapLock.lock(); try { noMoreAdds = true; for (PendingRequest pendingRequest : requestIdToPendingServerRequest.values()) { pendingRequest.failed(e); } } finally { mapLock.unlock(); } } public PendingRequest remove(int requestId) { mapLock.lock(); try { return requestIdToPendingServerRequest.remove(requestId); } finally { mapLock.unlock(); } } void put(int requestId, PendingRequest pendingServerRequest) { mapLock.lock(); try { if (noMoreAdds) { pendingServerRequest.failed(new IllegalStateException( "InputStream is closed")); } else { requestIdToPendingServerRequest.put(requestId, pendingServerRequest); } } finally { mapLock.unlock(); } } } class PendingResponse extends PendingSend { Message message; public PendingResponse(Message message) { this.message = message; } @Override public void failed(Exception e) { if (errorCallback != null) { errorCallback.onResponseException(e); } } @Override public void send(OutputStream outputStream) throws IOException { message.writeDelimitedTo(outputStream); } } abstract class PendingSend { abstract void failed(Exception e); abstract void send(OutputStream outputStream) throws IOException; } /** * A callable that does nothing. */ private static final Callable<Response> DUMMY_CALLABLE = new Callable<Response>() { public Response call() throws Exception { return null; } }; private final InputStream inputStream; private final AtomicBoolean isStarted = new AtomicBoolean(false); private final AtomicInteger nextMessageId = new AtomicInteger(); private final OutputStream outputStream; private final PendingRequestMap pendingRequestMap = new PendingRequestMap(); private final RequestProcessor requestProcessor; private final LinkedBlockingQueue<PendingSend> sendQueue = new LinkedBlockingQueue<PendingSend>(); private final ErrorCallback errorCallback; /** * Create a new instance using the given streams and request processor. * * @param inputStream an input stream for reading messages * @param outputStream an output stream for writing messages * @param requestProcessor a callback interface for handling remote client * requests * @param terminationCallback a callback that is invoked when the transport * terminates */ public MessageTransport(final InputStream inputStream, final OutputStream outputStream, RequestProcessor requestProcessor, ErrorCallback errorCallback) { this.requestProcessor = requestProcessor; this.errorCallback = errorCallback; this.inputStream = inputStream; this.outputStream = outputStream; } /** * Asynchronously executes the request on a remote server. * * @param requestMessage The request to execute * * @return a {@link Future} that can be used to access the server's response */ public Future<Response> executeRequestAsync(Request requestMessage) { Message.Builder messageBuilder = Message.newBuilder(); int messageId = nextMessageId.getAndIncrement(); messageBuilder.setMessageId(messageId); messageBuilder.setMessageType(Message.MessageType.REQUEST); messageBuilder.setRequest(requestMessage); Message message = messageBuilder.build(); class FutureTaskExtension extends FutureTask<Response> { private FutureTaskExtension() { super(DUMMY_CALLABLE); } @Override public void set(Response v) { super.set(v); } @Override public void setException(Throwable t) { super.setException(t); } } final FutureTaskExtension future = new FutureTaskExtension(); PendingRequest pendingRequest = new PendingRequest(message, new Callback<Response>() { public void onDone(Response result) { future.set(result); } public void onError(Throwable t) { future.setException(t); } }); sendQueue.add(pendingRequest); return future; } /** * Asynchronously executes the request on a remote server. The callback will * generally be called by another thread. Memory consistency effects: actions * in a thread prior to calling this method happen-before the callback is * invoked. * * @param requestMessage The request to execute * @param callback The callback to invoke when the response is received */ public void executeRequestAsync(Request requestMessage, Callback<Response> callback) { Message.Builder messageBuilder = Message.newBuilder(); int messageId = nextMessageId.getAndIncrement(); messageBuilder.setMessageId(messageId); messageBuilder.setMessageType(Message.MessageType.REQUEST); messageBuilder.setRequest(requestMessage); Message message = messageBuilder.build(); PendingRequest pendingRequest = new PendingRequest(message, callback); sendQueue.add(pendingRequest); } /** * Starts up the message transport. The message transport creates its own * threads, so it is not necessary to invoke this method from a separate * thread. * * Closing either stream will cause the termination of the transport. */ public void start() { if (isStarted.getAndSet(true)) { return; } // This thread terminates on interruption or IO failure Thread messageProcessingThread = new Thread(new Runnable() { public void run() { try { while (true) { Message message = Message.parseDelimitedFrom(inputStream); // TODO: This is where we would do a protocol check processMessage(message); } } catch (IOException e) { terminateDueToException(e); } catch (InterruptedException e) { terminateDueToException(e); } } }); messageProcessingThread.start(); // This thread only terminates if it is interrupted Thread sendThread = new Thread(new Runnable() { public void run() { while (true) { try { PendingSend pendingSend = sendQueue.take(); try { pendingSend.send(outputStream); } catch (IOException e) { pendingSend.failed(e); } } catch (InterruptedException e) { break; } } } }); sendThread.setDaemon(true); sendThread.start(); } private void processClientRequest(int messageId, Request request) throws InterruptedException { Message.Builder messageBuilder = Message.newBuilder(); messageBuilder.setMessageId(messageId); Response response = null; try { messageBuilder.setMessageType(Message.MessageType.RESPONSE); response = requestProcessor.execute(request); messageBuilder.setResponse(response); } catch (Exception e) { messageBuilder.setMessageType(Message.MessageType.FAILURE); Message.Failure.Builder failureMessage = Message.Failure.newBuilder(); failureMessage.setMessage(e.getLocalizedMessage() != null ? e.getLocalizedMessage() : e.getClass().getName()); StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw, true)); failureMessage.setStackTrace(sw.getBuffer().toString()); messageBuilder.setFailure(failureMessage); } PendingResponse pendingResponse = new PendingResponse( messageBuilder.build()); sendQueue.put(pendingResponse); } private void processFailure(int messageId, Failure failure) { PendingRequest pendingServerRequest = pendingRequestMap.remove(messageId); if (pendingServerRequest != null) { pendingServerRequest.failed(new RequestException(failure)); } } private void processMessage(final Message message) throws InterruptedException { MessageType messageType = message.getMessageType(); if (messageType == null) { processUnknownMessageType(message.getMessageId(), "unknown"); return; } switch (messageType) { case RESPONSE: { processServerResponse(message.getMessageId(), message.getResponse()); break; } case REQUEST: { processClientRequest(message.getMessageId(), message.getRequest()); break; } case FAILURE: { processFailure(message.getMessageId(), message.getFailure()); break; } default: { processUnknownMessageType(message.getMessageId(), messageType.name()); break; } } } private void processServerResponse(int messageId, Response response) { PendingRequest pendingServerRequest = pendingRequestMap.remove(messageId); if (pendingServerRequest != null) { pendingServerRequest.setResponse(response); } } private void processUnknownMessageType(int messageId, String messageTypeName) throws InterruptedException { Message.Builder messageBuilder = Message.newBuilder(); messageBuilder.setMessageId(messageId); messageBuilder.setMessageType(Message.MessageType.FAILURE); Message.Failure.Builder failureMessage = Message.Failure.newBuilder(); StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("Unknown message type '"); stringBuffer.append(messageTypeName); stringBuffer.append("'. Known message types are: "); for (MessageType type : MessageType.values()) { stringBuffer.append(type.name()); stringBuffer.append(" "); } failureMessage.setMessage(stringBuffer.toString()); messageBuilder.setFailure(failureMessage); PendingResponse pendingResponse = new PendingResponse( messageBuilder.build()); sendQueue.put(pendingResponse); } private void terminateDueToException(Exception e) { pendingRequestMap.blockAdds(e); if (errorCallback != null) { errorCallback.onTermination(e); } } }