Skip to content

Commit

Permalink
Use inject for non singleton class (#1617)
Browse files Browse the repository at this point in the history
* comment to start branch

* getFieldsAnnotatedWithInstance helper method

* todo comment deleted

* injectIntoField overloaded method

* injectIntoNonSingleton

* access modifier protected to public

* revert access modifier change

* changes:
added logger to HapiFhirResource in order to test a non-singleton class
created HapiFhirResourceTest in order to test the changes to HapiFhirResource

* Added implementors unit test to ApplicationContextTest

* Minor refactoring on some AppContext injection methods

* Make the skip flag on TestApplicationContext switchable from the test classes

* Update ApplicationContext.java

Remove unreachable if statement

* Update TestApplicationContext.groovy

Add additional line in coverage

* Update TestApplicationContext.groovy

Remove parameter from injectRegisteredImplementations

* added unit tests to cover new code

* Update ApplicationContextTest.groovy

Added test case for unsupported injection classes

* Update ApplicationContext.java

Print an error message if the class implementation is not found

* use return instead of System.err

* refactoring - dry

* thown(NullPointerException) -> noExceptionThrown()

* added comments to indicate changes that will be deleted

* deleted commented out code

* reinstated comments for test case

* deleted test changes:
in HapiFhirResource
and deleted file HapifhirResourceTest.groovy

---------

Co-authored-by: Luis Pabon <[email protected]>
Co-authored-by: Luis Pabon <[email protected]>
  • Loading branch information
3 people authored Dec 13, 2024
1 parent 2aa50dc commit e718efd
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class ApplicationContext {
protected static final Map<String, String> TEST_ENV_VARS = new ConcurrentHashMap<>();
protected static final Set<Object> IMPLEMENTATIONS = new HashSet<>();

protected static boolean skipMissingImplementations = false;

protected ApplicationContext() {}

public static void register(Class<?> clazz, Object implementation) {
Expand All @@ -53,17 +55,39 @@ public static <T> Set<Class<? extends T>> getImplementors(Class<T> interfaze) {
}

public static void injectRegisteredImplementations() {
injectRegisteredImplementations(false);
doInjectRegisteredImplementations();
}

protected static void injectRegisteredImplementations(boolean skipMissingImplementations) {
protected static void doInjectRegisteredImplementations() {
var fields = Reflection.getFieldsAnnotatedWith(Inject.class);

fields.forEach(field -> injectIntoField(field, skipMissingImplementations));
fields.forEach(ApplicationContext::injectIntoField);
}

public static void injectIntoNonSingleton(Object instance) {
var fields = Reflection.getFieldsAnnotatedWithInstance(instance.getClass(), Inject.class);

fields.forEach(field -> injectIntoField(field, instance));
}

private static void injectIntoField(Field field, boolean skipMissingImplementations) {
private static void injectIntoField(Field field, Object instance) {
var fieldType = field.getType();

Object fieldImplementation = getFieldImplementation(fieldType);
if (fieldImplementation == null) {
return;
}

field.trySetAccessible();
try {
field.set(instance, fieldImplementation);
} catch (IllegalAccessException | IllegalArgumentException exception) {
throw new IllegalArgumentException(
"unable to inject " + fieldType + " into " + instance.getClass(), exception);
}
}

private static void injectIntoField(Field field) {
var declaringClass = field.getDeclaringClass();

if (!IMPLEMENTATIONS.contains(declaringClass)) {
Expand All @@ -76,29 +100,16 @@ private static void injectIntoField(Field field, boolean skipMissingImplementati
declaringClassesToTry.add(declaringClass);
declaringClassesToTry.addAll(Arrays.asList(declaringClass.getInterfaces()));

Object fieldImplementation = getFieldImplementation(fieldType, skipMissingImplementations);
if (fieldImplementation == null) {
return;
}

Object declaringClassImplementation =
getDeclaringClassImplementation(declaringClassesToTry, skipMissingImplementations);
getDeclaringClassImplementation(declaringClassesToTry);
if (declaringClassImplementation == null) {
return;
}

field.trySetAccessible();

try {
field.set(declaringClassImplementation, fieldImplementation);
} catch (IllegalAccessException | IllegalArgumentException exception) {
throw new IllegalArgumentException(
"Unable to inject " + fieldType + " into " + declaringClass, exception);
}
injectIntoField(field, declaringClassImplementation);
}

private static Object getFieldImplementation(
Class<?> fieldType, boolean skipMissingImplementations) {
private static Object getFieldImplementation(Class<?> fieldType) {
Object fieldImplementation;

try {
Expand All @@ -116,8 +127,7 @@ private static Object getFieldImplementation(
return fieldImplementation;
}

private static Object getDeclaringClassImplementation(
List<Class<?>> declaringClassesToTry, boolean skipMissingImplementations) {
private static Object getDeclaringClassImplementation(List<Class<?>> declaringClassesToTry) {
Object declaringClassImplementation =
declaringClassesToTry.stream()
.map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import static org.reflections.scanners.Scanners.FieldsAnnotated;
import static org.reflections.scanners.Scanners.SubTypes;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import org.reflections.Reflections;

/**
Expand All @@ -27,4 +30,10 @@ public static <T> Set<Class<? extends T>> getImplementors(Class<T> interfaze) {
public static Set<Field> getFieldsAnnotatedWith(Class<?> annotation) {
return REFLECTIONS.get(FieldsAnnotated.with(annotation).as(Field.class));
}

public static Set<Field> getFieldsAnnotatedWithInstance(Class<?> clazz, Class<?> annotation) {
return Arrays.stream(clazz.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(annotation.asSubclass(Annotation.class)))
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package gov.hhs.cdc.trustedintermediary.context

import gov.hhs.cdc.trustedintermediary.wrappers.Logger
import spock.lang.Specification

import javax.inject.Inject
Expand All @@ -8,6 +9,34 @@ import java.nio.file.Paths

class ApplicationContextTest extends Specification {

interface TestingInterface {
void test()
}

class NonSingletonClazz {
@Inject
Logger logger
void test() {}
}

static class DogCow implements TestingInterface {

@Override
void test() {
print("test()")
}
}

static class DogCowTwo implements TestingInterface {

@Override
void test() {
print("testTwo()")
}
}
def DOGCOW = new DogCow()
def DOGCOWTWO = new DogCowTwo()

def setup() {
TestApplicationContext.reset()
}
Expand All @@ -21,6 +50,48 @@ class ApplicationContextTest extends Specification {
result == ApplicationContext.getImplementation(String.class)
}

def "implementors retrieval test"() {
setup:
def dogCow = DOGCOW
def dogCowTwo = DOGCOWTWO
def implementors = new HashSet()
implementors.add(DogCow)
implementors.add(DogCowTwo)

expect:
implementors == ApplicationContext.getImplementors(TestingInterface)
}

def "injectIntoNonSingleton unhappy path"() {
given:
def nonSingletonClass = new NonSingletonClazz()
def object = new Object()
ApplicationContext.register(Logger, object)
when:
ApplicationContext.injectIntoNonSingleton(nonSingletonClass)
then:
thrown(IllegalArgumentException)
}

def "injectIntoNonSingleton unhappy path when fieldImplementation runs into an error"() {
given:
def nonSingletonClass = new NonSingletonClazz()
when:
ApplicationContext.injectIntoNonSingleton(nonSingletonClass)
then:
thrown(IllegalArgumentException)
}

def "injectIntoNonSingleton unhappy path when fieldImplementation is null"() {
given:
def nonSingletonClass = new NonSingletonClazz()
when:
ApplicationContext.skipMissingImplementations = true
ApplicationContext.injectIntoNonSingleton(nonSingletonClass)
then:
noExceptionThrown()
}

def "implementation injection test"() {
given:
def injectedValue = "DogCow"
Expand Down Expand Up @@ -133,6 +204,25 @@ class ApplicationContextTest extends Specification {
Files.deleteIfExists(directoryPath)
}

def "registering an unsupported injection class"() {
given:
def injectedValue = "DogCow"
def injectionInstantiation = new InjectionDeclaringClass()

TestApplicationContext.register(List.class, injectionInstantiation)
// notice above that I'm registering the injectionInstantiation object as a List class.
// injectionInstantiation is of class InjectionDeclaringClass,
// and InjectionDeclaringClass doesn't implement List (it only implements AFieldInterface).
TestApplicationContext.register(String.class, injectedValue)

when:
TestApplicationContext.injectRegisteredImplementations()
injectionInstantiation.getAField()

then:
noExceptionThrown()
}

class InjectionDeclaringClass {
@Inject
private String aField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ class TestApplicationContext extends ApplicationContext {
}

def static injectRegisteredImplementations() {
injectRegisteredImplementations(true)
skipMissingImplementations = true
ApplicationContext.injectRegisteredImplementations()
}

def static addEnvironmentVariable(String key, String value) {
Expand Down

0 comments on commit e718efd

Please sign in to comment.