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}