uk.q3c.krail.core.ui.ScopedUI.java Source code

Java tutorial

Introduction

Here is the source code for uk.q3c.krail.core.ui.ScopedUI.java

Source

/*
 *
 *  * 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.github.mcollovati.vertx.vaadin.communication.SockJSPushConnection;
import com.vaadin.annotations.Push;
import com.vaadin.server.ErrorHandler;
import com.vaadin.server.Page;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinSession;
import com.vaadin.server.communication.PushConnection;
import com.vaadin.shared.ui.ui.UIState;
import com.vaadin.ui.AbstractOrderedLayout;
import com.vaadin.ui.Component;
import com.vaadin.ui.Panel;
import com.vaadin.ui.UI;
import net.engio.mbassy.listener.Handler;
import net.engio.mbassy.listener.Listener;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.ConfigurationException;
import uk.q3c.krail.core.eventbus.SessionBus;
import uk.q3c.krail.core.eventbus.UIBus;
import uk.q3c.krail.core.form.Form;
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.I18NProcessor;
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.KrailPushConfiguration;
import uk.q3c.krail.core.push.PushMessageRouter;
import uk.q3c.krail.core.view.KrailView;
import uk.q3c.krail.core.view.KrailViewHolder;
import uk.q3c.krail.eventbus.SubscribeTo;
import uk.q3c.krail.i18n.CurrentLocale;
import uk.q3c.krail.i18n.LocaleChangeBusMessage;
import uk.q3c.krail.i18n.Translate;
import uk.q3c.util.guice.SerializationSupport;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;

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, UIKey, int)} and passing them to the {@link PushMessageRouter}.
 * <p>
 * For a description of the Krail server push implementation see: http://krail.readthedocs.io/en/develop/tutorial/tutorial-push.html
 * <p>
 * The devloper guide provides more information on the changes for Vertx: See http://krail.readthedocs.io/en/develop/devguide/devguide-push.html
 *
 * @author David Sowerby
 */
@Listener
@SubscribeTo({ UIBus.class, SessionBus.class })
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;
    protected final Translate translate;
    protected final I18NProcessor i18NProcessor;
    protected boolean viewDisplayPanelSizeFull = true;
    private SerializationSupport serializationSupport;
    private KrailPushConfiguration pushConfig;
    private Broadcaster broadcaster;
    private UIKey instanceKey;
    protected AbstractOrderedLayout screenLayout;
    private UIScope uiScope;
    protected KrailView view;
    private Panel viewDisplayPanel;

    protected ScopedUI(Navigator navigator, ErrorHandler errorHandler, Broadcaster broadcaster,
            PushMessageRouter pushMessageRouter, ApplicationTitle applicationTitle, Translate translate,
            CurrentLocale currentLocale, I18NProcessor i18NProcessor, SerializationSupport serializationSupport,
            KrailPushConfiguration pushConfig) {
        super();
        this.errorHandler = errorHandler;
        this.navigator = navigator;
        //        this.converterFactory = converterFactory;
        this.broadcaster = broadcaster;
        this.pushMessageRouter = pushMessageRouter;
        this.applicationTitle = applicationTitle;
        this.translate = translate;
        this.i18NProcessor = i18NProcessor;
        this.currentLocale = currentLocale;
        this.serializationSupport = serializationSupport;
        registerWithBroadcaster();
        overridePushConfiguration(pushConfig);
    }

    /**
     * A rather horrible way of setting the value of private field from parent class.  See http://krail.readthedocs.io/en/develop/devguide/devguide-push.html
     *
     * @param pushConfig
     */
    private void overridePushConfiguration(KrailPushConfiguration pushConfig) {
        try {
            pushConfig.setUi(this);
            Field pushConfigField = FieldUtils.getField(this.getClass(), "pushConfiguration", true);
            pushConfigField.set(this, pushConfig);

        } catch (Exception e) {
            throw new ConfigurationException("Unable to set up push for the UI", e);
        }
    }

    protected final void registerWithBroadcaster() {
        broadcaster.register(Broadcaster.ALL_MESSAGES, this);
    }

    public UIKey getInstanceKey() {
        return instanceKey;
    }

    protected void setInstanceKey(UIKey instanceKey) {
        checkNotNull(instanceKey);
        this.instanceKey = instanceKey;
    }

    protected void setScope(UIScope uiScope) {
        checkNotNull(uiScope);
        this.uiScope = uiScope;
        uiScope.setSerializationSupport(serializationSupport);
    }

    @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;
    }

    @Override
    public void setNavigator(com.vaadin.navigator.Navigator navigator) {
        throw new MethodReconfigured("UI.setNavigator() not available, use injection instead");
    }

    @Override
    public void changeView(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");
        }
        if (toView instanceof Form) {
            ((Form) toView).translate();
        } else {
            i18NProcessor.translate(toView);
        }

        content.setSizeFull();
        getViewDisplayPanel().setContent(content);
        this.view = toView;
        updatePageTitle();
    }

    protected void updatePageTitle() {
        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) {
        log.debug("initialising ScopedUI");
        setConnectionUid();

        VaadinSession session = getSession();
        //        session.setConverterFactory(converterFactory);

        // page isn't available during injected construction, so we have to do this here
        Page page = getPage();
        page.addPopStateListener(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();
        i18NProcessor.translate(this);
        // Navigate to the correct start point
        String fragment = getPage().getUriFragment();
        getKrailNavigator().navigateTo(fragment);
    }

    /**
     *
     */
    private void setConnectionUid() {
        PushConnection pushConnection = getPushConnection();
        if (pushConnection != null) {
            if (pushConnection instanceof SockJSPushConnection) {
                SockJSPushConnection sockConnection = (SockJSPushConnection) pushConnection;
                Field uiIdField = FieldUtils.getField(SockJSPushConnection.class, "uiId", true);
                try {
                    uiIdField.set(sockConnection, this.getUIId());
                } catch (IllegalAccessException e) {
                    throw new ConfigurationException("Unable to set up push connection with correct UI Id", e);
                }
            }
        }
    }

    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);
        }
        if (viewDisplayPanelSizeFull) {
            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
     */
    public abstract AbstractOrderedLayout screenLayout();

    @Override
    public void receiveBroadcast(final String group, final String message, 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) {
        updatePageTitle();
    }

    public KrailView getView() {
        return view;
    }

    @SuppressWarnings("Duplicates")
    private void readObject(ObjectInputStream inputStream) throws ClassNotFoundException, IOException {
        beforeDeserialization();
        inputStream.defaultReadObject();
        beforeTransientInjection();
        serializationSupport.injectTransientFields(this);
        afterTransientInjection();
        serializationSupport.checkForNullTransients();
    }

    /**
     * By default does nothing but can be overridden to execute code before any other action is taken for deserialization.
     * It could be used to set exclusions for {@link #serializationSupport}
     */
    protected void beforeDeserialization() {

    }

    /**
     * By default does nothing but can be overridden to populate transient fields after {@link #serializationSupport}
     * has injected Guice dependencies. It could be used to set exclusions for {@link #serializationSupport}
     */
    protected void beforeTransientInjection() {

    }

    /**
     * By default does nothing but can be overridden to populate transient fields before {@link #serializationSupport}
     * injects Guice dependencies
     */
    protected void afterTransientInjection() {

    }

    /**
     * Made public to enable integration with VertxPushConfiguration
     */
    @Override
    public UIState getState(boolean markAsDirty) {
        return super.getState(markAsDirty);
    }

    /**
     * Made public to enable integration with VertxPushConfiguration
     */
    @Override
    public UIState getState() {
        return super.getState();
    }
}