diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c82fa0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Created by .ignore support plugin (hsz.mobi) +### Maven template +target/ \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..06138b3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2017 [MacFJA](https://github.com/MacFJA) + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8e86cd --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# Injector + + - [Injection possibility](#injection) + - [Constructor Injection](#injection-constructor) + - [Setters Injection](#injection-setters) + - [Properties Injection](#injection-properties) + - [Method Injection](#injection-method) + - [Injection types](#types) + - [Installation](#installation) + - [Examples](#examples) + - [Declaring a mapping](#examples-mapping) + - [For a singleton](#examples-mapping-singleton) + - [For an interface/abstract class to concrete implementation](#examples-mapping-interface) + +## Injection possibility + +This library offer several types of injection: + +- Constructors injection +- Setters injection +- Properties injection +- Method injection + +### Constructor Injection + +The constructor injection try to create a class instance by looping over every class constructor until it found one that can be used . + +The injection criteria are: + +- Parameters are not Java primitive +- Parameters packages are in injector package list +- All parameters constructor do the same + +### Setters Injection + +The setter injection is automatically run after the constructor injection if the injector have the option activated. +(Can be also be call on an existing instance) +For a setter method to be injected, it must validate the following conditions: + +- The method name **MUST** start with `set` +- The method **MUST** have exactly one parameter +- The method **MUST** have the annotation `@javax.inject.Inject` +- The method parameter must be an injectable class + +### Properties Injection + +The property injection is automatically run after the constructor injection if the injector have the option activated. +(Can be also be call on an existing instance) +For a property to be injected, it must validate the following conditions: + +- The property **MUST** be accessible +- The property **MUST** have the annotation `@javax.inject.Inject` +- The property must be an injectable class + +### Method Injection + +A method can have its parameters injected. + +There are two way to inject parameters in a method. +First one is with a `java.lang.reflect.Method` object, in this case there are no control, if a parameter can't be injected `null` will be used. +The second way is to use the method name, with this way, all method of the object with this name will be try, and the method must have the annotation `@javax.inject.Inject` and every parameters must be injectable. + +## Injection types + +There are two injection types: + +- Singleton +- Every times a new instance + +## Installation + +Clone the project: +``` +git clone https://github.com/MacFJA/Injector.git +``` +Install the project into your local Maven repository: +``` +cd Injector/ +mvn clean +mvn install +``` +Remove the source: +``` +cd .. +rm -r Injector/ +``` +Add the depency in your Maven project: +```xml + + + + + + io.github.macfja + injector + 1.0.0 + + + + + +``` + +## Examples + +### Declaring a mapping + +#### For a singleton + +```java +io.github.macfja.injector.Injector injector = new io.github.macfja.injector.Injector("mypackage"); +injector.addMapping(new mypackage.MyClass()); +// ... later +injector.get(mypackage.MyClass.class); // return the instance created in addMapping method +// ... later +injector.get(mypackage.MyClass.class); // still the same instance +``` + +or + +```java +io.github.macfja.injector.Injector injector = new io.github.macfja.injector.Injector("mypackage"); +injector.addMapping(mypackage.MyClass.class, new io.github.macfja.injector.InjectionUnit(new mypackage.MyClass())); +// ... later +injector.get(mypackage.MyClass.class); // return the instance created in addMapping method +// ... later +injector.get(mypackage.MyClass.class); // still the same instance +``` + +or + +```java +io.github.macfja.injector.Injector injector = new io.github.macfja.injector.Injector("mypackage"); +injector.addMapping( + mypackage.MyClass.class, + new io.github.macfja.injector.InjectionUnit( + mypackage.MyClass.class, + io.github.macfja.injector.InjectionUnit.Instantiation.Singleton + ) +); +// ... later +injector.get(mypackage.MyClass.class); // return the instance created in addMapping method +// ... later +injector.get(mypackage.MyClass.class); // still the same instance +``` + +#### For an interface/abstract class to concrete implementation + +```java +io.github.macfja.injector.Injector injector = new io.github.macfja.injector.Injector("mypackage"); +injector.addMapping(mypackage.MyInterface.class, new io.github.macfja.injector.InjectionUnit(/* ... */)); +// ... later +injector.get(mypackage.MyInterface.class); // return an instance according to the InjectionUnit +``` \ No newline at end of file diff --git a/doc/Example1.md b/doc/Example1.md new file mode 100644 index 0000000..b698669 --- /dev/null +++ b/doc/Example1.md @@ -0,0 +1,50 @@ +# Example 1 + +This example is a case where method injection is used (differing class loading). + +This approach can also be used for lazy loading. + +```java +package example; + +class PanelA extends javax.swing.JPanel { + // Do your stuff +} + +class PanelB extends javax.swing.JPanel { + // Do other stuff +} + +class Application extends javax.swing.JFrame { + private static io.github.macfja.injector.Injector injector = new io.github.macfja.injector.Injector("example"); + + public static void main(String[] args) { + injector.addMapping( + Application.class, + new io.github.macfja.injector.InjectionUnit( + Application.class, + io.github.macfja.injector.InjectionUnit.Instantiation.Singleton + ) + ); + + injector.get(Application.class); + + } + + public Application(PanelA panel) { + setContentPane(panel); + setVisible(true); + + new java.util.Timer().schedule(new java.util.TimerTask() { + @Override + public void run() { + injector.injectIntoMethodName(Application.this, "init"); + } + }, 1000); + } + + public void init(PanelB panelB) { + setContentPane(panelB); + } +} +``` \ No newline at end of file diff --git a/doc/Example2.md b/doc/Example2.md new file mode 100644 index 0000000..26df89c --- /dev/null +++ b/doc/Example2.md @@ -0,0 +1,78 @@ +# Example 2 + +This example show the interface use case. + +```java +package example; +interface ServiceInterface { + void connect(); + void disconnect(); + void doAction(String param); +} +class Application { + private ServiceInterface service; + public static io.github.macfja.injector.Injector injector; + + public static void main(String[] args) { + injector = new io.github.macfja.injector.Injector("example"); + injector.addMapping( + ServiceInterface.class, + new io.github.macfja.injector.InjectionUnit( + SocketService.class, + io.github.macfja.injector.InjectionUnit.Instantiation.Singleton + ) + ); + + injector.get(Application.class); + + } + + public Application(ServiceInterface service) { + this.service = service; + + doStuff(); + } + + + public void doStuff() { + service.connect(); + service.doAction("first"); + service.doAction("second"); + service.disconnect(); + } +} + +class SocketService implements ServiceInterface { + @Override + public void connect() { + // Open socket + } + + @Override + public void doAction(String param) { + // Send data in socket + } + + @Override + public void disconnect() { + // Close socket + } +} + +class FileService implements ServiceInterface { + @Override + public void connect() { + // Open file stream + } + + @Override + public void doAction(String param) { + // Write in file + } + + @Override + public void disconnect() { + // Close file stream + } +} +``` \ No newline at end of file diff --git a/doc/Mapping.md b/doc/Mapping.md new file mode 100644 index 0000000..3d19a64 --- /dev/null +++ b/doc/Mapping.md @@ -0,0 +1,51 @@ +# Mapping + +A mapping is a rule that indicate the _Injector_ how to handle a class. + +There are several mapping: + +- The singleton mapping (for a given class, the same instance is always return) +- The interface to implementation (when the interface is requested a new instance of a defined class is provided) +- The interface to singleton (when the interface is requested a singleton is provided) +- The parent to child (when requested, a new instance of the defined class children is returned) +- The parent to child singleton (when requested, a singleton of the defined class children is returned) + +## Interface and parent mapping + +The **interface to implementation**, the **interface to singleton**, the **parent to child** and the **parent to child singleton** are base on the same principle. +The method `addMapping` is used in its form: +``` +public void addMapping(Class, InjectionUnit) +``` +Where the first parameter is the interface/parent and the second contains information about implementation/child/singleton. + +``` + | First parameter | Second parameter +-----------------------------+-------------------+------------------------------------------------------------------------------ + interface to implementation | MyInterface.class | new InjectionUnit(MyImpl.class, InjectionUnit.Instantiation.NewInstance) + interface to singleton | MyInterface.class | new InjectionUnit(MyImpl.class, InjectionUnit.Instantiation.Singleton) + interface to singleton | MyInterface.class | new InjectionUnit(new MyImpl()) + parent to child | MyAbstract.class | new InjectionUnit(MyExtender.class, InjectionUnit.Instantiation.NewInstance) + parent to child singleton | MyAbstract.class | new InjectionUnit(MyExtender.class, InjectionUnit.Instantiation.Singleton) + parent to child singleton | MyAbstract.class | new InjectionUnit(new MyExtender()) +``` + +## Singleton mapping + +There are 3 way to declare a Singleton: + +``` +injector.addMapping(new MySingleton()); +``` + +``` +injector.addMapping(MySingleton.class, new InjectionUnit(new MySingleton())); +// or +injector.addMapping(MyInterface.class, new InjectionUnit(new MyImplementation())); +``` + +``` +injector.addMapping(MySingleton.class, new InjectionUnit(MySingleton.class, InjectionUnit.Instantiation.Singleton)); +// or +injector.addMapping(MyInterface.class, new InjectionUnit(MyImplementation.class, InjectionUnit.Instantiation.Singleton)); +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a6df11a --- /dev/null +++ b/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + io.github.macfja + injector + 1.0.0 + jar + + Injector + Classes injector. Injection can be done in constructors, setters, properties and methods + https://github.com/MacFJA/Injector + + + + MacFJA + https://github.com/MacFJA/ + + + + + scm:git:git://github.com/MacFJA/Injector.git + scm:git:ssh://github.com:MacFJA/Injector.git + http://github.com/MacFJA/Injector/tree/master + + + + + MIT License + https://opensource.org/licenses/mit-license.php + repo + + + + + UTF-8 + + 1.7 + 4.12 + 1.7.25 + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + javax.inject + javax.inject + 1 + + + + + junit + junit + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${jdk.version} + ${jdk.version} + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + \ No newline at end of file diff --git a/src/main/java/io/github/macfja/injector/InjectionUnit.java b/src/main/java/io/github/macfja/injector/InjectionUnit.java new file mode 100644 index 0000000..886d897 --- /dev/null +++ b/src/main/java/io/github/macfja/injector/InjectionUnit.java @@ -0,0 +1,187 @@ +package io.github.macfja.injector; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; + +/** + * InjectionUnit class. + * Contains information about the class to inject and if the instance must be a singleton or not. + * + * @author MacFJA + */ +public class InjectionUnit implements Cloneable { + /** + * Class to use when requesting an instance + */ + private final Class toInject; + /** + * The type of instance + */ + private final Instantiation type; + /** + * The singleton instance (if the Instantiation is NewInstance) + */ + private Object singleton; + + /** + * Simple Constructor + * + * @param toInject The class that will be used + * @param type The type of instance (Singleton or not) + */ + public InjectionUnit(Class toInject, Instantiation type) { + this.toInject = toInject; + this.type = type; + } + + /** + * Constructor with a pre-generated singleton; + * + * @param singletonInstance The singleton to use on later. + */ + public InjectionUnit(Object singletonInstance) { + this.toInject = singletonInstance.getClass(); + this.singleton = singletonInstance; + this.type = Instantiation.Singleton; + } + + /** + * Check if a class have at least one constructor that can be used + * + * @param toInject The class to check + * @param injector The class injector + * @return {@code true} if a constructor can be use + */ + public static Boolean isInstantiable(Class toInject, Injector injector) { + for (Constructor constructor : toInject.getConstructors()) { + if (isConstructorInjectable(constructor, injector)) { + return true; + } + } + return false; + } + + /** + * Check if a constructor can be use (no params, or all params can be injected) + * + * @param constructor The constructor to check + * @param injector The class injector + * @return {@code true} if the constructor can be use + */ + public static Boolean isConstructorInjectable(Constructor constructor, Injector injector) { + for (Class variable : constructor.getParameterTypes()) { + if (!injector.isInjectable(variable)) { + return false; + } + } + return true; + } + + /** + * Get an instance + * + * @return The instance + * @throws IllegalAccessException if this {@code Constructor} object is enforcing Java language access control + * and the underlying constructor is inaccessible. + * @throws InvocationTargetException if the underlying constructor throws an exception. + * @throws InstantiationException if the class that declares the underlying constructor represents + * an abstract class. + */ + public Object get(Injector parent) throws IllegalAccessException, InvocationTargetException, InstantiationException { + if (Instantiation.Singleton.equals(type)) { + if (singleton == null) { + singleton = build(parent); + } + return singleton; + } + return build(parent); + } + + /** + * Create an instance of toInject class + * + * @param parent The parent injector (which initiate the build) + * @return The new instance + * @throws IllegalAccessException if this {@code Constructor} object is enforcing Java language access control + * and the underlying constructor is inaccessible. + * @throws InstantiationException if the class that declares the underlying constructor + * represents an abstract class. + * @throws InvocationTargetException if the underlying constructor throws an exception. + */ + private Object build(Injector parent) throws IllegalAccessException, InstantiationException, InvocationTargetException { + Object instance = null; + + if (toInject.getConstructors().length == 0) { + instance = toInject.newInstance(); + } else { + for (Constructor constructor : toInject.getConstructors()) { + if (isConstructorInjectable(constructor, parent)) { + instance = runConstructor(constructor, parent); + break; + } + } + + if (instance == null) { + throw new InstantiationException(); + } + } + + if (parent.getInjectProperties()) { + parent.injectIntoProperties(instance); + } + if (parent.getInjectSetters()) { + parent.injectIntoSetters(instance); + } + return instance; + } + + /** + * Inject class and execute constructor + * + * @param constructor The constructor to execute + * @param parent The parent injector (which initiate the build) + * @return A new instance created with the constructor + * @throws IllegalAccessException if this {@code Constructor} object is enforcing Java language access control + * and the underlying constructor is inaccessible. + * @throws InvocationTargetException if the underlying constructor throws an exception. + * @throws InstantiationException if the class that declares the underlying constructor + * represents an abstract class. + */ + private Object runConstructor(Constructor constructor, Injector parent) + throws IllegalAccessException, InvocationTargetException, InstantiationException { + ArrayList objects = new ArrayList<>(); + for (Class param : constructor.getParameterTypes()) { + objects.add(parent.get(param)); + } + return constructor.newInstance(objects.toArray()); + + } + + /** + * Check if the class to inject have at least one constructor that can be used + * + * @param injector The class injector + * @return {@code true} if a constructor can be use + */ + public Boolean isInstantiable(Injector injector) { + return isInstantiable(toInject, injector); + } + + /** + * List of possible instance type + */ + public enum Instantiation { + Singleton, + NewInstance + } + + @Override + public InjectionUnit clone() throws CloneNotSupportedException { + InjectionUnit clone = (InjectionUnit) super.clone(); + if (Instantiation.Singleton.equals(type)) { + clone.singleton = singleton; + } + return clone; + } +} diff --git a/src/main/java/io/github/macfja/injector/Injector.java b/src/main/java/io/github/macfja/injector/Injector.java new file mode 100644 index 0000000..c94d826 --- /dev/null +++ b/src/main/java/io/github/macfja/injector/Injector.java @@ -0,0 +1,359 @@ +package io.github.macfja.injector; + +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Injector class. + * The dependency injector entry point. + * + * @author MacFJA + */ +public class Injector implements Cloneable { + /** + * Mapping of class that have a particular injection + */ + private final Map mapping = new ConcurrentHashMap<>(); + /** + * List of package name that can be injected + */ + private Set workingPackages = new HashSet<>(); + /** + * Should the injector inject into properties + */ + private Boolean injectProperties = true; + /** + * Should the injector inject class with setters + */ + private Boolean injectSetters = true; + + /** + * Create an injector + * + * @param basePackage The base package that can be injected + */ + public Injector(String basePackage) { + addWorkingPackage(basePackage); + } + + /** + * Create an injector with a list of package + * + * @param packages The list of packages that can be injected + */ + public Injector(Set packages) { + workingPackages = packages; + } + + /** + * Add an injection rule of a class + * + * @param forClass The class to inject + * @param injection The injection rule + */ + public void addMapping(Class forClass, InjectionUnit injection) { + mapping.put(forClass, injection); + } + + /** + * Add a singleton rule + * + * @param singleton The singleton to use + */ + public void addMapping(Object singleton) { + mapping.put(singleton.getClass(), new InjectionUnit(singleton)); + } + + /** + * Add a package in the list of packages that can be injected + * + * @param packageName Name of the package to add + */ + public void addWorkingPackage(String packageName) { + workingPackages.add(packageName); + } + + /** + * Check if a class can be injected + * + * @param aClass The class to check + * @return {@code true} is the class injectable + */ + public boolean isInjectable(Class aClass) { + if (mapping.containsKey(aClass)) { + return true; + } + + /* + Classes name for primitive type + {@link http://stackoverflow.com/a/12505922} + + [Z = boolean + [B = byte + [S = short + [I = int + [J = long + [F = float + [D = double + [C = char + [L = any non-primitives(Object) + */ + if (aClass.isPrimitive() || Arrays.asList("[Z", "[B", "[S", "[I", "[J", "[F", "[D", "[C").contains(aClass.getName())) { + return false; + } + + if (aClass.getPackage() == null) { + return false; + } + + Boolean inWorkingPackages = false; + for (String packageName : workingPackages) { + if (aClass.getPackage().getName().startsWith(packageName)) { + inWorkingPackages = true; + break; + } + + } + if (!inWorkingPackages) { + return false; + } + return InjectionUnit.isInstantiable(aClass, this); + } + + /** + * Get an instance of the requested class. + * Silently fail. + * + * @param aClass The class + * @return an instance of the class + */ + public T get(Class aClass) { + try { + if (mapping.containsKey(aClass)) { + return (T) mapping.get(aClass).get(this); + } else { + return (T) new InjectionUnit(aClass, InjectionUnit.Instantiation.NewInstance).get(this); + } + } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) { + LoggerFactory.getLogger(this.getClass()).error("Unable to get an instance of " + aClass.getName(), e); + } + return null; + } + + public T get(Class aClass, Object... params) { + try { + Injector wrapper = clone(); + for(Object item : params) { + wrapper.addMapping(item); + } + return wrapper.get(aClass); + } catch (CloneNotSupportedException e) { + LoggerFactory.getLogger(this.getClass()).warn("Unable to clone the current injector, skip 'params' injection", e); + return get(aClass); + } + } + + /** + * Inject instance into an existing object properties. + * Silently fail on non accessible properties. + * + * @param instance The object to work on + */ + public void injectIntoProperties(Object instance) { + Set fields = new HashSet<>(); + fields.addAll(Arrays.asList(instance.getClass().getFields())); + fields.addAll(Arrays.asList(instance.getClass().getDeclaredFields())); + for (Field field : fields) { + if (field.isAnnotationPresent(Inject.class)) { + try { + field.set(instance, get(field.getType())); + } catch (IllegalAccessException e) { + LoggerFactory.getLogger(this.getClass()).warn("Can't inject into property " + field.getName(), e); + } + } + } + } + + /** + * Inject instances into a method parameters and execute it. + * + * @param instance The object + * @param method The method to execute + * @return The method result + * @throws InvocationTargetException if the underlying method throws an exception. + * @throws IllegalArgumentException

