commit 03d1adf981fbfda2a660b6d467d19558032f32ae Author: JesseBrault0709 <62299747+JesseBrault0709@users.noreply.github.com> Date: Fri May 3 12:01:45 2024 +0200 Initial commit. Woot! diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5087da5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'GroowtConventions' + id 'java-library' +} + +dependencies { + api libs.jakarta.inject + compileOnlyApi libs.jetbrains.anotations + implementation libs.slf4j.api, libs.groovy +} diff --git a/src/main/java/groowt/util/di/AbstractInjectingObjectFactory.java b/src/main/java/groowt/util/di/AbstractInjectingObjectFactory.java new file mode 100644 index 0000000..679c53f --- /dev/null +++ b/src/main/java/groowt/util/di/AbstractInjectingObjectFactory.java @@ -0,0 +1,197 @@ +package groowt.util.di; + +import jakarta.inject.Inject; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.*; +import java.util.*; + +import static groowt.util.di.ObjectFactoryUtil.toTypes; + +// TODO: maybe inject fields +public abstract class AbstractInjectingObjectFactory implements ObjectFactory { + + protected record CachedInjectConstructor(Class clazz, Constructor constructor) {} + + protected record CachedNonInjectConstructor( + Class clazz, + Constructor constructor, + Class[] paramTypes + ) {} + + private final Map, Constructor[]> cachedAllConstructors = new HashMap<>(); + private final Collection> cachedInjectConstructors = new ArrayList<>(); + private final Collection> cachedNonInjectConstructors = new ArrayList<>(); + private final Map, Collection> cachedSetters = new HashMap<>(); + private final Map cachedSetterParameters = new HashMap<>(); + + @SuppressWarnings("unchecked") + private @Nullable Constructor findCachedInjectConstructor(Class clazz) { + for (final CachedInjectConstructor cachedConstructor : this.cachedInjectConstructors) { + if (clazz.equals(cachedConstructor.clazz())) { + return (Constructor) cachedConstructor.constructor(); + } + } + return null; + } + + /** + * @implNote If overridden, please cache any found inject constructors using {@link #putCachedInjectConstructor}. + * + * @param clazz the {@link Class} in which to search for an {@literal @}Inject annotated constructor. + * @return the inject constructor, or {@code null} if none found. + * @param the type of the class + */ + @SuppressWarnings("unchecked") + protected @Nullable Constructor findInjectConstructor(Class clazz) { + final Constructor cachedInjectConstructor = this.findCachedInjectConstructor(clazz); + if (cachedInjectConstructor != null) { + return cachedInjectConstructor; + } + + final Constructor[] constructors = this.cachedAllConstructors.computeIfAbsent(clazz, Class::getConstructors); + + final List> injectConstructors = Arrays.stream(constructors) + .filter(constructor -> constructor.isAnnotationPresent(Inject.class)) + .toList(); + + if (injectConstructors.size() > 1) { + // one day maybe support multiple inject constructors + throw new UnsupportedOperationException("Cannot have more than one @Inject constructor in class: " + clazz); + } else if (injectConstructors.size() == 1) { + final Constructor injectConstructor = (Constructor) injectConstructors.getFirst(); + this.putCachedInjectConstructor(new CachedInjectConstructor<>(clazz, injectConstructor)); + return injectConstructor; + } else { + return null; + } + } + + protected final void putCachedInjectConstructor(CachedInjectConstructor cached) { + this.cachedInjectConstructors.add(cached); + } + + @SuppressWarnings("unchecked") + private @Nullable Constructor findCachedNonInjectConstructor(Class clazz, Class[] paramTypes) { + for (final CachedNonInjectConstructor cachedConstructor : this.cachedNonInjectConstructors) { + if (clazz.equals(cachedConstructor.clazz()) && Arrays.equals(cachedConstructor.paramTypes(), paramTypes)) { + return (Constructor) cachedConstructor.constructor(); + } + } + return null; + } + + /** + * @implNote If overridden, please cache any found non-inject constructors using {@link #putCachedNonInjectConstructor}. + * + * @param clazz the {@link Class} in which to search for a constructor which does not have an {@literal @}Inject + * annotation + * @param constructorArgs the given constructor args + * @return the found non-inject constructor appropriate for the given constructor args, or {@code null} if no + * such constructor exists + * @param the type + */ + @SuppressWarnings("unchecked") + protected @Nullable Constructor findNonInjectConstructor(Class clazz, Object[] constructorArgs) { + final Class[] types = toTypes(constructorArgs); + final Constructor cachedConstructor = this.findCachedNonInjectConstructor(clazz, types); + if (cachedConstructor != null) { + return cachedConstructor; + } + + final Constructor[] constructors = this.cachedAllConstructors.computeIfAbsent(clazz, Class::getConstructors); + for (Constructor constructor : constructors) { + if (Arrays.equals(constructor.getParameterTypes(), types)) { + final Constructor found = (Constructor) constructor; + this.putCachedNonInjectConstructor(new CachedNonInjectConstructor<>(clazz, found, types)); + return found; + } + } + return null; + } + + protected final void putCachedNonInjectConstructor(CachedNonInjectConstructor cached) { + this.cachedNonInjectConstructors.add(cached); + } + + /** + * @implNote Please call {@code super.findConstructor()} first, and then implement custom + * constructor finding logic. If the custom logic finds a constructor, please cache it + * using either {@link #putCachedNonInjectConstructor} or {@link #putCachedInjectConstructor}. + */ + protected Constructor findConstructor(Class clazz, Object[] args) { + final Constructor injectConstructor = this.findInjectConstructor(clazz); + if (injectConstructor != null) { + return injectConstructor; + } + final Constructor nonInjectConstructor = this.findNonInjectConstructor(clazz, args); + if (nonInjectConstructor != null) { + return nonInjectConstructor; + } + throw new RuntimeException("Could not find an appropriate constructor for " + clazz.getName() + + " with args " + Arrays.toString(toTypes(args)) + ); + } + + protected Collection getAllInjectSetters(Class clazz) { + final Method[] allMethods = clazz.getMethods(); + final Collection injectSetters = new ArrayList<>(); + for (final var method : allMethods) { + if ( + method.isAnnotationPresent(Inject.class) + && method.getName().startsWith("set") + && !Modifier.isStatic(method.getModifiers()) + && method.getParameterCount() == 1 + ) { + injectSetters.add(method); + } + } + return injectSetters; + } + + protected Collection getCachedSettersFor(Object target) { + return this.cachedSetters.computeIfAbsent(target.getClass(), this::getAllInjectSetters); + } + + protected Parameter getCachedInjectParameter(Method setter) { + return this.cachedSetterParameters.computeIfAbsent(setter, s -> { + if (s.getParameterCount() != 1) { + throw new IllegalArgumentException("Setter " + s.getName() + " has a parameter count other than one (1)!"); + } + return s.getParameters()[0]; + }); + } + + protected void injectSetter(Object target, Method setter) { + try { + setter.invoke(target, this.getSetterInjectArg(target.getClass(), setter, this.getCachedInjectParameter(setter))); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + protected void injectSetters(Object target) { + this.getCachedSettersFor(target).forEach(setter -> this.injectSetter(target, setter)); + } + + /** + * {@inheritDoc} + */ + @Override + public T createInstance(Class clazz, Object... constructorArgs) { + final Constructor constructor = this.findConstructor(clazz, constructorArgs); + final Object[] allArgs = this.createArgs(constructor, constructorArgs); + try { + final T instance = constructor.newInstance(allArgs); + this.injectSetters(instance); + return instance; + } catch (InvocationTargetException | IllegalAccessException | InstantiationException e) { + throw new RuntimeException(e); // In the future, we might have an option to ignore exceptions + } + } + + protected abstract Object[] createArgs(Constructor constructor, Object[] constructorArgs); + + protected abstract Object getSetterInjectArg(Class targetType, Method setter, Parameter toInject); + +} diff --git a/src/main/java/groowt/util/di/AbstractRegistryObjectFactory.java b/src/main/java/groowt/util/di/AbstractRegistryObjectFactory.java new file mode 100644 index 0000000..70fa706 --- /dev/null +++ b/src/main/java/groowt/util/di/AbstractRegistryObjectFactory.java @@ -0,0 +1,113 @@ +package groowt.util.di; + +import groowt.util.di.filters.FilterHandler; +import groowt.util.di.filters.IterableFilterHandler; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static groowt.util.di.RegistryObjectFactoryUtil.orElseSupply; + +public abstract class AbstractRegistryObjectFactory extends AbstractInjectingObjectFactory implements RegistryObjectFactory { + + public static abstract class AbstractBuilder implements Builder { + + private final Collection> filterHandlers = new ArrayList<>(); + private final Collection> iterableFilterHandlers = new ArrayList<>(); + private final Registry registry; + private @Nullable RegistryObjectFactory parent; + + public AbstractBuilder(Registry registry) { + this.registry = registry; + } + + public AbstractBuilder() { + this.registry = new DefaultRegistry(); + } + + protected Registry getRegistry() { + return this.registry; + } + + protected Collection> getFilterHandlers() { + return this.filterHandlers; + } + + protected Collection> getIterableFilterHandlers() { + return this.iterableFilterHandlers; + } + + protected @Nullable RegistryObjectFactory getParent() { + return this.parent; + } + + @Override + public void configureRegistry(Consumer configure) { + configure.accept(this.registry); + } + + public void addFilterHandler(FilterHandler handler) { + this.filterHandlers.add(handler); + } + + public void addIterableFilterHandler(IterableFilterHandler handler) { + this.iterableFilterHandlers.add(handler); + } + + public void setParent(@Nullable RegistryObjectFactory parent) { + this.parent = parent; + } + + } + + protected final Registry registry; + @Nullable private final RegistryObjectFactory parent; + + public AbstractRegistryObjectFactory(Registry registry, @Nullable RegistryObjectFactory parent) { + this.registry = registry; + this.parent = parent; + } + + @Override + public void configureRegistry(Consumer use) { + use.accept(this.registry); + } + + @Override + public @Nullable ScopeHandler findScopeHandler(Class scopeType) { + return this.registry.getScopeHandler(scopeType); + } + + @Override + public @Nullable QualifierHandler findQualifierHandler(Class qualifierType) { + return this.registry.getQualifierHandler(qualifierType); + } + + protected final Optional findInParent(Function finder) { + return this.parent != null ? Optional.ofNullable(finder.apply(this.parent)) : Optional.empty(); + } + + protected final Optional findInSelfOrParent(Function finder) { + return orElseSupply( + finder.apply(this), + () -> this.parent != null ? finder.apply(this.parent) : null + ); + } + + protected final T getInSelfOrParent( + Function finder, + Supplier exceptionSupplier + ) { + return orElseSupply( + finder.apply(this), + () -> this.parent != null ? finder.apply(this.parent) : null + ).orElseThrow(exceptionSupplier); + } + +} diff --git a/src/main/java/groowt/util/di/Binding.java b/src/main/java/groowt/util/di/Binding.java new file mode 100644 index 0000000..d9b6ccb --- /dev/null +++ b/src/main/java/groowt/util/di/Binding.java @@ -0,0 +1,3 @@ +package groowt.util.di; + +sealed public interface Binding permits ClassBinding, ProviderBinding, SingletonBinding, LazySingletonBinding {} diff --git a/src/main/java/groowt/util/di/BindingConfigurator.java b/src/main/java/groowt/util/di/BindingConfigurator.java new file mode 100644 index 0000000..4352bc0 --- /dev/null +++ b/src/main/java/groowt/util/di/BindingConfigurator.java @@ -0,0 +1,12 @@ +package groowt.util.di; + +import jakarta.inject.Provider; + +import java.util.function.Supplier; + +public interface BindingConfigurator { + void to(Class target); + void toProvider(Provider provider); + void toSingleton(T target); + void toLazySingleton(Supplier singletonSupplier); +} diff --git a/src/main/java/groowt/util/di/BindingUtil.java b/src/main/java/groowt/util/di/BindingUtil.java new file mode 100644 index 0000000..8c4fcf0 --- /dev/null +++ b/src/main/java/groowt/util/di/BindingUtil.java @@ -0,0 +1,32 @@ +package groowt.util.di; + +import jakarta.inject.Provider; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class BindingUtil { + + public static Consumer> toClass(Class clazz) { + return bc -> bc.to(clazz); + } + + public static Consumer> toProvider(Provider provider) { + return bc -> bc.toProvider(provider); + } + + public static Consumer> toSingleton(T singleton) { + return bc -> bc.toSingleton(singleton); + } + + public static Consumer> toLazySingleton(Supplier singletonSupplier) { + return bc -> bc.toLazySingleton(singletonSupplier); + } + + public static Consumer> toSelf() { + return bc -> {}; + } + + private BindingUtil() {} + +} diff --git a/src/main/java/groowt/util/di/ClassBinding.java b/src/main/java/groowt/util/di/ClassBinding.java new file mode 100644 index 0000000..b01f909 --- /dev/null +++ b/src/main/java/groowt/util/di/ClassBinding.java @@ -0,0 +1,3 @@ +package groowt.util.di; + +public record ClassBinding(Class from, Class to) implements Binding {} diff --git a/src/main/java/groowt/util/di/DefaultNamedRegistryExtension.java b/src/main/java/groowt/util/di/DefaultNamedRegistryExtension.java new file mode 100644 index 0000000..22413de --- /dev/null +++ b/src/main/java/groowt/util/di/DefaultNamedRegistryExtension.java @@ -0,0 +1,80 @@ +package groowt.util.di; + +import jakarta.inject.Named; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class DefaultNamedRegistryExtension implements NamedRegistryExtension { + + protected static class NamedQualifierHandler implements QualifierHandler { + + private final DefaultNamedRegistryExtension extension; + + public NamedQualifierHandler(DefaultNamedRegistryExtension extension) { + this.extension = extension; + } + + @Override + public @Nullable Binding handle(Named named, Class dependencyClass) { + return this.extension.getBinding( + new SimpleKeyHolder<>(NamedRegistryExtension.class, dependencyClass, named.value()) + ); + } + + } + + protected final Map> bindings = new HashMap<>(); + protected final QualifierHandler qualifierHandler = this.getNamedQualifierHandler(); + + protected QualifierHandler getNamedQualifierHandler() { + return new NamedQualifierHandler(this); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable QualifierHandler getQualifierHandler(Class qualifierType) { + return Named.class.equals(qualifierType) ? (QualifierHandler) this.qualifierHandler : null; + } + + @Override + public Class getKeyClass() { + return String.class; + } + + @Override + public , T> void bind(KeyHolder keyHolder, Consumer> configure) { + final var configurator = new SimpleBindingConfigurator<>(keyHolder.type()); + configure.accept(configurator); + this.bindings.put(keyHolder.key(), configurator.getBinding()); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable , T> Binding getBinding(KeyHolder keyHolder) { + return (Binding) this.bindings.getOrDefault(keyHolder.key(), null); + } + + @Override + public , T> void removeBinding(KeyHolder keyHolder) { + this.bindings.remove(keyHolder.key()); + } + + @Override + public , T> void removeBindingIf(KeyHolder keyHolder, Predicate> filter) { + final String key = keyHolder.key(); + if (this.bindings.containsKey(key) && filter.test(this.getBinding(keyHolder))) { + this.bindings.remove(key); + } + } + + @Override + public void clearAllBindings() { + this.bindings.clear(); + } + +} diff --git a/src/main/java/groowt/util/di/DefaultRegistry.java b/src/main/java/groowt/util/di/DefaultRegistry.java new file mode 100644 index 0000000..419f5e0 --- /dev/null +++ b/src/main/java/groowt/util/di/DefaultRegistry.java @@ -0,0 +1,200 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public class DefaultRegistry implements Registry { + + protected record ClassKeyBinding(Class key, Binding binding) {} + + protected final Collection> classBindings = new ArrayList<>(); + protected final Collection extensions = new ArrayList<>(); + + @Override + public void removeBinding(Class key) { + this.classBindings.removeIf(classKeyBinding -> classKeyBinding.key().equals(key)); + } + + @SuppressWarnings("unchecked") + @Override + public void removeBindingIf(Class key, Predicate> filter) { + this.classBindings.removeIf(classKeyBinding -> + classKeyBinding.key().equals(key) && filter.test((Binding) classKeyBinding.binding()) + ); + } + + private List getAllRegistryExtensions(Class extensionType) { + return this.extensions.stream() + .filter(extension -> extensionType.isAssignableFrom(extension.getClass())) + .map(extensionType::cast) + .toList(); + } + + private E getOneRegistryExtension(Class extensionType) { + final List extensions = this.getAllRegistryExtensions(extensionType); + if (extensions.size() == 1) { + return extensions.getFirst(); + } else if (extensions.isEmpty()) { + throw new IllegalArgumentException("There is no " + extensionType + " registered for this " + this); + } else { + throw new IllegalArgumentException("There is more than one " + extensionType + " registered for this " + this); + } + } + + @Override + public void addExtension(RegistryExtension extension) { + final List existing = this.getAllRegistryExtensions(extension.getClass()); + if (existing.isEmpty()) { + this.extensions.add(extension); + } else { + throw new IllegalArgumentException("There is already at least one " + extension.getClass() + " registered in " + this); + } + } + + @Override + public E getExtension(Class extensionType) { + return this.getOneRegistryExtension(extensionType); + } + + @Override + public Collection getExtensions(Class extensionType) { + return this.getAllRegistryExtensions(extensionType); + } + + @Override + public void removeExtension(RegistryExtension extension) { + this.extensions.remove(extension); + } + + @Override + public @Nullable QualifierHandler getQualifierHandler(Class qualifierType) { + final List> handlers = new ArrayList<>(); + for (final var extension : this.extensions) { + if (extension instanceof QualifierHandlerContainer handlerContainer) { + final var handler = handlerContainer.getQualifierHandler(qualifierType); + if (handler != null) { + handlers.add(handler); + } + } + } + if (handlers.isEmpty()) { + return null; + } else if (handlers.size() > 1) { + throw new RuntimeException("There is more than one QualifierHandler for " + qualifierType.getName()); + } else { + return handlers.getFirst(); + } + } + + @Override + public @Nullable ScopeHandler getScopeHandler(Class scopeType) { + final List> handlers = new ArrayList<>(); + for (final var extension : this.extensions) { + if (extension instanceof ScopeHandlerContainer handlerContainer) { + final var handler = handlerContainer.getScopeHandler(scopeType); + if (handler != null) { + handlers.add(handler); + } + } + } + if (handlers.isEmpty()) { + return null; + } else if (handlers.size() > 1) { + throw new RuntimeException("There is more than one ScopeHandler for " + scopeType.getName()); + } else { + return handlers.getFirst(); + } + } + + @Override + public void bind(Class key, Consumer> configure) { + final var configurator = new SimpleBindingConfigurator<>(key); + configure.accept(configurator); + this.classBindings.add(new ClassKeyBinding<>(key, configurator.getBinding())); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable Binding getBinding(Class key) { + for (final var classKeyBinding : this.classBindings) { + if (key.isAssignableFrom(classKeyBinding.key())) { + return (Binding) classKeyBinding.binding(); + } + } + return null; + } + + private KeyBinder findKeyBinder(Class keyClass) { + final List> binders = new ArrayList<>(); + for (final var extension : this.extensions) { + if (extension instanceof KeyBinder keyBinder && keyBinder.getKeyClass().isAssignableFrom(keyClass)) { + binders.add(keyBinder); + } + } + if (binders.isEmpty()) { + throw new IllegalArgumentException("There are no configured RegistryExtensions that can handle keys with type " + keyClass.getName()); + } else if (binders.size() > 1) { + throw new IllegalArgumentException("There is more than one configured RegistryExtension that can handle keys with type " + keyClass.getName()); + } else { + return binders.getFirst(); + } + } + + @SuppressWarnings("rawtypes") + protected final void withKeyBinder(KeyHolder keyHolder, Consumer action) { + action.accept(this.findKeyBinder(keyHolder.key().getClass())); + } + + @SuppressWarnings("rawtypes") + protected final @Nullable R tapKeyBinder( + KeyHolder keyHolder, + Function function + ) { + return function.apply(this.findKeyBinder(keyHolder.key().getClass())); + } + + @SuppressWarnings("unchecked") + @Override + public void bind(KeyHolder keyHolder, Consumer> configure) { + this.withKeyBinder(keyHolder, b -> b.bind(keyHolder, configure)); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable Binding getBinding(KeyHolder keyHolder) { + return this.tapKeyBinder(keyHolder, b -> b.getBinding(keyHolder)); + } + + @SuppressWarnings("unchecked") + @Override + public void removeBinding(KeyHolder keyHolder) { + this.withKeyBinder(keyHolder, b -> b.removeBinding(keyHolder)); + } + + @SuppressWarnings("unchecked") + @Override + public void removeBindingIf( + KeyHolder keyHolder, + Predicate> filter + ) { + this.withKeyBinder(keyHolder, b -> b.removeBindingIf(keyHolder, filter)); + } + + @Override + public void clearAllBindings() { + this.classBindings.clear(); + for (final var extension : this.extensions) { + if (extension instanceof KeyBinder keyBinder) { + keyBinder.clearAllBindings(); + } + } + } + +} diff --git a/src/main/java/groowt/util/di/DefaultRegistryObjectFactory.java b/src/main/java/groowt/util/di/DefaultRegistryObjectFactory.java new file mode 100644 index 0000000..422c146 --- /dev/null +++ b/src/main/java/groowt/util/di/DefaultRegistryObjectFactory.java @@ -0,0 +1,313 @@ +package groowt.util.di; + +import groowt.util.di.filters.FilterHandler; +import groowt.util.di.filters.IterableFilterHandler; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import static groowt.util.di.RegistryObjectFactoryUtil.*; + +public class DefaultRegistryObjectFactory extends AbstractRegistryObjectFactory { + + public static final class Builder extends AbstractRegistryObjectFactory.AbstractBuilder { + + /** + * Creates a {@code Builder} initialized with a {@link DefaultRegistry}, which is in-turn configured with a + * {@link NamedRegistryExtension} and a {@link SingletonScopeHandler}. + * + * @return the builder + */ + public static Builder withDefaults() { + final var b = new Builder(); + + b.configureRegistry(r -> { + r.addExtension(new DefaultNamedRegistryExtension()); + r.addExtension(new SingletonRegistryExtension(r)); + }); + + return b; + } + + /** + * @return a blank builder with a {@link Registry} from the given {@link Supplier}. + */ + public static Builder withRegistry(Supplier registrySupplier) { + return new Builder(registrySupplier.get()); + } + + /** + * @return a blank builder which will use {@link DefaultRegistry}. + */ + public static Builder blank() { + return new Builder(); + } + + private Builder(Registry registry) { + super(registry); + } + + private Builder() { + super(); + } + + @Override + public DefaultRegistryObjectFactory build() { + return new DefaultRegistryObjectFactory( + this.getRegistry(), + this.getParent(), + this.getFilterHandlers(), + this.getIterableFilterHandlers() + ); + } + + } + + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + private static final Logger logger = LoggerFactory.getLogger(DefaultRegistryObjectFactory.class); // leave it for the future! + + private final Collection> filterHandlers; + private final Collection> iterableFilterHandlers; + + protected DefaultRegistryObjectFactory( + Registry registry, + @Nullable RegistryObjectFactory parent, + Collection> filterHandlers, + Collection> iterableFilterHandlers + ) { + super(registry, parent); + this.filterHandlers = new ArrayList<>(filterHandlers); + this.filterHandlers.forEach(handler -> checkIsValidFilter(handler.getAnnotationClass())); + + this.iterableFilterHandlers = new ArrayList<>(iterableFilterHandlers); + this.iterableFilterHandlers.forEach(handler -> checkIsValidIterableFilter(handler.getAnnotationClass())); + } + + /** + * Checks if the given parameter has any qualifier annotations; if it does, + * it delegates finding the desired object to the registered {@link QualifierHandler}. + * + * @param parameter the parameter + * @return the object returned from the {@code QualifierHandler}, or {@code null} if no qualifier + * is present or the {@code QualifierHandler} itself returns {@code null}. + * + * @throws RuntimeException if no {@code QualifierHandler} is registered for a qualifier annotation present on the + * given parameter, or if the handler itself throws an exception. + */ + @SuppressWarnings("unchecked") + protected final @Nullable Object tryQualifiers(Parameter parameter) { + final Class paramType = parameter.getType(); + final List qualifiers = RegistryObjectFactoryUtil.getQualifierAnnotations(parameter.getAnnotations()); + if (qualifiers.size() > 1) { + throw new RuntimeException("Parameter " + parameter + " cannot have more than one Qualifier annotation."); + } else if (qualifiers.size() == 1) { + final Annotation qualifier = qualifiers.getFirst(); + @SuppressWarnings("rawtypes") + final QualifierHandler handler = this.getInSelfOrParent( + f -> f.findQualifierHandler(qualifier.annotationType()), + () -> new RuntimeException("There is no configured QualifierHandler for " + qualifier.annotationType().getName()) + ); + final Binding binding = handler.handle(qualifier, paramType); + if (binding != null) { + return this.handleBinding(binding, EMPTY_OBJECT_ARRAY); + } + } + // no Qualifier or the QualifierHandler didn't return a Binding + return null; + } + + /** + * Checks the {@code resolvedArg} against all filters present on the given parameter. + * + * @param parameter the parameter + * @param resolvedArg the resolved argument + * + * @throws RuntimeException if the {@link FilterHandler} itself throws an exception. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected final void checkFilters(Parameter parameter, Object resolvedArg) { + final Annotation[] allAnnotations = parameter.getAnnotations(); + final Collection filterAnnotations = getFilterAnnotations(allAnnotations); + if (!filterAnnotations.isEmpty()) { + final Collection> filtersForParamType = this.filterHandlers.stream() + .filter(filterHandler -> + filterHandler.getArgumentClass().isAssignableFrom(parameter.getType()) + ) + .toList(); + for (final Annotation filterAnnotation : filterAnnotations) { + for (final FilterHandler filterHandler : filtersForParamType) { + if (filterAnnotation.annotationType().equals(filterHandler.getAnnotationClass())) { + // hopefully we've checked everything + ((FilterHandler) filterHandler).check(filterAnnotation, resolvedArg); + } + } + } + } + final Collection iterableFilterAnnotations = getIterableFilterAnnotations(allAnnotations); + if (!iterableFilterAnnotations.isEmpty() && resolvedArg instanceof Iterable iterable) { + for (final var annotation : iterableFilterAnnotations) { + this.iterableFilterHandlers.stream() + .filter(handler -> handler.getAnnotationClass().equals(annotation.annotationType())) + .forEach(handler -> { + ((IterableFilterHandler) handler).check(annotation, iterable); + }); + } + } + } + + protected final Object resolveInjectedArg(Parameter parameter) { + final Object qualifierProvidedArg = this.tryQualifiers(parameter); + if (qualifierProvidedArg != null) { + this.checkFilters(parameter, qualifierProvidedArg); + return qualifierProvidedArg; + } else { + final Object created = this.get(parameter.getType()); + this.checkFilters(parameter, created); + return created; + } + } + + protected final void resolveInjectedArgs(Object[] dest, Parameter[] params) { + for (int i = 0; i < params.length; i++) { + dest[i] = this.resolveInjectedArg(params[i]); + } + } + + protected final void resolveGivenArgs(Object[] dest, Parameter[] params, Object[] givenArgs, int startIndex) { + for (int i = startIndex; i < dest.length; i++) { + final int resolveIndex = i - startIndex; + final Object arg = givenArgs[resolveIndex]; + this.checkFilters(params[resolveIndex], arg); + dest[i] = arg; + } + } + + // TODO: when there is a null arg, we lose the type. Therefore this algorithm breaks. Fix this. + @Override + protected Object[] createArgs(Constructor constructor, Object[] givenArgs) { + final Class[] paramTypes = constructor.getParameterTypes(); + + // check no arg + if (paramTypes.length == 0 && givenArgs.length == 0) { + // no args given, none needed, so return empty array + return EMPTY_OBJECT_ARRAY; + } else if (paramTypes.length == 0) { // implicit that givenArgs.length != 0 + // zero expected, but got given args + throw new RuntimeException( + "Expected zero args for constructor " + constructor + " but received " + Arrays.toString(givenArgs) + ); + } else if (givenArgs.length > paramTypes.length) { + // expected is more than zero, but received too many given + throw new RuntimeException( + "Too many args given for constructor " + constructor + "; received " + Arrays.toString(givenArgs) + ); + } + + final Parameter[] allParams = constructor.getParameters(); + final Object[] resolvedArgs = new Object[allParams.length]; + + if (givenArgs.length == 0) { + // if no given args, then they are all injected + this.resolveInjectedArgs(resolvedArgs, allParams); + } else if (givenArgs.length == paramTypes.length) { + // all are given + this.resolveGivenArgs(resolvedArgs, allParams, givenArgs, 0); + } else { + // some are injected, some are given + // everything before (non-inclusive) is injected + // everything after (inclusive) is given + // ex: 1 inject, 1 given -> 2 (allParams) - 1 = 1 + // ex: 0 inject, 1 given -> 1 - 1 = 0 + final int firstGivenIndex = allParams.length - givenArgs.length; + + final Parameter[] injectedParams = new Parameter[firstGivenIndex]; + final Parameter[] givenParams = new Parameter[allParams.length - firstGivenIndex]; + + System.arraycopy(allParams, 0, injectedParams, 0, injectedParams.length); + System.arraycopy(allParams, firstGivenIndex, givenParams, 0, allParams.length - firstGivenIndex); + + this.resolveInjectedArgs(resolvedArgs, injectedParams); + this.resolveGivenArgs(resolvedArgs, givenParams, givenArgs, firstGivenIndex); + } + + return resolvedArgs; + } + + @SuppressWarnings("unchecked") + private T handleBinding(Binding binding, Object[] constructorArgs) { + return switch (binding) { + case ClassBinding(Class ignored, Class to) -> { + final Annotation scopeAnnotation = getScopeAnnotation(to); + if (scopeAnnotation != null) { + final Class scopeClass = scopeAnnotation.annotationType(); + @SuppressWarnings("rawtypes") + final ScopeHandler scopeHandler = this.getInSelfOrParent( + f -> f.findScopeHandler(scopeClass), + () -> new RuntimeException("There is no configured ScopeHandler for " + scopeClass.getName()) + ); + final Binding scopedBinding = scopeHandler.onScopedDependencyRequest(scopeAnnotation, to, this); + yield this.handleBinding(scopedBinding, constructorArgs); + } else { + yield this.createInstance(to, constructorArgs); + } + } + case ProviderBinding providerBinding -> providerBinding.provider().get(); + case SingletonBinding singletonBinding -> singletonBinding.to(); + case LazySingletonBinding lazySingletonBinding -> lazySingletonBinding.singletonSupplier().get(); + }; + } + + protected final @Nullable Binding searchRegistry(Class from) { + return this.registry.getBinding(from); + } + + protected @Nullable T tryParent(Class clazz, Object[] constructorArgs) { + return this.findInParent(f -> f.getOrNull(clazz, constructorArgs)).orElse(null); + } + + @Override + protected Object getSetterInjectArg(Class targetType, Method setter, Parameter toInject) { + return this.resolveInjectedArg(toInject); + } + + /** + * {@inheritDoc} + */ + @Override + public T get(Class clazz, Object... constructorArgs) { + final Binding binding = this.searchRegistry(clazz); + if (binding != null) { + return this.handleBinding(binding, constructorArgs); + } + final T parentResult = this.tryParent(clazz, constructorArgs); + if (parentResult != null) { + return parentResult; + } else { + throw new RuntimeException("No bindings for " + clazz + " with args " + Arrays.toString(constructorArgs) + "."); + } + } + + /** + * {@inheritDoc} + */ + @Override + public T getOrDefault(Class clazz, T defaultValue, Object... constructorArgs) { + final Binding binding = this.searchRegistry(clazz); + if (binding != null) { + return this.handleBinding(binding, constructorArgs); + } + final T parentResult = this.tryParent(clazz, constructorArgs); + return parentResult != null ? parentResult : defaultValue; + } + +} diff --git a/src/main/java/groowt/util/di/ExtensionContainer.java b/src/main/java/groowt/util/di/ExtensionContainer.java new file mode 100644 index 0000000..75bd6eb --- /dev/null +++ b/src/main/java/groowt/util/di/ExtensionContainer.java @@ -0,0 +1,10 @@ +package groowt.util.di; + +import java.util.Collection; + +public interface ExtensionContainer { + void addExtension(RegistryExtension extension); + E getExtension(Class extensionType); + Collection getExtensions(Class extensionType); + void removeExtension(RegistryExtension extension); +} diff --git a/src/main/java/groowt/util/di/KeyBinder.java b/src/main/java/groowt/util/di/KeyBinder.java new file mode 100644 index 0000000..ab12600 --- /dev/null +++ b/src/main/java/groowt/util/di/KeyBinder.java @@ -0,0 +1,16 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public interface KeyBinder { + Class getKeyClass(); + , T> void bind(KeyHolder keyHolder, Consumer> configure); + , T> @Nullable Binding getBinding(KeyHolder keyHolder); + , T> void removeBinding(KeyHolder keyHolder); + , T> void removeBindingIf(KeyHolder keyHolder, Predicate> filter); + void clearAllBindings(); +} diff --git a/src/main/java/groowt/util/di/KeyHolder.java b/src/main/java/groowt/util/di/KeyHolder.java new file mode 100644 index 0000000..953b645 --- /dev/null +++ b/src/main/java/groowt/util/di/KeyHolder.java @@ -0,0 +1,7 @@ +package groowt.util.di; + +public interface KeyHolder, K, T> { + Class binderType(); + Class type(); + K key(); +} diff --git a/src/main/java/groowt/util/di/LazySingletonBinding.java b/src/main/java/groowt/util/di/LazySingletonBinding.java new file mode 100644 index 0000000..d392a48 --- /dev/null +++ b/src/main/java/groowt/util/di/LazySingletonBinding.java @@ -0,0 +1,5 @@ +package groowt.util.di; + +import java.util.function.Supplier; + +public record LazySingletonBinding(Supplier singletonSupplier) implements Binding {} diff --git a/src/main/java/groowt/util/di/NamedRegistryExtension.java b/src/main/java/groowt/util/di/NamedRegistryExtension.java new file mode 100644 index 0000000..86be96f --- /dev/null +++ b/src/main/java/groowt/util/di/NamedRegistryExtension.java @@ -0,0 +1,9 @@ +package groowt.util.di; + +public interface NamedRegistryExtension extends RegistryExtension, KeyBinder, QualifierHandlerContainer { + + static KeyHolder named(String name, Class type) { + return new SimpleKeyHolder<>(NamedRegistryExtension.class, type, name); + } + +} diff --git a/src/main/java/groowt/util/di/ObjectFactory.java b/src/main/java/groowt/util/di/ObjectFactory.java new file mode 100644 index 0000000..db0ee81 --- /dev/null +++ b/src/main/java/groowt/util/di/ObjectFactory.java @@ -0,0 +1,61 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Contract; + +import java.util.function.Function; + +/** + * An {@link ObjectFactory} is an object that can construct objects of given types. + */ +@FunctionalInterface +public interface ObjectFactory { + + /** + * Create a new instance of the given {@code instanceType} with the given constructor args. + * + * @apiNote An implementation may provide a subclass of the given instance type, + * or it may directly instantiate the given type, if it is a class + * and it can determine the correct constructor from the given arguments. + * See individual implementation documentation for exact behavior. + * + * @implSpec It is up to individual implementations of {@link ObjectFactory} to determine how to + * select the appropriate constructor for the given type. The returned + * instance must be new and in a valid state. + * + * @param instanceType the {@link Class} of the desired type + * @param constructorArgs any arguments to pass to the constructor(s) of the class. + * @return the new instance + * @param the desired type + */ + @Contract("_, _-> new") + T createInstance(Class instanceType, Object... constructorArgs); + + /** + * Very similar to {@link #createInstance(Class, Object...)}, but catches any {@link RuntimeException} + * thrown by {@link #createInstance} and subsequently passes it to the given {@link Function}, returning + * instead the return value of the {@link Function}. + * + * @param instanceType the desired type of the created instance + * @param onException a {@link Function} to handle when an exception occurs and return a value nonetheless + * @param constructorArgs arguments to pass to the constructor + * @return the created instance + * @param the desired type + * + * @throws RuntimeException if the given {@link Function} itself throws a RuntimeException + * + * @see #createInstance(Class, Object...) + */ + @Contract("_, _, _ -> new") + default T createInstanceCatching( + Class instanceType, + Function onException, + Object... constructorArgs + ) { + try { + return this.createInstance(instanceType, constructorArgs); + } catch (RuntimeException runtimeException) { + return onException.apply(runtimeException); + } + } + +} diff --git a/src/main/java/groowt/util/di/ObjectFactoryUtil.java b/src/main/java/groowt/util/di/ObjectFactoryUtil.java new file mode 100644 index 0000000..9e0a10f --- /dev/null +++ b/src/main/java/groowt/util/di/ObjectFactoryUtil.java @@ -0,0 +1,23 @@ +package groowt.util.di; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class ObjectFactoryUtil { + + public static Class[] toTypes(Object... objects) { + final Class[] types = new Class[objects.length]; + for (int i = 0; i < objects.length; i++) { + final Object o = objects[i]; + if (o != null) { + types[i] = o.getClass(); + } else { + types[i] = Object.class; + } + } + return types; + } + + private ObjectFactoryUtil() {} + +} diff --git a/src/main/java/groowt/util/di/ProviderBinding.java b/src/main/java/groowt/util/di/ProviderBinding.java new file mode 100644 index 0000000..8ab5979 --- /dev/null +++ b/src/main/java/groowt/util/di/ProviderBinding.java @@ -0,0 +1,5 @@ +package groowt.util.di; + +import jakarta.inject.Provider; + +public record ProviderBinding(Class to, Provider provider) implements Binding {} diff --git a/src/main/java/groowt/util/di/QualifierHandler.java b/src/main/java/groowt/util/di/QualifierHandler.java new file mode 100644 index 0000000..3b04737 --- /dev/null +++ b/src/main/java/groowt/util/di/QualifierHandler.java @@ -0,0 +1,10 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +@FunctionalInterface +public interface QualifierHandler { + @Nullable Binding handle(A annotation, Class dependencyClass); +} diff --git a/src/main/java/groowt/util/di/QualifierHandlerContainer.java b/src/main/java/groowt/util/di/QualifierHandlerContainer.java new file mode 100644 index 0000000..2fd98bf --- /dev/null +++ b/src/main/java/groowt/util/di/QualifierHandlerContainer.java @@ -0,0 +1,20 @@ +package groowt.util.di; + +import jakarta.inject.Qualifier; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +public interface QualifierHandlerContainer { + + static void checkIsValidQualifier(Class annotationClass) { + if (!annotationClass.isAnnotationPresent(Qualifier.class)) { + throw new IllegalArgumentException( + "The given qualifier annotation + " + annotationClass + " is itself not annotated with @Qualifier" + ); + } + } + + @Nullable QualifierHandler getQualifierHandler(Class qualifierType); + +} diff --git a/src/main/java/groowt/util/di/Registry.java b/src/main/java/groowt/util/di/Registry.java new file mode 100644 index 0000000..e7c1862 --- /dev/null +++ b/src/main/java/groowt/util/di/Registry.java @@ -0,0 +1,20 @@ +package groowt.util.di; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +public interface Registry extends ExtensionContainer, QualifierHandlerContainer, ScopeHandlerContainer { + void bind(Class key, Consumer> configure); + @Nullable Binding getBinding(Class key); + void removeBinding(Class key); + void removeBindingIf(Class key, Predicate> filter); + + void bind(KeyHolder keyHolder, Consumer> configure); + @Nullable Binding getBinding(KeyHolder keyHolder); + void removeBinding(KeyHolder keyHolder); + void removeBindingIf(KeyHolder keyHolder, Predicate> filter); + void clearAllBindings(); + +} diff --git a/src/main/java/groowt/util/di/RegistryExtension.java b/src/main/java/groowt/util/di/RegistryExtension.java new file mode 100644 index 0000000..5010d43 --- /dev/null +++ b/src/main/java/groowt/util/di/RegistryExtension.java @@ -0,0 +1,3 @@ +package groowt.util.di; + +public interface RegistryExtension {} diff --git a/src/main/java/groowt/util/di/RegistryObjectFactory.java b/src/main/java/groowt/util/di/RegistryObjectFactory.java new file mode 100644 index 0000000..a0d236c --- /dev/null +++ b/src/main/java/groowt/util/di/RegistryObjectFactory.java @@ -0,0 +1,99 @@ +package groowt.util.di; + +import groowt.util.di.filters.FilterHandler; +import groowt.util.di.filters.IterableFilterHandler; +import jakarta.inject.Provider; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.function.Consumer; + +/** + * A {@link RegistryObjectFactory} is an {@link ObjectFactory} that offers the ability + * to provide desired objects based on an instance of + * {@link Registry} to determine how to provide those objects. + */ +public interface RegistryObjectFactory extends ObjectFactory { + + interface Builder { + void configureRegistry(Consumer configure); + void addFilterHandler(FilterHandler handler); + void addIterableFilterHandler(IterableFilterHandler handler); + T build(); + } + + void configureRegistry(Consumer use); + + @Nullable ScopeHandler findScopeHandler(Class scopeType); + + @Nullable QualifierHandler findQualifierHandler(Class qualifierType); + + /** + * Get an object with the desired type. How it is retrieved/created + * depends upon the {@link Binding} present in this {@link RegistryObjectFactory}'s held + * instances of {@link Registry}. The type of the {@link Binding} determines + * how the object is fetched: + * + *
    + *
  • {@link ClassBinding}: A new instance of the object is created using the given {@code constructorArgs}.
  • + *
  • {@link ProviderBinding}: An instance of the object is fetched from the bound {@link Provider}. + * Whether the instance is new or not depends on the {@link Provider}.
  • + *
  • {@link SingletonBinding}: The bound singleton object is returned.
  • + *
