|
| 1 | +package cat.michal.catbase.injector; |
| 2 | + |
| 3 | +import cat.michal.catbase.injector.annotations.*; |
| 4 | +import cat.michal.catbase.injector.exceptions.InjectorException; |
| 5 | +import org.jetbrains.annotations.Nullable; |
| 6 | + |
| 7 | +import java.lang.reflect.*; |
| 8 | +import java.util.*; |
| 9 | + |
| 10 | +public class InjectionContext { |
| 11 | + private final List<Class<?>> classes; |
| 12 | + private final List<Dependency<?>> dependencies; |
| 13 | + |
| 14 | + public InjectionContext(Collection<String> packagePaths, ClassLoader classLoader) { |
| 15 | + this.classes = new ArrayList<>(); |
| 16 | + this.dependencies = new ArrayList<>(); |
| 17 | + ClassFinder classFinder = classLoader == null ? new ClassFinder() : new ClassFinder(classLoader); |
| 18 | + |
| 19 | + packagePaths.forEach(packagePath -> this.classes.addAll(classFinder.findAllClasses(packagePath))); |
| 20 | + } |
| 21 | + |
| 22 | + public void createDependencyTree() { |
| 23 | + this.classes.stream() |
| 24 | + .filter(clazz -> clazz.isAnnotationPresent(Component.class)) |
| 25 | + .filter(clazz -> !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) |
| 26 | + .forEach(clazz -> { |
| 27 | + this.dependencies.add(createDependency(clazz, null, null)); |
| 28 | + Arrays.stream(clazz.getMethods()) |
| 29 | + .filter(method -> method.isAnnotationPresent(Provide.class)) |
| 30 | + .forEach(method -> this.dependencies.add(createDependency(method.getReturnType(), method, method.getAnnotation(Provide.class).value()))); |
| 31 | + |
| 32 | + // Adding external dependencies |
| 33 | + Arrays.stream(clazz.getFields()) |
| 34 | + .filter(field -> Modifier.isStatic(field.getModifiers())) |
| 35 | + .filter(field -> field.isAnnotationPresent(ExternalDependency.class)) |
| 36 | + .forEach(field -> { |
| 37 | + Dependency<?> dependency = createDependency(field.getType(), null, field.getAnnotation(ExternalDependency.class).value()); |
| 38 | + field.setAccessible(true); |
| 39 | + try { |
| 40 | + dependency.setInstance(field.get(null)); |
| 41 | + } catch (IllegalAccessException e) { |
| 42 | + throw new InjectorException("Could not get value of external dependency " + field.getName(), e); |
| 43 | + } |
| 44 | + this.dependencies.add(dependency); |
| 45 | + }); |
| 46 | + }); |
| 47 | + } |
| 48 | + |
| 49 | + public List<Dependency<?>> getDependencies() { |
| 50 | + return this.dependencies; |
| 51 | + } |
| 52 | + |
| 53 | + /** |
| 54 | + * Method that finds the best corresponding dependency to provided class |
| 55 | + * If user provides an abstraction layer, it automatically matches it and tries to find a primary or a named implementation of it |
| 56 | + * @param clazz class your want to find dependency to |
| 57 | + * @return none if there is no dependency connected with your class |
| 58 | + * @param <T> class type |
| 59 | + * |
| 60 | + */ |
| 61 | + @SuppressWarnings("unchecked") |
| 62 | + public <T> Optional<Dependency<T>> findBestMatchingDependency(Class<T> clazz, @Nullable String dependencyName) { |
| 63 | + // class is an abstraction layer |
| 64 | + if(clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { |
| 65 | + List<Dependency<?>> matchingDependencies = this.dependencies.stream() |
| 66 | + .filter(dependency -> clazz.isAssignableFrom(dependency.getClazz())) |
| 67 | + .toList(); |
| 68 | + if(matchingDependencies.isEmpty()) { |
| 69 | + throw new InjectorException("Could not find dependency for type " + clazz.getName()); |
| 70 | + } |
| 71 | + if(matchingDependencies.size() > 1) { |
| 72 | + List<Dependency<?>> nameFiltered = matchingDependencies.stream() |
| 73 | + .filter(dependency -> (dependencyName == null || dependencyName.isEmpty()) || dependency.getName().equals(dependencyName)) |
| 74 | + .toList(); |
| 75 | + if(nameFiltered.isEmpty()) { |
| 76 | + throw new InjectorException("Could not find matching dependency for type "+ clazz.getName() + " with name " + dependencyName); |
| 77 | + } |
| 78 | + |
| 79 | + if(nameFiltered.size() > 1) { |
| 80 | + List<Dependency<?>> primaryFiltered = nameFiltered.stream() |
| 81 | + .filter(dependency -> dependency.getClazz().isAnnotationPresent(Primary.class)) |
| 82 | + .toList(); |
| 83 | + if(primaryFiltered.isEmpty()) { |
| 84 | + throw new InjectorException("Could not find primary implementation for type "+ clazz.getName()); |
| 85 | + } |
| 86 | + if(primaryFiltered.size() > 1) { |
| 87 | + throw new InjectorException("Multiple primary implementations found for type "+ clazz.getName()); |
| 88 | + } |
| 89 | + return Optional.of((Dependency<T>) primaryFiltered.get(0)); |
| 90 | + } else { |
| 91 | + return Optional.of((Dependency<T>) nameFiltered.get(0)); |
| 92 | + } |
| 93 | + } else { |
| 94 | + return Optional.of((Dependency<T>) matchingDependencies.get(0)); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + return this.dependencies.stream() |
| 99 | + .filter(dependency -> dependency.getClazz().equals(clazz)) |
| 100 | + .filter(dependency -> (dependencyName == null || dependencyName.isEmpty()) || dependency.getName().equals(dependencyName)) |
| 101 | + .map(dependency -> (Dependency<T>) dependency) |
| 102 | + .findFirst(); |
| 103 | + } |
| 104 | + |
| 105 | + @SuppressWarnings("unchecked") |
| 106 | + <T> List<Dependency<T>> findAllDependsOfType(Type type, String dependencyName) { |
| 107 | + List<Dependency<T>> depends = new ArrayList<>(); |
| 108 | + if(type instanceof ParameterizedType parameterizedType && Collection.class.isAssignableFrom(((Class<?>) parameterizedType.getRawType()))) { |
| 109 | + Type[] arguments = parameterizedType.getActualTypeArguments(); |
| 110 | + if(arguments.length != 1 || !(arguments[0] instanceof Class<?> abstractionType)) { |
| 111 | + throw new InjectorException("Invalid generic parameters with the collection class " + type.getTypeName()); |
| 112 | + } |
| 113 | + this.dependencies.stream() |
| 114 | + .filter(depend -> abstractionType.isAssignableFrom(depend.getClazz())) |
| 115 | + .forEach(depend -> depends.add((Dependency<T>) depend)); |
| 116 | + } else if (type instanceof Class<?> clazz) { |
| 117 | + this.findBestMatchingDependency((Class<T>) clazz, dependencyName).ifPresent(depends::add); |
| 118 | + } else { |
| 119 | + throw new InjectorException("Dependency with type " + type + " is not a valid class or a collection"); |
| 120 | + } |
| 121 | + |
| 122 | + return depends; |
| 123 | + } |
| 124 | + |
| 125 | + |
| 126 | + @SuppressWarnings("unchecked") |
| 127 | + public <T> List<Dependency<T>> findDependants(Dependency<T> dependency) { |
| 128 | + List<Dependency<T>> dependants = new ArrayList<>(); |
| 129 | + |
| 130 | + Optional<? extends Constructor<?>> validConstructor = getConstructor(dependency); |
| 131 | + validConstructor.ifPresent(constructor -> Arrays.stream(constructor.getAnnotatedParameterTypes()) |
| 132 | + .forEach(type -> { |
| 133 | + Inject inject = type.getAnnotation(Inject.class); |
| 134 | + |
| 135 | + this.findAllDependsOfType(type.getType(), inject == null ? null : inject.value()) |
| 136 | + .forEach(dep -> dependants.add((Dependency<T>) dep)); |
| 137 | + })); |
| 138 | + |
| 139 | + Arrays.stream(dependency.getClazz().getDeclaredFields()) |
| 140 | + .filter(field -> field.isAnnotationPresent(Inject.class)) |
| 141 | + .forEach(field -> this.findAllDependsOfType(field.getGenericType(), field.getAnnotation(Inject.class).value()) |
| 142 | + .forEach(dep -> dependants.add((Dependency<T>) dep))); |
| 143 | + |
| 144 | + return dependants; |
| 145 | + } |
| 146 | + |
| 147 | + |
| 148 | + @SuppressWarnings("unchecked") |
| 149 | + public <T> Optional<Dependency<T>> getDependency(Class<T> clazz) { |
| 150 | + return this.dependencies.stream() |
| 151 | + .filter(dependency -> dependency.getClazz().equals(clazz)) |
| 152 | + .map(dependency -> (Dependency<T>) dependency) |
| 153 | + .findFirst(); |
| 154 | + } |
| 155 | + |
| 156 | + private <T> Dependency<T> createDependency(Class<T> clazz, Method provideMethod, String dependencyName) { |
| 157 | + if(this.containsByClass(clazz)) { |
| 158 | + throw new InjectorException("Dependency with type " + clazz.getName() + " has been added to injector context twice."); |
| 159 | + } |
| 160 | + |
| 161 | + if(dependencyName == null || dependencyName.isEmpty()) { |
| 162 | + if(provideMethod == null) { |
| 163 | + dependencyName = clazz.getAnnotation(Component.class) == null ? clazz.getSimpleName() : clazz.getAnnotation(Component.class).value(); |
| 164 | + } else { |
| 165 | + String provideName = provideMethod.getAnnotation(Provide.class).value(); |
| 166 | + dependencyName = provideName.isEmpty() ? provideMethod.getName() : provideName; |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + Optional<Dependency<?>> depend = findByName(dependencyName); |
| 171 | + if(depend.isPresent()) { |
| 172 | + throw new InjectorException("Dependency with name '" + dependencyName + "' already exists (" + depend.get().getClass().getSimpleName() + ")"); |
| 173 | + } |
| 174 | + return new Dependency<>(dependencyName, clazz, provideMethod); |
| 175 | + } |
| 176 | + |
| 177 | + |
| 178 | + private <T> boolean containsByClass(Class<T> clazz) { |
| 179 | + return this.dependencies.stream() |
| 180 | + .anyMatch(dependency -> dependency.getClazz().equals(clazz)); |
| 181 | + } |
| 182 | + |
| 183 | + private Optional<Dependency<?>> findByName(String injectableName) { |
| 184 | + if(injectableName.isEmpty()) { |
| 185 | + return Optional.empty(); |
| 186 | + } |
| 187 | + |
| 188 | + return this.dependencies.stream() |
| 189 | + .filter(dependency -> dependency.getName().equals(injectableName)) |
| 190 | + .findAny(); |
| 191 | + } |
| 192 | + |
| 193 | + |
| 194 | + @SuppressWarnings("unchecked") |
| 195 | + <T> Optional<Constructor<T>> getConstructor(Dependency<T> dependency) { |
| 196 | + List<Constructor<?>> injectConstructors = Arrays.stream(dependency.getClazz().getConstructors()) |
| 197 | + .filter(constructor -> constructor.isAnnotationPresent(Inject.class)) |
| 198 | + .toList(); |
| 199 | + if(injectConstructors.isEmpty()) { |
| 200 | + return Arrays.stream(dependency.getClazz().getConstructors()) |
| 201 | + .filter(constructorElement -> constructorElement.getParameterTypes().length == 0 |
| 202 | + || Arrays.stream(constructorElement.getGenericParameterTypes()).allMatch(this::isValidInjectableType) |
| 203 | + ) |
| 204 | + .map(constructor -> (Constructor<T>) constructor) |
| 205 | + .findAny(); |
| 206 | + } else { |
| 207 | + return Optional.of((Constructor<T>) injectConstructors.get(0)); |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + private boolean isValidInjectableType(Type type) { |
| 212 | + if(type instanceof ParameterizedType parameterType && Collection.class.isAssignableFrom(((Class<?>) parameterType.getRawType()))) { |
| 213 | + Type[] actualTypeArguments = parameterType.getActualTypeArguments(); |
| 214 | + if (actualTypeArguments.length != 1 || !(actualTypeArguments[0] instanceof Class)) { |
| 215 | + return false; |
| 216 | + } |
| 217 | + |
| 218 | + return this.isValidInjectable((Class<?>) actualTypeArguments[0]); |
| 219 | + } |
| 220 | + if (!(type instanceof Class<?> clazz)) { |
| 221 | + return false; |
| 222 | + } |
| 223 | + |
| 224 | + return this.isValidInjectable(clazz); |
| 225 | + } |
| 226 | + |
| 227 | + private boolean isValidInjectable(Class<?> clazz) { |
| 228 | + return this.findBestMatchingDependency(clazz, null).isPresent(); |
| 229 | + } |
| 230 | +} |
0 commit comments