/** Copyright 2013 the original author or authors. * * 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. */ /** * * @author <a href='mailto:th33musk3t33rs@gmail.com'>3.musket33rs</a> * * @since 0.1 */ package org.threemusketeers.eventsource; import io.netty.channel.*; import io.netty.handler.codec.http.*; import java.net.URI; import java.util.concurrent.TimeUnit; @ChannelHandler.Sharable public class EventSourceClientHandler extends ChannelInboundMessageHandlerAdapter<Object> { URI uri; EventSourceNotification notification; EventSource eventSource; Channel channel; EventSourceHandshaker handshaker = new EventSourceHandshaker(); Message message; String lastEventId; boolean closed = false; long reconnectDelay = 3000; String messageType; EventSourceClientHandler(URI uri, EventSourceNotification notification, EventSource eventSource) { super(); this.uri = uri; this.notification = notification; this.eventSource = eventSource; } @Override public void channelUnregistered(final ChannelHandlerContext ctx) throws Exception { if (!closed) { notification.onerror("RECONNECTING"); final EventLoop loop = ctx.channel().eventLoop(); // Wait a delay equal to the reconnection time of the event source. loop.schedule(new Runnable() { @Override public void run() { handshaker = new EventSourceHandshaker(); // Reconnect eventSource.createBootstrap(); } }, reconnectDelay, TimeUnit.MILLISECONDS); } } @Override public void channelInactive(final ChannelHandlerContext ctx) throws Exception { } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { channel = ctx.channel(); FullHttpRequest request = getConnectHttpRequest(); channel.write(request); } @Override public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { String content = (String)msg; // First have the hand shake done if (!handshaker.isHandshakeComplete()) { if (handshaker.continueHandshake(content)) { if (handshaker.error != null) { notification.onerror(handshaker.error); message = null; } else { if (Constants.EVENT_STREAM.equals(handshaker.contentType)) { messageType = Constants.EVENT_STREAM; message = new EventStreamMessage(); } else { messageType = Constants.JSON; message = new JSONMessage(); } notification.onopen(); } } return; } // Then when handshake active can parse the content if(message.parse(content)) { // Set the last event ID string of the event source to value of the last event ID buffer. // The buffer does not get reset, so the last event ID string of the event source remains // set to this value until the next time it is set by the server. if (message.id != null) { lastEventId = message.id; } if (message.retry > 0) { reconnectDelay = message.retry; } notification.onmessage(message); if (Constants.EVENT_STREAM.equals(messageType)) { message = new EventStreamMessage(); } else { message = new JSONMessage(); } } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { notification.onerror(cause.getMessage()); ctx.close(); } public void close() { this.closed = true; if (this.channel == null) { return; } if (this.channel.isOpen()) { this.channel.close(); } } FullHttpRequest getConnectHttpRequest() { FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.toString()); //the Accept header may be included but this is not mandatory request.headers().add(HttpHeaders.Names.ACCEPT, Constants.EVENT_STREAM); request.headers().add(HttpHeaders.Names.HOST, uri.getHost()); int port = uri.getPort(); if (uri.getScheme().equals("http") && port == 80) { port = -1; } else if (uri.getScheme().equals("https") && port == 443) { port = -1; } StringBuilder origin = new StringBuilder(); origin.append(uri.getScheme()).append("://").append(uri.getHost()); if (port != -1) { origin.append(':').append(port); } request.headers().add(HttpHeaders.Names.ORIGIN, origin); //User agents should use the Cache-Control: no-cache header in requests to bypass any caches for requests of event sources request.headers().add(HttpHeaders.Names.CACHE_CONTROL, "no-cache"); //If the event source's last event ID string is not the empty string, //then a Last-Event-ID HTTP header must be included with the request, //whose value is the value of the event source's last event ID string, encoded as UTF-8. if (lastEventId != null && !lastEventId.isEmpty()) { request.headers().add("Last-Event-ID", lastEventId); } return request; } // Check if this is useful in case the person do not close the eventsource // but do not use it anymore protected void finalize() throws Throwable { this.close(); } }