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}