diff --git a/eventbus/pom.xml b/eventbus/pom.xml index fae03f1..583f434 100644 --- a/eventbus/pom.xml +++ b/eventbus/pom.xml @@ -26,6 +26,11 @@ feathercore-eventbus + + ru.progrm-jarvis + reflector + + org.projectlombok lombok diff --git a/eventbus/src/main/java/org/feathercore/eventbus/SimpleEventManager.java b/eventbus/src/main/java/org/feathercore/eventbus/SimpleEventManager.java index f1d12e7..73396dc 100644 --- a/eventbus/src/main/java/org/feathercore/eventbus/SimpleEventManager.java +++ b/eventbus/src/main/java/org/feathercore/eventbus/SimpleEventManager.java @@ -21,11 +21,9 @@ import lombok.experimental.FieldDefaults; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import ru.progrm_jarvis.reflector.invoke.InvokeUtil; -import java.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandles.Lookup; -import java.lang.invoke.MethodType; -import java.lang.reflect.Method; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; @@ -83,7 +81,14 @@ public void register(@NonNull final Object listener) { } EventHandler annotation = method.getAnnotation(EventHandler.class); - register((Class) parameterType, constructConsumer(listener, method), annotation.priority()); + register( + (Class) parameterType, + InvokeUtil., Object>invokeFactory() + .boundTo(listener) + .implementing(Consumer.class) + .via(method) + .createUnsafely(), + annotation.priority()); } } @@ -106,23 +111,6 @@ public void call(@NotNull final T event) { } } - @SuppressWarnings("unchecked") - protected static Consumer constructConsumer(Object listener, Method method) { - try { - Lookup lookup = constructLookup(listener.getClass()); - return (Consumer) LambdaMetafactory.metafactory( - lookup, - "accept", - MethodType.methodType(Consumer.class, listener.getClass()), - MethodType.methodType(void.class, Object.class), - lookup.unreflect(method), - MethodType.methodType(void.class, method.getParameterTypes()[0]) - ).getTarget().invoke(listener); - } catch (Throwable t) { - throw new RuntimeException("Could not create event listener", t); - } - } - /** * We are in need of our own Lookup creation with an argument of given owner class, because we want to be able * to execute private and any other non-public methods. diff --git a/module-api/pom.xml b/module-api/pom.xml index 426da93..5bf647c 100644 --- a/module-api/pom.xml +++ b/module-api/pom.xml @@ -31,6 +31,16 @@ feathercore-shared + + ru.progrm-jarvis + java-commons + + + + com.google.guava + guava + + org.projectlombok lombok @@ -45,6 +55,10 @@ org.junit.jupiter junit-jupiter-api + + org.junit.jupiter + junit-jupiter-params + org.mockito mockito-core diff --git a/module-api/src/main/java/ru/feathercore/moduleapi/AbstractModuleLoader.java b/module-api/src/main/java/ru/feathercore/moduleapi/AbstractModuleLoader.java index e73ef94..f32bdd3 100644 --- a/module-api/src/main/java/ru/feathercore/moduleapi/AbstractModuleLoader.java +++ b/module-api/src/main/java/ru/feathercore/moduleapi/AbstractModuleLoader.java @@ -20,58 +20,116 @@ import lombok.experimental.FieldDefaults; import org.jetbrains.annotations.NotNull; -import java.util.Collection; -import java.util.Collections; +import java.util.*; /** * An abstract implementation of {@link ModuleLoader} sufficient for all operations it requires. * - * @param super-type of all modules managed + * @param super-type of modules which this module-loader can manage + * + * // TODO: 15.11.2019 Docs */ @ToString @EqualsAndHashCode @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) -public abstract class AbstractModuleLoader implements ModuleLoader { +public class AbstractModuleLoader implements ModuleLoader { + + @NonNull Map, M> modules; + /** + * Storage of unique instanced of loaded modules. + */ + @NonNull Set loadedModules, loadedModulesView; + + /** + * Creates new instance of module loader using the given {@link Collection collections} for storage of modules. + * + * @param modulesMap set which will be used for storage of associations of module types with their implementations + * @param loadedModulesSet set which will be used for storage of loaded modules + * @throws IllegalArgumentException if the given set is non-empty + */ + public AbstractModuleLoader(@NonNull final Map, M> modulesMap, + @NonNull final Set loadedModulesSet) { + if (!modulesMap.isEmpty()) throw new IllegalArgumentException("modules map should be empty"); + if (!loadedModulesSet.isEmpty()) throw new IllegalArgumentException("loaded modules set should be empty"); + + modules = modulesMap; + loadedModules = loadedModulesSet; + loadedModulesView = Collections.unmodifiableSet(loadedModules); + } - @NonNull Collection modules, modulesView; + /* ************************************************* Callbacks ************************************************* */ - public AbstractModuleLoader(@NonNull final Collection modules) { - this.modules = modules; - modulesView = Collections.unmodifiableCollection(modules); + /** + * Callback used by method-loading methods to notify that the module has been loaded. + * + * @param module module which has just been loaded + */ + protected void onModuleLoad(@NotNull final M module) {} + + /** + * Callback used by {@link #unloadModule(Class)} to notify that the module has been unloaded. + * + * @param module module which has just been unloaded + */ + protected void onModuleUnload(@NotNull final M module) {} + + /* *************************************************** Logic *************************************************** */ + + @Override + public Collection getModules() { + return loadedModulesView; } @Override - public Collection getModules() { - return modulesView; + @SuppressWarnings("unchecked") + public Optional getModule(@NonNull final Class type) { + return Optional.ofNullable((T) modules.get(type)); } @Override - public T loadModule(@NonNull final ModuleInitializer initializer, final C configuration) { + public boolean isModuleLoaded(@NonNull final Class type) { + return modules.containsKey(type); + } + + @Override + public boolean isModuleLoaded(@NonNull final T module) { + return modules.containsValue(module); + } + + @Override + public T loadModule(@NonNull final ModuleInitializer initializer, final C configuration, + final Iterable> moduleTypes) { val module = initializer.loadModule(configuration); - if (modules.add(module)) onModuleLoad(module); + for (val moduleType : moduleTypes) modules.put(moduleType, module); + loadedModules.add(module); + + onModuleLoad(module); return module; } @Override - public boolean unloadModule(@NonNull final T module) { - val unloaded = modules.remove(module); - if (unloaded) onModuleUnload(module); + public Optional unloadModule(@NonNull final Class moduleType) { + @SuppressWarnings("unchecked") val module = (T) modules.remove(moduleType); + if (module != null) { + if (!modules.containsValue(module)) loadedModules.remove(module); + onModuleUnload(module); + + return Optional.of(module); + } - return unloaded; + return Optional.empty(); } - /** - * Callback used by {@link #loadModule(ModuleInitializer, Object)} to notify that the module has been loaded. - * - * @param module module which has just been loaded - */ - protected void onModuleLoad(@NotNull final M module) {} + @Override + public Collection unloadAllModules() { + val modulesUnloaded = new ArrayList<>(loadedModules); - /** - * Callback used by {@link #unloadModule(Module)} to notify that the module has been unloaded. - * - * @param module module which has just been loaded - */ - protected void onModuleUnload(@NotNull final M module) {} + for (val module : modulesUnloaded) onModuleUnload(module); + + modules.clear(); + loadedModules.clear(); + + return modulesUnloaded; + } } diff --git a/module-api/src/main/java/ru/feathercore/moduleapi/ModuleInitializer.java b/module-api/src/main/java/ru/feathercore/moduleapi/ModuleInitializer.java index 4883ccf..9007e56 100644 --- a/module-api/src/main/java/ru/feathercore/moduleapi/ModuleInitializer.java +++ b/module-api/src/main/java/ru/feathercore/moduleapi/ModuleInitializer.java @@ -16,8 +16,8 @@ package ru.feathercore.moduleapi; -import org.feathercore.shared.object.ValueContainer; import org.jetbrains.annotations.NotNull; +import ru.progrm_jarvis.javacommons.object.ValueContainer; /** * Initializer of a module which is normally used by {@link ModuleLoader} to load a specific module. diff --git a/module-api/src/main/java/ru/feathercore/moduleapi/ModuleLoader.java b/module-api/src/main/java/ru/feathercore/moduleapi/ModuleLoader.java index 168ea3b..097085c 100644 --- a/module-api/src/main/java/ru/feathercore/moduleapi/ModuleLoader.java +++ b/module-api/src/main/java/ru/feathercore/moduleapi/ModuleLoader.java @@ -17,17 +17,15 @@ package ru.feathercore.moduleapi; import lombok.NonNull; -import lombok.val; -import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Optional; -import java.util.stream.Collectors; /** * Loader of {@link Module modules} which uses {@link ModuleInitializer initializers} for this purpose. * - * @param super-type of all modules managed + * @param super-type of modules which this module-loader can manage */ public interface ModuleLoader { @@ -40,22 +38,7 @@ public interface ModuleLoader { * however it is recommended to perform copying of it if loading/unloading modules while iterating * so that {@link java.util.ConcurrentModificationException} does not happen */ - Collection getModules(); - - /** - * Gets all modules of given type loaded by this module loader. - * - * @param type type of modules for which to look - * @param exact type of modules searched - * @return all loaded modules of given type - */ - @SuppressWarnings("unchecked") - default Collection getModules(@NonNull final Class type) { - return (Collection) getModules() - .stream() - .filter(module -> type.isAssignableFrom(module.getClass())) - .collect(Collectors.toSet()); - } + Collection getModules(); /** * Finds a module of given type loaded by this module loader. @@ -64,13 +47,7 @@ default Collection getModules(@NonNull final Class * @param exact type of module searched * @return {@link Optional} containing a module of given type if it was found or an empty {@link Optional} otherwise */ - @SuppressWarnings("unchecked") - default Optional getModule(@NonNull final Class type) { - return (Optional) getModules() - .stream() - .filter(module -> type.isAssignableFrom(module.getClass())) - .findAny(); - } + Optional getModule(@NonNull final Class type); /** * Checks whether there is a module of given type currently loaded by this module loader. @@ -80,9 +57,7 @@ default Optional getModule(@NonNull final Class ty * * @apiNote multiple modules of given type may be loaded at once */ - default boolean isModuleLoaded(@NonNull final Class type) { - return getModules().stream().anyMatch(module -> type.isAssignableFrom(module.getClass())); - } + boolean isModuleLoaded(@NonNull final Class type); /** * Checks whether the given module is currently loaded by this module loader. @@ -90,69 +65,63 @@ default boolean isModuleLoaded(@NonNull final Class type) { * @param module module to check * @return {@link true} if the module is loaded and {@link false} otherwise */ - default boolean isModuleLoaded(@NonNull final T module) { - return getModules().contains(module); - } + boolean isModuleLoaded(@NonNull final T module); /** * Loads a module provided by the specified initializer using the given configuration. * + * @param exact type of a module loaded + * @param configuration type of a module ({@link Void} if none is expected) * @param initializer initializer to use for module's initialization * @param configuration configuration required by the module * (might be {@link null} depending on the initializer implementation) - * @param exact type of a module loaded - * @param configuration type of a module ({@link Void} if none is expected) + * @param moduleTypes * @return initialized module */ - T loadModule(@NonNull ModuleInitializer initializer, C configuration); + T loadModule(@NonNull ModuleInitializer initializer, C configuration, + Iterable> moduleTypes); /** * Loads a module provided by the specified initializer using its default configuration. * - * @param initializer initializer to use for module's initialization * @param exact type of a module loaded * @param configuration type of a module ({@link Void} if none is expected) + * @param initializer initializer to use for module's initialization + * @param moduleTypes types by which the modules will be known * @return initialized module * @throws ModuleConfigurationException if this initializer doesn't allow default configuration */ - default T loadModule(@NonNull ModuleInitializer initializer) { + default T loadModule(@NonNull final ModuleInitializer initializer, + final Iterable> moduleTypes) { return loadModule(initializer, initializer.getDefaultConfiguration().orElseThrow( () -> new ModuleConfigurationException(initializer + " does not allow usage of default configuration") - )); + ), moduleTypes); + } + + default T loadModule(@NonNull final ModuleInitializer initializer, C configuration, + @NonNull final Class moduleType) { + return loadModule(initializer, configuration, Collections.singletonList(moduleType)); } - /** - * Unloads all modules of the specified type. - * - * @param moduleTypes type of modules to unload - * @param exact type of modules unloaded - * @return collection (possibly empty) of unloaded modules of given type - */ - default Collection unloadModules(@NonNull final Class moduleTypes) { - val modules = getModules(moduleTypes); - for (val loadedModule : modules) unloadModule(loadedModule); - return modules; + default T loadModule(@NonNull final ModuleInitializer initializer, + @NonNull final Class moduleType) { + return loadModule(initializer, Collections.singletonList(moduleType)); } /** * Unloads the specified module. * - * @param module module which should unloaded * @param exact type of a module unloaded + * @param moduleType module which should unloaded * @return {@code true} if the specified module was loaded (and so got unloaded) and {@link false} otherwise */ - boolean unloadModule(@NonNull T module); + Optional unloadModule(@NonNull Class moduleType); /** * Unloads all modules loaded at current time. * * @return all modules unloaded */ - default Collection unloadAllModules() { - val modules = new ArrayList<>(getModules()); - for (val loadedModule : modules) unloadModule(loadedModule); - - return modules; - } + Collection unloadAllModules(); } diff --git a/module-api/src/main/java/ru/feathercore/moduleapi/SimpleModuleLoader.java b/module-api/src/main/java/ru/feathercore/moduleapi/SimpleModuleLoader.java new file mode 100644 index 0000000..8b2f9c3 --- /dev/null +++ b/module-api/src/main/java/ru/feathercore/moduleapi/SimpleModuleLoader.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.feathercore.moduleapi; + +import lombok.NonNull; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Simple module loader based on {@link AbstractModuleLoader}. + * It provides shorthand constructors for most commons use-cases. + * + * @param super-type of modules which this module-loader can manage + */ +public class SimpleModuleLoader extends AbstractModuleLoader { + + /** + * Creates new instance of module loader using the given collections for storage of modules. + * + * @param modulesMap set which will be used for storage of associations of module types with their implementations + * @param loadedModulesSet set which will be used for storage of loaded modules + * + * @throws IllegalArgumentException if the given set is non-empty + */ + protected SimpleModuleLoader(@NonNull final Map, M> modulesMap, + @NonNull final Set loadedModulesSet) { + super(modulesMap, loadedModulesSet); + } + + public static ModuleLoader create() { + return new SimpleModuleLoader(new HashMap<>(), new HashSet<>()); + } + + public static ModuleLoader createConcurrent() { + return new SimpleModuleLoader(new ConcurrentHashMap<>(), ConcurrentHashMap.newKeySet()); + } +} diff --git a/module-api/src/test/java/ru/feathercore/moduleapi/AbstractModuleLoaderTest.java b/module-api/src/test/java/ru/feathercore/moduleapi/AbstractModuleLoaderTest.java index 486a5b9..14e9aad 100644 --- a/module-api/src/test/java/ru/feathercore/moduleapi/AbstractModuleLoaderTest.java +++ b/module-api/src/test/java/ru/feathercore/moduleapi/AbstractModuleLoaderTest.java @@ -16,20 +16,21 @@ package ru.feathercore.moduleapi; -import org.feathercore.shared.object.ValueContainer; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import ru.progrm_jarvis.javacommons.object.ValueContainer; -import java.util.HashSet; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -39,134 +40,138 @@ class AbstractModuleLoaderTest { @Mock FooModule fooModule; @Spy ModuleInitializer fooModuleInitializer; @Mock BarModule barModule; - @Spy ModuleInitializer barModuleInitializer; - // < exact type is required for use of protected methods > - @SuppressWarnings("unchecked") private AbstractModuleLoader moduleLoader = mock(AbstractModuleLoader.class, - withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS).useConstructor(new HashSet<>()) - ); + @Spy ModuleInitializer barModuleInitializer; private Exception - nullConfigException = new RuntimeException("null passed as config"){}, - config0Exception = new RuntimeException("0 passed as config"){}, - config1Exception = new RuntimeException("1 passed as config"){}; + nullConfigException = new RuntimeException("null passed as config") {}, + config0Exception = new RuntimeException("0 passed as config") {}, + config1Exception = new RuntimeException("1 passed as config") {}; + + + interface SomeModule extends Module {} // Stubs to have different parent classes for modules - private class FooModule implements Module {} - private class BarModule implements Module {} - private class BazModule implements Module {} + private static class FooModule implements SomeModule {} + + private static class BarModule implements SomeModule {} + + private static class BazModule implements SomeModule {} - @Test - void testSimpleLoadUnload() { + protected static Stream provideTestedModuleLoader() { + throw new IllegalStateException("provideTestedModuleLoader() should be overwritten in extending classes"); + } + + @ParameterizedTest + @MethodSource("provideTestedModuleLoader") + void testSimpleLoadUnload(final ModuleLoader moduleLoader) { doReturn(fooModule).when(fooModuleInitializer).loadModule(any()); // check initial state assertThat(moduleLoader.getModules(), empty()); // load foo ... - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - verify(moduleLoader).onModuleLoad(fooModule); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); // ... and check if it was loaded normally assertThat(moduleLoader.getModules(), contains(fooModule)); - assertThat(moduleLoader.getModules(fooModule.getClass()), contains(fooModule)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule.getClass()), is(true)); + assertSame(moduleLoader.getModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); // unload foo - assertThat(moduleLoader.unloadModule(fooModule), is(true)); - verify(moduleLoader).onModuleUnload(fooModule); + assertThat(moduleLoader.unloadModule(FooModule.class), notNullValue()); // and check if it was unloaded assertThat(moduleLoader.getModules(), empty()); } - @Test - void checkContainment() { + @ParameterizedTest + @MethodSource("provideTestedModuleLoader") + void checkContainment(final ModuleLoader moduleLoader) { doReturn(fooModule).when(fooModuleInitializer).loadModule(any()); - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); assertThat(moduleLoader.getModules(), contains(fooModule)); - assertThat(moduleLoader.getModules(fooModule.getClass()), contains(fooModule)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule.getClass()), is(true)); + assertSame(moduleLoader.getModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); assertThat(moduleLoader.getModules(), not(contains(barModule))); - assertThat(moduleLoader.getModules(barModule.getClass()), empty()); - assertThat(moduleLoader.getModule(barModule.getClass()).isPresent(), is(false)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(barModule.getClass()), is(false)); + assertSame(moduleLoader.getModule(BarModule.class).isPresent(), false); + assertSame(moduleLoader.isModuleLoaded(barModule), false); + assertSame(moduleLoader.isModuleLoaded(BarModule.class), false); } - @Test - void testDuplicateLoading() { + @ParameterizedTest + @MethodSource("provideTestedModuleLoader") + void testDuplicateLoading(final ModuleLoader moduleLoader) { doReturn(fooModule).when(fooModuleInitializer).loadModule(any()); // check initial state assertThat(moduleLoader.getModules(), empty()); // load foo once ... - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - verify(moduleLoader).onModuleLoad(fooModule); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); // ... and check if it was loaded normally assertThat(moduleLoader.getModules(), contains(fooModule)); - assertThat(moduleLoader.getModules(fooModule.getClass()), contains(fooModule)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule.getClass()), is(true)); + assertSame(moduleLoader.getModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); // load foo again ... - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - verify(moduleLoader).onModuleLoad(fooModule); // 1 time again which means that it was not called again + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); // ... and check if module loader did not load anything new assertThat(moduleLoader.getModules(), contains(fooModule)); - assertThat(moduleLoader.getModules(fooModule.getClass()), contains(fooModule)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule.getClass()), is(true)); + assertSame(moduleLoader.getModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); } - @Test - void testMultipleLoading() { + @ParameterizedTest + @MethodSource("provideTestedModuleLoader") + void testMultipleLoading(final ModuleLoader moduleLoader) { doReturn(fooModule).when(fooModuleInitializer).loadModule(any()); doReturn(barModule).when(barModuleInitializer).loadModule(any()); // load foo ... - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - verify(moduleLoader).onModuleLoad(fooModule); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); // ... and check if it was loaded normally assertThat(moduleLoader.getModules(), contains(fooModule)); - assertThat(moduleLoader.getModules(fooModule.getClass()), contains(fooModule)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule.getClass()), is(true)); + assertSame(moduleLoader.getModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); // load bar ... - assertThat(moduleLoader.loadModule(barModuleInitializer), is(barModule)); - verify(moduleLoader).onModuleLoad(barModule); + assertSame(moduleLoader.loadModule(barModuleInitializer, BarModule.class), barModule); // ... and check if it was loaded normally assertThat(moduleLoader.getModules(), containsInAnyOrder(fooModule, barModule)); - assertThat(moduleLoader.getModules(fooModule.getClass()), contains(fooModule)); - assertThat(moduleLoader.getModules(barModule.getClass()), contains(barModule)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule.getClass()), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule.getClass()), is(true)); + assertSame(moduleLoader.getModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); + assertSame(moduleLoader.getModule(BarModule.class).orElseThrow(AssertionError::new), barModule); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertTrue(moduleLoader.isModuleLoaded(barModule)); + assertTrue(moduleLoader.isModuleLoaded(BarModule.class)); // unload each - assertThat(moduleLoader.unloadModule(fooModule), is(true)); + assertSame(moduleLoader.unloadModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); } - @Test - void testUnloadAll() { + @ParameterizedTest + @MethodSource("provideTestedModuleLoader") + void testUnloadAll(final ModuleLoader moduleLoader) { doReturn(fooModule).when(fooModuleInitializer).loadModule(any()); doReturn(barModule).when(barModuleInitializer).loadModule(any()); // load foo ... - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - assertThat(moduleLoader.loadModule(barModuleInitializer), is(barModule)); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); + assertSame(moduleLoader.loadModule(barModuleInitializer, BarModule.class), barModule); assertThat(moduleLoader.unloadAllModules(), containsInAnyOrder(fooModule, barModule)); assertThat(moduleLoader.getModules(), empty()); } - @Test - void testIntensiveLoadingWithParameters() { + @ParameterizedTest + @MethodSource("provideTestedModuleLoader") + void testIntensiveLoadingWithParameters(final ModuleLoader moduleLoader) { // foo module doReturn(fooModule).when(fooModuleInitializer).loadModule(any()); // bar module @@ -181,60 +186,49 @@ void testIntensiveLoadingWithParameters() { /////////////////////////////////////////////////////////////////////////// // make sure that by default there are no loaded modules - assertThat(moduleLoader.isModuleLoaded(fooModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(Module.class), is(false)); + assertSame(moduleLoader.isModuleLoaded(fooModule), false); + assertSame(moduleLoader.isModuleLoaded(FooModule.class), false); + assertSame(moduleLoader.isModuleLoaded(barModule), false); + assertSame(moduleLoader.isModuleLoaded(BarModule.class), false); assertThat(moduleLoader.getModules(), empty()); // try to load foo module... - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - verify(moduleLoader).onModuleLoad(fooModule); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); // ... and check if it is now loaded - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); assertThat(moduleLoader.getModules(), contains(fooModule)); // try to load bar module... // * With default (0) configuration - assertThrows(config0Exception.getClass(), () -> moduleLoader.loadModule(barModuleInitializer)); + assertThrows(config0Exception.getClass(), () -> moduleLoader.loadModule(barModuleInitializer, BarModule.class)); verify(barModuleInitializer).getDefaultConfiguration(); - verify(moduleLoader, never()).onModuleLoad(barModule); // ~ checking if nothing has broken - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(false)); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertSame(moduleLoader.isModuleLoaded(barModule), false); + assertSame(moduleLoader.isModuleLoaded(BarModule.class), false); // * With null configuration - assertThrows(nullConfigException.getClass(), () -> moduleLoader.loadModule(barModuleInitializer, null)); - verify(moduleLoader, never()).onModuleLoad(barModule); + assertThrows(nullConfigException.getClass(), () -> moduleLoader.loadModule(barModuleInitializer, null, BarModule.class)); // ~ checking if nothing has broken - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(false)); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertSame(moduleLoader.isModuleLoaded(barModule), false); + assertSame(moduleLoader.isModuleLoaded(BarModule.class), false); // * With 1 as configuration - assertThrows(config1Exception.getClass(), () -> moduleLoader.loadModule(barModuleInitializer, 1)); - verify(moduleLoader, never()).onModuleLoad(barModule); + assertThrows(config1Exception.getClass(), () -> moduleLoader.loadModule(barModuleInitializer, 1, BarModule.class)); // ~ checking if nothing has broken - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(false)); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertSame(moduleLoader.isModuleLoaded(barModule), false); + assertSame(moduleLoader.isModuleLoaded(BarModule.class), false); // * Normally - assertThat(moduleLoader.loadModule(barModuleInitializer, 2), is(barModule)); - verify(moduleLoader).onModuleLoad(barModule); + assertSame(moduleLoader.loadModule(barModuleInitializer, 2, BarModule.class), barModule); // ... and check if it is now loaded - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(true)); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertTrue(moduleLoader.isModuleLoaded(barModule)); + assertTrue(moduleLoader.isModuleLoaded(BarModule.class)); assertThat(moduleLoader.getModules(), containsInAnyOrder(fooModule, barModule)); /////////////////////////////////////////////////////////////////////////// @@ -242,40 +236,32 @@ void testIntensiveLoadingWithParameters() { /////////////////////////////////////////////////////////////////////////// // make sure that unloading dummy modules does nothing - assertThat(moduleLoader.unloadModule(new Module(){}), is(false)); - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(true)); - - assertThat(moduleLoader.unloadModules(new Module(){}.getClass()), empty()); - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(true)); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertTrue(moduleLoader.isModuleLoaded(barModule)); + assertTrue(moduleLoader.isModuleLoaded(BarModule.class)); + + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertTrue(moduleLoader.isModuleLoaded(barModule)); + assertTrue(moduleLoader.isModuleLoaded(BarModule.class)); // unload foo module ... - assertThat(moduleLoader.unloadModule(fooModule), is(true)); - verify(moduleLoader).onModuleUnload(fooModule); + assertSame(moduleLoader.unloadModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); // ... and check loaded modules - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(true)); + assertSame(moduleLoader.isModuleLoaded(fooModule), false); + assertSame(moduleLoader.isModuleLoaded(FooModule.class), false); + assertTrue(moduleLoader.isModuleLoaded(barModule)); + assertTrue(moduleLoader.isModuleLoaded(BarModule.class)); assertThat(moduleLoader.getModules(), contains(barModule)); // unload bar module... - assertThat(moduleLoader.unloadModule(barModule), is(true)); - verify(moduleLoader).onModuleUnload(barModule); + assertSame(moduleLoader.unloadModule(BarModule.class).orElseThrow(AssertionError::new), barModule); // ... and check loaded modules - assertThat(moduleLoader.isModuleLoaded(Module.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(false)); + assertSame(moduleLoader.isModuleLoaded(fooModule), false); + assertSame(moduleLoader.isModuleLoaded(FooModule.class), false); + assertSame(moduleLoader.isModuleLoaded(barModule), false); + assertSame(moduleLoader.isModuleLoaded(BarModule.class), false); assertThat(moduleLoader.getModules(), empty()); /////////////////////////////////////////////////////////////////////////// @@ -283,12 +269,10 @@ void testIntensiveLoadingWithParameters() { /////////////////////////////////////////////////////////////////////////// // try to load foo module... - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - verify(moduleLoader, times(2)).onModuleLoad(fooModule); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); // ... and check if it is now loaded - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); assertThat(moduleLoader.getModules(), contains(fooModule)); /////////////////////////////////////////////////////////////////////////// @@ -296,14 +280,12 @@ void testIntensiveLoadingWithParameters() { /////////////////////////////////////////////////////////////////////////// // unload bar module... - assertThat(moduleLoader.unloadModule(fooModule), is(true)); - verify(moduleLoader, times(2)).onModuleUnload(fooModule); + assertSame(moduleLoader.unloadModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); // ... and check loaded modules - assertThat(moduleLoader.isModuleLoaded(Module.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(false)); + assertSame(moduleLoader.isModuleLoaded(fooModule), false); + assertSame(moduleLoader.isModuleLoaded(FooModule.class), false); + assertSame(moduleLoader.isModuleLoaded(barModule), false); + assertSame(moduleLoader.isModuleLoaded(BarModule.class), false); assertThat(moduleLoader.getModules(), empty()); /////////////////////////////////////////////////////////////////////////// @@ -311,12 +293,10 @@ void testIntensiveLoadingWithParameters() { /////////////////////////////////////////////////////////////////////////// // try to load foo module... - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - verify(moduleLoader, times(3)).onModuleLoad(fooModule); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); // ... and check if it is now loaded - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); assertThat(moduleLoader.getModules(), contains(fooModule)); /////////////////////////////////////////////////////////////////////////// @@ -324,26 +304,24 @@ void testIntensiveLoadingWithParameters() { /////////////////////////////////////////////////////////////////////////// // unload foo module... - assertThat(moduleLoader.unloadModule(fooModule), is(true)); + assertSame(moduleLoader.unloadModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); // ... and check loaded modules - assertThat(moduleLoader.isModuleLoaded(Module.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(false)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(false)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(false)); + assertSame(moduleLoader.isModuleLoaded(fooModule), false); + assertSame(moduleLoader.isModuleLoaded(FooModule.class), false); + assertSame(moduleLoader.isModuleLoaded(barModule), false); + assertSame(moduleLoader.isModuleLoaded(BarModule.class), false); assertThat(moduleLoader.getModules(), empty()); /////////////////////////////////////////////////////////////////////////// // Try to load multiple modules now /////////////////////////////////////////////////////////////////////////// - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - assertThat(moduleLoader.loadModule(barModuleInitializer, 2), is(barModule)); - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(true)); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); + assertSame(moduleLoader.loadModule(barModuleInitializer, 2, BarModule.class), barModule); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertTrue(moduleLoader.isModuleLoaded(barModule)); + assertTrue(moduleLoader.isModuleLoaded(BarModule.class)); assertThat(moduleLoader.getModules(), containsInAnyOrder(fooModule, barModule)); /////////////////////////////////////////////////////////////////////////// @@ -357,26 +335,25 @@ void testIntensiveLoadingWithParameters() { // Try to load multiple modules now again for other unload type /////////////////////////////////////////////////////////////////////////// - assertThat(moduleLoader.loadModule(fooModuleInitializer), is(fooModule)); - assertThat(moduleLoader.loadModule(barModuleInitializer, 2), is(barModule)); - assertThat(moduleLoader.isModuleLoaded(Module.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(fooModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(FooModule.class), is(true)); - assertThat(moduleLoader.isModuleLoaded(barModule), is(true)); - assertThat(moduleLoader.isModuleLoaded(BarModule.class), is(true)); + assertSame(moduleLoader.loadModule(fooModuleInitializer, FooModule.class), fooModule); + assertSame(moduleLoader.loadModule(barModuleInitializer, 2, BarModule.class), barModule); + assertTrue(moduleLoader.isModuleLoaded(fooModule)); + assertTrue(moduleLoader.isModuleLoaded(FooModule.class)); + assertTrue(moduleLoader.isModuleLoaded(barModule)); + assertTrue(moduleLoader.isModuleLoaded(BarModule.class)); assertThat(moduleLoader.getModules(), containsInAnyOrder(fooModule, barModule)); /////////////////////////////////////////////////////////////////////////// // Unload by type now /////////////////////////////////////////////////////////////////////////// - assertThat(moduleLoader.unloadModules(fooModule.getClass()), contains(fooModule)); + assertSame(moduleLoader.unloadModule(FooModule.class).orElseThrow(AssertionError::new), fooModule); assertThat(moduleLoader.getModules(), contains(barModule)); - assertThat(moduleLoader.unloadModules(BazModule.class), empty()); + assertFalse(moduleLoader.unloadModule(BazModule.class).isPresent()); assertThat(moduleLoader.getModules(), contains(barModule)); - assertThat(moduleLoader.unloadModules(barModule.getClass()), contains(barModule)); + assertSame(moduleLoader.unloadModule(BarModule.class).orElseThrow(AssertionError::new), barModule); assertThat(moduleLoader.getModules(), empty()); } } \ No newline at end of file diff --git a/module-api/src/test/java/ru/feathercore/moduleapi/SimpleModuleLoaderTest.java b/module-api/src/test/java/ru/feathercore/moduleapi/SimpleModuleLoaderTest.java new file mode 100644 index 0000000..7f0a81c --- /dev/null +++ b/module-api/src/test/java/ru/feathercore/moduleapi/SimpleModuleLoaderTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.feathercore.moduleapi; + +import org.junit.jupiter.params.provider.Arguments; + +import java.util.stream.Stream; + +import static org.junit.jupiter.params.provider.Arguments.arguments; + +public class SimpleModuleLoaderTest extends AbstractModuleLoaderTest { + + public static Stream provideTestedModuleLoader() { + return Stream.of( + arguments(SimpleModuleLoader.create()), + arguments(SimpleModuleLoader.createConcurrent()) + ); + } +} diff --git a/pom.xml b/pom.xml index b65e4df..585f038 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,7 @@ 1.8 1.8 4.1.43.Final + 1.0-SNAPSHOT 5.5.2 1.5.2 @@ -53,6 +54,7 @@ eventbus module-api protocol + server @@ -61,6 +63,10 @@ Minecraft Libraries https://libraries.minecraft.net + + sonatype-snapshot-repo + https://oss.sonatype.org/content/repositories/snapshots/ + @@ -107,6 +113,11 @@ feathercore-protocol ${project.version} + + ${project.groupId} + feathercore-server + ${project.version} + org.hamcrest @@ -115,6 +126,16 @@ test + + ru.progrm-jarvis + java-commons + ${version.padla} + + + ru.progrm-jarvis + reflector + ${version.padla} + io.netty netty-all @@ -123,7 +144,7 @@ net.md-5 bungeecord-chat - 1.13-SNAPSHOT + 1.14-SNAPSHOT com.google.guava @@ -159,7 +180,7 @@ org.projectlombok lombok - 1.18.8 + 1.18.10 provided true diff --git a/protocol/pom.xml b/protocol/pom.xml index a3272e3..417546d 100644 --- a/protocol/pom.xml +++ b/protocol/pom.xml @@ -64,5 +64,22 @@ io.netty netty-all + + + org.junit.jupiter + junit-jupiter-api + + + org.mockito + mockito-core + + + org.mockito + mockito-junit-jupiter + + + org.hamcrest + hamcrest-all + \ No newline at end of file diff --git a/protocol/src/main/java/org/feathercore/protocol/annotation/PacketFactory.java b/protocol/src/main/java/org/feathercore/protocol/annotation/PacketFactory.java new file mode 100644 index 0000000..3c6d95d --- /dev/null +++ b/protocol/src/main/java/org/feathercore/protocol/annotation/PacketFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.feathercore.protocol.annotation; + +import java.lang.annotation.*; + +/** + * Annotation used for creation of {@link org.feathercore.protocol.packet.PacketType} + * from the class containing the annotation. + * . + */ +@Documented +@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PacketFactory { + + /** + * Retrieves the ID which should be used for creation of {@link org.feathercore.protocol.packet.PacketType}. + * + * @return ID which should be used for creation of {@link org.feathercore.protocol.packet.PacketType} + int value(); + */ +} diff --git a/protocol/src/main/java/org/feathercore/protocol/annotation/PacketId.java b/protocol/src/main/java/org/feathercore/protocol/annotation/PacketId.java new file mode 100644 index 0000000..0f71334 --- /dev/null +++ b/protocol/src/main/java/org/feathercore/protocol/annotation/PacketId.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.feathercore.protocol.annotation; + +import java.lang.annotation.*; + +/** + * Annotation used for creation of {@link org.feathercore.protocol.packet.PacketType} + * from the class annotated with it. + * + * @apiNote This is an alternative to {@link PacketFactory} - they cannot be used at the same time. + */ +@Documented +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PacketId { + /** + * ID of a packet which this class represents. + * + * @return ID of the packet which this class represents + */ + int value(); +} diff --git a/protocol/src/main/java/org/feathercore/protocol/exception/PacketHandleException.java b/protocol/src/main/java/org/feathercore/protocol/exception/PacketHandleException.java index 9b415fc..5d2c615 100644 --- a/protocol/src/main/java/org/feathercore/protocol/exception/PacketHandleException.java +++ b/protocol/src/main/java/org/feathercore/protocol/exception/PacketHandleException.java @@ -16,13 +16,20 @@ package org.feathercore.protocol.exception; -/** - * Created by k.shandurenko on 12/04/2019 - */ +import lombok.NoArgsConstructor; + +@NoArgsConstructor public class PacketHandleException extends IllegalStateException { - public PacketHandleException(String message, Throwable cause) { + public PacketHandleException(final String s) { + super(s); + } + + public PacketHandleException(final String message, final Throwable cause) { super(message, cause); } + public PacketHandleException(final Throwable cause) { + super(cause); + } } diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/handshake/client/HandshakePacketClientHandshake.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/handshake/client/HandshakePacketClientHandshake.java index 68ad70f..643f57e 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/handshake/client/HandshakePacketClientHandshake.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/handshake/client/HandshakePacketClientHandshake.java @@ -17,6 +17,7 @@ package org.feathercore.protocol.minecraft.packet.handshake.client; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.feathercore.protocol.packet.ConnectionState; import org.jetbrains.annotations.NotNull; @@ -24,6 +25,7 @@ /** * Created by k.shandurenko on 09/04/2019 */ +@PacketId(HandshakePacketClientHandshake.ID) public class HandshakePacketClientHandshake implements MinecraftPacket { public static final int ID = 0x00; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/client/LoginPacketClientEncryptionResponse.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/client/LoginPacketClientEncryptionResponse.java index e6459c3..2579f17 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/client/LoginPacketClientEncryptionResponse.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/client/LoginPacketClientEncryptionResponse.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.jetbrains.annotations.NotNull; @@ -32,6 +33,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(LoginPacketClientEncryptionResponse.ID) public class LoginPacketClientEncryptionResponse implements MinecraftPacket { public static final int ID = 0x01; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/client/LoginPacketClientLoginStart.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/client/LoginPacketClientLoginStart.java index f165903..d58ae1b 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/client/LoginPacketClientLoginStart.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/client/LoginPacketClientLoginStart.java @@ -23,6 +23,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.jetbrains.annotations.NotNull; @@ -33,6 +34,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(LoginPacketClientLoginStart.ID) public class LoginPacketClientLoginStart implements MinecraftPacket { public static final int ID = 0x00; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerDisconnect.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerDisconnect.java index 24ab75d..8341f4f 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerDisconnect.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerDisconnect.java @@ -23,6 +23,7 @@ import lombok.experimental.FieldDefaults; import net.md_5.bungee.api.chat.BaseComponent; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.jetbrains.annotations.NotNull; @@ -33,6 +34,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(LoginPacketServerDisconnect.ID) public class LoginPacketServerDisconnect implements MinecraftPacket { public static final int ID = 0x00; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerEnableCompression.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerEnableCompression.java index 7c67388..cfe0f00 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerEnableCompression.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerEnableCompression.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.jetbrains.annotations.NotNull; @@ -32,6 +33,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(LoginPacketServerEnableCompression.ID) public class LoginPacketServerEnableCompression implements MinecraftPacket { public static final int ID = 0x03; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerEncryptionRequest.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerEncryptionRequest.java index 82e6ccd..efce816 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerEncryptionRequest.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerEncryptionRequest.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.feathercore.protocol.encrypt.CryptManager; import org.jetbrains.annotations.NotNull; @@ -35,6 +36,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(LoginPacketServerEncryptionRequest.ID) public class LoginPacketServerEncryptionRequest implements MinecraftPacket { public static final int ID = 0x01; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerLoginSuccess.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerLoginSuccess.java index 7cafc54..4641e4e 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerLoginSuccess.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/login/server/LoginPacketServerLoginSuccess.java @@ -23,6 +23,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.jetbrains.annotations.NotNull; @@ -35,6 +36,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(LoginPacketServerLoginSuccess.ID) public class LoginPacketServerLoginSuccess implements MinecraftPacket { public static final int ID = 0x02; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/client/StatusPacketClientPing.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/client/StatusPacketClientPing.java index c3732aa..e5b6943 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/client/StatusPacketClientPing.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/client/StatusPacketClientPing.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.jetbrains.annotations.NotNull; @@ -32,6 +33,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(StatusPacketClientPing.ID) public class StatusPacketClientPing implements MinecraftPacket { public static final int ID = 0x01; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/client/StatusPacketClientServerQuery.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/client/StatusPacketClientServerQuery.java index 943a21c..b9322e7 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/client/StatusPacketClientServerQuery.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/client/StatusPacketClientServerQuery.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.jetbrains.annotations.NotNull; @@ -27,6 +28,7 @@ */ @Data @NoArgsConstructor +@PacketId(StatusPacketClientServerQuery.ID) public class StatusPacketClientServerQuery implements MinecraftPacket { public static final int ID = 0x00; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/server/StatusPacketServerPong.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/server/StatusPacketServerPong.java index e23724c..e45d0a5 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/server/StatusPacketServerPong.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/server/StatusPacketServerPong.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldDefaults; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.jetbrains.annotations.NotNull; @@ -32,6 +33,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(StatusPacketServerPong.ID) public class StatusPacketServerPong implements MinecraftPacket { public static final int ID = 0x01; diff --git a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/server/StatusPacketServerServerInfo.java b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/server/StatusPacketServerServerInfo.java index 09b1989..6a1a5e7 100644 --- a/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/server/StatusPacketServerServerInfo.java +++ b/protocol/src/main/java/org/feathercore/protocol/minecraft/packet/status/server/StatusPacketServerServerInfo.java @@ -22,6 +22,7 @@ import lombok.experimental.FieldDefaults; import net.md_5.bungee.api.chat.BaseComponent; import org.feathercore.protocol.Buffer; +import org.feathercore.protocol.annotation.PacketId; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; import org.feathercore.shared.util.json.JsonUtil; import org.jetbrains.annotations.NotNull; @@ -36,6 +37,7 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PROTECTED) +@PacketId(StatusPacketServerPong.ID) public class StatusPacketServerServerInfo implements MinecraftPacket { public static final int ID = 0x00; diff --git a/protocol/src/main/java/org/feathercore/protocol/netty/HandlerBoss.java b/protocol/src/main/java/org/feathercore/protocol/netty/HandlerBoss.java index a16ca51..0b85501 100644 --- a/protocol/src/main/java/org/feathercore/protocol/netty/HandlerBoss.java +++ b/protocol/src/main/java/org/feathercore/protocol/netty/HandlerBoss.java @@ -31,7 +31,7 @@ import org.feathercore.protocol.netty.util.NettyAttributes; import org.feathercore.protocol.packet.Packet; import org.feathercore.protocol.registry.PacketRegistry; -import org.feathercore.protocol.server.BaseServer; +import org.feathercore.protocol.server.Server; import java.io.IOException; import java.lang.ref.SoftReference; @@ -44,12 +44,12 @@ public class HandlerBoss extends ChannelInboundHandlerAdapter { @Setter @Getter private PacketRegistry packetRegistry; - private SoftReference serverSoftReference; + private SoftReference serverSoftReference; private final Logger logger; - public HandlerBoss(SoftReference serverSoftReference, Logger logger) { + public HandlerBoss(SoftReference serverSoftReference, Logger logger) { this.serverSoftReference = serverSoftReference; - this.packetRegistry = serverSoftReference.get().getInitialPacketRegistry(); + this.packetRegistry = serverSoftReference.get().getPacketRegistry(); this.logger = logger; } @@ -68,7 +68,7 @@ public void channelActive(ChannelHandlerContext ctx) { NettyAttributes.setAttribute(ctx, NettyAttributes.CONNECTION_ATTRIBUTE_KEY, connection); NettyAttributes.setAttribute(ctx, NettyAttributes.HANDLER_BOSS_ATTRIBUTE_KEY, this); - this.serverSoftReference.get().onConnected(connection); + this.serverSoftReference.get().handleConnect(connection); } @Override @@ -78,7 +78,7 @@ public void channelInactive(ChannelHandlerContext ctx) { return; } new PreDisconnectionEvent(connection).callGlobally(); - this.serverSoftReference.get().onDisconnected(connection); + this.serverSoftReference.get().handleDisconnect(connection); } @Override diff --git a/protocol/src/main/java/org/feathercore/protocol/netty/NettyServer.java b/protocol/src/main/java/org/feathercore/protocol/netty/NettyServer.java index cba7ead..553f379 100644 --- a/protocol/src/main/java/org/feathercore/protocol/netty/NettyServer.java +++ b/protocol/src/main/java/org/feathercore/protocol/netty/NettyServer.java @@ -22,46 +22,50 @@ import io.netty.channel.ChannelFutureListener; import lombok.NonNull; import lombok.extern.log4j.Log4j2; -import org.feathercore.protocol.server.BaseServer; import org.feathercore.protocol.netty.channel.ChannelInitializer; import org.feathercore.protocol.netty.util.SharedNettyResources; +import org.feathercore.protocol.packet.Packet; +import org.feathercore.protocol.server.AbstractServer; import java.lang.ref.SoftReference; +import java.net.SocketAddress; /** - * Created by k.shandurenko on 12/04/2019 + * Netty-based {@link org.feathercore.protocol.server.Server} implementation. + * + * @param

super-type of packets used by this server's protocol */ @Log4j2 -public abstract class NettyServer extends BaseServer { +public abstract class NettyServer

extends AbstractServer

{ @NonNull private final SharedNettyResources sharedNettyResources; protected Channel channel; - public NettyServer(@NonNull final String host, final int port) { - super(host, port); - this.sharedNettyResources = SharedNettyResources.builder().build(); + protected NettyServer(@NonNull final SocketAddress socketAddress, + @NonNull final SharedNettyResources sharedNettyResources) { + super(socketAddress); + this.sharedNettyResources = sharedNettyResources; } @Override - public ChannelFuture start() { + protected ChannelFuture performStart() { ServerBootstrap bootstrap = new ServerBootstrap() .group(sharedNettyResources.getBossLoopGroup(), sharedNettyResources.getWorkerLoopGroup()) - .channel(sharedNettyResources.getServerChannelClass()) + .channel(sharedNettyResources.getTransportType().getServerChannelClass()) .childHandler(new ChannelInitializer(new SoftReference<>(this), log)); - return bootstrap.bind(host, port).addListener((ChannelFutureListener) future -> { + return bootstrap.bind(address).addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { channel = future.channel(); - log.info("BaseServer has been started on " + host + ":" + port); + log.info("Server has been started on " + address); } else { - log.warn("BaseServer could not bind to " + host + ":" + port, future.cause()); + log.warn("Server could not bind to " + address, future.cause()); } }); } @Override - public ChannelFuture stop() { + protected ChannelFuture performStop() { return channel.close().syncUninterruptibly(); } - } diff --git a/protocol/src/main/java/org/feathercore/protocol/netty/channel/ChannelInitializer.java b/protocol/src/main/java/org/feathercore/protocol/netty/channel/ChannelInitializer.java index d00e1d1..6ecc6c1 100644 --- a/protocol/src/main/java/org/feathercore/protocol/netty/channel/ChannelInitializer.java +++ b/protocol/src/main/java/org/feathercore/protocol/netty/channel/ChannelInitializer.java @@ -21,12 +21,12 @@ import io.netty.handler.timeout.ReadTimeoutHandler; import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.Logger; -import org.feathercore.protocol.server.BaseServer; import org.feathercore.protocol.netty.HandlerBoss; import org.feathercore.protocol.netty.codec.InboundDecoder; import org.feathercore.protocol.netty.codec.InboundPacketDecoder; import org.feathercore.protocol.netty.codec.OutboundEncoder; import org.feathercore.protocol.netty.codec.ServerPingAdapter; +import org.feathercore.protocol.server.Server; import java.lang.ref.SoftReference; @@ -36,7 +36,7 @@ @RequiredArgsConstructor public class ChannelInitializer extends io.netty.channel.ChannelInitializer { - private final SoftReference serverSoftReference; + private final SoftReference serverSoftReference; private final Logger logger; @Override diff --git a/protocol/src/main/java/org/feathercore/protocol/netty/util/AbstractSharedNettyResources.java b/protocol/src/main/java/org/feathercore/protocol/netty/util/AbstractSharedNettyResources.java new file mode 100644 index 0000000..64ac261 --- /dev/null +++ b/protocol/src/main/java/org/feathercore/protocol/netty/util/AbstractSharedNettyResources.java @@ -0,0 +1,197 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.feathercore.protocol.netty.util; + +import io.netty.channel.EventLoopGroup; +import io.netty.util.concurrent.Future; +import lombok.*; +import lombok.experimental.FieldDefaults; +import lombok.experimental.NonFinal; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) +public abstract class AbstractSharedNettyResources implements SharedNettyResources { + + @NonNull @Getter TransportType transportType; + boolean bossIsWorker; + + @NonNull final Lock readLock, writeLock; + @NonNull final AtomicBoolean shutDown; + + @NonFinal @Nullable volatile EventLoopGroup bossLoopGroup; + @NonFinal @Nullable volatile EventLoopGroup workerLoopGroup; + + public AbstractSharedNettyResources(@NonNull final TransportType transportType, final boolean bossIsWorker) { + this.transportType = transportType; + this.bossIsWorker = bossIsWorker; + + shutDown = new AtomicBoolean(); + { + val lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + } + } + + /* *********************************************** Stateful data *********************************************** */ + + protected abstract EventLoopGroup newBossLoopGroup(); + + protected abstract EventLoopGroup newWorkerLoopGroup(); + + /** + * Asserts that this instance is not shut down. + * + * @throws IllegalStateException if this instance is already shut down + */ + protected void assertNotShutDown() { + if (isShutDown()) { + throw new IllegalStateException("LoopGroupGracefully is already shut down."); + } + } + + @Override + public boolean isInitialized() { + assertNotShutDown(); + + readLock.lock(); + try { + return bossLoopGroup != null && workerLoopGroup != null; + } finally { + readLock.unlock(); + } + } + + @Override + public boolean isShutDown() { + return shutDown.get(); + } + + @SneakyThrows(InterruptedException.class) + protected void performLoopGroupsShutdownGracefully(final boolean await) { + readLock.lock(); + try { + if (bossLoopGroup != null || workerLoopGroup != null) { + writeLock.lock(); + try { + // used not to duplicate GETFIELD invocations and to omit potential null-checks + var loopGroup = bossLoopGroup; + @Nullable final Future bossShutDown; + /* Shutdown boss */ + if (loopGroup != null) { + bossShutDown = loopGroup.shutdownGracefully(); + bossLoopGroup = null; + } else { + bossShutDown = null; + } + + loopGroup = workerLoopGroup; + @Nullable final Future workerShutDown; + if (loopGroup != null) { + /* Shutdown worker */ + workerShutDown = loopGroup.shutdownGracefully().await(); + workerLoopGroup = null; + } else { + workerShutDown = null; + } + + if (await) { + if (bossShutDown != null) { + bossShutDown.await(); + } + if (workerShutDown != null) { + workerShutDown.await(); + } + } + } finally { + writeLock.unlock(); + } + } + } finally { + readLock.unlock(); + } + } + + @Override + public void shutdownLoopGroupsGracefully(final boolean await) { + if (shutDown.compareAndSet(false, true)) { + performLoopGroupsShutdownGracefully(await); + } else { + throw new IllegalStateException("LoopGroupGracefully is already shut down."); + } + } + + @Override + public boolean tryShutdownLoopGroupsGracefully(final boolean await) { + if (shutDown.compareAndSet(false, true)) { + performLoopGroupsShutdownGracefully(await); + + return true; + } + + return false; + } + + @Override + @NotNull public EventLoopGroup getBossLoopGroup() { + assertNotShutDown(); + + readLock.lock(); + try { + val loopGroup = bossLoopGroup; + if (loopGroup == null) { + writeLock.lock(); + try { + return bossLoopGroup = newWorkerLoopGroup(); + } finally { + writeLock.unlock(); + } + } + + return loopGroup; + } finally { + readLock.unlock(); + } + } + + @Override + @NotNull public EventLoopGroup getWorkerLoopGroup() { + assertNotShutDown(); + + readLock.lock(); + try { + val loopGroup = workerLoopGroup; + if (loopGroup == null) { + writeLock.lock(); + try { + return workerLoopGroup = newWorkerLoopGroup(); + } finally { + writeLock.unlock(); + } + } + + return loopGroup; + } finally { + readLock.unlock(); + } + } +} diff --git a/protocol/src/main/java/org/feathercore/protocol/netty/util/DefaultTransportType.java b/protocol/src/main/java/org/feathercore/protocol/netty/util/DefaultTransportType.java new file mode 100644 index 0000000..8f58bd3 --- /dev/null +++ b/protocol/src/main/java/org/feathercore/protocol/netty/util/DefaultTransportType.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.feathercore.protocol.netty.util; + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.channel.kqueue.KQueueSocketChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import lombok.*; +import lombok.experimental.FieldDefaults; + +import java.util.concurrent.ThreadFactory; + +@Getter +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public enum DefaultTransportType implements TransportType { + EPOLL(EpollSocketChannel.class, EpollServerSocketChannel.class) { + @Override + public boolean isAvailable() { + return Epoll.isAvailable(); + } + + @Override + public EventLoopGroup newEventLoopGroup(final int threads, @NonNull final ThreadFactory threadFactory) { + return new EpollEventLoopGroup(threads, threadFactory); + } + }, + KQUEUE(KQueueSocketChannel.class, KQueueServerSocketChannel.class) { + @Override + public boolean isAvailable() { + return KQueue.isAvailable(); + } + + @Override + public EventLoopGroup newEventLoopGroup(final int threads, @NonNull final ThreadFactory threadFactory) { + return new KQueueEventLoopGroup(threads, threadFactory); + } + }, + NIO(NioSocketChannel.class, NioServerSocketChannel.class) { + @Override + public boolean isAvailable() { + return true; + } + + @Override + public EventLoopGroup newEventLoopGroup(final int threads, @NonNull final ThreadFactory threadFactory) { + return new NioEventLoopGroup(threads, threadFactory); + } + }; + + Class socketChannelClass; + Class serverChannelClass; + + public static TransportType getNative() { + for (val transportType : values()) { + if (transportType.isAvailable()) { + return transportType; + } + } + + return getDefault(); + } + + public static TransportType getDefault() { + return NIO; + } +} diff --git a/protocol/src/main/java/org/feathercore/protocol/netty/util/SharedNettyResources.java b/protocol/src/main/java/org/feathercore/protocol/netty/util/SharedNettyResources.java index 8093096..6620345 100644 --- a/protocol/src/main/java/org/feathercore/protocol/netty/util/SharedNettyResources.java +++ b/protocol/src/main/java/org/feathercore/protocol/netty/util/SharedNettyResources.java @@ -17,164 +17,32 @@ package org.feathercore.protocol.netty.util; import io.netty.channel.EventLoopGroup; -import io.netty.channel.ServerChannel; -import io.netty.channel.socket.SocketChannel; -import lombok.*; -import lombok.experimental.FieldDefaults; -import lombok.experimental.NonFinal; -import org.feathercore.shared.util.NamedThreadFactory; -import org.jetbrains.annotations.Nullable; - -import javax.annotation.Nonnegative; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.function.Supplier; - -import static com.google.common.base.Preconditions.checkArgument; -import static java.lang.Math.max; +import org.jetbrains.annotations.NotNull; /** - * Created by k.shandurenko on 12/04/2019 + * Holder of shared data used by Netty's basic elements. */ -@Value -@Builder -@FieldDefaults(level = AccessLevel.PROTECTED) -@NonFinal -public class SharedNettyResources { - - @NonNull protected final ReadWriteLock lock; - - boolean bossIsWorker; - @Builder.Default boolean useNativeTransport = true; - @Builder.Default @Nonnegative int bossThreads = 2, workerThreads = 0; - - @NonFinal @Nullable volatile EventLoopGroup bossLoopGroup, workerLoopGroup; - - @NonNull Supplier - bossThreadFactorySupplier = () -> new NamedThreadFactory("Netty Boss Thread #", false), - workerThreadFactorySupplier = () -> new NamedThreadFactory("Netty Worker Thread #", false); +public interface SharedNettyResources { - public boolean isInitialized() { - lock.readLock().lock(); - try { - return bossLoopGroup != null && workerLoopGroup != null; - } finally { - lock.readLock().unlock(); - } - } - - @SneakyThrows(InterruptedException.class) - public void shutdownLoopGroupsGracefully(final boolean await) { - lock.readLock().lock(); - try { - if (bossLoopGroup != null) { - lock.writeLock().lock(); - try { - if (await) { - bossLoopGroup.shutdownGracefully().await(); - } else { - bossLoopGroup.shutdownGracefully(); - } - - bossLoopGroup = null; - } finally { - lock.writeLock().unlock(); - } - } - if (workerLoopGroup != null) { - lock.writeLock().lock(); - try { - if (await) { - workerLoopGroup.shutdownGracefully().await(); - } - workerLoopGroup.shutdownGracefully(); + boolean isInitialized(); - workerLoopGroup = null; - } finally { - lock.writeLock().unlock(); - } - } - } finally { - lock.readLock().unlock(); - } - } + boolean isShutDown(); - public void shutdownLoopGroupsGracefully() { - shutdownLoopGroupsGracefully(false); - } + @NotNull TransportType getTransportType(); - public EventLoopGroup getBossLoopGroup() { - lock.readLock().lock(); - try { - if (bossLoopGroup == null) { - lock.writeLock().lock(); - try { - bossLoopGroup = (useNativeTransport ? TransportType.getNative() : TransportType.getDefault()) - .newEventLoopGroup(bossThreads, bossThreadFactorySupplier.get()); - } finally { - lock.writeLock().unlock(); - } - } + @NotNull EventLoopGroup getBossLoopGroup(); - return bossLoopGroup; - } finally { - lock.readLock().unlock(); - } - } + @NotNull EventLoopGroup getWorkerLoopGroup(); - public EventLoopGroup getWorkerLoopGroup() { - lock.readLock().lock(); - try { - if (workerLoopGroup == null) { - lock.writeLock().lock(); - try { - if (bossIsWorker) { - workerLoopGroup = getWorkerLoopGroup(); - } else { - workerLoopGroup = (useNativeTransport ? TransportType.getNative() : TransportType.getDefault()) - .newEventLoopGroup(max(1, workerThreads), workerThreadFactorySupplier.get()); - } - } finally { - lock.writeLock().unlock(); - } - } + void shutdownLoopGroupsGracefully(final boolean await); - return workerLoopGroup; - } finally { - lock.readLock().unlock(); - } + default void shutdownLoopGroupsGracefully() { + shutdownLoopGroupsGracefully(true); } - public Class getSocketChannelClass() { - return (useNativeTransport ? TransportType.getNative() : TransportType.getDefault()).getSocketChannelClass(); - } - - public Class getServerChannelClass() { - return (useNativeTransport ? TransportType.getNative() : TransportType.getDefault()).getServerChannelClass(); - } - - public static class SharedNettyResourcesBuilder { - - public SharedNettyResourcesBuilder bossThreads(@Nonnegative final int bossThreads) { - checkArgument(bossThreads > 0, "There should be at least 1 boss thread"); - - this.bossThreads = bossThreads; - - return this; - } - - public SharedNettyResourcesBuilder workerThreads(@Nonnegative final int workerThreads) { - checkArgument(workerThreads <= 0, "Number of worker threads cannot be negative"); - - this.workerThreads = workerThreads; - - return this; - } - - protected SharedNettyResourcesBuilder lock(@NonNull final ReadWriteLock lock) { - this.lock = lock; + boolean tryShutdownLoopGroupsGracefully(final boolean await); - return this; - } + default boolean tryShutdownLoopGroupsGracefully() { + return tryShutdownLoopGroupsGracefully(true); } -} \ No newline at end of file +} diff --git a/protocol/src/main/java/org/feathercore/protocol/netty/util/SimpleSharedNettyResources.java b/protocol/src/main/java/org/feathercore/protocol/netty/util/SimpleSharedNettyResources.java new file mode 100644 index 0000000..1a9c01a --- /dev/null +++ b/protocol/src/main/java/org/feathercore/protocol/netty/util/SimpleSharedNettyResources.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.feathercore.protocol.netty.util; + +import io.netty.channel.EventLoopGroup; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.NonNull; +import lombok.experimental.FieldDefaults; +import lombok.experimental.NonFinal; +import org.feathercore.shared.util.thread.ThreadFactories; + +import static com.google.common.base.Preconditions.checkArgument; + +@FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) +@NonFinal public class SimpleSharedNettyResources extends AbstractSharedNettyResources { + + protected int bossThreads, workerThreads; + + @Builder + public SimpleSharedNettyResources(@NonNull final TransportType transportType, final boolean bossIsWorker, + final int bossThreads, final int workerThreads) { + super(transportType, bossIsWorker); + + checkArgument(bossThreads >= 0, "There should be at least 1 boss thread (or 0 for default)"); + checkArgument(workerThreads >= 0, "There should be at least 1 worker thread (or 0 for default)"); + + this.bossThreads = bossThreads; + this.workerThreads = workerThreads; + } + + public static SimpleSharedNettyResources createDefault() { + return SimpleSharedNettyResources.builder() + .transportType(DefaultTransportType.getNative()) + .bossThreads(2) + .workerThreads(0) + .build(); + } + + @Override + protected EventLoopGroup newBossLoopGroup() { + return transportType.newEventLoopGroup( + bossThreads, ThreadFactories.createPaginated("Netty Worker Thread #", false) + ); + } + + @Override + protected EventLoopGroup newWorkerLoopGroup() { + return transportType.newEventLoopGroup( + workerThreads, ThreadFactories.createPaginated("Netty Worker Thread #", false) + ); + } +} \ No newline at end of file diff --git a/protocol/src/main/java/org/feathercore/protocol/netty/util/TransportType.java b/protocol/src/main/java/org/feathercore/protocol/netty/util/TransportType.java index b2231ea..8a13336 100644 --- a/protocol/src/main/java/org/feathercore/protocol/netty/util/TransportType.java +++ b/protocol/src/main/java/org/feathercore/protocol/netty/util/TransportType.java @@ -18,81 +18,19 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; -import io.netty.channel.epoll.Epoll; -import io.netty.channel.epoll.EpollEventLoopGroup; -import io.netty.channel.epoll.EpollServerSocketChannel; -import io.netty.channel.epoll.EpollSocketChannel; -import io.netty.channel.kqueue.KQueue; -import io.netty.channel.kqueue.KQueueEventLoopGroup; -import io.netty.channel.kqueue.KQueueServerSocketChannel; -import io.netty.channel.kqueue.KQueueSocketChannel; -import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.channel.socket.nio.NioSocketChannel; -import lombok.*; -import lombok.experimental.FieldDefaults; +import lombok.NonNull; +import javax.annotation.Nonnegative; import java.util.concurrent.ThreadFactory; -@Getter -@RequiredArgsConstructor -@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) -public enum TransportType { - EPOLL(EpollSocketChannel.class, EpollServerSocketChannel.class) { - @Override - public boolean isAvailable() { - return Epoll.isAvailable(); - } +public interface TransportType { - @Override - public EventLoopGroup newEventLoopGroup(final int threads, @NonNull final ThreadFactory threadFactory) { - return new EpollEventLoopGroup(threads, threadFactory); - } - }, - KQUEUE(KQueueSocketChannel.class, KQueueServerSocketChannel.class) { - @Override - public boolean isAvailable() { - return KQueue.isAvailable(); - } + boolean isAvailable(); - @Override - public EventLoopGroup newEventLoopGroup(final int threads, @NonNull final ThreadFactory threadFactory) { - return new KQueueEventLoopGroup(threads, threadFactory); - } - }, - NIO(NioSocketChannel.class, NioServerSocketChannel.class) { - @Override - public boolean isAvailable() { - return true; - } + EventLoopGroup newEventLoopGroup(@Nonnegative int threads, @NonNull ThreadFactory threadFactory); - @Override - public EventLoopGroup newEventLoopGroup(final int threads, @NonNull final ThreadFactory threadFactory) { - return new NioEventLoopGroup(threads, threadFactory); - } - }; + Class getSocketChannelClass(); - private static final TransportType[] TRANSPORT_TYPES = values(); - - Class socketChannelClass; - Class serverChannelClass; - - public abstract boolean isAvailable(); - - public abstract EventLoopGroup newEventLoopGroup(int threads, @NonNull ThreadFactory threadFactory); - - public static TransportType getNative() { - for (val transportType : TRANSPORT_TYPES) { - if (transportType.isAvailable()) { - return transportType; - } - } - - return getDefault(); - } - - public static TransportType getDefault() { - return NIO; - } + Class getServerChannelClass(); } diff --git a/protocol/src/main/java/org/feathercore/protocol/packet/PacketType.java b/protocol/src/main/java/org/feathercore/protocol/packet/PacketType.java index b7d0325..b765111 100644 --- a/protocol/src/main/java/org/feathercore/protocol/packet/PacketType.java +++ b/protocol/src/main/java/org/feathercore/protocol/packet/PacketType.java @@ -21,40 +21,121 @@ import lombok.Value; import lombok.experimental.FieldDefaults; import lombok.experimental.NonFinal; +import lombok.val; +import org.feathercore.protocol.annotation.PacketFactory; +import org.feathercore.protocol.annotation.PacketId; +import ru.progrm_jarvis.reflector.invoke.InvokeUtil; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.google.common.base.Preconditions.checkArgument; /** * A value-class containing all data identifying packet type. * * @param

type of packet */ -@Value @NonFinal +@Value(staticConstructor = "create") @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) public class PacketType

{ + /** + * ID of the packet + */ int id; - @NonNull Class

type; - @NonNull Supplier

supplier; - - private PacketType(Class

type, Supplier

supplier) { - try { - this.id = type.getField("ID").getInt(null); - } catch (IllegalAccessException | NoSuchFieldException e) { - throw new RuntimeException( - "Could not instantiate packet type, because there is not ID field in given packet class or it is " - + "not accessible", - e + /** + * Factory used for instantiation of packets + */ + @NonNull Supplier

factory; + + /** + * Creates a packet type from the given class using its meta-structure. + *

+ * This will look for constructors and methods annotated with {@link PacketFactory} which will be used as factories + * for the packets of given IDs. + * + * @param type class which should be analyzed to create packet type from it + * @param

type of the packet + * @return created packet type + * + * @throws IllegalArgumentException if the given class contains multiple methods annotated with {@link PacketFactory} + */ + public static

PacketType

create(@NonNull final Class

type) { + { + @SuppressWarnings("unchecked") val optionalConstructor + = lookupPacketFactory(Arrays.stream((Constructor

[]) type.getDeclaredConstructors())); + if (optionalConstructor.isPresent()) { + val constructor = optionalConstructor.get(); + + return create(type, InvokeUtil.toSupplier(constructor)); + } + } + { + val optionalMethod = lookupPacketFactory( + Arrays.stream(type.getDeclaredMethods()) + .filter(method -> Modifier.isStatic(method.getModifiers())) ); + if (optionalMethod.isPresent()) { + val method = optionalMethod.get(); + + return create(type, InvokeUtil.toStaticSupplier(method)); + } } - this.type = type; - this.supplier = supplier; + + throw new IllegalArgumentException( + "There should be a no-args method or constructor annotated with @PacketFactory" + ); } + /** + * Creates a packet-type for the given class or its sub-type using the given factory. + *

+ * This will try to use {@link PacketFactory} annotation on this class + * and {@link PacketId} on its static field or method to get the ID of the packet. + * + * @param type type of the packet whose type is being created + * @param factory factory to be used for instantiation of packets + * @param

exact type of packet + * @return created packet-type + */ public static

PacketType

create(@NonNull final Class

type, - @NonNull final Supplier

supplier) { - return new PacketType<>(type, supplier); + @NonNull final Supplier

factory) { + val packetIdAnnotation = type.getAnnotation(PacketId.class); + checkArgument(packetIdAnnotation != null, "Type should be annotated with @PacketId"); + + return create(packetIdAnnotation.value(), factory); } + /** + * Finds an executable which may be used as a packet-type factory. + * + * @param possibleFactories array of elements some of which which may be packet factories + * @param exact type of passed executable + * @return {@link Optional} containing found factory (if there was only one found) or + * an empty one if there were no possible packet-factories in the given array + * + * @throws IllegalArgumentException if there is more than one possible packet-factory in the given array + */ + private static Optional lookupPacketFactory(@NonNull final Stream possibleFactories) { + val factories = possibleFactories + .filter(possibleFactory -> possibleFactory.getParameterCount() == 0) + .filter(possibleFactory -> possibleFactory.isAnnotationPresent(PacketFactory.class)) + .collect(Collectors.toList()); + + if (factories.isEmpty()) return Optional.empty(); + checkArgument( + factories.size() == 1, + "There should only be one no-args method or constructor annotated with @PacketFactory" + ); + + return Optional.of(factories.get(0)); + } } diff --git a/protocol/src/main/java/org/feathercore/protocol/registry/PacketRegistry.java b/protocol/src/main/java/org/feathercore/protocol/registry/PacketRegistry.java index 45d6510..744b93f 100644 --- a/protocol/src/main/java/org/feathercore/protocol/registry/PacketRegistry.java +++ b/protocol/src/main/java/org/feathercore/protocol/registry/PacketRegistry.java @@ -54,7 +54,7 @@ public interface PacketRegistry

{ default P createById(final int id) { val type = getTypeById(id); - return type == null ? null : type.getSupplier().get(); + return type == null ? null : type.getFactory().get(); } void handlePacket(@NotNull Connection connection, @NotNull Packet packet); diff --git a/protocol/src/main/java/org/feathercore/protocol/registry/common/CommonHandshakePacketRegistry.java b/protocol/src/main/java/org/feathercore/protocol/registry/common/CommonHandshakePacketRegistry.java index 4a95a96..f011417 100644 --- a/protocol/src/main/java/org/feathercore/protocol/registry/common/CommonHandshakePacketRegistry.java +++ b/protocol/src/main/java/org/feathercore/protocol/registry/common/CommonHandshakePacketRegistry.java @@ -32,6 +32,7 @@ public class CommonHandshakePacketRegistry { @SuppressWarnings("unchecked") public static PacketRegistry createNew() { + //noinspection RedundantCast this is a javac bug: cast is required return ArrayBasedPacketRegistry.Builder.create() .exceptionHandler((BiConsumer) (connection, throwable) -> connection.disconnect()) diff --git a/protocol/src/main/java/org/feathercore/protocol/server/AbstractServer.java b/protocol/src/main/java/org/feathercore/protocol/server/AbstractServer.java index f2c8d26..bc9944a 100644 --- a/protocol/src/main/java/org/feathercore/protocol/server/AbstractServer.java +++ b/protocol/src/main/java/org/feathercore/protocol/server/AbstractServer.java @@ -16,28 +16,56 @@ package org.feathercore.protocol.server; +import lombok.AccessLevel; +import lombok.Getter; import lombok.NonNull; -import org.feathercore.protocol.Connection; -import org.feathercore.protocol.netty.NettyServer; -import org.jetbrains.annotations.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import lombok.experimental.NonFinal; +import org.feathercore.protocol.packet.Packet; + +import java.net.SocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; /** - * Created by k.shandurenko on 16/04/2019 + * Abstract base for a {@link Server implementation}. + * + * @param

super-type of packets used by this server's protocol */ -public abstract class AbstractServer extends NettyServer { +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) +public abstract class AbstractServer

implements Server

{ - public AbstractServer(final @NonNull String host, final int port) { - super(host, port); - } + @NonNull @Getter SocketAddress address; + + @NonNull Object startStopMutex = new Object[0]; + @NonNull @NonFinal volatile boolean running = false; + + protected abstract Future performStart(); + + protected abstract Future performStop(); @Override - public void onConnected(@NotNull final Connection connection) { + public Future start() { + synchronized (startStopMutex) { + if (running) throw new IllegalStateException("This server is already started"); + + running = true; + return performStart(); + } } @Override - public void onDisconnected(@NotNull final Connection connection) { + public Future stop() { + synchronized (startStopMutex) { + if (!running) throw new IllegalStateException("This server is not running"); - } + running = false; + return performStop(); + } + } } diff --git a/protocol/src/main/java/org/feathercore/protocol/server/BaseServer.java b/protocol/src/main/java/org/feathercore/protocol/server/Server.java similarity index 59% rename from protocol/src/main/java/org/feathercore/protocol/server/BaseServer.java rename to protocol/src/main/java/org/feathercore/protocol/server/Server.java index 503f62b..009a76c 100644 --- a/protocol/src/main/java/org/feathercore/protocol/server/BaseServer.java +++ b/protocol/src/main/java/org/feathercore/protocol/server/Server.java @@ -16,35 +16,32 @@ package org.feathercore.protocol.server; -import lombok.AccessLevel; -import lombok.Getter; import lombok.NonNull; -import lombok.RequiredArgsConstructor; import org.feathercore.protocol.Connection; import org.feathercore.protocol.packet.Packet; import org.feathercore.protocol.registry.PacketRegistry; import org.jetbrains.annotations.NotNull; +import java.net.SocketAddress; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; /** - * Created by k.shandurenko on 16/04/2019 + * Base for IO-server based on {@link PacketRegistry}. + * + * @param

super-type of packets used by this server's protocol */ -@Getter -@RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public abstract class BaseServer { - - @NonNull protected final String host; - protected final int port; +public interface Server

{ - public abstract Future start(); + SocketAddress getAddress(); - public abstract Future stop(); + Future start(); - public abstract void onConnected(@NotNull Connection connection); + Future stop(); - public abstract void onDisconnected(@NotNull Connection connection); + @NonNull PacketRegistry

getPacketRegistry(); - public abstract @NonNull PacketRegistry getInitialPacketRegistry(); + default void handleConnect(@NotNull final Connection connection) {} + default void handleDisconnect(@NotNull final Connection connection) {} } diff --git a/protocol/src/main/java/org/feathercore/protocol/server/CommonServer.java b/protocol/src/main/java/org/feathercore/protocol/server/SimpleMinecraftServer.java similarity index 61% rename from protocol/src/main/java/org/feathercore/protocol/server/CommonServer.java rename to protocol/src/main/java/org/feathercore/protocol/server/SimpleMinecraftServer.java index f0eb7e5..20cebaa 100644 --- a/protocol/src/main/java/org/feathercore/protocol/server/CommonServer.java +++ b/protocol/src/main/java/org/feathercore/protocol/server/SimpleMinecraftServer.java @@ -16,26 +16,26 @@ package org.feathercore.protocol.server; +import lombok.Getter; import lombok.NonNull; import org.feathercore.protocol.minecraft.packet.MinecraftPacket; -import org.feathercore.protocol.packet.Packet; +import org.feathercore.protocol.netty.NettyServer; +import org.feathercore.protocol.netty.util.SharedNettyResources; import org.feathercore.protocol.registry.PacketRegistry; import org.feathercore.protocol.registry.common.CommonHandshakePacketRegistry; +import java.net.SocketAddress; + /** * Created by k.shandurenko on 16/04/2019 */ -public class CommonServer extends AbstractServer { - - private final PacketRegistry initialPacketRegistry = CommonHandshakePacketRegistry.createNew(); +@Deprecated +public class SimpleMinecraftServer extends NettyServer { - public CommonServer(final @NonNull String host, final int port) { - super(host, port); - } + @Getter private final PacketRegistry packetRegistry = CommonHandshakePacketRegistry.createNew(); - @Override - public @NonNull PacketRegistry getInitialPacketRegistry() { - return this.initialPacketRegistry; + public SimpleMinecraftServer(@NonNull final SocketAddress address, + @NonNull final SharedNettyResources sharedNettyResources) { + super(address, sharedNettyResources); } - } diff --git a/protocol/src/test/java/org/feathercore/protocol/packet/PacketTypeTest.java b/protocol/src/test/java/org/feathercore/protocol/packet/PacketTypeTest.java new file mode 100644 index 0000000..078eb3d --- /dev/null +++ b/protocol/src/test/java/org/feathercore/protocol/packet/PacketTypeTest.java @@ -0,0 +1,202 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.feathercore.protocol.packet; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.val; +import lombok.var; +import org.apache.commons.lang3.RandomUtils; +import org.feathercore.protocol.annotation.PacketFactory; +import org.feathercore.protocol.annotation.PacketId; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PacketTypeTest { + + @Test + public void testCreateValidations() { + // test that there should be @PacketFactory + assertThrows( + IllegalArgumentException.class, () -> PacketType.create(PacketWithNoFactory.class) + ); + // test that there should be only one @PacketFactory on static method + assertThrows( + IllegalArgumentException.class, () -> PacketType.create(PacketWithMultipleStaticMethodFactories.class) + ); + + } + + @Test + public void testConstructorFactoryCreate() { + val packetType = PacketType.create(PacketWithConstructorFactory.class); + + assertThat(packetType.getId(), is(PacketWithConstructorFactory.ID)); + + val attempts = RandomUtils.nextInt(8, 16 + 1); + for (var i = 0; i < attempts; i++) { + val instanceId = PacketWithConstructorFactory.nextInstanceTestId(); + val packet = packetType.getFactory().get(); + assertThat(packet, notNullValue()); + + assertThat(instanceId, is(packet.instanceId)); + } + } + + @Test + public void testStaticMethodFactoryCreate() { + val packetType = PacketType.create(PacketWithStaticMethodFactory.class); + + assertThat(packetType.getId(), is(PacketWithStaticMethodFactory.ID)); + + val attempts = RandomUtils.nextInt(8, 16 + 1); + for (var i = 0; i < attempts; i++) { + val instanceId = PacketWithStaticMethodFactory.nextInstanceTestId(); + val packet = packetType.getFactory().get(); + assertThat(packet, notNullValue()); + + assertThat(instanceId, is(packet.instanceId)); + } + } + + @Test + public void testSupplierFactoryCreate() { + val instanceIdCounter = new AtomicInteger(); + val packetType = PacketType.create( + PacketWithNoFactory.class, () -> new PacketWithNoFactory(instanceIdCounter.incrementAndGet()) + ); + + assertThat(packetType.getId(), is(PacketWithNoFactory.ID)); + + val attempts = RandomUtils.nextInt(8, 16 + 1); + for (var i = 0; i < attempts; i++) { + val packet = packetType.getFactory().get(); + + assertThat(packet, notNullValue()); + assertThat(packet.instanceId, is(instanceIdCounter.intValue())); + } + } + + @RequiredArgsConstructor + @PacketId(PacketWithNoFactory.ID) + private static final class PacketWithNoFactory implements Packet { + + private static final int ID = 0x10; + + private final int instanceId; + + @Override + public int getId() { + return ID; + } + } + + @PacketId(PacketWithMultipleStaticMethodFactories.ID) + private static final class PacketWithMultipleStaticMethodFactories implements Packet { + + private static final int ID = 0x20; + + @Override + public int getId() { + return ID; + } + + @PacketFactory + public static PacketWithMultipleStaticMethodFactories create1() { + return new PacketWithMultipleStaticMethodFactories(); + } + + @PacketFactory + public static PacketWithMultipleStaticMethodFactories create2() { + return new PacketWithMultipleStaticMethodFactories(); + } + } + + @PacketId(PacketWithConstructorFactory.ID) + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static final class PacketWithConstructorFactory implements Packet { + + private static final ThreadLocal PACKET_TEST_ID_COUNTER = new ThreadLocal<>(); + private static final int ID = 0x50; + + private final int instanceId; + + private static AtomicInteger instanceIdCounter() { + var counter = PACKET_TEST_ID_COUNTER.get(); + if (counter == null) { + counter = new AtomicInteger(); + PACKET_TEST_ID_COUNTER.set(counter); + } + + return counter; + } + + public static int nextInstanceTestId() { + return instanceIdCounter().intValue(); + } + + @PacketFactory + public PacketWithConstructorFactory() { + instanceId = instanceIdCounter().getAndIncrement(); + } + + @Override + public int getId() { + return ID; + } + } + + @PacketId(PacketWithStaticMethodFactory.ID) + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static final class PacketWithStaticMethodFactory implements Packet { + + private static final ThreadLocal PACKET_TEST_ID_COUNTER = new ThreadLocal<>(); + private static final int ID = 0x60; + + private final int instanceId; + + private static AtomicInteger testIdCounter() { + var counter = PACKET_TEST_ID_COUNTER.get(); + if (counter == null) { + counter = new AtomicInteger(); + PACKET_TEST_ID_COUNTER.set(counter); + } + + return counter; + } + + public static int nextInstanceTestId() { + return testIdCounter().intValue(); + } + + @PacketFactory + public static PacketWithStaticMethodFactory create() { + return new PacketWithStaticMethodFactory(testIdCounter().getAndIncrement()); + } + + @Override + public int getId() { + return ID; + } + } +} \ No newline at end of file diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000..726661b --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,47 @@ + + + + 4.0.0 + + feather-core + org.feathercore + 1.0.0-SNAPSHOT + + + feathercore-server + + + + ${project.parent.groupId} + feathercore-protocol + + + ${project.parent.groupId} + feathercore-module-api + + + + org.projectlombok + lombok + + + org.jetbrains + annotations + + + \ No newline at end of file diff --git a/server/src/main/java/AbstractFeatherServer.java b/server/src/main/java/AbstractFeatherServer.java new file mode 100644 index 0000000..bafa873 --- /dev/null +++ b/server/src/main/java/AbstractFeatherServer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import lombok.extern.log4j.Log4j2; +import org.feathercore.protocol.packet.Packet; +import org.feathercore.protocol.server.Server; +import ru.feathercore.moduleapi.Module; +import ru.feathercore.moduleapi.ModuleLoader; + +import javax.annotation.OverridingMethodsMustInvokeSuper; +import java.util.concurrent.ExecutionException; + +/** + * Base for a regular feather-server implementation. + * + * @param

super-type of packets used by the server's protocol + * @param super-type of modules which this server is able to load + */ +@Log4j2 +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) +public abstract class AbstractFeatherServer

implements FeatherServer { + + /** + * IO-server used by this feather-server + */ + @NonNull @Getter Server

ioServer; + /** + * Module-loader used by this server + */ + @NonNull @Getter ModuleLoader moduleLoader; + + @Override + @OverridingMethodsMustInvokeSuper + public void start() { + try { + ioServer.start().get(); + } catch (final InterruptedException | ExecutionException e) { + log.error("An exception occurred while starting the IO-server", e); + } + } + + @Override + @OverridingMethodsMustInvokeSuper + public void close() { + try { + moduleLoader.unloadAllModules(); + } catch (final Throwable e) { + log.error("An exception occurred while unloading modules", e); + } + try { + ioServer.stop().get(); + } catch (final Throwable e) { + log.error("An exception occurred while stopping the IO-server", e); + } + } +} diff --git a/server/src/main/java/FeatherServer.java b/server/src/main/java/FeatherServer.java new file mode 100644 index 0000000..c5439cf --- /dev/null +++ b/server/src/main/java/FeatherServer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.feathercore.protocol.packet.Packet; +import org.feathercore.protocol.server.Server; +import ru.feathercore.moduleapi.Module; +import ru.feathercore.moduleapi.ModuleLoader; + +/** + * feather-core server, core component of the whole project. + * + * @param

super-type of packets used by the server's protocol + * @param super-type of modules which this server is able to load + */ +public interface FeatherServer

extends AutoCloseable { + + /** + * Gets the IO-server used by this feather-server. + * + * @return IO-server used by this feather-server + */ + Server

getIoServer(); + + /** + * Gets the module-loader of this feather-server. + * + * @return module-loader of this feather-server + */ + ModuleLoader getModuleLoader(); + + /** + * Starts this feather-server. + */ + void start(); + + /** + * Shuts down this feather-server. + */ + @Override + void close(); +} diff --git a/server/src/main/java/SimpleMinecraftFeatherServer.java b/server/src/main/java/SimpleMinecraftFeatherServer.java new file mode 100644 index 0000000..209787a --- /dev/null +++ b/server/src/main/java/SimpleMinecraftFeatherServer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import lombok.NonNull; +import org.feathercore.protocol.minecraft.packet.MinecraftPacket; +import org.feathercore.protocol.netty.util.SimpleSharedNettyResources; +import org.feathercore.protocol.server.SimpleMinecraftServer; +import ru.feathercore.moduleapi.Module; +import ru.feathercore.moduleapi.SimpleModuleLoader; + +import java.net.SocketAddress; + +/** + * Simple feather-server for standard minecraft protocol. + * + * @param super-type of modules which this server is able to load + */ +public class SimpleMinecraftFeatherServer extends AbstractFeatherServer { + + /** + * Creates new feather-server for standard minecraft protocol + * + * @param address address of this server used by IO-server + */ + public SimpleMinecraftFeatherServer(@NonNull final SocketAddress address) { + super( + new SimpleMinecraftServer(address, SimpleSharedNettyResources.createDefault()), + SimpleModuleLoader.createConcurrent() + ); + } +} diff --git a/shared/pom.xml b/shared/pom.xml index 155b851..7993e47 100644 --- a/shared/pom.xml +++ b/shared/pom.xml @@ -26,6 +26,16 @@ feathercore-shared + + ru.progrm-jarvis + java-commons + + + + com.google.guava + guava + + com.google.code.gson gson @@ -40,5 +50,14 @@ org.jetbrains annotations + + + org.junit.jupiter + junit-jupiter-api + + + org.hamcrest + hamcrest-all + \ No newline at end of file diff --git a/shared/src/main/java/org/feathercore/shared/object/ValueContainer.java b/shared/src/main/java/org/feathercore/shared/object/ValueContainer.java deleted file mode 100644 index 454a99e..0000000 --- a/shared/src/main/java/org/feathercore/shared/object/ValueContainer.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2019 Feather Core - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.feathercore.shared.object; - -import lombok.*; -import lombok.experimental.Delegate; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Supplier; - -/** - * A value which may be present (which includes {@link null}) or not-present. - *

- * This differs from {@link java.util.Optional} as presence of a {@link null} value - * does not make this value container empty. - */ -public interface ValueContainer extends Supplier { - - /** - * Gets a value container being empty. - * - * @param type of value container - * @return empty value container - */ - @SuppressWarnings("unchecked") - static ValueContainer empty() { - return (ValueContainer) Empty.INSTANCE; - } - - /** - * Gets a non-empty value container containing {@link null}. - * - * @param type of value container - * @return value container containing {@link null} - */ - @SuppressWarnings("unchecked") - static ValueContainer ofNull() { - return (ValueContainer) OfNull.INSTANCE; - } - - /** - * Gets a non-empty value container containing the specified value. - * - * @param type of value container - * @return non-empty value container containing the specified value - */ - @SuppressWarnings("unchecked") - static ValueContainer of(@Nullable final T value) { - return value == null ? (ValueContainer) OfNull.INSTANCE : new Containing<>(value); - } - - /** - * Gets a value container containing the specified value or an empty one if the values is {@link null}. - * - * @param type of value container - * @return non-empty value container containing the specified value - * if it is not {@link null} and an empty one otherwise - */ - @SuppressWarnings("unchecked") - static ValueContainer nonnullOrEmpty(@Nullable final T value) { - return value == null ? (ValueContainer) Empty.INSTANCE : new Containing<>(value); - } - - /** - * {@inheritDoc} - * - * @return {@inheritDoc} - * @throws EmptyValueException if this value container is empty - */ - @Override - T get() throws EmptyValueException; - - /** - * Checks whether the object is present ot not. - * - * @return {@link true} if the value is present (might be {@link null}) and {@link false} otherwise - */ - boolean isPresent(); - - /** - * Gets the value if it is present otherwise using the specified one. - * - * @param value value to use if this value container is empty - * @return this value container's value if it is not empty or the specified value otherwise - */ - @Nullable default T orElse(@Nullable final T value) { - return isPresent() ? get() : value; - } - - /** - * Gets the value if it is present otherwise using the one got from the specified supplier. - * - * @param valueSupplier supplier to be used for getting the value if this value container is empty - * @return this value container's value if it is not empty or the one got from the supplier otherwise - */ - @Nullable default T orElseGet(@NonNull final Supplier valueSupplier) { - return isPresent() ? get() : valueSupplier.get(); - } - - /** - * Gets the value if it is present otherwise throwing an exception. - * - * @param exceptionSupplier supplier to be used for getting the exception if this value container is empty - * @param type of an exception thrown if this value container is empty - * @return this value container's value if it is not empty - * @throws X if this value container is empty - */ - @Nullable default T orElseThrow(@NonNull final Supplier<@NotNull X> exceptionSupplier) - throws X { - if (isPresent()) { - return get(); - } - - throw exceptionSupplier.get(); - } - - // use default equals and hashCode - @NoArgsConstructor(access = AccessLevel.PRIVATE) - final class Empty implements ValueContainer { - - private static final Empty INSTANCE = new Empty<>(); - - @Override - public T get() throws EmptyValueException { - throw new EmptyValueException("There is no value associated with this value container"); - } - - @Override - public boolean isPresent() { - return false; - } - - @Override - public String toString() { - return "Empty ValueContainer"; - } - } - - // use default equals and hashCode - @NoArgsConstructor(access = AccessLevel.PRIVATE) - final class OfNull implements ValueContainer { - - private static final OfNull INSTANCE = new OfNull<>(); - - @Override - public T get() throws EmptyValueException { - return null; - } - - @Override - public boolean isPresent() { - return true; - } - - @Override - public String toString() { - return "ValueContainer{null}"; - } - } - - @Value - @RequiredArgsConstructor(access = AccessLevel.PRIVATE) - class Containing implements ValueContainer { - - @NonNull private final T value; - - @Override - public T get() throws EmptyValueException { - return value; - } - - @Override - public boolean isPresent() { - return true; - } - } - - @Value - @RequiredArgsConstructor(access = AccessLevel.PRIVATE) - class Supplying implements ValueContainer { - - @NonNull @Delegate private final Supplier valueSupplier; - - @Override - public boolean isPresent() { - return true; - } - } - - /** - * An exception thrown whenever {@link #get()} is called on an empty value container. - */ - @NoArgsConstructor - class EmptyValueException extends RuntimeException { - // - public EmptyValueException(final String message) { - super(message); - } - - public EmptyValueException(final String message, final Throwable cause) { - super(message, cause); - } - - public EmptyValueException(final Throwable cause) { - super(cause); - } - - public EmptyValueException(final String message, final Throwable cause, final boolean enableSuppression, - final boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - // - } -} diff --git a/shared/src/main/java/org/feathercore/shared/util/NamedThreadFactory.java b/shared/src/main/java/org/feathercore/shared/util/NamedThreadFactory.java deleted file mode 100644 index acb3be4..0000000 --- a/shared/src/main/java/org/feathercore/shared/util/NamedThreadFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2019 Feather Core - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.feathercore.shared.util; - -import lombok.*; -import lombok.experimental.FieldDefaults; - -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Created by k.shandurenko on 12/04/2019 - */ -@ToString -@EqualsAndHashCode -@RequiredArgsConstructor -@FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) -public class NamedThreadFactory implements ThreadFactory { - - @NonNull String name; - boolean daemon; - @NonNull AtomicInteger counter = new AtomicInteger(1); - - @Override - public Thread newThread(@NonNull final Runnable runnable) { - val thread = new Thread(runnable); - thread.setDaemon(daemon); - thread.setName(name + counter.getAndIncrement()); - - return thread; - } -} \ No newline at end of file diff --git a/shared/src/main/java/org/feathercore/shared/util/thread/ThreadFactories.java b/shared/src/main/java/org/feathercore/shared/util/thread/ThreadFactories.java new file mode 100644 index 0000000..3fa7f9f --- /dev/null +++ b/shared/src/main/java/org/feathercore/shared/util/thread/ThreadFactories.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019 Feather Core + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.feathercore.shared.util.thread; + +import lombok.NonNull; +import lombok.experimental.UtilityClass; +import lombok.val; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Utility-methods used for creation of typical {@link ThreadFactory thread factories}. + * + * @author Dimasik + * @author PROgrammer_JARvis + */ +@UtilityClass +public class ThreadFactories { + + /** + * Creates a {@link ThreadFactory thread factory} which picks names of created threads using a counter. + * + * @param prefix prefix of the thread name + * @param suffix suffix of the thread name + * @param daemon daemon-flag of the thread + * @return created thread factory + * + * @see #createPaginated(String, boolean) simpler alternative + */ + public ThreadFactory createPaginated(@NonNull final String prefix, + @NonNull final String suffix, + @NonNull final boolean daemon) { + // intialized here so that it gets captured by lambda + val counter = new AtomicInteger(); + + return (runnable) -> { + val thread = new Thread(runnable); + thread.setDaemon(daemon); + thread.setName(prefix + counter.getAndIncrement() + suffix); + + return thread; + }; + } + + /** + * Creates a {@link ThreadFactory thread factory} which picks names of created threads using a counter. + * + * @param prefix prefix of the thread name + * @param daemon daemon-flag of the thread + * @return created thread factory + * + * @see #createPaginated(String, String, boolean) alternative method with richer naming control + */ + public ThreadFactory createPaginated(@NonNull final String prefix, + @NonNull final boolean daemon) { + return createPaginated(prefix, "", daemon); + } +}