Add template for assignment 2

This commit is contained in:
2021-04-26 14:12:50 +02:00
parent 6d5b4b7b48
commit dd1e95d6aa
100 changed files with 6085 additions and 0 deletions
@@ -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();
}
@@ -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();
}
@@ -0,0 +1,10 @@
package dst.ass2.aop;
public class PluginExecutorFactory {
public static IPluginExecutor createPluginExecutor() {
// TODO
return null;
}
}
@@ -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 {
}
@@ -0,0 +1,7 @@
package dst.ass2.aop.logging;
public class LoggingAspect {
// TODO
}
@@ -0,0 +1,7 @@
package dst.ass2.aop.management;
public class ManagementAspect {
// TODO
}
@@ -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();
}
@@ -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<? extends IPluginExecutable> pluginClass;
private EventType type;
private String message;
public Event(EventType type, Class<? extends IPluginExecutable> pluginClass, String message) {
this.type = type;
this.pluginClass = pluginClass;
this.message = message;
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
int pos = stackTrace[1].getMethodName().equals("<init>") ? 1 : 2;
Assert.state(stackTrace[pos].getMethodName().equals("<init>"), "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<? extends IPluginExecutable> 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();
}
}
@@ -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.
* <p/>
* Note that this implementation is thread safe.
*/
public class EventBus {
private static final EventBus instance = new EventBus();
private final List<Event> events = new ArrayList<Event>();
public static EventBus getInstance() {
return instance;
}
private EventBus() {
}
/**
* Returns all events of the certain type(s).<br/>
* 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<Event> getEvents(EventType... types) {
synchronized (events) {
if (types == null || types.length == 0) {
return new ArrayList<Event>(events);
} else {
List<Event> list = new ArrayList<Event>();
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<? extends IPluginExecutable>) 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<? extends IPluginExecutable> 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;
}
}
@@ -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<? extends IPluginExecutable> clazz = (Class<? extends IPluginExecutable>) 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() {
}
}
@@ -0,0 +1,5 @@
package dst.ass2.aop.event;
public enum EventType {
PLUGIN_START, PLUGIN_END, INFO
}
@@ -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() {
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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());
}
@@ -0,0 +1,4 @@
package dst.ass2.aop.sample;
public class SystemOutPluginExecutable extends AbstractPluginExecutable {
}
@@ -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<Event> 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<Event> starts = eventBus.getEvents(EventType.PLUGIN_START);
List<Event> 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<Event> starts = eventBus.getEvents(EventType.PLUGIN_START);
List<Event> 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());
}
}
}
}
@@ -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<Method, Around> around = PluginUtils.findMethodAnnotation(LoggingAspect.class, Around.class);
Map<Method, Before> before = PluginUtils.findMethodAnnotation(LoggingAspect.class, Before.class);
Map<Method, After> 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<Event> 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() {
}
}
@@ -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<Method, Around> around = PluginUtils.findMethodAnnotation(ManagementAspect.class, Around.class);
Map<Method, Before> before = PluginUtils.findMethodAnnotation(ManagementAspect.class, Before.class);
Map<Method, After> 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<Event> 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);
}
}
@@ -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 {
}
@@ -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);
}
}
}
}
@@ -0,0 +1,277 @@
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
* (should not be the case, since it is set to accessible manually)
*/
@SuppressWarnings("unchecked")
public static <T> T setStaticFinalField(Class<?> clazz, String name, T value)
throws IllegalAccessException {
// Retrieve the desired field
Field field = findField(clazz, name);
field.setAccessible(true);
// 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.<br/>
*
* @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.<br/>
* 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.<br/>
* 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<? extends IPluginExecutable> 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 <A extends Annotation> Map<Method, A> findMethodAnnotation(
Class<?> clazz, Class<A> annotationType) {
Map<Method, A> map = new HashMap<Method, A>();
for (Method method : clazz.getDeclaredMethods()) {
A annotation = AnnotationUtils.findAnnotation(method,
annotationType);
if (annotation != null) {
map.put(method, annotation);
}
}
return map;
}
}