GuiceInjectorFactory.java
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 }