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 info.magnolia.objectfactory.Components;
37  
38  import java.lang.reflect.Constructor;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Iterator;
42  import java.util.List;
43  import java.util.Objects;
44  import java.util.Optional;
45  
46  import javax.inject.Inject;
47  
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  import com.google.common.collect.ArrayListMultimap;
52  import com.google.common.collect.ListMultimap;
53  import com.google.common.collect.Multimaps;
54  import com.google.inject.AbstractModule;
55  import com.google.inject.Binding;
56  import com.google.inject.Key;
57  import com.google.inject.Module;
58  import com.google.inject.TypeLiteral;
59  import com.google.inject.binder.ScopedBindingBuilder;
60  import com.google.inject.internal.Scoping;
61  import com.google.inject.spi.Element;
62  import com.google.inject.spi.Elements;
63  import com.google.inject.spi.LinkedKeyBinding;
64  import com.google.inject.spi.ProviderInstanceBinding;
65  import com.google.inject.spi.ProviderKeyBinding;
66  
67  /**
68   * Wrapper Guice module which goes over the bindings of the source module
69   * and analyses the bindings annotated with custom Magnolia
70   * {@link com.google.inject.BindingAnnotation binding annotations}.
71   * <p>
72   * The goal is to detect the bindings which map the same type to different
73   * implementations bound to different Magnolia UI contexts. There are two scenarios
74   * that we are interested in.
75   * </p>
76   * <p>
77   * First scenario is when there are several bindings of the same type to the same
78   * implementation in the same scope with only binding annotations different. In such
79   * case there is no need to have all those bindings, because apparently the
80   * implementation choice does not depend on the UI context. We then remove
81   * all such context-bound bindings and replace them with a single un-annotated
82   * binding.
83   * </p>
84   * <p>
85   * Second scenario is when there are different bindings of the same type
86   * that map it to several different implementations depepnding on the UI
87   * context. In such case our job is make sure that when the un-annotated
88   * injection is supported and the implementation
89   * is chosen automatically (i.e. we're injecting some type {@code Foo}
90   * without any binding annotation specified). In order to provide such
91   * support we bind the un-annotated type to {@link UiContextApplyingProvider}.
92   * <strong>NOTE:</strong> there is a caveat with such an approach: without
93   * proper care it might cause infinite injection cycle when some non-abstract
94   * types are bound to themselves.
95   * </p>
96   * <p>For example imagine a non-abstract type {@code Foo} which is bound to
97   * itself in app context {@code Foo@App -> Foo} and to {@code FooBar} in
98   * sub-app scope {@code Foo@SubApp -> FooBar}. In such case un-annotated {@code Foo}
99   * will end up being bound to the provider {@code Foo -> UiInjectionContextResolvingProvider}.
100  * However, when in app context, the request to inject {@code Foo} through the
101  * provider will boil down to {@code Foo} again, which will trigger the provider
102  * again and again and so on. In order to avoid that, we detect such cases
103  * and bind non-abstract types with annotation via {@link com.google.inject.spi.ConstructorBinding}.
104  * </p>
105  */
106 final class DeflateUiContextDependentBindings extends AbstractModule {
107 
108     private static final Logger log = LoggerFactory.getLogger(DeflateUiContextDependentBindings.class);
109 
110     private final Module sourceModule;
111 
112     DeflateUiContextDependentBindings(Module sourceModule) {
113         this.sourceModule = sourceModule;
114     }
115 
116     @Override
117     @SuppressWarnings("unchecked")
118     protected void configure() {
119         final List<Element> sourceElements = Elements.getElements(sourceModule);
120         final List<Element> elementsToWrite = new ArrayList<>(sourceElements);
121 
122         // Group all the module bindings by the raw type of the key
123         final ListMultimap<TypeLiteral, Binding> annotatedBindings = ArrayListMultimap.create();
124         GuiceSpi.getBindings(sourceElements).forEach(binding -> {
125             final Key<?> key = binding.getKey();
126             if (key.getAnnotation() instanceof UiContextAnnotation) {
127                 annotatedBindings.put(key.getTypeLiteral(), binding);
128             }
129         });
130 
131         Multimaps.asMap(annotatedBindings).forEach((typeLiteral, bindings) -> {
132             // If all the bindings are the same then we replace them
133             // all with a single one that has no binding annotation at all
134             if (mayDeflateBindings(bindings)) {
135                 elementsToWrite.removeAll(bindings);
136 
137                 // We just use the first binding from the list as a 'sample'
138                 final Scoping scope = GuiceSpi.resolveScope(bindings.get(0));
139                 GuiceSpi.<ScopedBindingBuilder> inspect(bindings.get(0))
140                         // provider key binding is replaced by the same binding, but we don't use binding annotation for the key anymore
141                         .onProviderKeyBinding(binding -> bind(typeLiteral).toProvider(binding.getProviderKey()))
142                         // provider instance binding is replaced by the same binding, but we don't use binding annotation for the key anymore
143                         .onProviderInstanceBinding(binding -> bind(typeLiteral).toProvider(binding.getUserSuppliedProvider()))
144                         // interface-to-implementation binding has to be handled carefully: binding of type Key<Foo@App> -> Key<Foo> will work fine,
145                         // but if we replace it with Key<Foo> -> Key<Foo>, Guice will bail with a self-bound type error. In such case the type
146                         // should be bound without a target.
147                         .onLinkedBinding(binding -> {
148                             final Key linkedKey = binding.getLinkedKey();
149                             final Key sourceKey = Key.get(typeLiteral);
150 
151                             if (linkedKey.equals(sourceKey)) {
152                                 return bind(typeLiteral);
153                             } else {
154                                 return bind(typeLiteral).to(linkedKey);
155                             }
156                         })
157                         .visit()
158                         .ifPresent(scopedBindingBuilder -> {
159                             if (!Scoping.UNSCOPED.equals(scope)) {
160                                 scope.applyTo(scopedBindingBuilder);
161                             }
162                         });
163 
164             } else {
165                 // If UI context actually does matter then bind the un-annotated type to the special provider...
166                 final UiContextApplyingProvider resolvingProvider = UiContextApplyingProvider.of(typeLiteral.getRawType());
167                 // ...but first make that any annotated binding does use the current type as implementation (that would lead to circular dependencies!),
168                 // in order to avoid that - replace such linked key bindings with a c-tor binding (see #bindNonAbstractType(...))
169                 bindings.forEach(binding -> GuiceSpi.<Void>inspect(binding)
170                         .consumeLinkedBinding(linkedKeyBinding -> {
171                             // Handle non-abstract self-bindings separately
172                             if (Objects.equals(typeLiteral.getRawType(), linkedKeyBinding.getLinkedKey().getTypeLiteral().getRawType())) {
173                                 elementsToWrite.remove(linkedKeyBinding);
174                                 bindNonAbstractType(linkedKeyBinding.getKey(), GuiceSpi.resolveScope(linkedKeyBinding));
175                             }
176                         })
177                         .visit());
178                 bind(typeLiteral).toProvider(resolvingProvider);
179             }
180         });
181 
182         elementsToWrite.forEach(element -> element.applyTo(binder()));
183     }
184 
185     /**
186      * When binding non-abstract type to itself, enforce the
187      * {@link com.google.inject.spi.ConstructorBinding} to be used in order to avoid the
188      * circular dependencies (described in class JavaDoc).
189      */
190     @SuppressWarnings("unchecked")
191     private void bindNonAbstractType(Key key, Scoping scope) {
192         final Class type = key.getTypeLiteral().getRawType();
193 
194         // Look-up an @Inject-annotated constructor
195         final Optional<Constructor> injectConstructor =
196                 Arrays.stream(type.getConstructors())
197                         .filter(constructor -> constructor.isAnnotationPresent(Inject.class))
198                         .findFirst();
199         // If it's there - bind the type to the constructor
200         if (injectConstructor.isPresent()) {
201             scope.applyTo(bind(key).toConstructor(injectConstructor.get()));
202         } else {
203             // otherwise - use the provider which lets the Mgnl factory machinery create an instance
204             scope.applyTo(bind(key).toProvider(() -> Components.newInstance(type)));
205         }
206     }
207 
208     /**
209      * Go over all the bindings related to the same type
210      * and see if there's at least one that is different
211      * from the others.
212      */
213     private boolean mayDeflateBindings(List<Binding> bindings) {
214         boolean allBindingsIdentical = true;
215 
216         Binding currentBinding = bindings.get(0);
217         final Iterator<Binding> bindingIterator = bindings.subList(1, bindings.size()).iterator();
218         while (allBindingsIdentical && bindingIterator.hasNext()) {
219             final Binding<?> nextBinding = bindingIterator.next();
220             allBindingsIdentical = isSameBinding(nextBinding, currentBinding);
221             currentBinding = nextBinding;
222         }
223 
224         if (!allBindingsIdentical) {
225             return false;
226         }
227 
228         // If all bindings are the same but none of them is generic
229         // (i.e. the same binding is related for instance to several
230         // specific apps), then do not let such bindings be squashed
231         // into one.
232         for (final Binding<?> binding : bindings) {
233             if (UiAnnotations.cast(binding.getKey().getAnnotation()).isGeneric()) {
234                 return true;
235             }
236         }
237 
238         return false;
239     }
240 
241     private boolean isSameBinding(Binding currentBinding, Binding previousBinding) {
242         return Objects.equals(resolveTarget(currentBinding), resolveTarget(previousBinding)) &&
243                Objects.equals(GuiceSpi.resolveScope(currentBinding), GuiceSpi.resolveScope(previousBinding));
244     }
245 
246     private Object resolveTarget(Binding<?> binding) {
247         return GuiceSpi.inspect(binding)
248                 .onLinkedBinding(LinkedKeyBinding::getLinkedKey)
249                 .onProviderKeyBinding(ProviderKeyBinding::getProviderKey)
250                 .onProviderInstanceBinding(ProviderInstanceBinding::getUserSuppliedProvider)
251                 .visit()
252                 .orElseThrow(() -> new IllegalArgumentException("Unable ot resolve target key of the binding: " + binding));
253     }
254 }