001/**
002 * Copyright (C) 2006-2024 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.MissingResourceException;
031import java.util.ResourceBundle;
032import java.util.concurrent.ConcurrentHashMap;
033import java.util.concurrent.ConcurrentMap;
034import java.util.function.Function;
035import java.util.function.Supplier;
036import java.util.stream.Stream;
037
038import org.talend.sdk.component.api.internationalization.Language;
039import org.talend.sdk.component.runtime.impl.Mode;
040import org.talend.sdk.component.runtime.reflect.Defaults;
041
042import lombok.RequiredArgsConstructor;
043
044@RequiredArgsConstructor
045public class InternationalizationServiceFactory {
046
047    private final Supplier<Locale> localeSupplier;
048
049    public <T> T create(final Class<T> api, final ClassLoader loader) {
050        if (Mode.mode != Mode.UNSAFE) {
051            if (!api.isInterface()) {
052                throw new IllegalArgumentException(api + " is not an interface");
053            }
054            if (Stream
055                    .of(api.getMethods())
056                    .filter(m -> m.getDeclaringClass() != Object.class)
057                    .anyMatch(m -> m.getReturnType() != String.class)) {
058                throw new IllegalArgumentException(api + " methods must return a String");
059            }
060            if (Stream
061                    .of(api.getMethods())
062                    .flatMap(m -> Stream.of(m.getParameters()))
063                    .anyMatch(p -> p.isAnnotationPresent(Language.class)
064                            && (p.getType() != Locale.class && p.getType() != String.class))) {
065                throw new IllegalArgumentException("@Language can only be used with Locale or String.");
066            }
067        }
068        final String pck = api.getPackage().getName();
069        return api
070                .cast(Proxy
071                        .newProxyInstance(loader, new Class<?>[] { api },
072                                new InternationalizedHandler(api.getName() + '.', api.getSimpleName() + '.',
073                                        (pck == null || pck.isEmpty() ? "" : (pck + '.')) + "Messages",
074                                        localeSupplier)));
075    }
076
077    @RequiredArgsConstructor
078    private static class InternationalizedHandler implements InvocationHandler {
079
080        private static final Object[] NO_ARG = new Object[0];
081
082        private final String prefix;
083
084        private final String shortPrefix;
085
086        private final String messages;
087
088        private final Supplier<Locale> localeSupplier;
089
090        private final ConcurrentMap<Locale, ResourceBundle> bundles = new ConcurrentHashMap<>();
091
092        private final transient ConcurrentMap<Method, MethodMeta> methods = new ConcurrentHashMap<>();
093
094        @Override
095        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
096            if (Defaults.isDefaultAndShouldHandle(method)) {
097                return Defaults.handleDefault(method.getDeclaringClass(), method, proxy, args);
098            }
099
100            if (Object.class == method.getDeclaringClass()) {
101                switch (method.getName()) {
102                case "equals":
103                    return args != null && args.length == 1 && args[0] != null && Proxy.isProxyClass(args[0].getClass())
104                            && this == Proxy.getInvocationHandler(args[0]);
105                case "hashCode":
106                    return hashCode();
107                default:
108                    try {
109                        return method.invoke(this, args);
110                    } catch (final InvocationTargetException ite) {
111                        throw ite.getTargetException();
112                    }
113                }
114            }
115
116            final MethodMeta methodMeta = methods
117                    .computeIfAbsent(method, m -> new MethodMeta(createLocaleExtractor(m), createParameterFactory(m),
118                            prefix + m.getName(), shortPrefix + m.getName(), m.getName()));
119            final Locale locale = methodMeta.localeExtractor.apply(args);
120            final String template = getTemplate(locale, methodMeta, method.getDeclaringClass().getClassLoader());
121            // note: if we need we could pool message formats but not sure we'll abuse of it
122            // that much at runtime yet
123            return new MessageFormat(template, locale).format(methodMeta.parameterFactory.apply(args));
124        }
125
126        private Function<Object[], Object[]> createParameterFactory(final Method method) {
127            final Collection<Integer> included = new ArrayList<>();
128            final Parameter[] parameters = method.getParameters();
129            for (int i = 0; i < parameters.length; i++) {
130                if (!parameters[i].isAnnotationPresent(Language.class)) {
131                    included.add(i);
132                }
133            }
134            if (included.size() == method.getParameterCount()) {
135                return identity();
136            }
137            if (included.size() == 0) {
138                return a -> NO_ARG;
139            }
140            return args -> {
141                final Object[] modified = new Object[included.size()];
142                int current = 0;
143                for (final int i : included) {
144                    modified[current++] = args[i];
145                }
146                return modified;
147            };
148        }
149
150        private Function<Object[], Locale> createLocaleExtractor(final Method method) {
151            final Parameter[] parameters = method.getParameters();
152            for (int i = 0; i < method.getParameterCount(); i++) {
153                final Parameter p = parameters[i];
154                if (p.isAnnotationPresent(Language.class)) {
155                    final int idx = i;
156                    if (String.class == p.getType()) {
157                        return params -> new Locale(ofNullable(params[idx]).map(String::valueOf).orElse("en"));
158                    }
159                    return params -> Locale.class.cast(params[idx]);
160                }
161            }
162            return p -> localeSupplier.get();
163        }
164
165        private String getTemplate(final Locale locale, final MethodMeta methodMeta,
166                final ClassLoader declaringLoader) {
167            ResourceBundle bundle;
168            try {
169                bundle = bundles.computeIfAbsent(locale,
170                        l -> ResourceBundle.getBundle(messages, l, Thread.currentThread().getContextClassLoader()));
171            } catch (MissingResourceException e) {
172                bundle = bundles.computeIfAbsent(locale,
173                        l -> ResourceBundle.getBundle(messages, l, declaringLoader));
174            }
175            return bundle.containsKey(methodMeta.longName) ? bundle.getString(methodMeta.longName)
176                    : (bundle.containsKey(methodMeta.shortName) ? bundle.getString(methodMeta.shortName)
177                            : methodMeta.name);
178        }
179    }
180
181    @RequiredArgsConstructor
182    private static class MethodMeta {
183
184        private final Function<Object[], Locale> localeExtractor;
185
186        private final Function<Object[], Object[]> parameterFactory;
187
188        private final String longName;
189
190        private final String shortName;
191
192        private final String name;
193    }
194}