/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.github.rodionmoiseev.c10n; import com.github.rodionmoiseev.c10n.formatters.MessageFormatter; import com.github.rodionmoiseev.c10n.plugin.C10NPlugin; import com.github.rodionmoiseev.c10n.plugin.PluginResult; import com.github.rodionmoiseev.c10n.share.Constants; import com.github.rodionmoiseev.c10n.share.LocaleMapping; import com.github.rodionmoiseev.c10n.share.utils.ReflectionUtils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URL; import java.nio.CharBuffer; import java.util.*; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.github.rodionmoiseev.c10n.share.utils.Preconditions.assertNotNull; class DefaultC10NMsgFactory implements InternalC10NMsgFactory { private final ConfiguredC10NModule conf; private final LocaleMapping localeMapping; DefaultC10NMsgFactory(ConfiguredC10NModule conf, LocaleMapping localeMapping) { this.conf = conf; this.localeMapping = localeMapping; } @Override public <T> T get(Class<T> c10nInterface) { assertNotNull(c10nInterface, "c10nInterface"); return get(c10nInterface, null, conf::getCurrentLocale); } @Override public <T> T get(Class<T> c10nInterface, Locale locale) { assertNotNull(c10nInterface, "c10nInterface"); assertNotNull(locale, "locale"); return get(c10nInterface, null, LocaleProviders.fixed(locale)); } @Override @SuppressWarnings("unchecked") public <T> T get(Class<T> c10nInterface, String delegatingValue, LocaleProvider localeProvider) { return (T) Proxy.newProxyInstance(conf.getProxyClassLoader(), new Class<?>[]{c10nInterface}, C10NInvocationHandler.create(this, delegatingValue, conf, localeProvider, localeMapping, c10nInterface)); } private static final class C10NString { final String text; final boolean raw; public static C10NString def(String text) { return new C10NString(text, false); } private C10NString(String text, boolean raw) { this.text = text; this.raw = raw; } } private static final class C10NInvocationHandler implements InvocationHandler { private static final Annotation[] NO_ANNOTATIONS = new Annotation[0]; private final InternalC10NMsgFactory c10nFactory; private final String delegatingValue; private final ConfiguredC10NModule conf; private final LocaleProvider localeProvider; private final LocaleMapping localeMapping; private final Class<?> proxiedClass; private final Map<String, Map<Locale, C10NString>> translationsByMethod; //private final Set<Locale> availableLocales; private final Set<Locale> availableImplLocales; private final Map<AnnotatedClass, C10NFilterProvider<?>> filters; private final Map<Method, String> bundleKeys; private final MessageFormatter formatter; C10NInvocationHandler(InternalC10NMsgFactory c10nFactory, String delegatingValue, ConfiguredC10NModule conf, LocaleProvider localeProvider, LocaleMapping localeMapping, Class<?> proxiedClass, Map<String, Map<Locale, C10NString>> translationsByMethod, Map<Method, String> bundleKeys) { this.c10nFactory = c10nFactory; this.delegatingValue = delegatingValue; this.conf = conf; this.localeProvider = localeProvider; this.localeMapping = localeMapping; this.proxiedClass = proxiedClass; this.translationsByMethod = translationsByMethod; this.availableImplLocales = conf.getImplementationBindings(proxiedClass); this.filters = conf.getFilterBindings(proxiedClass); this.bundleKeys = bundleKeys; this.formatter = conf.getMessageFormatter(); } static C10NInvocationHandler create(InternalC10NMsgFactory c10nFactory, String delegatingValue, ConfiguredC10NModule conf, LocaleProvider localeProvider, LocaleMapping localeMapping, Class<?> c10nInterface) { Map<String, Map<Locale, C10NString>> translationsByMethod = new HashMap<>(); Map<Method, String> bundleKeys = new HashMap<>(); //Translations defined in @C10NDef annotation are //always considered a fallback for (Method m : c10nInterface.getMethods()) { C10NDef c10nDef = m.getAnnotation(C10NDef.class); if (null != c10nDef) { Map<Locale, C10NString> defMapping = new HashMap<>(); defMapping.put(C10N.FALLBACK_LOCALE, C10NString.def(c10nDef.value())); translationsByMethod.put(m.toString(), defMapping); } String key = ReflectionUtils.getC10NKey(conf.getKeyPrefix(), m); if (conf.isDebug()) { System.out.println("c10n: method " + ReflectionUtils.getDefaultKey(m) + " was bound to bundle key '" + key + "'"); } bundleKeys.put(m, key); } // Process custom bound annotations for (Entry<Class<? extends Annotation>, Set<Locale>> entry : conf .getAnnotationBindings(c10nInterface).entrySet()) { Class<? extends Annotation> annotationClass = entry.getKey(); for (Method m : c10nInterface.getMethods()) { Annotation a = m.getAnnotation(annotationClass); if (null != a) { try { C10NString translation = getAnnotationValue(c10nInterface, annotationClass, a); Map<Locale, C10NString> translationsByLocale = translationsByMethod.get(m.toString()); if (null == translationsByLocale) { translationsByLocale = new HashMap<>(); translationsByMethod.put(m.toString(), translationsByLocale); } for (Locale locale : entry.getValue()) { translationsByLocale.put(locale, translation); } } catch (SecurityException e) { throw new RuntimeException("Annotation " + annotationClass.getName() + " value() method is not accessible", e); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException( "Could not call value() on annotation " + annotationClass.getName(), e); } } } } return new C10NInvocationHandler(c10nFactory, delegatingValue, conf, localeProvider, localeMapping, c10nInterface, translationsByMethod, bundleKeys); } private static C10NString getAnnotationValue(Class<?> c10nInterface, Class<? extends Annotation> annotationClass, Annotation a) { boolean raw = extractAnnotationValue(annotationClass, "raw", a, false); Object valueTranslation = extractAnnotationValue(annotationClass, "value", a, Constants.UNDEF); if (valueTranslation.equals(Constants.UNDEF)) { //check for external resource declarations Object extRes = extractAnnotationValue(annotationClass, "extRes", a, Constants.UNDEF); if (extRes.equals(Constants.UNDEF)) { Object intRes = extractAnnotationValue(annotationClass, "intRes", a, Constants.UNDEF); if (intRes.equals(Constants.UNDEF)) { throw new RuntimeException("One of @" + annotationClass.getSimpleName() + " annotations on the " + c10nInterface.getCanonicalName() + " class does not have any of 'value' or 'extRes' or 'intRes' specified."); } return new C10NString(readTextFromInternalResource(replaceSystemProps(String.valueOf(intRes))), raw); } return new C10NString(readTextFromUrl(replaceSystemProps(String.valueOf(extRes))), raw); } return new C10NString(String.valueOf(valueTranslation), raw); } @SuppressWarnings("unchecked") private static <R> R extractAnnotationValue(Class<? extends Annotation> annotationClass, String method, Annotation annotation, R defaultValue) { try { return (R) annotationClass.getMethod(method).invoke(annotation); } catch (InvocationTargetException e) { return defaultValue; } catch (NoSuchMethodException e) { return defaultValue; } catch (IllegalAccessException e) { throw new IllegalStateException("Failed to extract value of '" + method + "' from annotation" + "class " + annotationClass.getCanonicalName(), e); } } private static String replaceSystemProps(String string) { if (null == string) { return null; } String res = string; Pattern p = Pattern.compile("\\$\\{.*?\\}"); Matcher m = p.matcher(string); while (m.find()) { String prop = string.substring(m.start() + 2, m.end() - 1); String propValue = System.getProperty(prop); if (propValue != null) { res = res.replace("${" + prop + "}", propValue); } } return res; } private static String readTextFromUrl(String urlString) { try { URL url = new URL(urlString); InputStream is = null; try { try { is = url.openStream(); return readTextFromInputStream(is); } finally { if (is != null) { is.close(); } } } catch (IOException e) { throw new RuntimeException("Failed to read text data from URL: " + urlString, e); } } catch (MalformedURLException e) { throw new RuntimeException("Could not interpret external resource URL: " + urlString, e); } } private static String readTextFromInternalResource(String path) { InputStream is = null; try { try { is = C10N.class.getClassLoader().getResourceAsStream(path); if (null == is) { throw new RuntimeException("Internal resource: " + path + " does not exist"); } return readTextFromInputStream(is); } finally { if (null != is) { is.close(); } } } catch (IOException e) { throw new RuntimeException("Failed to read text data from internal resource: " + path, e); } } private static String readTextFromInputStream(InputStream is) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF8"), 1024 * 8); CharBuffer buf = CharBuffer.allocate(64); int read; do { read = br.read(buf); if (read == 0 && !buf.hasRemaining()) { CharBuffer newBuf = CharBuffer.allocate(buf.capacity() * 2); buf.flip(); newBuf.put(buf); buf = newBuf; } } while (read != -1); buf.flip(); return buf.toString(); } @Override public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { Locale currentLocale = localeProvider.getLocale(); String stringValue = getStringValue(method, args, currentLocale); PluginResult result = PluginResult.passOn(translate(method, args, stringValue, currentLocale)); for (C10NPlugin plugin : conf.getPlugins()) { if (result.isInterrupt()) { //The last execution requests that //no further plugin processing should take //place break; } PluginResult pluginResult = plugin.format( stringValue, result.getValue(), new InvocationDetails(proxy, proxiedClass, method, args)); if (null == pluginResult) { //ignore the execution of this plugin continue; } result = pluginResult; } return result.getValue(); } private Object translate(Method method, Object[] args, String stringValue, Locale currentLocale) throws Throwable { Class<?> returnType = method.getReturnType(); if (C10NMessage.class.equals(returnType)) { Map<Locale, String> msgs = new HashMap<>(); Set<Locale> declaredLocales = translationsByMethod.get(method.toString()).keySet(); for (Locale locale : declaredLocales) { msgs.put(locale, getStringValue(method, args, locale)); } Locale actualCurrentLocale = localeMapping.findClosestMatch(declaredLocales, currentLocale); return new C10NMessage(actualCurrentLocale, getStringValue(method, args, currentLocale), msgs); } Locale implLocale = localeMapping.findClosestMatch(availableImplLocales, currentLocale); Class<?> binding = conf.getImplementationBinding(proxiedClass, implLocale); if (null != binding) { // user specified binding exists // simply delegate the call to the binding Object instance = binding.newInstance(); return method.invoke(instance, args); } if (returnType.isAssignableFrom(String.class)) { // For methods returning String or CharSequence return stringValue; } else if (returnType.isInterface()) { if (null != returnType.getAnnotation(C10NMessages.class)) { return c10nFactory.get(returnType, stringValue, localeProvider); } } // don't know how to handle this return type return null; } private boolean isObjectToString(Method method, Object[] args) { return method.getName().equals("toString") && (args == null || args.length == 0); } private String getStringValue(Method method, Object[] args, Locale locale) { List<ResourceBundle> bundles = conf.getBundleBindings(proxiedClass, locale); for (ResourceBundle bundle : bundles) { String key = bundleKeys.get(method); if (null != key) { if (bundle.containsKey(key)) { return format(bundle.getString(key), method, locale, args); } }//else: should never happen! } C10NString res = findTranslationFromAnnotations(method, locale); if (null == res) { if (delegatingValue != null && isObjectToString(method, args)) { return delegatingValue; } return conf.getUntranslatedMessageString(proxiedClass, method, args); } return format(res, method, locale, args); } private C10NString findTranslationFromAnnotations(Method method, Locale locale) { Map<Locale, C10NString> translationsByLocale = translationsByMethod.get(method.toString()); if (null != translationsByLocale) { return translationsByLocale.get(localeMapping.findClosestMatch(translationsByLocale.keySet(), locale)); } return null; } private String format(C10NString message, Method method, Locale locale, Object... args) { return format(message.text, message.raw, method, locale, args); } private String format(String message, Method method, Locale locale, Object... args) { return format(message, false, method, locale, args); } private String format(String message, boolean raw, Method method, Locale locale, Object... args) { if (raw) { //Raw messages accept no parameters return message; } Annotation[][] argAnnotations = method.getParameterAnnotations(); Class<?>[] argTypes = method.getParameterTypes(); if (args != null && args.length > 0) { Object[] filteredArgs = new Object[args.length]; for (int i = 0; i < args.length; i++) { Annotation[] annotations = argAnnotations != null ? argAnnotations[i] : NO_ANNOTATIONS; filteredArgs[i] = applyArgFilterIfExists(annotations, argTypes[i], args[i]); } return formatter.format(method, message, locale, filteredArgs); } return formatter.format(method, message, locale, args); } private Object applyArgFilterIfExists(Annotation[] annotations, Class<?> argType, Object arg) { //1. Look for first filter matching any of the annotations for (Annotation annotation : annotations) { C10NFilterProvider<Object> filter = findFilterFor(argType, annotation.annotationType()); if (null != filter) { //filter found, look no further return filter.get().apply(arg); } } //2. Try annotation-less filter binding C10NFilterProvider<Object> filter = findFilterFor(argType, null); if (null != filter) { return filter.get().apply(arg); } //3. No filter found, return argument as-is return arg; } @SuppressWarnings("unchecked") private C10NFilterProvider<Object> findFilterFor(Class<?> argType, Class<? extends Annotation> annotationClass) { return (C10NFilterProvider<Object>) filters .get(new AnnotatedClass(argType, annotationClass)); } } }