View Javadoc
1   /**
2    * This file Copyright (c) 2019 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.i18nsystem.bytebudddy;
35  
36  import static info.magnolia.i18nsystem.bytebudddy.AnyTypeInGenericSignatureMatcher.annotationIsPresentInGenericSignature;
37  import static info.magnolia.i18nsystem.bytebudddy.MethodOrSuperMethodMatcher.annotationIsPresentInHierarchy;
38  import static java.util.stream.Collectors.*;
39  import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.Default.WRAPPER;
40  import static net.bytebuddy.implementation.MethodDelegation.to;
41  import static net.bytebuddy.matcher.ElementMatchers.*;
42  
43  import info.magnolia.context.Context;
44  import info.magnolia.dynamic.MagnoliaProxy;
45  import info.magnolia.i18nsystem.ContextLocaleProvider;
46  import info.magnolia.i18nsystem.I18nKeyGenerator;
47  import info.magnolia.i18nsystem.I18nKeyGeneratorFactory;
48  import info.magnolia.i18nsystem.I18nParentable;
49  import info.magnolia.i18nsystem.I18nText;
50  import info.magnolia.i18nsystem.I18nable;
51  import info.magnolia.i18nsystem.I18nizer;
52  import info.magnolia.i18nsystem.TranslationService;
53  
54  import java.lang.reflect.Field;
55  import java.lang.reflect.InvocationTargetException;
56  import java.lang.reflect.Method;
57  import java.util.Collection;
58  import java.util.LinkedHashMap;
59  import java.util.List;
60  import java.util.Map;
61  import java.util.Optional;
62  import java.util.function.BiFunction;
63  
64  import javax.inject.Inject;
65  import javax.inject.Provider;
66  import javax.inject.Singleton;
67  
68  import org.objenesis.Objenesis;
69  import org.objenesis.ObjenesisStd;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  import com.google.common.collect.Collections2;
74  
75  import net.bytebuddy.ByteBuddy;
76  import net.bytebuddy.TypeCache;
77  import net.bytebuddy.dynamic.DynamicType;
78  import net.bytebuddy.implementation.FieldAccessor;
79  import net.bytebuddy.implementation.bind.annotation.AllArguments;
80  import net.bytebuddy.implementation.bind.annotation.Origin;
81  import net.bytebuddy.implementation.bind.annotation.RuntimeType;
82  import net.bytebuddy.implementation.bind.annotation.This;
83  import net.sf.cglib.proxy.Enhancer;
84  
85  /**
86   * I18nizer implementation based on ByteBuddy framework.
87   */
88  @Singleton
89  public class ByteBuddyI18nizer implements I18nizer {
90  
91      private static final Logger log = LoggerFactory.getLogger(ByteBuddyI18nizer.class);
92  
93      private static final String I18N_SOURCE = "_I18N_SOURCE";
94      private static final String I18N_PARENT = "_I18N_PARENT";
95  
96      private final Objenesis objenesis = new ObjenesisStd(true);
97  
98      private final TypeCache<Class> typeCache = new TypeCache.WithInlineExpunction<>(TypeCache.Sort.SOFT);
99  
100     private final TranslationService translationService;
101     private final Provider<Context> contextProvider;
102 
103     @Inject
104     public ByteBuddyI18nizer(TranslationService translationService, Provider<Context> contextProvider) {
105         this.translationService = translationService;
106         this.contextProvider = contextProvider;
107     }
108 
109     @Override
110     public <C> C decorate(C obj) {
111         if (obj == null) {
112             return null;
113         }
114         // Check if this has already been decorated to avoid redundancy in client code
115         if (isProxied(obj.getClass()) && obj instanceof I18nParentable) {
116             throw new IllegalStateException(obj + " has already been enhanced by " + getClass().getSimpleName());
117         }
118 
119         return decorateInstance(obj, null);
120     }
121 
122     @SuppressWarnings("unchecked")
123     private  <C> C decorateInstance(C instance, Object parent) {
124         if (instance == null) {
125             return null;
126         }
127 
128         Class<C> clazz = getOriginalType(instance);
129         Class<C> proxyType = (Class<C>) typeCache.findOrInsert(getClass().getClassLoader(), clazz, () -> generateByteBuddyDecorator(clazz));
130 
131         final C proxy = objenesis.newInstance(proxyType);
132         setFieldValue(proxy, I18N_SOURCE, instance);
133         setFieldValue(proxy, I18N_PARENT, parent);
134 
135         return proxy;
136     }
137 
138     private <C> Class generateByteBuddyDecorator(Class<C> type) {
139         DynamicType.Builder<C> subclass = new ByteBuddy().subclass(type);
140 
141         return subclass
142                 .defineField(I18N_SOURCE, type)
143                 .defineField(I18N_PARENT, Object.class)
144                 .implement(MagnoliaProxy.class)
145                 .implement(DelegateResolver.class)
146                 .implement(I18nParentable.class)
147                 .method(not(isDeclaredBy(Object.class)).and(isPublic()))
148                     .intercept(to(new DefaultInterceptor<C>()))
149                 .method(isGetter().and(annotationIsPresentInHierarchy(I18nText.class)))
150                     .intercept(to((new I18NTextAnnotatedGetterInterceptor<>(translationService, I18nKeyGeneratorFactory.newKeyGeneratorFor(type)))))
151                 .method(isGetter().and(returnsGeneric(annotationIsPresentInGenericSignature(I18nable.class))))
152                     .intercept(to(new I18NableGetterInterceptor<>(this::decorateInstance)))
153                 .method(isDeclaredBy(DelegateResolver.class))
154                     .intercept(FieldAccessor.ofField(I18N_SOURCE))
155                 .method(isDeclaredBy(I18nParentable.class))
156                     .intercept(FieldAccessor.ofField(I18N_PARENT))
157                 .make()
158                 .load(getClass().getClassLoader(), WRAPPER).getLoaded();
159     }
160 
161     private boolean isProxied(Class<?> clazz) {
162         return MagnoliaProxy.class.isAssignableFrom(clazz);
163     }
164 
165     private Object getUndecoratedValue(Method method, Object target) {
166         try {
167             return method.invoke(target);
168         } catch (IllegalAccessException | InvocationTargetException e) {
169             log.warn("Undecorated value couldn't be retrieved {}", e.getMessage());
170             return null;
171         }
172     }
173 
174     @SuppressWarnings("unchecked")
175     private  <C> Class<C> getOriginalType(C instance) {
176         Class<C> clazz = (Class<C>) instance.getClass();
177         if (instance instanceof MagnoliaProxy || Enhancer.isEnhanced(clazz)) {
178             clazz = (Class<C>) clazz.getSuperclass();
179         }
180 
181         return clazz;
182     }
183 
184     private <C> void setFieldValue(C target, String propertyName, Object value) {
185         try {
186             final Field delegateField = target.getClass().getDeclaredField(propertyName);
187             delegateField.setAccessible(true);
188             delegateField.set(target, value);
189         } catch (IllegalAccessException | NoSuchFieldException e) {
190             log.error("Failed to set property [{}], i18n enhancement process failed", propertyName, e);
191         }
192     }
193 
194     /**
195      * Mixin interface which makes it easier for the i18n proxy to expose
196      * the object that it wraps.
197      *
198      * @param <T>
199      *     type of the wrapped object
200      */
201     public interface DelegateResolver<T> {
202         T getProxyDelegate();
203     }
204 
205     /**
206      * Catch-all interceptor which merely dispatches all the calls to the wrapped object.
207      * Note that the annotated getters will be handled by other intercepters accordingly.
208      *
209      * @param <T>
210      *     type of the wrapped object
211      */
212     public class DefaultInterceptor<T> {
213 
214         @RuntimeType
215         public Object intercept(@This DelegateResolver<T> that, @Origin Method method, @AllArguments Object[] args) {
216             try {
217                 return method.invoke(that.getProxyDelegate(), args);
218             } catch (IllegalAccessException | InvocationTargetException e) {
219                 log.error("Failed to intercept method [{}]", method.getName(), e);
220                 return null;
221             }
222         }
223     }
224 
225     /**
226      * Handles the getters which return i18n-annotated types.
227      * Automatically recursively i18n-izes them.
228      *
229      * @param <T>
230      *     type of the getter owner
231      */
232     public class I18NableGetterInterceptor<T> {
233 
234         private final BiFunction<Object, T, Object> decorator;
235 
236         public I18NableGetterInterceptor(BiFunction<Object, T, Object> decorator) {
237             this.decorator = decorator;
238         }
239 
240         @SuppressWarnings("unchecked")
241         @RuntimeType
242         public Object decorate(@Origin Method method, @This DelegateResolver<T> delegateResolver, @This T parent) {
243             Object value = getUndecoratedValue(method, delegateResolver.getProxyDelegate());
244             if (value instanceof Map) {
245                 value = ((Map<?, ?>) value).entrySet().stream()
246                         .collect(toMap(Map.Entry::getKey,
247                                 el -> decorator.apply(el.getValue(), parent),
248                                 (u, v) -> {
249                                     throw new IllegalStateException(String.format("Duplicate key %s", u));
250                                 },
251                                 LinkedHashMap::new)
252                         );
253             } else if (value instanceof Collection) {
254                 // We can't use Collections2.transform() or Lists.transform() as these create new immutable collections
255                 if (value instanceof List) {
256                     //this is wrapped in newArrayList() to make it modifiable since info.magnolia.ui.contentapp.movedialog.MoveDialogPresenterImpl#prepareWorkbenchDefinition relies on it (just for compatibility although it should be fixed there)
257                     value = ((List<?>) value).stream().map(el -> decorator.apply(el, parent)).collect(toList());
258                 } else {
259                     value = Collections2.transform((Collection) value, el -> decorator.apply(el, parent));
260                 }
261             } else {
262                 value = decorator.apply(value, parent);
263             }
264             return value;
265         }
266     }
267 
268     /**
269      * Handles the getters annotated with {@link I18nText}.
270      * Delegates the calls to the translation service.
271      *
272      * @param <T>
273      *     type of the getter owner
274      */
275     public class I18NTextAnnotatedGetterInterceptor<T> {
276 
277         private final TranslationService translationService;
278         private final I18nKeyGenerator<T> keyGenerator;
279 
280         public I18NTextAnnotatedGetterInterceptor(TranslationService translationService, I18nKeyGenerator<T> keyGenerator) {
281             this.translationService = translationService;
282             this.keyGenerator = keyGenerator;
283         }
284 
285         @RuntimeType
286         public String i18nTextMethod(@Origin Method method, @This DelegateResolver<T> delegateResolver, @This T that) {
287             String undecoratedValue = (String) getUndecoratedValue(method, delegateResolver.getProxyDelegate());
288             final String[] keys = Optional.ofNullable(undecoratedValue)
289                     .map(value -> new String[]{value})
290                     .orElseGet(() -> keyGenerator.keysFor(null, that, method));
291             return translationService.translate(new ContextLocaleProvider(contextProvider.get()), keys);
292         }
293     }
294 }