View Javadoc
1   /**
2    * This file Copyright (c) 2017-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.framework.ioc;
35  
36  import static info.magnolia.ui.framework.ioc.GuiceSpi.rawType;
37  
38  import info.magnolia.objectfactory.Components;
39  
40  import java.lang.reflect.Constructor;
41  import java.util.ArrayList;
42  import java.util.Arrays;
43  import java.util.Iterator;
44  import java.util.List;
45  import java.util.Objects;
46  import java.util.Optional;
47  
48  import javax.inject.Inject;
49  
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  import com.google.common.collect.ArrayListMultimap;
54  import com.google.common.collect.ListMultimap;
55  import com.google.common.collect.Multimaps;
56  import com.google.inject.AbstractModule;
57  import com.google.inject.Binding;
58  import com.google.inject.Key;
59  import com.google.inject.Module;
60  import com.google.inject.TypeLiteral;
61  import com.google.inject.binder.LinkedBindingBuilder;
62  import com.google.inject.binder.ScopedBindingBuilder;
63  import com.google.inject.internal.Scoping;
64  import com.google.inject.spi.Element;
65  import com.google.inject.spi.Elements;
66  import com.google.inject.spi.LinkedKeyBinding;
67  import com.google.inject.spi.ProviderInstanceBinding;
68  import com.google.inject.spi.ProviderKeyBinding;
69  
70  /**
71   * Wrapper Guice module which goes over the bindings of the source module
72   * and analyses the bindings annotated with custom Magnolia
73   * {@link com.google.inject.BindingAnnotation binding annotations}.
74   * <p>
75   * The goal is to detect the bindings which map the same type to different
76   * implementations bound to different Magnolia UI contexts. There are two scenarios
77   * that we are interested in.
78   * </p>
79   * <p>
80   * First scenario is when there are several bindings of the same type to the same
81   * implementation in the same scope with only binding annotations different. In such
82   * case there is no need to have all those bindings, because apparently the
83   * implementation choice does not depend on the UI context. We then remove
84   * all such context-bound bindings and replace them with a single un-annotated
85   * binding.
86   * </p>
87   * <p>
88   * Second scenario is when there are different bindings of the same type
89   * that map it to several different implementations depepnding on the UI
90   * context. In such case our job is make sure that when the un-annotated
91   * injection is supported and the implementation
92   * is chosen automatically (i.e. we're injecting some type {@code Foo}
93   * without any binding annotation specified). In order to provide such
94   * support we bind the un-annotated type to {@link UiContextApplyingProvider}.
95   * <strong>NOTE:</strong> there is a caveat with such an approach: without
96   * proper care it might cause infinite injection cycle when some non-abstract
97   * types are bound to themselves.
98   * </p>
99   * <p>For example imagine a non-abstract type {@code Foo} which is bound to
100  * itself in app context {@code Foo@App -> Foo} and to {@code FooBar} in
101  * sub-app scope {@code Foo@SubApp -> FooBar}. In such case un-annotated {@code Foo}
102  * will end up being bound to the provider {@code Foo -> UiInjectionContextResolvingProvider}.
103  * However, when in app context, the request to inject {@code Foo} through the
104  * provider will boil down to {@code Foo} again, which will trigger the provider
105  * again and again and so on. In order to avoid that, we detect such cases
106  * and bind non-abstract types with annotation via {@link com.google.inject.spi.ConstructorBinding}.
107  * </p>
108  */
109 final class DeflateUiContextDependentBindings extends AbstractModule {
110 
111     private static final Logger log = LoggerFactory.getLogger(DeflateUiContextDependentBindings.class);
112 
113     private List<Element> sourceElements;
114 
115     DeflateUiContextDependentBindings(Module sourceModule) {
116         this.sourceElements = Elements.getElements(sourceModule);
117     }
118 
119     @Override
120     @SuppressWarnings("unchecked")
121     protected void configure() {
122         final List<Element> elementsToWrite = new ArrayList<>(sourceElements);
123 
124         // Group all the module bindings by the raw type of the key
125         final ListMultimap<TypeLiteral, Binding> annotatedBindings = ArrayListMultimap.create();
126         GuiceSpi.getBindings(sourceElements).forEach(binding -> {
127             final Key<?> key = binding.getKey();
128             if (key.getAnnotation() instanceof UiContextAnnotation) {
129                 annotatedBindings.put(key.getTypeLiteral(), binding);
130             }
131         });
132 
133         Multimaps.asMap(annotatedBindings).forEach((typeLiteral, bindings) -> {
134             // If all the bindings are the same then we replace them
135             // all with a single one that has no binding annotation at all
136             if (mayDeflateBindings(bindings)) {
137                 elementsToWrite.removeAll(bindings);
138 
139                 // if the 'bare' type literal is already bound to something - nothing to do here, simply discarding the
140                 // context-specific bindings is enough
141                 if (GuiceSpi.findBinding(sourceElements, Key.get(typeLiteral)).isPresent()) {
142                     return;
143                 }
144 
145                 // We just use the first binding from the list as a 'sample'
146                 final Scoping scope = GuiceSpi.resolveScope(bindings.get(0));
147                 GuiceSpi.<ScopedBindingBuilder> inspect(bindings.get(0))
148                         // provider key binding is replaced by the same binding, but we don't use binding annotation for the key anymore
149                         .onProviderKeyBinding(binding -> bind(typeLiteral).toProvider(binding.getProviderKey()))
150                         // provider instance binding is replaced by the same binding, but we don't use binding annotation for the key anymore
151                         .onProviderInstanceBinding(binding -> bind(typeLiteral).toProvider(binding.getUserSuppliedProvider()))
152                         // interface-to-implementation binding has to be handled carefully: binding of type Key<Foo@App> -> Key<Foo> will work fine,
153                         // but if we replace it with Key<Foo> -> Key<Foo>, Guice will bail with a self-bound type error. In such case the type
154                         // should be bound without a target.
155                         .onLinkedBinding(binding -> {
156                             final Key linkedKey = binding.getLinkedKey();
157                             final Key sourceKey = Key.get(typeLiteral);
158 
159                             if (linkedKey.equals(sourceKey)) {
160                                 return bind(typeLiteral);
161                             } else {
162                                 return bind(typeLiteral).to(linkedKey);
163                             }
164                         })
165                         .visit()
166                         .ifPresent(scopedBindingBuilder -> {
167                             if (!Scoping.UNSCOPED.equals(scope)) {
168                                 scope.applyTo(scopedBindingBuilder);
169                             }
170                         });
171 
172             } else {
173                 // If UI context actually does matter then bind the un-annotated type to the special provider...
174                 final UiContextApplyingProvider resolvingProvider = UiContextApplyingProvider.of(typeLiteral.getRawType());
175                 // ...but first make that any annotated binding does use the current type as implementation (that would lead to circular dependencies!),
176                 // in order to avoid that - replace such linked key bindings with a c-tor binding (see #bindNonAbstractType(...))
177                 bindings.forEach(binding -> GuiceSpi.<Void>inspect(binding)
178                         .consumeLinkedBinding(linkedKeyBinding -> {
179                             // Handle non-abstract self-bindings separately
180                             Class rawType = rawType(linkedKeyBinding.getLinkedKey());
181                             Key rawKey = Key.get(rawType);
182                             if (Objects.equals(typeLiteral.getRawType(), rawType)) {
183                                 elementsToWrite.remove(linkedKeyBinding);
184                                 elementsToWrite.removeIf(element -> element instanceof Binding && ((Binding) element).getKey().equals(rawKey));
185                                 bindNonAbstractType(linkedKeyBinding.getKey(), GuiceSpi.resolveScope(linkedKeyBinding));
186                             }
187                         })
188                         .visit());
189                 bind(typeLiteral).toProvider(resolvingProvider);
190             }
191         });
192 
193         elementsToWrite.forEach(element -> element.applyTo(binder()));
194     }
195 
196     /**
197      * When binding non-abstract type to itself, enforce the
198      * {@link com.google.inject.spi.ConstructorBinding} to be used in order to avoid the
199      * circular dependencies (described in class JavaDoc).
200      */
201     @SuppressWarnings("unchecked")
202     private <T> void bindNonAbstractType(Key<T> key, Scoping fallbackScope) {
203         Class type = rawType(key);
204         LinkedBindingBuilder bind = bind(key);
205         Key rawKey = Key.get(rawType(key));
206 
207         // First locate possible existing [provider] binding of the type
208         Optional<Binding<T>> existingBinding = GuiceSpi.findBinding(sourceElements, rawKey);
209 
210         // Start re-building the binding
211         Optional<ScopedBindingBuilder> scopedBindingBuilder = existingBinding
212                 .map(GuiceSpi::<ScopedBindingBuilder>inspect)
213                 .flatMap(visitor ->
214                                 visitor.onProviderKeyBinding(providerKeyBinding -> bind.toProvider(providerKeyBinding.getProviderKey()))
215                                        .onProviderInstanceBinding(providerInstanceBinding -> bind.toProvider(providerInstanceBinding.getUserSuppliedProvider()))
216                                        .visit());
217 
218         // If it is there - update the scoping to be applied
219         Scoping scoping = existingBinding
220                 .map(GuiceSpi::resolveScope)
221                 .filter(scope -> scope != Scoping.UNSCOPED)
222                 .orElse(fallbackScope);
223 
224         // Apply resulting scoping to the binding builder
225         scoping.applyTo(scopedBindingBuilder.orElseGet(() -> {
226             // Look-up an @Inject-annotated constructor
227             final Optional<Constructor> injectConstructor =
228                     Arrays.stream(type.getConstructors())
229                             .filter(constructor -> constructor.isAnnotationPresent(Inject.class))
230                             .findFirst();
231             // If it's there - bind the type to the constructor
232             if (injectConstructor.isPresent()) {
233                 return bind.toConstructor(injectConstructor.get());
234             } else {
235                 // otherwise - use the provider which lets the Mgnl factory machinery create an instance
236                 return bind.toProvider(() -> Components.newInstance(type));
237             }
238         }));
239     }
240 
241     /**
242      * Go over all the bindings related to the same type
243      * and see if there's at least one that is different
244      * from the others.
245      */
246     private boolean mayDeflateBindings(List<Binding> bindings) {
247         boolean allBindingsIdentical = true;
248 
249         Binding currentBinding = bindings.get(0);
250         final Iterator<Binding> bindingIterator = bindings.subList(1, bindings.size()).iterator();
251         while (allBindingsIdentical && bindingIterator.hasNext()) {
252             final Binding<?> nextBinding = bindingIterator.next();
253             allBindingsIdentical = isSameBinding(nextBinding, currentBinding);
254             currentBinding = nextBinding;
255         }
256 
257         if (!allBindingsIdentical) {
258             return false;
259         }
260 
261         // If all bindings are the same but none of them is generic
262         // (i.e. the same binding is related for instance to several
263         // specific apps), then do not let such bindings be squashed
264         // into one.
265         for (final Binding<?> binding : bindings) {
266             if (UiAnnotations.cast(binding.getKey().getAnnotation()).isGeneric()) {
267                 return true;
268             }
269         }
270 
271         return false;
272     }
273 
274     private boolean isSameBinding(Binding currentBinding, Binding previousBinding) {
275         return Objects.equals(resolveTarget(currentBinding), resolveTarget(previousBinding)) &&
276                Objects.equals(GuiceSpi.resolveScope(currentBinding), GuiceSpi.resolveScope(previousBinding));
277     }
278 
279     private Object resolveTarget(Binding<?> binding) {
280         return GuiceSpi.inspect(binding)
281                 .onLinkedBinding(LinkedKeyBinding::getLinkedKey)
282                 .onProviderKeyBinding(ProviderKeyBinding::getProviderKey)
283                 .onProviderInstanceBinding(ProviderInstanceBinding::getUserSuppliedProvider)
284                 .visit()
285                 .orElseThrow(() -> new IllegalArgumentException("Unable ot resolve target key of the binding: " + binding));
286     }
287 }