if the method is an instance method and the specified object argument is not + * an instance of the class or interface declaring the underlying method (or of a + * subclass or implementor thereof); if the number of actual and formal parameters + * differ; if an unwrapping conversion for primitive arguments fails; or if, after + * possible unwrapping, a parameter value cannot be converted to the corresponding + * formal parameter type by a method invocation conversion.

+ * @throws IllegalAccessException if this {@code Method} object is enforcing Java language access control and + * the underlying method is inaccessible. + */ + public Object injectIntoMethod(Object instance, Method method) + throws InvocationTargetException, IllegalAccessException { + ArrayList objects = new ArrayList<>(); + for (Class param : method.getParameterTypes()) { + objects.add(get(param)); + } + return method.invoke(instance, objects.toArray()); + } + + /** + * Try to inject instances into a method parameters and execute it base of a method. + * Will try all method with that name. + * + * @param instance The object + * @param methodName The method name to execute + * @return The method result + * @throws InvocationTargetException if the underlying method throws an exception. + * @throws IllegalAccessException if this {@code Method} object is enforcing Java language access control and + * the underlying method is inaccessible. + * @throws IllegalArgumentException

if the method is an instance method and the specified object argument is not + * an instance of the class or interface declaring the underlying method (or of a + * subclass or implementor thereof); if the number of actual and formal parameters + * differ; if an unwrapping conversion for primitive arguments fails; or if, after + * possible unwrapping, a parameter value cannot be converted to the corresponding + * formal parameter type by a method invocation conversion.

+ * @throws NoSuchMethodException The no method with the provided name can be executed + */ + public Object injectIntoMethodName(Object instance, String methodName) + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, IllegalArgumentException { + for (Method method : instance.getClass().getMethods()) { + if (!method.getName().equals(methodName)) { + continue; + } + + if (isMethodInjectable(method)) { + return injectIntoMethod(instance, method); + } + } + throw new NoSuchMethodException(); + } + + /** + * Loop over all setters of an object and inject an instance. + * Silently fail on non accessible setters. + * + * @param instance The object to work on + */ + public void injectIntoSetters(Object instance) { + Set methods = new HashSet<>(); + methods.addAll(Arrays.asList(instance.getClass().getDeclaredMethods())); + methods.addAll(Arrays.asList(instance.getClass().getMethods())); + for (Method method : methods) { + if ( + method.getName().startsWith("set") + && method.getParameterTypes().length == 1 + && isMethodInjectable(method) + ) { + try { + injectIntoMethod(instance, method); + } catch (InvocationTargetException | IllegalAccessException e) { + LoggerFactory.getLogger(this.getClass()).warn("Can't inject into setter " + method.getName(), e); + } + } + } + } + + /** + * Check if a method can be used with injected classes. + * The method must have the annotation @Inject. + * Return also true to is the method have the @Inject and no parameters + * + * @param method The method to check + * @return {@code true} is the method can be used + */ + public Boolean isMethodInjectable(Method method) { + return isMethodInjectable(method, false); + } + + /** + * Check if a method can be used with injected classes. + * + * @param method The method to check + * @param force If true, the @Inject annotation presence is not checked + * @return {@code true} if the method can be used + */ + public Boolean isMethodInjectable(Method method, Boolean force) { + if (!method.isAnnotationPresent(Inject.class) && !force) { + return false; + } + for (Class variable : method.getParameterTypes()) { + if (!isInjectable(variable)) { + return false; + } + } + return true; + } + + /** + * Indicate if the injector will inject into object properties + * + * @return {@code true} if the injection is active + */ + public Boolean getInjectProperties() { + return injectProperties; + } + + /** + * Indicate if the injector should inject public properties + * + * @param injectProperties {@code true} to activate + */ + public void setInjectProperties(Boolean injectProperties) { + this.injectProperties = injectProperties; + } + + /** + * Indicate if the injector will inject into object setters + * + * @return {@code true} if the injection is active + */ + public Boolean getInjectSetters() { + return injectSetters; + } + + /** + * Indicate if the injector should inject into setters + * + * @param injectSetters {@code true} to activate + */ + public void setInjectSetters(Boolean injectSetters) { + this.injectSetters = injectSetters; + } + + @Override + public Injector clone() throws CloneNotSupportedException { + Injector clone = (Injector) super.clone(); + clone.workingPackages.addAll(workingPackages); + clone.mapping.putAll(mapping); + clone.injectProperties = injectProperties; + clone.injectSetters = injectSetters; + + return clone; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Injector injector = (Injector) o; + + if (!mapping.equals(injector.mapping)) return false; + if (!workingPackages.equals(injector.workingPackages)) return false; + if (!injectProperties.equals(injector.injectProperties)) return false; + return injectSetters.equals(injector.injectSetters); + } +} diff --git a/src/test/java/io/github/macfja/injector/InjectionUnitTest.java b/src/test/java/io/github/macfja/injector/InjectionUnitTest.java new file mode 100644 index 0000000..1f21db7 --- /dev/null +++ b/src/test/java/io/github/macfja/injector/InjectionUnitTest.java @@ -0,0 +1,200 @@ +package io.github.macfja.injector; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +public class InjectionUnitTest { + private static Injector parent; + + @BeforeClass + public static void beforeSetup() { + parent = new Injector("io.github"); + } + + @Test + public void testSingletonConstructor() { + int expected = TestIUSingleton.getStaticCount() + 1; + TestIUSingleton singleton = new TestIUSingleton(); + InjectionUnit unit = new InjectionUnit(singleton); + + Assert.assertEquals(expected, singleton.getCount()); + try { + Assert.assertEquals(expected, ((TestIUSingleton) unit.get(parent)).getCount()); + Assert.assertEquals(expected, ((TestIUSingleton) unit.get(parent)).getCount()); + Assert.assertEquals(expected, ((TestIUSingleton) unit.get(parent)).getCount()); + Assert.assertEquals(expected, ((TestIUSingleton) unit.get(parent)).getCount()); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + Assert.fail(); + } + } + + @Test + public void testConstructorTypeSingleton() { + int expected = TestIUSingleton.getStaticCount() + 1; + InjectionUnit unit = new InjectionUnit(TestIUSingleton.class, InjectionUnit.Instantiation.Singleton); + try { + Assert.assertEquals(expected - 1, TestIUSingleton.getStaticCount()); + Assert.assertEquals(expected, ((TestIUSingleton) unit.get(parent)).getCount()); + Assert.assertEquals(expected, TestIUSingleton.getStaticCount()); + Assert.assertEquals(expected, ((TestIUSingleton) unit.get(parent)).getCount()); + Assert.assertEquals(expected, TestIUSingleton.getStaticCount()); + Assert.assertEquals(expected, ((TestIUSingleton) unit.get(parent)).getCount()); + Assert.assertEquals(expected, TestIUSingleton.getStaticCount()); + Assert.assertEquals(expected, ((TestIUSingleton) unit.get(parent)).getCount()); + Assert.assertEquals(expected, TestIUSingleton.getStaticCount()); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + Assert.fail(); + } + } + + @Test + public void testConstructorTypeNewInstance() { + int expected = TestIUNewInstance.getStaticCount() + 1; + InjectionUnit unit = new InjectionUnit(TestIUNewInstance.class, InjectionUnit.Instantiation.NewInstance); + try { + Assert.assertEquals(expected - 1, TestIUNewInstance.getStaticCount()); + Assert.assertEquals(expected, ((TestIUNewInstance) unit.get(parent)).getCount()); + Assert.assertEquals(expected, TestIUNewInstance.getStaticCount()); + expected++; + Assert.assertEquals(expected, ((TestIUNewInstance) unit.get(parent)).getCount()); + Assert.assertEquals(expected, TestIUNewInstance.getStaticCount()); + expected++; + Assert.assertEquals(expected, ((TestIUNewInstance) unit.get(parent)).getCount()); + Assert.assertEquals(expected, TestIUNewInstance.getStaticCount()); + expected++; + Assert.assertEquals(expected, ((TestIUNewInstance) unit.get(parent)).getCount()); + Assert.assertEquals(expected, TestIUNewInstance.getStaticCount()); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + Assert.fail(); + } + } + + @Test + public void testStaticMethodIsInstantiable() { + Assert.assertTrue(InjectionUnit.isInstantiable(TestIUSingleton.class, parent)); + Assert.assertFalse(InjectionUnit.isInstantiable(TestIUPrivateConstructor.class, parent)); + } + + @Test + public void testStaticMethodIsConstructorInjectable() { + try { + Constructor singletonConstructor = TestIUSingleton.class.getConstructor(); + Assert.assertTrue(InjectionUnit.isConstructorInjectable(singletonConstructor, parent)); + } catch (NoSuchMethodException e) { + Assert.fail(e.getClass().getName()); + } + + try { + Constructor uninjectable = TestIUUnInjectableConstructor.class.getConstructor(java.awt.Button.class); + Assert.assertFalse(InjectionUnit.isConstructorInjectable(uninjectable, parent)); + } catch (NoSuchMethodException e) { + Assert.fail(e.getClass().getName()); + } + + } + + @Test + public void testMethodBuild() { + InjectionUnit unit = new InjectionUnit(TestIUNoConstructor.class, InjectionUnit.Instantiation.NewInstance); + try { + TestIUNoConstructor object = (TestIUNoConstructor) unit.get(parent); + Assert.assertNotNull(object); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + Assert.fail(); + } + + unit = new InjectionUnit(TestIUUnInjectableConstructor.class, InjectionUnit.Instantiation.NewInstance); + try { + TestIUUnInjectableConstructor object = (TestIUUnInjectableConstructor) unit.get(parent); + Assert.fail(); + } catch (IllegalAccessException | InvocationTargetException e) { + Assert.fail(); + } catch (InstantiationException e) { + Assert.assertTrue(true); + } + } + + @Test + public void testMethodRunConstructor() { + InjectionUnit unit = new InjectionUnit(TestIUMultipleParamConstructor.class, InjectionUnit.Instantiation.NewInstance); + try { + TestIUMultipleParamConstructor object = (TestIUMultipleParamConstructor) unit.get(parent); + Assert.assertNotNull(object); + } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) { + Assert.fail(); + } + } + + @Test + public void testMethodIsInstantiable() { + InjectionUnit unit = new InjectionUnit(TestIUSingleton.class, InjectionUnit.Instantiation.Singleton); + Assert.assertTrue(unit.isInstantiable(parent)); + + unit = new InjectionUnit(TestIUPrivateConstructor.class, InjectionUnit.Instantiation.NewInstance); + Assert.assertFalse(unit.isInstantiable(parent)); + } + + @Test + public void testMethodClone() { + InjectionUnit unit = new InjectionUnit(new TestIUSingleton()); + try { + InjectionUnit clone = unit.clone(); + } catch (CloneNotSupportedException e) { + Assert.fail(); + } + } +} + +class TestIUSingleton { + private static int count = 0; + + public TestIUSingleton() { + count++; + } + + public int getCount() { + return count; + } + + public static int getStaticCount() { + return count; + } +} + +class TestIUNewInstance { + private static int count = 0; + + public TestIUNewInstance() { + count++; + } + + public int getCount() { + return count; + } + + public static int getStaticCount() { + return count; + } +} + +class TestIUPrivateConstructor { + private TestIUPrivateConstructor() { + } +} + +class TestIUUnInjectableConstructor { + public TestIUUnInjectableConstructor(java.awt.Button button) { + } +} + +class TestIUNoConstructor { +} + +class TestIUMultipleParamConstructor { + public TestIUMultipleParamConstructor(TestIUNewInstance instance1, TestIUNewInstance instance2) { + } +} \ No newline at end of file diff --git a/src/test/java/io/github/macfja/injector/InjectorTest.java b/src/test/java/io/github/macfja/injector/InjectorTest.java new file mode 100644 index 0000000..6fdd483 --- /dev/null +++ b/src/test/java/io/github/macfja/injector/InjectorTest.java @@ -0,0 +1,301 @@ +package io.github.macfja.injector; + +import org.junit.Assert; +import org.junit.Test; + +import javax.inject.Inject; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class InjectorTest { + @Test + public void testConstructors() { + Injector simple = new Injector("io.github"); + Assert.assertNotNull(simple); + + Injector multiple = new Injector(new HashSet(Arrays.asList("io.github", "io.github.macfja"))); + Assert.assertNotNull(multiple); + } + + @Test + public void testMethodAddMappingSingleton() { + int startAt = TestISingleton.getCount(); + Injector simple = new Injector("io.github"); + simple.addMapping(new TestISingleton()); + Assert.assertEquals(startAt + 1, TestISingleton.getCount()); + simple.get(TestISingleton.class); + Assert.assertEquals(startAt + 1, TestISingleton.getCount()); + } + + @Test + public void testMethodAddMappingNormalSingleton() { + int startAt = TestISingleton.getCount(); + Injector simple = new Injector("io.github"); + simple.addMapping(TestISingleton.class, new InjectionUnit(TestISingleton.class, InjectionUnit.Instantiation.Singleton)); + Assert.assertEquals(startAt, TestISingleton.getCount()); + simple.get(TestISingleton.class); + Assert.assertEquals(startAt + 1, TestISingleton.getCount()); + simple.get(TestISingleton.class); + Assert.assertEquals(startAt + 1, TestISingleton.getCount()); + } + + @Test + public void testMethodAddMappingNormalNewInstance() { + int startAt = TestISingleton.getCount(); + Injector simple = new Injector("io.github"); + simple.addMapping(TestISingleton.class, new InjectionUnit(TestISingleton.class, InjectionUnit.Instantiation.NewInstance)); + Assert.assertEquals(startAt, TestISingleton.getCount()); + simple.get(TestISingleton.class); + Assert.assertEquals(startAt + 1, TestISingleton.getCount()); + simple.get(TestISingleton.class); + Assert.assertEquals(startAt + 2, TestISingleton.getCount()); + } + + @Test + public void testMethodAddWorkingPackage() { + Injector injector = new Injector("io.github"); + Assert.assertFalse(injector.isInjectable(InjectionUnit.class)); + injector.addWorkingPackage("java.lang"); + Assert.assertTrue(injector.isInjectable(InjectionUnit.class)); + } + + @Test + public void testMethodIsInjectable() { + Injector injector = new Injector("io.github"); + + Assert.assertFalse(injector.isInjectable(Integer.TYPE)); + Assert.assertFalse(injector.isInjectable(TestIPrimitive.class)); + + Integer number = 10; + Assert.assertFalse(injector.isInjectable(TestIJavaPackage.class)); + injector.addMapping(number); + Assert.assertTrue(injector.isInjectable(TestIJavaPackage.class)); + } + + @Test + public void testMethodGetSimple() { + Injector injector = new Injector("io.github"); + int count = TestISingleton.getCount(); + Assert.assertEquals(count, TestISingleton.getCount()); + injector.get(TestISingleton.class); + Assert.assertEquals(count + 1, TestISingleton.getCount()); + + injector.addMapping(new TestISingleton()); + Assert.assertEquals(count + 2, TestISingleton.getCount()); + injector.get(TestISingleton.class); + Assert.assertEquals(count + 2, TestISingleton.getCount()); + + Assert.assertNull(injector.get(TestIPrimitive.class)); + } + + @Test + public void testMethodGetComplex() { + Injector injector = new Injector("io.github"); + + Assert.assertNull(injector.get(TestIJavaPackage.class)); + + Assert.assertNotNull(injector.get(TestIJavaPackage.class, 10)); + } + + @Test + public void testInjectIntoProperties() { + Injector injector = new Injector("io.github"); + TestIInjections testIProperties = new TestIInjections(); + + injector.injectIntoProperties(testIProperties); + + Assert.assertNotNull(testIProperties.getPublicProp()); + Assert.assertNotNull(testIProperties.getProtectedProp()); + Assert.assertNotNull(testIProperties.getPackageProp()); + Assert.assertNull(testIProperties.getPrivateProp()); + } + + @Test + public void testInjectIntoMethod() { + Injector injector = new Injector("io.github"); + TestIInjections testIProperties = new TestIInjections(); + + Set methods = new HashSet<>(); + methods.addAll(Arrays.asList(testIProperties.getClass().getMethods())); + methods.addAll(Arrays.asList(testIProperties.getClass().getDeclaredMethods())); + for (Method field : methods) { + try { + injector.injectIntoMethod(testIProperties, field); + } catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException e) { + // noop + } + } + + Assert.assertNotNull(testIProperties.getPublicProp()); + Assert.assertNotNull(testIProperties.getProtectedProp()); + Assert.assertNotNull(testIProperties.getPackageProp()); + Assert.assertNull(testIProperties.getPrivateProp()); + } + + @Test + public void testInjectIntoMethodName() { + Injector injector = new Injector("io.github"); + TestIInjections testIProperties = new TestIInjections(); + + List methods = new ArrayList<>(Arrays.asList( + "setPublicProp", + "setProtectedProp", + "setPackageProp", + "setPrivateProp" + )); + for (String field : methods) { + try { + injector.injectIntoMethodName(testIProperties, field); + } catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) { + // noop + } + } + + Assert.assertNotNull(testIProperties.getPublicProp()); + Assert.assertNull(testIProperties.getProtectedProp()); + Assert.assertNull(testIProperties.getPackageProp()); + Assert.assertNull(testIProperties.getPrivateProp()); + } + + @Test + public void testMethodInjectSetters() { + Injector injector = new Injector("io.github"); + TestIInjections testIProperties = new TestIInjections(); + + injector.injectIntoSetters(testIProperties); + + Assert.assertNotNull(testIProperties.getPublicProp()); + Assert.assertNotNull(testIProperties.getProtectedProp()); + Assert.assertNotNull(testIProperties.getPackageProp()); + Assert.assertNull(testIProperties.getPrivateProp()); + } + + @Test + public void testMethodIsMethodInjectable() { + Injector injector = new Injector("io.github"); + TestIInjections testIProperties = new TestIInjections(); + + Method[] methods = testIProperties.getClass().getMethods(); + + for (Method method : methods) { + if (Arrays.asList("setPublicProp", "setProtectedProp", "setPackageProp").contains(method.getName())) { + Assert.assertTrue(injector.isMethodInjectable(method)); + } else { + Assert.assertFalse(injector.isMethodInjectable(method)); + } + } + + for (Method method : methods) { + if (Arrays.asList("setPublicProp", "setProtectedProp", "setPackageProp", "getPublicProp", "getProtectedProp", "getPackageProp", "getPrivateProp").contains(method.getName())) { + Assert.assertTrue(injector.isMethodInjectable(method, true)); + } + if (Objects.equals(method.getName(), "equals")) { + Assert.assertFalse(injector.isMethodInjectable(method, true)); + Assert.assertFalse(injector.isMethodInjectable(method, false)); + } + } + } + + @Test + public void testPropertyInjectProperties() { + Injector injector = new Injector("io.github"); + Assert.assertTrue(injector.getInjectProperties()); + injector.setInjectProperties(false); + Assert.assertFalse(injector.getInjectProperties()); + } + + @Test + public void testPropertyInjectSetters() { + Injector injector = new Injector("io.github"); + Assert.assertTrue(injector.getInjectSetters()); + injector.setInjectSetters(false); + Assert.assertFalse(injector.getInjectSetters()); + } + + @Test + public void testMethodCloneEquals() { + Injector injector = new Injector("io.github"); + try { + Injector clone = injector.clone(); + Assert.assertEquals(injector, clone); + Assert.assertNotSame(injector, clone); + } catch (CloneNotSupportedException e) { + Assert.fail(); + } + } +} + +class TestISingleton { + private static int count = 0; + + public TestISingleton() { + count++; + } + + public static int getCount() { + return count; + } +} + +class TestIPrimitive { + public TestIPrimitive(int value) { + } +} + +class TestIJavaPackage { + public TestIJavaPackage(Integer value) { + } +} + +class TestIInjections { + @Inject + public TestISingleton publicProp; + @Inject + protected TestISingleton protectedProp; + @Inject + TestISingleton packageProp; + @Inject + private TestISingleton privateProp; + + public TestISingleton getPublicProp() { + return publicProp; + } + + public TestISingleton getProtectedProp() { + return protectedProp; + } + + public TestISingleton getPackageProp() { + return packageProp; + } + + public TestISingleton getPrivateProp() { + return privateProp; + } + + @Inject + public void setPublicProp(TestISingleton publicProp) { + this.publicProp = publicProp; + } + + @Inject + protected void setProtectedProp(TestISingleton protectedProp) { + this.protectedProp = protectedProp; + } + + @Inject + void setPackageProp(TestISingleton packageProp) { + this.packageProp = packageProp; + } + + @Inject + private void setPrivateProp(TestISingleton privateProp) { + this.privateProp = privateProp; + } +} \ No newline at end of file