001/**
002 * Copyright (C) 2006-2025 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
104                                && Proxy.isProxyClass(args[0].getClass())
105                                && this == Proxy.getInvocationHandler(args[0]);
106                    case "hashCode":
107                        return hashCode();
108                    default:
109                        try {
110                            return method.invoke(this, args);
111                        } catch (final InvocationTargetException ite) {
112                            throw ite.getTargetException();
113                        }
114                }
115            }
116
117            final MethodMeta methodMeta = methods
118                    .computeIfAbsent(method, m -> new MethodMeta(createLocaleExtractor(m), createParameterFactory(m),
119                            prefix + m.getName(), shortPrefix + m.getName(), m.getName()));
120            final Locale locale = methodMeta.localeExtractor.apply(args);
121            final String template = getTemplate(locale, methodMeta, method.getDeclaringClass().getClassLoader());
122            // note: if we need we could pool message formats but not sure we'll abuse of it
123            // that much at runtime yet
124            return new MessageFormat(template, locale).format(methodMeta.parameterFactory.apply(args));
125        }
126
127        private Function<Object[], Object[]> createParameterFactory(final Method method) {
128            final Collection<Integer> included = new ArrayList<>();
129            final Parameter[] parameters = method.getParameters();
130            for (int i = 0; i < parameters.length; i++) {
131                if (!parameters[i].isAnnotationPresent(Language.class)) {
132                    included.add(i);
133                }
134            }
135            if (included.size() == method.getParameterCount()) {
136                return identity();
137            }
138            if (included.size() == 0) {
139                return a -> NO_ARG;
140            }
141            return args -> {
142                final Object[] modified = new Object[included.size()];
143                int current = 0;
144                for (final int i : included) {
145                    modified[current++] = args[i];
146                }
147                return modified;
148            };
149        }
150
151        private Function<Object[], Locale> createLocaleExtractor(final Method method) {
152            final Parameter[] parameters = method.getParameters();
153            for (int i = 0; i < method.getParameterCount(); i++) {
154                final Parameter p = parameters[i];
155                if (p.isAnnotationPresent(Language.class)) {
156                    final int idx = i;
157                    if (String.class == p.getType()) {
158                        return params -> new Locale(ofNullable(params[idx]).map(String::valueOf).orElse("en"));
159                    }
160                    return params -> Locale.class.cast(params[idx]);
161                }
162            }
163            return p -> localeSupplier.get();
164        }
165
166        private String getTemplate(final Locale locale, final MethodMeta methodMeta,
167                final ClassLoader declaringLoader) {
168            ResourceBundle bundle;
169            try {
170                bundle = bundles.computeIfAbsent(locale,
171                        l -> ResourceBundle.getBundle(messages, l, Thread.currentThread().getContextClassLoader()));
172            } catch (MissingResourceException e) {
173                bundle = bundles.computeIfAbsent(locale,
174                        l -> ResourceBundle.getBundle(messages, l, declaringLoader));
175            }
176            return bundle.containsKey(methodMeta.longName) ? bundle.getString(methodMeta.longName)
177                    : (bundle.containsKey(methodMeta.shortName) ? bundle.getString(methodMeta.shortName)
178                            : methodMeta.name);
179        }
180    }
181
182    @RequiredArgsConstructor
183    private static class MethodMeta {
184
185        private final Function<Object[], Locale> localeExtractor;
186
187        private final Function<Object[], Object[]> parameterFactory;
188
189        private final String longName;
190
191        private final String shortName;
192
193        private final String name;
194    }
195}