/*
*
* * Copyright (c) 2016. David Sowerby
* *
* * Licensed 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 uk.q3c.krail.core.i18n;
import com.google.inject.AbstractModule;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import org.apache.commons.lang3.LocaleUtils;
import uk.q3c.krail.core.guice.uiscope.UIScoped;
import uk.q3c.krail.core.guice.vsscope.VaadinSessionScoped;
import uk.q3c.krail.core.option.Option;
import uk.q3c.krail.core.persist.cache.i18n.DefaultPatternCacheLoader;
import uk.q3c.krail.core.persist.cache.i18n.PatternCacheLoader;
import uk.q3c.krail.core.persist.clazz.i18n.ClassPatternDao;
import uk.q3c.krail.core.persist.clazz.i18n.ClassPatternSource;
import uk.q3c.krail.core.persist.clazz.i18n.DefaultClassPatternDao;
import uk.q3c.krail.core.persist.clazz.i18n.EnumResourceBundle;
import uk.q3c.krail.core.persist.common.common.KrailPersistenceUnitHelper;
import uk.q3c.krail.core.persist.common.i18n.PatternDao;
import javax.annotation.Nonnull;
import java.lang.annotation.Annotation;
import java.util.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.inject.multibindings.Multibinder.newSetBinder;
/**
* Configures I18N for an application.
* <p>
* An I18N source is the equivalent of a persistence unit (the class based, EnumResourceBundle provision of I18N is considered to be a single source / PU).
* <p>
* A source is represented by an annotation, for example {@link ClassPatternSource} - which is provided by this module. Other persistence providers (for
* example krail-jpa) will provide bindings to their own {@link #sources}, which Guice merges into a single map.
* <p>
* An I18NKey implementation - for example, {@link LabelKey}, and its associated {@link EnumResourceBundle}s, are the equivalent to a Java Resource bundle
*/
public class I18NModule extends AbstractModule {
private final TypeLiteral<Class<? extends Annotation>> annotationLiteral = KrailPersistenceUnitHelper.annotationClassLiteral();
private final TypeLiteral<PatternDao> patternDaoTypeLiteral = new TypeLiteral<PatternDao>() {
};
private Locale defaultLocale = Locale.UK;
private LinkedHashSet<Class<? extends Annotation>> prepSources = new LinkedHashSet<>(); // retain order;
private Set<Class<? extends Annotation>> prepSourcesDefaultOrder = new LinkedHashSet<>();
private Map<Class<? extends I18NKey>, LinkedHashSet<Class<? extends Annotation>>> prepSourcesOrderByBundle = new LinkedHashMap<>();
private Set<Locale> prepSupportedLocales = new LinkedHashSet<>();
private LinkedHashSet<Class<? extends Annotation>> prepTargets = new LinkedHashSet<>();
private MapBinder<Class<? extends Annotation>, PatternDao> sources;
private Multibinder<Class<? extends Annotation>> sourcesDefaultOrder;
private MapBinder<Class<? extends I18NKey>, LinkedHashSet<Class<? extends Annotation>>> sourcesOrderByBundle;
private Multibinder<Locale> supportedLocales;
private MapBinder<Class<? extends Annotation>, PatternDao> targets;
@Override
protected void configure() {
TypeLiteral<LinkedHashSet<Class<? extends Annotation>>> setOfAnnotationsTypeLiteral = new TypeLiteral<LinkedHashSet<Class<? extends Annotation>>>() {
};
TypeLiteral<Class<? extends I18NKey>> keyClassTypeLiteral = new TypeLiteral<Class<? extends I18NKey>>() {
};
supportedLocales = newSetBinder(binder(), Locale.class, SupportedLocales.class);
sourcesDefaultOrder = newSetBinder(binder(), annotationLiteral, PatternSourceOrderDefault.class);
sources = MapBinder.newMapBinder(binder(), annotationLiteral, patternDaoTypeLiteral, PatternSources.class);
targets = MapBinder.newMapBinder(binder(), annotationLiteral, patternDaoTypeLiteral, PatternTargets.class);
sourcesOrderByBundle = MapBinder.newMapBinder(binder(), keyClassTypeLiteral, setOfAnnotationsTypeLiteral, PatternSourceOrderByBundle.class);
define();
bindProcessor();
bindCurrentLocale();
bindDefaultLocale();
bindTranslate();
bindPatternSource();
bindPatternCacheLoader();
bindPatternUtility();
bindFieldScanner();
bindHostClassIdentifier();
// bindDatabaseBundleReader();
bindSupportedLocales();
bindSources();
bindSourcesDefaultOrder();
bindSourceOrderByBundle();
bindTargets();
bindClassPatternDao();
bindPatternDao();
bindI18NSourceProvider();
}
/**
* Binds the entries set by calls to {@link #target} (which may be none)
*/
protected void bindTargets() {
for (Class<? extends Annotation> entry : prepTargets) {
Key<PatternDao> key = Key.get(PatternDao.class, entry);
targets.addBinding(entry)
.to(key);
}
}
/**
* Binds the {@link ClassPatternDao} to its default implementation, override to provide your own implementation
*/
protected void bindClassPatternDao() {
bind(ClassPatternDao.class).to(DefaultClassPatternDao.class);
}
/**
* Binds the {@link PatternDao} to the annotation for {@link ClassPatternDao}. This enables class based I18N patterns to be used, if {@link
* ClassPatternSource} is included within I18NModule as a source.
*/
@SuppressWarnings("UninstantiableBinding") // fooled by bindClassPatternDao causing indirection
protected void bindPatternDao() {
bind(PatternDao.class).annotatedWith(ClassPatternSource.class)
.to(ClassPatternDao.class);
}
/**
* Binds sources to {@link PatternDao} classes as defined by {@link #prepSources}, setting {@link ClassPatternDao} as default if nothing
* defined.
*/
public void bindSources() {
if (prepSources.isEmpty()) {
prepSources.add(ClassPatternSource.class);
}
for (Class<? extends Annotation> entry : prepSources) {
Key<PatternDao> key = Key.get(PatternDao.class, entry);
sources.addBinding(entry)
.to(key);
}
}
/**
* Binds {@link Locale} in {@link SupportedLocales} as defined by {@link #prepSupportedLocales}, setting {@link Locale#UK} as default if nothing defined.
*/
protected void bindSupportedLocales() {
if (prepSupportedLocales.isEmpty()) {
prepSupportedLocales.add(Locale.UK);
}
for (Locale locale : prepSupportedLocales) {
supportedLocales.addBinding()
.toInstance(locale);
}
}
/**
* See javadoc for {@link I18NHostClassIdentifier} for an explanation of what this is for. Override this method if you provide your own implementation
*/
protected void bindHostClassIdentifier() {
bind(I18NHostClassIdentifier.class).to(DefaultI18NHostClassIdentifier.class);
}
/**
* See javadoc for {@link I18NFieldScanner} for an explanation of what this is for. Override this method if you provide your own implementation
*/
protected void bindFieldScanner() {
bind(I18NFieldScanner.class).to(DefaultI18NFieldScanner.class);
}
/**
* See javadoc for {@link PatternUtility} for an explanation of what this is for. Override this method if you provide your own implementation
*/
protected void bindPatternUtility() {
bind(PatternUtility.class).to(DefaultPatternUtility.class);
}
/**
* See javadoc for {@link PatternSourceProvider} for an explanation of what this is for. Override this method if you provide your own implementation
*/
protected void bindI18NSourceProvider() {
bind(PatternSourceProvider.class).to(DefaultPatternSourceProvider.class);
}
/**
* See javadoc for {@link DefaultPatternCacheLoader} for an explanation of what this is for. Override this method if you provide your own implementation
*/
protected void bindPatternCacheLoader() {
bind(PatternCacheLoader.class).to(DefaultPatternCacheLoader.class);
}
/**
* It is generally advisable to use the same scope for this as for current locale (see {@link #bindCurrentLocale()}. See javadoc for {@link
* DefaultPatternSource} for an explanation of what this is for. Override this method if you provide your own implementation
*/
protected void bindPatternSource() {
bind(PatternSource.class).to(DefaultPatternSource.class)
.in(VaadinSessionScoped.class);
}
/**
* See javadoc for {@link DefaultTranslate} for an explanation of what this is for. Override this method if you provide your own implementation
*/
protected void bindTranslate() {
bind(Translate.class).to(DefaultTranslate.class);
}
/**
* Override this method to provide your own implementation of {@link CurrentLocale} or to change the scope used.
* Choose between {@link UIScoped} or {@link VaadinSessionScoped}, depending on whether you want users to set the
* language for each browser tab or each browser instance, respectively.
*/
protected void bindCurrentLocale() {
bind(CurrentLocale.class).to(DefaultCurrentLocale.class)
.in(VaadinSessionScoped.class);
}
/**
* If you don't wish to configure this module from your Binding Manager, sub-class and override this method to define calls to {@link
* #supportedLocales(Locale...)}, {@link #defaultLocale(Locale)} etc - then modify your Binding Manager to use your sub-class
* <p>
* If you are only using more than one I18N source, the order which you want them accessed needs to be specified using {@link #sourcesDefaultOrder}
* and/or {@link #sourcesOrderByBundle}. This is because Guice does not guarantee order if multiple MapBinders are combined (through the use of multiple
* modules)
*/
protected void define() {
}
/**
* Override this method to provide your own implementation of {@link I18NProcessor}
*/
protected void bindProcessor() {
bind(I18NProcessor.class).to(DefaultI18NProcessor.class);
}
/**
* Binds {{@link #defaultLocale} to annotation {@link DefaultLocale}
*/
protected void bindDefaultLocale() {
bind(Locale.class).annotatedWith(DefaultLocale.class)
.toInstance(defaultLocale);
}
protected void bindSourcesDefaultOrder() {
for (Class<? extends Annotation> source : prepSourcesDefaultOrder) {
sourcesDefaultOrder.addBinding()
.toInstance(source);
}
}
protected void bindSourceOrderByBundle() {
for (Map.Entry<Class<? extends I18NKey>, LinkedHashSet<Class<? extends Annotation>>> entry : prepSourcesOrderByBundle.entrySet()) {
sourcesOrderByBundle.addBinding(entry.getKey())
.toInstance(entry.getValue());
}
}
/**
* This locale is used when all else fails - that is, when the neither the browser locale or user option is valid. See {@link DefaultCurrentLocale} for
* more
* detail. This is also added to {@link #supportedLocales}, so if you only ant to support one Locale, just call this method.
*
* @param localeString
* valid locale string to be used as default
*
* @return this for fluency
*
* @throws IllegalArgumentException
* if the locale string is invalid (see {@link LocaleUtils#toLocale(String)} for format)
*/
public I18NModule defaultLocale(@Nonnull String localeString) {
checkNotNull(localeString);
checkArgument(!localeString.isEmpty());
defaultLocale(localeFromString(localeString));
return this;
}
/**
* This locale is used when all else fails - that is, when the neither the browser locale or user option is valid. See {@link DefaultCurrentLocale} for
* more
* detail. This is also added to {@link #supportedLocales}, so if you only want to support one Locale, just call this method.
*
* @param locale
* Locale object for the default
*/
public I18NModule defaultLocale(@Nonnull Locale locale) {
checkNotNull(locale);
defaultLocale = locale;
prepSupportedLocales.add(defaultLocale);
return this;
}
/**
* Converts String to Locale, strictly
*
* @param localeString
* the String to convert, see {@link LocaleUtils#toLocale(String)} for format
*
* @return selected Locale
*
* @throws IllegalArgumentException
* if the {@code localeString} is not valid
*/
protected Locale localeFromString(String localeString) {
return LocaleUtils.toLocale(localeString);
}
/**
* These are the locales that you will provide language support for. Attempts to change to any other Locale will throw an exception. {@link
* #defaultLocale} is automatically added
*
* @param locales
* the locales to support
*
* @return this for fluency
*/
public I18NModule supportedLocales(@Nonnull Locale... locales) {
Collections.addAll(prepSupportedLocales, locales);
return this;
}
/**
* These are the locales that you will provide language support for. Attempts to change to any other Locale will throw an exception. {@link
* #defaultLocale} is automatically added
*
* @param localeStrings
* the locales to support
*
* @return this for fluency
*
* @throws IllegalArgumentException
* if a locale string is invalid (see {@link LocaleUtils#toLocale(String)} for format)
*/
public I18NModule supportedLocales(@Nonnull String... localeStrings) {
for (String localeString : localeStrings) {
prepSupportedLocales.add(localeFromString(localeString));
}
return this;
}
/**
* If you are using one source for I18N, there is no need to use this method
* <p>
* However, Guice does not guarantee order if multiple MapBinders are combined (through the use of multiple modules) - the order must then be explicitly
* specified using this method.
* <p>
* This order is used for ALL key classes, unless overridden by {@link #sourcesOrderByBundle}, or by {@link Option} in {@link
* DefaultPatternSource}
*
* @return this for fluency
*/
@SafeVarargs
public final I18NModule sourcesDefaultOrder(@Nonnull Class<? extends Annotation>... sources) {
checkNotNull(sources);
Collections.addAll(prepSourcesDefaultOrder, sources);
return this;
}
/**
* This method sets the order in which to poll the I18N pattern sources, but for a specific bundle (I18NKey class)
* <p>
* {@link #sourcesDefaultOrder} applies to all key classes (bundles)
* <p>
* <p>
* If you have only one source - you definitely won't need this method
*
* @param keyClass
* the class of the I18NKey to use (in Java terms the resource 'bundle')
* @param sources
* a set of sources, (or 'formats' in resourceBundle terms). These should be all, or a subset, of {@link #sources}
*
* @return this for fluency
*/
@SafeVarargs
public final I18NModule sourcesOrderByBundle(@Nonnull Class<? extends I18NKey> keyClass, @Nonnull Class<? extends Annotation>... sources) {
checkNotNull(keyClass);
checkNotNull(sources);
LinkedHashSet<Class<? extends Annotation>> sourceSet = new LinkedHashSet<>(Arrays.asList(sources));
prepSourcesOrderByBundle.put(keyClass, sourceSet);
return this;
}
/**
* An I18N target is used when writing out I18N patterns. Typically this is used either by the auto-stub feature of {@link PatternSource} or the {@link
* PatternUtility} for moving patterns from one source to another.
* <p>
* All the targets which may be required should be added with this method (this ensures the bindings are in place). The selection of which target to use,
* if any, is made using the Option settings for {@link PatternSourceProvider}
* <p>
* <p>
* Adds an I18N target, identified by {@code target} (target is roughly equivalent to 'format' in the native Java I18N support, except that it does not
* imply any particular type of target - it is just an identifier). Within Krail the target is expected to be the equivalent of a persistence unit
* <p>
* Note that if targets are set in multiple Guice modules, they will be merged by Guice into a
*
* @param target
* A BindingAnnotation identifying a Persistence Unit (or equivalent) that is providing a DAO as a source
*
* @return this for fluency
*/
public final I18NModule target(@Nonnull Class<? extends Annotation> target) {
checkNotNull(target);
prepTargets.add(target);
return this;
}
/**
* Adds an I18N source, identified by {@code source} (source is roughly equivalent to 'format' in the native Java I18N support, except that it does not
* imply any particular type of source - it is just an identifier). Within Krail the source is expected to be the equivalent of a persistence unit
*
* @param source
* A BindingAnnotation identifying a Persistence Unit (or equivalent) that is providing a DAO as a source
*
* @return this for fluency
*/
public I18NModule source(@Nonnull Class<? extends Annotation> source) {
checkNotNull(source);
prepSources.add(source);
return this;
}
}