Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(extract-subsystems): extract Context, @In, InjectionHelper and @Share to Subsystem #4930

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions engine/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ dependencies {
implementation group: 'org.terasology.crashreporter', name: 'cr-terasology', version: '4.2.0'

api(project(":subsystems:TypeHandlerLibrary"))
api(project(":subsystems:Context"))
}

protobuf {
Expand Down
21 changes: 21 additions & 0 deletions subsystems/Context/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Context

>It is really not subsystem. but used by them

Context is simple DI implementation for TS

Provide interface for working with context.

## How to use (from scratch, not Terasology)

1. Implement `Context` class
2. Create it
3. Provide it.
4. Use `InjectionHelper` with context to inject at `@In` fields

## How to use (with Terasology)

1. Create your system/manager/another injectable class type
2. Write field with `@In` - be sure that Field Type is injectable. then Field will be injected
with manager/system/ another injectable class type
3. Mark with `@Share` - be sure that your class is injectable type. then your class be provided to `Context`.
19 changes: 19 additions & 0 deletions subsystems/Context/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

plugins {
java
`java-library`
}

apply(from = "$rootDir/config/gradle/common.gradle")

dependencies {
implementation("org.terasology.gestalt:gestalt-util:7.2.0-SNAPSHOT")
implementation("org.terasology.gestalt:gestalt-module:7.2.0-SNAPSHOT")

implementation("ch.qos.logback:logback-classic:1.2.3")

testImplementation("org.junit.jupiter:junit-jupiter-api:5.5.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.5.2")
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
package org.terasology.engine.registry;

import org.reflections.ReflectionUtils;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.engine.context.Context;
Expand All @@ -16,6 +16,7 @@
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;

public final class InjectionHelper {
private static final Logger logger = LoggerFactory.getLogger(InjectionHelper.class);
Expand All @@ -25,7 +26,7 @@ private InjectionHelper() {

public static void inject(final Object object, Context context) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
for (Field field : ReflectionUtils.getAllFields(object.getClass(), ReflectionUtils.withAnnotation(In.class))) {
for (Field field : getAllFields(object.getClass(), In.class)) {
Object value = context.get(field.getType());
if (value != null) {
try {
Expand All @@ -43,7 +44,7 @@ public static void inject(final Object object, Context context) {

public static void inject(final Object object) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
for (Field field : ReflectionUtils.getAllFields(object.getClass(), ReflectionUtils.withAnnotation(In.class))) {
for (Field field : getAllFields(object.getClass(), In.class)) {
Object value = CoreRegistry.get(field.getType());
if (value != null) {
try {
Expand All @@ -64,7 +65,7 @@ public static void inject(final Object object) {

public static <T> void inject(final Object object, final Class<? extends Annotation> annotation, final Map<Class<? extends T>, T> source) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
for (Field field : ReflectionUtils.getAllFields(object.getClass(), ReflectionUtils.withAnnotation(annotation))) {
for (Field field : getAllFields(object.getClass(), annotation)) {
Object value = source.get(field.getType());
if (value != null) {
try {
Expand All @@ -82,6 +83,20 @@ public static <T> void inject(final Object object, final Class<? extends Annotat
});
}

private static Set<Field> getAllFields(Class<?> clazz, Class<? extends Annotation> annotation) {
Set<Field> result = Sets.newLinkedHashSet();
Class<?> candidate = clazz;
while (candidate != null) {
for (Field field : candidate.getDeclaredFields()) {
if (field.getAnnotation(annotation) != null) {
result.add(field);
}
}
candidate = candidate.getSuperclass();
}
return result;
}

public static void share(Object object) {
Share share = object.getClass().getAnnotation(Share.class);
if (share != null && share.value() != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

package org.terasology.engine.registry;

import com.google.common.collect.Maps;
import org.terasology.engine.context.Context;

import java.util.Map;

public class ContextImplementation implements Context {
private final Map<Class<?>, Object> map = Maps.newConcurrentMap();

@Override
public <T> T get(Class<? extends T> type) {
T result = type.cast(map.get(type));
if (result != null) {
return result;
}
return null;
}

@Override
public <T, U extends T> void put(Class<T> type, U object) {
map.put(type, object);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,11 @@

package org.terasology.engine.registry;

import com.google.common.collect.Maps;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.terasology.engine.context.Context;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

public class CoreRegistryTest {
private Context context;

Expand All @@ -31,7 +26,7 @@ public void setup() {
@Test
public void testContextChange() {
CoreRegistry.setContext(new ContextImplementation());
assertNotEquals(CoreRegistry.get(Context.class), context);
Assertions.assertNotEquals(CoreRegistry.get(Context.class), context);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentional? (Adding the Assertions. qualifier back in, instead of a static import of the assertNotEquals method.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just personal prefer to avoid static imports

}

/**
Expand All @@ -41,8 +36,8 @@ public void testContextChange() {
public void testNullReturnOnMissingContext() {
CoreRegistry.setContext(null);

assertEquals(CoreRegistry.put(Integer.class, 10), null);
assertEquals(CoreRegistry.get(Integer.class), null);
Assertions.assertEquals(CoreRegistry.put(Integer.class, 10), null);
Assertions.assertEquals(CoreRegistry.get(Integer.class), null);
}

/**
Expand All @@ -51,9 +46,9 @@ public void testNullReturnOnMissingContext() {
*/
@Test
public void testContextGetIndependenceFromContextInterfaceImplementation() {
assertEquals(CoreRegistry.get(Context.class), context);
Assertions.assertEquals(CoreRegistry.get(Context.class), context);

assertEquals(context.get(Context.class), null);
Assertions.assertEquals(context.get(Context.class), null);
}

/**
Expand All @@ -64,35 +59,18 @@ public void testContextMethodsCalled() {
// Load value in context
Integer value = 10;
CoreRegistry.put(Integer.class, value);
assertEquals(value, context.get(Integer.class));
assertEquals(context.get(Integer.class), CoreRegistry.get(Integer.class));
Assertions.assertEquals(value, context.get(Integer.class));
Assertions.assertEquals(context.get(Integer.class), CoreRegistry.get(Integer.class));

// Change context
CoreRegistry.setContext(new ContextImplementation());
assertNotEquals(CoreRegistry.get(Context.class), context);
assertEquals(CoreRegistry.get(Integer.class), null);
Assertions.assertNotEquals(CoreRegistry.get(Context.class), context);
Assertions.assertEquals(CoreRegistry.get(Integer.class), null);

// Restore first context
CoreRegistry.setContext(context);
assertEquals(CoreRegistry.get(Integer.class), value);
Assertions.assertEquals(CoreRegistry.get(Integer.class), value);
}


private static class ContextImplementation implements Context {
private final Map<Class<?>, Object> map = Maps.newConcurrentMap();

@Override
public <T> T get(Class<? extends T> type) {
T result = type.cast(map.get(type));
if (result != null) {
return result;
}
return null;
}

@Override
public <T, U extends T> void put(Class<T> type, U object) {
map.put(type, object);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.terasology.engine.context.Context;
import org.terasology.engine.context.internal.ContextImpl;

import java.util.NoSuchElementException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

public class InjectionHelperTest {

private ServiceA serviceA;
Expand All @@ -24,23 +20,23 @@ public void setUp() {
serviceB = new ServiceBImpl();
//injection helper uses the core registry,
//make sure the shared classes are not used over multiple tests
CoreRegistry.setContext(new ContextImpl());
CoreRegistry.setContext(new ContextImplementation());
}

@Test
public void testSharePopulatesCoreRegistry() {
assertNull(CoreRegistry.get(ServiceA.class));
Assertions.assertNull(CoreRegistry.get(ServiceA.class));

InjectionHelper.share(serviceA);

assertEquals(CoreRegistry.get(ServiceA.class), serviceA);
Assertions.assertEquals(CoreRegistry.get(ServiceA.class), serviceA);
}

@Test
public void testShareRequiresShareAnnotation() {
InjectionHelper.share(new ServiceAImplNoAnnotation());

assertNull(CoreRegistry.get(ServiceA.class));
Assertions.assertNull(CoreRegistry.get(ServiceA.class));
}

@Test
Expand All @@ -52,8 +48,8 @@ public void testDefaultFieldInjection() {
FieldInjectionAB fieldInjectionAB = new FieldInjectionAB();
InjectionHelper.inject(fieldInjectionAB);

assertEquals(fieldInjectionAB.getServiceA(), serviceA);
assertEquals(fieldInjectionAB.getServiceB(), serviceB);
Assertions.assertEquals(fieldInjectionAB.getServiceA(), serviceA);
Assertions.assertEquals(fieldInjectionAB.getServiceB(), serviceB);
}

@Test
Expand All @@ -64,54 +60,54 @@ public void testInjectUnavailableObject() {
FieldInjectionAB fieldInjectionAB = new FieldInjectionAB();
InjectionHelper.inject(fieldInjectionAB);

assertEquals(fieldInjectionAB.getServiceA(), serviceA);
assertNull(fieldInjectionAB.getServiceB());
Assertions.assertEquals(fieldInjectionAB.getServiceA(), serviceA);
Assertions.assertNull(fieldInjectionAB.getServiceB());
}

@Test
public void testDefaultConstructorInjection() {
Context context = new ContextImpl();
Context context = new ContextImplementation();
context.put(ServiceA.class, serviceA);
context.put(ServiceB.class, serviceB);

ConstructorAB constructorAB = InjectionHelper.createWithConstructorInjection(ConstructorAB.class, context);

//the two-arg constructor should be used as it has the most parameters and all can be populated
assertEquals(constructorAB.getServiceA(), serviceA);
assertEquals(constructorAB.getServiceB(), serviceB);
Assertions.assertEquals(constructorAB.getServiceA(), serviceA);
Assertions.assertEquals(constructorAB.getServiceB(), serviceB);
}

@Test
public void testConstructorInjectionNotAllParametersPopulated() {
Context context = new ContextImpl();
Context context = new ContextImplementation();
context.put(ServiceA.class, serviceA);
//context.put(ServiceB.class, serviceB);

ConstructorAB constructorAB = InjectionHelper.createWithConstructorInjection(ConstructorAB.class, context);

//the two-arg constructor can't be populated because serviceB is not available
//there is no fallback for a constructor with only serviceA, so the default constructor is called
assertNull(constructorAB.getServiceA());
assertNull(constructorAB.getServiceB());
Assertions.assertNull(constructorAB.getServiceA());
Assertions.assertNull(constructorAB.getServiceB());
}

@SuppressWarnings("checkstyle:LocalVariableName")
@Test
public void testConstructorInjectionNotAllParametersPopulatedFallback() {
Context context = new ContextImpl();
Context context = new ContextImplementation();
context.put(ServiceA.class, serviceA);
//context.put(ServiceB.class, serviceB);

ConstructorA_AB constructorA_AB = InjectionHelper.createWithConstructorInjection(ConstructorA_AB.class, context);

//the one-arg constructor is used as it can be populated with serviceA which is available
assertEquals(constructorA_AB.getServiceA(), serviceA);
assertNull(constructorA_AB.getServiceB());
Assertions.assertEquals(constructorA_AB.getServiceA(), serviceA);
Assertions.assertNull(constructorA_AB.getServiceB());
}

@Test
public void testConstructorInjectionNoDefaultConstructorForFallback() {
Context context = new ContextImpl();
Context context = new ContextImplementation();
context.put(ServiceA.class, serviceA);
//context.put(ServiceB.class, serviceB);

Expand Down