001 /*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2008-2017 the original author or authors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 package org.codehaus.griffon.runtime.injection;
019
020 import com.google.inject.AbstractModule;
021 import com.google.inject.Guice;
022 import com.google.inject.Module;
023 import com.google.inject.TypeLiteral;
024 import com.google.inject.matcher.AbstractMatcher;
025 import com.google.inject.matcher.Matchers;
026 import com.google.inject.spi.InjectionListener;
027 import com.google.inject.spi.ProvisionListener;
028 import com.google.inject.spi.TypeEncounter;
029 import com.google.inject.spi.TypeListener;
030 import griffon.core.ApplicationEvent;
031 import griffon.core.Context;
032 import griffon.core.GriffonApplication;
033 import griffon.core.artifact.GriffonArtifact;
034 import griffon.core.env.ApplicationPhase;
035 import griffon.core.injection.Binding;
036 import griffon.core.injection.Injector;
037 import griffon.core.injection.InjectorFactory;
038 import griffon.exceptions.FieldException;
039 import griffon.exceptions.NewInstanceException;
040 import griffon.inject.Contextual;
041 import org.codehaus.griffon.runtime.core.injection.InjectorProvider;
042 import org.kordamp.jipsy.ServiceProviderFor;
043 import org.slf4j.Logger;
044 import org.slf4j.LoggerFactory;
045
046 import javax.annotation.Nonnull;
047 import javax.annotation.PostConstruct;
048 import javax.inject.Inject;
049 import javax.inject.Singleton;
050 import java.beans.PropertyDescriptor;
051 import java.lang.reflect.AnnotatedElement;
052 import java.lang.reflect.Field;
053 import java.lang.reflect.InvocationTargetException;
054 import java.lang.reflect.Method;
055 import java.lang.reflect.Modifier;
056 import java.util.ArrayList;
057 import java.util.Arrays;
058 import java.util.Collection;
059 import java.util.LinkedHashMap;
060 import java.util.List;
061 import java.util.Map;
062 import java.util.ServiceLoader;
063
064 import static com.google.inject.util.Providers.guicify;
065 import static griffon.util.AnnotationUtils.annotationsOfMethodParameter;
066 import static griffon.util.AnnotationUtils.findAnnotation;
067 import static griffon.util.AnnotationUtils.namesFor;
068 import static griffon.util.AnnotationUtils.sortByDependencies;
069 import static griffon.util.GriffonClassUtils.getAllDeclaredFields;
070 import static griffon.util.GriffonClassUtils.getPropertyDescriptors;
071 import static griffon.util.GriffonClassUtils.invokeAnnotatedMethod;
072 import static griffon.util.GriffonClassUtils.setFieldValue;
073 import static java.util.Arrays.asList;
074 import static java.util.Objects.requireNonNull;
075 import static org.codehaus.griffon.runtime.injection.GuiceInjector.moduleFromBindings;
076
077 /**
078 * @author Andres Almiray
079 * @since 2.0.0
080 */
081 @ServiceProviderFor(InjectorFactory.class)
082 public class GuiceInjectorFactory implements InjectorFactory {
083 private static final Logger LOG = LoggerFactory.getLogger(GuiceInjectorFactory.class);
084
085 @Nonnull
086 @Override
087 public GuiceInjector createInjector(@Nonnull GriffonApplication application, @Nonnull Iterable<Binding<?>> bindings) {
088 requireNonNull(application, "Argument 'application' must not be null");
089 requireNonNull(bindings, "Argument 'bindings' must not be null");
090 InjectorProvider injectorProvider = new InjectorProvider();
091 GuiceInjector injector = createModules(application, injectorProvider, bindings);
092 injectorProvider.setInjector(injector);
093 return injector;
094 }
095
096 private GuiceInjector createModules(@Nonnull final GriffonApplication application, @Nonnull final InjectorProvider injectorProvider, @Nonnull Iterable<Binding<?>> bindings) {
097 final InjectionListener<GriffonArtifact> injectionListener = new InjectionListener<GriffonArtifact>() {
098 @Override
099 public void afterInjection(GriffonArtifact injectee) {
100 application.getEventRouter().publishEvent(
101 ApplicationEvent.NEW_INSTANCE.getName(),
102 asList(injectee.getClass(), injectee)
103 );
104 }
105 };
106
107 final InjectionListener<Object> postConstructorInjectorListener = new InjectionListener<Object>() {
108 @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
109 @Override
110 public void afterInjection(Object injectee) {
111 resolveContextualInjections(injectee, application);
112 resolveConfigurationInjections(injectee, application);
113 invokeAnnotatedMethod(injectee, PostConstruct.class);
114 }
115 };
116
117 final InstanceTracker instanceTracker = new InstanceTracker();
118 Module injectorModule = new AbstractModule() {
119 @Override
120 protected void configure() {
121 bind(Injector.class)
122 .toProvider(guicify(injectorProvider))
123 .in(Singleton.class);
124
125 bindListener(new AbstractMatcher<TypeLiteral<?>>() {
126 public boolean matches(TypeLiteral<?> typeLiteral) {
127 return GriffonArtifact.class.isAssignableFrom(typeLiteral.getRawType());
128 }
129 }, new TypeListener() {
130 @SuppressWarnings("unchecked")
131 @Override
132 public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
133 if (GriffonArtifact.class.isAssignableFrom(type.getRawType())) {
134 TypeEncounter<GriffonArtifact> artifactEncounter = (TypeEncounter<GriffonArtifact>) encounter;
135 artifactEncounter.register(injectionListener);
136 }
137 }
138 }
139 );
140
141 bindListener(Matchers.any(), new TypeListener() {
142 @Override
143 public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
144 encounter.register(postConstructorInjectorListener);
145 }
146 });
147
148 bindListener(Matchers.any(), new ProvisionListener() {
149 @Override
150 public <T> void onProvision(ProvisionInvocation<T> provision) {
151 instanceTracker.track(provision.getBinding(), provision.provision());
152 }
153 });
154 }
155 };
156
157 Collection<Module> modules = new ArrayList<>();
158 modules.add(injectorModule);
159 modules.add(moduleFromBindings(bindings));
160
161 List<Module> loadedModules = new ArrayList<>();
162 ServiceLoader<Module> moduleLoader = ServiceLoader.load(Module.class, getClass().getClassLoader());
163 for (Module module : moduleLoader) {
164 LOG.trace("Adding module {}", module);
165 loadedModules.add(module);
166 }
167 Map<String, Module> sortedModules = sortByDependencies(loadedModules, "Module", "module");
168 modules.addAll(sortedModules.values());
169
170 com.google.inject.Injector injector = Guice.createInjector(modules);
171 instanceTracker.setInjector(injector);
172 return new GuiceInjector(instanceTracker);
173 }
174
175 protected void resolveContextualInjections(@Nonnull Object injectee, @Nonnull GriffonApplication application) {
176 if (application.getPhase() == ApplicationPhase.INITIALIZE || injectee instanceof GriffonArtifact) {
177 // skip
178 return;
179 }
180
181 Map<String, Field> fields = new LinkedHashMap<>();
182 for (Field field : getAllDeclaredFields(injectee.getClass())) {
183 fields.put(field.getName(), field);
184 }
185
186 Map<String, InjectionPoint> injectionPoints = new LinkedHashMap<>();
187 for (PropertyDescriptor descriptor : getPropertyDescriptors(injectee.getClass())) {
188 Method method = descriptor.getWriteMethod();
189 if (method == null || isInjectable(method)) { continue; }
190 boolean nullable = method.getAnnotation(Nonnull.class) == null && findAnnotation(annotationsOfMethodParameter(method, 0), Nonnull.class) == null;
191 InjectionPoint.Type type = resolveType(method);
192 Field field = fields.get(descriptor.getName());
193 if (field != null && type == InjectionPoint.Type.OTHER) {
194 type = resolveType(field);
195 nullable = field.getAnnotation(Nonnull.class) == null;
196 }
197 injectionPoints.put(descriptor.getName(), new MethodInjectionPoint(descriptor.getName(), nullable, method, type));
198 }
199
200 for (Field field : getAllDeclaredFields(injectee.getClass())) {
201 if (Modifier.isStatic(field.getModifiers()) || isInjectable(field)) { continue; }
202 if (!injectionPoints.containsKey(field.getName())) {
203 boolean nullable = field.getAnnotation(Nonnull.class) == null;
204 InjectionPoint.Type type = resolveType(field);
205 injectionPoints.put(field.getName(), new FieldInjectionPoint(field.getName(), nullable, field, type));
206 }
207 }
208
209 for (InjectionPoint ip : injectionPoints.values()) {
210 ip.apply(application.getContext(), injectee);
211 }
212 }
213
214 @Nonnull
215 protected InjectionPoint.Type resolveType(@Nonnull AnnotatedElement element) {
216 if (isContextual(element)) {
217 return InjectionPoint.Type.CONTEXTUAL;
218 }
219 return InjectionPoint.Type.OTHER;
220 }
221
222 protected boolean isContextual(AnnotatedElement element) {
223 return element != null && element.getAnnotation(Contextual.class) != null;
224 }
225
226 protected boolean isInjectable(AnnotatedElement element) {
227 return element != null && element.getAnnotation(Inject.class) != null;
228 }
229
230 protected void resolveConfigurationInjections(@Nonnull Object injectee, @Nonnull GriffonApplication application) {
231 if (application.getPhase() == ApplicationPhase.INITIALIZE || injectee instanceof GriffonArtifact) {
232 // skip
233 return;
234 }
235 application.getConfigurationManager().injectConfiguration(injectee);
236 }
237
238 protected abstract static class InjectionPoint {
239 protected final String name;
240 protected final boolean nullable;
241 protected final Type type;
242
243 protected InjectionPoint(String name, boolean nullable, Type type) {
244 this.name = name;
245 this.nullable = nullable;
246 this.type = type;
247 }
248
249 protected enum Type {
250 CONTEXTUAL,
251 OTHER
252 }
253
254 protected abstract void apply(@Nonnull Context context, @Nonnull Object instance);
255 }
256
257 protected static class FieldInjectionPoint extends InjectionPoint {
258 protected final Field field;
259
260 protected FieldInjectionPoint(String name, boolean nullable, Field field, Type type) {
261 super(name, nullable, type);
262 this.field = field;
263 }
264
265 @Override
266 protected void apply(@Nonnull Context context, @Nonnull Object instance) {
267 if (type == Type.CONTEXTUAL) {
268 String[] keys = namesFor(field);
269 Object argValue = null;
270
271 for (String key : keys) {
272 if (context.containsKey(key)) {
273 argValue = context.get(key);
274 break;
275 }
276 }
277
278 try {
279 if (argValue == null) {
280 if (!nullable) {
281 throw new IllegalStateException("Could not find an instance of type " +
282 field.getType().getName() + " under keys '" + Arrays.toString(keys) +
283 "' in the application context to be injected on field '" + field.getName() +
284 "' in " + instance.getClass().getName() + ". Field does not accept null values.");
285 }
286 return;
287 }
288
289 setFieldValue(instance, name, argValue);
290 } catch (IllegalStateException | FieldException e) {
291 throw new NewInstanceException(instance.getClass(), e);
292 }
293 }
294 }
295 }
296
297 protected static class MethodInjectionPoint extends InjectionPoint {
298 protected final Method method;
299
300 protected MethodInjectionPoint(String name, boolean nullable, Method method, Type type) {
301 super(name, nullable, type);
302 this.method = method;
303 }
304
305 @Override
306 protected void apply(@Nonnull Context context, @Nonnull Object instance) {
307 if (type == Type.CONTEXTUAL) {
308 String[] keys = namesFor(method);
309 Object argValue = null;
310
311 for (String key : keys) {
312 if (context.containsKey(key)) {
313 argValue = context.get(key);
314 break;
315 }
316 }
317
318 try {
319 if (argValue == null) {
320 if (!nullable) {
321 throw new IllegalStateException("Could not find an instance of type " +
322 method.getParameterTypes()[0].getName() + " under keys '" + Arrays.toString(keys) +
323 "' in the application context to be injected on property '" + name +
324 "' in " + instance.getClass().getName() + "). Property does not accept null values.");
325 } return;
326 }
327
328 method.invoke(instance, argValue);
329 } catch (IllegalStateException | IllegalAccessException | InvocationTargetException e) {
330 throw new NewInstanceException(instance.getClass(), e);
331 }
332 }
333 }
334 }
335 }
|