package com.pekall.smartplug.model;
import com.pekall.smartplug.codec.SmartPlugDecoder;
import com.pekall.smartplug.codec.SmartPlugEncoder;
import com.pekall.smartplug.message.BaseMessage;
import com.pekall.smartplug.message.GetStatusResponse;
import com.pekall.smartplug.message.Heartbeat;
import com.pekall.smartplug.message.HelloRequest;
import com.pekall.smartplug.message.HelloResponse;
import com.pekall.smartplug.message.ReportStatusRequest;
import com.pekall.smartplug.message.ReportStatusResponse;
import com.pekall.smartplug.message.SetStatusRequest;
import com.pekall.smartplug.message.SetStatusResponse;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.oio.OioClientSocketChannelFactory;
import java.net.InetSocketAddress;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class SmartPlugImpl implements SmartPlug {
private static final int CONNECT_TIMEOUT_MS = 10000;
private static final short STATUS_ON = 1;
private static final short STATUS_OFF = 0;
private static final short RESULT_OK = 0;
private static final short RESULT_ERR = 1;
private SmartPlugListener mListener;
private ClientBootstrap mClientBootstrap;
private Channel mClientChannel;
private AtomicInteger mCounter;
private ScheduledExecutorService mHeartbeatScheduler;
private ScheduledFuture<?> mHeartbeatFuture;
public SmartPlugImpl(SmartPlugListener listener) {
super();
if (listener == null) {
throw new IllegalArgumentException("listener should be be null");
}
mListener = listener;
mCounter = new AtomicInteger();
// Set up.
mClientBootstrap = new ClientBootstrap(
new OioClientSocketChannelFactory(
Executors.newCachedThreadPool()));
// Configure the event pipeline factory.
mClientBootstrap.setOption("connectTimeoutMillis", CONNECT_TIMEOUT_MS); // 10s
mClientBootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new SmartPlugDecoder());
pipeline.addLast("encoder", new SmartPlugEncoder());
pipeline.addLast("handler", new ClientHandler());
return pipeline;
}
});
}
public SmartPlugListener getListener() {
return mListener;
}
public Channel getChannel() {
return mClientChannel;
}
public int getNextMessageId() {
return mCounter.getAndIncrement();
}
@Override
public boolean connect(String host, int port) {
clog("connect E");
if (isConnected()) {
clog("already connected. nop");
return true;
} else {
// Make a new connection.
ChannelFuture connectFuture = mClientBootstrap.connect(new InetSocketAddress(host, port));
// Wait until the connection is made successfully.
connectFuture.awaitUninterruptibly();
if (connectFuture.isSuccess()) {
mClientChannel = connectFuture.getChannel();
clog("connect ok");
return true;
} else {
clog("connect fail");
return false;
}
}
}
@Override
public void disconnect() {
if (isConnected()) {
mClientChannel.disconnect();
}
}
@Override
public boolean isConnected() {
return mClientChannel != null && mClientChannel.isConnected();
}
@Override
public boolean login(String pn, String sn) {
clog("login E");
if (!isConnected()) {
clog("not connected");
return false;
}
ClientHandler handler = mClientChannel.getPipeline().get(ClientHandler.class);
HelloRequest request = new HelloRequest(getNextMessageId(), sn, pn);
HelloResponse response = (HelloResponse) handler.getResponse(request);
boolean success = (response.getResultCode() == 0);
if (success) {
startHeartbeat();
}
clog("login X");
return success;
}
private void startHeartbeat() {
clog("startHeartbeat E");
if (isConnected()) {
mHeartbeatScheduler = Executors.newSingleThreadScheduledExecutor();
mHeartbeatFuture = mHeartbeatScheduler.scheduleAtFixedRate(new HeartbeatTimerTask(), 0, 10, TimeUnit.SECONDS);
} else {
clog("Error not connected");
}
clog("startHeartbeat X");
}
private void stopHeartbeat() {
clog("stopHearbeat E");
if (mHeartbeatScheduler != null && !mHeartbeatScheduler.isShutdown()) {
if (mHeartbeatFuture != null) {
mHeartbeatFuture.cancel(true);
}
mHeartbeatScheduler.shutdown();
}
clog("stopHeartbeat X");
}
@Override
public boolean reportStatus(boolean on) {
if (!isConnected()) {
return false;
}
ClientHandler handler = mClientChannel.getPipeline().get(ClientHandler.class);
ReportStatusRequest request = new ReportStatusRequest(getNextMessageId(), (on ? STATUS_ON : STATUS_OFF));
ReportStatusResponse response = (ReportStatusResponse) handler.getResponse(request);
return true;
}
@Override
public void release() {
stopHeartbeat();
clog(">>>>> releaseExternalResources");
mClientBootstrap.releaseExternalResources();
clog("<<<<< releaseExternalResources");
}
private static void clog(String msg) {
System.out.println("client --> " + msg);
}
private class ErrorEventCaller implements Runnable {
private ExceptionEvent mExceptionEvent;
public ErrorEventCaller(ExceptionEvent e) {
mExceptionEvent = e;
}
@Override
public void run() {
SmartPlugImpl smartPlugImpl = SmartPlugImpl.this;
smartPlugImpl.mListener.onError(smartPlugImpl, mExceptionEvent.toString());
mExceptionEvent.getChannel().close();
}
}
private class DisconnectedEventCaller implements Runnable {
@Override
public void run() {
SmartPlugImpl smartPlugImpl = SmartPlugImpl.this;
smartPlugImpl.stopHeartbeat();
smartPlugImpl.mListener.onDisconnected(smartPlugImpl);
}
}
private class InBoundMessageCaller implements Runnable {
private BaseMessage mMessage;
public InBoundMessageCaller(BaseMessage message) {
this.mMessage = message;
}
@Override
public void run() {
switch (mMessage.getMessageType()) {
case MSG_GET_STATUS_REQ:
onGetStatusRequested();
break;
case MSG_SET_STATUS_REQ:
onSetStatusRequested();
break;
default:
// ignore unknown message
break;
}
}
private void onSetStatusRequested() {
SetStatusRequest request = (SetStatusRequest) mMessage;
final SmartPlugImpl smartPlugImpl = SmartPlugImpl.this;
final SmartPlugListener listener = smartPlugImpl.mListener;
boolean ret = listener.onSetStatusRequested(smartPlugImpl, request.getStatus() == STATUS_ON);
SetStatusResponse response = new SetStatusResponse(mMessage.getMessageId(), ret ? RESULT_OK : RESULT_ERR);
smartPlugImpl.mClientChannel.write(response);
}
private void onGetStatusRequested() {
final SmartPlugImpl smartPlugImpl = SmartPlugImpl.this;
final SmartPlugListener listener = smartPlugImpl.mListener;
boolean status = listener.onGetStatusRequested(smartPlugImpl);
GetStatusResponse response = new GetStatusResponse(mMessage.getMessageId(), status ? STATUS_ON : STATUS_OFF);
smartPlugImpl.mClientChannel.write(response);
}
}
private class HeartbeatTimerTask implements Runnable {
@Override
public void run() {
final SmartPlugImpl smartPlugImpl = SmartPlugImpl.this;
if (smartPlugImpl.isConnected()) {
final SmartPlugListener listener = smartPlugImpl.mListener;
boolean status = listener.onGetStatusRequested(smartPlugImpl);
Heartbeat heartbeat = new Heartbeat(getNextMessageId(), status ? STATUS_ON : STATUS_OFF);
smartPlugImpl.mClientChannel.write(heartbeat);
clog("HeartbeatTimerTask send heartbeat");
}
}
}
private class ClientHandler extends SimpleChannelUpstreamHandler {
// Stateful properties
private volatile Channel mChannel;
private final BlockingQueue<BaseMessage> mAnswerQueue = new LinkedBlockingQueue<BaseMessage>();
private ExecutorService mExecutor = Executors.newCachedThreadPool();
public BaseMessage getResponse(BaseMessage request) {
mChannel.write(request);
boolean interrupted = false;
BaseMessage response;
for (;;) {
try {
clog("before take");
response = mAnswerQueue.take();
clog("after take");
if (response.getMessageId() == request.getMessageId()) {
// got the matched response
break;
}
} catch (InterruptedException e) {
interrupted = true;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
return response;
}
@Override
public void handleUpstream(
ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
if (e instanceof ChannelStateEvent) {
clog(e.toString());
}
super.handleUpstream(ctx, e);
}
@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
mChannel = e.getChannel();
clog("channelOpen");
super.channelOpen(ctx, e);
}
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
clog("channelDisconnected");
mExecutor.execute(new DisconnectedEventCaller());
super.channelDisconnected(ctx, e);
}
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
clog("channelClosed");
super.channelClosed(ctx, e);
mExecutor.shutdown();
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
BaseMessage message = (BaseMessage) e.getMessage();
clog("messageReceived --> " + message.toString());
switch (message.getMessageType()) {
case MSG_GET_STATUS_REQ:
case MSG_SET_STATUS_REQ: {
mExecutor.execute(new InBoundMessageCaller(message));
break;
}
case MSG_HELLO_RES:
case MSG_REPORT_STATUS_RES: {
boolean offered = mAnswerQueue.offer(message);
assert offered;
break;
}
default:
break;
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
clog("exceptionCaught: " + e);
mExecutor.execute(new ErrorEventCaller(e));
}
}
}