+ * + * @implNote If {@code constructorArgs} are provided + * and the {@link Binding} for the desired type is not a + * {@link ClassBinding}, the implementation should + * either throw an exception or log a warning at the least. + * + * @param clazz the {@link Class} of the desired type + * @param constructorArgs As in {@link #createInstance(Class, Object...)}, + * the arguments which will be used to create the desired object + * if the {@link Binding} is a {@link ClassBinding}. + * @return an object of the desired type + * @param the desired type + * + * @throws RuntimeException if there is no registered {@link Binding} or there is a problem + * fetching or constructing the object. + */ + T get(Class clazz, Object... constructorArgs); + + /** + * Similarly to {@link #get(Class, Object...)}, fetches an object + * of the desired type, but does not throw if there is no registered {@link Binding} + * in any of the held instances of {@link Registry}, + * and instead returns the given {@code defaultValue}. + * + * @param clazz the {@link Class} of the desired type + * @param defaultValue the defaultValue to return + * @param constructorArgs see {@link #get(Class, Object...)} + * @return an object of the desired type + * @param the desired type + * + * @throws RuntimeException if there is a registered {@link Binding} and there is a problem + * fetching or constructing the object. + * + * @see #get(Class, Object...) + */ + T getOrDefault(Class clazz, T defaultValue, Object... constructorArgs); + + /** + * Similar to {@link #getOrDefault(Class, Object, Object...)}, except that + * it returns null by default if there is no registered {@link Binding}. + * + * @param clazz the {@link Class} of the desired type + * @param constructorArgs see {@link RegistryObjectFactory#get(Class, Object...)} + * @return an object of the desired type + * @param the desired type + * + * @see RegistryObjectFactory#get(Class, Object...) + * @see RegistryObjectFactory#getOrDefault(Class, Object, Object...) + * + * @throws RuntimeException if there is a registered {@code Binding} and there + * is a problem fetching or constructing the object. + */ + default @Nullable T getOrNull(Class clazz, Object... constructorArgs) { + return this.getOrDefault(clazz, null, constructorArgs); + } + +} diff --git a/src/main/java/groowt/util/di/RegistryObjectFactoryUtil.java b/src/main/java/groowt/util/di/RegistryObjectFactoryUtil.java new file mode 100644 index 0000000..d32b351 --- /dev/null +++ b/src/main/java/groowt/util/di/RegistryObjectFactoryUtil.java @@ -0,0 +1,69 @@ +package groowt.util.di; + +import groowt.util.di.filters.Filter; +import groowt.util.di.filters.IterableFilter; +import jakarta.inject.Qualifier; +import jakarta.inject.Scope; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +@ApiStatus.Internal +public final class RegistryObjectFactoryUtil { + + private RegistryObjectFactoryUtil() {} + + public static List getQualifierAnnotations(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class)) + .toList(); + } + + public static List getFilterAnnotations(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(a -> a.annotationType().isAnnotationPresent(Filter.class)) + .toList(); + } + + public static List getIterableFilterAnnotations(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(a -> a.annotationType().isAnnotationPresent(IterableFilter.class)) + .toList(); + } + + public static Optional orElseSupply(T first, Supplier onNullFirst) { + return first != null ? Optional.of(first) : Optional.ofNullable(onNullFirst.get()); + } + + public static void checkIsValidFilter(Class annotationClass) { + if (!annotationClass.isAnnotationPresent(Filter.class)) { + throw new IllegalArgumentException( + "The given filter annotation " + annotationClass.getName() + " is itself not annotated with @Filter" + ); + } + } + + public static void checkIsValidIterableFilter(Class annotationClass) { + if (!annotationClass.isAnnotationPresent(IterableFilter.class)) { + throw new IllegalArgumentException( + "The given iterable filter annotation " + annotationClass.getName() + " is itself not annotated with @IterableFilter" + ); + } + } + + public static @Nullable Annotation getScopeAnnotation(Class clazz) { + final List scopeAnnotations = Arrays.stream(clazz.getAnnotations()) + .filter(annotation -> annotation.annotationType().isAnnotationPresent(Scope.class)) + .toList(); + if (scopeAnnotations.size() > 1) { + throw new RuntimeException(clazz.getName() + " has too many annotations that are themselves annotated with @Scope"); + } + return scopeAnnotations.size() == 1 ? scopeAnnotations.getFirst() : null; + } + +} diff --git a/src/main/java/groowt/util/di/ScopeHandler.java b/src/main/java/groowt/util/di/ScopeHandler.java new file mode 100644 index 0000000..31f3bc8 --- /dev/null +++ b/src/main/java/groowt/util/di/ScopeHandler.java @@ -0,0 +1,8 @@ +package groowt.util.di; + +import java.lang.annotation.Annotation; + +public interface ScopeHandler
{ + Binding onScopedDependencyRequest(A annotation, Class dependencyClass, RegistryObjectFactory objectFactory); + void reset(); +} diff --git a/src/main/java/groowt/util/di/ScopeHandlerContainer.java b/src/main/java/groowt/util/di/ScopeHandlerContainer.java new file mode 100644 index 0000000..b08d3aa --- /dev/null +++ b/src/main/java/groowt/util/di/ScopeHandlerContainer.java @@ -0,0 +1,20 @@ +package groowt.util.di; + +import jakarta.inject.Scope; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +public interface ScopeHandlerContainer { + + static void checkIsValidScope(Class scope) { + if (!scope.isAnnotationPresent(Scope.class)) { + throw new IllegalArgumentException( + "The given scope annotation " + scope + " is itself not annotated with @Scope" + ); + } + } + + @Nullable ScopeHandler getScopeHandler(Class scopeType); + +} diff --git a/src/main/java/groowt/util/di/SimpleBindingConfigurator.java b/src/main/java/groowt/util/di/SimpleBindingConfigurator.java new file mode 100644 index 0000000..4a682b1 --- /dev/null +++ b/src/main/java/groowt/util/di/SimpleBindingConfigurator.java @@ -0,0 +1,43 @@ +package groowt.util.di; + +import jakarta.inject.Provider; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public class SimpleBindingConfigurator implements BindingConfigurator { + + private final Class from; + private @Nullable Binding binding; + + public SimpleBindingConfigurator(Class from) { + this.from = from; + } + + public final Binding getBinding() { + return this.binding != null + ? this.binding + : new ClassBinding<>(this.from, this.from); // return SelfBinding in case we never called anything + } + + @Override + public void to(Class target) { + this.binding = new ClassBinding<>(this.from, target); + } + + @Override + public void toProvider(Provider provider) { + this.binding = new ProviderBinding<>(this.from, provider); + } + + @Override + public void toSingleton(T target) { + this.binding = new SingletonBinding<>(target); + } + + @Override + public void toLazySingleton(Supplier singletonSupplier) { + this.binding = new LazySingletonBinding<>(singletonSupplier); + } + +} diff --git a/src/main/java/groowt/util/di/SimpleKeyHolder.java b/src/main/java/groowt/util/di/SimpleKeyHolder.java new file mode 100644 index 0000000..47a1745 --- /dev/null +++ b/src/main/java/groowt/util/di/SimpleKeyHolder.java @@ -0,0 +1,4 @@ +package groowt.util.di; + +public record SimpleKeyHolder, K, T>(Class binderType, Class type, K key) + implements KeyHolder {} diff --git a/src/main/java/groowt/util/di/SingletonBinding.java b/src/main/java/groowt/util/di/SingletonBinding.java new file mode 100644 index 0000000..67e04c6 --- /dev/null +++ b/src/main/java/groowt/util/di/SingletonBinding.java @@ -0,0 +1,3 @@ +package groowt.util.di; + +public record SingletonBinding(T to) implements Binding {} diff --git a/src/main/java/groowt/util/di/SingletonRegistryExtension.java b/src/main/java/groowt/util/di/SingletonRegistryExtension.java new file mode 100644 index 0000000..0e6e70e --- /dev/null +++ b/src/main/java/groowt/util/di/SingletonRegistryExtension.java @@ -0,0 +1,22 @@ +package groowt.util.di; + +import jakarta.inject.Singleton; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; + +public class SingletonRegistryExtension implements RegistryExtension, ScopeHandlerContainer { + + private final SingletonScopeHandler handler; + + public SingletonRegistryExtension(Registry owner) { + this.handler = new SingletonScopeHandler(owner); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable ScopeHandler getScopeHandler(Class scopeType) { + return Singleton.class.isAssignableFrom(scopeType) ? (ScopeHandler) this.handler : null; + } + +} diff --git a/src/main/java/groowt/util/di/SingletonScopeHandler.java b/src/main/java/groowt/util/di/SingletonScopeHandler.java new file mode 100644 index 0000000..0be9ab6 --- /dev/null +++ b/src/main/java/groowt/util/di/SingletonScopeHandler.java @@ -0,0 +1,35 @@ +package groowt.util.di; + +import jakarta.inject.Singleton; + +import static groowt.util.di.BindingUtil.toSingleton; + +public final class SingletonScopeHandler implements ScopeHandler { + + private final Registry owner; + + public SingletonScopeHandler(Registry owner) { + this.owner = owner; + } + + @Override + public Binding onScopedDependencyRequest( + Singleton annotation, + Class dependencyClass, + RegistryObjectFactory objectFactory + ) { + final Binding potentialBinding = this.owner.getBinding(dependencyClass); + if (potentialBinding != null) { + return potentialBinding; + } else { + this.owner.bind(dependencyClass, toSingleton(objectFactory.createInstance(dependencyClass))); + return this.owner.getBinding(dependencyClass); + } + } + + @Override + public void reset() { + throw new UnsupportedOperationException("Cannot reset the Singleton scope!"); + } + +} diff --git a/src/main/java/groowt/util/di/annotation/Given.java b/src/main/java/groowt/util/di/annotation/Given.java new file mode 100644 index 0000000..ce94d25 --- /dev/null +++ b/src/main/java/groowt/util/di/annotation/Given.java @@ -0,0 +1,10 @@ +package groowt.util.di.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.PARAMETER) +public @interface Given {} diff --git a/src/main/java/groowt/util/di/filters/Filter.java b/src/main/java/groowt/util/di/filters/Filter.java new file mode 100644 index 0000000..3f36642 --- /dev/null +++ b/src/main/java/groowt/util/di/filters/Filter.java @@ -0,0 +1,10 @@ +package groowt.util.di.filters; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.ANNOTATION_TYPE }) +public @interface Filter {} diff --git a/src/main/java/groowt/util/di/filters/FilterHandler.java b/src/main/java/groowt/util/di/filters/FilterHandler.java new file mode 100644 index 0000000..8581259 --- /dev/null +++ b/src/main/java/groowt/util/di/filters/FilterHandler.java @@ -0,0 +1,39 @@ +package groowt.util.di.filters; + +import java.lang.annotation.Annotation; +import java.util.Objects; +import java.util.function.BiPredicate; + +public interface FilterHandler { + + boolean check(A annotation, T arg); + Class getAnnotationClass(); + Class getArgumentClass(); + + default FilterHandler and(BiPredicate and) { + Objects.requireNonNull(and); + return new SimpleFilterHandler<>( + (a, t) -> this.check(a, t) && and.test(a, t), + this.getAnnotationClass(), + this.getArgumentClass() + ); + } + + default FilterHandler or(BiPredicate or) { + Objects.requireNonNull(or); + return new SimpleFilterHandler<>( + (a, t) -> this.check(a, t) || or.test(a, t), + this.getAnnotationClass(), + this.getArgumentClass() + ); + } + + default FilterHandler negate() { + return new SimpleFilterHandler<>( + (a, t) -> !this.check(a, t), + this.getAnnotationClass(), + this.getArgumentClass() + ); + } + +} diff --git a/src/main/java/groowt/util/di/filters/FilterHandlers.java b/src/main/java/groowt/util/di/filters/FilterHandlers.java new file mode 100644 index 0000000..08e908e --- /dev/null +++ b/src/main/java/groowt/util/di/filters/FilterHandlers.java @@ -0,0 +1,35 @@ +package groowt.util.di.filters; + +import java.lang.annotation.*; +import java.util.function.BiPredicate; + +import static groowt.util.di.filters.FilterUtil.isAssignableToAnyOf; + +public final class FilterHandlers { + + private FilterHandlers() {} + + public static FilterHandler of( + Class annotationClass, + Class argClass, + BiPredicate predicate + ) { + return new SimpleFilterHandler<>(predicate, annotationClass, argClass); + } + + @Filter + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface AllowTypes { + Class[] value(); + } + + public static FilterHandler getAllowsTypesFilterHandler(Class targetType) { + return of( + AllowTypes.class, + targetType, + (annotation, target) -> isAssignableToAnyOf(target.getClass(), annotation.value()) + ); + } + +} diff --git a/src/main/java/groowt/util/di/filters/FilterUtil.java b/src/main/java/groowt/util/di/filters/FilterUtil.java new file mode 100644 index 0000000..7471625 --- /dev/null +++ b/src/main/java/groowt/util/di/filters/FilterUtil.java @@ -0,0 +1,16 @@ +package groowt.util.di.filters; + +public final class FilterUtil { + + private FilterUtil() {} + + public static boolean isAssignableToAnyOf(Class subject, Class[] tests) { + for (final var test : tests) { + if (test.isAssignableFrom(subject)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/groowt/util/di/filters/IterableFilter.java b/src/main/java/groowt/util/di/filters/IterableFilter.java new file mode 100644 index 0000000..63a80d1 --- /dev/null +++ b/src/main/java/groowt/util/di/filters/IterableFilter.java @@ -0,0 +1,10 @@ +package groowt.util.di.filters; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface IterableFilter {} diff --git a/src/main/java/groowt/util/di/filters/IterableFilterHandler.java b/src/main/java/groowt/util/di/filters/IterableFilterHandler.java new file mode 100644 index 0000000..26005e1 --- /dev/null +++ b/src/main/java/groowt/util/di/filters/IterableFilterHandler.java @@ -0,0 +1,8 @@ +package groowt.util.di.filters; + +import java.lang.annotation.Annotation; + +public interface IterableFilterHandler { + boolean check(A annotation, Iterable iterable); + Class getAnnotationClass(); +} diff --git a/src/main/java/groowt/util/di/filters/IterableFilterHandlers.java b/src/main/java/groowt/util/di/filters/IterableFilterHandlers.java new file mode 100644 index 0000000..78cde67 --- /dev/null +++ b/src/main/java/groowt/util/di/filters/IterableFilterHandlers.java @@ -0,0 +1,33 @@ +package groowt.util.di.filters; + +import java.lang.annotation.*; +import java.util.function.BiPredicate; + +import static groowt.util.di.filters.FilterUtil.isAssignableToAnyOf; + +public final class IterableFilterHandlers { + + private IterableFilterHandlers() {} + + public static IterableFilterHandler of( + Class filterType, + BiPredicate elementPredicate + ) { + return new SimpleIterableFilterHandler<>(filterType, elementPredicate); + } + + @IterableFilter + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface IterableElementTypes { + Class[] value(); + } + + public static IterableFilterHandler getIterableElementTypesFilterHandler() { + return of( + IterableElementTypes.class, + (annotation, element) -> isAssignableToAnyOf(element.getClass(), annotation.value()) + ); + } + +} diff --git a/src/main/java/groowt/util/di/filters/SimpleFilterHandler.java b/src/main/java/groowt/util/di/filters/SimpleFilterHandler.java new file mode 100644 index 0000000..338c504 --- /dev/null +++ b/src/main/java/groowt/util/di/filters/SimpleFilterHandler.java @@ -0,0 +1,33 @@ +package groowt.util.di.filters; + +import java.lang.annotation.Annotation; +import java.util.function.BiPredicate; + +final class SimpleFilterHandler implements FilterHandler { + + private final BiPredicate predicate; + private final Class annotationClass; + private final Class argClass; + + public SimpleFilterHandler(BiPredicate predicate, Class annotationClass, Class argClass) { + this.predicate = predicate; + this.annotationClass = annotationClass; + this.argClass = argClass; + } + + @Override + public boolean check(A annotation, T arg) { + return this.predicate.test(annotation, arg); + } + + @Override + public Class getAnnotationClass() { + return this.annotationClass; + } + + @Override + public Class getArgumentClass() { + return this.argClass; + } + +} diff --git a/src/main/java/groowt/util/di/filters/SimpleIterableFilterHandler.java b/src/main/java/groowt/util/di/filters/SimpleIterableFilterHandler.java new file mode 100644 index 0000000..3236cce --- /dev/null +++ b/src/main/java/groowt/util/di/filters/SimpleIterableFilterHandler.java @@ -0,0 +1,32 @@ +package groowt.util.di.filters; + +import java.lang.annotation.Annotation; +import java.util.Objects; +import java.util.function.BiPredicate; + +final class SimpleIterableFilterHandler implements IterableFilterHandler { + + private final Class annotationClass; + private final BiPredicate elementPredicate; + + public SimpleIterableFilterHandler(Class annotationClass, BiPredicate elementPredicate) { + this.annotationClass = annotationClass; + this.elementPredicate = elementPredicate; + } + + @Override + public boolean check(A annotation, Iterable iterable) { + for (final var e : Objects.requireNonNull(iterable)) { + if (!this.elementPredicate.test(annotation, e)) { + return false; + } + } + return true; + } + + @Override + public Class getAnnotationClass() { + return this.annotationClass; + } + +} diff --git a/src/test/java/groowt/util/di/DefaultRegistryObjectFactoryTests.java b/src/test/java/groowt/util/di/DefaultRegistryObjectFactoryTests.java new file mode 100644 index 0000000..aed45cb --- /dev/null +++ b/src/test/java/groowt/util/di/DefaultRegistryObjectFactoryTests.java @@ -0,0 +1,170 @@ +package groowt.util.di; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.junit.jupiter.api.Test; + +import static groowt.util.di.BindingUtil.*; +import static groowt.util.di.NamedRegistryExtension.named; +import static org.junit.jupiter.api.Assertions.*; + +public class DefaultRegistryObjectFactoryTests { + + public interface Greeter { + String greet(); + } + + public static final class DefaultGreeter implements Greeter { + + @Override + public String greet() { + return "Hello, World!"; + } + + } + + public static final class GivenArgGreeter implements Greeter { + + private final String greeting; + + @Inject + public GivenArgGreeter(String greeting) { + this.greeting = greeting; + } + + @Override + public String greet() { + return this.greeting; + } + + } + + public static final class InjectedArgGreeter implements Greeter { + + private final String greeting; + + @Inject + public InjectedArgGreeter(String greeting) { + this.greeting = greeting; + } + + @Override + public String greet() { + return this.greeting; + } + + } + + public static final class InjectedNamedArgGreeter implements Greeter { + + private final String greeting; + + @Inject + public InjectedNamedArgGreeter(@Named("greeting") String greeting) { + this.greeting = greeting; + } + + @Override + public String greet() { + return this.greeting; + } + + } + + public static final class InjectedNamedSetterGreeter implements Greeter { + + private String greeting; + + @Inject + public void setGreeting(@Named("greeting") String greeting) { + this.greeting = greeting; + } + + @Override + public String greet() { + return this.greeting; + } + + } + + @Test + public void classSmokeScreen() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, bc -> bc.to(DefaultGreeter.class)); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void singletonSmokeScreen() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, toSingleton(new DefaultGreeter())); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void providerSmokeScreen() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, toProvider(DefaultGreeter::new)); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void givenArgSmokeScreen() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, bc -> bc.to(GivenArgGreeter.class)); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class, "Hello, World!"); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void injectedArg() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, bc -> bc.to(InjectedArgGreeter.class)); + registry.bind(String.class, toSingleton("Hello, World!")); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void injectedNamedArg() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(registry -> { + registry.bind(Greeter.class, bc -> bc.to(InjectedNamedArgGreeter.class)); + registry.bind(named("greeting", String.class), toSingleton("Hello, World!")); + }); + final RegistryObjectFactory container = b.build(); + final Greeter greeter = container.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + + @Test + public void injectedSetter() { + final var b = DefaultRegistryObjectFactory.Builder.withDefaults(); + b.configureRegistry(r -> { + r.bind(Greeter.class, toClass(InjectedNamedSetterGreeter.class)); + r.bind(named("greeting", String.class), toSingleton("Hello, World!")); + }); + final RegistryObjectFactory f = b.build(); + final Greeter greeter = f.get(Greeter.class); + assertEquals("Hello, World!", greeter.greet()); + } + +} diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml new file mode 100644 index 0000000..beab8e9 --- /dev/null +++ b/src/test/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file