From 8ba9dfced6070efe66385555d39d0ff7d8877a24 Mon Sep 17 00:00:00 2001 From: Thomas Rausch Date: Wed, 4 Apr 2018 04:10:34 +0200 Subject: [PATCH] Add template for assignment 2 --- ass2-aop/pom.xml | 45 +++ .../java/dst/ass2/aop/IPluginExecutable.java | 17 ++ .../java/dst/ass2/aop/IPluginExecutor.java | 40 +++ .../dst/ass2/aop/PluginExecutorFactory.java | 10 + .../java/dst/ass2/aop/logging/Invisible.java | 11 + .../dst/ass2/aop/logging/LoggingAspect.java | 7 + .../ass2/aop/management/ManagementAspect.java | 7 + .../java/dst/ass2/aop/management/Timeout.java | 12 + .../test/java/dst/ass2/aop/event/Event.java | 74 +++++ .../java/dst/ass2/aop/event/EventBus.java | 135 ++++++++ .../dst/ass2/aop/event/EventBusHandler.java | 38 +++ .../java/dst/ass2/aop/event/EventType.java | 5 + .../aop/sample/AbstractPluginExecutable.java | 28 ++ .../sample/InterruptedPluginExecutable.java | 37 +++ .../aop/sample/InvisiblePluginExecutable.java | 11 + .../aop/sample/LoggingPluginExecutable.java | 9 + .../aop/sample/SystemOutPluginExecutable.java | 4 + .../java/dst/ass2/aop/tests/Ass2_4_1Test.java | 151 +++++++++ .../java/dst/ass2/aop/tests/Ass2_4_2Test.java | 180 +++++++++++ .../java/dst/ass2/aop/tests/Ass2_4_3Test.java | 90 ++++++ .../java/dst/ass2/aop/tests/Ass2_4_Suite.java | 13 + .../test/java/dst/ass2/aop/util/JarUtils.java | 60 ++++ .../java/dst/ass2/aop/util/PluginUtils.java | 275 +++++++++++++++++ ass2-aop/src/test/resources/all.zip | Bin 0 -> 2932 bytes ass2-aop/src/test/resources/simple.zip | Bin 0 -> 1730 bytes ass2-di/pom.xml | 32 ++ .../dst/ass2/di/IInjectionController.java | 28 ++ .../ass2/di/InjectionControllerFactory.java | 52 ++++ .../java/dst/ass2/di/InjectionException.java | 33 ++ .../java/dst/ass2/di/agent/InjectorAgent.java | 18 ++ .../dst/ass2/di/annotation/Component.java | 12 + .../dst/ass2/di/annotation/ComponentId.java | 11 + .../java/dst/ass2/di/annotation/Inject.java | 14 + .../java/dst/ass2/di/annotation/Scope.java | 7 + .../java/dst/ass2/di/BasicInjectionTest.java | 116 +++++++ .../test/java/dst/ass2/di/InjectionUtils.java | 170 +++++++++++ .../dst/ass2/di/SpecialInjectionTest.java | 164 ++++++++++ .../dst/ass2/di/TransparentInjectionTest.java | 84 +++++ .../dst/ass2/di/type/ComplexComponent.java | 26 ++ .../test/java/dst/ass2/di/type/Container.java | 22 ++ .../test/java/dst/ass2/di/type/Invalid.java | 9 + .../dst/ass2/di/type/SimpleComponent.java | 12 + .../dst/ass2/di/type/SimpleSingleton.java | 12 + .../test/java/dst/ass2/di/type/SubType.java | 15 + .../test/java/dst/ass2/di/type/SuperType.java | 15 + ass2-service/api/pom.xml | 47 +++ .../api/auth/AuthenticationException.java | 24 ++ .../api/auth/IAuthenticationService.java | 52 ++++ .../service/api/auth/NoSuchUserException.java | 25 ++ .../auth/rest/IAuthenticationResource.java | 18 ++ .../CourseNotAvailableException.java | 16 + .../service/api/courseplan/CoursePlan.java | 41 +++ .../courseplan/EntityNotFoundException.java | 17 ++ .../api/courseplan/ICoursePlanService.java | 68 +++++ .../courseplan/rest/ICoursePlanResource.java | 34 +++ .../ass2/service/api/auth/proto/auth.proto | 3 + .../proto/auth/ProtoSpecificationTest.java | 66 ++++ ass2-service/auth-client/pom.xml | 72 +++++ .../AuthenticationClientProperties.java | 37 +++ .../auth/client/IAuthenticationClient.java | 17 ++ .../client/impl/GrpcAuthenticationClient.java | 32 ++ .../auth/client/AuthenticationClientTest.java | 76 +++++ ass2-service/auth/pom.xml | 80 +++++ .../auth/ICachingAuthenticationService.java | 30 ++ .../auth/grpc/GrpcServerProperties.java | 26 ++ .../service/auth/grpc/IGrpcServerRunner.java | 16 + .../auth/src/main/resources/logback.xml | 14 + .../AuthenticationServiceApplication.java | 15 + ...uthenticationServiceApplicationConfig.java | 116 +++++++ .../service/auth/SpringGrpcServerRunner.java | 32 ++ .../ass2/service/auth/TestDataInserter.java | 30 ++ .../auth/tests/AuthenticationServiceTest.java | 138 +++++++++ .../auth/tests/GrpcServerRunnerTest.java | 43 +++ .../dst/ass2/service/auth/grpc.properties | 1 + ass2-service/courseplan/pom.xml | 57 ++++ .../dst/ass2/service/courseplan/impl/.gitkeep | 0 .../courseplan/CoursePlanApplication.java | 14 + .../CoursePlanApplicationConfig.java | 58 ++++ .../service/courseplan/TestDataConfig.java | 39 +++ .../service/courseplan/TestDataInserter.java | 34 +++ .../tests/CoursePlanResourceTest.java | 231 ++++++++++++++ .../tests/CoursePlanServiceTest.java | 288 ++++++++++++++++++ .../src/test/resources/application.properties | 1 + ass2-service/facade/pom.xml | 53 ++++ .../dst/ass2/service/facade/impl/.gitkeep | 0 .../facade/ServiceFacadeApplication.java | 13 + .../ServiceFacadeApplicationConfig.java | 82 +++++ .../test/AuthenticationResourceTest.java | 97 ++++++ .../src/test/resources/application.properties | 8 + .../facade/src/test/resources/logback.xml | 14 + pom.xml | 174 +++++++++++ 91 files changed, 4460 insertions(+) create mode 100644 ass2-aop/pom.xml create mode 100644 ass2-aop/src/main/java/dst/ass2/aop/IPluginExecutable.java create mode 100644 ass2-aop/src/main/java/dst/ass2/aop/IPluginExecutor.java create mode 100644 ass2-aop/src/main/java/dst/ass2/aop/PluginExecutorFactory.java create mode 100644 ass2-aop/src/main/java/dst/ass2/aop/logging/Invisible.java create mode 100644 ass2-aop/src/main/java/dst/ass2/aop/logging/LoggingAspect.java create mode 100644 ass2-aop/src/main/java/dst/ass2/aop/management/ManagementAspect.java create mode 100644 ass2-aop/src/main/java/dst/ass2/aop/management/Timeout.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/event/Event.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/event/EventBus.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/event/EventBusHandler.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/event/EventType.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/sample/AbstractPluginExecutable.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/sample/InterruptedPluginExecutable.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/sample/InvisiblePluginExecutable.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/sample/LoggingPluginExecutable.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/sample/SystemOutPluginExecutable.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_1Test.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_2Test.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_3Test.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_Suite.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/util/JarUtils.java create mode 100644 ass2-aop/src/test/java/dst/ass2/aop/util/PluginUtils.java create mode 100644 ass2-aop/src/test/resources/all.zip create mode 100644 ass2-aop/src/test/resources/simple.zip create mode 100644 ass2-di/pom.xml create mode 100644 ass2-di/src/main/java/dst/ass2/di/IInjectionController.java create mode 100644 ass2-di/src/main/java/dst/ass2/di/InjectionControllerFactory.java create mode 100644 ass2-di/src/main/java/dst/ass2/di/InjectionException.java create mode 100644 ass2-di/src/main/java/dst/ass2/di/agent/InjectorAgent.java create mode 100644 ass2-di/src/main/java/dst/ass2/di/annotation/Component.java create mode 100644 ass2-di/src/main/java/dst/ass2/di/annotation/ComponentId.java create mode 100644 ass2-di/src/main/java/dst/ass2/di/annotation/Inject.java create mode 100644 ass2-di/src/main/java/dst/ass2/di/annotation/Scope.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/BasicInjectionTest.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/InjectionUtils.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/SpecialInjectionTest.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/TransparentInjectionTest.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/type/ComplexComponent.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/type/Container.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/type/Invalid.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/type/SimpleComponent.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/type/SimpleSingleton.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/type/SubType.java create mode 100644 ass2-di/src/test/java/dst/ass2/di/type/SuperType.java create mode 100644 ass2-service/api/pom.xml create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/auth/AuthenticationException.java create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/auth/IAuthenticationService.java create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/auth/NoSuchUserException.java create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/auth/rest/IAuthenticationResource.java create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/CourseNotAvailableException.java create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/CoursePlan.java create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/EntityNotFoundException.java create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/ICoursePlanService.java create mode 100644 ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/rest/ICoursePlanResource.java create mode 100644 ass2-service/api/src/main/proto/dst/ass2/service/api/auth/proto/auth.proto create mode 100644 ass2-service/api/src/test/java/dst/ass2/proto/auth/ProtoSpecificationTest.java create mode 100644 ass2-service/auth-client/pom.xml create mode 100644 ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/AuthenticationClientProperties.java create mode 100644 ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/IAuthenticationClient.java create mode 100644 ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/impl/GrpcAuthenticationClient.java create mode 100644 ass2-service/auth-client/src/test/java/dst/ass2/service/auth/client/AuthenticationClientTest.java create mode 100644 ass2-service/auth/pom.xml create mode 100644 ass2-service/auth/src/main/java/dst/ass2/service/auth/ICachingAuthenticationService.java create mode 100644 ass2-service/auth/src/main/java/dst/ass2/service/auth/grpc/GrpcServerProperties.java create mode 100644 ass2-service/auth/src/main/java/dst/ass2/service/auth/grpc/IGrpcServerRunner.java create mode 100644 ass2-service/auth/src/main/resources/logback.xml create mode 100644 ass2-service/auth/src/test/java/dst/ass2/service/auth/AuthenticationServiceApplication.java create mode 100644 ass2-service/auth/src/test/java/dst/ass2/service/auth/AuthenticationServiceApplicationConfig.java create mode 100644 ass2-service/auth/src/test/java/dst/ass2/service/auth/SpringGrpcServerRunner.java create mode 100644 ass2-service/auth/src/test/java/dst/ass2/service/auth/TestDataInserter.java create mode 100644 ass2-service/auth/src/test/java/dst/ass2/service/auth/tests/AuthenticationServiceTest.java create mode 100644 ass2-service/auth/src/test/java/dst/ass2/service/auth/tests/GrpcServerRunnerTest.java create mode 100644 ass2-service/auth/src/test/resources/dst/ass2/service/auth/grpc.properties create mode 100644 ass2-service/courseplan/pom.xml create mode 100644 ass2-service/courseplan/src/main/java/dst/ass2/service/courseplan/impl/.gitkeep create mode 100644 ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/CoursePlanApplication.java create mode 100644 ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/CoursePlanApplicationConfig.java create mode 100644 ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/TestDataConfig.java create mode 100644 ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/TestDataInserter.java create mode 100644 ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/tests/CoursePlanResourceTest.java create mode 100644 ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/tests/CoursePlanServiceTest.java create mode 100644 ass2-service/courseplan/src/test/resources/application.properties create mode 100644 ass2-service/facade/pom.xml create mode 100644 ass2-service/facade/src/main/java/dst/ass2/service/facade/impl/.gitkeep create mode 100644 ass2-service/facade/src/test/java/dst/ass2/service/facade/ServiceFacadeApplication.java create mode 100644 ass2-service/facade/src/test/java/dst/ass2/service/facade/ServiceFacadeApplicationConfig.java create mode 100644 ass2-service/facade/src/test/java/dst/ass2/service/facade/test/AuthenticationResourceTest.java create mode 100644 ass2-service/facade/src/test/resources/application.properties create mode 100644 ass2-service/facade/src/test/resources/logback.xml diff --git a/ass2-aop/pom.xml b/ass2-aop/pom.xml new file mode 100644 index 0000000..57d06a1 --- /dev/null +++ b/ass2-aop/pom.xml @@ -0,0 +1,45 @@ + + + + 4.0.0 + + + at.ac.tuwien.infosys.dst + dst + 2018.1 + .. + + + ass2-aop + + jar + + DST :: Assignment 2 :: Aspect-oriented Programming + + + + + commons-io + commons-io + + + org.aspectj + aspectjrt + + + org.aspectj + aspectjweaver + + + org.springframework + spring-aop + + + org.apache.commons + commons-lang3 + + + + diff --git a/ass2-aop/src/main/java/dst/ass2/aop/IPluginExecutable.java b/ass2-aop/src/main/java/dst/ass2/aop/IPluginExecutable.java new file mode 100644 index 0000000..ea07690 --- /dev/null +++ b/ass2-aop/src/main/java/dst/ass2/aop/IPluginExecutable.java @@ -0,0 +1,17 @@ +package dst.ass2.aop; + +/** + * Implementations of this interface are executable by the IPluginExecutor. + */ +public interface IPluginExecutable { + + /** + * Called when this plugin is executed. + */ + void execute(); + + /** + * Called when the execution of the plugin is interrupted + */ + void interrupted(); +} diff --git a/ass2-aop/src/main/java/dst/ass2/aop/IPluginExecutor.java b/ass2-aop/src/main/java/dst/ass2/aop/IPluginExecutor.java new file mode 100644 index 0000000..3bb74ee --- /dev/null +++ b/ass2-aop/src/main/java/dst/ass2/aop/IPluginExecutor.java @@ -0,0 +1,40 @@ +package dst.ass2.aop; + +import java.io.File; + +/** + * The plugin executor interface. + */ +public interface IPluginExecutor { + + /** + * Adds a directory to monitor. + * May be called before and also after start has been called. + * + * @param dir the directory to monitor. + */ + void monitor(File dir); + + /** + * Stops monitoring the specified directory. + * May be called before and also after start has been called. + * + * @param dir the directory which should not be monitored anymore. + */ + void stopMonitoring(File dir); + + /** + * Starts the plugin executor. + * All added directories will be monitored and any .jar file processed. + * If there are any {@link IPluginExecutable} implementations, + * they are executed within own threads. + */ + void start(); + + /** + * Stops the plugin executor. + * The monitoring of directories and the execution + * of the plugins should stop as soon as possible. + */ + void stop(); +} diff --git a/ass2-aop/src/main/java/dst/ass2/aop/PluginExecutorFactory.java b/ass2-aop/src/main/java/dst/ass2/aop/PluginExecutorFactory.java new file mode 100644 index 0000000..f0fcf1e --- /dev/null +++ b/ass2-aop/src/main/java/dst/ass2/aop/PluginExecutorFactory.java @@ -0,0 +1,10 @@ +package dst.ass2.aop; + +public class PluginExecutorFactory { + + public static IPluginExecutor createPluginExecutor() { + // TODO + return null; + } + +} diff --git a/ass2-aop/src/main/java/dst/ass2/aop/logging/Invisible.java b/ass2-aop/src/main/java/dst/ass2/aop/logging/Invisible.java new file mode 100644 index 0000000..12b0b9e --- /dev/null +++ b/ass2-aop/src/main/java/dst/ass2/aop/logging/Invisible.java @@ -0,0 +1,11 @@ +package dst.ass2.aop.logging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Invisible { +} diff --git a/ass2-aop/src/main/java/dst/ass2/aop/logging/LoggingAspect.java b/ass2-aop/src/main/java/dst/ass2/aop/logging/LoggingAspect.java new file mode 100644 index 0000000..c0336c6 --- /dev/null +++ b/ass2-aop/src/main/java/dst/ass2/aop/logging/LoggingAspect.java @@ -0,0 +1,7 @@ +package dst.ass2.aop.logging; + +public class LoggingAspect { + + // TODO + +} diff --git a/ass2-aop/src/main/java/dst/ass2/aop/management/ManagementAspect.java b/ass2-aop/src/main/java/dst/ass2/aop/management/ManagementAspect.java new file mode 100644 index 0000000..aba3eab --- /dev/null +++ b/ass2-aop/src/main/java/dst/ass2/aop/management/ManagementAspect.java @@ -0,0 +1,7 @@ +package dst.ass2.aop.management; + +public class ManagementAspect { + + // TODO + +} diff --git a/ass2-aop/src/main/java/dst/ass2/aop/management/Timeout.java b/ass2-aop/src/main/java/dst/ass2/aop/management/Timeout.java new file mode 100644 index 0000000..aa190bb --- /dev/null +++ b/ass2-aop/src/main/java/dst/ass2/aop/management/Timeout.java @@ -0,0 +1,12 @@ +package dst.ass2.aop.management; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Timeout { + long value(); +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/event/Event.java b/ass2-aop/src/test/java/dst/ass2/aop/event/Event.java new file mode 100644 index 0000000..5cfaa4d --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/event/Event.java @@ -0,0 +1,74 @@ +package dst.ass2.aop.event; + +import org.springframework.util.Assert; + +import dst.ass2.aop.IPluginExecutable; + +/** + * Events triggered by {@link IPluginExecutable}s. + */ +public class Event { + private final long time = System.currentTimeMillis(); + private Class pluginClass; + private EventType type; + private String message; + + public Event(EventType type, Class pluginClass, String message) { + this.type = type; + this.pluginClass = pluginClass; + this.message = message; + + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + int pos = stackTrace[1].getMethodName().equals("") ? 1 : 2; + Assert.state(stackTrace[pos].getMethodName().equals(""), "Invalid Event Creation"); + Assert.state(stackTrace[pos + 1].getClassName().equals(EventBus.class.getName()), "Invalid Event Creation"); + Assert.state(stackTrace[pos + 1].getMethodName().equals("add"), "Invalid Event Creation"); + } + + /** + * Returns the time when the event occurred. + * + * @return the event creation time + */ + public long getTime() { + return time; + } + + /** + * Returns the type of the plugin that triggered the event. + * + * @return the plugin type + */ + public Class getPluginClass() { + return pluginClass; + } + + /** + * Returns the type of the event. + * + * @return the event type + */ + public EventType getType() { + return type; + } + + /** + * Returns the message of the event + * + * @return the event message + */ + public String getMessage() { + return message; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Event"); + sb.append("{time=").append(time); + sb.append(", pluginClass=").append(pluginClass); + sb.append(", type=").append(type); + sb.append('}'); + return sb.toString(); + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/event/EventBus.java b/ass2-aop/src/test/java/dst/ass2/aop/event/EventBus.java new file mode 100644 index 0000000..741f8c1 --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/event/EventBus.java @@ -0,0 +1,135 @@ +package dst.ass2.aop.event; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.aop.support.AopUtils; + +import dst.ass2.aop.IPluginExecutable; + +/** + * Stateful event bus that stores events triggered by executable plugins. + *

+ * Note that this implementation is thread safe. + */ +public class EventBus { + private static final EventBus instance = new EventBus(); + private final List events = new ArrayList(); + + public static EventBus getInstance() { + return instance; + } + + private EventBus() { + } + + /** + * Returns all events of the certain type(s).
+ * If no types are specified, all events are returned instead. + * + * @param types the event types + * @return list of all events of the given types. + */ + public List getEvents(EventType... types) { + synchronized (events) { + if (types == null || types.length == 0) { + return new ArrayList(events); + } else { + List list = new ArrayList(); + for (Event event : events) { + for (EventType type : types) { + if (type == event.getType()) { + list.add(event); + } + } + } + return list; + } + } + } + + /** + * Resets the event bus by purging the event history. + */ + public synchronized void reset() { + synchronized (events) { + events.clear(); + } + } + + /** + * Adds a new event of a certain type triggered by the given plugin. + * + * @param type the event type + * @param pluginExecutable the plugin that triggered the event + * @param message the event message + */ + @SuppressWarnings("unchecked") + public void add(EventType type, IPluginExecutable pluginExecutable, String message) { + add(type, (Class) AopUtils.getTargetClass(pluginExecutable), message); + } + + /** + * Adds a new event of a certain type triggered by a plugin of the given type. + * + * @param type the event type + * @param pluginType the type of the plugin + * @param message the event message + */ + public void add(EventType type, Class pluginType, String message) { + Event event = new Event(type, pluginType, message); + synchronized (events) { + events.add(event); + } + } + + /** + * Returns the number of events of a certain type fired by this event bus. + * + * @param type the event type + * @return number of events + */ + public int count(EventType type) { + int counter = 0; + synchronized (events) { + for (Event event : events) { + if (event.getType() == type) { + counter++; + } + } + } + return counter; + } + + /** + * Returns the number of events fired so far. + * + * @return number of events + */ + public int size() { + return events.size(); + } + + /** + * Checks if there was at least one event of a certain type triggered by a plugin with the given full-qualified + * class name. + * + * If {@code pluginType} is {@code null}, the type of the plugin is not checked. The same is true for {@code type}. + * If all parameters are {@code null}, {@code true} is returned if there is at least one event. + * + * @param pluginType the class name of the plugin + * @param type the type of the event + * @return {@code true} if there is at least one event matching the criteria, {@code false} otherwise + */ + public boolean has(String pluginType, EventType type) { + synchronized (events) { + for (Event event : events) { + if ((pluginType == null || pluginType.equals(event.getPluginClass().getName())) + && (type == null || type == event.getType())) { + return true; + } + } + } + return false; + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/event/EventBusHandler.java b/ass2-aop/src/test/java/dst/ass2/aop/event/EventBusHandler.java new file mode 100644 index 0000000..aed173a --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/event/EventBusHandler.java @@ -0,0 +1,38 @@ +package dst.ass2.aop.event; + +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import dst.ass2.aop.IPluginExecutable; + +/** + * Logging handler that uses the {@link EventBus} for publishing events. + */ +public class EventBusHandler extends Handler { + @SuppressWarnings("unchecked") + @Override + public void publish(LogRecord record) { + if (record.getLoggerName().endsWith("PluginExecutable") && record.getMessage().contains("PluginExecutable")) { + try { + Class clazz = (Class) Class.forName(record.getLoggerName()); + EventBus.getInstance().add(EventType.INFO, clazz, record.getSourceClassName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Simply does nothing. + */ + @Override + public void flush() { + } + + /** + * Simply does nothing. + */ + @Override + public void close() { + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/event/EventType.java b/ass2-aop/src/test/java/dst/ass2/aop/event/EventType.java new file mode 100644 index 0000000..adee6b8 --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/event/EventType.java @@ -0,0 +1,5 @@ +package dst.ass2.aop.event; + +public enum EventType { + PLUGIN_START, PLUGIN_END, INFO +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/sample/AbstractPluginExecutable.java b/ass2-aop/src/test/java/dst/ass2/aop/sample/AbstractPluginExecutable.java new file mode 100644 index 0000000..be9b57e --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/sample/AbstractPluginExecutable.java @@ -0,0 +1,28 @@ +package dst.ass2.aop.sample; + +import org.springframework.aop.support.AopUtils; + +import dst.ass2.aop.IPluginExecutable; +import dst.ass2.aop.event.EventBus; +import dst.ass2.aop.event.EventType; + +public abstract class AbstractPluginExecutable implements IPluginExecutable { + @Override + public void execute() { + EventBus eventBus = EventBus.getInstance(); + eventBus.add(EventType.PLUGIN_START, this, AopUtils.getTargetClass(this).getSimpleName() + " is executed!"); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Should not happen but is not critical so the stack trace is printed to grab some attention ;-) + e.printStackTrace(); + } + + eventBus.add(EventType.PLUGIN_END, this, AopUtils.getTargetClass(this).getSimpleName() + " is finished!"); + } + + @Override + public void interrupted() { + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/sample/InterruptedPluginExecutable.java b/ass2-aop/src/test/java/dst/ass2/aop/sample/InterruptedPluginExecutable.java new file mode 100644 index 0000000..e935125 --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/sample/InterruptedPluginExecutable.java @@ -0,0 +1,37 @@ +package dst.ass2.aop.sample; + +import org.springframework.aop.support.AopUtils; + +import dst.ass2.aop.IPluginExecutable; +import dst.ass2.aop.event.EventBus; +import dst.ass2.aop.event.EventType; +import dst.ass2.aop.logging.Invisible; +import dst.ass2.aop.management.Timeout; + +public class InterruptedPluginExecutable implements IPluginExecutable { + private boolean interrupted = false; + + @Override + @Invisible + @Timeout(2000) + public void execute() { + EventBus eventBus = EventBus.getInstance(); + eventBus.add(EventType.PLUGIN_START, this, AopUtils.getTargetClass(this).getSimpleName() + " is executed!"); + + while (!interrupted) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // Should not happen but is not critical so the stack trace is printed to grab some attention ;-) + e.printStackTrace(); + } + } + + eventBus.add(EventType.PLUGIN_END, this, AopUtils.getTargetClass(this).getSimpleName() + " is finished!"); + } + + @Override + public void interrupted() { + interrupted = true; + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/sample/InvisiblePluginExecutable.java b/ass2-aop/src/test/java/dst/ass2/aop/sample/InvisiblePluginExecutable.java new file mode 100644 index 0000000..023f1ee --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/sample/InvisiblePluginExecutable.java @@ -0,0 +1,11 @@ +package dst.ass2.aop.sample; + +import dst.ass2.aop.logging.Invisible; + +public class InvisiblePluginExecutable extends AbstractPluginExecutable { + @Override + @Invisible + public void execute() { + super.execute(); + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/sample/LoggingPluginExecutable.java b/ass2-aop/src/test/java/dst/ass2/aop/sample/LoggingPluginExecutable.java new file mode 100644 index 0000000..a3caac7 --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/sample/LoggingPluginExecutable.java @@ -0,0 +1,9 @@ +package dst.ass2.aop.sample; + +import java.util.logging.Logger; + +public class LoggingPluginExecutable extends AbstractPluginExecutable { + @SuppressWarnings("unused") + private static Logger log = Logger.getLogger(LoggingPluginExecutable.class + .getName()); +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/sample/SystemOutPluginExecutable.java b/ass2-aop/src/test/java/dst/ass2/aop/sample/SystemOutPluginExecutable.java new file mode 100644 index 0000000..341dda4 --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/sample/SystemOutPluginExecutable.java @@ -0,0 +1,4 @@ +package dst.ass2.aop.sample; + +public class SystemOutPluginExecutable extends AbstractPluginExecutable { +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_1Test.java b/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_1Test.java new file mode 100644 index 0000000..e9e4232 --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_1Test.java @@ -0,0 +1,151 @@ +package dst.ass2.aop.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.List; + +import dst.ass2.aop.IPluginExecutor; +import dst.ass2.aop.PluginExecutorFactory; +import dst.ass2.aop.event.Event; +import dst.ass2.aop.event.EventBus; +import dst.ass2.aop.event.EventType; +import dst.ass2.aop.util.PluginUtils; +import org.apache.commons.io.FileUtils; +import org.junit.*; + +public class Ass2_4_1Test { + static final String SIMPLE_PLUGIN = "dst.ass2.aop.sample.SimplePluginExecutable"; + IPluginExecutor executor; + EventBus eventBus = EventBus.getInstance(); + + @BeforeClass + public static void beforeClass() { + Assert.assertEquals("Cannot create temporary plugin directory: " + PluginUtils.PLUGINS_DIR.getAbsolutePath(), + true, PluginUtils.PLUGINS_DIR.isDirectory() || PluginUtils.PLUGINS_DIR.mkdirs()); + } + + @AfterClass + public static void afterClass() throws IOException { + FileUtils.forceDeleteOnExit(PluginUtils.PLUGINS_DIR); + } + + @Before + public void before() { + PluginUtils.cleanPluginDirectory(); + executor = PluginExecutorFactory.createPluginExecutor(); + executor.monitor(PluginUtils.PLUGINS_DIR); + executor.start(); + eventBus.reset(); + } + + @After + public void after() { + executor.stop(); + eventBus.reset(); + PluginUtils.cleanPluginDirectory(); + } + + /** + * Executing plugin copied to plugin directory. + */ + @Test(timeout = PluginUtils.PLUGIN_TEST_TIMEOUT) + public void copiedPlugin_isExecutedCorrectly() throws Exception { + // Preparing new plugin + PluginUtils.preparePlugin(PluginUtils.SIMPLE_FILE); + + // Periodically check for the plugin to be executed + while (eventBus.size() != 2) { + Thread.sleep(100); + } + + // Verify that the plugin was started and stopped orderly + assertTrue(SIMPLE_PLUGIN + " was not started properly.", eventBus.has(SIMPLE_PLUGIN, EventType.PLUGIN_START)); + assertTrue(SIMPLE_PLUGIN + " did not finish properly.", eventBus.has(SIMPLE_PLUGIN, EventType.PLUGIN_END)); + } + + /** + * Checking that each plugin JAR uses its own ClassLoader. + */ + @Test(timeout = PluginUtils.PLUGIN_TEST_TIMEOUT) + public void samePlugins_useSeparateClassLoaders() throws Exception { + // Preparing two plugins + PluginUtils.preparePlugin(PluginUtils.SIMPLE_FILE); + PluginUtils.preparePlugin(PluginUtils.SIMPLE_FILE); + + // Periodically check for the plugins to be executed + while (eventBus.size() != 4) { + Thread.sleep(100); + } + + /* + * Verify that the plugins were loaded by different classloaders. + * This can be checked by comparing the ClassLoaders or comparing the classes themselves. + * In other words, if a class is loaded by two different ClassLoaders, it holds that + * a.getClass() != b.getClass() even if the byte code is identical. + */ + List events = eventBus.getEvents(EventType.PLUGIN_START); + String msg = "Both plugins where loaded by the same ClassLoader"; + assertNotSame(msg, events.get(0).getPluginClass().getClassLoader(), events.get(1).getPluginClass().getClassLoader()); + assertNotSame(msg, events.get(0).getPluginClass(), events.get(1).getPluginClass()); + } + + /** + * Checking whether two plugins in a single JAR are executed concurrently. + */ + @Test(timeout = PluginUtils.PLUGIN_TEST_TIMEOUT) + public void allPlugins_executeConcurrently() throws Exception { + // Start a plugin containing two IPluginExecutable classes + PluginUtils.preparePlugin(PluginUtils.ALL_FILE); + + // Periodically check for the plugins to be executed + while (eventBus.size() != 4) { + Thread.sleep(100); + } + + // Check that there is exactly one start and end event each + List starts = eventBus.getEvents(EventType.PLUGIN_START); + List ends = eventBus.getEvents(EventType.PLUGIN_END); + assertEquals("EventBus must contain exactly 2 start events.", 2, starts.size()); + assertEquals("EventBus must contain exactly 2 end events.", 2, ends.size()); + + // Verify that the plugins were started concurrently + String msg = "All plugins should have been started before the first ended - %d was after %d."; + for (Event end : ends) { + for (Event start : starts) { + assertTrue(String.format(msg, start.getTime(), end.getTime()), start.getTime() < end.getTime()); + } + } + } + + /** + * Checking whether two plugin JARs are executed concurrently. + */ + @Test(timeout = PluginUtils.PLUGIN_TEST_TIMEOUT) + public void multiplePlugins_executeConcurrently() throws Exception { + // Start two plugins at once + PluginUtils.preparePlugin(PluginUtils.SIMPLE_FILE); + PluginUtils.preparePlugin(PluginUtils.SIMPLE_FILE); + + // Periodically check for the plugins to be executed + while (eventBus.size() != 4) { + Thread.sleep(100); + } + + // Check that there is exactly one start and end event each + List starts = eventBus.getEvents(EventType.PLUGIN_START); + List ends = eventBus.getEvents(EventType.PLUGIN_END); + assertEquals("EventBus must contain exactly 2 start events.", 2, starts.size()); + assertEquals("EventBus must contain exactly 2 end events.", 2, ends.size()); + + // Verify that the plugins were started concurrently. + String msg = "All plugins should have been started before the first ended - %d was after %d."; + for (Event end : ends) { + for (Event start : starts) { + assertTrue(String.format(msg, start.getTime(), end.getTime()), start.getTime() < end.getTime()); + } + } + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_2Test.java b/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_2Test.java new file mode 100644 index 0000000..026ae8f --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_2Test.java @@ -0,0 +1,180 @@ +package dst.ass2.aop.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import dst.ass2.aop.IPluginExecutable; +import dst.ass2.aop.event.Event; +import dst.ass2.aop.event.EventBus; +import dst.ass2.aop.event.EventType; +import dst.ass2.aop.sample.InvisiblePluginExecutable; +import dst.ass2.aop.sample.LoggingPluginExecutable; +import dst.ass2.aop.sample.SystemOutPluginExecutable; +import dst.ass2.aop.util.PluginUtils; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.weaver.internal.tools.PointcutExpressionImpl; +import org.aspectj.weaver.tools.ShadowMatch; +import org.junit.Test; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.Advised; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import dst.ass2.aop.logging.Invisible; +import dst.ass2.aop.logging.LoggingAspect; + +public class Ass2_4_2Test { + final EventBus eventBus = EventBus.getInstance(); + + @org.junit.Before + @org.junit.After + public void beforeAndAfter() { + eventBus.reset(); + } + + /** + * Verifies that the {@link LoggingAspect} is a valid AspectJ aspect i.e., {@link Aspect @Aspect} as well as + * {@link Around @Around} or {@link Before @Before} / {@link After @After}. + */ + @Test + public void loggingAspect_isValid() { + Aspect aspect = AnnotationUtils.findAnnotation(LoggingAspect.class, Aspect.class); + assertNotNull("LoggingAspect is not annotated with @Aspect", aspect); + + Map around = PluginUtils.findMethodAnnotation(LoggingAspect.class, Around.class); + Map before = PluginUtils.findMethodAnnotation(LoggingAspect.class, Before.class); + Map after = PluginUtils.findMethodAnnotation(LoggingAspect.class, After.class); + + boolean found = !around.isEmpty() || (!before.isEmpty() && !after.isEmpty()); + assertTrue("LoggingAspect does not contain methods annotated with @Around OR @Before / @After", found); + } + + /** + * Verifies that the pointcut expression of the {@link LoggingAspect} does not match any method except the + * {@link IPluginExecutable#execute()} method. + */ + @Test + public void pointcutExpression_matchesCorrectly() { + IPluginExecutable executable = PluginUtils.getExecutable(LoggingPluginExecutable.class, LoggingAspect.class); + assertTrue("Executable must implement the Advised interface", executable instanceof Advised); + Advised advised = (Advised) executable; + + PointcutAdvisor pointcutAdvisor = PluginUtils.getPointcutAdvisor(advised); + assertNotNull("PointcutAdvisor not found because there is no pointcut or the pointcut does not match", pointcutAdvisor); + + String expression = PluginUtils.getBestExpression(advised); + assertTrue("Pointcut expression must include '" + IPluginExecutable.class.getName() + "'", expression.contains(IPluginExecutable.class.getName())); + assertTrue("Pointcut expression must include '" + PluginUtils.EXECUTE_METHOD.getName() + "'", expression.contains(PluginUtils.EXECUTE_METHOD.getName())); + + PointcutExpressionImpl pointcutExpression = PluginUtils.getPointcutExpression(advised); + ShadowMatch shadowMatch = pointcutExpression.matchesMethodExecution(PluginUtils.EXECUTE_METHOD); + assertTrue("Pointcut does not match IPluginExecute.execute()", shadowMatch.alwaysMatches()); + + shadowMatch = pointcutExpression.matchesMethodExecution(PluginUtils.INTERRUPTED_METHOD); + assertTrue("Pointcut must not match IPluginExecute.interrupted()", shadowMatch.neverMatches()); + + shadowMatch = pointcutExpression.matchesMethodExecution(ReflectionUtils.findMethod(getClass(), PluginUtils.EXECUTE_METHOD.getName())); + assertTrue("Pointcut must not match LoggingPluginTest.execute()", shadowMatch.neverMatches()); + } + + /** + * Verifies that the pointcut expression of the LoggingAspect contains the {@link Invisible @Invisible} annotation. + */ + @Test + public void pointcutExpression_containsInvisibleAnnotation() { + IPluginExecutable executable = PluginUtils.getExecutable(LoggingPluginExecutable.class, LoggingAspect.class); + Advised advised = (Advised) executable; + + String expression = PluginUtils.getBestExpression(advised); + String annotationName = Invisible.class.getName(); + assertTrue("Pointcut expression does not contain " + annotationName, expression.contains(annotationName)); + } + + /** + * Verifies that the pointcut expression of the {@link LoggingAspect} does not match any method annotated with + * {@link Invisible @Invisible}. + */ + @Test + public void pointcutExpression_doesNotMatchInvisible() { + IPluginExecutable executable = PluginUtils.getExecutable(LoggingPluginExecutable.class, LoggingAspect.class); + Advised advised = (Advised) executable; + + PointcutExpressionImpl pointcutExpression = PluginUtils.getPointcutExpression(advised); + + Method loggingMethod = ReflectionUtils.findMethod(LoggingPluginExecutable.class, PluginUtils.EXECUTE_METHOD.getName()); + ShadowMatch shadowMatch = pointcutExpression.matchesMethodExecution(loggingMethod); + assertTrue("Pointcut does not match LoggingPluginExecutable.execute()", shadowMatch.alwaysMatches()); + + Method invisibleMethod = ReflectionUtils.findMethod(InvisiblePluginExecutable.class, PluginUtils.EXECUTE_METHOD.getName()); + shadowMatch = pointcutExpression.matchesMethodExecution(invisibleMethod); + assertTrue("Pointcut matches InvisiblePluginExecutable.execute()", shadowMatch.neverMatches()); + } + + /** + * Tests if the {@link LoggingAspect} uses the {@link java.util.logging.Logger Logger} defined in the plugin. + */ + @Test + public void loggingAspect_usesLogger() { + IPluginExecutable executable = PluginUtils.getExecutable(LoggingPluginExecutable.class, LoggingAspect.class); + Advised advised = (Advised) executable; + + // Add handler end check that there are no events + PluginUtils.addBusHandlerIfNecessary(advised); + assertEquals("EventBus must be empty", 0, eventBus.count(EventType.INFO)); + + // Execute plugin and check that there are 2 events + executable.execute(); + List events = eventBus.getEvents(EventType.INFO); + assertEquals("EventBus must exactly contain 2 INFO events", 2, events.size()); + + // Check if the logger contains the correct class name + events = eventBus.getEvents(EventType.INFO); + for (Event event : events) { + assertEquals("Event message must contain the name of the " + LoggingAspect.class.getSimpleName(), LoggingAspect.class.getName(), event.getMessage()); + assertSame("Event must be logged for " + LoggingPluginExecutable.class.getSimpleName(), LoggingPluginExecutable.class, event.getPluginClass()); + } + } + + /** + * Tests if the {@link LoggingAspect} uses {@code System.out} if the plugin does not contain a + * {@link java.util.logging.Logger Logger} field. + * + * @throws IllegalAccessException if {@code System.out} cannot be modified (must not happen) + */ + @Test + public void loggingAspect_usesSystemOut() throws IllegalAccessException { + // Redirect System.out + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + PrintStream out = PluginUtils.setStaticFinalField(System.class, "out", new PrintStream(byteArrayOutputStream)); + try { + // Execute plugin + IPluginExecutable executable = PluginUtils.getExecutable(SystemOutPluginExecutable.class, LoggingAspect.class); + assertEquals("EventBus must be empty", 0, eventBus.size()); + executable.execute(); + assertEquals("EventBus must exactly contain 2 events", 2, eventBus.size()); + + // Verify that the log output contains the class name of the executed plugin + String output = byteArrayOutputStream.toString(); + assertTrue(String.format("Log output must contain %s\n\tbut was%s", SystemOutPluginExecutable.class.getName(), output), + output.contains(SystemOutPluginExecutable.class.getName())); + } finally { + // Reset System.out + PluginUtils.setStaticFinalField(System.class, "out", out); + } + } + + public void execute() { + + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_3Test.java b/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_3Test.java new file mode 100644 index 0000000..f13f8aa --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_3Test.java @@ -0,0 +1,90 @@ +package dst.ass2.aop.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import dst.ass2.aop.IPluginExecutable; +import dst.ass2.aop.event.Event; +import dst.ass2.aop.event.EventBus; +import dst.ass2.aop.sample.InterruptedPluginExecutable; +import dst.ass2.aop.util.PluginUtils; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.weaver.internal.tools.PointcutExpressionImpl; +import org.aspectj.weaver.tools.ShadowMatch; +import org.junit.Test; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.Advised; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import dst.ass2.aop.management.ManagementAspect; + +public class Ass2_4_3Test { + final EventBus eventBus = EventBus.getInstance(); + + @org.junit.Before + @org.junit.After + public void beforeAndAfter() { + eventBus.reset(); + } + + /** + * Verifies that the {@link ManagementAspect} is a valid AspectJ aspect i.e., {@link Aspect @Aspect} as well as + * {@link Around @Around} or {@link Before @Before} / {@link After @After}. + */ + @Test + public void managementAspect_isValid() { + Aspect aspect = AnnotationUtils.findAnnotation(ManagementAspect.class, Aspect.class); + assertNotNull("ManagementAspect is not annotated with @Aspect", aspect); + + Map around = PluginUtils.findMethodAnnotation(ManagementAspect.class, Around.class); + Map before = PluginUtils.findMethodAnnotation(ManagementAspect.class, Before.class); + Map after = PluginUtils.findMethodAnnotation(ManagementAspect.class, After.class); + + boolean found = !around.isEmpty() || (!before.isEmpty() && !after.isEmpty()); + assertEquals("ManagementAspect does not contain methods annotated with @Around OR @Before and @After", true, found); + } + + /** + * Verifies that the pointcut expression of the {@link ManagementAspect} + * does not match any method except the {@link IPluginExecutable#execute()} method. + */ + @Test + public void pointcutExpression_matchesCorrectly() { + IPluginExecutable executable = PluginUtils.getExecutable(InterruptedPluginExecutable.class, ManagementAspect.class); + assertEquals("Executable must implement the Advised interface", true, executable instanceof Advised); + Advised advised = (Advised) executable; + + PointcutAdvisor pointcutAdvisor = PluginUtils.getPointcutAdvisor(advised); + assertNotNull("PointcutAdvisor not found because there is no pointcut or the pointcut does not match", pointcutAdvisor); + + PointcutExpressionImpl pointcutExpression = PluginUtils.getPointcutExpression(advised); + Method interruptedMethod = ReflectionUtils.findMethod(InterruptedPluginExecutable.class, PluginUtils.EXECUTE_METHOD.getName()); + ShadowMatch shadowMatch = pointcutExpression.matchesMethodExecution(interruptedMethod); + assertEquals("Pointcut does not match InterruptedPluginExecutable.execute()", true, shadowMatch.alwaysMatches()); + } + + /** + * Tests if the {@link ManagementAspect} interrupts the plugin after the given timeout. + */ + @Test(timeout = PluginUtils.PLUGIN_TEST_TIMEOUT) + public void managementAspect_interruptsCorrectly() { + IPluginExecutable executable = PluginUtils.getExecutable(InterruptedPluginExecutable.class, ManagementAspect.class); + assertEquals("EventBus must be empty", 0, eventBus.size()); + executable.execute(); + + List events = eventBus.getEvents(); + assertEquals("EventBus must contain 2 events", 2, events.size()); + + long duration = events.get(1).getTime() - events.get(0).getTime(); + assertTrue("Plugin was not interrupted 2 seconds after starting it", duration < 3000); + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_Suite.java b/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_Suite.java new file mode 100644 index 0000000..8702bff --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/tests/Ass2_4_Suite.java @@ -0,0 +1,13 @@ +package dst.ass2.aop.tests; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + Ass2_4_1Test.class, + Ass2_4_2Test.class, + Ass2_4_3Test.class +}) +public class Ass2_4_Suite { +} \ No newline at end of file diff --git a/ass2-aop/src/test/java/dst/ass2/aop/util/JarUtils.java b/ass2-aop/src/test/java/dst/ass2/aop/util/JarUtils.java new file mode 100644 index 0000000..801decb --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/util/JarUtils.java @@ -0,0 +1,60 @@ +package dst.ass2.aop.util; + +import static org.apache.commons.io.FileUtils.openOutputStream; +import static org.apache.commons.io.IOUtils.copy; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.join; +import static org.springframework.util.ClassUtils.CLASS_FILE_SUFFIX; +import static org.springframework.util.ClassUtils.convertClassNameToResourcePath; + +import java.io.File; +import java.io.IOException; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.io.input.AutoCloseInputStream; +import org.springframework.core.io.ClassPathResource; + +/** + * Builds plugin JARs on demand. + * + * This class is for internal purposes only. + * Note that the {@link #main(String...)} method can be adjusted to create other plugins. + */ +public final class JarUtils { + private JarUtils() { + } + + public static void main(String... args) throws IOException { + String path = join(args, " "); + File dir = new File(defaultIfBlank(path, "ass2-aop/src/test/resources")); + + createJar(new File(dir, "simple.zip"), + "dst.ass2.aop.sample.SimplePluginExecutable" + ); + + createJar(new File(dir, "all.zip"), + "dst.ass2.aop.sample.SimplePluginExecutable", + "dst.ass2.aop.sample.IgnoredPluginExecutable" + ); + } + + /** + * Creates a new JAR file containing the given classes. + * + * @param jarFile the destination JAR file + * @param classes the classes to add + * @throws IOException if an I/O error has occurred + */ + public static void createJar(File jarFile, String... classes) throws IOException { + try (JarOutputStream stream = new JarOutputStream(openOutputStream(jarFile))) { + stream.setLevel(ZipOutputStream.STORED); + for (String clazz : classes) { + String path = convertClassNameToResourcePath(clazz) + CLASS_FILE_SUFFIX; + stream.putNextEntry(new JarEntry(path)); + copy(new AutoCloseInputStream(new ClassPathResource(path).getInputStream()), stream); + } + } + } +} diff --git a/ass2-aop/src/test/java/dst/ass2/aop/util/PluginUtils.java b/ass2-aop/src/test/java/dst/ass2/aop/util/PluginUtils.java new file mode 100644 index 0000000..f7591b8 --- /dev/null +++ b/ass2-aop/src/test/java/dst/ass2/aop/util/PluginUtils.java @@ -0,0 +1,275 @@ +package dst.ass2.aop.util; + +import static org.apache.commons.io.filefilter.FileFileFilter.FILE; +import static org.apache.commons.io.filefilter.FileFilterUtils.and; +import static org.apache.commons.io.filefilter.FileFilterUtils.or; +import static org.apache.commons.io.filefilter.FileFilterUtils.prefixFileFilter; +import static org.springframework.util.ReflectionUtils.findField; +import static org.springframework.util.ReflectionUtils.findMethod; +import static org.springframework.util.ReflectionUtils.getField; +import static org.springframework.util.ReflectionUtils.makeAccessible; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Handler; +import java.util.logging.Logger; + +import dst.ass2.aop.event.EventBusHandler; +import org.apache.commons.io.FileUtils; +import org.aspectj.weaver.internal.tools.PointcutExpressionImpl; +import org.aspectj.weaver.patterns.Pointcut; +import org.springframework.aop.Advisor; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.aspectj.AbstractAspectJAdvice; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.io.ClassPathResource; + +import dst.ass2.aop.IPluginExecutable; + +/** + * Contains some utility methods for plugins. + */ +public final class PluginUtils { + + public final static int PLUGIN_TEST_TIMEOUT = 30000; + + public static final File PLUGINS_DIR = new File( + FileUtils.getTempDirectoryPath(), "plugins_" + + System.currentTimeMillis()); + + public static File ALL_FILE; + public static File SIMPLE_FILE; + + public static final Method EXECUTE_METHOD = findMethod( + IPluginExecutable.class, "execute"); + public static final Method INTERRUPTED_METHOD = findMethod( + IPluginExecutable.class, "interrupted"); + + static { + try { + ALL_FILE = new ClassPathResource("all.zip").getFile(); + SIMPLE_FILE = new ClassPathResource("simple.zip").getFile(); + } catch (IOException e) { + throw new RuntimeException("Cannot locate plugin in classpath", e); + } + } + + private PluginUtils() { + } + + /** + * Modifies the value of a static (final) field and returns the previous + * value. + * + * @param clazz the class containing the static field + * @param name the name of the field + * @param value the value to set + * @return the previous value + * @throws IllegalAccessException if the field is inaccessible + */ + @SuppressWarnings("unchecked") + public static T setStaticFinalField(Class clazz, String name, T value) + throws IllegalAccessException { + // Retrieve the desired field + Field field = findField(clazz, name); + + // Remove the final modifier (if necessary) + if (Modifier.isFinal(field.getModifiers())) { + Field modifiers = findField(field.getClass(), "modifiers"); + makeAccessible(modifiers); + modifiers.set(field, (Integer) modifiers.get(field) + & ~Modifier.FINAL); + } + + // Get the current value + T current = (T) field.get(null); + + // Set the new value + field.set(null, value); + + return current; + } + + /** + * Creates a new unique {@link File} object within the {@link #PLUGINS_DIR} + * directory. + * + * @return the file + */ + public static File uniqueFile() { + return new File(PLUGINS_DIR, "_" + System.nanoTime() + ".jar"); + } + + /** + * Copies the given file to a file in the plugin directory.
+ * + * @throws IOException if the destination file already exists or the file was not copied + * @see #uniqueFile() + */ + public static void preparePlugin(File file) throws IOException { + File destFile = uniqueFile(); + if (destFile.exists()) { + throw new IOException("Destination file must not exist."); + } + + File tempFile = new File(destFile.getParentFile(), "tmp_" + + UUID.randomUUID().toString()); + FileUtils.copyFile(file, tempFile); + if (!tempFile.renameTo(destFile) || !destFile.isFile()) { + throw new IOException(String.format( + "File '%s' was not copied to '%s'.", file, destFile)); + } + } + + /** + * Deletes all plugin JARs copied to the plugin directory. + */ + public static void cleanPluginDirectory() { + FileFilter filter = and(FILE, + or(prefixFileFilter("_"), prefixFileFilter("tmp_"))); + System.gc(); + + for (File file : PLUGINS_DIR.listFiles(filter)) { + file.delete(); + } + } + + /** + * Ads a new {@link EventBusHandler} to the logger declared within the given + * objects class if necessary.
+ * This method does nothing if the logger already has an + * {@link EventBusHandler} or there is no logger. + * + * @param obj the object + */ + public static void addBusHandlerIfNecessary(Object obj) { + Class targetClass = AopUtils.getTargetClass(obj); + Field field = findField(targetClass, null, Logger.class); + if (field != null) { + makeAccessible(field); + Logger logger = (Logger) getField(field, obj); + for (Handler handler : logger.getHandlers()) { + if (handler instanceof EventBusHandler) { + return; + } + } + logger.addHandler(new EventBusHandler()); + } + } + + /** + * Creates a new instance of the given {@link IPluginExecutable} class and + * returns a proxy with the AspectJ aspect applied to it.
+ * If {@code aspectClass} is {@code null}, no aspect is applied. + * + * @param clazz the plugin class + * @param aspectClass the class containing AspectJ definitions + * @return proxy of the plugin instance + */ + public static IPluginExecutable getExecutable( + Class clazz, Class aspectClass) { + IPluginExecutable target = BeanUtils.instantiateClass(clazz); + AspectJProxyFactory factory = new AspectJProxyFactory(target); + factory.setProxyTargetClass(true); + if (aspectClass != null) { + factory.addAspect(BeanUtils.instantiateClass(aspectClass)); + } + return factory.getProxy(); + } + + /** + * Returns the pointcut expression of the given advised proxy. + * + * @param advised the proxy with the applied aspect + * @return the pointcut expression or {@code null} if none was found + */ + public static PointcutExpressionImpl getPointcutExpression(Advised advised) { + PointcutAdvisor pointcutAdvisor = getPointcutAdvisor(advised); + if (pointcutAdvisor != null) { + AspectJExpressionPointcut pointcut = (AspectJExpressionPointcut) pointcutAdvisor + .getPointcut(); + if (pointcut.getPointcutExpression() instanceof PointcutExpressionImpl) { + return (PointcutExpressionImpl) pointcut + .getPointcutExpression(); + } + } + return null; + } + + /** + * Returns the pointcut advisor of the given proxy if its advice part is an + * {@link AbstractAspectJAdvice}. + * + * @param advised the proxy with the applied aspect + * @return the pointcut advisor or {@code null} if there is no AspectJ pointcut advisor applied + */ + public static PointcutAdvisor getPointcutAdvisor(Advised advised) { + for (Advisor advisor : advised.getAdvisors()) { + if (advisor instanceof PointcutAdvisor + && advisor.getAdvice() instanceof AbstractAspectJAdvice) { + return (PointcutAdvisor) advisor; + } + } + return null; + } + + /** + * Attempts to resolve all parts of the pointcut expression of the aspect + * applied to the given proxy. + * + * @param advised the proxy with the applied aspect + * @return a string representation of this pointcut expression + * @see #getPointcutExpression(org.springframework.aop.framework.Advised) + * @see #getPointcutAdvisor(org.springframework.aop.framework.Advised) + */ + public static String getBestExpression(Advised advised) { + PointcutExpressionImpl pointcutExpression = getPointcutExpression(advised); + if (pointcutExpression != null) { + Pointcut underlyingPointcut = pointcutExpression + .getUnderlyingPointcut(); + if (findMethod(underlyingPointcut.getClass(), "toString") + .getDeclaringClass() != Object.class) { + return underlyingPointcut.toString(); + } + return pointcutExpression.getPointcutExpression(); + } + PointcutAdvisor advisor = getPointcutAdvisor(advised); + AspectJExpressionPointcut pointcut = (AspectJExpressionPointcut) advisor + .getPointcut(); + return pointcut.getExpression(); + } + + /** + * Finds all public methods of the given class annotated with a certain + * annotation. + * + * @param clazz the class + * @param annotationType the annotation to search for + * @return methods annotated with the given annotation + */ + public static Map findMethodAnnotation( + Class clazz, Class annotationType) { + Map map = new HashMap(); + for (Method method : clazz.getMethods()) { + A annotation = AnnotationUtils.findAnnotation(method, + annotationType); + if (annotation != null) { + map.put(method, annotation); + } + } + return map; + } +} diff --git a/ass2-aop/src/test/resources/all.zip b/ass2-aop/src/test/resources/all.zip new file mode 100644 index 0000000000000000000000000000000000000000..fbc9d27077ecba6ad145003fd11199d863d36d2e GIT binary patch literal 2932 zcmbuBcT`i^7REyf9W)F@L7Fl`fPqMP^m!nU9v}>ajv*w7LZ}H4q$5?DfT5`rX#!$^ zFjSGIp(#D6v%bAQm@xw*4}cbtNF4Oj z;>*Mc5Cx#II61f_CxAJ6zRE6cf6(53cy<6C;|v1;@M(hmk6`QcX9DXN0uqar|BSJp z`;7Qo7|z2Oq?h-fQ4xRVL_B{*VUZr*7?f&8cemJBv#zhu|*-yr&jNaH!Ma=&A%K4KlJ+M^0qoK0?Ckt=0Mrj%8h!qsU&&3Tr z*?<%tB}7=3F=1(L>q0;h}Ss8RCUN)Ah;dsM_3>O3#ho zMqLAz7Aaw&TMTX7b(lod0Qtl;#feT?MrKG-Fex_0BRKL$Qj2YSsb1*$@Vq0pJ`i^6 zR)F_wmkCMc!5$--(hV--g6#D_1xrJ4(uq1D_W6_jo~+jf(=xb^t##VGP;VZVVxRQ? z&*Q8-MVHziNl)gtdTV}X#YO;I3GQaoAJZh#pZ8693V?|qfohQ75Yi!}AVl=sem;{;88Nn6T>Byus=G z9B0L!Db-z+T{S8K`!$rAszNC>9E%KelMJ+OxZihEA1ulI=A8Du$u5AbkC4{ZS{wl+us@)=?wq(p0p9RwQ##rnrXNb#&5+>s8(=js%kza97~E#MZjzk zGZQl7Z%o5e7CI5~Jsi{3-OSaNSDW`T@pxe`}*k=#Na4dmE1-k@U_v1#B!B1@pu*sT|j`Cm-n?PZ1B5?*@s=>vO1IAO-j zEa1?DZt?vxMAPBtDnwJL=OKmaGXnr;zM)V{9*c&U4^JVp?2-5G=rqkpp!5p!ha zDdW%NoGaG_o^h|SdKx}2_1&};0IJ?K9wVzvyn4ybS#NIXqu(jB*t<-jN|rU0PO$U3 z08w?rR^A2M-5QW2$y&_in7XOjBzgUDBG_QG_Js63U5tNOfoS7+T~y?3%~YIGZR@G+ zoJJH?-opQS$tWbmxIc)DZg2&%W%N~wDn1gmQGyOC&|_bbqRrjZ@5rSe~~S~0Z17fh*f7$h)^N4*7M9R(ioswX;AOlB;V z6tAZ!o5;zY8E{T#1LHl^eb2fh6z{a%DjietykNAel>yP({G})j`vjOC@N6!#M604H z^%YNmOj$?ARa0v;LXFScP#?Gf$IO1<9+oOsbG!YvAw1!ZsXhau?1NnwLvZ0(Fe1rq zhLpJ0U1Q&2y;7OHwA_*`QdRj{EEG*TF5Uh90mktu(ul&9BW0(?=+EmQTv?f1cgNH0 zbaIb>qhO?F8n}BeMHlW}mp8N9f9femc2IaePJVhKu6XiRy>0`NTIuw!W*jE{fu3y7 z`!MK;4M$nlR#C2C+zeaK)sILC?RG{KB9w?WW0Ff!(@p1)Rn@+rW^x*VUVYk(LuNIv zll19BDjZtv%Qfx{unjA*sa`YbFwFGZsRQ9_`W^Fj_9iz|nk)DMbKl15wXe;|+~B^6 zELNf}xd$!aiKE5#>Lv@|7|Wj;S)15Op_CuEC>^Wb)uxNifmu5B?h`jrbq27<$%{md zOCpsw)9#Rp~q*IW1aUA+Xae$6N7@;t7 z&@eM@HWzh+HGEU-m#J+c3>zl8Gj{y6Z|oDbta+`x?1_rY3PMoI_`KNpQ0+_aE-_=H z^3;L5bD(Xog`d5^BTe> z;(PI>8s!zfK$4G6eAuyESas1!W?kK%vJDJ zGq(^%M=KdJh55MJ{gOc|3@~FldLD*vo1X(Sw4hB}eV-NjgZMQw^at_m7vgtXqia~19D2!GxJ<4Qj<$d5|eUL^^$XdfdLNDNMJ}h12Kq33(?+?+~Uh_ z0(E+2N{d%;wm5h>D=u6T8rN~_!uNn%DiT=%J_o$geyuFt$nUoY3-zCCGe*)s3r+Pv>K@1~c(e|NsV?l>e3qK^Dp`__20IStoycnnA#gylY3{n_Kwtt(%jp1YqSkY zw0oRuqZd3mm&ZBf`lM+>5?>ETT(#bPTZ{WinakN3^G#B>sBXUS=J%;h_c)6iS@%!P z_J8(slKSK;r=R9YX|5NP+9+6hD>0^a;VjwQIn!8mLf^d5|1tm5{8P^_b*J3yPTQ~Y~v;$?w1DPBew6XZ0fe#+YY zoj2)9NtpO_trJUTZn!HSu(gi;vc>u3o)4NTvotEhckDYo*IiSC{dt_@o+rl{^a^FQ zriku2e)T1zLFefydv5(cS05O?=+2$P4fg-Fy^8lQ{&QHH?e^gS@i6hJ8;xUjZP+a# z*S@)ZVthbk{EJ6A51;!r^uIkEZ*-F9*~Sy>8$%}Sa&tOhaNn&a$y-^f4h&U&hrH^7^nL$j{?gbJ`20_DR1Z$>5&X51witYmxZ2x1Xet|3gsRX zbb^{lWI2c1SWrm^18*HWk&Pv;+{0}&sN{ozw~klPjV8JrgxCNp36Wy}R1(6#mPR#3 hNGOBD1mt+A4V09N0p6^@m}X#L1;Qsl^G~pVcmPr6bE^OV literal 0 HcmV?d00001 diff --git a/ass2-di/pom.xml b/ass2-di/pom.xml new file mode 100644 index 0000000..87980e5 --- /dev/null +++ b/ass2-di/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + + at.ac.tuwien.infosys.dst + dst + 2018.1 + .. + + + ass2-di + + jar + + DST :: Assignment 2 :: Dependency Injection + + + + org.javassist + javassist + + + org.springframework + spring-core + ${spring.version} + test + + + diff --git a/ass2-di/src/main/java/dst/ass2/di/IInjectionController.java b/ass2-di/src/main/java/dst/ass2/di/IInjectionController.java new file mode 100644 index 0000000..651d2ee --- /dev/null +++ b/ass2-di/src/main/java/dst/ass2/di/IInjectionController.java @@ -0,0 +1,28 @@ +package dst.ass2.di; + +/** + * The dependency injection controller interface. + */ +public interface IInjectionController { + + /** + * Injects all fields annotated with {@link dst.ass2.di.annotation.Inject @Inject}.
+ * Only objects of classes annotated with {@link dst.ass2.di.annotation.Component @Component} shall be accepted. + * + * @param obj the object to initialize. + * @throws InjectionException if it's no component or dependency injection fails. + */ + void initialize(Object obj) throws InjectionException; + + /** + * Gets the fully initialized instance of a singleton component.
+ * Multiple calls of this method always have to return the same instance. + * Only classes that are annotated with {@link dst.ass2.di.annotation.Component Component} shall be accepted. + * + * @param the class type. + * @param clazz the class of the component. + * @return the initialized singleton object. + * @throws InjectionException if it's no component or dependency injection fails. + */ + T getSingletonInstance(Class clazz) throws InjectionException; +} diff --git a/ass2-di/src/main/java/dst/ass2/di/InjectionControllerFactory.java b/ass2-di/src/main/java/dst/ass2/di/InjectionControllerFactory.java new file mode 100644 index 0000000..64bbf96 --- /dev/null +++ b/ass2-di/src/main/java/dst/ass2/di/InjectionControllerFactory.java @@ -0,0 +1,52 @@ +package dst.ass2.di; + +/** + * Creates and provides {@link IInjectionController} instances. + */ +public class InjectionControllerFactory { + + + /** + * Returns the singleton {@link IInjectionController} instance.
+ * If none is available, a new one is created. + * + * @return the instance + */ + public static synchronized IInjectionController getStandAloneInstance() { + // TODO + return null; + } + + /** + * Returns the singleton {@link IInjectionController} instance for processing objects modified by an + * {@link dst.ass2.di.agent.InjectorAgent InjectorAgent}.
+ * If none is available, a new one is created. + * + * @return the instance + */ + public static synchronized IInjectionController getTransparentInstance() { + // TODO + return null; + } + + /** + * Creates and returns a new {@link IInjectionController} instance. + * + * @return the newly created instance + */ + public static IInjectionController getNewStandaloneInstance() { + // TODO + return null; + } + + /** + * Creates and returns a new {@link IInjectionController} instance for processing objects modified by an + * {@link dst.ass2.di.agent.InjectorAgent InjectorAgent}.
+ * + * @return the instance + */ + public static IInjectionController getNewTransparentInstance() { + // TODO + return null; + } +} diff --git a/ass2-di/src/main/java/dst/ass2/di/InjectionException.java b/ass2-di/src/main/java/dst/ass2/di/InjectionException.java new file mode 100644 index 0000000..15b0a43 --- /dev/null +++ b/ass2-di/src/main/java/dst/ass2/di/InjectionException.java @@ -0,0 +1,33 @@ +package dst.ass2.di; + +/** + * Thrown whenever an DI problem occurs. + */ +public class InjectionException extends RuntimeException { + + private static final long serialVersionUID = 3221609361590670030L; + + /** + * Creates a new instance of InjectionException without detail message. + */ + public InjectionException() { + } + + /** + * Constructs an instance of InjectionException with the specified detail message. + * + * @param msg the detail message. + */ + public InjectionException(String msg) { + super(msg); + } + + /** + * Constructs an instance of InjectionException wrapping the specified throwable. + * + * @param t the throwable to wrap. + */ + public InjectionException(Throwable t) { + super(t); + } +} diff --git a/ass2-di/src/main/java/dst/ass2/di/agent/InjectorAgent.java b/ass2-di/src/main/java/dst/ass2/di/agent/InjectorAgent.java new file mode 100644 index 0000000..d0b5298 --- /dev/null +++ b/ass2-di/src/main/java/dst/ass2/di/agent/InjectorAgent.java @@ -0,0 +1,18 @@ +package dst.ass2.di.agent; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +public class InjectorAgent implements ClassFileTransformer { + + @Override + public byte[] transform(ClassLoader loader, String className, + Class classBeingRedefined, ProtectionDomain protectionDomain, + byte[] classfileBuffer) throws IllegalClassFormatException { + + // TODO + return classfileBuffer; + } + +} diff --git a/ass2-di/src/main/java/dst/ass2/di/annotation/Component.java b/ass2-di/src/main/java/dst/ass2/di/annotation/Component.java new file mode 100644 index 0000000..900b5dc --- /dev/null +++ b/ass2-di/src/main/java/dst/ass2/di/annotation/Component.java @@ -0,0 +1,12 @@ +package dst.ass2.di.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Component { + Scope scope(); +} diff --git a/ass2-di/src/main/java/dst/ass2/di/annotation/ComponentId.java b/ass2-di/src/main/java/dst/ass2/di/annotation/ComponentId.java new file mode 100644 index 0000000..243b3c3 --- /dev/null +++ b/ass2-di/src/main/java/dst/ass2/di/annotation/ComponentId.java @@ -0,0 +1,11 @@ +package dst.ass2.di.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ComponentId { +} diff --git a/ass2-di/src/main/java/dst/ass2/di/annotation/Inject.java b/ass2-di/src/main/java/dst/ass2/di/annotation/Inject.java new file mode 100644 index 0000000..5359168 --- /dev/null +++ b/ass2-di/src/main/java/dst/ass2/di/annotation/Inject.java @@ -0,0 +1,14 @@ +package dst.ass2.di.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Inject { + boolean required() default true; + + Class specificType() default Void.class; +} diff --git a/ass2-di/src/main/java/dst/ass2/di/annotation/Scope.java b/ass2-di/src/main/java/dst/ass2/di/annotation/Scope.java new file mode 100644 index 0000000..e40e4fd --- /dev/null +++ b/ass2-di/src/main/java/dst/ass2/di/annotation/Scope.java @@ -0,0 +1,7 @@ +package dst.ass2.di.annotation; + +public enum Scope { + + SINGLETON, PROTOTYPE + +} diff --git a/ass2-di/src/test/java/dst/ass2/di/BasicInjectionTest.java b/ass2-di/src/test/java/dst/ass2/di/BasicInjectionTest.java new file mode 100644 index 0000000..8fd3dd4 --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/BasicInjectionTest.java @@ -0,0 +1,116 @@ +package dst.ass2.di; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; + +import dst.ass2.di.type.Container; +import dst.ass2.di.type.Invalid; +import dst.ass2.di.type.SimpleComponent; +import dst.ass2.di.type.SimpleSingleton; + +public class BasicInjectionTest { + private IInjectionController ic; + + @Before + public void before() { + ic = InjectionControllerFactory.getNewStandaloneInstance(); + } + + /** + * Initializing two prototype components. + */ + @Test + public void testSimpleComponent() { + SimpleComponent first = new SimpleComponent(); + assertNull("The ID of an uninitialized component must be null", first.id); + + ic.initialize(first); + assertEquals("The ID of the first component must be 0", Long.valueOf(0L), first.id); + + SimpleComponent second = new SimpleComponent(); + ic.initialize(second); + assertEquals("The ID of the second component must be 1", Long.valueOf(1L), second.id); + } + + /** + * Retrieving a singleton twice. + */ + @Test + public void testSimpleSingleton() { + SimpleSingleton first = ic.getSingletonInstance(SimpleSingleton.class); + SimpleSingleton second = ic.getSingletonInstance(SimpleSingleton.class); + + assertNotNull("The ID of the singleton must not be null.", first.id); + assertEquals("Singleton has wrong ID.", Long.valueOf(0L), first.id); + assertSame("Singletons must be the same object instance.", first, second); + } + + /** + * Injecting prototypes and singletons into an object. + */ + @Test + public void testInject() { + Container container = new Container(); + + assertNotNull("'timestamp' must not be null.", container.timestamp); + Long timestamp = container.timestamp; + + assertNull("'id' must be null.", container.id); + assertNull("'first' must be null.", container.first); + assertNull("'second' must be null.", container.second); + assertNull("'component' must ne null.", container.component); + assertNull("'anotherComponent' must be null.", container.anotherComponent); + + ic.initialize(container); + assertSame("Initial timestamp was modified.", timestamp, container.timestamp); + assertNotNull("'id' must not be null.", container.id); + assertNotNull("'first' must not be null.", container.first); + assertNotNull("'second' must not be null.", container.second); + assertNotNull("'component' must not be null.", container.component); + assertNotNull("'anotherComponent' must not be null.", container.anotherComponent); + + assertSame("Singletons must be the same object instance.", container.first, container.second); + assertNotSame("Prototype references must not be the same object instance.", container.component, container.anotherComponent); + + Collection ids = InjectionUtils.getIds(container); + assertEquals("Initialized object graph with 4 components.", 4, ids.size()); + + for (long i = 0; i < ids.size(); i++) { + assertTrue("There is no component with ID " + i + ".", ids.contains(i)); + } + } + + /** + * Attempts to initialize an object that is not annotated with {@link dst.ass2.di.annotation.Component @Component}. + */ + @Test(expected = InjectionException.class) + public void testInvalidPrototype() { + ic.initialize(new Invalid()); + } + + /** + * Attempts to retrieve a singleton instance of a class that is not annotated with + * {@link dst.ass2.di.annotation.Component @Component}. + */ + @Test(expected = InjectionException.class) + public void testInvalidSingleton() { + ic.getSingletonInstance(Invalid.class); + } + + /** + * Attempts to retrieve a singleton instance of a prototype component. + */ + @Test(expected = InjectionException.class) + public void testPrototypeAsSingleton() { + ic.getSingletonInstance(SimpleComponent.class); + } +} diff --git a/ass2-di/src/test/java/dst/ass2/di/InjectionUtils.java b/ass2-di/src/test/java/dst/ass2/di/InjectionUtils.java new file mode 100644 index 0000000..90ef53b --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/InjectionUtils.java @@ -0,0 +1,170 @@ +package dst.ass2.di; + +import static org.springframework.util.ReflectionUtils.FieldCallback; +import static org.springframework.util.ReflectionUtils.FieldFilter; +import static org.springframework.util.ReflectionUtils.doWithFields; +import static org.springframework.util.ReflectionUtils.getField; +import static org.springframework.util.ReflectionUtils.makeAccessible; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; + +import dst.ass2.di.annotation.Component; +import dst.ass2.di.annotation.ComponentId; +import dst.ass2.di.annotation.Inject; + +/** + * Contains some utilities for testing dependency injection. + */ +public final class InjectionUtils { + private InjectionUtils() { + } + + /** + * Returns the component ID of the given object declared by a certain type. + *

+ * The ID must be declared by the given type. + * Inherited fields or fields of super classes are not checked.
+ * If {@code type} is {@code null}, the actual type of the object is used instead. + * + * @param component the object + * @param type the type of the object to retrieve (may be {@code null}) + * @return the component ID or {@code null} if none was found + * @see Class#getDeclaredFields() + */ + public static Long getId(Object component, Class type) { + Assert.notNull(AnnotationUtils.findAnnotation(component.getClass(), Component.class), "Object is not annotated with @Component: " + component); + type = type != null ? type : component.getClass(); + for (Field field : type.getDeclaredFields()) { + if (field.isAnnotationPresent(ComponentId.class)) { + makeAccessible(field); + return (Long) getField(field, component); + } + } + return null; + } + + /** + * Returns all component ID fields (including inherited and hidden) and their values of the given object. + * + * @param component the component to check + * @return the fields and their values + */ + public static Map getIdsOfHierarchy(Object component) { + Assert.notNull(AnnotationUtils.findAnnotation(component.getClass(), Component.class), "Object is not annotated with @Component: " + component); + ComponentIdCallback callback = new ComponentIdCallback(component); + doWithFields(component.getClass(), callback, new ComponentIdFilter()); + return callback.ids; + } + + /** + * Traverses the given object graph and returns all component IDs. + * + * @param component the component to check + * @param map the map to store the IDs + * @return the component IDs of the objects + * @see #getIdsOfHierarchy(Object) + */ + public static Map> getIdsOfObjectGraph(final Object component, Map> map) { + map = map != null ? map : new HashMap>(); + if (component != null && !map.containsKey(component)) { + final Map> finalMap = map; + map.put(component, getIdsOfHierarchy(component)); + doWithFields(component.getClass(), new FieldCallback() { + @Override + public void doWith(Field field) { + makeAccessible(field); + Object value = getField(field, component); + if (value != null) { + finalMap.putAll(getIdsOfObjectGraph(value, finalMap)); + } + } + }, new InjectFilter()); + } + return map; + } + + /** + * Returns a list of all component IDs retrieved by {@link #getIdsOfObjectGraph(Object, Map)}.
+ * + * @param component the component to check + * @return the component IDs + */ + public static List getIds(Object component) { + Map> map = getIdsOfObjectGraph(component, null); + List ids = new ArrayList(); + for (Map.Entry> entry : map.entrySet()) { + for (Map.Entry id : entry.getValue().entrySet()) { + ids.add(id.getValue()); + } + } + return ids; + } + + /** + * Returns the names of all declared fields and their values of the given object declared by a certain type. + *

+ * The ID must be declared by the given type. + * Inherited fields or fields of super classes are not checked.
+ * If {@code type} is {@code null}, the actual type of the object is used instead. + * + * @param obj the object + * @param type the type of the object to retrieve (may be {@code null}) + * @return all declared field names and the field values + */ + public static Map getFields(Object obj, Class type) { + type = type != null ? type : obj.getClass(); + Map map = new HashMap(); + for (Field field : type.getDeclaredFields()) { + makeAccessible(field); + map.put(field.getName(), getField(field, obj)); + } + return map; + } + + /** + * Callback invoked on each component ID in the hierarchy. + */ + private static class ComponentIdCallback implements FieldCallback { + protected final Map ids = new HashMap(); + protected Object component; + + private ComponentIdCallback(Object component) { + this.component = component; + } + + @Override + public void doWith(Field field) throws IllegalAccessException { + makeAccessible(field); + ids.put(field, (Long) field.get(component)); + } + + } + + /** + * Callback used to filter component IDs.
+ * A component ID is a field of type {@link Long} annotated with {@link ComponentId @ComponentId}. + */ + private static class ComponentIdFilter implements FieldFilter { + @Override + public boolean matches(Field field) { + return field.getType() == Long.class && field.isAnnotationPresent(ComponentId.class); + } + } + + /** + * Callback used to filter fields marked to be injected i.e., annotated with {@link Inject @Inject}. + */ + private static class InjectFilter implements FieldFilter { + @Override + public boolean matches(Field field) { + return field.isAnnotationPresent(Inject.class); + } + } +} diff --git a/ass2-di/src/test/java/dst/ass2/di/SpecialInjectionTest.java b/ass2-di/src/test/java/dst/ass2/di/SpecialInjectionTest.java new file mode 100644 index 0000000..3623118 --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/SpecialInjectionTest.java @@ -0,0 +1,164 @@ +package dst.ass2.di; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.util.ReflectionUtils; + +import dst.ass2.di.type.ComplexComponent; +import dst.ass2.di.type.Container; +import dst.ass2.di.type.SimpleSingleton; +import dst.ass2.di.type.SubType; +import dst.ass2.di.type.SuperType; + +public class SpecialInjectionTest { + private IInjectionController ic; + + @Before + public void before() { + ic = InjectionControllerFactory.getNewStandaloneInstance(); + } + + /** + * Attempts to create and initialize two singletons of the same time. + */ + @Test + public void testInitializeSingletonTwice() { + // Initializing a singleton component also registers it for further use + ic.initialize(new SimpleSingleton()); + try { + // Initializing a singleton of the same type results in an InjectionException + ic.initialize(new SimpleSingleton()); + fail(InjectionException.class.getSimpleName() + " expected."); + } catch (InjectionException e) { + // expected + } + } + + /** + * Attempts to initialize a singleton manually twice and retrieves it from the {@link IInjectionController}. + */ + @Test + public void testInitializeSingletonManually() { + SimpleSingleton singleton = new SimpleSingleton(); + ic.initialize(singleton); + try { + // Initializing a singleton instance twice results in an InjectionException + ic.initialize(singleton); + fail(InjectionException.class.getSimpleName() + " expected."); + } catch (InjectionException e) { + // expected + } + + // Retrieving a singleton of a certain type always returns the same object instance. + assertSame("Singletons must be the same object instance.", singleton, ic.getSingletonInstance(SimpleSingleton.class)); + } + + /** + * Initializes a prototype twice. + *

+ * Every time a prototype component is initialized, all fields annotated with + * {@link dst.ass2.di.annotation.Inject Inject} become injected again. In other words, for every prototype the + * {@link IInjectionController} creates a new instance and replaces the reference to the the previous one.
+ * Finally, if nothing went wrong, the component gets a new component ID indicating that all fields have been + * re-injected successfully. + */ + @Test + public void testInitializePrototypeTwice() { + // Create an initialize the container + Container container = new Container(); + ic.initialize(container); + + // Create a copy of the container by copying the object references for later comparison. + Container copy = new Container(); + ReflectionUtils.shallowCopyFieldState(container, copy); + + // Re-initialize the same object + ic.initialize(container); + + // Verify that the object still references the same singleton. + assertSame("Both singleton references must be the same object instance.", copy.first, container.first); + assertSame("Both singleton references must be the same object instance.", copy.second, container.second); + + // Verify that the prototype references have been changed. + assertNotSame("Prototype components must be different.", copy.component, container.component); + assertNotSame("Prototype components must be different.", copy.anotherComponent, container.anotherComponent); + + // Verify that the IDs are different + assertTrue("The ID after the first initialization must be lower or equal to 3.", copy.id <= 3); + assertTrue("The ID after the second initialization must be between 4 and 6 inclusive.", 4 <= container.id && container.id <= 6); + + // Verify that the IDs of the prototype components are correct + String afterFirst = "The ID of '%s' after the first initialization must be lower or equal to 3."; + String afterSecond = "The ID of '%s' after the second initialization must be between 4 and 6 inclusive."; + + // Due to the fact that Container consists of two prototypes and one singleton, which is referenced twice, + // initializing the first Container requires 4 IDs and every additional instance 3 IDs + assertTrue(String.format(afterFirst, "component"), copy.component.id <= 3); + assertTrue(String.format(afterSecond, "component"), 4 <= container.component.id && container.component.id <= 6); + assertTrue(String.format(afterFirst, "anotherComponent"), copy.anotherComponent.id <= 3); + assertTrue(String.format(afterSecond, "anotherComponent"), 4 <= container.anotherComponent.id && container.anotherComponent.id <= 6); + } + + /** + * Tests the type qualifier of the {@link dst.ass2.di.annotation.Inject} annotation. + */ + @Test + public void testQualifier() { + ComplexComponent component = new ComplexComponent(); + ic.initialize(component); + + // Required components + + assertNotNull("'id' must not be null.", component.id); + // singleton is a required field + assertNotNull("'singleton' must not be null.", component.singleton); + // unknownSingleton is of type Object but requires the specific type SimpleSingleton + assertNotNull("'unknownSingleton' must not be null.", component.unknownSingleton); + assertSame("Both singleton references must be the same object instance.", component.singleton, component.unknownSingleton); + + // Optional components + + // theVoid is of type Void + assertNull("'theVoid must be null.'", component.theVoid); + // type Invalid, which is not a valid component + assertNull("'invalid' must be null.'", component.invalid); + // type SimpleComponent but requires SimpleSingleton which results in a ClassCastException + assertNull("'singletonPrototype' must be null.", component.singletonPrototype); + } + + /** + * Tests whether private hidden fields of super types are injected. + */ + @Test + public void testInheritance() { + // When initializing an object, all fields are initialized (even those, which are not visible). + SubType subType = new SubType(); + ic.initialize(subType); + + // Retrieving all declared fields of the SubType and SuperType + Map subFields = InjectionUtils.getFields(subType, SubType.class); + Map superFields = InjectionUtils.getFields(subType, SuperType.class); + + // Verify that the component IDs are set + assertNotNull("'id' of SubType must not be null.", subFields.get("id")); + assertNotNull("'id' of SuperType must not be null.", superFields.get("id")); + // The component IDs must be equal + assertTrue("The 'id's of SuperType and SubType must be equal.", subFields.get("id").equals(superFields.get("id"))); + + // Verify that all fields are initialized properly + assertNotNull("'component' of SubType must not be null.", subFields.get("component")); + assertNotNull("'component' of SuperType must not be null.", superFields.get("component")); + // The injected objects must be different + assertFalse("The 'component's of SuperType and SubType must not be the same object instance.", subFields.get("component") == superFields.get("component")); + } +} diff --git a/ass2-di/src/test/java/dst/ass2/di/TransparentInjectionTest.java b/ass2-di/src/test/java/dst/ass2/di/TransparentInjectionTest.java new file mode 100644 index 0000000..34d839c --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/TransparentInjectionTest.java @@ -0,0 +1,84 @@ +package dst.ass2.di; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import dst.ass2.di.type.Container; +import dst.ass2.di.type.SimpleSingleton; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TransparentInjectionTest { + + protected IInjectionController ic; + + @Before + public void before() { + ic = InjectionControllerFactory.getTransparentInstance(); + } + + /** + * Injecting prototypes and singletons into an object. + */ + @Test + public void test_01_inject() { + Container container = new Container(); + + assertNotNull("'timestamp' must not be null.", container.timestamp); + Long timestamp = container.timestamp; + + assertSame("Initial timestamp was modified.", timestamp, container.timestamp); + assertNotNull("'id' must not be null.", container.id); + assertNotNull("'first' must not be null.", container.first); + assertNotNull("'second' must not be null.", container.second); + assertNotNull("'component' must not be null.", container.component); + assertNotNull("'anotherComponent' must not be null.", container.anotherComponent); + + assertSame("Singletons must be the same object instance.", container.first, container.second); + assertNotSame("Prototype references must not be the same object instance.", container.component, container.anotherComponent); + + List ids = InjectionUtils.getIds(container); + Collections.sort(ids); + assertEquals("Initialized object graph with 4 components.", 4, ids.size()); + + for (long i = ids.get(0); i < ids.get(0) + ids.size(); i++) { + assertTrue("There is no component with ID " + i + ".", ids.contains(i)); + } + } + + /** + * Injecting components into hierarchies. + */ + @Test + public void test_02_hierarchy() throws IllegalAccessException { + Container container = new Container(); + Long oldId = container.id; + + SimpleSingleton singleton = ic.getSingletonInstance(SimpleSingleton.class); + assertNotNull("'id' must not be null.", singleton.id); + + // Check that the same singleton is used + assertSame("Singletons must be the same object instance.", container.first, singleton); + + // Verify that exactly 4 new component IDs where used + assertEquals("More than 4 components were instantiated with the container.", oldId + 3L, new Container().id.longValue()); + + try { + new SimpleSingleton(); + fail(InjectionException.class.getSimpleName() + " expected"); + } catch (InjectionException e) { + // expected + } + } +} diff --git a/ass2-di/src/test/java/dst/ass2/di/type/ComplexComponent.java b/ass2-di/src/test/java/dst/ass2/di/type/ComplexComponent.java new file mode 100644 index 0000000..91d1989 --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/type/ComplexComponent.java @@ -0,0 +1,26 @@ +package dst.ass2.di.type; + +import static dst.ass2.di.annotation.Scope.PROTOTYPE; + +import dst.ass2.di.annotation.Component; +import dst.ass2.di.annotation.ComponentId; +import dst.ass2.di.annotation.Inject; + +@Component(scope = PROTOTYPE) +public class ComplexComponent { + @ComponentId + public Long id; + + @Inject(required = false) + public Void theVoid; + @Inject(required = false) + public Invalid invalid; + + @Inject(required = true) + public SimpleSingleton singleton; + @Inject(specificType = SimpleSingleton.class) + public Object unknownSingleton; + + @Inject(specificType = SimpleSingleton.class, required = false) + public SimpleComponent singletonPrototype; +} diff --git a/ass2-di/src/test/java/dst/ass2/di/type/Container.java b/ass2-di/src/test/java/dst/ass2/di/type/Container.java new file mode 100644 index 0000000..b36da6c --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/type/Container.java @@ -0,0 +1,22 @@ +package dst.ass2.di.type; + +import static dst.ass2.di.annotation.Scope.PROTOTYPE; + +import dst.ass2.di.annotation.Component; +import dst.ass2.di.annotation.ComponentId; +import dst.ass2.di.annotation.Inject; + +@Component(scope = PROTOTYPE) +public class Container { + @ComponentId + public Long id; + public Long timestamp = System.currentTimeMillis(); + @Inject + public SimpleSingleton first; + @Inject + public SimpleSingleton second; + @Inject + public SimpleComponent component; + @Inject + public SimpleComponent anotherComponent; +} diff --git a/ass2-di/src/test/java/dst/ass2/di/type/Invalid.java b/ass2-di/src/test/java/dst/ass2/di/type/Invalid.java new file mode 100644 index 0000000..9d0e78a --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/type/Invalid.java @@ -0,0 +1,9 @@ +package dst.ass2.di.type; + +import dst.ass2.di.annotation.Inject; + +public class Invalid { + public Long id; + @Inject + public SimpleSingleton singleton; +} diff --git a/ass2-di/src/test/java/dst/ass2/di/type/SimpleComponent.java b/ass2-di/src/test/java/dst/ass2/di/type/SimpleComponent.java new file mode 100644 index 0000000..80d1d87 --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/type/SimpleComponent.java @@ -0,0 +1,12 @@ +package dst.ass2.di.type; + +import static dst.ass2.di.annotation.Scope.PROTOTYPE; + +import dst.ass2.di.annotation.Component; +import dst.ass2.di.annotation.ComponentId; + +@Component(scope = PROTOTYPE) +public class SimpleComponent { + @ComponentId + public Long id; +} diff --git a/ass2-di/src/test/java/dst/ass2/di/type/SimpleSingleton.java b/ass2-di/src/test/java/dst/ass2/di/type/SimpleSingleton.java new file mode 100644 index 0000000..09c2d50 --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/type/SimpleSingleton.java @@ -0,0 +1,12 @@ +package dst.ass2.di.type; + +import static dst.ass2.di.annotation.Scope.SINGLETON; + +import dst.ass2.di.annotation.Component; +import dst.ass2.di.annotation.ComponentId; + +@Component(scope = SINGLETON) +public class SimpleSingleton { + @ComponentId + public Long id; +} diff --git a/ass2-di/src/test/java/dst/ass2/di/type/SubType.java b/ass2-di/src/test/java/dst/ass2/di/type/SubType.java new file mode 100644 index 0000000..207a976 --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/type/SubType.java @@ -0,0 +1,15 @@ +package dst.ass2.di.type; + +import static dst.ass2.di.annotation.Scope.PROTOTYPE; + +import dst.ass2.di.annotation.Component; +import dst.ass2.di.annotation.ComponentId; +import dst.ass2.di.annotation.Inject; + +@Component(scope = PROTOTYPE) +public class SubType extends SuperType { + @ComponentId + private Long id; + @Inject + private SimpleComponent component; +} diff --git a/ass2-di/src/test/java/dst/ass2/di/type/SuperType.java b/ass2-di/src/test/java/dst/ass2/di/type/SuperType.java new file mode 100644 index 0000000..0f050a7 --- /dev/null +++ b/ass2-di/src/test/java/dst/ass2/di/type/SuperType.java @@ -0,0 +1,15 @@ +package dst.ass2.di.type; + +import static dst.ass2.di.annotation.Scope.PROTOTYPE; + +import dst.ass2.di.annotation.Component; +import dst.ass2.di.annotation.ComponentId; +import dst.ass2.di.annotation.Inject; + +@Component(scope = PROTOTYPE) +public abstract class SuperType { + @ComponentId + private Long id; + @Inject + private SimpleComponent component; +} diff --git a/ass2-service/api/pom.xml b/ass2-service/api/pom.xml new file mode 100644 index 0000000..62b0ab5 --- /dev/null +++ b/ass2-service/api/pom.xml @@ -0,0 +1,47 @@ + + + + 4.0.0 + + + at.ac.tuwien.infosys.dst + dst + 2018.1 + ../.. + + + ass2-service-api + + DST :: Assignment 2 :: Service :: API + + jar + + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + + + + + kr.motd.maven + os-maven-plugin + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + + + diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/auth/AuthenticationException.java b/ass2-service/api/src/main/java/dst/ass2/service/api/auth/AuthenticationException.java new file mode 100644 index 0000000..e6c3746 --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/auth/AuthenticationException.java @@ -0,0 +1,24 @@ +package dst.ass2.service.api.auth; + +public class AuthenticationException extends Exception { + private static final long serialVersionUID = 1L; + + public AuthenticationException() { + } + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } + + public AuthenticationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/auth/IAuthenticationService.java b/ass2-service/api/src/main/java/dst/ass2/service/api/auth/IAuthenticationService.java new file mode 100644 index 0000000..352a44b --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/auth/IAuthenticationService.java @@ -0,0 +1,52 @@ +package dst.ass2.service.api.auth; + +public interface IAuthenticationService { + + /** + * Attempts to authenticate the user with the given unique email address and the given password in plain text, by + * checking the data against the records in the database. If the credentials are successfully authenticated, the + * service generates a new authentication token which is stored (with the users email address) in-memory and then + * returned. + * + * @param email the user email + * @param password the password + * @return a new authentication token + * @throws NoSuchUserException if the given user was not found + * @throws AuthenticationException if the credentials could not be authenticated + */ + String authenticate(String email, String password) throws NoSuchUserException, AuthenticationException; + + /** + * Changes the password of the given user in the database. Also updates the in-memory cache in a thread-safe way. + * + * @param email the user email + * @param newPassword the new password in plain text. + * @throws NoSuchUserException if the given user was not found + */ + void changePassword(String email, String newPassword) throws NoSuchUserException; + + /** + * Returns the user that is associated with this token. Returns null if the token does not exist. + * + * @param token an authentication token previously created via {@link #authenticate(String, String)} + * @return the user's email address or null + */ + String getUser(String token); + + /** + * Checks whether the given token is valid (i.e., was issued by this service and has not been invalidated). + * + * @param token the token to validate + * @return true if the token is valid, false otherwise + */ + boolean isValid(String token); + + /** + * Invalidates the given token, i.e., removes it from the cache. Returns false if the token did not exist. + * + * @param token the token to invalidate + * @return true if the token existed and was successfully invalidated, false otherwise + */ + boolean invalidate(String token); + +} diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/auth/NoSuchUserException.java b/ass2-service/api/src/main/java/dst/ass2/service/api/auth/NoSuchUserException.java new file mode 100644 index 0000000..e1dbe53 --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/auth/NoSuchUserException.java @@ -0,0 +1,25 @@ +package dst.ass2.service.api.auth; + +public class NoSuchUserException extends Exception { + + private static final long serialVersionUID = 1L; + + public NoSuchUserException() { + } + + public NoSuchUserException(String message) { + super(message); + } + + public NoSuchUserException(String message, Throwable cause) { + super(message, cause); + } + + public NoSuchUserException(Throwable cause) { + super(cause); + } + + public NoSuchUserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/auth/rest/IAuthenticationResource.java b/ass2-service/api/src/main/java/dst/ass2/service/api/auth/rest/IAuthenticationResource.java new file mode 100644 index 0000000..56c971e --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/auth/rest/IAuthenticationResource.java @@ -0,0 +1,18 @@ +package dst.ass2.service.api.auth.rest; + +import javax.ws.rs.core.Response; + +import dst.ass2.service.api.auth.AuthenticationException; +import dst.ass2.service.api.auth.NoSuchUserException; + +/** + * The IAuthenticationResource exposes parts of the {@code IAuthenticationService} as a RESTful interface. + */ +public interface IAuthenticationResource { + + // TODO annotate the class and methods with the correct javax.ws.rs annotations + + Response authenticate(String email, String password) + throws NoSuchUserException, AuthenticationException; + +} diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/CourseNotAvailableException.java b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/CourseNotAvailableException.java new file mode 100644 index 0000000..5ec42ce --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/CourseNotAvailableException.java @@ -0,0 +1,16 @@ +package dst.ass2.service.api.courseplan; + +/** + * Exception indicating that a desired course is unavailable, either because it has no further capacity, or is a premium + * course and the requesting participant does not have a premium membership. + */ +public class CourseNotAvailableException extends Exception { + + public CourseNotAvailableException(String message) { + super(message); + } + + public CourseNotAvailableException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/CoursePlan.java b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/CoursePlan.java new file mode 100644 index 0000000..791604d --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/CoursePlan.java @@ -0,0 +1,41 @@ +package dst.ass2.service.api.courseplan; + +import java.io.Serializable; +import java.util.List; + +/** + * A course plan belongs to a Participant in the context of a Membership (hence it holds the membership id). The course + * plan data is simply a list of course ids. + */ +public class CoursePlan implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private Long membershipId; + private List courses; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getMembershipId() { + return membershipId; + } + + public void setMembershipId(Long membershipId) { + this.membershipId = membershipId; + } + + public List getCourses() { + return courses; + } + + public void setCourses(List courses) { + this.courses = courses; + } +} diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/EntityNotFoundException.java b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/EntityNotFoundException.java new file mode 100644 index 0000000..8a7419c --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/EntityNotFoundException.java @@ -0,0 +1,17 @@ +package dst.ass2.service.api.courseplan; + +/** + * Exception indicating that a resource that was trying to be accessed does not exist. + */ +public class EntityNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + public EntityNotFoundException(String message) { + super(message); + } + + public EntityNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/ICoursePlanService.java b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/ICoursePlanService.java new file mode 100644 index 0000000..116b26c --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/ICoursePlanService.java @@ -0,0 +1,68 @@ +package dst.ass2.service.api.courseplan; + +/** + * Manages {@link CoursePlan} instances. Note that {@link CoursePlan}s are transient entities and do not have to be + * persisted. They can be kept in memory for the duration of the application session. + */ +public interface ICoursePlanService { + + /** + * Creates a new course plan for the participant of the given membership relation. The service is responsible for + * assigning a unique CoursePlan ID in a thread-safe way. + * + * @param membershipId the membership id + * @return a new course plan object + * @throws EntityNotFoundException if the membership does not exist + */ + CoursePlan create(Long membershipId) throws EntityNotFoundException; + + /** + * Removes the CoursePlan with the given id. + * + * @param coursePlanId the course plan id + * @throws EntityNotFoundException if the course plan does not exist + */ + void delete(Long coursePlanId) throws EntityNotFoundException; + + /** + * Finds the CoursePlan with the given id. + * + * @param coursePlanId the course plan id + * @return the course plan instance, or null if it does not exist + */ + CoursePlan find(Long coursePlanId); + + /** + * Adds the given course to the given course plan if possible (i.e., if the owner of the course plan is eligible to + * enroll in the course, and the course has sufficient capacity). Does nothing if the course is already in the + * course plan. + * + * @param courseId the id of the course to add + * @return true if the course was added, false if the course was already in the course plan + * @throws EntityNotFoundException if a referenced membership or course could not be found + * @throws CourseNotAvailableException if the course doesn't exist or can't be enrolled (e.g., no premium membership + * or not enough capacity) + */ + boolean addCourse(CoursePlan plan, Long courseId) throws EntityNotFoundException, CourseNotAvailableException; + + /** + * Removes the given course from the given course plan. + * + * @param plan course plan to remove the course from + * @param courseId the id of the course to remove + * @return true if the course was removed, false if the course was not contained in the course plan + */ + boolean removeCourse(CoursePlan plan, Long courseId); + + /** + * Submits the given course plan by attempting to enroll the owner of the course plan into each course in the plan. + * This method should create and persist the necessary {@code IEnrollment} objects in an atomic way, i.e., only if + * all courses can be enrolled. Otherwise a given transaction should be rolled back. If the commit was successful, + * the course plan should be removed from the in-memory storage. + * + * @throws CourseNotAvailableException if the course plan couldn't be submitted (e.g., because a course enrollment + * couldn't be finalized because the capacity changed) + * @throws EntityNotFoundException if a referenced membership or course could not be found + */ + void commit(CoursePlan coursePlan) throws EntityNotFoundException, CourseNotAvailableException; +} diff --git a/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/rest/ICoursePlanResource.java b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/rest/ICoursePlanResource.java new file mode 100644 index 0000000..f5f6ee0 --- /dev/null +++ b/ass2-service/api/src/main/java/dst/ass2/service/api/courseplan/rest/ICoursePlanResource.java @@ -0,0 +1,34 @@ +package dst.ass2.service.api.courseplan.rest; + +import javax.ws.rs.core.Response; + +import dst.ass2.service.api.courseplan.CourseNotAvailableException; +import dst.ass2.service.api.courseplan.CoursePlan; +import dst.ass2.service.api.courseplan.EntityNotFoundException; + +/** + * The ICoursePlanResource exposes the {@code ICoursePlanService} as a RESTful interface. + */ +public interface ICoursePlanResource { + + // TODO annotate the class and methods with the correct javax.ws.rs annotations + + Response createCoursePlan(Long membershipId) + throws EntityNotFoundException; + + Response deleteCoursePlan(Long coursePlanId) + throws EntityNotFoundException; + + CoursePlan getCoursePlan(Long coursePlanId) + throws EntityNotFoundException; + + Response addCourse(Long coursePlanId, Long courseId) + throws EntityNotFoundException, CourseNotAvailableException; + + Response removeCourse(Long coursePlanId, Long courseId) + throws EntityNotFoundException; + + Response submitCoursePlan(Long coursePlanId) + throws EntityNotFoundException, CourseNotAvailableException; + +} diff --git a/ass2-service/api/src/main/proto/dst/ass2/service/api/auth/proto/auth.proto b/ass2-service/api/src/main/proto/dst/ass2/service/api/auth/proto/auth.proto new file mode 100644 index 0000000..2c92277 --- /dev/null +++ b/ass2-service/api/src/main/proto/dst/ass2/service/api/auth/proto/auth.proto @@ -0,0 +1,3 @@ +syntax = "proto3"; + +// TODO implement authentication service diff --git a/ass2-service/api/src/test/java/dst/ass2/proto/auth/ProtoSpecificationTest.java b/ass2-service/api/src/test/java/dst/ass2/proto/auth/ProtoSpecificationTest.java new file mode 100644 index 0000000..a3c7bc4 --- /dev/null +++ b/ass2-service/api/src/test/java/dst/ass2/proto/auth/ProtoSpecificationTest.java @@ -0,0 +1,66 @@ +package dst.ass2.proto.auth; + +import io.grpc.MethodDescriptor; +import io.grpc.ServiceDescriptor; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.junit.Assert.assertThat; + +public class ProtoSpecificationTest { + + private ClassLoader cl; + public static final String SERVICE_NAME = "dst.ass2.service.api.auth.proto.AuthService"; + public static final String GRPC_CLASS_NAME = SERVICE_NAME + "Grpc"; + + @Before + public void setUp() throws Exception { + cl = ProtoSpecificationTest.class.getClassLoader(); + } + + @Test + public void generatedClass_exists() throws Exception { + try { + cl.loadClass(GRPC_CLASS_NAME); + } catch (ClassNotFoundException e) { + throw new AssertionError("Classpath did not contain expected gRPC service class", e); + } + } + + @Test + public void generatedClass_hasMethods() throws Exception { + assertThat(getMethodDescriptors().keySet(), hasItems( + SERVICE_NAME + "/authenticate", + SERVICE_NAME + "/validateToken" + )); + } + + private Map getMethodDescriptors() throws ClassNotFoundException { + return getMethodDescriptors(getServiceDescriptor(cl.loadClass(GRPC_CLASS_NAME))); + } + + private Map getMethodDescriptors(ServiceDescriptor sd) { + return sd.getMethods().stream() + .collect(Collectors.toMap( + MethodDescriptor::getFullMethodName, + Function.identity() + )); + } + + private ServiceDescriptor getServiceDescriptor(Class grpcClass) { + try { + Method getServiceDescriptor = grpcClass.getDeclaredMethod("getServiceDescriptor"); + getServiceDescriptor.setAccessible(true); + return (ServiceDescriptor) getServiceDescriptor.invoke(null); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Error finding service descriptor in " + grpcClass.getName(), e); + } + } +} diff --git a/ass2-service/auth-client/pom.xml b/ass2-service/auth-client/pom.xml new file mode 100644 index 0000000..1d88d71 --- /dev/null +++ b/ass2-service/auth-client/pom.xml @@ -0,0 +1,72 @@ + + + + 4.0.0 + + + at.ac.tuwien.infosys.dst + dst + 2018.1 + ../.. + + + ass2-service-auth-client + + DST :: Assignment 2 :: Service :: Auth Client + + jar + + + + at.ac.tuwien.infosys.dst + ass2-service-api + ${project.version} + + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + io.grpc + grpc-netty + + + + at.ac.tuwien.infosys.dst + ass2-service-auth + ${project.version} + test + + + at.ac.tuwien.infosys.dst + ass1-jpa + ${project.version} + test-jar + test + + + at.ac.tuwien.infosys.dst + ass2-service-auth + ${project.version} + test-jar + test + + + org.springframework + spring-orm + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/AuthenticationClientProperties.java b/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/AuthenticationClientProperties.java new file mode 100644 index 0000000..ad91c83 --- /dev/null +++ b/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/AuthenticationClientProperties.java @@ -0,0 +1,37 @@ +package dst.ass2.service.auth.client; + +/** + * This class holds the host and port value used to connect to the gRPC server. The CDI context provides an instance + * that you can inject into your implementation of {@link IAuthenticationClient}. The config values are loaded from the + * application.properties file. + */ +public class AuthenticationClientProperties { + + private String host; + private int port; + + public AuthenticationClientProperties() { + + } + + public AuthenticationClientProperties(String host, int port) { + this.host = host; + this.port = port; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } +} diff --git a/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/IAuthenticationClient.java b/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/IAuthenticationClient.java new file mode 100644 index 0000000..dc38f0a --- /dev/null +++ b/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/IAuthenticationClient.java @@ -0,0 +1,17 @@ +package dst.ass2.service.auth.client; + +import dst.ass2.service.api.auth.AuthenticationException; +import dst.ass2.service.api.auth.NoSuchUserException; + +public interface IAuthenticationClient extends AutoCloseable { + + String authenticate(String email, String password) throws NoSuchUserException, AuthenticationException; + + boolean isTokenValid(String token); + + /** + * Shuts down any underlying resource required to maintain this client. + */ + @Override + void close(); +} diff --git a/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/impl/GrpcAuthenticationClient.java b/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/impl/GrpcAuthenticationClient.java new file mode 100644 index 0000000..3ae5082 --- /dev/null +++ b/ass2-service/auth-client/src/main/java/dst/ass2/service/auth/client/impl/GrpcAuthenticationClient.java @@ -0,0 +1,32 @@ +package dst.ass2.service.auth.client.impl; + +import dst.ass2.service.api.auth.AuthenticationException; +import dst.ass2.service.api.auth.NoSuchUserException; +import dst.ass2.service.auth.client.AuthenticationClientProperties; +import dst.ass2.service.auth.client.IAuthenticationClient; + +public class GrpcAuthenticationClient implements IAuthenticationClient { + + // TODO make use of the generated grpc sources to implement a blocking client + + public GrpcAuthenticationClient(AuthenticationClientProperties properties) { + // TODO + } + + @Override + public String authenticate(String email, String password) throws NoSuchUserException, AuthenticationException { + // TODO + return null; + } + + @Override + public boolean isTokenValid(String token) { + // TODO + return false; + } + + @Override + public void close() { + // TODO + } +} diff --git a/ass2-service/auth-client/src/test/java/dst/ass2/service/auth/client/AuthenticationClientTest.java b/ass2-service/auth-client/src/test/java/dst/ass2/service/auth/client/AuthenticationClientTest.java new file mode 100644 index 0000000..ab039da --- /dev/null +++ b/ass2-service/auth-client/src/test/java/dst/ass2/service/auth/client/AuthenticationClientTest.java @@ -0,0 +1,76 @@ +package dst.ass2.service.auth.client; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.UUID; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +import dst.ass1.jpa.tests.TestData; +import dst.ass2.service.api.auth.AuthenticationException; +import dst.ass2.service.api.auth.NoSuchUserException; +import dst.ass2.service.auth.AuthenticationServiceApplication; +import dst.ass2.service.auth.client.impl.GrpcAuthenticationClient; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AuthenticationServiceApplication.class) +@Transactional +@ActiveProfiles("testdata") +public class AuthenticationClientTest { + + @Value("${grpc.port}") + private int port; + + private IAuthenticationClient client; + + @Before + public void setUp() throws Exception { + AuthenticationClientProperties properties = new AuthenticationClientProperties("localhost", port); + client = new GrpcAuthenticationClient(properties); + } + + @After + public void tearDown() throws Exception { + client.close(); + } + + @Test(expected = NoSuchUserException.class) + public void authenticate_invalidUser_throwsException() throws Exception { + client.authenticate("nonexisting@example.com", "foo"); + } + + @Test(expected = AuthenticationException.class) + public void authenticate_invalidPassword_throwsException() throws Exception { + client.authenticate(TestData.PARTICIPANT_1_EMAIL, "foo"); + } + + @Test + public void authenticate_existingUser_returnsToken() throws Exception { + String token = client.authenticate(TestData.PARTICIPANT_1_EMAIL, TestData.PARTICIPANT_1_PW); + assertNotNull(token); + } + + @Test + public void isTokenValid_invalidToken_returnsFalse() throws Exception { + boolean valid = client.isTokenValid(UUID.randomUUID().toString()); // should be false in *almost* all cases ;-) + assertFalse(valid); + } + + @Test + public void isTokenValid_onCreatedToken_returnsTrue() throws Exception { + String token = client.authenticate(TestData.PARTICIPANT_2_EMAIL, TestData.PARTICIPANT_2_PW); + boolean valid = client.isTokenValid(token); + assertTrue(valid); + } + +} diff --git a/ass2-service/auth/pom.xml b/ass2-service/auth/pom.xml new file mode 100644 index 0000000..b27cb8f --- /dev/null +++ b/ass2-service/auth/pom.xml @@ -0,0 +1,80 @@ + + + + 4.0.0 + + + at.ac.tuwien.infosys.dst + dst + 2018.1 + ../.. + + + ass2-service-auth + + DST :: Assignment 2 :: Service :: Auth Server + + jar + + + + at.ac.tuwien.infosys.dst + ass1-jpa + ${project.version} + + + at.ac.tuwien.infosys.dst + ass2-service-api + ${project.version} + + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + io.grpc + grpc-netty + + + + at.ac.tuwien.infosys.dst + ass1-jpa + ${project.version} + test-jar + test + + + org.springframework + spring-orm + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + kr.motd.maven + os-maven-plugin + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + + + diff --git a/ass2-service/auth/src/main/java/dst/ass2/service/auth/ICachingAuthenticationService.java b/ass2-service/auth/src/main/java/dst/ass2/service/auth/ICachingAuthenticationService.java new file mode 100644 index 0000000..c2c4362 --- /dev/null +++ b/ass2-service/auth/src/main/java/dst/ass2/service/auth/ICachingAuthenticationService.java @@ -0,0 +1,30 @@ +package dst.ass2.service.auth; + +import dst.ass2.service.api.auth.AuthenticationException; +import dst.ass2.service.api.auth.IAuthenticationService; +import dst.ass2.service.api.auth.NoSuchUserException; + +public interface ICachingAuthenticationService extends IAuthenticationService { + + /** + * {@inheritDoc} + * + *

+ * Instead of checking database records directly, the method first checks the cache for existing users. If the user + * is not in the cache, then the service checks the database for the given email address, and updates the cache if + * necessary. + *

+ */ + @Override + String authenticate(String email, String password) throws NoSuchUserException, AuthenticationException; + + /** + * Loads user data from the database into memory. + */ + void loadData(); + + /** + * Clears the data cached from the database. + */ + void clearCache(); +} diff --git a/ass2-service/auth/src/main/java/dst/ass2/service/auth/grpc/GrpcServerProperties.java b/ass2-service/auth/src/main/java/dst/ass2/service/auth/grpc/GrpcServerProperties.java new file mode 100644 index 0000000..badbfa2 --- /dev/null +++ b/ass2-service/auth/src/main/java/dst/ass2/service/auth/grpc/GrpcServerProperties.java @@ -0,0 +1,26 @@ +package dst.ass2.service.auth.grpc; + +/** + * This class holds the port value used to bind the gRPC server. The CDI context provides an instance that you can + * inject into your implementation of {@link IGrpcServerRunner}. The config values are loaded from the + * grpc.properties. + */ +public class GrpcServerProperties { + + private int port; + + public GrpcServerProperties() { + } + + public GrpcServerProperties(int port) { + this.port = port; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } +} diff --git a/ass2-service/auth/src/main/java/dst/ass2/service/auth/grpc/IGrpcServerRunner.java b/ass2-service/auth/src/main/java/dst/ass2/service/auth/grpc/IGrpcServerRunner.java new file mode 100644 index 0000000..c847b9b --- /dev/null +++ b/ass2-service/auth/src/main/java/dst/ass2/service/auth/grpc/IGrpcServerRunner.java @@ -0,0 +1,16 @@ +package dst.ass2.service.auth.grpc; + +import java.io.IOException; + +/** + * An implementation of this interface is expected by the application to start the grpc server. Inject get + * {@link GrpcServerProperties} to access the configuration. + */ +public interface IGrpcServerRunner { + /** + * Starts the gRPC server. + * + * @throws IOException start error + */ + void run() throws IOException; +} diff --git a/ass2-service/auth/src/main/resources/logback.xml b/ass2-service/auth/src/main/resources/logback.xml new file mode 100644 index 0000000..e16fad1 --- /dev/null +++ b/ass2-service/auth/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} - %highlight(%5p) [%12.12thread] %cyan(%-40.40logger{39}): %m%n + + + + + + + + diff --git a/ass2-service/auth/src/test/java/dst/ass2/service/auth/AuthenticationServiceApplication.java b/ass2-service/auth/src/test/java/dst/ass2/service/auth/AuthenticationServiceApplication.java new file mode 100644 index 0000000..6c9772b --- /dev/null +++ b/ass2-service/auth/src/test/java/dst/ass2/service/auth/AuthenticationServiceApplication.java @@ -0,0 +1,15 @@ +package dst.ass2.service.auth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication +@EnableTransactionManagement +public class AuthenticationServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthenticationServiceApplication.class, args); + } + +} diff --git a/ass2-service/auth/src/test/java/dst/ass2/service/auth/AuthenticationServiceApplicationConfig.java b/ass2-service/auth/src/test/java/dst/ass2/service/auth/AuthenticationServiceApplicationConfig.java new file mode 100644 index 0000000..d6b560a --- /dev/null +++ b/ass2-service/auth/src/test/java/dst/ass2/service/auth/AuthenticationServiceApplicationConfig.java @@ -0,0 +1,116 @@ +package dst.ass2.service.auth; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalEntityManagerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; + +import dst.ass1.jpa.dao.IDAOFactory; +import dst.ass1.jpa.dao.impl.DAOFactory; +import dst.ass1.jpa.model.IModelFactory; +import dst.ass1.jpa.model.impl.ModelFactory; +import dst.ass1.jpa.tests.TestData; +import dst.ass1.jpa.util.Constants; +import dst.ass2.service.auth.grpc.GrpcServerProperties; + +@SpringBootConfiguration +@PropertySource("classpath:/dst/ass2/service/auth/grpc.properties") +public class AuthenticationServiceApplicationConfig { + + @PersistenceContext + private EntityManager em; + + @Bean + public IModelFactory modelFactory() { + return new ModelFactory(); + } + + @Bean + public IDAOFactory daoFactory() { + return new DAOFactory(em); + } + + @Bean + public GrpcServerProperties grpcServerProperties(@Value("${grpc.port}") int port) { + return new GrpcServerProperties(port); + } + + @Bean + public SpringGrpcServerRunner springGrpcServerRunner() { + return new SpringGrpcServerRunner(); + } + + @Bean + public LocalEntityManagerFactoryBean entityManagerFactoryBean() { + LocalEntityManagerFactoryBean bean = new LocalEntityManagerFactoryBean(); + bean.setPersistenceUnitName(Constants.JPA_PERSISTENCE_UNIT); + // fixes collection proxy problem when using jersey + bean.getJpaPropertyMap().put("hibernate.enable_lazy_load_no_trans", true); + return bean; + } + + @Bean + public PlatformTransactionManager transactionManager(LocalEntityManagerFactoryBean entityManagerFactoryBean) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setPersistenceUnitName(Constants.JPA_PERSISTENCE_UNIT); + transactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject()); + return transactionManager; + } + + @Bean + @Profile("testdata") + public TestData testData() { + return new TestData(); + } + + @Bean + @Profile("testdata") + public TestDataInserter testDataInserter(TestData testData, IModelFactory modelFactory, PlatformTransactionManager transactionManager) { + return new TestDataInserter(testData, modelFactory, transactionManager); + } + + @Bean + @Profile("testdata") + public AuthServiceDataInjector dataInjector(TestDataInserter testDataInserter) { + return new AuthServiceDataInjector(em, testDataInserter); + } + + /** + * Makes sure data is in the database before the {@link ICachingAuthenticationService} is initialized. + */ + public static class AuthServiceDataInjector implements BeanPostProcessor { + private boolean dataInjected = false; + + private EntityManager em; + private TestDataInserter testDataInserter; + + public AuthServiceDataInjector(EntityManager em, TestDataInserter testDataInserter) { + this.em = em; + this.testDataInserter = testDataInserter; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!dataInjected && (bean instanceof ICachingAuthenticationService)) { + testDataInserter.insertTestData(em); + dataInjected = true; + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + } + +} diff --git a/ass2-service/auth/src/test/java/dst/ass2/service/auth/SpringGrpcServerRunner.java b/ass2-service/auth/src/test/java/dst/ass2/service/auth/SpringGrpcServerRunner.java new file mode 100644 index 0000000..2b77fe0 --- /dev/null +++ b/ass2-service/auth/src/test/java/dst/ass2/service/auth/SpringGrpcServerRunner.java @@ -0,0 +1,32 @@ +package dst.ass2.service.auth; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import dst.ass2.service.auth.grpc.IGrpcServerRunner; + +/** + * This class loads the {@link IGrpcServerRunner} from the application context and runs it after the application starts. + */ +public class SpringGrpcServerRunner implements CommandLineRunner, ApplicationContextAware { + + private static final Logger LOG = LoggerFactory.getLogger(SpringGrpcServerRunner.class); + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void run(String... args) throws Exception { + LOG.info("Getting instance of GrpcServerRunner"); + IGrpcServerRunner bean = applicationContext.getBean(IGrpcServerRunner.class); + LOG.info("Starting IGrpcServerRunner instance {}", bean); + bean.run(); + } +} diff --git a/ass2-service/auth/src/test/java/dst/ass2/service/auth/TestDataInserter.java b/ass2-service/auth/src/test/java/dst/ass2/service/auth/TestDataInserter.java new file mode 100644 index 0000000..a602f49 --- /dev/null +++ b/ass2-service/auth/src/test/java/dst/ass2/service/auth/TestDataInserter.java @@ -0,0 +1,30 @@ +package dst.ass2.service.auth; + +import dst.ass1.jpa.model.IModelFactory; +import dst.ass1.jpa.tests.TestData; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.persistence.EntityManager; + +public class TestDataInserter { + + private PlatformTransactionManager transactionManager; + private IModelFactory modelFactory; + private TestData testData; + + public TestDataInserter(TestData testData, IModelFactory modelFactory, PlatformTransactionManager transactionManager) { + this.testData = testData; + this.modelFactory = modelFactory; + this.transactionManager = transactionManager; + } + + public void insertTestData(EntityManager em) { + TransactionTemplate tx = new TransactionTemplate(transactionManager); + tx.execute(status -> { + testData.insert(modelFactory, em); + return null; + }); + } + +} diff --git a/ass2-service/auth/src/test/java/dst/ass2/service/auth/tests/AuthenticationServiceTest.java b/ass2-service/auth/src/test/java/dst/ass2/service/auth/tests/AuthenticationServiceTest.java new file mode 100644 index 0000000..fbfcefa --- /dev/null +++ b/ass2-service/auth/src/test/java/dst/ass2/service/auth/tests/AuthenticationServiceTest.java @@ -0,0 +1,138 @@ +package dst.ass2.service.auth.tests; + +import dst.ass1.jpa.model.IModelFactory; +import dst.ass1.jpa.model.IParticipant; +import dst.ass1.jpa.tests.TestData; +import dst.ass2.service.api.auth.AuthenticationException; +import dst.ass2.service.api.auth.NoSuchUserException; +import dst.ass2.service.auth.AuthenticationServiceApplication; +import dst.ass2.service.auth.ICachingAuthenticationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.BeansException; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AuthenticationServiceApplication.class) +@Transactional +@ActiveProfiles("testdata") +public class AuthenticationServiceTest implements ApplicationContextAware { + + @PersistenceContext + private EntityManager em; + + private ApplicationContext applicationContext; + + private IModelFactory modelFactory; + private ICachingAuthenticationService authenticationService; + + @Override + public void setApplicationContext(ApplicationContext ctx) throws BeansException { + applicationContext = ctx; + } + + @Before + public void setUp() { + modelFactory = applicationContext.getBean(IModelFactory.class); + authenticationService = applicationContext.getBean(ICachingAuthenticationService.class); + + // reload the data before each test + authenticationService.loadData(); + } + + @Test + public void authenticate_existingUser_createsTokenCorrectly() throws Exception { + String token = authenticationService.authenticate(TestData.PARTICIPANT_1_EMAIL, TestData.PARTICIPANT_1_PW); + assertNotNull(token); + } + + @Test + public void authenticate_existingUserNotInCache_createsTokenCorrectly() throws Exception { + IParticipant p = modelFactory.createParticipant(); + + p.setEmail("non-cached@example.com"); + try { + p.setPassword(MessageDigest.getInstance("SHA1").digest("somepw".getBytes())); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + p.setName("non-cached"); + p.setAccountNo("accountno"); + p.setBankCode("bankcode"); + + em.persist(p); + + String token = authenticationService.authenticate(p.getEmail(), "somepw"); + assertNotNull(token); + } + + @Test(expected = NoSuchUserException.class) + public void authenticate_invalidUser_throwsException() throws Exception { + authenticationService.authenticate("nonexisting@example.com", "foo"); + } + + @Test(expected = AuthenticationException.class) + public void authenticate_invalidPassword_throwsException() throws Exception { + authenticationService.authenticate(TestData.PARTICIPANT_1_EMAIL, "foo"); + } + + @Test + public void changePassword_existingUser_passwordChanged() throws Exception { + authenticationService.changePassword(TestData.PARTICIPANT_1_EMAIL, "newPwd"); + assertNotNull(authenticationService.authenticate(TestData.PARTICIPANT_1_EMAIL, "newPwd")); + } + + @Test(expected = NoSuchUserException.class) + public void changePassword_nonExistingUser_throwsException() throws Exception { + authenticationService.changePassword("nonexisting@example.com", "foo"); + } + + @Test + public void getUser_existingToken_returnsUser() throws Exception { + String token = authenticationService.authenticate(TestData.PARTICIPANT_1_EMAIL, TestData.PARTICIPANT_1_PW); + assertEquals(TestData.PARTICIPANT_1_EMAIL, authenticationService.getUser(token)); + } + + @Test + public void getUser_nonExistingToken_returnsNull() throws Exception { + assertNull(authenticationService.getUser("invalidToken")); + } + + @Test + public void isValid_existingToken_returnsTrue() throws Exception { + String token = authenticationService.authenticate(TestData.PARTICIPANT_1_EMAIL, TestData.PARTICIPANT_1_PW); + assertTrue(authenticationService.isValid(token)); + } + + @Test + public void isValid_nonExistingToken_returnsFalse() throws Exception { + assertFalse(authenticationService.isValid("invalidToken")); + } + + @Test + public void invalidate_validToken_tokenInvalidatedReturnsTrue() throws Exception { + String token = authenticationService.authenticate(TestData.PARTICIPANT_1_EMAIL, TestData.PARTICIPANT_1_PW); + assertTrue(authenticationService.invalidate(token)); + assertFalse(authenticationService.isValid(token)); + assertNull(authenticationService.getUser(token)); + } + + @Test + public void invalidate_invalidToken_returnsFalse() throws Exception { + assertFalse(authenticationService.invalidate("invalidToken")); + } + +} diff --git a/ass2-service/auth/src/test/java/dst/ass2/service/auth/tests/GrpcServerRunnerTest.java b/ass2-service/auth/src/test/java/dst/ass2/service/auth/tests/GrpcServerRunnerTest.java new file mode 100644 index 0000000..790cc1e --- /dev/null +++ b/ass2-service/auth/src/test/java/dst/ass2/service/auth/tests/GrpcServerRunnerTest.java @@ -0,0 +1,43 @@ +package dst.ass2.service.auth.tests; + +import java.net.Socket; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import dst.ass2.service.auth.AuthenticationServiceApplication; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = AuthenticationServiceApplication.class) +public class GrpcServerRunnerTest { + + private static final Logger LOG = LoggerFactory.getLogger(GrpcServerRunnerTest.class); + + @Value("${grpc.port}") + private int port; + + @Test + public void canConnectToGrpcSocketAfterApplicationInitialization() throws Exception { + int n = 4; + + while (true) { + LOG.info("Tyring to connect to TCP socket on localhost:{}", port); + try (Socket socket = new Socket("localhost", port)) { + return; + } catch (Exception e) { + if (n == 0) { + throw new AssertionError("Expected gRPC server to run on port " + port, e); + } else { + Thread.sleep(250); + } + } + n--; + } + + } +} diff --git a/ass2-service/auth/src/test/resources/dst/ass2/service/auth/grpc.properties b/ass2-service/auth/src/test/resources/dst/ass2/service/auth/grpc.properties new file mode 100644 index 0000000..9d203f4 --- /dev/null +++ b/ass2-service/auth/src/test/resources/dst/ass2/service/auth/grpc.properties @@ -0,0 +1 @@ +grpc.port=50051 \ No newline at end of file diff --git a/ass2-service/courseplan/pom.xml b/ass2-service/courseplan/pom.xml new file mode 100644 index 0000000..f214252 --- /dev/null +++ b/ass2-service/courseplan/pom.xml @@ -0,0 +1,57 @@ + + + + 4.0.0 + + + at.ac.tuwien.infosys.dst + dst + 2018.1 + ../.. + + + ass2-service-courseplan + + DST :: Assignment 2 :: Service :: Course Plan + + jar + + + + at.ac.tuwien.infosys.dst + ass1-jpa + ${project.version} + + + at.ac.tuwien.infosys.dst + ass2-service-api + ${project.version} + + + + at.ac.tuwien.infosys.dst + ass1-jpa + ${project.version} + test-jar + test + + + org.springframework + spring-orm + test + + + org.springframework.boot + spring-boot-starter-jersey + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + diff --git a/ass2-service/courseplan/src/main/java/dst/ass2/service/courseplan/impl/.gitkeep b/ass2-service/courseplan/src/main/java/dst/ass2/service/courseplan/impl/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/CoursePlanApplication.java b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/CoursePlanApplication.java new file mode 100644 index 0000000..19a3f7e --- /dev/null +++ b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/CoursePlanApplication.java @@ -0,0 +1,14 @@ +package dst.ass2.service.courseplan; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication +@EnableTransactionManagement +public class CoursePlanApplication { + + public static void main(String[] args) { + SpringApplication.run(CoursePlanApplication.class, args); + } +} diff --git a/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/CoursePlanApplicationConfig.java b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/CoursePlanApplicationConfig.java new file mode 100644 index 0000000..935db2b --- /dev/null +++ b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/CoursePlanApplicationConfig.java @@ -0,0 +1,58 @@ +package dst.ass2.service.courseplan; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.glassfish.jersey.server.ResourceConfig; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalEntityManagerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; + +import dst.ass1.jpa.dao.IDAOFactory; +import dst.ass1.jpa.dao.impl.DAOFactory; +import dst.ass1.jpa.model.IModelFactory; +import dst.ass1.jpa.model.impl.ModelFactory; +import dst.ass1.jpa.util.Constants; + +@SpringBootConfiguration +public class CoursePlanApplicationConfig { + + @PersistenceContext + private EntityManager em; + + @Bean + public ResourceConfig jerseyConfig() { + return new ResourceConfig() + .packages("dst.ass2.service.courseplan"); + } + + @Bean + public IModelFactory modelFactory() { + return new ModelFactory(); + } + + @Bean + public IDAOFactory daoFactory() { + return new DAOFactory(em); + } + + @Bean + public LocalEntityManagerFactoryBean entityManagerFactoryBean() { + LocalEntityManagerFactoryBean bean = new LocalEntityManagerFactoryBean(); + bean.setPersistenceUnitName(Constants.JPA_PERSISTENCE_UNIT); + // fixes collection proxy problem when using jersey + bean.getJpaPropertyMap().put("hibernate.enable_lazy_load_no_trans", true); + return bean; + } + + @Bean + public PlatformTransactionManager transactionManager(LocalEntityManagerFactoryBean entityManagerFactoryBean) { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setPersistenceUnitName(Constants.JPA_PERSISTENCE_UNIT); + transactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject()); + return transactionManager; + } + +} diff --git a/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/TestDataConfig.java b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/TestDataConfig.java new file mode 100644 index 0000000..1851901 --- /dev/null +++ b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/TestDataConfig.java @@ -0,0 +1,39 @@ +package dst.ass2.service.courseplan; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.transaction.PlatformTransactionManager; + +import dst.ass1.jpa.model.IModelFactory; +import dst.ass1.jpa.tests.TestData; + +@SpringBootConfiguration +@Profile("testdata") +public class TestDataConfig implements ApplicationListener { + + @PersistenceContext + private EntityManager em; + + @Bean + public TestData testData() { + return new TestData(); + } + + @Bean + public TestDataInserter testDataInserter(TestData testData, IModelFactory modelFactory, PlatformTransactionManager transactionManager) { + return new TestDataInserter(testData, modelFactory, transactionManager); + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + event.getApplicationContext() + .getBean(TestDataInserter.class) + .insertTestData(em); + } +} diff --git a/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/TestDataInserter.java b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/TestDataInserter.java new file mode 100644 index 0000000..483384f --- /dev/null +++ b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/TestDataInserter.java @@ -0,0 +1,34 @@ +package dst.ass2.service.courseplan; + +import dst.ass1.jpa.model.IModelFactory; +import dst.ass1.jpa.tests.TestData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.persistence.EntityManager; + +public class TestDataInserter { + private static final Logger LOG = LoggerFactory.getLogger(TestDataInserter.class); + + private PlatformTransactionManager transactionManager; + private IModelFactory modelFactory; + private TestData testData; + + public TestDataInserter(TestData testData, IModelFactory modelFactory, PlatformTransactionManager transactionManager) { + this.testData = testData; + this.modelFactory = modelFactory; + this.transactionManager = transactionManager; + } + + public void insertTestData(EntityManager em) { + LOG.info("Inserting test data..."); + TransactionTemplate tx = new TransactionTemplate(transactionManager); + tx.execute(status -> { + testData.insert(modelFactory, em); + return null; + }); + } + +} diff --git a/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/tests/CoursePlanResourceTest.java b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/tests/CoursePlanResourceTest.java new file mode 100644 index 0000000..975c917 --- /dev/null +++ b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/tests/CoursePlanResourceTest.java @@ -0,0 +1,231 @@ +package dst.ass2.service.courseplan.tests; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import dst.ass1.jpa.tests.TestData; +import dst.ass2.service.api.courseplan.CoursePlan; +import dst.ass2.service.courseplan.CoursePlanApplication; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CoursePlanApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("testdata") +@org.springframework.transaction.annotation.Transactional +public class CoursePlanResourceTest { + + @Autowired + private TestData testData; + + @LocalServerPort + private int port; + + private TestRestTemplate restTemplate; + private HttpHeaders headers; + + @Before + public void setUp() { + headers = new HttpHeaders(); + restTemplate = new TestRestTemplate(); + } + + @Test + public void createCoursePlan_withWrongHttpMethod_returnsError() { + String url = url("/courseplans"); + ResponseEntity response = restTemplate.getForEntity(url, String.class, body("membershipId", testData.membership1Id)); + + assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.CLIENT_ERROR)); + } + + @Test + public void createCoursePlan_withNonExistingMembershipKey_returnsNotFoundError() { + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String url = url("/courseplans"); + MultiValueMap body = body("membershipId", 123456789); + + HttpEntity request = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + + assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + } + + @Test + public void createCoursePlan_withExistingMembershipKey_returnsCourseplanId() { + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String url = url("/courseplans"); + MultiValueMap body = body("membershipId", testData.membership1Id); + + HttpEntity request = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + + assertThat(response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL)); + assertThat(response.getBody(), notNullValue()); + + try { + Long.parseLong(response.getBody()); + } catch (NumberFormatException e) { + throw new AssertionError("Response body of " + url + " should be a Long value (the courseplan ID)", e); + } + } + + @Test + public void getCoursePlan_onNonExistingPlan_returns404() { + String url = url("/courseplans/" + 12345678); + ResponseEntity response = restTemplate.getForEntity(url, String.class); + assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + } + + @Test + public void getCoursePlan_onCreatedCoursePlan_returnsJsonEntity() { + Long id = createCoursePlan(testData.membership1Id); + + String url = url("/courseplans/" + id); + ResponseEntity response = restTemplate.getForEntity(url, CoursePlan.class); + + assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL)); + assertThat(response.getBody(), notNullValue()); + assertThat(response.getBody().getId(), is(id)); + assertThat(response.getBody().getMembershipId(), is(testData.membership1Id)); + } + + @Test + public void addCourse_onCreatedCoursePlan_returnsOk() { + Long id = createCoursePlan(testData.membership2Id); + ResponseEntity response = addCourse(id, testData.course2Id); + assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL)); + } + + @Test + public void addCourse_andThenGetCoursePlan_containsCourseInList() { + Long id = createCoursePlan(testData.membership2Id); + + addCourse(id, testData.course2Id); + CoursePlan coursePlan = getCoursePlan(id); + + assertThat(coursePlan.getCourses().size(), is(1)); + assertThat(coursePlan.getCourses(), hasItem(testData.course2Id)); + } + + @Test + public void addCourse_onUnavailableCourse_returnsAppropriateError() { + Long id = createCoursePlan(testData.membership1Id); + ResponseEntity response = addCourse(id, testData.course1Id); + + assertThat("Make use of an appropriate HTTP status code", response.getStatusCode(), allOf( + is(not(HttpStatus.OK)), + is(not(HttpStatus.NOT_FOUND)), + is(not(HttpStatus.INTERNAL_SERVER_ERROR)) + )); + } + + @Test + public void addCourse_onNonExistingCourse_returns404() { + Long id = createCoursePlan(testData.membership1Id); + ResponseEntity response = addCourse(id, 123456789L); + assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + } + + @Test + public void submit_nonExistingCoursePlan_returns404() { + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String url = url("/courseplans/123456789/submit"); + ResponseEntity response = restTemplate.postForEntity(url, new LinkedMultiValueMap<>(), String.class); + assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + } + + @Test + public void submit_onExistingCoursePlan_returnsOk() { + Long id = createCoursePlan(testData.membership2Id); + addCourse(id, testData.course3Id); + + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String url = url("/courseplans/" + id + "/submit"); + ResponseEntity response = restTemplate.postForEntity(url, new LinkedMultiValueMap<>(), String.class); + assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL)); + } + + @Test + public void delete_existingCoursePlan_returnsSuccessful() { + Long id = createCoursePlan(testData.membership1Id); + String url = url("/courseplans/" + id); + + HttpEntity request = new HttpEntity<>(null, headers); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class); + assertThat("Response was: " + response, response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL)); + } + + @Test + public void delete_nonExistingCoursePlan_returns404() { + String url = url("/courseplans/12345678"); + HttpEntity request = new HttpEntity<>(null, headers); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.DELETE, request, String.class); + assertThat("Response was: " + response, response.getStatusCode(), is(HttpStatus.NOT_FOUND)); + } + + private CoursePlan getCoursePlan(Long id) { + String url = url("/courseplans/" + id); + ResponseEntity response = restTemplate.getForEntity(url, CoursePlan.class); + return response.getBody(); + } + + private Long createCoursePlan(Long membershipId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String url = url("/courseplans"); + MultiValueMap body = body("membershipId", membershipId); + HttpEntity request = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + + if(!response.getStatusCode().is2xxSuccessful()) { + throw new AssertionError("Expected createCoursePlan to return 2xx, instead got: " + response); + } + + try { + return Long.parseLong(response.getBody()); + } catch (NumberFormatException e) { + throw new AssertionError("Expected createCoursePlan to return an id, instead got: " + response); + } + } + + private ResponseEntity addCourse(Long coursePlanId, Long courseId) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String url = url("/courseplans/" + coursePlanId + "/courses"); + MultiValueMap body = body("courseId", courseId); + return restTemplate.postForEntity(url, new HttpEntity<>(body, headers), String.class); + } + + private String url(String uri) { + return "http://localhost:" + port + uri; + } + + private MultiValueMap body(String key, Object value) { + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add(key, String.valueOf(value)); + return map; + } + +} diff --git a/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/tests/CoursePlanServiceTest.java b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/tests/CoursePlanServiceTest.java new file mode 100644 index 0000000..c40e9f5 --- /dev/null +++ b/ass2-service/courseplan/src/test/java/dst/ass2/service/courseplan/tests/CoursePlanServiceTest.java @@ -0,0 +1,288 @@ +package dst.ass2.service.courseplan.tests; + +import dst.ass1.jpa.dao.IDAOFactory; +import dst.ass1.jpa.dao.IParticipantDAO; +import dst.ass1.jpa.model.*; +import dst.ass1.jpa.tests.TestData; +import dst.ass2.service.api.courseplan.CourseNotAvailableException; +import dst.ass2.service.api.courseplan.CoursePlan; +import dst.ass2.service.api.courseplan.EntityNotFoundException; +import dst.ass2.service.api.courseplan.ICoursePlanService; +import dst.ass2.service.courseplan.CoursePlanApplication; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.Collection; +import java.util.Optional; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CoursePlanApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@org.springframework.transaction.annotation.Transactional +@ActiveProfiles("testdata") +public class CoursePlanServiceTest implements ApplicationContextAware { + + private static final Logger LOG = LoggerFactory.getLogger(CoursePlanServiceTest.class); + + private ApplicationContext ctx; + + @PersistenceContext + private EntityManager em; + + private ICoursePlanService service; + private IDAOFactory daoFactory; + private IModelFactory modelFactory; + private TestData testData; + private IParticipantDAO participantDAO; + + private Long p1cp1p; + private Long p1cp2np; + private Long p2cp1np; + private Long p2cp2p; + private Long course4Id; + + @Override + public void setApplicationContext(ApplicationContext ctx) throws BeansException { + this.ctx = ctx; + } + + @Before + public void setUp() { + LOG.info("Test resolving beans from application context"); + daoFactory = ctx.getBean(IDAOFactory.class); + modelFactory = ctx.getBean(IModelFactory.class); + service = ctx.getBean(ICoursePlanService.class); + testData = ctx.getBean(TestData.class); + participantDAO = daoFactory.createParticipantDAO(); + + // course4Id is wrong in TestData, find out the correct ID + course4Id = daoFactory.createCourseDAO().findAll().stream() + .filter(c -> c.getName().equals(TestData.COURSE_4_NAME)).findFirst().get().getId(); + + // Create an additional membership for tests + IMembership membership4 = modelFactory.createMembership(); + membership4.setDiscount(10.0); + membership4.setPremium(true); + membership4.setParticipant(participantDAO.findById(testData.participant2Id)); + membership4.setCoursePlatform(daoFactory.createCoursePlatformDAO().findById(testData.coursePlatform2Id)); + IParticipant participant2 = participantDAO.findById(testData.participant2Id); + participant2.getMemberships().add(membership4); + em.persist(membership4); + em.persist(participant2); + + // find the IDs of the memberships + Collection memberships1 = participantDAO.findById(testData.participant1Id).getMemberships(); + Collection memberships2 = participantDAO.findById(testData.participant2Id).getMemberships(); + + // participant 1, course platform 1, premium + p1cp1p = memberships1.stream().filter(m -> m.getPremium()).findFirst().get().getId(); + + // participant 1, course platform 2, non-premium + p1cp2np = memberships1.stream().filter(m -> !m.getPremium()).findFirst().get().getId(); + + // participant 2, course platform 1, non-premium + p2cp1np = memberships2.stream().filter(m -> !m.getPremium()).findFirst().get().getId(); + + // participant 2, course platform 2, premium + p2cp2p = memberships2.stream().filter(m -> m.getPremium()).findFirst().get().getId(); + } + + @Test + public void testCreateWithValidMembership_returnsNewCoursePlan() throws EntityNotFoundException { + CoursePlan plan = service.create(p1cp1p); + assertNotNull(plan); + assertNotNull(plan.getId()); + assertEquals(p1cp1p, plan.getMembershipId()); + } + + @Test(expected = EntityNotFoundException.class) + public void testCreateWithInvalidMembership_throwsException() throws EntityNotFoundException { + service.create(1337L); + } + + @Test(expected = EntityNotFoundException.class) + public void testDeleteInvalidId_throwsException() throws EntityNotFoundException { + service.delete(1337L); + } + + @Test + public void testDelete_findReturnsNull() throws EntityNotFoundException { + CoursePlan plan = service.create(p1cp1p); + assertNotNull(plan); + assertNotNull(plan.getId()); + service.delete(plan.getId()); + assertNull(service.find(plan.getId())); + } + + @Test + public void testFind_returnsCorrectPlan() throws EntityNotFoundException { + CoursePlan plan = service.create(p1cp1p); + CoursePlan plan1 = service.find(plan.getId()); + assertEquals(plan, plan1); + } + + @Test + public void testFindInvalidId_returnsNull() { + assertNull(service.find(1337L)); + } + + @Test + public void testAdd_courseAdded() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p1cp2np); + assertTrue(service.addCourse(plan, testData.course2Id)); + assertFalse(plan.getCourses().isEmpty()); + assertEquals(testData.course2Id, plan.getCourses().get(0)); + } + + @Test + public void testAddPremiumCourseWithPremium_courseAdded() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p2cp2p); + assertTrue(service.addCourse(plan, course4Id)); + assertFalse(plan.getCourses().isEmpty()); + assertEquals(course4Id, plan.getCourses().get(0)); + } + + @Test(expected = CourseNotAvailableException.class) + public void testAddCourseFromDifferentPlatform_throwsException() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p2cp1np); + service.addCourse(plan, course4Id); + } + + @Test + public void testAddExistingCourse_returnsFalse() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p1cp2np); + assertTrue(service.addCourse(plan, testData.course2Id)); + assertFalse(service.addCourse(plan, testData.course2Id)); + } + + @Test(expected = EntityNotFoundException.class) + public void testAddInvalidCourse_throwsException() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p2cp1np); + service.addCourse(plan, 1337L); + } + + @Test(expected = EntityNotFoundException.class) + public void testAddInvalidMembership_throwsException() throws EntityNotFoundException { + service.create(1337L); + } + + @Test(expected = CourseNotAvailableException.class) + public void testAddPremiumCourseWithoutPremium_throwsException() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p2cp1np); + service.addCourse(plan, testData.course1Id); + } + + @Test(expected = CourseNotAvailableException.class) + public void testAddCourseWithoutCapacity_throwsException() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p1cp1p); + service.addCourse(plan, testData.course3Id); + } + + @Test(expected = CourseNotAvailableException.class) + public void testAddCourseAlreadyInDB_throwsException() throws CourseNotAvailableException, EntityNotFoundException { + CoursePlan plan = service.create(p1cp1p); + service.addCourse(plan, testData.course1Id); + } + + @Test + public void testRemoveCourse_courseRemoved() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p2cp2p); + assertTrue(service.addCourse(plan, course4Id)); + assertTrue(service.removeCourse(plan, course4Id)); + CoursePlan plan1 = service.find(plan.getId()); + assertTrue(plan1.getCourses().isEmpty()); + } + + @Test + public void testRemoveNotAddedCourse_returnsFalse() throws EntityNotFoundException { + CoursePlan plan = service.create(p1cp1p); + assertFalse(service.removeCourse(plan, testData.course1Id)); + } + + @Test + public void testCommit_committed() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p1cp2np); + assertTrue(service.addCourse(plan, testData.course2Id)); + service.commit(plan); + assertTrue(isEnrolled(testData.participant1Id, testData.course2Id)); + } + + @Test + public void testCommitPremiumCourseWithPremium_committed() throws EntityNotFoundException, CourseNotAvailableException { + CoursePlan plan = service.create(p2cp2p); + assertTrue(service.addCourse(plan, course4Id)); + service.commit(plan); + assertTrue(isEnrolled(testData.participant2Id, course4Id)); + } + + @Test(expected = EntityNotFoundException.class) + public void testCommitWithInvalidMembership_throwsException() throws CourseNotAvailableException, EntityNotFoundException { + CoursePlan plan = service.create(p1cp2np); + assertTrue(service.addCourse(plan, testData.course2Id)); + em.remove(daoFactory.createMembershipDAO().findById(p1cp2np)); + service.commit(plan); + } + + @Test(expected = EntityNotFoundException.class) + public void testCommitWithInvalidCourse_throwsException() throws CourseNotAvailableException, EntityNotFoundException { + CoursePlan plan = service.create(p1cp2np); + assertTrue(service.addCourse(plan, testData.course2Id)); + em.remove(daoFactory.createCourseDAO().findById(testData.course2Id)); + service.commit(plan); + } + + @Test(expected = CourseNotAvailableException.class) + public void testCommitPremiumCourseWithoutPremium_throwsException() throws CourseNotAvailableException, EntityNotFoundException { + CoursePlan plan = service.create(p2cp2p); + assertTrue(service.addCourse(plan, course4Id)); + IMembership membership = daoFactory.createMembershipDAO().findById(p2cp2p); + membership.setPremium(false); + em.persist(membership); + service.commit(plan); + } + + @Test(expected = CourseNotAvailableException.class) + public void testCommitCourseFromDifferentPlatform_throwsException() throws CourseNotAvailableException, EntityNotFoundException { + CoursePlan plan = service.create(p2cp2p); + assertTrue(service.addCourse(plan, course4Id)); + IMembership membership = daoFactory.createMembershipDAO().findById(p2cp2p); + membership.setCoursePlatform(daoFactory.createCoursePlatformDAO().findById(testData.coursePlatform1Id)); + em.persist(membership); + service.commit(plan); + } + + @Test(expected = CourseNotAvailableException.class) + public void testCommitCourseWithoutCapacity_throwsException() throws CourseNotAvailableException, EntityNotFoundException { + CoursePlan plan = service.create(p1cp2np); + assertTrue(service.addCourse(plan, testData.course2Id)); + ICourse course = daoFactory.createCourseDAO().findById(testData.course2Id); + course.setCapacity(0); + em.persist(course); + service.commit(plan); + } + + @Test(expected = CourseNotAvailableException.class) + public void testCommitCourseAlreadyInDB_throwsException() throws CourseNotAvailableException, EntityNotFoundException { + CoursePlan plan = service.create(p1cp2np); + service.addCourse(plan, testData.course2Id); + service.commit(plan); + service.commit(plan); + } + + private boolean isEnrolled(Long participantId, Long courseId) { + Optional first = participantDAO.findById(participantId).getEnrollments().stream().filter(e -> e.getId().getCourse().getId().equals(courseId)).findFirst(); + return first.isPresent(); + } +} diff --git a/ass2-service/courseplan/src/test/resources/application.properties b/ass2-service/courseplan/src/test/resources/application.properties new file mode 100644 index 0000000..c68b46d --- /dev/null +++ b/ass2-service/courseplan/src/test/resources/application.properties @@ -0,0 +1 @@ +server.port=8091 diff --git a/ass2-service/facade/pom.xml b/ass2-service/facade/pom.xml new file mode 100644 index 0000000..c4bde9f --- /dev/null +++ b/ass2-service/facade/pom.xml @@ -0,0 +1,53 @@ + + + + 4.0.0 + + + at.ac.tuwien.infosys.dst + dst + 2018.1 + ../.. + + + ass2-service-facade + + DST :: Assignment 2 :: Service :: Facade + + jar + + + + at.ac.tuwien.infosys.dst + ass2-service-api + ${project.version} + + + at.ac.tuwien.infosys.dst + ass2-service-auth-client + ${project.version} + + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jersey.ext + jersey-proxy-client + + + + org.springframework.boot + spring-boot-starter-jersey + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file diff --git a/ass2-service/facade/src/main/java/dst/ass2/service/facade/impl/.gitkeep b/ass2-service/facade/src/main/java/dst/ass2/service/facade/impl/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ass2-service/facade/src/test/java/dst/ass2/service/facade/ServiceFacadeApplication.java b/ass2-service/facade/src/test/java/dst/ass2/service/facade/ServiceFacadeApplication.java new file mode 100644 index 0000000..998d5f2 --- /dev/null +++ b/ass2-service/facade/src/test/java/dst/ass2/service/facade/ServiceFacadeApplication.java @@ -0,0 +1,13 @@ +package dst.ass2.service.facade; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ServiceFacadeApplication { + + public static void main(String[] args) { + SpringApplication.run(ServiceFacadeApplication.class, args); + } + +} diff --git a/ass2-service/facade/src/test/java/dst/ass2/service/facade/ServiceFacadeApplicationConfig.java b/ass2-service/facade/src/test/java/dst/ass2/service/facade/ServiceFacadeApplicationConfig.java new file mode 100644 index 0000000..0b79458 --- /dev/null +++ b/ass2-service/facade/src/test/java/dst/ass2/service/facade/ServiceFacadeApplicationConfig.java @@ -0,0 +1,82 @@ +package dst.ass2.service.facade; + +import java.net.URI; + +import org.glassfish.jersey.server.ResourceConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; + +import dst.ass2.service.api.auth.AuthenticationException; +import dst.ass2.service.api.auth.NoSuchUserException; +import dst.ass2.service.auth.client.AuthenticationClientProperties; +import dst.ass2.service.auth.client.IAuthenticationClient; +import dst.ass2.service.auth.client.impl.GrpcAuthenticationClient; + +@SpringBootConfiguration +public class ServiceFacadeApplicationConfig { + + @Bean + public ResourceConfig jerseyConfig() { + return new ResourceConfig() + .packages("dst.ass2.service.facade"); + } + + @Bean + public URI coursePlanURI(@Value("${courseplan.uri}") URI target) { + return target; + } + + @Bean + public AuthenticationClientProperties authenticationClientProperties( + @Value("${auth.host}") String host, + @Value("${auth.port}") int port) { + return new AuthenticationClientProperties(host, port); + } + + @Bean + @Profile("!AuthenticationResourceTest") + // only use this when we're not running individual tests + public IAuthenticationClient grpcAuthenticationClient(AuthenticationClientProperties authenticationClientProperties) { + return new GrpcAuthenticationClient(authenticationClientProperties); + } + + @Bean + @Profile("AuthenticationResourceTest") + public IAuthenticationClient mockAuthenticationClient() { + return new MockAuthenticationClient(); + } + + public static class MockAuthenticationClient implements IAuthenticationClient { + + private static final Logger LOG = LoggerFactory.getLogger(MockAuthenticationClient.class); + + public static String TOKEN = "123e4567-e89b-12d3-a456-426655440000"; + + @Override + public String authenticate(String email, String password) throws NoSuchUserException, AuthenticationException { + LOG.info("Calling MockAuthenticationClient with {}, {}", email, password); + + if (email.equals("junit@example.com")) { + if (password.equals("junit")) { + return TOKEN; + } + throw new AuthenticationException(); + } + throw new NoSuchUserException(); + } + + @Override + public boolean isTokenValid(String t) { + return TOKEN.equals(t); + } + + @Override + public void close() { + // pass + } + } +} diff --git a/ass2-service/facade/src/test/java/dst/ass2/service/facade/test/AuthenticationResourceTest.java b/ass2-service/facade/src/test/java/dst/ass2/service/facade/test/AuthenticationResourceTest.java new file mode 100644 index 0000000..b033012 --- /dev/null +++ b/ass2-service/facade/src/test/java/dst/ass2/service/facade/test/AuthenticationResourceTest.java @@ -0,0 +1,97 @@ +package dst.ass2.service.facade.test; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import dst.ass2.service.facade.ServiceFacadeApplication; +import dst.ass2.service.facade.ServiceFacadeApplicationConfig; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ServiceFacadeApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("AuthenticationResourceTest") +public class AuthenticationResourceTest { + + @LocalServerPort + private int port; + + private RestTemplate restTemplate; + private HttpHeaders headers; + + @Before + public void setUp() { + headers = new HttpHeaders(); + restTemplate = new RestTemplate(); + + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + BufferingClientHttpRequestFactory bufferingClientHttpRequestFactory = new BufferingClientHttpRequestFactory(requestFactory); + requestFactory.setOutputStreaming(false); + restTemplate.setRequestFactory(bufferingClientHttpRequestFactory); + } + + @Test + public void authenticate_withValidUser_returnsOkAndToken() throws Exception { + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String url = url("/auth/authenticate"); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("email", "junit@example.com"); + body.add("password", "junit"); + + HttpEntity request = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + + assertThat(response.getStatusCode().series(), is(HttpStatus.Series.SUCCESSFUL)); + assertNotNull(response.getBody()); + assertThat(response.getBody(), is(ServiceFacadeApplicationConfig.MockAuthenticationClient.TOKEN)); + } + + @Test + public void authenticate_withInvalidUser_returnsAppropriateCode() throws Exception { + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + String url = url("/auth/authenticate"); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("email", "nonexisting@example.com"); + body.add("password", "wrong"); + + HttpEntity request = new HttpEntity<>(body, headers); + HttpStatus status; + + try { + status = restTemplate.postForEntity(url, request, String.class).getStatusCode(); + } catch (HttpClientErrorException e) { + status = e.getStatusCode(); + } + + assertThat("Return an appropriate error code", status, allOf( + not(HttpStatus.OK), + not(HttpStatus.NOT_FOUND), + not(HttpStatus.INTERNAL_SERVER_ERROR) + )); + } + + private String url(String uri) { + return "http://localhost:" + port + uri; + } + +} diff --git a/ass2-service/facade/src/test/resources/application.properties b/ass2-service/facade/src/test/resources/application.properties new file mode 100644 index 0000000..90326e9 --- /dev/null +++ b/ass2-service/facade/src/test/resources/application.properties @@ -0,0 +1,8 @@ +server.port=8090 + +# this is the gRPC host the client tries to connect to +auth.host=localhost +auth.port=50051 + +# this is the root of the remote resource that the facade accesses via its client +courseplan.uri=http://localhost:8091/ diff --git a/ass2-service/facade/src/test/resources/logback.xml b/ass2-service/facade/src/test/resources/logback.xml new file mode 100644 index 0000000..e16fad1 --- /dev/null +++ b/ass2-service/facade/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} - %highlight(%5p) [%12.12thread] %cyan(%-40.40logger{39}): %m%n + + + + + + + + diff --git a/pom.xml b/pom.xml index b27e897..eb160e3 100644 --- a/pom.xml +++ b/pom.xml @@ -240,6 +240,93 @@ javassist ${javassist.version} + + + commons-io + commons-io + ${commons-io.version} + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + org.springframework + spring-aop + ${spring.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + io.grpc + grpc-all + ${grpc.version} + + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-testing + ${grpc.version} + + + org.springframework + spring-orm + ${spring.version} + + + org.springframework.boot + spring-boot-starter-jersey + ${spring-boot.version} + + + org.glassfish.jersey.core + jersey-client + ${jersey.version} + + + org.glassfish.jersey.ext + jersey-proxy-client + ${jersey.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + @@ -251,6 +338,13 @@ ass1-jpa ass1-doc ass1-kv + ass2-service/api + ass2-service/auth-client + ass2-service/auth + ass2-service/courseplan + ass2-service/facade + ass2-aop + ass2-di @@ -275,6 +369,77 @@ + + ass2-service + + ass1-jpa + ass2-service/api + ass2-service/auth-client + ass2-service/auth + ass2-service/courseplan + ass2-service/facade + + + + + ass2-di + + + + maven-surefire-plugin + + + dst/ass2/di/**/*Transparent*.java + + + + + + + ass2-di + + + + + ass2-di-agent + + + + maven-jar-plugin + 2.6 + + + + dst.ass2.di.agent.InjectorAgent + + + dst-di-agent + + + + + jar + + test-compile + + + + + maven-surefire-plugin + + -javaagent:"${project.build.directory}/dst-di-agent.jar" + + dst/ass2/di/**/*Transparent*.java + + + + + + + ass2-di + + + @@ -303,6 +468,15 @@ 1.4.196 3.6.1 2.0.0 + + 1.8.13 + 4.3.13.RELEASE + 1.5.9.RELEASE + 2.25.1 + 2.6 + 1.10.1 + 0.5.1 + 1.5.0.Final -- 2.43.0