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
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>at.ac.tuwien.infosys.dst</groupId>
<artifactId>dst</artifactId>
<version>2021.1</version>
<relativePath>..</relativePath>
</parent>
<artifactId>ass2-ioc</artifactId>
<packaging>jar</packaging>
<name>DST :: Assignment 2 :: IoC</name>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,27 @@
package dst.ass2.ioc.di;
import dst.ass2.ioc.di.annotation.Property;
import java.util.Properties;
public interface IObjectContainer {
/**
* Returns the (mutable) data structure holding property values that can be injected via the
* {@link Property} annotation.
*
* @return A mutable Properties object
*/
Properties getProperties();
/**
* Returns a container-managed object of the given type.
*
* @param type the type of object
* @param <T> the class type
* @return the object
* @throws InjectionException throw the concrete InjectionException as specified in the assignment
*/
<T> T getObject(Class<T> type) throws InjectionException;
}
@@ -0,0 +1,17 @@
package dst.ass2.ioc.di;
import dst.ass2.ioc.di.annotation.Property;
import java.util.Properties;
public interface IObjectContainerFactory {
/**
* Creates a new IObjectContainer instance that uses the given properties for injecting
* {@link Property} instances.
*
* @param properties a Properties object
* @return a new IObjectContainer instance
*/
IObjectContainer newObjectContainer(Properties properties);
}
@@ -0,0 +1,18 @@
package dst.ass2.ioc.di;
public class InjectionException extends RuntimeException {
private static final long serialVersionUID = 1L;
public InjectionException(String message) {
super(message);
}
public InjectionException(String message, Throwable cause) {
super(message, cause);
}
public InjectionException(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,15 @@
package dst.ass2.ioc.di;
public class InvalidDeclarationException extends InjectionException {
public InvalidDeclarationException(String message) {
super(message);
}
public InvalidDeclarationException(String message, Throwable cause) {
super(message, cause);
}
public InvalidDeclarationException(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,15 @@
package dst.ass2.ioc.di;
public class ObjectCreationException extends InjectionException {
public ObjectCreationException(String message) {
super(message);
}
public ObjectCreationException(String message, Throwable cause) {
super(message, cause);
}
public ObjectCreationException(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,15 @@
package dst.ass2.ioc.di;
public class TypeConversionException extends InjectionException {
public TypeConversionException(String message) {
super(message);
}
public TypeConversionException(String message, Throwable cause) {
super(message, cause);
}
public TypeConversionException(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,21 @@
package dst.ass2.ioc.di.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a class to be container managed.
*/
// TODO: add correct retention policy and target type
public @interface Component {
/**
* The component can either be marked to be a singleton or a prototype via the {@link Scope} enum. The default scope
* is singleton.
*
* @return the scope
*/
Scope scope() default Scope.SINGLETON;
}
@@ -0,0 +1,15 @@
package dst.ass2.ioc.di.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Initialize marks a method to be invoked by the container after it has been constructed and all dependencies and
* properties have been injected.
*/
// TODO: add correct retention policy and target type
public @interface Initialize {
}
@@ -0,0 +1,28 @@
package dst.ass2.ioc.di.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Inject marks a field in a class to be autowired by the container.
*/
// TODO: add correct retention policy and target type
public @interface Inject {
/**
* Marks whether the dependency is required or not.
*
* @return a boolean value
*/
boolean optional() default false;
/**
* Explicitly states the class that should be injected by the container into this field. If the target type is Void,
* then we assume the targetType is not set, and the container uses the declared field type instead.
*
* @return a class
*/
Class<?> targetType() default Void.class;
}
@@ -0,0 +1,20 @@
package dst.ass2.ioc.di.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A property injects the value of a {@link java.util.Properties} object into a field based on a key.
*/
// TODO: add correct retention policy and target type
public @interface Property {
/**
* The key to look up in the container's Properties object.
*
* @return a key
*/
String value();
}
@@ -0,0 +1,9 @@
package dst.ass2.ioc.di.annotation;
/**
* The scope of a {@link Component}.
*/
public enum Scope {
SINGLETON,
PROTOTYPE
}
@@ -0,0 +1,16 @@
package dst.ass2.ioc.di.impl;
import dst.ass2.ioc.di.IObjectContainer;
import dst.ass2.ioc.di.IObjectContainerFactory;
import java.util.Properties;
public final class ObjectContainerFactory implements IObjectContainerFactory {
@Override
public IObjectContainer newObjectContainer(Properties properties) {
// TODO: implement
return null;
}
}
@@ -0,0 +1,21 @@
package dst.ass2.ioc.lock;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a method to be executed by acquiring a named lock.
*/
// TODO: add correct retention policy and target type
public @interface Lock {
/**
* The name of the container managed lock.
*
* @return a name
*/
String value();
}
@@ -0,0 +1,19 @@
package dst.ass2.ioc.lock;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class LockingInjector implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
// TODO transform all @Lock annotated methods of classes with a @Component annotation
return classfileBuffer;
}
}
@@ -0,0 +1,11 @@
package dst.ass2.ioc.lock;
import java.lang.instrument.Instrumentation;
public class LockingInjectorAgent {
public static void premain(String args, Instrumentation inst) {
// TODO
}
}
@@ -0,0 +1,19 @@
package dst.ass2.ioc.tests;
import dst.ass2.ioc.tests.di.DependencyInjectionTest;
import dst.ass2.ioc.tests.di.HierarchyTest;
import dst.ass2.ioc.tests.di.InitializeTest;
import dst.ass2.ioc.tests.di.PropertyInjectionTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
DependencyInjectionTest.class,
PropertyInjectionTest.class,
HierarchyTest.class,
InitializeTest.class
})
public class Ass2_2_1_Suite {
// suite
}
@@ -0,0 +1,13 @@
package dst.ass2.ioc.tests;
import dst.ass2.ioc.tests.lock.LockingTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
LockingTest.class
})
public class Ass2_2_2_Suite {
// suite
}
@@ -0,0 +1,10 @@
package dst.ass2.ioc.tests.di;
public class CustomInitializeTest {
/*
* TODO: In cases where the object hierarchy has multiple methods annotated with @Initialize or overwrites
* previously annotated methods, you should come up with your own behavior. Document the behavior with at least two
* unit tests.
*/
}
@@ -0,0 +1,218 @@
package dst.ass2.ioc.tests.di;
import dst.ass2.ioc.di.IObjectContainer;
import dst.ass2.ioc.di.IObjectContainerFactory;
import dst.ass2.ioc.di.InjectionException;
import dst.ass2.ioc.di.InvalidDeclarationException;
import dst.ass2.ioc.di.annotation.Component;
import dst.ass2.ioc.di.annotation.Inject;
import dst.ass2.ioc.di.annotation.Scope;
import dst.ass2.ioc.di.impl.ObjectContainerFactory;
import org.junit.Before;
import org.junit.Test;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import static org.hamcrest.CoreMatchers.instanceOf;
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.hamcrest.MatcherAssert.assertThat;
public class DependencyInjectionTest {
private IObjectContainerFactory factory;
private IObjectContainer container;
@Before
public void setUp() {
factory = new ObjectContainerFactory();
container = factory.newObjectContainer(new Properties());
if (container == null) {
throw new NullPointerException("ObjectContainerFactory did not return an ObjectContainer instance");
}
}
@Component
public static class SimpleSingleton {
public String someValue;
public SimpleSingleton() {
// just to verify that the constructor has been called correctly
this.someValue = "42";
}
}
public static class NotAComponentClass {
// a plain class shouldn't be autowired
@Inject
SimpleSingleton singleton;
}
@Test(expected = InvalidDeclarationException.class)
public void getObject_onNotAnnotatedClass_throwsException() {
container.getObject(NotAComponentClass.class);
}
@Component
public static abstract class AbstractComponentClass {
// abstract classes can't be instantiated
}
@Test(expected = InjectionException.class)
public void getObject_onAbstractClass_throwsException() throws Exception {
container.getObject(AbstractComponentClass.class);
}
@Test
public void getObject_onSimpleSingleton_createsObjectCorrectly() {
var object = container.getObject(SimpleSingleton.class);
assertNotNull(object);
assertThat(object, instanceOf(SimpleSingleton.class));
assertEquals("constructor was not called correctly", "42", object.someValue);
}
@Test
public void getObject_onSimpleSingleton_returnsSameObject() {
var object1 = container.getObject(SimpleSingleton.class);
var object2 = container.getObject(SimpleSingleton.class);
assertNotNull(object1);
assertNotNull(object2);
assertSame(object1, object2);
}
@Test
public void getObject_onDifferentContainer_returnsDifferentObject() {
var container1 = factory.newObjectContainer(new Properties());
var object1 = container1.getObject(SimpleSingleton.class);
var container2 = factory.newObjectContainer(new Properties());
var object2 = container2.getObject(SimpleSingleton.class);
assertNotNull(object1);
assertNotNull(object2);
assertNotSame(object1, object2);
}
@Component(scope = Scope.PROTOTYPE)
public static class SimplePrototype {
static final AtomicInteger COUNTER = new AtomicInteger(0);
public int cnt;
public SimplePrototype() {
// just to verify that the constructor has been called correctly
cnt = SimplePrototype.COUNTER.incrementAndGet();
}
}
@Test
public void getObject_onSimplePrototype_returnsDifferentObjects() {
SimplePrototype.COUNTER.set(0);
var object1 = container.getObject(SimplePrototype.class);
var object2 = container.getObject(SimplePrototype.class);
assertNotNull(object1);
assertNotNull(object2);
assertNotSame(object1, object2);
assertEquals(1, object1.cnt);
assertEquals(2, object2.cnt);
}
@Component(scope = Scope.PROTOTYPE)
public static class CompositePrototype {
@Inject
SimpleSingleton simpleSingleton;
}
@Test
public void getObject_onCompositePrototype_createsObjectCorrectly() {
var object = container.getObject(CompositePrototype.class);
assertNotNull(object.simpleSingleton);
}
@Test
public void getObject_onCompositePrototype_injectsPreviouslyCreatedSingleton() {
var singleton = container.getObject(SimpleSingleton.class);
var object = container.getObject(CompositePrototype.class);
assertNotNull(object.simpleSingleton);
assertSame("autowire should use already container managed singletons", singleton, object.simpleSingleton);
}
@Component
public static class CompositeSingleton {
@Inject
SimpleSingleton simpleSingleton;
}
@Test
public void getObject_onCompositeSingleton_createsObjectGraphCorrectly() {
var object = container.getObject(CompositeSingleton.class);
assertNotNull(object);
assertThat(object, instanceOf(CompositeSingleton.class));
assertNotNull("dependency of CompositeSingleton not initialized", object.simpleSingleton);
assertNotNull("dependency of CompositeSingleton not initialized correctly", object.simpleSingleton.someValue);
}
public interface IService {
int getSomeValue();
}
@Component
public static class ServiceImpl implements IService {
private int someValue;
public ServiceImpl() {
someValue = 42;
}
@Override
public int getSomeValue() {
return someValue;
}
}
@Component
public static class ServiceUser {
@Inject(targetType = ServiceImpl.class)
IService service;
}
@Test
public void getObject_usingTargetType_createsGraphCorrectly() throws Exception {
var object = container.getObject(ServiceUser.class);
assertNotNull(object);
assertNotNull("dependency of ServiceUser not initialized", object.service);
assertThat("dependency of ServiceUser not initialized correctly", object.service, instanceOf(ServiceImpl.class));
assertEquals("dependency of ServiceUser not initialized correctly", 42, object.service.getSomeValue());
}
@Component
public static class InvalidServiceUser {
// trying to inject an existing but invalid type (a SimplePrototype is not an IService)
@Inject(targetType = SimplePrototype.class)
IService service;
}
@Test(expected = InvalidDeclarationException.class)
public void getObject_onInvalidTargetType_throwsException() {
container.getObject(InvalidServiceUser.class);
}
}
@@ -0,0 +1,125 @@
package dst.ass2.ioc.tests.di;
import dst.ass2.ioc.di.IObjectContainer;
import dst.ass2.ioc.di.IObjectContainerFactory;
import dst.ass2.ioc.di.annotation.Component;
import dst.ass2.ioc.di.annotation.Inject;
import dst.ass2.ioc.di.impl.ObjectContainerFactory;
import org.junit.Before;
import org.junit.Test;
import java.util.Properties;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
public class HierarchyTest {
private IObjectContainerFactory factory;
private IObjectContainer container;
@Before
public void setUp() throws Exception {
factory = new ObjectContainerFactory();
container = factory.newObjectContainer(new Properties());
}
@Component
public static class SimpleSingleton {
public String someValue;
public SimpleSingleton() {
// just to verify that the constructor has been called correctly
this.someValue = "42";
}
}
public static abstract class AbstractComponent {
@Inject
protected SimpleSingleton superSingleton;
}
@Component
public static class ConcreteComponent extends AbstractComponent {
@Inject
private SimpleSingleton privateSingleton;
public SimpleSingleton getSuperSingleton() {
return superSingleton;
}
public SimpleSingleton getPrivateSingleton() {
return privateSingleton;
}
}
@Test
public void getObject_injectsDependencyOfSuperClassCorrectly() throws Exception {
ConcreteComponent component = container.getObject(ConcreteComponent.class);
assertNotNull("getObject returned null", component);
assertNotNull("Dependency of superclass was not injected", component.getSuperSingleton());
assertNotNull("Private dependency was not injected", component.getPrivateSingleton());
assertSame("Multiple singleton instances", component.getSuperSingleton(), component.getPrivateSingleton());
}
@Component
public static class ClassA {
@Inject
SimpleSingleton singletonA;
}
@Component
public static class ClassB extends ClassA {
@Inject
SimpleSingleton singletonB;
@Inject
ClassD classD;
}
@Component
public static class ClassC {
@Inject
SimpleSingleton singletonC;
}
@Component
public static class ClassD extends ClassC {
@Inject
SimpleSingleton singletonD;
}
/**
* Tests the following hierarchy. Getting first D then B should should inject the previously created D into B.
*
* +-------+ +-------+
* | A | | C |
* +-------+ +-------+
* ^ is a ^ is a
* | |
* +-------+ uses +-------+
* | B | ---> | D |
* +-------+ +-------+
*/
@Test
public void getObject_initializesHierarchyCorrectly() throws Exception {
ClassD d = container.getObject(ClassD.class);
assertNotNull("getObject returned null for ClassD", d);
assertNotNull("ClassD dependency was not injected", d.singletonD);
assertNotNull("ClassC dependency was not injected when instantiating ClassD", d.singletonC);
ClassB b = container.getObject(ClassB.class);
assertNotNull("getObject returned null for ClassB", b);
assertNotNull("ClassB dependency was not injected", b.singletonB);
assertNotNull("ClassA dependency was not injected when instantiating ClassB", b.singletonA);
assertNotNull("ClassD dependency was not injected into ClassB", b.classD);
assertSame("Container did not re-use already initialized ClassD instance", b.classD, d);
}
}
@@ -0,0 +1,114 @@
package dst.ass2.ioc.tests.di;
import dst.ass2.ioc.di.IObjectContainer;
import dst.ass2.ioc.di.IObjectContainerFactory;
import dst.ass2.ioc.di.annotation.Component;
import dst.ass2.ioc.di.annotation.Initialize;
import dst.ass2.ioc.di.annotation.Inject;
import dst.ass2.ioc.di.impl.ObjectContainerFactory;
import org.junit.Before;
import org.junit.Test;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class InitializeTest {
private IObjectContainerFactory factory;
private IObjectContainer container;
@Before
public void setUp() {
factory = new ObjectContainerFactory();
container = factory.newObjectContainer(new Properties());
if (container == null) {
throw new NullPointerException("ObjectContainerFactory did not return an ObjectContainer instance");
}
}
@Component
public static class ComponentClass {
private AtomicInteger initializeCalls = new AtomicInteger(0);
@Initialize
public void myInitMethod() {
initializeCalls.incrementAndGet();
}
public int getInitializeCalls() {
return initializeCalls.get();
}
}
@Test
public void getObject_runsInitializeMethodCorrectly() throws Exception {
ComponentClass component = container.getObject(ComponentClass.class);
assertEquals("expected exactly one call to myInitMethod", 1, component.getInitializeCalls());
}
@Test
public void getObject_runsInitializeMethodOnlyOnce() throws Exception {
ComponentClass component = container.getObject(ComponentClass.class);
assertEquals("expected exactly one call to myInitMethod", 1, component.getInitializeCalls());
component = container.getObject(ComponentClass.class);
assertEquals("expected exactly one call to myInitMethod", 1, component.getInitializeCalls());
}
@Component
public static class AnotherComponentClass {
@Inject
private ComponentClass dependency;
boolean wasNull;
@Initialize
public void myOtherInitMethod() {
this.wasNull = dependency == null;
}
public ComponentClass getDependency() {
return dependency;
}
}
@Test
public void getObject_runsInitializeMethodAfterInjection() throws Exception {
AnotherComponentClass component = container.getObject(AnotherComponentClass.class);
assertFalse("Expected dependency to be injected before calls to @Initialize", component.wasNull);
}
@Test
public void getObject_runsInitializeMethodOfDependencyCorrectly() throws Exception {
AnotherComponentClass component = container.getObject(AnotherComponentClass.class);
assertEquals("expected exactly one call to myInitMethod", 1, component.getDependency().getInitializeCalls());
}
@Component
public static class ComponentWithPrivateInitMethod {
private AtomicInteger initializeCalls = new AtomicInteger(0);
@Initialize
private void myPrivateInitMethod() {
initializeCalls.incrementAndGet();
}
public int getInitializeCalls() {
return initializeCalls.get();
}
}
@Test
public void getObject_runsPrivateInitializeMethodCorrectly() throws Exception {
ComponentWithPrivateInitMethod component = container.getObject(ComponentWithPrivateInitMethod.class);
assertEquals("expected exactly one call to myPrivateInitMethod", 1, component.getInitializeCalls());
}
}
@@ -0,0 +1,268 @@
package dst.ass2.ioc.tests.di;
import dst.ass2.ioc.di.IObjectContainerFactory;
import dst.ass2.ioc.di.ObjectCreationException;
import dst.ass2.ioc.di.TypeConversionException;
import dst.ass2.ioc.di.annotation.Component;
import dst.ass2.ioc.di.annotation.Property;
import dst.ass2.ioc.di.annotation.Scope;
import dst.ass2.ioc.di.impl.ObjectContainerFactory;
import org.junit.Before;
import org.junit.Test;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.hamcrest.MatcherAssert.assertThat;
public class PropertyInjectionTest {
private IObjectContainerFactory factory;
@Before
public void setUp() {
factory = new ObjectContainerFactory();
}
@Component
static class StringPropertyClass {
@Property("my_string")
String value;
}
@Test
public void simple_string_value_injection() {
var properties = new Properties();
properties.setProperty("my_string", "my string value");
var container = factory.newObjectContainer(properties);
StringPropertyClass obj = container.getObject(StringPropertyClass.class);
assertNotNull("getObject(StringPropertyClass.class) returned null", obj);
assertEquals("my string value", obj.value);
}
@Test(expected = ObjectCreationException.class)
public void wireMissingProperty_throwsException() {
var container = factory.newObjectContainer(new Properties());
container.getObject(StringPropertyClass.class);
}
@Component
static class IntegerPropertyClass {
@Property("my_integer")
Integer value;
}
@Test
public void simple_integer_value_injection() {
var properties = new Properties();
properties.setProperty("my_integer", "42");
var container = factory.newObjectContainer(properties);
IntegerPropertyClass obj = container.getObject(IntegerPropertyClass.class);
assertNotNull("getObject(IntegerPropertyClass.class) returned null", obj);
assertEquals(Integer.valueOf(42), obj.value);
}
@Test(expected = TypeConversionException.class)
public void wireInvalidPropertyType_throwsException() {
var properties = new Properties();
properties.setProperty("my_integer", "fourtytwo");
var container = factory.newObjectContainer(properties);
IntegerPropertyClass obj = container.getObject(IntegerPropertyClass.class);
assertNotNull("getObject(IntegerPropertyClass.class) returned null", obj);
assertEquals(Integer.valueOf(42), obj.value);
}
@Component
static class IntegerPrimitivePropertyClass {
@Property("my_integer")
int value;
}
@Test
public void simple_integer_primitive_value_injection() {
var properties = new Properties();
properties.setProperty("my_integer", "42");
var container = factory.newObjectContainer(properties);
IntegerPrimitivePropertyClass obj = container.getObject(IntegerPrimitivePropertyClass.class);
assertNotNull("getObject(IntegerPrimitivePropertyClass.class) returned null", obj);
assertEquals(42, obj.value);
}
@Component
static class MultiplePropertyClass {
@Property("my_string")
String stringValue;
@Property("my_integer")
int intValue;
@Property("my_integer")
Integer integerValue;
@Property("my_float")
Float floatValue;
}
@Test
public void simple_multiple_property_injection() {
var properties = new Properties();
properties.setProperty("my_string", "my string value");
properties.setProperty("my_integer", "42");
properties.setProperty("my_float", "3.14");
var container = factory.newObjectContainer(properties);
MultiplePropertyClass obj = container.getObject(MultiplePropertyClass.class);
assertNotNull("getObject(MultiplePropertyClass.class) returned null", obj);
assertEquals("my string value", obj.stringValue);
assertEquals(42, obj.intValue);
assertEquals(Integer.valueOf(42), obj.integerValue);
assertEquals(Float.valueOf(3.14f), obj.floatValue);
}
@Component
static class InheritedMultiplePropertyClass extends MultiplePropertyClass {
@Property("my_float")
Float theSameFloatValue;
}
@Test
public void multiple_property_injection_with_inheritance() {
var properties = new Properties();
properties.setProperty("my_string", "my string value");
properties.setProperty("my_integer", "42");
properties.setProperty("my_float", "3.14");
var container = factory.newObjectContainer(properties);
InheritedMultiplePropertyClass obj = container.getObject(InheritedMultiplePropertyClass.class);
assertNotNull("getObject(InheritedMultiplePropertyClass.class) returned null", obj);
assertEquals(Float.valueOf(3.14f), obj.theSameFloatValue);
// inherited values
assertEquals("Property not set correctly through inheritance", "my string value", obj.stringValue);
assertEquals("Property not set correctly through inheritance", 42, obj.intValue);
assertEquals("Property not set correctly through inheritance", Integer.valueOf(42), obj.integerValue);
assertEquals("Property not set correctly through inheritance", Float.valueOf(3.14f), obj.floatValue);
}
@Component(scope = Scope.PROTOTYPE)
static class PrototypePropertyClass {
@Property("my_string")
String stringValue;
}
@Test
public void getPrototypeObject_usesLatestPropertyValue() {
Properties properties = new Properties();
properties.setProperty("my_string", "initial value");
var container = factory.newObjectContainer(properties);
var object1 = container.getObject(PrototypePropertyClass.class);
assertEquals("initial value", object1.stringValue);
// Properties returned by IObjectContainerFactory should be mutable
container.getProperties().setProperty("my_string", "changed value");
var object2 = container.getObject(PrototypePropertyClass.class);
assertEquals("properties should be mutable", "changed value", object2.stringValue);
assertEquals("initial value", object1.stringValue);
}
@SuppressWarnings("unused")
@Component
static class ManyValuesClass {
@Property("my_string")
String stringValue01;
@Property("my_integer")
Integer value01;
@Property("my_integer")
Integer value02;
@Property("my_integer")
Integer value03;
@Property("my_integer")
Integer value04;
@Property("my_integer")
Integer value05;
@Property("my_string")
String stringValue02;
@Property("my_integer")
Integer value06;
@Property("my_integer")
Integer value07;
@Property("my_integer")
Integer value08;
@Property("my_integer")
Integer value09;
@Property("my_integer")
Integer value10;
@Property("my_string")
String stringValue03;
@Property("my_integer")
Integer value11;
@Property("my_integer")
Integer value12;
@Property("my_integer")
Integer value13;
@Property("my_integer")
Integer value14;
@Property("my_integer")
Integer value15;
}
@SuppressWarnings("unchecked")
@Test
public void mutatePropertiesConcurrently_verifyThreadSafety() throws Exception {
var container = factory.newObjectContainer(new Properties());
Properties properties = container.getProperties();
properties.setProperty("my_integer", "1");
properties.setProperty("my_string", "some string value");
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch latch = new CountDownLatch(10);
executor.execute(() -> {
for (int i = 0; i < 100000; i++) {
latch.countDown();
properties.setProperty("my_integer", Integer.toString(i));
}
});
Future<ManyValuesClass> getObject = executor.submit(() -> {
latch.await(); // wait until the property set loop has started and warmed up
return container.getObject(ManyValuesClass.class);
});
ManyValuesClass object = getObject.get();
executor.shutdown();
executor.awaitTermination(2, TimeUnit.SECONDS);
executor.shutdownNow();
assertNotEquals("updated property value was not used", Integer.valueOf(1), object.value01);
assertThat("property values are inconsistent indicating non-thread-safe property injection",
object.value01, allOf(
is(object.value01), is(object.value02), is(object.value03), is(object.value04),
is(object.value05), is(object.value06), is(object.value07), is(object.value08),
is(object.value09), is(object.value10), is(object.value11), is(object.value12),
is(object.value13), is(object.value14), is(object.value15)
));
}
}
@@ -0,0 +1,434 @@
package dst.ass2.ioc.tests.lock;
import dst.ass2.ioc.di.annotation.Component;
import dst.ass2.ioc.lock.Lock;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ConcurrentModificationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
public class LockingTest {
private static final Logger LOG = LoggerFactory.getLogger(LockingTest.class);
@Rule
public ThreadPoolResource executor = new ThreadPoolResource(2);
/**
* This class simulates the use of a shared resource (the semaphore). If the resource is already acquired
* by some other thread, then a {@link ConcurrentModificationException} is thrown. We use this behavior to test
* various scenarios (whether mutex access is guaranteed when the @Lock value refers to the same lock).
*/
@Component
public static class SimpleLockManaged {
public Semaphore semaphore;
public SimpleLockManaged(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Lock("my_lock")
public void useResource() {
useResourceImpl();
}
@Lock("my_other_lock")
public void useResourceSomeMore() {
useResourceImpl();
}
private void useResourceImpl() {
LOG.info("{} trying to acquire semaphore", this);
boolean acquired = semaphore.tryAcquire();
if (!acquired) {
LOG.info("{} failed to acquire semaphore", this);
throw new ConcurrentModificationException("Semaphore was acquired concurrently");
}
// hold the permit for a few ms
try {
LOG.info("{} holding permit for 500ms", this);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOG.info("{} releasing semaphore", this);
semaphore.release();
}
}
}
@Test
public void mutexTest_onSameClassAndObject_behavesCorrectly() throws Exception {
Semaphore semaphore = new Semaphore(1);
SimpleLockManaged lockManaged = new SimpleLockManaged(semaphore);
Future<?> r1 = executor.submit(lockManaged::useResource);
Future<?> r2 = executor.submit(lockManaged::useResource);
try {
r1.get();
r2.get();
} catch (ExecutionException e) {
if (e.getCause() instanceof ConcurrentModificationException) {
throw new AssertionError("Access to shared resource was not mutually exclusive as required", e);
} else {
throw e;
}
}
}
@Test
public void mutexTest_onSameClassButDifferentObject_behavesCorrectly() throws Exception {
Semaphore semaphore = new Semaphore(1);
SimpleLockManaged lockManaged1 = new SimpleLockManaged(semaphore);
SimpleLockManaged lockManaged2 = new SimpleLockManaged(semaphore);
Future<?> r1 = executor.submit(lockManaged1::useResource);
Future<?> r2 = executor.submit(lockManaged2::useResource);
try {
r1.get();
r2.get();
} catch (ExecutionException e) {
if (e.getCause() instanceof ConcurrentModificationException) {
throw new AssertionError("Access to shared resource was not mutually exclusive as required", e);
} else {
throw e;
}
}
}
@Test
public void nonMutexTest_onSameClassAndObject_behavesCorrectly() throws Exception {
Semaphore semaphore = new Semaphore(1);
SimpleLockManaged lockManaged = new SimpleLockManaged(semaphore);
Future<?> r1 = executor.submit(lockManaged::useResource);
Future<?> r2 = executor.submit(lockManaged::useResourceSomeMore);
try {
r1.get();
r2.get();
throw new AssertionError("Different locks should allow concurrent access to resources");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof ConcurrentModificationException)) {
throw e;
}
// we expected a concurrent modification exception here
}
}
@Test
public void nonMutexTest_onSameClassButDifferentObject_behavesCorrectly() throws Exception {
Semaphore semaphore = new Semaphore(1);
SimpleLockManaged lockManaged1 = new SimpleLockManaged(semaphore);
SimpleLockManaged lockManaged2 = new SimpleLockManaged(semaphore);
Future<?> r1 = executor.submit(lockManaged1::useResource);
Future<?> r2 = executor.submit(lockManaged2::useResourceSomeMore);
try {
r1.get();
r2.get();
throw new AssertionError("Different locks should allow concurrent access to resources");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof ConcurrentModificationException)) {
throw e;
}
// we expected a concurrent modification exception here
}
}
@Component
public static class AnotherSimpleLockManaged {
private Semaphore semaphore;
public AnotherSimpleLockManaged(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Lock("my_lock")
protected void useResourceAgain() {
LOG.info("{} trying to acquire semaphore", this);
boolean acquired = semaphore.tryAcquire();
if (!acquired) {
LOG.info("{} failed to acquire semaphore", this);
throw new ConcurrentModificationException("Semaphore was acquired concurrently");
}
// hold the permit for a few ms
try {
LOG.info("{} holding permit for 500ms", this);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOG.info("{} releasing semaphore", this);
semaphore.release();
}
}
}
@Test
public void mutexTest_onDifferentClass_behavesCorrectly() throws Exception {
Semaphore semaphore = new Semaphore(1);
SimpleLockManaged lockManaged1 = new SimpleLockManaged(semaphore);
AnotherSimpleLockManaged lockManaged2 = new AnotherSimpleLockManaged(semaphore);
Future<?> r1 = executor.submit(lockManaged1::useResource);
Future<?> r2 = executor.submit(lockManaged2::useResourceAgain);
try {
r1.get();
r2.get();
} catch (ExecutionException e) {
if (e.getCause() instanceof ConcurrentModificationException) {
throw new AssertionError("Access to shared resource was not mutually exclusive as required", e);
} else {
throw e;
}
}
}
/**
* This class is annotated with @Lock, but does not have a @Component annotation, so it should not be instrumented.
*/
public static class NotLockManaged {
public Semaphore semaphore;
public NotLockManaged(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Lock("my_lock_0")
public void useResource() {
LOG.info("{} trying to acquire semaphore", this);
boolean acquired = semaphore.tryAcquire();
if (!acquired) {
LOG.info("{} failed to acquire semaphore", this);
throw new ConcurrentModificationException("Semaphore was acquired concurrently");
}
// hold the permit for a few ms
try {
LOG.info("{} holding permit for 500ms", this);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOG.info("{} releasing semaphore", this);
semaphore.release();
}
}
}
@Test
public void testOnNonComponent_shouldNotBeInstrumented() throws Exception {
Semaphore semaphore = new Semaphore(1);
NotLockManaged notManaged = new NotLockManaged(semaphore);
Future<?> r1 = executor.submit(notManaged::useResource);
Future<?> r2 = executor.submit(notManaged::useResource);
try {
r1.get();
r2.get();
throw new AssertionError("Only classes with the @Component annotation should be instrumented");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof ConcurrentModificationException)) {
throw e;
}
// we expected a concurrent modification exception here
}
}
@Component
public static class LockManagedWithParameter {
@Lock("my_lock_1")
protected void useResource(Semaphore semaphore) {
LOG.info("{} trying to acquire semaphore", this);
boolean acquired = semaphore.tryAcquire();
if (!acquired) {
LOG.info("{} failed to acquire semaphore", this);
throw new ConcurrentModificationException("Semaphore was acquired concurrently");
}
// hold the permit for a few ms
try {
LOG.info("{} holding permit for 500ms", this);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOG.info("{} releasing semaphore", this);
semaphore.release();
}
}
}
@Test
public void mutexTest_withParameter_behavesCorrectly() throws Exception {
Semaphore semaphore = new Semaphore(1);
LockManagedWithParameter lockManaged = new LockManagedWithParameter();
Future<?> r1 = executor.submit(() -> lockManaged.useResource(semaphore));
Future<?> r2 = executor.submit(() -> lockManaged.useResource(semaphore));
try {
r1.get();
r2.get();
} catch (ExecutionException e) {
if (e.getCause() instanceof ConcurrentModificationException) {
throw new AssertionError("Access to shared resource was not mutually exclusive as required", e);
} else {
throw e;
}
}
}
@Component
public static class LockManagedWithReturnValue {
public Semaphore semaphore;
public LockManagedWithReturnValue(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Lock("my_lock")
public Integer useResource() {
useResourceImpl();
return 42;
}
private void useResourceImpl() {
LOG.info("{} trying to acquire semaphore", this);
boolean acquired = semaphore.tryAcquire();
if (!acquired) {
LOG.info("{} failed to acquire semaphore", this);
throw new ConcurrentModificationException("Semaphore was acquired concurrently");
}
// hold the permit for a few ms
try {
LOG.info("{} holding permit for 500ms", this);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOG.info("{} releasing semaphore", this);
semaphore.release();
}
}
}
@Test
public void mutexTest_withReturnValue_behavesCorrectly() throws Exception {
Semaphore semaphore = new Semaphore(1);
LockManagedWithReturnValue lockManaged = new LockManagedWithReturnValue(semaphore);
Future<Integer> r1 = executor.submit(lockManaged::useResource);
Future<Integer> r2 = executor.submit(lockManaged::useResource);
try {
Integer i1 = r1.get();
Integer i2 = r2.get();
Assert.assertEquals(Integer.valueOf(42), i1);
Assert.assertEquals(Integer.valueOf(42), i2);
} catch (ExecutionException e) {
if (e.getCause() instanceof ConcurrentModificationException) {
throw new AssertionError("Access to shared resource was not mutually exclusive as required", e);
} else {
throw e;
}
}
}
@Component
public static class LockManagedWithParamAndReturnValue {
private Integer i = 0;
@Lock("my_lock_2")
public Integer checkAndIncrement(Integer test) throws RuntimeException {
System.out.println(Thread.currentThread().getName() + " getting value " + i);
int x = i;
if (i < test) {
// this condition will always be true in the tests, just to add some logic
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
x = x + 1;
i = x;
}
return i;
}
}
/**
* This test verifies that the instrumentation also works when the function has input parameters and return values.
*/
@Test
public void mutexTest_withParamAndReturnValue_behavesCorrectly() throws Exception {
LockManagedWithParamAndReturnValue managed = new LockManagedWithParamAndReturnValue();
Future<Integer> r1 = executor.submit(() -> managed.checkAndIncrement(10));
Future<Integer> r2 = executor.submit(() -> managed.checkAndIncrement(10));
try {
Integer i1 = r1.get();
Integer i2 = r2.get();
Assert.assertNotEquals("Result of concurrent increment is equal, indicating a race condition", i1, i2);
if (i1 < i2) {
Assert.assertEquals(Integer.valueOf(1), i1);
Assert.assertEquals(Integer.valueOf(2), i2);
} else {
Assert.assertEquals(Integer.valueOf(2), i1);
Assert.assertEquals(Integer.valueOf(1), i2);
}
} catch (ExecutionException e) {
if (e.getCause() instanceof ConcurrentModificationException) {
throw new AssertionError("Access to shared resource was not mutually exclusive as required", e);
} else {
throw e;
}
}
}
}
@@ -0,0 +1,65 @@
package dst.ass2.ioc.tests.lock;
import org.junit.rules.ExternalResource;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Simple JUnit Rule wrapper of a fixed thread pool {@link ExecutorService} that shuts down after each test execution.
*/
public class ThreadPoolResource extends ExternalResource {
private ExecutorService executor;
private int nThreads;
public ThreadPoolResource(int nThreads) {
this.nThreads = nThreads;
}
@Override
protected void before() {
executor = Executors.newFixedThreadPool(nThreads);
}
@Override
protected void after() {
shutdownAndAwaitTermination(executor);
}
public Future<?> submit(Runnable task) {
return executor.submit(task);
}
public <T> Future<T> submit(Callable<T> task) {
return executor.submit(task);
}
public ExecutorService getExecutor() {
return executor;
}
private void shutdownAndAwaitTermination(ExecutorService pool) {
System.out.println("shutting down pool");
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
System.out.println("awaiting pool termination");
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("Pool did not terminate");
}
}
} catch (InterruptedException ie) {
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
}