/** * 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 org.waveprotocol.box.server.rpc; import com.google.protobuf.Message; import com.google.protobuf.RpcCallback; import com.google.protobuf.RpcController; /** * Implements the client end-point of a wave server RPC connection. This class * implements {{@link #reset} and is reusable across multiple RPC calls backed * onto the same ClientRpcChannel. * * TODO: This class is not currently thread-safe and has some * concurrency issues. * * */ class ClientRpcController implements RpcController { enum Status { PENDING, ACTIVE, COMPLETE } /** * Internal status class to manage the state of a specific outgoing RPC. */ static class RpcState { private final ClientRpcChannel creator; private final boolean isStreamingRpc; private final RpcCallback<Message> callback; private final Runnable cancelRpc; private boolean complete = false; private boolean failed = false; private String errorText = null; RpcState(ClientRpcChannel creator, boolean isStreamingRpc, RpcCallback<Message> callback, Runnable cancelRpc) { this.creator = creator; this.isStreamingRpc = isStreamingRpc; this.callback = callback; this.cancelRpc = cancelRpc; } } // The ClientRpcChannel instance that owns this class. private final ClientRpcChannel owner; // Represents the current call, or null if this is a pending controller. private RpcState state = null; /** * Package-public constructor, to be invoked by instances of ClientRpcChannel. */ ClientRpcController(ClientRpcChannel owner) { this.owner = owner; state = null; } /** * Returns the current status of this class in terms of the {@link Status} * enum. */ Status status() { return state == null ? Status.PENDING : (state.complete ? Status.COMPLETE : Status.ACTIVE); } /** * Assert that this controller is in the given status. */ private void checkStatus(Status statusToAssert) { Status currentStatus = status(); if (!currentStatus.equals(statusToAssert)) { throw new IllegalStateException("Controller expected status " + statusToAssert + ", was " + currentStatus); } } /** * Configure this RpcController with a new RpcStatus instance. */ void configure(RpcState state) { checkStatus(Status.PENDING); if (this.state != null) { throw new IllegalStateException("Can't configure this RPC, already configured."); } else if (!owner.equals(state.creator)) { throw new IllegalArgumentException("Should only be configured by " + owner + ", configuration attempted by " + state.creator); } this.state = state; } /** * Provide a response to this RpcController. Intercepts valid completion * conditions in order to mark a RPC as complete. Passes through all messages * to the internal callback for the current RPC invocation. */ void response(Message message) { checkStatus(Status.ACTIVE); // Any message will complete a normal RPC, whereas only a null message will // end a streaming RPC. if (!state.isStreamingRpc) { if (message == null) { // The server end-point should not actually allow non-streaming RPCs // to call back with null messages - we should never get here. throw new IllegalStateException("Normal RPCs should not be completed early."); } else { // Normal RPCs will complete on any valid incoming message. state.complete = true; } } else if (message == null) { // Complete this streaming RPC with this blank message. state.complete = true; } try { state.callback.run(message); } catch (RuntimeException e) { e.printStackTrace(); } } /** * Indicate that the RPC has failed. This requires that the RPC is currently * active, and marks this RPC as complete. */ void failure(String errorText) { checkStatus(Status.ACTIVE); state.complete = true; state.failed = true; state.errorText = errorText; // Hint to the internal callback that this RPC is finished (Normal RPCs // will always understand this as an error case, whereas streaming RPCs // will have to check their controller). state.callback.run(null); } @Override public String errorText() { return failed() ? state.errorText : null; } @Override public boolean failed() { checkStatus(Status.COMPLETE); return state.failed; } @Override public boolean isCanceled() { throw new UnsupportedOperationException("Server-side method of RpcController only."); } @Override public void notifyOnCancel(RpcCallback<Object> callback) { throw new UnsupportedOperationException("Server-side method of RpcController only."); } @Override public void reset() { checkStatus(Status.COMPLETE); state = null; } @Override public void setFailed(String reason) { throw new UnsupportedOperationException("Server-side method of RpcController only."); } @Override public void startCancel() { Status status = status(); if (status == Status.PENDING) { throw new IllegalStateException("Can't cancel this RPC, not currently active."); } else if (status == Status.COMPLETE) { // We drop these requests silently - since there is no way for the client // to know whether the RPC has finished while they are setting up their // cancellation. } else { state.cancelRpc.run(); } } }