/*
*
* * 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.Inject;
import com.vaadin.data.Property;
import com.vaadin.server.WebBrowser;
import net.engio.mbassy.bus.common.PubSubSupport;
import net.engio.mbassy.listener.Handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.eventbus.BusMessage;
import uk.q3c.krail.core.eventbus.SessionBusProvider;
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.option.OptionContext;
import uk.q3c.krail.core.option.OptionKey;
import uk.q3c.krail.core.shiro.SubjectProvider;
import uk.q3c.krail.core.ui.BrowserProvider;
import uk.q3c.krail.core.user.status.UserStatusBusMessage;
import javax.annotation.Nonnull;
import java.util.Locale;
import java.util.Set;
/**
* When a CurrentLocale is instantiated, or its {@link #readFromEnvironment()} method is called, it sets the current
* locale according to the following priorities:
* <ol>
* <li>If a user is authenticated, the {@link Option} for preferred locale is used, if valid</li>
* <li>If a user is not logged in, or the user option was invalid, the browser locale is used</li>
* <li>If the browser locale is not accessible, or is not a supported locale (as defined in {@link I18NModule} or its
* sub-class), the {@link #defaultLocale} is used.</li>
* </ol>
* When a user logs in after initialisation, the {@link Option} value for preferred locale is used, and the locale
* changed
* if required.
* When a user logs out, no change to locale is made, as the user may still have public pages they can view.
* <p>
* Scope for this class is set in {@link I18NModule} or its sub-class - this enables the developer to choose
* between {@link UIScoped} or {@link VaadinSessionScoped}, depending on whether they want their users to set the
* language for each browser tab or each browser instance, respectively. By default it is set to {@link
* VaadinSessionScoped}
* <p>
* {@link #defaultLocale} and {@link #supportedLocales} are set in {@link I18NModule} or its sub-class. An {@link
* UnsupportedLocaleException} will be thrown if an attempt is made to set a locale which is not in {@link
* #supportedLocales}, or if {@link #defaultLocale} the is not in {@link #supportedLocales}.
* <p>
* When a locale change is made a {@link LocaleChangeBusMessage} is despatched via the session event bus
*
* @author David Sowerby
* @date 5 May 2014
*/
public class DefaultCurrentLocale implements CurrentLocale, OptionContext {
public static final OptionKey<Locale> optionPreferredLocale = new OptionKey<>(Locale.UK, DefaultCurrentLocale.class, LabelKey.Preferred_Locale,
DescriptionKey.Preferred_Locale);
private static Logger log = LoggerFactory.getLogger(DefaultCurrentLocale.class);
private final BrowserProvider browserProvider;
private final Locale defaultLocale;
private final PubSubSupport<BusMessage> eventBus;
private final SubjectProvider subjectProvider;
private final Option option;
private Locale locale;
private Set<Locale> supportedLocales;
@Inject
protected DefaultCurrentLocale(BrowserProvider browserProvider, @SupportedLocales Set<Locale> supportedLocales, @DefaultLocale Locale defaultLocale,
SessionBusProvider eventBusProvider, SubjectProvider subjectProvider, Option option) {
super();
this.browserProvider = browserProvider;
this.supportedLocales = supportedLocales;
this.defaultLocale = defaultLocale;
this.eventBus = eventBusProvider.get();
this.subjectProvider = subjectProvider;
this.option = option;
locale = defaultLocale;
}
/**
* , see the Javadoc for this class
*/
@Override
public void readFromEnvironment() {
if (setLocaleFromOption(true)) {
return;
}
if (setLocaleFromBrowser(true)) {
return;
}
setLocale(defaultLocale, true);
}
/**
* Sets locale to the browser locale, if available. Browser locale will not be available if the browser is not
* active ( this usually only happens in testing or background tasks)
*
* @param fireListeners
* if true, fires change listeners if a change is made
*
* @return true if the browser was accessible and its locale is supported, false if no suitable locale has been set
*/
private boolean setLocaleFromBrowser(boolean fireListeners) {
WebBrowser webBrowser = browserProvider.get();
if (webBrowser != null) {
Locale browserLocale = webBrowser.getLocale();
if (supportedLocales.contains(browserLocale)) {
setLocale(browserLocale, fireListeners);
return true;
}
}
return false;
}
/**
* Sets the locale from the value held in {@link Option}, if available. {@link Option} will not be available if
* the user is not authenticated. It is possible that a user option is not supported (unlikely, but support for a language
* could be withdrawn after the user has chosen it), in which case the locale is set to the first supported locale. This is indicated by returning a
* value of false
*
* If a user has just logged out, but they may still have public pages to view, and they would
* probably want to view those in the same language as they had selected while logged in, so no changes are made
*
* @param fireListeners if true, fire the locale change listeners
*
* @return true if the user options was valid, otherwise false. Irrelevant for a logout
*/
private boolean setLocaleFromOption(boolean fireListeners) {
if (subjectProvider.get()
.isAuthenticated()) {
Locale selectedLocale = option.get(optionPreferredLocale);
if (supportedLocales.contains(selectedLocale)) {
setLocale(selectedLocale, fireListeners);
return true;
} else {
return false;
}
} else {
// user logged out, nothing to change
return false;
}
}
/**
* Sets the locale and optionally fires listeners. Typically, a call to this method is from a component which only
* allows the selection of a supported locale. However, if an attempt is made to set a locale which is not defined
* in {@link #supportedLocales}, an UnsupportedLocaleException is thrown
*
* @param locale
* the locale to set
* @param fireListeners
* if true, fire registered listeners
*/
@Override
public void setLocale(Locale locale, boolean fireListeners) {
if (supportedLocales.contains(locale)) {
if (locale != this.locale) {
this.locale = locale;
// Locale.setDefault(locale);
log.debug("CurrentLocale set to {}", locale);
if (fireListeners) {
log.debug("publish locale change");
eventBus.publish(new LocaleChangeBusMessage(this, locale));
}
}
} else {
throw new UnsupportedLocaleException(locale);
}
}
@Override
public Locale getLocale() {
return locale;
}
/**
* Explicitly set the locale
*/
@Override
public void setLocale(Locale locale) {
setLocale(locale, true);
}
@Nonnull
@Override
public Option getOption() {
return option;
}
@Override
public void optionValueChanged(Property.ValueChangeEvent event) {
setLocaleFromOption(true);
}
/**
* User has just logged in, look for their preferred Locale from user options.
*
* @param busMessage message provided by the {@link #eventBus}
*/
@Handler
public void userStatusChange(UserStatusBusMessage busMessage) {
log.debug("UserStatusBusMessage received");
setLocaleFromOption(true);
}
}