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.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
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
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
196
197
198
199
200
201 public interface DelegateResolver<T> {
202 T getProxyDelegate();
203 }
204
205
206
207
208
209
210
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
227
228
229
230
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
255 if (value instanceof List) {
256
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
270
271
272
273
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 }