From 28d5d0ecb1342139a30a8cfdafd7aa819a24fc9a Mon Sep 17 00:00:00 2001 From: codingchili Date: Fri, 18 Aug 2017 20:52:56 +0200 Subject: [PATCH] Add support for annotated CoreHandlers using Runtime retention only. --- .../core/listener/CoreHandler.java | 54 +-- .../codingchili/core/protocol/Address.java | 15 + .../codingchili/core/protocol/Private.java | 15 + .../codingchili/core/protocol/Protocol.java | 249 +++++++------ .../com/codingchili/core/protocol/Public.java | 15 + .../core/protocol/ProtocolTest.java | 335 ++++++++++++------ 6 files changed, 446 insertions(+), 237 deletions(-) create mode 100644 core/main/java/com/codingchili/core/protocol/Address.java create mode 100644 core/main/java/com/codingchili/core/protocol/Private.java create mode 100644 core/main/java/com/codingchili/core/protocol/Public.java diff --git a/core/main/java/com/codingchili/core/listener/CoreHandler.java b/core/main/java/com/codingchili/core/listener/CoreHandler.java index 034f8cb4..91acc195 100644 --- a/core/main/java/com/codingchili/core/listener/CoreHandler.java +++ b/core/main/java/com/codingchili/core/listener/CoreHandler.java @@ -1,21 +1,35 @@ -package com.codingchili.core.listener; - -/** - * @author Robin Duda - *

- * A simplified handler that may be deployed directly. - */ -public interface CoreHandler extends CoreDeployment { - - /** - * Handles an incoming request without exception handling. - * - * @param request the request to be handled. - */ - void handle(Request request); - - /** - * @return the address of the handler. - */ - String address(); +package com.codingchili.core.listener; + +import com.codingchili.core.protocol.Address; + +/** + * @author Robin Duda + *

+ * A simplified handler that may be deployed directly. + */ +public interface CoreHandler extends CoreDeployment { + + /** + * Handles an incoming request without exception handling. + * + * @param request the request to be handled. + */ + void handle(Request request); + + /** + * @return the address of the handler. If not implemented the @Address + * annotation will be used, if missing an error is thrown. + * + * Could potentially lead to Runtime errors but is allowed here as + * this is called during deployment. Reconsider this decision later. + */ + default String address() { + Address annotation = getClass().getAnnotation(Address.class); + if (annotation != null) { + return annotation.value(); + } else { + throw new RuntimeException("Class " + getClass().getName() + " does not" + + " implement CoreHandler::address nor is annotated with @Address."); + } + } } \ No newline at end of file diff --git a/core/main/java/com/codingchili/core/protocol/Address.java b/core/main/java/com/codingchili/core/protocol/Address.java new file mode 100644 index 00000000..02415311 --- /dev/null +++ b/core/main/java/com/codingchili/core/protocol/Address.java @@ -0,0 +1,15 @@ +package com.codingchili.core.protocol; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternate way of specifying a handlers listening address. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Address { + String value(); +} diff --git a/core/main/java/com/codingchili/core/protocol/Private.java b/core/main/java/com/codingchili/core/protocol/Private.java new file mode 100644 index 00000000..dd5b7dbe --- /dev/null +++ b/core/main/java/com/codingchili/core/protocol/Private.java @@ -0,0 +1,15 @@ +package com.codingchili.core.protocol; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated method requires authentication. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Private { + String value(); +} diff --git a/core/main/java/com/codingchili/core/protocol/Protocol.java b/core/main/java/com/codingchili/core/protocol/Protocol.java index f8b00363..85a62da3 100644 --- a/core/main/java/com/codingchili/core/protocol/Protocol.java +++ b/core/main/java/com/codingchili/core/protocol/Protocol.java @@ -1,103 +1,146 @@ -package com.codingchili.core.protocol; - -import com.codingchili.core.protocol.exception.AuthorizationRequiredException; -import com.codingchili.core.protocol.exception.HandlerMissingException; - -import io.vertx.core.json.JsonObject; - -import static com.codingchili.core.configuration.CoreStrings.*; - -/** - * @author Robin Duda - *

- * Maps packet data to handlers and manages authentication status for handlers. - */ -public class Protocol { - private final AuthorizationHandler handlers = new AuthorizationHandler<>(); - - /** - * Returns the route handler for the given target route and its access level. - * - * @param access the access level the request is valid for. - * @param route the handler route to find - * @return the handler that is mapped to the route and access level. - * @throws AuthorizationRequiredException when authorization level is not fulfilled for given route. - * @throws HandlerMissingException when the requested route handler is not registered. - */ - public Handler get(Access access, String route) throws AuthorizationRequiredException, HandlerMissingException { - if (handlers.contains(route)) { - return handlers.get(route, access); - } else { - return handlers.get(ANY, access); - } - } - - /** - * Returns the route handler for the given target route and its access level. - * - * @param route the handler route to find. - * @return the handler that is mapped to the route. - * @throws AuthorizationRequiredException when authorization level is not fulfilled for the given route. - * @throws HandlerMissingException when the requested route handler is not registered. - */ - public Handler get(String route) throws AuthorizationRequiredException, HandlerMissingException { - return get(Access.AUTHORIZED, route); - } - - /** - * Registers a handler for the given route. - * - * @param route the route to register a handler for. - * @param handler the handler to be registered for the given route. - * @return the updated protocol specification for fluent use. - */ - public Protocol use(String route, Handler handler) { - use(route, handler, Access.AUTHORIZED); - return this; - } - - /** - * Registers a handler for the given route with an access level. - * - * @param route the route to register a handler for. - * @param handler the handler to be registered for the given route with the access level. - * @param access specifies the authorization level required to access the route. - * @return the updated protocol specification for fluent use. - */ - public Protocol use(String route, Handler handler, Access access) { - handlers.use(route, handler, access); - return this; - } - - /** - * @return returns a list of all registered routes on the protoocol. - */ - public ProtocolMapping list() { - return handlers.list(); - } - - /** - * Creates a response object given a response status. - * - * @param status the status to create the response from. - * @return a JSON encoded response packed in a buffer. - */ - public static JsonObject response(ResponseStatus status) { - return new JsonObject() - .put(PROTOCOL_STATUS, status); - } - - /** - * Creates a response object given a response status and a throwable. - * - * @param status the status to include in the response. - * @param e an exception that was the cause of an abnormal response status. - * @return a JSON encoded response packed in a buffer. - */ - public static JsonObject response(ResponseStatus status, Throwable e) { - return new JsonObject() - .put(PROTOCOL_STATUS, status) - .put(PROTOCOL_MESSAGE, e.getMessage()); - } -} - +package com.codingchili.core.protocol; + +import com.codingchili.core.listener.CoreHandler; +import com.codingchili.core.listener.Request; +import com.codingchili.core.protocol.exception.AuthorizationRequiredException; +import com.codingchili.core.protocol.exception.HandlerMissingException; +import io.vertx.core.json.JsonObject; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static com.codingchili.core.configuration.CoreStrings.*; + +/** + * @author Robin Duda + *

+ * Maps packet data to handlers and manages authentication status for handlers. + */ +public class Protocol { + private final AuthorizationHandler handlers = new AuthorizationHandler<>(); + + public Protocol() {} + + /** + * Creates a protocol by mapping @Public and @Private annotated methods. + * + * @param handler contains methods to be mapped. + */ + public Protocol(CoreHandler handler) { + Method[] methods = handler.getClass().getDeclaredMethods(); + + for (Method method : methods) { + Annotation annotation = method.getAnnotation(Public.class); + + if (annotation != null) { + use(((Public) annotation).value(), wrap(handler, method), Access.PUBLIC); + } else { + annotation = method.getAnnotation(Private.class); + if (annotation != null) { + use(((Private) annotation).value(), wrap(handler, method), Access.AUTHORIZED); + } + } + } + } + + @SuppressWarnings("unchecked") + private Handler wrap(Object coreHandler, Method method) { + return (Handler) new RequestHandler() { + @Override + public void handle(Request request) { + try { + method.invoke(coreHandler, request); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + }; + } + + /** + * Returns the route handler for the given target route and its access level. + * + * @param access the access level the request is valid for. + * @param route the handler route to find + * @return the handler that is mapped to the route and access level. + * @throws AuthorizationRequiredException when authorization level is not fulfilled for given route. + * @throws HandlerMissingException when the requested route handler is not registered. + */ + public Handler get(Access access, String route) throws AuthorizationRequiredException, HandlerMissingException { + if (handlers.contains(route)) { + return handlers.get(route, access); + } else { + return handlers.get(ANY, access); + } + } + + /** + * Returns the route handler for the given target route and its access level. + * + * @param route the handler route to find. + * @return the handler that is mapped to the route. + * @throws AuthorizationRequiredException when authorization level is not fulfilled for the given route. + * @throws HandlerMissingException when the requested route handler is not registered. + */ + public Handler get(String route) throws AuthorizationRequiredException, HandlerMissingException { + return get(Access.AUTHORIZED, route); + } + + /** + * Registers a handler for the given route. + * + * @param route the route to register a handler for. + * @param handler the handler to be registered for the given route. + * @return the updated protocol specification for fluent use. + */ + public Protocol use(String route, Handler handler) { + use(route, handler, Access.AUTHORIZED); + return this; + } + + /** + * Registers a handler for the given route with an access level. + * + * @param route the route to register a handler for. + * @param handler the handler to be registered for the given route with the access level. + * @param access specifies the authorization level required to access the route. + * @return the updated protocol specification for fluent use. + */ + public Protocol use(String route, Handler handler, Access access) { + handlers.use(route, handler, access); + return this; + } + + /** + * @return returns a list of all registered routes on the protoocol. + */ + public ProtocolMapping list() { + return handlers.list(); + } + + /** + * Creates a response object given a response status. + * + * @param status the status to create the response from. + * @return a JSON encoded response packed in a buffer. + */ + public static JsonObject response(ResponseStatus status) { + return new JsonObject() + .put(PROTOCOL_STATUS, status); + } + + /** + * Creates a response object given a response status and a throwable. + * + * @param status the status to include in the response. + * @param e an exception that was the cause of an abnormal response status. + * @return a JSON encoded response packed in a buffer. + */ + public static JsonObject response(ResponseStatus status, Throwable e) { + return new JsonObject() + .put(PROTOCOL_STATUS, status) + .put(PROTOCOL_MESSAGE, e.getMessage()); + } +} + diff --git a/core/main/java/com/codingchili/core/protocol/Public.java b/core/main/java/com/codingchili/core/protocol/Public.java new file mode 100644 index 00000000..398c10b8 --- /dev/null +++ b/core/main/java/com/codingchili/core/protocol/Public.java @@ -0,0 +1,15 @@ +package com.codingchili.core.protocol; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated method does not require authentication. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Public { + public String value(); +} diff --git a/core/test/java/com/codingchili/core/protocol/ProtocolTest.java b/core/test/java/com/codingchili/core/protocol/ProtocolTest.java index 0f103a3f..c11e3ec6 100644 --- a/core/test/java/com/codingchili/core/protocol/ProtocolTest.java +++ b/core/test/java/com/codingchili/core/protocol/ProtocolTest.java @@ -1,114 +1,221 @@ -package com.codingchili.core.protocol; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import com.codingchili.core.listener.Request; -import com.codingchili.core.protocol.exception.AuthorizationRequiredException; -import com.codingchili.core.protocol.exception.HandlerMissingException; -import com.codingchili.core.testing.EmptyRequest; - -import io.vertx.ext.unit.Async; -import io.vertx.ext.unit.TestContext; -import io.vertx.ext.unit.junit.VertxUnitRunner; - -import static com.codingchili.core.protocol.Access.*; - -/** - * @author Robin Duda - *

- * Tests the protocol class and its authorization mechanism. - */ -@RunWith(VertxUnitRunner.class) -public class ProtocolTest { - private static final String TEST = "test"; - private static final String ANOTHER = "another"; - private Protocol> protocol; - - @Before - public void setUp() { - protocol = new Protocol<>(); - } - - @Test - public void testHandlerMissing(TestContext test) throws Exception { - Async async = test.async(); - - try { - protocol.use(TEST, Request::accept, PUBLIC) - .get(PUBLIC, ANOTHER).handle(new EmptyRequest()); - - test.fail("Should throw handler missing exception."); - } catch (HandlerMissingException e) { - async.complete(); - } - } - - @Test - public void testPrivateRouteNoAccess(TestContext test) throws Exception { - Async async = test.async(); - - try { - protocol.use(TEST, Request::accept, AUTHORIZED) - .get(PUBLIC, TEST).handle(new EmptyRequest()); - - test.fail("Should throw authorization exception."); - } catch (AuthorizationRequiredException e) { - async.complete(); - } - } - - @Test - public void testPublicRouteWithAccess(TestContext test) throws Exception { - Async async = test.async(); - - protocol.use(TEST, Request::accept, PUBLIC) - .get(PUBLIC, TEST).handle(new EmptyRequest() { - @Override - public void accept() { - async.complete(); - } - }); - } - - @Test - public void testPrivateRoute(TestContext test) throws Exception { - Async async = test.async(); - - protocol.use(TEST, Request::accept, AUTHORIZED) - .get(AUTHORIZED, TEST).handle(new EmptyRequest() { - @Override - public void accept() { - async.complete(); - } - }); - } - - @Test - public void testPublicRoute(TestContext test) throws Exception { - Async async = test.async(); - - protocol.use(TEST, Request::accept, PUBLIC) - .get(PUBLIC, TEST).handle(new EmptyRequest() { - @Override - public void accept() { - async.complete(); - } - }); - } - - @Test - public void testListRoutes(TestContext test) { - protocol.use(TEST, Request::accept, PUBLIC) - .use(ANOTHER, Request::accept, AUTHORIZED); - - ProtocolMapping mapping = protocol.list(); - - test.assertEquals(2, mapping.getRoutes().size()); - test.assertEquals(AUTHORIZED, mapping.getRoutes().get(0).getAccess()); - test.assertEquals(ANOTHER, mapping.getRoutes().get(0).getRoute()); - test.assertEquals(PUBLIC, mapping.getRoutes().get(1).getAccess()); - test.assertEquals(TEST, mapping.getRoutes().get(1).getRoute()); - } -} +package com.codingchili.core.protocol; + +import com.codingchili.core.configuration.CoreStrings; +import com.codingchili.core.listener.BaseRequest; +import com.codingchili.core.listener.CoreHandler; +import com.codingchili.core.listener.Request; +import com.codingchili.core.protocol.exception.AuthorizationRequiredException; +import com.codingchili.core.protocol.exception.HandlerMissingException; +import com.codingchili.core.testing.EmptyRequest; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.Consumer; + +import static com.codingchili.core.protocol.Access.AUTHORIZED; +import static com.codingchili.core.protocol.Access.PUBLIC; +import static com.codingchili.core.protocol.ProtocolTest.AnnotatedRouter.ADDRESS; +import static com.codingchili.core.protocol.ProtocolTest.AnnotatedRouter.PRIVATE_ROUTE; +import static com.codingchili.core.protocol.ProtocolTest.AnnotatedRouter.PUBLIC_ROUTE; + +/** + * @author Robin Duda + *

+ * Tests the protocol class and its authorization mechanism. + */ +@RunWith(VertxUnitRunner.class) +public class ProtocolTest { + private static final String TEST = "test"; + private static final String ANOTHER = "another"; + private Protocol> protocol; + + @Before + public void setUp() { + protocol = new Protocol<>(); + } + + @Test + public void testHandlerMissing(TestContext test) throws Exception { + Async async = test.async(); + + try { + protocol.use(TEST, Request::accept, PUBLIC) + .get(PUBLIC, ANOTHER).handle(new EmptyRequest()); + + test.fail("Should throw handler missing exception."); + } catch (HandlerMissingException e) { + async.complete(); + } + } + + @Test + public void testPrivateRouteNoAccess(TestContext test) throws Exception { + Async async = test.async(); + + try { + protocol.use(TEST, Request::accept, AUTHORIZED) + .get(PUBLIC, TEST).handle(new EmptyRequest()); + + test.fail("Should throw authorization exception."); + } catch (AuthorizationRequiredException e) { + async.complete(); + } + } + + @Test + public void testPublicRouteWithAccess(TestContext test) throws Exception { + Async async = test.async(); + + protocol.use(TEST, Request::accept, PUBLIC) + .get(PUBLIC, TEST).handle(new EmptyRequest() { + @Override + public void accept() { + async.complete(); + } + }); + } + + @Test + public void testPrivateRoute(TestContext test) throws Exception { + Async async = test.async(); + + protocol.use(TEST, Request::accept, AUTHORIZED) + .get(AUTHORIZED, TEST).handle(new EmptyRequest() { + @Override + public void accept() { + async.complete(); + } + }); + } + + @Test + public void testPublicRoute(TestContext test) throws Exception { + Async async = test.async(); + + protocol.use(TEST, Request::accept, PUBLIC) + .get(PUBLIC, TEST).handle(new EmptyRequest() { + @Override + public void accept() { + async.complete(); + } + }); + } + + @Test + public void testAnnotatedPublic(TestContext test) { + new AnnotatedRouter().handle(new TestRequest((response) -> { + test.assertEquals(response, PUBLIC_ROUTE); + }, PUBLIC_ROUTE, Access.PUBLIC)); + } + + @Test + public void testAnnotatedPrivate(TestContext test) { + try { + new AnnotatedRouter().handle(new TestRequest((response) -> { + test.assertEquals(response, PRIVATE_ROUTE); + }, PRIVATE_ROUTE, Access.PUBLIC)); + test.fail("Unauthorized call did not fail."); + } catch (AuthorizationRequiredException ignored) { + } + } + + @Test + public void testCoreHandlerMissingAddress(TestContext test) { + try { + new AnnotatedRouterNoAddress().address(); + test.fail("Test case must fail when implementing class fails to provide address."); + } catch (RuntimeException ignored) { + } + } + + @Test + public void testCoreHandlerAnnotatedAddress(TestContext test) { + test.assertEquals(new AnnotatedRouter().address(), ADDRESS); + } + + @Test + public void testListRoutes(TestContext test) { + protocol.use(TEST, Request::accept, PUBLIC) + .use(ANOTHER, Request::accept, AUTHORIZED); + + ProtocolMapping mapping = protocol.list(); + + test.assertEquals(2, mapping.getRoutes().size()); + test.assertEquals(AUTHORIZED, mapping.getRoutes().get(0).getAccess()); + test.assertEquals(ANOTHER, mapping.getRoutes().get(0).getRoute()); + test.assertEquals(PUBLIC, mapping.getRoutes().get(1).getAccess()); + test.assertEquals(TEST, mapping.getRoutes().get(1).getRoute()); + } + + @Address(AnnotatedRouter.ADDRESS) + public class AnnotatedRouter implements CoreHandler { + static final String PUBLIC_ROUTE = "public"; + static final String PRIVATE_ROUTE = "private"; + static final String ADDRESS = "home"; + private Protocol> protocol = new Protocol<>(this); + + @Public(PUBLIC_ROUTE) + public void route(TestRequest request) { + request.write(PUBLIC_ROUTE); + } + + @Private(PRIVATE_ROUTE) + public void route2(TestRequest request) { + request.write(PRIVATE_ROUTE); + } + + @Override + public void handle(Request request) { + protocol.get(((TestRequest) request).authorized(), request.route()).handle(request); + } + } + + // no @Address annotation and does not implement getAddress. + public class AnnotatedRouterNoAddress implements CoreHandler { + @Override + public void handle(Request request) { + // + } + } + + public class TestRequest extends BaseRequest { + private Consumer listener; + private Access access; + private String route; + + public TestRequest(Consumer listener, String route, Access access) { + this.listener = listener; + this.route = route; + this.access = access; + } + + public Access authorized() { + return access; + } + + @Override + public void write(Object object) { + listener.accept(object.toString()); + } + + @Override + public JsonObject data() { + return new JsonObject().put(CoreStrings.PROTOCOL_ROUTE, route); + } + + @Override + public int timeout() { + return 0; + } + + @Override + public int size() { + return 0; + } + } +}