Skip to content

Commit

Permalink
Implemented context holder. Bumped version to 0.1.1.
Browse files Browse the repository at this point in the history
  • Loading branch information
erikhofer committed Aug 30, 2017
1 parent 7b8cbd7 commit 0357ff8
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 21 deletions.
8 changes: 5 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ apply plugin: 'com.novoda.bintray-release'

sourceCompatibility = 1.8
targetCompatibility = 1.8
version = '0.1.0'
version = '0.1.1'

jar {
manifest {
Expand All @@ -30,15 +30,17 @@ dependencies {
compile 'com.google.guava:guava:19.0'
compile 'org.codehaus.groovy:groovy:2.4.7'
compile 'org.springframework:spring-context:4.3.5.RELEASE'
testCompile group: 'junit', name: 'junit', version: '4.+'
testCompile 'junit:junit:4.+'
testCompile 'org.mockito:mockito-core:2.+'
testCompile 'org.assertj:assertj-core:3.8.0'
}

publish {
userOrg = 'xinra'
repoName = 'nucleus'
groupId = 'com.xinra.nucleus'
artifactId = 'nucleus-common'
publishVersion = '0.1.0'
publishVersion = '0.1.1'
licences = ['BSD 3-Clause']
desc = 'Nucleus cross-layer utilities'
website = 'https://github.com/xinra-nucleus/nucleus-common'
Expand Down
44 changes: 44 additions & 0 deletions src/main/groovy/com/xinra/nucleus/common/ContextHolder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.xinra.nucleus.common;

/**
* Holds a context object of arbitrary type. A context holds data that is used across different
* parts of an application but should not be passed around as method parameters. Typically,
* a context is a bean with a suiting scope (e.g. 'request'). Instead of autowiring the
* context directly, this wrapper is used to provide a (mock) context if context-dependent
* code is executed outside of the bean's scope, e.g. in tests.
*
* @author Erik Hofer
* @param <T> the context type
*/
public interface ContextHolder<T> {

/**
* Returns the current context. Either the context is retrieved from the application context
* (typically as a request-scoped bean) or a mock context is returned if it has been set
* previously by {@link #mock()}.
*
* @throws IllegalStateException if there is neither a context provided by
* the application context nor a mock context.
*/
T get();

/**
* Creates a mock context. This is used if the application context does not hold a context
* (typically if the context is request-scoped but the current thread is not a web request).
*
* <p>IMPORTANT: When the mock context is not needed anymore, it has to be discarded using
* {@link #clearMock()}. The mock context is stored in a {@link ThreadLocal} and unlike
* request-scoped beans it is not cleaned up automatically. To prevent leaking the
* mock context wrap the context-dependent code in a try-finally block.
*
* @return the created context
* @throws IllegalStateException if a context is already provided by the application context.
*/
T mock();

/**
* Clears the mock context that has been created with {@link #mock()}.
*/
void clearMock();

}
72 changes: 72 additions & 0 deletions src/main/groovy/com/xinra/nucleus/common/GenericContextHolder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.xinra.nucleus.common;

import java.util.Objects;
import java.util.function.Supplier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
* Default implementation of {@link ContextHolder}. Holds context of arbitrary type.
*
* @author Erik Hofer
* @param <T> the context type
*/
public class GenericContextHolder<T> implements ApplicationContextAware, ContextHolder<T> {

private final ThreadLocal<T> mock = new ThreadLocal<>();
private final Class<T> contextType;
private final Supplier<T> contextSupplier;
private final Supplier<Boolean> useApplicationContext;
private ApplicationContext applicationContext;

/**
* Creates a {@link GenericContextHolder}. If this is not used to create a managed bean,
* make sure to call {@link #setApplicationContext(ApplicationContext)}.
* @param contextType the type used to retrieve the current context from the application context
* @param contextSupplier used to create a new mock context
* @param useApplicationContext used to determine if the application context should be used to
* retrieve the context (true) or if a mock context should be used (false).
*/
public GenericContextHolder(Class<T> contextType, Supplier<T> contextSupplier,
Supplier<Boolean> useApplicationContext) {
this.contextType = Objects.requireNonNull(contextType);
this.contextSupplier = Objects.requireNonNull(contextSupplier);
this.useApplicationContext = Objects.requireNonNull(useApplicationContext);
}

@Override
public T get() {
if (useApplicationContext.get()) {
return applicationContext.getBean(contextType);
} else {
T context = mock.get();
if (context == null) {
mock.remove();
throw new IllegalStateException("Cannot get current context. This is not a web"
+ " request and there is no mock context!");
}
return context;
}
}

@Override
public T mock() {
if (useApplicationContext.get()) {
throw new IllegalStateException("This is a request. Use the request-scoped context"
+ " instead of mocking one!");
}
final T context = contextSupplier.get();
mock.set(context);
return context;
}

@Override
public void clearMock() {
mock.remove();
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}
18 changes: 0 additions & 18 deletions src/test/groovy/com/xinra/nucleus/common/test/AllTests.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.xinra.nucleus.common.test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.xinra.nucleus.common.GenericContextHolder;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;

public class TestGenericContextHolder {

private static enum TestContext {
BEAN,
MOCK
}

private static GenericContextHolder<TestContext> contextHolder;
private static boolean useApplicationContext;

/**
* Creates the instance of {@link GenericContextHolder} and a mock application
* context. The instance is shared across all tests because it is stateless
* aside from {@link #useApplicationContext}.
*/
@BeforeClass
public static void setUp() {
contextHolder = new GenericContextHolder<>(TestContext.class, () -> TestContext.MOCK,
() -> useApplicationContext);

ApplicationContext applicationContext = mock(ApplicationContext.class);
when(applicationContext.getBean(TestContext.class)).thenReturn(TestContext.BEAN);
contextHolder.setApplicationContext(applicationContext);
}

@Test
public void getContextBean() {
useApplicationContext = true;
assertThat(contextHolder.get()).isEqualTo(TestContext.BEAN);
}

@Test
public void createAndGetMockContext() {
useApplicationContext = false;
assertThat(contextHolder.mock()).isEqualTo(TestContext.MOCK);
assertThat(contextHolder.get()).isEqualTo(TestContext.MOCK);
}

@Test
public void createMockContextWhenNotApplicable() {
useApplicationContext = true;
assertThatIllegalStateException().isThrownBy(contextHolder::mock);
}

@Test
public void getMockContextWihtoutCreatingOne() {
useApplicationContext = false;
assertThatIllegalStateException().isThrownBy(contextHolder::get);
}

@Test
public void clearMockContext() {
useApplicationContext = false;
contextHolder.mock();
contextHolder.clearMock();
assertThatIllegalStateException().isThrownBy(contextHolder::get);
}
}

0 comments on commit 0357ff8

Please sign in to comment.