/*
* Copyright 2009-2010 Brian S O'Neill
*
* 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.cojen.dirmi.io;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.cojen.dirmi.ClosedException;
import org.cojen.dirmi.RejectedException;
import org.cojen.dirmi.RemoteTimeoutException;
import org.cojen.dirmi.util.Timer;
import static org.cojen.dirmi.io.BasicChannelBrokerAcceptor.OPEN_REQUEST;
import static org.cojen.dirmi.io.BasicChannelBrokerAcceptor.CONNECT_REQUEST;
import static org.cojen.dirmi.io.BasicChannelBrokerAcceptor.CONNECT_RESPONSE;
import static org.cojen.dirmi.io.BasicChannelBrokerAcceptor.PING_REQUEST;
import static org.cojen.dirmi.io.BasicChannelBrokerAcceptor.PING_RESPONSE;
import static org.cojen.dirmi.io.BasicChannelBrokerAcceptor.ACCEPT_REQUEST;
import static org.cojen.dirmi.io.BasicChannelBrokerAcceptor.ACCEPT_CONFIRM_REQUEST;
import static org.cojen.dirmi.io.BasicChannelBrokerAcceptor.ACCEPT_SUCCESS_RESPONSE;
/**
* Paired with {@link BasicChannelBrokerAcceptor} to adapt a ChannelConnector
* into a ChannelBrokerConnector.
*
* @author Brian S O'Neill
*/
public class BasicChannelBrokerConnector implements ChannelBrokerConnector {
private final IOExecutor mExecutor;
private final ChannelConnector mConnector;
private final CloseableGroup<Broker> mConnectedBrokers;
public BasicChannelBrokerConnector(IOExecutor executor, ChannelConnector connector) {
mExecutor = executor;
mConnector = connector;
mConnectedBrokers = new CloseableGroup<Broker>();
}
@Override
public Object getRemoteAddress() {
return mConnector.getRemoteAddress();
}
@Override
public Object getLocalAddress() {
return mConnector.getLocalAddress();
}
@Override
public ChannelBroker connect() throws IOException {
mConnectedBrokers.checkClosed();
return connected(mConnector.connect(), null);
}
@Override
public ChannelBroker connect(long timeout, TimeUnit unit) throws IOException {
return timeout < 0 ? connect() : connect(new Timer(timeout, unit));
}
@Override
public ChannelBroker connect(Timer timer) throws IOException {
return connected(mConnector.connect(timer), timer);
}
@Override
public void connect(final Listener listener) {
mConnector.connect(new ChannelConnector.Listener() {
public void connected(Channel channel) {
if (mConnectedBrokers.isClosed()) {
listener.closed(new ClosedException());
}
ChannelBroker broker;
try {
broker = BasicChannelBrokerConnector.this.connected(channel, null);
} catch (IOException e) {
listener.failed(e);
return;
}
listener.connected(broker);
}
public void rejected(RejectedException e) {
listener.rejected(e);
}
public void failed(IOException e) {
listener.failed(e);
}
public void closed(IOException e) {
listener.closed(e);
}
});
}
@Override
public void close() {
mConnectedBrokers.close();
}
private ChannelBroker connected(Channel channel, Timer timer) throws IOException {
if (timer == null) {
timer = new Timer(15, TimeUnit.SECONDS);
}
try {
long id;
ChannelTimeout timeout = new ChannelTimeout(mExecutor, channel, timer);
try {
channel.getOutputStream().write(OPEN_REQUEST);
channel.flush();
id = new DataInputStream(channel.getInputStream()).readLong();
} finally {
timeout.cancel();
}
return new Broker(id, channel);
} catch (IOException e) {
channel.disconnect();
throw e;
}
}
private class Broker extends BasicChannelBroker {
// 0: unknown, 1: old, 2: new
private volatile int mProtocol;
Broker(long id, Channel control) throws RejectedException {
super(mExecutor, id, control);
mConnectedBrokers.add(this);
mControl.inputNotify(new Channel.Listener() {
public void ready() {
Channel control = mControl;
int op;
try {
op = control.getInputStream().read();
if (op != PING_REQUEST && op != CONNECT_REQUEST) {
if (op < 0) {
throw new ClosedException("Control channel is closed");
} else {
throw new IOException
("Invalid operation from control channel: " + op);
}
}
control.inputNotify(this);
} catch (IOException e) {
closed(e);
return;
}
if (op == PING_REQUEST) {
try {
if (cPingLogger != null) {
logPingMessage("Ping request from " + mControl);
}
control.getOutputStream().write(PING_RESPONSE);
control.getOutputStream().flush();
if (cPingLogger != null) {
logPingMessage("Ping response to " + mControl);
}
pinged();
} catch (IOException e) {
if (cPingLogger != null) {
logPingMessage("Ping response failure to " + mControl + ": " + e);
}
close(new ClosedException("Ping failure", e));
}
return;
}
Channel channel;
try {
channel = mConnector.connect();
DataOutputStream dout = new DataOutputStream(channel.getOutputStream());
dout.writeByte(CONNECT_RESPONSE);
dout.writeLong(mId);
dout.flush();
} catch (IOException e) {
closed(e);
return;
}
accepted(channel);
}
public void rejected(RejectedException e) {
// Safer to close if no threads.
// FIXME: Reconsider this choice, but always close if shutdown.
closed(e);
if (cPingLogger != null) {
logPingMessage("Ping check stopping for " + mControl + ": " + e);
}
}
public void closed(IOException e) {
Broker.this.close(e);
if (cPingLogger != null) {
logPingMessage("Ping check stopping for " + mControl + ": " + e);
}
}
});
}
@Override
public Channel connect() throws IOException {
return connect(new Timer(15, TimeUnit.SECONDS));
}
@Override
public Channel connect(long timeout, TimeUnit unit) throws IOException {
if (timeout < 0) {
Channel channel = mConnector.connect();
if (!sendRequest(channel)) {
// Retry with old protocol.
sendRequest(channel = mConnector.connect());
}
return channel;
} else {
return connect(new Timer(timeout, unit));
}
}
@Override
public Channel connect(Timer timer) throws IOException {
Channel channel = mConnector.connect
(RemoteTimeoutException.checkRemaining(timer), timer.unit());
ChannelTimeout timeoutTask =
new ChannelTimeout(mExecutor, channel,
RemoteTimeoutException.checkRemaining(timer),
timer.unit());
try {
if (!sendRequest(channel)) {
// Retry with old protocol.
channel = mConnector.connect
(RemoteTimeoutException.checkRemaining(timer), timer.unit());
sendRequest(channel);
}
return channel;
} finally {
timeoutTask.cancel();
}
}
@Override
public void connect(final ChannelConnector.Listener listener) {
mConnector.connect(new ChannelConnector.Listener() {
public void connected(Channel channel) {
try {
if (!sendRequest(channel)) {
// Retry with old protocol.
channel = connect(15, TimeUnit.SECONDS);
}
} catch (IOException e) {
listener.failed(e);
return;
}
listener.connected(channel);
}
public void rejected(RejectedException e) {
listener.rejected(e);
}
public void failed(IOException e) {
listener.failed(e);
}
public void closed(IOException e) {
listener.closed(e);
}
});
}
@Override
protected void close(IOException cause) {
mConnectedBrokers.remove(this);
super.close(cause);
}
@Override
protected boolean requirePingTask() {
// Nothing to do. This side receives pings.
return false;
}
@Override
protected void doPing() {
// Should not be called.
throw new AssertionError();
}
/**
* @return false if channel was closed because only old protocol is supported
*/
private boolean sendRequest(Channel channel) throws IOException {
DataOutputStream dout = new DataOutputStream(channel.getOutputStream());
if (mProtocol == 1) {
// Always use old deprecated protocol.
dout.writeByte(ACCEPT_REQUEST);
dout.writeLong(mId);
dout.flush();
} else {
dout.writeByte(ACCEPT_CONFIRM_REQUEST);
dout.writeLong(mId);
dout.flush();
int response = channel.getInputStream().read();
if (response < 0) {
if (mProtocol == 0) {
// Force old protocol.
mProtocol = 1;
channel.disconnect();
return false;
}
throw new ClosedException("New connection immediately closed");
}
// Always use new protocol.
mProtocol = 2;
if (response != ACCEPT_SUCCESS_RESPONSE) {
channel.disconnect();
ClosedException exception = new ClosedException("Stale session");
close(exception);
throw exception;
}
}
channel.register(mAllChannels);
return true;
}
}
}