GuiceInjectorFactory.java
001 /*
002  * SPDX-License-Identifier: Apache-2.0
003  *
004  * Copyright 2008-2018 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 = injectee -> application.getEventRouter().publishEvent(
098             ApplicationEvent.NEW_INSTANCE.getName(),
099             asList(injectee.getClass(), injectee)
100         );
101 
102         final InjectionListener<Object> postConstructorInjectorListener = injectee -> {
103             resolveContextualInjections(injectee, application);
104             resolveConfigurationInjections(injectee, application);
105             invokeAnnotatedMethod(injectee, PostConstruct.class);
106         };
107 
108         final InstanceTracker instanceTracker = new InstanceTracker();
109         Module injectorModule = new AbstractModule() {
110             @Override
111             protected void configure() {
112                 bind(Injector.class)
113                     .toProvider(guicify(injectorProvider))
114                     .in(Singleton.class);
115 
116                 bindListener(new AbstractMatcher<TypeLiteral<?>>() {
117                                  public boolean matches(TypeLiteral<?> typeLiteral) {
118                                      return GriffonArtifact.class.isAssignableFrom(typeLiteral.getRawType());
119                                  }
120                              }new TypeListener() {
121                                  @SuppressWarnings("unchecked")
122                                  @Override
123                                  public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
124                                      if (GriffonArtifact.class.isAssignableFrom(type.getRawType())) {
125                                          TypeEncounter<GriffonArtifact> artifactEncounter = (TypeEncounter<GriffonArtifact>encounter;
126                                          artifactEncounter.register(injectionListener);
127                                      }
128                                  }
129                              }
130                 );
131 
132                 bindListener(Matchers.any()new TypeListener() {
133                     @Override
134                     public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
135                         encounter.register(postConstructorInjectorListener);
136                     }
137                 });
138 
139                 bindListener(Matchers.any()new ProvisionListener() {
140                     @Override
141                     public <T> void onProvision(ProvisionInvocation<T> provision) {
142                         instanceTracker.track(provision.getBinding(), provision.provision());
143                     }
144                 });
145             }
146         };
147 
148         Collection<Module> modules = new ArrayList<>();
149         modules.add(injectorModule);
150         modules.add(moduleFromBindings(bindings));
151 
152         List<Module> loadedModules = new ArrayList<>();
153         ServiceLoader<Module> moduleLoader = ServiceLoader.load(Module.class, getClass().getClassLoader());
154         for (Module module : moduleLoader) {
155             LOG.trace("Adding module {}", module);
156             loadedModules.add(module);
157         }
158         Map<String, Module> sortedModules = sortByDependencies(loadedModules, "Module""module");
159         modules.addAll(sortedModules.values());
160 
161         com.google.inject.Injector injector = Guice.createInjector(modules);
162         instanceTracker.setInjector(injector);
163         return new GuiceInjector(instanceTracker);
164     }
165 
166     protected void resolveContextualInjections(@Nonnull Object injectee, @Nonnull GriffonApplication application) {
167         if (application.getPhase() == ApplicationPhase.INITIALIZE || injectee instanceof GriffonArtifact) {
168             // skip
169             return;
170         }
171 
172         Map<String, Field> fields = new LinkedHashMap<>();
173         for (Field field : getAllDeclaredFields(injectee.getClass())) {
174             fields.put(field.getName(), field);
175         }
176 
177         Map<String, InjectionPoint> injectionPoints = new LinkedHashMap<>();
178         for (PropertyDescriptor descriptor : getPropertyDescriptors(injectee.getClass())) {
179             Method method = descriptor.getWriteMethod();
180             if (method == null || isInjectable(method)) { continue}
181             boolean nullable = method.getAnnotation(Nonnull.class== null && findAnnotation(annotationsOfMethodParameter(method, 0), Nonnull.class== null;
182             InjectionPoint.Type type = resolveType(method);
183             Field field = fields.get(descriptor.getName());
184             if (field != null && type == InjectionPoint.Type.OTHER) {
185                 type = resolveType(field);
186                 nullable = field.getAnnotation(Nonnull.class== null;
187             }
188             injectionPoints.put(descriptor.getName()new MethodInjectionPoint(descriptor.getName(), nullable, method, type));
189         }
190 
191         for (Field field : getAllDeclaredFields(injectee.getClass())) {
192             if (Modifier.isStatic(field.getModifiers()) || isInjectable(field)) { continue}
193             if (!injectionPoints.containsKey(field.getName())) {
194                 boolean nullable = field.getAnnotation(Nonnull.class== null;
195                 InjectionPoint.Type type = resolveType(field);
196                 injectionPoints.put(field.getName()new FieldInjectionPoint(field.getName(), nullable, field, type));
197             }
198         }
199 
200         for (InjectionPoint ip : injectionPoints.values()) {
201             ip.apply(application.getContext(), injectee);
202         }
203     }
204 
205     @Nonnull
206     protected InjectionPoint.Type resolveType(@Nonnull AnnotatedElement element) {
207         if (isContextual(element)) {
208             return InjectionPoint.Type.CONTEXTUAL;
209         }
210         return InjectionPoint.Type.OTHER;
211     }
212 
213     protected boolean isContextual(AnnotatedElement element) {
214         return element != null && element.getAnnotation(Contextual.class!= null;
215     }
216 
217     protected boolean isInjectable(AnnotatedElement element) {
218         return element != null && element.getAnnotation(Inject.class!= null;
219     }
220 
221     protected void resolveConfigurationInjections(@Nonnull Object injectee, @Nonnull GriffonApplication application) {
222         if (application.getPhase() == ApplicationPhase.INITIALIZE || injectee instanceof GriffonArtifact) {
223             // skip
224             return;
225         }
226         application.getConfigurationManager().injectConfiguration(injectee);
227     }
228 
229     protected abstract static class InjectionPoint {
230         protected final String name;
231         protected final boolean nullable;
232         protected final Type type;
233 
234         protected InjectionPoint(String name, boolean nullable, Type type) {
235             this.name = name;
236             this.nullable = nullable;
237             this.type = type;
238         }
239 
240         protected enum Type {
241             CONTEXTUAL,
242             OTHER
243         }
244 
245         protected abstract void apply(@Nonnull Context context, @Nonnull Object instance);
246     }
247 
248     protected static class FieldInjectionPoint extends InjectionPoint {
249         protected final Field field;
250 
251         protected FieldInjectionPoint(String name, boolean nullable, Field field, Type type) {
252             super(name, nullable, type);
253             this.field = field;
254         }
255 
256         @Override
257         protected void apply(@Nonnull Context context, @Nonnull Object instance) {
258             if (type == Type.CONTEXTUAL) {
259                 String[] keys = namesFor(field);
260                 Object argValue = null;
261 
262                 for (String key : keys) {
263                     if (context.containsKey(key)) {
264                         argValue = context.get(key);
265                         break;
266                     }
267                 }
268 
269                 try {
270                     if (argValue == null) {
271                         if (!nullable) {
272                             throw new IllegalStateException("Could not find an instance of type " +
273                                 field.getType().getName() " under keys '" + Arrays.toString(keys+
274                                 "' in the application context to be injected on field '" + field.getName() +
275                                 "' in " + instance.getClass().getName() ". Field does not accept null values.");
276                         }
277                         return;
278                     }
279 
280                     setFieldValue(instance, name, argValue);
281                 catch (IllegalStateException | FieldException e) {
282                     throw new NewInstanceException(instance.getClass(), e);
283                 }
284             }
285         }
286     }
287 
288     protected static class MethodInjectionPoint extends InjectionPoint {
289         protected final Method method;
290 
291         protected MethodInjectionPoint(String name, boolean nullable, Method method, Type type) {
292             super(name, nullable, type);
293             this.method = method;
294         }
295 
296         @Override
297         protected void apply(@Nonnull Context context, @Nonnull Object instance) {
298             if (type == Type.CONTEXTUAL) {
299                 String[] keys = namesFor(method);
300                 Object argValue = null;
301 
302                 for (String key : keys) {
303                     if (context.containsKey(key)) {
304                         argValue = context.get(key);
305                         break;
306                     }
307                 }
308 
309                 try {
310                     if (argValue == null) {
311                         if (!nullable) {
312                             throw new IllegalStateException("Could not find an instance of type " +
313                                 method.getParameterTypes()[0].getName() " under keys '" + Arrays.toString(keys+
314                                 "' in the application context to be injected on property '" + name +
315                                 "' in " + instance.getClass().getName() "). Property does not accept null values.");
316                         return;
317                     }
318 
319                     method.invoke(instance, argValue);
320                 catch (IllegalStateException | IllegalAccessException | InvocationTargetException e) {
321                     throw new NewInstanceException(instance.getClass(), e);
322                 }
323             }
324         }
325     }
326 }