First pass at WebSocketEventDriver with tests

This commit is contained in:
Joakim Erdfelt 2012-06-28 11:59:15 -07:00
parent 324431a072
commit deef1a3ac1
12 changed files with 355 additions and 135 deletions

View File

@ -1,4 +1,4 @@
package org.eclipse.jetty.websocket.annotations;
package org.eclipse.jetty.websocket.api;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
@ -16,7 +16,7 @@ public class EventMethod
{
if (args.length == 1)
{
return null;
return new Object[0];
}
Object ret[] = new Object[args.length - 1];
System.arraycopy(args,1,ret,0,ret.length);
@ -40,10 +40,8 @@ public class EventMethod
return NOOP;
}
private Class<?> pojo;
private Method method;
protected Class<?> pojo;
protected Method method;
private Class<?>[] paramTypes;
private EventMethod()
@ -64,11 +62,11 @@ public class EventMethod
{
this.pojo = pojo;
this.paramTypes = paramTypes;
this.method = pojo.getClass().getMethod(methodName,paramTypes);
this.method = pojo.getMethod(methodName,paramTypes);
}
catch (NoSuchMethodException | SecurityException e)
{
LOG.debug("Cannot use method {}({}): {}",methodName,paramTypes,e.getMessage(),e);
LOG.warn("Cannot use method {}({}): {}",methodName,paramTypes,e.getMessage());
this.method = null;
}
}
@ -77,6 +75,7 @@ public class EventMethod
{
if ((this.pojo == null) || (this.method == null))
{
LOG.warn("Cannot execute call: pojo={}, method={}",pojo,method);
return; // no call event method determined
}
if (obj == null)
@ -87,12 +86,13 @@ public class EventMethod
if (args.length > paramTypes.length)
{
Object trimArgs[] = dropFirstArg(args);
call(trimArgs);
call(obj,trimArgs);
return;
}
if (args.length < paramTypes.length)
{
throw new IllegalArgumentException("Call arguments length must always be greater than or equal to captured args length");
throw new IllegalArgumentException("Call arguments length [" + args.length + "] must always be greater than or equal to captured args length ["
+ paramTypes.length + "]");
}
try
{

View File

@ -1,9 +1,8 @@
package org.eclipse.jetty.websocket.annotations;
package org.eclipse.jetty.websocket.api;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
import org.eclipse.jetty.websocket.frames.BaseFrame;
/**

View File

@ -1,4 +1,4 @@
package org.eclipse.jetty.websocket.annotations;
package org.eclipse.jetty.websocket.api;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
@ -8,10 +8,12 @@ import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketConnection;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.annotations.OnWebSocketBinary;
import org.eclipse.jetty.websocket.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.annotations.OnWebSocketFrame;
import org.eclipse.jetty.websocket.annotations.OnWebSocketText;
import org.eclipse.jetty.websocket.annotations.WebSocket;
import org.eclipse.jetty.websocket.frames.BaseFrame;
import org.eclipse.jetty.websocket.frames.BinaryFrame;
import org.eclipse.jetty.websocket.frames.CloseFrame;
@ -243,7 +245,7 @@ public class EventMethodsCache
*/
private EventMethods discoverMethods(Class<?> pojo) throws InvalidWebSocketException
{
if (WebSocketListener.class.isInstance(pojo))
if (WebSocketListener.class.isAssignableFrom(pojo))
{
return scanListenerMethods(pojo);
}
@ -365,9 +367,9 @@ public class EventMethodsCache
// This is a WebSocketListener object
events.onConnect = new EventMethod(pojo,"onWebSocketConnect",WebSocketConnection.class);
events.onClose = new EventMethod(pojo,"onWebSocketClose",Integer.TYPE,String.class);
events.onBinary = new EventMethod(pojo,"onWebSocketBinary",byte[].class,Integer.TYPE,Integer.TYPE);
events.onText = new EventMethod(pojo,"onWebSocketText",WebSocketConnection.class);
events.onClose = new EventMethod(pojo,"onWebSocketClose",int.class,String.class);
events.onBinary = new EventMethod(pojo,"onWebSocketBinary",byte[].class,int.class,int.class);
events.onText = new EventMethod(pojo,"onWebSocketText",String.class);
events.onException = new EventMethod(pojo,"onWebSocketException",WebSocketException.class);
return events;

View File

@ -1,9 +1,10 @@
package org.eclipse.jetty.websocket.api;
import org.eclipse.jetty.websocket.annotations.EventMethods;
import org.eclipse.jetty.websocket.annotations.EventMethodsCache;
import org.eclipse.jetty.websocket.annotations.WebSocket;
import org.eclipse.jetty.websocket.frames.BaseFrame;
import org.eclipse.jetty.websocket.frames.BinaryFrame;
import org.eclipse.jetty.websocket.frames.CloseFrame;
import org.eclipse.jetty.websocket.frames.TextFrame;
import org.eclipse.jetty.websocket.parser.Parser;
/**
@ -72,18 +73,61 @@ public class WebSocketEventDriver implements Parser.Listener
* @param frame
* the frame that appeared
*/
@SuppressWarnings("unchecked")
@Override
public void onFrame(BaseFrame frame)
{
// TODO Auto-generated method stub
// Specified Close Case
if ((frame instanceof CloseFrame) && (events.onClose != null))
{
CloseFrame close = (CloseFrame)frame;
events.onClose.call(websocket,connection,close.getStatusCode(),close.getReason());
return;
}
// Specified Text Case
if ((frame instanceof TextFrame) && (events.onText != null))
{
TextFrame text = (TextFrame)frame;
events.onText.call(websocket,connection,text.getPayloadUTF8());
return;
}
// Specified Binary Case
if ((frame instanceof BinaryFrame) && (events.onBinary != null))
{
BinaryFrame bin = (BinaryFrame)frame;
events.onBinary.call(websocket,connection,bin.getPayload());
return;
}
// Basic Hierarchy Case
Class<? extends BaseFrame> frameType = frame.getClass();
while (true)
{
EventMethod event = events.getOnFrame(frameType);
if (event != null)
{
event.call(websocket,connection,frame);
return;
}
if (!frameType.getSuperclass().isAssignableFrom(BaseFrame.class))
{
// not assignable
return;
}
frameType = (Class<? extends BaseFrame>)frameType.getSuperclass();
}
}
@Override
public void onWebSocketException(WebSocketException e)
{
// TODO Auto-generated method stub
if (events.onException != null)
{
events.onException.call(websocket,connection,e);
}
}
/**

View File

@ -1,53 +0,0 @@
package org.eclipse.jetty.websocket.annotations;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.websocket.api.WebSocketConnection;
@WebSocket
public class CaptureSocket
{
private List<String> events = new ArrayList<>();
private void addEvent(String format, Object ... args)
{
events.add(String.format(format,args));
}
public void clear()
{
events.clear();
}
public List<String> getEvents()
{
return events;
}
@OnWebSocketClose
public void onClose(int statusCode, String reason)
{
addEvent("OnWebSocketClose(%d, %s)",statusCode,qoute(reason));
}
@OnWebSocketConnect
public void onConnect(WebSocketConnection conn)
{
addEvent("OnWebSocketConnect(conn)");
}
@OnWebSocketText
public void onText(String message) {
addEvent("@OnWebSocketText(%s)", qoute(message));
}
private String qoute(String str)
{
if (str == null)
{
return "<null>";
}
return '"' + str + '"';
}
}

View File

@ -0,0 +1,45 @@
package org.eclipse.jetty.websocket.api;
import static org.hamcrest.Matchers.*;
import java.util.ArrayList;
import org.junit.Assert;
@SuppressWarnings("serial")
public class EventCapture extends ArrayList<String>
{
public void add(String format, Object... args)
{
super.add(String.format(format,args));
}
public void assertEvent(int eventNum, String expected)
{
Assert.assertThat("Event[" + eventNum + "]",get(eventNum),is(expected));
}
public void assertEventContains(int eventNum, String expected)
{
Assert.assertThat("Event[" + eventNum + "]",get(eventNum),containsString(expected));
}
public void assertEventCount(int expectedCount)
{
Assert.assertThat("Event Count",size(),is(expectedCount));
}
public void assertEventStartsWith(int eventNum, String expected)
{
Assert.assertThat("Event[" + eventNum + "]",get(eventNum),startsWith(expected));
}
public String q(String str)
{
if (str == null)
{
return "<null>";
}
return '"' + str + '"';
}
}

View File

@ -1,9 +1,20 @@
package org.eclipse.jetty.websocket.annotations;
package org.eclipse.jetty.websocket.api;
import static org.hamcrest.Matchers.*;
import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.annotations.BadBinarySignatureSocket;
import org.eclipse.jetty.websocket.annotations.BadDuplicateBinarySocket;
import org.eclipse.jetty.websocket.annotations.BadDuplicateFrameSocket;
import org.eclipse.jetty.websocket.annotations.BadTextSignatureSocket;
import org.eclipse.jetty.websocket.annotations.FrameSocket;
import org.eclipse.jetty.websocket.annotations.MyEchoBinarySocket;
import org.eclipse.jetty.websocket.annotations.MyEchoSocket;
import org.eclipse.jetty.websocket.annotations.MyStatelessEchoSocket;
import org.eclipse.jetty.websocket.annotations.NoopSocket;
import org.eclipse.jetty.websocket.annotations.NotASocket;
import org.eclipse.jetty.websocket.annotations.WebSocket;
import org.eclipse.jetty.websocket.api.samples.AdapterConnectCloseSocket;
import org.eclipse.jetty.websocket.api.samples.ListenerBasicSocket;
import org.eclipse.jetty.websocket.frames.BaseFrame;
import org.eclipse.jetty.websocket.frames.TextFrame;
import org.junit.Assert;
@ -13,19 +24,47 @@ public class EventMethodsCacheTest
{
private void assertHasEventMethod(String message, EventMethod actual)
{
Assert.assertThat(message + "Event method should have been discovered",actual,notNullValue());
Assert.assertThat(message + " EventMethod",actual,notNullValue());
Assert.assertThat(message + " EventMethod.pojo",actual.pojo,notNullValue());
Assert.assertThat(message + " EventMethod.method",actual.method,notNullValue());
}
private void assertNoEventMethod(String message, EventMethod actual)
{
Assert.assertThat(message + "Event method should have been NOOP",actual,nullValue());
Assert.assertThat(message + "Event method",actual,nullValue());
}
/**
* Test Case for no exceptions and 5 methods (extends WebSocketAdapter)
*/
@Test
public void testAdapterConnectCloseSocket()
{
EventMethodsCache cache = new EventMethodsCache();
EventMethods methods = cache.getMethods(AdapterConnectCloseSocket.class);
String classId = AdapterConnectCloseSocket.class.getSimpleName();
Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
// Directly Declared
assertHasEventMethod(classId + ".onClose",methods.onClose);
assertHasEventMethod(classId + ".onConnect",methods.onConnect);
// From WebSocketAdapter
assertHasEventMethod(classId + ".onBinary",methods.onBinary);
assertHasEventMethod(classId + ".onException",methods.onException);
assertHasEventMethod(classId + ".onText",methods.onText);
Assert.assertThat(".getOnFrames()",methods.getOnFrames().size(),is(0));
}
/**
* Test Case for bad declaration (duplicate OnWebSocketBinary declarations)
*/
@Test
public void testDiscoverBadDuplicateBinarySocket()
public void testAnnotatedBadDuplicateBinarySocket()
{
EventMethodsCache cache = new EventMethodsCache();
try
@ -45,7 +84,7 @@ public class EventMethodsCacheTest
* Test Case for bad declaration (duplicate frame type methods)
*/
@Test
public void testDiscoverBadDuplicateFrameSocket()
public void testAnnotatedBadDuplicateFrameSocket()
{
EventMethodsCache cache = new EventMethodsCache();
try
@ -65,7 +104,7 @@ public class EventMethodsCacheTest
* Test Case for bad declaration a method with a non-void return type
*/
@Test
public void testDiscoverBadSignature_NonVoidReturn()
public void testAnnotatedBadSignature_NonVoidReturn()
{
EventMethodsCache cache = new EventMethodsCache();
try
@ -85,7 +124,7 @@ public class EventMethodsCacheTest
* Test Case for bad declaration a method with a public static method
*/
@Test
public void testDiscoverBadSignature_Static()
public void testAnnotatedBadSignature_Static()
{
EventMethodsCache cache = new EventMethodsCache();
try
@ -105,7 +144,7 @@ public class EventMethodsCacheTest
* Test Case for no exceptions and 4 methods (3 methods from parent)
*/
@Test
public void testDiscoverMyEchoBinarySocket()
public void testAnnotatedMyEchoBinarySocket()
{
EventMethodsCache cache = new EventMethodsCache();
EventMethods methods = cache.getMethods(MyEchoBinarySocket.class);
@ -127,7 +166,7 @@ public class EventMethodsCacheTest
* Test Case for no exceptions and 3 methods
*/
@Test
public void testDiscoverMyEchoSocket()
public void testAnnotatedMyEchoSocket()
{
EventMethodsCache cache = new EventMethodsCache();
EventMethods methods = cache.getMethods(MyEchoSocket.class);
@ -147,7 +186,7 @@ public class EventMethodsCacheTest
* Test Case for no exceptions and 1 method
*/
@Test
public void testDiscoverMyStatelessEchoSocket()
public void testAnnotatedMyStatelessEchoSocket()
{
EventMethodsCache cache = new EventMethodsCache();
EventMethods methods = cache.getMethods(MyStatelessEchoSocket.class);
@ -167,7 +206,7 @@ public class EventMethodsCacheTest
* Test Case for no exceptions and no methods
*/
@Test
public void testDiscoverNoop()
public void testAnnotatedNoop()
{
EventMethodsCache cache = new EventMethodsCache();
EventMethods methods = cache.getMethods(NoopSocket.class);
@ -183,30 +222,11 @@ public class EventMethodsCacheTest
Assert.assertThat("MyEchoSocket.getOnFrames()",methods.getOnFrames().size(),is(0));
}
/**
* Test Case for bad declaration (duplicate OnWebSocketBinary declarations)
*/
@Test
public void testDiscoverNotASocket()
{
EventMethodsCache cache = new EventMethodsCache();
try
{
// Should toss exception
cache.getMethods(NotASocket.class);
}
catch (InvalidWebSocketException e)
{
// Validate that we have clear error message to the developer
Assert.assertThat(e.getMessage(),allOf(containsString(WebSocketListener.class.getSimpleName()),containsString(WebSocket.class.getSimpleName())));
}
}
/**
* Test Case for no exceptions and 3 methods
*/
@Test
public void testDiscoverOnFrame()
public void testAnnotatedOnFrame()
{
EventMethodsCache cache = new EventMethodsCache();
EventMethods methods = cache.getMethods(FrameSocket.class);
@ -223,4 +243,45 @@ public class EventMethodsCacheTest
assertHasEventMethod("MyEchoSocket.onFrame(BaseFrame)",methods.getOnFrame(BaseFrame.class));
assertHasEventMethod("MyEchoSocket.onFrame(BaseFrame)",methods.getOnFrame(TextFrame.class));
}
/**
* Test Case for bad declaration (duplicate OnWebSocketBinary declarations)
*/
@Test
public void testBadNotASocket()
{
EventMethodsCache cache = new EventMethodsCache();
try
{
// Should toss exception
cache.getMethods(NotASocket.class);
}
catch (InvalidWebSocketException e)
{
// Validate that we have clear error message to the developer
Assert.assertThat(e.getMessage(),allOf(containsString(WebSocketListener.class.getSimpleName()),containsString(WebSocket.class.getSimpleName())));
}
}
/**
* Test Case for no exceptions and 5 methods (implement WebSocketListener)
*/
@Test
public void testListenerBasicSocket()
{
EventMethodsCache cache = new EventMethodsCache();
EventMethods methods = cache.getMethods(ListenerBasicSocket.class);
String classId = AdapterConnectCloseSocket.class.getSimpleName();
Assert.assertThat("ListenerBasicSocket for " + classId,methods,notNullValue());
assertHasEventMethod(classId + ".onClose",methods.onClose);
assertHasEventMethod(classId + ".onConnect",methods.onConnect);
assertHasEventMethod(classId + ".onBinary",methods.onBinary);
assertHasEventMethod(classId + ".onException",methods.onException);
assertHasEventMethod(classId + ".onText",methods.onText);
Assert.assertThat(".getOnFrames()",methods.getOnFrames().size(),is(0));
}
}

View File

@ -6,97 +6,99 @@ import java.nio.ByteBuffer;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.frames.BaseFrame;
import org.junit.rules.TestName;
public class LocalWebSocketConnection implements WebSocketConnection
{
private final String id;
public LocalWebSocketConnection()
{
this("anon");
}
public LocalWebSocketConnection(String id)
{
this.id = id;
}
public LocalWebSocketConnection(TestName testname)
{
this.id = testname.getMethodName();
}
@Override
public void close()
{
// TODO Auto-generated method stub
}
@Override
public void close(int statusCode, String reason)
{
// TODO Auto-generated method stub
}
@Override
public WebSocketPolicy getPolicy()
{
// TODO Auto-generated method stub
return null;
}
@Override
public InetAddress getRemoteAddress()
{
// TODO Auto-generated method stub
return null;
}
@Override
public String getSubProtocol()
{
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isOpen()
{
// TODO Auto-generated method stub
return false;
}
@Override
public String toString()
{
return String.format("LocalWebSocketConnection[%s]",id);
}
@Override
public void write(BaseFrame frame) throws IOException
{
// TODO Auto-generated method stub
}
@Override
public void write(byte[] data, int offset, int length) throws IOException
{
// TODO Auto-generated method stub
}
@Override
public void write(ByteBuffer... buffers) throws IOException
{
// TODO Auto-generated method stub
}
@Override
public <C> void write(C context, Callback<C> callback, BaseFrame... frames) throws IOException
{
// TODO Auto-generated method stub
}
@Override
public <C> void write(C context, Callback<C> callback, ByteBuffer... buffers) throws IOException
{
// TODO Auto-generated method stub
}
@Override
public <C> void write(C context, Callback<C> callback, String... messages) throws IOException
{
// TODO Auto-generated method stub
}
@Override
public void write(String message) throws IOException
{
// TODO Auto-generated method stub
}
}

View File

@ -0,0 +1,57 @@
package org.eclipse.jetty.websocket.api;
import org.eclipse.jetty.websocket.api.samples.AdapterConnectCloseSocket;
import org.eclipse.jetty.websocket.api.samples.ListenerBasicSocket;
import org.eclipse.jetty.websocket.frames.CloseFrame;
import org.eclipse.jetty.websocket.frames.TextFrame;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
public class WebSocketEventDriverTest
{
@Rule
public TestName testname = new TestName();
private WebSocketEventDriver newDriver(Object websocket)
{
EventMethodsCache methodsCache = new EventMethodsCache();
methodsCache.register(websocket.getClass());
WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
return new WebSocketEventDriver(methodsCache,policy,websocket);
}
@Test
public void testAdapterConnectClose()
{
AdapterConnectCloseSocket socket = new AdapterConnectCloseSocket();
WebSocketEventDriver driver = newDriver(socket);
LocalWebSocketConnection conn = new LocalWebSocketConnection(testname);
driver.setConnection(conn);
driver.onConnect();
driver.onFrame(new CloseFrame(StatusCode.NORMAL));
socket.capture.assertEventCount(2);
socket.capture.assertEventStartsWith(0,"onWebSocketConnect");
socket.capture.assertEventStartsWith(1,"onWebSocketClose");
}
@Test
public void testListenerBasic()
{
ListenerBasicSocket socket = new ListenerBasicSocket();
WebSocketEventDriver driver = newDriver(socket);
LocalWebSocketConnection conn = new LocalWebSocketConnection(testname);
driver.setConnection(conn);
driver.onConnect();
driver.onFrame(new TextFrame("Hello World"));
driver.onFrame(new CloseFrame(StatusCode.NORMAL));
socket.capture.assertEventCount(3);
socket.capture.assertEventStartsWith(0,"onWebSocketConnect");
socket.capture.assertEventStartsWith(1,"onWebSocketText(\"Hello World\")");
socket.capture.assertEventStartsWith(2,"onWebSocketClose(1000,");
}
}

View File

@ -0,0 +1,22 @@
package org.eclipse.jetty.websocket.api.samples;
import org.eclipse.jetty.websocket.api.EventCapture;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.WebSocketConnection;
public class AdapterConnectCloseSocket extends WebSocketAdapter
{
public EventCapture capture = new EventCapture();
@Override
public void onWebSocketClose(int statusCode, String reason)
{
capture.add("onWebSocketClose(%d, %s)",statusCode,capture.q(reason));
}
@Override
public void onWebSocketConnect(WebSocketConnection connection)
{
capture.add("onWebSocketConnect(%s)",connection);
}
}

View File

@ -0,0 +1,41 @@
package org.eclipse.jetty.websocket.api.samples;
import org.eclipse.jetty.websocket.api.EventCapture;
import org.eclipse.jetty.websocket.api.WebSocketConnection;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketListener;
public class ListenerBasicSocket implements WebSocketListener
{
public EventCapture capture = new EventCapture();
@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
capture.add("onWebSocketBinary([%d], %d, %d)",payload.length,offset,len);
}
@Override
public void onWebSocketClose(int statusCode, String reason)
{
capture.add("onWebSocketClose(%d, %s)",statusCode,capture.q(reason));
}
@Override
public void onWebSocketConnect(WebSocketConnection connection)
{
capture.add("onWebSocketConnect(%s)",connection);
}
@Override
public void onWebSocketException(WebSocketException error)
{
capture.add("onWebSocketException((%s) %s)",error.getClass().getSimpleName(),error.getMessage());
}
@Override
public void onWebSocketText(String message)
{
capture.add("onWebSocketText(%s)",capture.q(message));
}
}

View File

@ -32,8 +32,8 @@ import org.eclipse.jetty.server.HttpConnection;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.annotations.EventMethodsCache;
import org.eclipse.jetty.websocket.annotations.WebSocket;
import org.eclipse.jetty.websocket.api.EventMethodsCache;
import org.eclipse.jetty.websocket.api.ExtensionConfig;
import org.eclipse.jetty.websocket.api.WebSocketEventDriver;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;