/*
* 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.share.EncodedResourceControl;
import com.github.rodionmoiseev.c10n.share.utils.Preconditions;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;
import java.util.Map.Entry;
@SuppressWarnings("WeakerAccess")//rationale: designed for 3rd party usage
public abstract class C10NConfigBase {
//DI
private final C10NCoreModule coreModule = new C10NCoreModule();
private LocaleProvider localeProvider = coreModule.defaultLocaleProvider();
private ClassLoader proxyClassLoader = C10N.class.getClassLoader();
private UntranslatedMessageHandler untranslatedMessageHandler = coreModule.defaultUnknownMessageHandler();
private final Map<String, C10NBundleBinder> bundleBinders = new HashMap<String, C10NBundleBinder>();
private final Map<Class<?>, C10NImplementationBinder<?>> binders = new HashMap<Class<?>, C10NImplementationBinder<?>>();
private final Map<Class<? extends Annotation>, C10NAnnotationBinder> annotationBinders = new HashMap<Class<? extends Annotation>, C10NAnnotationBinder>();
private final List<C10NFilterBinder<?>> filterBinders = new ArrayList<C10NFilterBinder<?>>();
private final List<C10NConfigBase> childConfigs = new ArrayList<C10NConfigBase>();
private final List<C10NPlugin> plugins = new ArrayList<C10NPlugin>();
private String keyPrefix = "";
private boolean debug = false;
private boolean configured = false;
private MessageFormatter formatter = new DefaultMessageFormatter();
/**
* <p>To be implemented by subclasses of {@link C10NConfigBase}.
*
* <p>Configuration methods are as follows:
* <ul>
* <li>{@link #bindAnnotation(Class)} - binds annotation that holds translation for a specific locale.</li>
* <li>{@link #bindBundle(String)} - binds a resource bundle containing translated messages.</li>
* <li>{@link #install(C10NConfigBase)} - includes configuration from another c10n configuration module</li>
* <li>{@link #bind(Class)} - binds a custom class as an implementation for the given c10n interface</li>
* <li>{@link #setLocaleProvider(LocaleProvider)} - customises the locale retrieval logic</li>
* <li>{@link #setUntranslatedMessageHandler(UntranslatedMessageHandler)} - customises the placeholder for
* unresolved translation mappings</li>
* <li>{@link #setKeyPrefix(String)} - sets global key prefix to auto-prepend to all bundle keys</li>
* </ul>
*/
protected abstract void configure();
/**
* <p>Get the name of the package for which the current
* module is responsible.
*
* <p>The name of the package is used to determine
* which c10n interfaces this configuration is
* responsible for
*
* <p>The default implementation, which returns
* the string representation of the package of
* the configuration class, should suffice in most
* cases
*
* @return name of package the current module is responsible for
*/
protected String getConfigurationPackage() {
Package pkg = getClass().getPackage();
return pkg != null ? pkg.getName() : "";
}
void doConfigure() {
if (!configured) {
configure();
}
configured = true;
}
/**
* Registers a C10N extension plugin to this configuration module.
* @param plugin plugin to install (not-null)
*/
protected void installPlugin(C10NPlugin plugin){
plugins.add(plugin);
}
/**
* Returns the current list of plugins.
*
* @return list of registered plugins (not-null)
*/
protected List<C10NPlugin> getPlugins(){
return plugins;
}
/**
* <p>Install the given child c10n configuration module
*
* <p>This will apply the configuration to all c10n interfaces
* located in the child configuration package or below.
*
* @param childConfig child c10n configuration to install (not-null)
*/
protected void install(C10NConfigBase childConfig) {
childConfig.doConfigure();
childConfigs.add(childConfig);
}
/**
* <p>Create a custom implementation binding for the given c10n interface
*
* <p>There are two basic usages:
* <pre><code>
* bind(Messages.class).to(JapaneseMessagesImpl.class, Locale.JAPANESE);
* </code></pre>
*
* which will use the <code>JapaneseMessagesImpl.class</code> when locale is
* set to <code>Locale.JAPANESE</code>
*
* <p>The second usage is:
* <pre><code>
* bind(Messages.class).to(FallbackMessagesImpl.class);
* </code></pre>
*
* which will use the <code>FallbackMessagesImpl.class</code> when no other
* implementation class was matched for the current locale.
*
* @param c10nInterface C10N interface to create an implementation binding for (not-null)
* @param <T> C10N interface type
* @return implementation binding DSL object
*/
protected <T> C10NImplementationBinder<T> bind(Class<T> c10nInterface) {
C10NImplementationBinder<T> binder = new C10NImplementationBinder<T>();
binders.put(c10nInterface, binder);
return binder;
}
/**
* <p>Sets the {@link LocaleProvider} for this configuration.
*
* <p>Locale provider is consulted every time a translation is requested,
* that is every time a method on an c10n interface is called.
*
* <p>As a rule, there should be only one locale provider per application.
* Any locale providers defined in child configurations (see {@link #install(C10NConfigBase)}
* are disregarded.
*
* <p>Because locale provider has to be consulted on every translation request
* {@link com.github.rodionmoiseev.c10n.LocaleProvider#getLocale()} should avoid any CPU intensive processing
*
* <p>Default locale provider implementation always returns the same result as
* {@link java.util.Locale#getDefault()}
*
* @param localeProvider custom locale provider (not-null)
*/
protected void setLocaleProvider(LocaleProvider localeProvider) {
Preconditions.assertNotNull(localeProvider, "localeProvider");
this.localeProvider = localeProvider;
}
/**
* <p>Fixes the {@link java.util.Locale} to the specified locale.
*
* <p>Generally useful when your application needs to create separate
* {@link C10NMsgFactory} instances for each locale.
*
* @param locale Locale to use
*/
protected void setLocale(Locale locale) {
this.localeProvider = LocaleProviders.fixed(locale);
}
/**
* <p>Customise placeholder value for unresolved translations.
*
* <p>The default behaviour is to return a string in format:
* <pre>
* [InterfaceName].[MethodName]([ArgumentValues])
* </pre>
*
* @param handler custom implementation of untranslated message handler (not-null)
*/
protected void setUntranslatedMessageHandler(UntranslatedMessageHandler handler) {
Preconditions.assertNotNull(handler, "handler");
this.untranslatedMessageHandler = handler;
}
/**
* <p>Overrides the classloader used for loading c10n-interface proxies.
* This maybe useful in the context of hot-swapping enabled classloaders, like
* the one for Play framework 2.0, or OSGi.
*
* <p>By default, the classloader of {@link com.github.rodionmoiseev.c10n.C10N#getClass()} class is used.
*
* @param proxyClassLoader the classloader to use for loading c10n-interface proxies (not-null)
*/
protected void setProxyClassLoader(ClassLoader proxyClassLoader) {
Preconditions.assertNotNull(proxyClassLoader, "proxyClassLoader");
this.proxyClassLoader = proxyClassLoader;
}
/**
* <p>The c10n intefrace proxy classloader that will be used
* by the current instance of c10n message factory.</p>
*
* @return the classloader to be used for loading c10n-interface proxies (not-null)
*/
protected ClassLoader getProxyClassLoader() {
return proxyClassLoader;
}
/**
* <p>Create a method annotation binding to the specified locale
*
* <p>There are two basic usages:
* <pre><code>
* bindAnnotation(Ja.class).to(Locale.JAPANESE);
* </code></pre>
*
* which will tell c10n to take the value given in the <code>@Ja</code>
* annotation whenever the current locale is <code>Locale.JAPANESE</code>
*
* <p>The second usage is:
* <pre><code>
* bindAnnotation(En.class);
* </code></pre>
*
* which will make c10n always fallback to the value given in the <code>@En</code>
* annotation if no other annotation binding matched the current locale.
*
* <p>Note: Some default annotation bindings are defined in {@link com.github.rodionmoiseev.c10n.annotations.DefaultC10NAnnotations}.
* In order to use <code>install(new DefaultC10NAnnotations());</code> somewhere in your configuration
* (see {@link #install(C10NConfigBase)}
*
* @param annotationClass Class of the annotation to create a local binding for (not-null)
* @return annotation locale binding DSL object
*/
protected C10NAnnotationBinder bindAnnotation(Class<? extends Annotation> annotationClass) {
Preconditions.assertNotNull(annotationClass, "annotationClass");
checkAnnotationInterface(annotationClass);
C10NAnnotationBinder binder = new C10NAnnotationBinder();
annotationBinders.put(annotationClass, binder);
return binder;
}
/**
* <p>Create a filter binding for one or more argument types.
* <p>All arguments passed to c10n-interfaces with the specified type(s) will
* be converted to string using the filter generated by the given filter provider,
* instead of the conventional <code>toString()</code> method.
*
* <p>Filter creation (using {@link com.github.rodionmoiseev.c10n.C10NFilterProvider#get()} method) will be
* deferred until the first call to a c10n-interface method with a matching
* argument type is executed.
*
* @param c10NFilterProvider provider of filter implementation (not-null)
* @param type method argument type to which the filter should be applied
* @param <T> method argument type to which the filter should be applied
* @return filter binding DSL object
* @see C10NFilterBinder
*/
protected <T> C10NFilterBinder<T> bindFilter(C10NFilterProvider<T> c10NFilterProvider, Class<T> type) {
Preconditions.assertNotNull(c10NFilterProvider, "c10nFilterProvider");
Preconditions.assertNotNull(type, "type");
C10NFilterBinder<T> filterBinder = new C10NFilterBinder<T>(c10NFilterProvider, type);
filterBinders.add(filterBinder);
return filterBinder;
}
/**
* <p>Create a filter binding for one or more argument types.
* <p>All arguments passed to c10n-interfaces with the specified type(s) will
* be converted to string using this filter, instead of the conventional <code>toString()</code>
* method.
*
* @param c10nFilter filter implementation (not-null)
* @param type method argument type to which the filter should be applied
* @param <T> method argument type to which the filter should be applied
* @return filter binding DSL object
* @see C10NFilterBinder
*/
protected <T> C10NFilterBinder<T> bindFilter(C10NFilter<T> c10nFilter, Class<T> type) {
Preconditions.assertNotNull(c10nFilter, "c10nFilter");
Preconditions.assertNotNull(type, "type");
C10NFilterBinder<T> filterBinder = new C10NFilterBinder<T>(C10NFilters.staticFilterProvider(c10nFilter), type);
filterBinders.add(filterBinder);
return filterBinder;
}
/**
* <p>Set global key prefix. All other keys will be automatically prepended with the global key.
* <p>Settings key prefix to an empty string resets to default behaviour (no prefix).
*
* @param key the key to use at configuration scope (not null)
*/
protected void setKeyPrefix(String key) {
Preconditions.assertNotNull(key, "key");
keyPrefix = key;
}
String getKeyPrefix() {
return keyPrefix;
}
/**
* <p>If set to 'true', c10n will output debugging information to std-out at configuration and lookup time.
*
* @param debug debug flag
*/
protected void setDebug(boolean debug) {
this.debug = debug;
}
boolean isDebug() {
return debug;
}
protected void setMessageFormatter(MessageFormatter formatter){
this.formatter = formatter;
}
public MessageFormatter getMessageFormatter(){
return formatter;
}
List<C10NFilterBinder<?>> getFilterBinders() {
return filterBinders;
}
private void checkAnnotationInterface(Class<? extends Annotation> annotationClass) {
if (noMethod(annotationClass, "value") &&
noMethod(annotationClass, "intRes") &&
noMethod(annotationClass, "extRes"))
throw new C10NConfigException("Annotation could not be bound because it's missing any of value()," +
" intRes() or extRes() methods that return String. " +
"Please add at least one of those methods with return type of String. " +
"annotationClass=" + annotationClass.getName());
}
private boolean noMethod(Class<? extends Annotation> annotationClass, String methodName) {
Method valueMethod;
try {
valueMethod = annotationClass.getMethod(methodName);
if (!valueMethod.getReturnType().isAssignableFrom(String.class)) {
return true;
}
} catch (NoSuchMethodException e) {
return true;
}
return false;
}
protected C10NBundleBinder bindBundle(String baseName) {
C10NBundleBinder binder = new C10NBundleBinder();
bundleBinders.put(baseName, binder);
return binder;
}
List<ResourceBundle> getBundlesForLocale(Class<?> c10nInterface, Locale locale) {
List<ResourceBundle> res = new ArrayList<ResourceBundle>();
for (Entry<String, C10NBundleBinder> entry : bundleBinders.entrySet()) {
C10NBundleBinder binder = entry.getValue();
if (binder.getBoundInterfaces().isEmpty()
|| binder.getBoundInterfaces().contains(c10nInterface)) {
res.add(ResourceBundle.getBundle(entry.getKey(), locale,
new EncodedResourceControl("UTF-8")));
}
}
return res;
}
/**
* For each annotation bound in this configuration find all
* locales it has been bound to.
*
* @return annotation -> set of locale mapping
*/
Map<Class<? extends Annotation>, Set<Locale>> getAnnotationToLocaleMapping() {
Map<Class<? extends Annotation>, Set<Locale>> res = new HashMap<Class<? extends Annotation>, Set<Locale>>();
for (Entry<Class<? extends Annotation>, C10NAnnotationBinder> entry : annotationBinders.entrySet()) {
Set<Locale> locales = getLocales(entry.getKey(), res);
locales.add(entry.getValue().getLocale());
}
return res;
}
private Set<Locale> getLocales(Class<? extends Annotation> key, Map<Class<? extends Annotation>, Set<Locale>> res) {
Set<Locale> locales = res.get(key);
if (null == locales) {
locales = new HashSet<Locale>();
res.put(key, locales);
}
return locales;
}
Class<?> getBindingForLocale(Class<?> c10nInterface, Locale locale) {
C10NImplementationBinder<?> binder = binders.get(c10nInterface);
if (null != binder) {
Class<?> impl = binder.getBindingForLocale(locale);
if (null != impl) {
return impl;
}
}
return null;
}
/**
* List of all installed child configurations in
* the order they were installed.
*
* @return List of child configurations
*/
List<C10NConfigBase> getChildConfigs() {
return childConfigs;
}
/**
* Find all locales that have explicit implementation class
* bindings for this c10n interface.
*
* @param c10nInterface interface to find bindings for (not-null)
* @return Set of locales (not-null)
*/
Set<Locale> getImplLocales(Class<?> c10nInterface) {
Set<Locale> res = new HashSet<Locale>();
C10NImplementationBinder<?> binder = binders.get(c10nInterface);
if (binder != null) {
res.addAll(binder.bindings.keySet());
}
return res;
}
/**
* <p>Get a set of all locales explicitly declared in implementation bindings
*
* @return set of all bound locales
*/
Set<Locale> getAllImplementationBoundLocales() {
Set<Locale> res = new HashSet<Locale>();
for (C10NImplementationBinder<?> binder : binders.values()) {
res.addAll(binder.bindings.keySet());
}
return res;
}
/**
* Get the current locale as stipulated by the locale provider
*
* @return current locale
*/
Locale getCurrentLocale() {
return localeProvider.getLocale();
}
String getUntranslatedMessageString(Class<?> c10nInterface, Method method, Object[] methodArgs) {
return untranslatedMessageHandler.render(c10nInterface, method, methodArgs);
}
protected static class C10NAnnotationBinder {
private Locale locale = C10N.FALLBACK_LOCALE;
public void toLocale(Locale locale) {
Preconditions.assertNotNull(locale, "locale");
this.locale = locale;
}
public Locale getLocale() {
return locale;
}
}
protected static final class C10NImplementationBinder<T> {
private final Map<Locale, Class<?>> bindings = new HashMap<Locale, Class<?>>();
public C10NImplementationBinder<T> to(Class<? extends T> to, Locale forLocale) {
bindings.put(forLocale, to);
return this;
}
public C10NImplementationBinder<T> to(Class<? extends T> to) {
bindings.put(C10N.FALLBACK_LOCALE, to);
return this;
}
Class<?> getBindingForLocale(Locale locale) {
return bindings.get(locale);
}
}
/**
* <p>Filter binding DSL object.
* <p>Use {@link #annotatedWith(Class)} method to restrict the filter
* to arguments annotated with the specified annotation(s). Multiple
* annotations may be specified using chained {@link #annotatedWith(Class)} methods.
*/
protected static final class C10NFilterBinder<T> {
private final C10NFilterProvider<T> filter;
private final Class<T> type;
private final List<Class<? extends Annotation>> annotatedWith = new ArrayList<Class<? extends Annotation>>();
private C10NFilterBinder(C10NFilterProvider<T> filter, Class<T> type) {
this.filter = filter;
this.type = type;
}
/**
* <p>Restrict filter application only to arguments annotated with the
* given annotation.
* <p>Multiple annotations can be specified using method chaining.
*
* @param annotation annotation class to restrict filter application to
* @return this DLS object for method chaining
*/
public C10NFilterBinder<T> annotatedWith(Class<? extends Annotation> annotation) {
this.annotatedWith.add(annotation);
return this;
}
C10NFilterProvider<T> getFilterProvider() {
return filter;
}
Class<T> getType() {
return type;
}
public List<Class<? extends Annotation>> getAnnotatedWith() {
for (Class<? extends Annotation> a : annotatedWith) {
}
return annotatedWith;
}
}
}