/*
*
* * 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.ui;
import com.vaadin.annotations.Push;
import com.vaadin.data.util.converter.ConverterFactory;
import com.vaadin.server.ErrorHandler;
import com.vaadin.server.Page;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinSession;
import com.vaadin.ui.AbstractOrderedLayout;
import com.vaadin.ui.Component;
import com.vaadin.ui.Panel;
import com.vaadin.ui.UI;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.engio.mbassy.listener.Handler;
import net.engio.mbassy.listener.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.config.ConfigurationException;
import uk.q3c.krail.core.guice.uiscope.UIKey;
import uk.q3c.krail.core.guice.uiscope.UIScope;
import uk.q3c.krail.core.guice.uiscope.UIScoped;
import uk.q3c.krail.core.i18n.CurrentLocale;
import uk.q3c.krail.core.i18n.I18NProcessor;
import uk.q3c.krail.core.i18n.LocaleChangeBusMessage;
import uk.q3c.krail.core.i18n.Translate;
import uk.q3c.krail.core.navigate.Navigator;
import uk.q3c.krail.core.push.Broadcaster;
import uk.q3c.krail.core.push.Broadcaster.BroadcastListener;
import uk.q3c.krail.core.push.PushMessageRouter;
import uk.q3c.krail.core.view.KrailView;
import uk.q3c.krail.core.view.KrailViewHolder;
import javax.annotation.Nonnull;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* The base class for all Krail UIs, it provides an essential part of the {@link UIScoped} mechanism. It also provides
* support for Vaadin Server Push (but only if you annotate your sub-class with {@link Push}), by capturing broadcast
* messages in {@link #processBroadcastMessage(String, String)} and passing them to the {@link PushMessageRouter}. For
* a
* full description of the Krail server push implementation see: https://sites.google.com/site/q3cjava/server-push
*
* @author David Sowerby
*/
@Listener
public abstract class ScopedUI extends UI implements KrailViewHolder, BroadcastListener {
private static Logger log = LoggerFactory.getLogger(ScopedUI.class);
protected final CurrentLocale currentLocale;
private final ErrorHandler errorHandler;
private final ConverterFactory converterFactory;
private final PushMessageRouter pushMessageRouter;
private final Navigator navigator;
private final ApplicationTitle applicationTitle;
private final Translate translate;
private final I18NProcessor translator;
private Broadcaster broadcaster;
private UIKey instanceKey;
private AbstractOrderedLayout screenLayout;
private UIScope uiScope;
private KrailView view;
private Panel viewDisplayPanel;
protected ScopedUI(Navigator navigator, ErrorHandler errorHandler, ConverterFactory converterFactory, Broadcaster broadcaster, PushMessageRouter
pushMessageRouter, ApplicationTitle applicationTitle, Translate translate, CurrentLocale currentLocale, I18NProcessor translator) {
super();
this.errorHandler = errorHandler;
this.navigator = navigator;
this.converterFactory = converterFactory;
this.broadcaster = broadcaster;
this.pushMessageRouter = pushMessageRouter;
this.applicationTitle = applicationTitle;
this.translate = translate;
this.translator = translator;
this.currentLocale = currentLocale;
registerWithBroadcaster();
}
protected final void registerWithBroadcaster() {
broadcaster.register(Broadcaster.ALL_MESSAGES, this);
}
public UIKey getInstanceKey() {
return instanceKey;
}
protected void setInstanceKey(@Nonnull UIKey instanceKey) {
checkNotNull(instanceKey);
this.instanceKey = instanceKey;
}
protected void setScope(@Nonnull UIScope uiScope) {
checkNotNull(uiScope);
this.uiScope = uiScope;
}
@Override
public void detach() {
if (uiScope != null) {
uiScope.releaseScope(instanceKey);
}
broadcaster.unregister(Broadcaster.ALL_MESSAGES, this);
super.detach();
}
/**
* The Vaadin navigator has been replaced by the Navigator, use {@link #getKrailNavigator()} instead. Would prefer to throw an exception but this method
* still gets called by core Vaadin
*
* @see com.vaadin.ui.UI#getNavigator()
*/
@Override
@Deprecated
public com.vaadin.navigator.Navigator getNavigator() {
return null;
}
@SuppressFBWarnings("ACEM_ABSTRACT_CLASS_EMPTY_METHODS")
@Override
public void setNavigator(com.vaadin.navigator.Navigator navigator) {
throw new MethodReconfigured("UI.setNavigator() not available, use injection instead");
}
@Override
public void changeView(@Nonnull KrailView toView) {
checkNotNull(toView);
log.debug("changing view to {}", toView.getName());
Component content = toView.getRootComponent();
if (content == null) {
throw new ConfigurationException("The root component for " + toView.getName() + " cannot be null");
}
translator.translate(toView);
content.setSizeFull();
getViewDisplayPanel().setContent(content);
this.view = toView;
String pageTitle = pageTitle();
getPage().setTitle(pageTitle);
log.debug("Page title set to '{}'", pageTitle);
}
public Panel getViewDisplayPanel() {
if (viewDisplayPanel == null) {
viewDisplayPanel = new Panel();
}
return viewDisplayPanel;
}
/**
* Make sure you call this from sub-class overrides. The Vaadin Page is not available during the construction of
* this class, but is available when this method is invoked. As a result, this method sets the navigator a listener
* for URI changes and obtains the browser locale setting for initialising {@link CurrentLocale}. Both of these are
* provided by the Vaadin Page.
*
* @see com.vaadin.ui.UI#init(com.vaadin.server.VaadinRequest)
*/
@Override
protected void init(VaadinRequest request) {
VaadinSession session = getSession();
session.setConverterFactory(converterFactory);
// page isn't available during injected construction, so we have to do this here
Page page = getPage();
page.addUriFragmentChangedListener(navigator);
setErrorHandler(errorHandler);
session.setErrorHandler(errorHandler);
page.setTitle(pageTitle());
// also loads the UserSitemap if not already loaded
getKrailNavigator().init();
//layout this UI, which may also create UYI components
doLayout();
// now that browser is active, and user sitemap loaded, and UI constructed, set up currentLocale
currentLocale.readFromEnvironment();
translator.translate(this);
// Navigate to the correct start point
String fragment = getPage().getUriFragment();
getKrailNavigator().navigateTo(fragment);
}
public Navigator getKrailNavigator() {
return navigator;
}
/**
* Provides a locale sensitive title for your application (which appears in the browser tab). The title is defined
* by the {@link #applicationTitle}, which should be specified in your sub-class of {@link DefaultUIModule}. If view is not null, the view name is
* appended to the application name
*
* @return locale sensitive page title
*/
protected String pageTitle() {
return view == null ? translate.from(applicationTitle.getTitleKey()) : translate.from(applicationTitle.getTitleKey()) + ' ' + view.getName();
}
/**
* Uses the {@link #screenLayout} defined by sub-class implementations of {@link #screenLayout()}, expands it to
* full size, and sets the View display panel to take up all spare space.
*/
protected void doLayout() {
if (screenLayout == null) {
screenLayout = screenLayout();
}
screenLayout.setSizeFull();
if (viewDisplayPanel == null || viewDisplayPanel.getParent() == null) {
String msg = "Your implementation of ScopedUI.screenLayout() must include getViewDisplayPanel(). AS a "
+ "minimum this could be 'return new VerticalLayout(getViewDisplayPanel())'";
log.error(msg);
throw new ConfigurationException(msg);
}
viewDisplayPanel.setSizeFull();
setContent(screenLayout);
}
/**
* Override this to provide your screen layout. In order for Views to work one child component of this layout must
* be provided by {@link #getViewDisplayPanel()}. The simplest example would be
* {@code return new VerticalLayout(getViewDisplayPanel()}, which would set the View to take up all the available
* screen space. {@link BasicUI} is an example of a UI which contains a header and footer bar.
*
* @return the layout in which views are placed
*/
protected abstract AbstractOrderedLayout screenLayout();
@Override
public void receiveBroadcast(@Nonnull final String group, @Nonnull final String message, @Nonnull UIKey sender, int messageId) {
checkNotNull(group);
checkNotNull(message);
log.debug("UI instance {} receiving message id: {} from: {}", this.getInstanceKey(), messageId, sender);
access(() -> {
processBroadcastMessage(group, message, sender, messageId);
});
}
/**
* Distribute the message to listeners within this UIScope
*/
protected void processBroadcastMessage(String group, String message, UIKey sender, int messageId) {
pushMessageRouter.messageIn(group, message, sender, messageId);
}
/**
* Responds to a locale change from {@link CurrentLocale} and updates the translation for this UI and the current
* KrailView
*
* @param busMessage the message from the event bus. Not actually used, as translate looks up the current locale
*/
@SuppressWarnings("UnusedParameters")
@Handler
public void localeChanged(LocaleChangeBusMessage busMessage) {
translator.translate(this);
//during initial set up view has not been created but locale change gets called for other components
if (getView() != null) {
translator.translate(getView());
}
}
public KrailView getView() {
return view;
}
}