001/**
002 * Copyright (C) 2006-2022 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.runtime.internationalization;
017
018import static java.util.Optional.ofNullable;
019import static java.util.function.Function.identity;
020
021import java.lang.reflect.InvocationHandler;
022import java.lang.reflect.InvocationTargetException;
023import java.lang.reflect.Method;
024import java.lang.reflect.Parameter;
025import java.lang.reflect.Proxy;
026import java.text.MessageFormat;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Locale;
030import java.util.ResourceBundle;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.ConcurrentMap;
033import java.util.function.Function;
034import java.util.function.Supplier;
035import java.util.stream.Stream;
036
037import org.talend.sdk.component.api.internationalization.Language;
038import org.talend.sdk.component.runtime.impl.Mode;
039import org.talend.sdk.component.runtime.reflect.Defaults;
040
041import lombok.RequiredArgsConstructor;
042
043@RequiredArgsConstructor
044public class InternationalizationServiceFactory {
045
046    private final Supplier<Locale> localeSupplier;
047
048    public <T> T create(final Class<T> api, final ClassLoader loader) {
049        if (Mode.mode != Mode.UNSAFE) {
050            if (!api.isInterface()) {
051                throw new IllegalArgumentException(api + " is not an interface");
052            }
053            if (Stream
054                    .of(api.getMethods())
055                    .filter(m -> m.getDeclaringClass() != Object.class)
056                    .anyMatch(m -> m.getReturnType() != String.class)) {
057                throw new IllegalArgumentException(api + " methods must return a String");
058            }
059            if (Stream
060                    .of(api.getMethods())
061                    .flatMap(m -> Stream.of(m.getParameters()))
062                    .anyMatch(p -> p.isAnnotationPresent(Language.class)
063                            && (p.getType() != Locale.class && p.getType() != String.class))) {
064                throw new IllegalArgumentException("@Language can only be used with Locale or String.");
065            }
066        }
067        final String pck = api.getPackage().getName();
068        return api
069                .cast(Proxy
070                        .newProxyInstance(loader, new Class<?>[] { api },
071                                new InternationalizedHandler(api.getName() + '.', api.getSimpleName() + '.',
072                                        (pck == null || pck.isEmpty() ? "" : (pck + '.')) + "Messages",
073                                        localeSupplier)));
074    }
075
076    @RequiredArgsConstructor
077    private static class InternationalizedHandler implements InvocationHandler {
078
079        private static final Object[] NO_ARG = new Object[0];
080
081        private final String prefix;
082
083        private final String shortPrefix;
084
085        private final String messages;
086
087        private final Supplier<Locale> localeSupplier;
088
089        private final ConcurrentMap<Locale, ResourceBundle> bundles = new ConcurrentHashMap<>();
090
091        private final transient ConcurrentMap<Method, MethodMeta> methods = new ConcurrentHashMap<>();
092
093        @Override
094        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
095            if (Defaults.isDefaultAndShouldHandle(method)) {
096                return Defaults.handleDefault(method.getDeclaringClass(), method, proxy, args);
097            }
098
099            if (Object.class == method.getDeclaringClass()) {
100                switch (method.getName()) {
101                case "equals":
102                    return args != null && args.length == 1 && args[0] != null && Proxy.isProxyClass(args[0].getClass())
103                            && this == Proxy.getInvocationHandler(args[0]);
104                case "hashCode":
105                    return hashCode();
106                default:
107                    try {
108                        return method.invoke(this, args);
109                    } catch (final InvocationTargetException ite) {
110                        throw ite.getTargetException();
111                    }
112                }
113            }
114
115            final MethodMeta methodMeta = methods
116                    .computeIfAbsent(method, m -> new MethodMeta(createLocaleExtractor(m), createParameterFactory(m),
117                            prefix + m.getName(), shortPrefix + m.getName(), m.getName()));
118            final Locale locale = methodMeta.localeExtractor.apply(args);
119            final String template = getTemplate(locale, methodMeta);
120            // note: if we need we could pool message formats but not sure we'll abuse of it
121            // that much at runtime yet
122            return new MessageFormat(template, locale).format(methodMeta.parameterFactory.apply(args));
123        }
124
125        private Function<Object[], Object[]> createParameterFactory(final Method method) {
126            final Collection<Integer> included = new ArrayList<>();
127            final Parameter[] parameters = method.getParameters();
128            for (int i = 0; i < parameters.length; i++) {
129                if (!parameters[i].isAnnotationPresent(Language.class)) {
130                    included.add(i);
131                }
132            }
133            if (included.size() == method.getParameterCount()) {
134                return identity();
135            }
136            if (included.size() == 0) {
137                return a -> NO_ARG;
138            }
139            return args -> {
140                final Object[] modified = new Object[included.size()];
141                int current = 0;
142                for (final int i : included) {
143                    modified[current++] = args[i];
144                }
145                return modified;
146            };
147        }
148
149        private Function<Object[], Locale> createLocaleExtractor(final Method method) {
150            final Parameter[] parameters = method.getParameters();
151            for (int i = 0; i < method.getParameterCount(); i++) {
152                final Parameter p = parameters[i];
153                if (p.isAnnotationPresent(Language.class)) {
154                    final int idx = i;
155                    if (String.class == p.getType()) {
156                        return params -> new Locale(ofNullable(params[idx]).map(String::valueOf).orElse("en"));
157                    }
158                    return params -> Locale.class.cast(params[idx]);
159                }
160            }
161            return p -> localeSupplier.get();
162        }
163
164        private String getTemplate(final Locale locale, final MethodMeta methodMeta) {
165            final ResourceBundle bundle = bundles
166                    .computeIfAbsent(locale,
167                            l -> ResourceBundle.getBundle(messages, l, Thread.currentThread().getContextClassLoader()));
168            return bundle.containsKey(methodMeta.longName) ? bundle.getString(methodMeta.longName)
169                    : (bundle.containsKey(methodMeta.shortName) ? bundle.getString(methodMeta.shortName)
170                            : methodMeta.name);
171        }
172    }
173
174    @RequiredArgsConstructor
175    private static class MethodMeta {
176
177        private final Function<Object[], Locale> localeExtractor;
178
179        private final Function<Object[], Object[]> parameterFactory;
180
181        private final String longName;
182
183        private final String shortName;
184
185        private final String name;
186    }
187}