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;
+ }
+ }
+}