1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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.lang.reflect.Type;
58 import java.lang.reflect.TypeVariable;
59 import java.util.Arrays;
60 import java.util.Collection;
61 import java.util.LinkedHashMap;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Optional;
65 import java.util.concurrent.ConcurrentHashMap;
66 import java.util.function.BiFunction;
67 import java.util.stream.Stream;
68
69 import javax.inject.Inject;
70 import javax.inject.Provider;
71 import javax.inject.Singleton;
72
73 import org.objenesis.Objenesis;
74 import org.objenesis.ObjenesisStd;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
77
78 import com.google.common.collect.Collections2;
79
80 import net.bytebuddy.ByteBuddy;
81 import net.bytebuddy.TypeCache;
82 import net.bytebuddy.description.type.TypeDescription;
83 import net.bytebuddy.dynamic.DynamicType;
84 import net.bytebuddy.implementation.FieldAccessor;
85 import net.bytebuddy.implementation.bind.annotation.AllArguments;
86 import net.bytebuddy.implementation.bind.annotation.Origin;
87 import net.bytebuddy.implementation.bind.annotation.RuntimeType;
88 import net.bytebuddy.implementation.bind.annotation.This;
89 import net.sf.cglib.proxy.Enhancer;
90
91
92
93
94 @Singleton
95 public class ByteBuddyI18nizer implements I18nizer {
96
97 private static final Logger log = LoggerFactory.getLogger(ByteBuddyI18nizer.class);
98
99 private static final String I18N_SOURCE = "_I18N_SOURCE";
100 private static final String I18N_PARENT = "_I18N_PARENT";
101
102 private final Objenesis objenesis = new ObjenesisStd(true);
103
104 private final TypeCache<Class> typeCache = new TypeCache.WithInlineExpunction<>(TypeCache.Sort.SOFT);
105 private final Map<Method, String> i18nTextFallbackCache = new ConcurrentHashMap<>();
106
107 private final TranslationService translationService;
108 private final Provider<Context> contextProvider;
109
110 @Inject
111 public ByteBuddyI18nizer(TranslationService translationService, Provider<Context> contextProvider) {
112 this.translationService = translationService;
113 this.contextProvider = contextProvider;
114 }
115
116 @Override
117 public <C> C decorate(C obj) {
118 if (obj == null) {
119 return null;
120 }
121
122 if (isProxied(obj.getClass()) && obj instanceof I18nParentable) {
123 throw new IllegalStateException(obj + " has already been enhanced by " + getClass().getSimpleName());
124 }
125
126 return decorateInstance(obj, null);
127 }
128
129 @SuppressWarnings("unchecked")
130 private <C> C decorateInstance(C instance, Object parent) {
131 if (instance == null) {
132 return null;
133 }
134
135 Class<C> clazz = getOriginalType(instance);
136 Class<C> proxyType = (Class<C>) typeCache.findOrInsert(getClass().getClassLoader(), clazz, () -> generateByteBuddyDecorator(clazz));
137
138 final C proxy = objenesis.newInstance(proxyType);
139 setFieldValue(proxy, I18N_SOURCE, instance);
140 setFieldValue(proxy, I18N_PARENT, parent);
141
142 return proxy;
143 }
144
145 private <C> Class generateByteBuddyDecorator(Class<C> type) {
146 TypeVariable<Class<C>>[] typeParameters = type.getTypeParameters();
147 DynamicType.Builder<C> subclass;
148
149 if (typeParameters.length == 0) {
150 subclass = new ByteBuddy().subclass(type);
151 } else {
152 Type[] stubParameterTypes = Arrays.stream(typeParameters).map(param -> {
153 Type[] bounds = param.getBounds();
154 return bounds.length == 0 ? Object.class : bounds[0];
155 }).toArray(Type[]::new);
156
157 subclass = (DynamicType.Builder<C>) new ByteBuddy().subclass(TypeDescription.Generic.Builder.parameterizedType(type, stubParameterTypes).build());
158 }
159
160 return subclass
161 .defineField(I18N_SOURCE, type)
162 .defineField(I18N_PARENT, Object.class)
163 .implement(MagnoliaProxy.class)
164 .implement(DelegateResolver.class)
165 .implement(I18nParentable.class)
166 .method(not(isDeclaredBy(Object.class)).and(isPublic()))
167 .intercept(to(new DefaultInterceptor<C>()))
168 .method(isGetter().and(annotationIsPresentInHierarchy(I18nText.class)))
169 .intercept(to((new I18NTextAnnotatedGetterInterceptor<>(translationService, I18nKeyGeneratorFactory.newKeyGeneratorFor(type)))))
170 .method(isGetter().and(returnsGeneric(annotationIsPresentInGenericSignature(I18nable.class))))
171 .intercept(to(new I18NableGetterInterceptor<>(this::decorateInstance)))
172 .method(isDeclaredBy(DelegateResolver.class))
173 .intercept(FieldAccessor.ofField(I18N_SOURCE))
174 .method(isDeclaredBy(I18nParentable.class))
175 .intercept(FieldAccessor.ofField(I18N_PARENT))
176 .make()
177 .load(getClass().getClassLoader(), WRAPPER).getLoaded();
178 }
179
180 private boolean isProxied(Class<?> clazz) {
181 return MagnoliaProxy.class.isAssignableFrom(clazz);
182 }
183
184 private Object getUndecoratedValue(Method method, Object target) {
185 try {
186 return method.invoke(target);
187 } catch (IllegalAccessException | InvocationTargetException e) {
188 log.warn("Undecorated value couldn't be retrieved {}", e.getMessage());
189 return null;
190 }
191 }
192
193 @SuppressWarnings("unchecked")
194 private <C> Class<C> getOriginalType(C instance) {
195 Class<C> clazz = (Class<C>) instance.getClass();
196 if (instance instanceof MagnoliaProxy || Enhancer.isEnhanced(clazz)) {
197 clazz = (Class<C>) clazz.getSuperclass();
198 }
199
200 return clazz;
201 }
202
203 private <C> void setFieldValue(C target, String propertyName, Object value) {
204 try {
205 final Field delegateField = target.getClass().getDeclaredField(propertyName);
206 delegateField.setAccessible(true);
207 delegateField.set(target, value);
208 } catch (IllegalAccessException | NoSuchFieldException e) {
209 log.error("Failed to set property [{}], i18n enhancement process failed", propertyName, e);
210 }
211 }
212
213
214
215
216
217
218
219
220 public interface DelegateResolver<T> {
221 T getProxyDelegate();
222 }
223
224
225
226
227
228
229
230
231 public class DefaultInterceptor<T> {
232
233 @RuntimeType
234 public Object intercept(@This DelegateResolver<T> that, @Origin Method method, @AllArguments Object[] args) {
235 try {
236 return method.invoke(that.getProxyDelegate(), args);
237 } catch (IllegalAccessException | InvocationTargetException e) {
238 log.error("Failed to intercept method [{}]", method.getName(), e);
239 return null;
240 }
241 }
242 }
243
244
245
246
247
248
249
250
251 public class I18NableGetterInterceptor<T> {
252
253 private final BiFunction<Object, T, Object> decorator;
254
255 public I18NableGetterInterceptor(BiFunction<Object, T, Object> decorator) {
256 this.decorator = decorator;
257 }
258
259 @SuppressWarnings("unchecked")
260 @RuntimeType
261 public Object decorate(@Origin Method method, @This DelegateResolver<T> delegateResolver, @This T parent) {
262 Object value = getUndecoratedValue(method, delegateResolver.getProxyDelegate());
263 if (value instanceof Map) {
264 value = ((Map<?, ?>) value).entrySet().stream()
265 .collect(toMap(Map.Entry::getKey,
266 el -> decorator.apply(el.getValue(), parent),
267 (u, v) -> {
268 throw new IllegalStateException(String.format("Duplicate key %s", u));
269 },
270 LinkedHashMap::new)
271 );
272 } else if (value instanceof Collection) {
273
274 if (value instanceof List) {
275
276 value = ((List<?>) value).stream().map(el -> decorator.apply(el, parent)).collect(toList());
277 } else {
278 value = Collections2.transform((Collection) value, el -> decorator.apply(el, parent));
279 }
280 } else {
281 value = decorator.apply(value, parent);
282 }
283 return value;
284 }
285 }
286
287
288
289
290
291
292
293
294 public class I18NTextAnnotatedGetterInterceptor<T> {
295
296 private final TranslationService translationService;
297 private final I18nKeyGenerator<T> keyGenerator;
298
299 public I18NTextAnnotatedGetterInterceptor(TranslationService translationService, I18nKeyGenerator<T> keyGenerator) {
300 this.translationService = translationService;
301 this.keyGenerator = keyGenerator;
302 }
303
304 @RuntimeType
305 public String i18nTextMethod(@Origin Method method, @This DelegateResolver<T> delegateResolver, @This T that) {
306 String undecoratedValue = (String) getUndecoratedValue(method, delegateResolver.getProxyDelegate());
307 final ContextLocaleProvider#ContextLocaleProvider">ContextLocaleProvider localeProvider = new ContextLocaleProvider(contextProvider.get());
308 final String[] keys = Optional.ofNullable(undecoratedValue)
309 .map(value -> new String[]{value})
310 .orElseGet(() -> keyGenerator.keysFor(null, that, method));
311 return translationService.translate(localeProvider, keys, getFallback(method));
312 }
313
314 private String getFallback(@Origin Method method) {
315 String fallback = i18nTextFallbackCache.get(method);
316 if (fallback == null) {
317 fallback = Optional.ofNullable(method.getAnnotation(I18nText.class)).map(I18nText::fallback).orElseGet(() ->
318 Stream.concat(
319 Arrays.stream(method.getDeclaringClass().getInterfaces()).flatMap(aClass -> Arrays.stream(aClass.getMethods())),
320 Arrays.stream(Optional.ofNullable(method.getDeclaringClass().getSuperclass()).map(Class::getMethods).orElse(new Method[]{})))
321 .filter(interfaceMethod -> interfaceMethod.getName().equals(method.getName()))
322 .findFirst()
323 .map(this::getFallback)
324 .orElse(I18nText.NO_FALLBACK)
325 );
326 i18nTextFallbackCache.put(method, fallback);
327 }
328 return fallback;
329 }
330 }
331 }