123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293 |
- /*
- * Copyright 2000-2018 Vaadin Ltd.
- *
- * 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 com.vaadin.navigator;
-
- import java.io.Serializable;
- import java.net.URI;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Map;
- import java.util.Objects;
-
- import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
- import com.vaadin.server.Page;
- import com.vaadin.server.Page.PopStateEvent;
- import com.vaadin.shared.Registration;
- import com.vaadin.shared.util.SharedUtil;
- import com.vaadin.ui.Component;
- import com.vaadin.ui.ComponentContainer;
- import com.vaadin.ui.CssLayout;
- import com.vaadin.ui.SingleComponentContainer;
- import com.vaadin.ui.UI;
- import com.vaadin.util.ReflectTools;
-
- /**
- * A navigator utility that allows switching of views in a part of an
- * application.
- * <p>
- * The view switching can be based e.g. on URI fragments containing the view
- * name and parameters to the view. There are two types of parameters for views:
- * an optional parameter string that is included in the fragment (may be
- * bookmarkable).
- * <p>
- * Views can be explicitly registered or dynamically generated and listening to
- * view changes is possible.
- * <p>
- * Note that {@link Navigator} is not a component itself but uses a
- * {@link ViewDisplay} to update contents based on the state.
- *
- * @author Vaadin Ltd
- * @since 7.0
- */
- public class Navigator implements Serializable {
-
- // TODO investigate relationship with TouchKit navigation support
-
- private static final String DEFAULT_VIEW_SEPARATOR = "/";
-
- private static final String DEFAULT_STATE_PARAMETER_SEPARATOR = "&";
- private static final String DEFAULT_STATE_PARAMETER_KEY_VALUE_SEPARATOR = "=";
-
- /**
- * Empty view component.
- */
- public static class EmptyView extends CssLayout implements View {
- /**
- * Create minimally sized empty view.
- */
- public EmptyView() {
- setWidth("0px");
- setHeight("0px");
- }
-
- @Override
- public void enter(ViewChangeEvent event) {
- // nothing to do
- }
- }
-
- /**
- * A {@link NavigationStateManager} using path info, HTML5 push state and
- * {@link PopStateEvent}s to track views and enable listening to view
- * changes. This manager can be enabled with UI annotation
- * {@link PushStateNavigation}.
- * <p>
- * The part of path after UI's "root path" (UI's path without view
- * identifier) is used as {@link View}s identifier. The rest of the path
- * after the view name can be used by the developer for extra parameters for
- * the View.
- * <p>
- * This class is mostly for internal use by Navigator, and is only public
- * and static to enable testing.
- *
- * @since 8.2
- */
- public static class PushStateManager implements NavigationStateManager {
- private Registration popStateListenerRegistration;
- private UI ui;
-
- /**
- * Creates a new PushStateManager.
- *
- * @param ui
- * the UI where the Navigator is attached to
- */
- public PushStateManager(UI ui) {
- this.ui = ui;
- }
-
- @Override
- public void setNavigator(Navigator navigator) {
- if (popStateListenerRegistration != null) {
- popStateListenerRegistration.remove();
- popStateListenerRegistration = null;
- }
- if (navigator != null) {
- popStateListenerRegistration = ui.getPage().addPopStateListener(
- event -> navigator.navigateTo(getState()));
- }
- }
-
- @Override
- public String getState() {
- // Get the current URL
- URI location = ui.getPage().getLocation();
- String path = location.getPath();
- if (ui.getUiPathInfo() != null
- && path.contains(ui.getUiPathInfo())) {
- // Split the path from after the UI PathInfo
- path = path.substring(path.indexOf(ui.getUiPathInfo())
- + ui.getUiPathInfo().length());
- } else if (path.startsWith(ui.getUiRootPath())) {
- // Use the whole path after UI RootPath
- String uiRootPath = ui.getUiRootPath();
- path = path.substring(uiRootPath.length());
- } else {
- throw new IllegalStateException(getClass().getSimpleName()
- + " is unable to determine the view path from the URL.");
- }
-
- if (path.startsWith("/")) {
- // Strip leading '/'
- path = path.substring(1);
- }
- return path;
- }
-
- @Override
- public void setState(String state) {
- StringBuilder sb = new StringBuilder(ui.getUiRootPath());
- if (!ui.getUiRootPath().endsWith("/")) {
- // make sure there is a '/' between the root path and the
- // navigation state.
- sb.append('/');
- }
- sb.append(state);
- URI location = ui.getPage().getLocation();
- if (location != null) {
- ui.getPage().pushState(location.resolve(sb.toString()));
- } else {
- throw new IllegalStateException(
- "The Page of the UI does not have a location.");
- }
- }
- }
-
- /**
- * A {@link NavigationStateManager} using hashbang fragments in the Page
- * location URI to track views and enable listening to view changes.
- * <p>
- * A hashbang URI is one where the optional fragment or "hash" part - the
- * part following a # sign - is used to encode navigation state in a web
- * application. The advantage of this is that the fragment can be
- * dynamically manipulated by javascript without causing page reloads.
- * <p>
- * This class is mostly for internal use by Navigator, and is only public
- * and static to enable testing.
- * <p>
- * <strong>Note:</strong> Since 8.2 you can use {@link PushStateManager},
- * which is based on HTML5 History API. To use it, add
- * {@link PushStateNavigation} annotation to the UI.
- */
- public static class UriFragmentManager implements NavigationStateManager {
- private final Page page;
- private Navigator navigator;
- private Registration uriFragmentRegistration;
-
- /**
- * Creates a new URIFragmentManager and attach it to listen to URI
- * fragment changes of a {@link Page}.
- *
- * @param page
- * page whose URI fragment to get and modify
- */
- public UriFragmentManager(Page page) {
- this.page = page;
- }
-
- @Override
- public void setNavigator(Navigator navigator) {
- if (this.navigator == null && navigator != null) {
- uriFragmentRegistration = page.addUriFragmentChangedListener(
- event -> navigator.navigateTo(getState()));
- } else if (this.navigator != null && navigator == null) {
- uriFragmentRegistration.remove();
- }
- this.navigator = navigator;
- }
-
- @Override
- public String getState() {
- String fragment = getFragment();
- if (fragment == null || !fragment.startsWith("!")) {
- return "";
- } else {
- return fragment.substring(1);
- }
- }
-
- @Override
- public void setState(String state) {
- setFragment("!" + state);
- }
-
- /**
- * Returns the current URI fragment tracked by this UriFragentManager.
- *
- * @return The URI fragment.
- */
- protected String getFragment() {
- return page.getUriFragment();
- }
-
- /**
- * Sets the URI fragment to the given string.
- *
- * @param fragment
- * The new URI fragment.
- */
- protected void setFragment(String fragment) {
- page.setUriFragment(fragment, false);
- }
- }
-
- /**
- * A ViewDisplay that replaces the contents of a {@link ComponentContainer}
- * with the active {@link View}.
- * <p>
- * All components of the container are removed before adding the new view to
- * it.
- * <p>
- * This display only supports views that are {@link Component}s themselves.
- * Attempting to display a view that is not a component causes an exception
- * to be thrown.
- */
- public static class ComponentContainerViewDisplay implements ViewDisplay {
-
- private final ComponentContainer container;
-
- /**
- * Create new {@link ViewDisplay} that updates a
- * {@link ComponentContainer} to show the view.
- */
- public ComponentContainerViewDisplay(ComponentContainer container) {
- this.container = container;
- }
-
- @Override
- public void showView(View view) {
- container.removeAllComponents();
- container.addComponent(view.getViewComponent());
- }
- }
-
- /**
- * A ViewDisplay that replaces the contents of a
- * {@link SingleComponentContainer} with the active {@link View}.
- * <p>
- * This display only supports views that are {@link Component}s themselves.
- * Attempting to display a view that is not a component causes an exception
- * to be thrown.
- */
- public static class SingleComponentContainerViewDisplay
- implements ViewDisplay {
-
- private final SingleComponentContainer container;
-
- /**
- * Create new {@link ViewDisplay} that updates a
- * {@link SingleComponentContainer} to show the view.
- */
- public SingleComponentContainerViewDisplay(
- SingleComponentContainer container) {
- this.container = container;
- }
-
- @Override
- public void showView(View view) {
- container.setContent(view.getViewComponent());
- }
- }
-
- /**
- * A ViewProvider which supports mapping a single view name to a single
- * pre-initialized view instance.
- *
- * For most cases, ClassBasedViewProvider should be used instead of this.
- */
- public static class StaticViewProvider implements ViewProvider {
- private final String viewName;
- private final View view;
-
- /**
- * Creates a new view provider which returns a pre-created view
- * instance.
- *
- * @param viewName
- * name of the view (not null)
- * @param view
- * view instance to return (not null), reused on every
- * request
- */
- public StaticViewProvider(String viewName, View view) {
- this.viewName = viewName;
- this.view = view;
- }
-
- @Override
- public String getViewName(String navigationState) {
- if (null == navigationState) {
- return null;
- }
- if (navigationState.equals(viewName)
- || navigationState.startsWith(viewName + "/")) {
- return viewName;
- }
- return null;
- }
-
- @Override
- public View getView(String viewName) {
- if (this.viewName.equals(viewName)) {
- return view;
- }
- return null;
- }
-
- /**
- * Get the view name for this provider.
- *
- * @return view name for this provider
- */
- public String getViewName() {
- return viewName;
- }
- }
-
- /**
- * A ViewProvider which maps a single view name to a class to instantiate
- * for the view.
- * <p>
- * Note that the view class must be accessible by the class loader used by
- * the provider. This may require its visibility to be public.
- * <p>
- * This class is primarily for internal use by {@link Navigator}.
- */
- public static class ClassBasedViewProvider implements ViewProvider {
-
- private final String viewName;
- private final Class<? extends View> viewClass;
-
- /**
- * Create a new view provider which creates new view instances based on
- * a view class.
- *
- * @param viewName
- * name of the views to create (not null)
- * @param viewClass
- * class to instantiate when a view is requested (not null)
- */
- public ClassBasedViewProvider(String viewName,
- Class<? extends View> viewClass) {
- if (null == viewName || null == viewClass) {
- throw new IllegalArgumentException(
- "View name and class should not be null");
- }
- this.viewName = viewName;
- this.viewClass = viewClass;
- }
-
- @Override
- public String getViewName(String navigationState) {
- if (null == navigationState) {
- return null;
- }
- if (navigationState.equals(viewName)
- || navigationState.startsWith(viewName + "/")) {
- return viewName;
- }
- return null;
- }
-
- @Override
- public View getView(String viewName) {
- if (this.viewName.equals(viewName)) {
- return ReflectTools.createInstance(viewClass);
- }
- return null;
- }
-
- /**
- * Get the view name for this provider.
- *
- * @return view name for this provider
- */
- public String getViewName() {
- return viewName;
- }
-
- /**
- * Get the view class for this provider.
- *
- * @return {@link View} class
- */
- public Class<? extends View> getViewClass() {
- return viewClass;
- }
- }
-
- /**
- * The {@link UI} bound with the Navigator.
- */
- protected UI ui;
-
- /**
- * The {@link NavigationStateManager} that is used to get, listen to and
- * manipulate the navigation state used by the Navigator.
- */
- protected NavigationStateManager stateManager;
-
- /**
- * The {@link ViewDisplay} used by the Navigator.
- */
- protected ViewDisplay display;
-
- private View currentView = null;
- private List<ViewChangeListener> listeners = new LinkedList<>();
- private List<ViewProvider> providers = new LinkedList<>();
- private String currentNavigationState = null;
- private ViewProvider errorProvider;
-
- /**
- * Creates a navigator that is tracking the active view using URI fragments
- * of the {@link Page} containing the given UI and replacing the contents of
- * a {@link ComponentContainer} with the active view.
- * <p>
- * All components of the container are removed each time before adding the
- * active {@link View}. Views must implement {@link Component} when using
- * this constructor.
- * <p>
- * Navigation is automatically initiated after {@code UI.init()} if a
- * navigator was created. If at a later point changes are made to the
- * navigator, {@code navigator.navigateTo(navigator.getState())} may need to
- * be explicitly called to ensure the current view matches the navigation
- * state.
- *
- * @param ui
- * The UI to which this Navigator is attached.
- * @param container
- * The ComponentContainer whose contents should be replaced with
- * the active view on view change
- */
- public Navigator(UI ui, ComponentContainer container) {
- this(ui, new ComponentContainerViewDisplay(container));
- }
-
- /**
- * Creates a navigator that is tracking the active view using URI fragments
- * of the {@link Page} containing the given UI and replacing the contents of
- * a {@link SingleComponentContainer} with the active view.
- * <p>
- * Views must implement {@link Component} when using this constructor.
- * <p>
- * Navigation is automatically initiated after {@code UI.init()} if a
- * navigator was created. If at a later point changes are made to the
- * navigator, {@code navigator.navigateTo(navigator.getState())} may need to
- * be explicitly called to ensure the current view matches the navigation
- * state.
- *
- * @param ui
- * The UI to which this Navigator is attached.
- * @param container
- * The SingleComponentContainer whose contents should be replaced
- * with the active view on view change
- */
- public Navigator(UI ui, SingleComponentContainer container) {
- this(ui, new SingleComponentContainerViewDisplay(container));
- }
-
- /**
- * Creates a navigator that is tracking the active view using URI fragments
- * of the {@link Page} containing the given UI.
- * <p>
- * Navigation is automatically initiated after {@code UI.init()} if a
- * navigator was created. If at a later point changes are made to the
- * navigator, {@code navigator.navigateTo(navigator.getState())} may need to
- * be explicitly called to ensure the current view matches the navigation
- * state.
- *
- * @param ui
- * The UI to which this Navigator is attached.
- * @param display
- * The ViewDisplay used to display the views.
- */
- public Navigator(UI ui, ViewDisplay display) {
- this(ui, null, display);
- }
-
- /**
- * Creates a navigator.
- * <p>
- * When a custom navigation state manager is not needed, use one of the
- * other constructors which use a URI fragment based state manager.
- * <p>
- * Navigation is automatically initiated after {@code UI.init()} if a
- * navigator was created. If at a later point changes are made to the
- * navigator, {@code navigator.navigateTo(navigator.getState())} may need to
- * be explicitly called to ensure the current view matches the navigation
- * state.
- *
- * @param ui
- * The UI to which this Navigator is attached.
- * @param stateManager
- * The NavigationStateManager keeping track of the active view
- * and enabling bookmarking and direct navigation or null to use
- * the default implementation
- * @param display
- * The ViewDisplay used to display the views handled by this
- * navigator
- */
- public Navigator(UI ui, NavigationStateManager stateManager,
- ViewDisplay display) {
- init(ui, stateManager, display);
- }
-
- /**
- * Creates a navigator. This method is for use by dependency injection
- * frameworks etc. and must be followed by a call to
- * {@link #init(UI, NavigationStateManager, ViewDisplay)} before use.
- *
- * @since 7.6
- */
- protected Navigator() {
- }
-
- /**
- * Initializes a navigator created with the no arguments constructor.
- * <p>
- * When a custom navigation state manager is not needed, use null to create
- * a default one based on URI fragments.
- * <p>
- * Navigation is automatically initiated after {@code UI.init()} if a
- * navigator was created. If at a later point changes are made to the
- * navigator, {@code navigator.navigateTo(navigator.getState())} may need to
- * be explicitly called to ensure the current view matches the navigation
- * state.
- *
- * @since 7.6
- * @param ui
- * The UI to which this Navigator is attached.
- * @param stateManager
- * The NavigationStateManager keeping track of the active view
- * and enabling bookmarking and direct navigation or null for
- * default
- * @param display
- * The ViewDisplay used to display the views handled by this
- * navigator
- */
- protected void init(UI ui, NavigationStateManager stateManager,
- ViewDisplay display) {
- this.ui = ui;
- this.ui.setNavigator(this);
- if (stateManager == null) {
- stateManager = createNavigationStateManager(ui);
- }
- if (stateManager != null && this.stateManager != null
- && stateManager != this.stateManager) {
- this.stateManager.setNavigator(null);
- }
- this.stateManager = stateManager;
- this.stateManager.setNavigator(this);
- this.display = display;
- }
-
- /**
- * Creates a navigation state manager for given UI. This method should take
- * into account any navigation related annotations.
- *
- * @param ui
- * the ui
- * @return the navigation state manager
- *
- * @since 8.2
- */
- protected NavigationStateManager createNavigationStateManager(UI ui) {
- if (ui.getClass().getAnnotation(PushStateNavigation.class) != null) {
- return new PushStateManager(ui);
- }
- // Fall back to old default
- return new UriFragmentManager(ui.getPage());
- }
-
- /**
- * Navigates to a view and initialize the view with given parameters.
- * <p>
- * The view string consists of a view name optionally followed by a slash
- * and a parameters part that is passed as-is to the view. ViewProviders are
- * used to find and create the correct type of view.
- * <p>
- * If multiple providers return a matching view, the view with the longest
- * name is selected. This way, e.g. hierarchies of subviews can be
- * registered like "admin/", "admin/users", "admin/settings" and the longest
- * match is used.
- * <p>
- * If the view being deactivated indicates it wants a confirmation for the
- * navigation operation, the user is asked for the confirmation.
- * <p>
- * Registered {@link ViewChangeListener}s are called upon successful view
- * change.
- *
- * @param navigationState
- * view name and parameters
- *
- * @throws IllegalArgumentException
- * if <code>navigationState</code> does not map to a known view
- * and no error view is registered
- */
- public void navigateTo(String navigationState) {
- ViewProvider longestViewNameProvider = getViewProvider(navigationState);
- String longestViewName = longestViewNameProvider == null ? null
- : longestViewNameProvider.getViewName(navigationState);
- View viewWithLongestName = null;
-
- if (longestViewName != null) {
- viewWithLongestName = longestViewNameProvider
- .getView(longestViewName);
- }
-
- if (viewWithLongestName == null && errorProvider != null) {
- longestViewName = errorProvider.getViewName(navigationState);
- viewWithLongestName = errorProvider.getView(longestViewName);
- }
-
- if (viewWithLongestName == null) {
- throw new IllegalArgumentException(
- "Trying to navigate to an unknown state '" + navigationState
- + "' and an error view provider not present");
- }
-
- String parameters = "";
- if (navigationState.length() > longestViewName.length() + 1) {
- parameters = navigationState
- .substring(longestViewName.length() + 1);
- } else if (navigationState.endsWith("/")) {
- navigationState = navigationState.substring(0,
- navigationState.length() - 1);
- }
- if (getCurrentView() == null
- || !SharedUtil.equals(getCurrentView(), viewWithLongestName)
- || !SharedUtil.equals(currentNavigationState,
- navigationState)) {
- navigateTo(viewWithLongestName, longestViewName, parameters);
- } else {
- updateNavigationState(new ViewChangeEvent(this, getCurrentView(),
- viewWithLongestName, longestViewName, parameters));
- }
- }
-
- /**
- * Internal method activating a view, setting its parameters and calling
- * listeners.
- * <p>
- * This method also verifies that the user is allowed to perform the
- * navigation operation.
- *
- * @param view
- * view to activate
- * @param viewName
- * (optional) name of the view or null not to change the
- * navigation state
- * @param parameters
- * parameters passed in the navigation state to the view
- */
- protected void navigateTo(View view, String viewName, String parameters) {
- runAfterLeaveConfirmation(
- () -> performNavigateTo(view, viewName, parameters));
- }
-
- /**
- * Triggers {@link View#beforeLeave(ViewBeforeLeaveEvent)} for the current
- * view with the given action.
- * <p>
- * This method is typically called by
- * {@link #navigateTo(View, String, String)} but can be called from
- * application code when you want to e.g. show a confirmation dialog before
- * perfoming an action which is not a navigation but which would cause the
- * view to be hidden, e.g. logging out.
- * <p>
- * Note that this method will not trigger any {@link ViewChangeListener}s as
- * it does not navigate to a new view. Use {@link #navigateTo(String)} to
- * change views and trigger all listeners.
- *
- * @param action
- * the action to execute when the view confirms it is ok to leave
- * @since 8.1
- */
- public void runAfterLeaveConfirmation(ViewLeaveAction action) {
- View currentView = getCurrentView();
- if (currentView == null) {
- action.run();
- } else {
- ViewBeforeLeaveEvent beforeLeaveEvent = new ViewBeforeLeaveEvent(
- this, action);
- currentView.beforeLeave(beforeLeaveEvent);
- if (!beforeLeaveEvent.isNavigateRun()) {
- // The event handler prevented navigation
- // Revert URL to previous state in case the navigation was
- // caused by the back-button
- revertNavigation();
- }
- }
- }
-
- /**
- * Internal method for activating a view, setting its parameters and calling
- * listeners.
- * <p>
- * Invoked after the current view has confirmed that leaving is ok.
- * <p>
- * This method also verifies that the user is allowed to perform the
- * navigation operation.
- *
- * @param view
- * view to activate
- * @param viewName
- * (optional) name of the view or null not to change the
- * navigation state
- * @param parameters
- * parameters passed in the navigation state to the view
- * @since 8.1
- */
- protected void performNavigateTo(View view, String viewName,
- String parameters) {
- ViewChangeEvent event = new ViewChangeEvent(this, currentView, view,
- viewName, parameters);
- boolean navigationAllowed = beforeViewChange(event);
- if (!navigationAllowed) {
- // #10901. Revert URL to previous state if back-button navigation
- // was canceled
- revertNavigation();
- return;
- }
-
- updateNavigationState(event);
-
- if (getDisplay() != null) {
- getDisplay().showView(view);
- }
-
- switchView(event);
-
- view.enter(event);
-
- fireAfterViewChange(event);
- }
-
- /**
- * Check whether view change is allowed by view change listeners (
- * {@link ViewChangeListener#beforeViewChange(ViewChangeEvent)}).
- *
- * This method can be overridden to extend the behavior, and should not be
- * called directly except by {@link #navigateTo(View, String, String)}.
- *
- * @since 7.6
- * @param event
- * the event to fire as the before view change event
- * @return true if view change is allowed
- */
- protected boolean beforeViewChange(ViewChangeEvent event) {
- return fireBeforeViewChange(event);
- }
-
- /**
- * Revert the changes to the navigation state. When navigation fails, this
- * method can be called by {@link #navigateTo(View, String, String)} to
- * revert the URL fragment to point to the previous view to which navigation
- * succeeded.
- *
- * This method should only be called by
- * {@link #navigateTo(View, String, String)}. Normally it should not be
- * overridden, but can be by frameworks that need to hook into view change
- * cancellations of this type.
- *
- * @since 7.6
- */
- protected void revertNavigation() {
- if (currentNavigationState != null) {
- getStateManager().setState(currentNavigationState);
- }
- }
-
- /**
- * Update the internal state of the navigator (parameters, previous
- * successful URL fragment navigated to) when navigation succeeds.
- *
- * Normally this method should not be overridden nor called directly from
- * application code, but it can be called by a custom implementation of
- * {@link #navigateTo(View, String, String)}.
- *
- * @since 7.6
- * @param event
- * a view change event with details of the change
- */
- protected void updateNavigationState(ViewChangeEvent event) {
- String viewName = event.getViewName();
- String parameters = event.getParameters();
- if (null != viewName && getStateManager() != null) {
- String navigationState = viewName;
- if (!parameters.isEmpty()) {
- navigationState += "/" + parameters;
- }
- if (!navigationState.equals(getStateManager().getState())) {
- getStateManager().setState(navigationState);
- }
- currentNavigationState = navigationState;
- }
- }
-
- /**
- * Update the internal state of the navigator to reflect the actual
- * switching of views.
- *
- * This method should only be called by
- * {@link #navigateTo(View, String, String)} between showing the view and
- * calling {@link View#enter(ViewChangeEvent)}. If this method is
- * overridden, the overriding version must call the super method.
- *
- * @since 7.6
- * @param event
- * a view change event with details of the change
- */
- protected void switchView(ViewChangeEvent event) {
- currentView = event.getNewView();
- }
-
- /**
- * Fires an event before an imminent view change.
- * <p>
- * Listeners are called in registration order. If any listener returns
- * <code>false</code>, the rest of the listeners are not called and the view
- * change is blocked.
- * <p>
- * The view change listeners may also e.g. open a warning or question dialog
- * and save the parameters to re-initiate the navigation operation upon user
- * action.
- *
- * @param event
- * view change event (not null, view change not yet performed)
- * @return true if the view change should be allowed, false to silently
- * block the navigation operation
- */
- protected boolean fireBeforeViewChange(ViewChangeEvent event) {
- // a copy of the listener list is needed to avoid
- // ConcurrentModificationException as a listener can add/remove
- // listeners
- for (ViewChangeListener l : new ArrayList<>(listeners)) {
- if (!l.beforeViewChange(event)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Returns the {@link NavigationStateManager} that is used to get, listen to
- * and manipulate the navigation state used by this Navigator.
- *
- * @return NavigationStateManager in use
- */
- protected NavigationStateManager getStateManager() {
- return stateManager;
- }
-
- /**
- * Returns the current navigation state reported by this Navigator's
- * {@link NavigationStateManager}.
- * <p>
- * When the navigation is triggered by the browser (for example by pressing
- * the back or forward button in the browser), the navigation state may
- * already have been updated to reflect the new address, before the
- * {@link #navigateTo(String)} is notified.
- *
- * @return The navigation state.
- */
- public String getState() {
- return getStateManager().getState();
- }
-
- /**
- * Returns the current navigation state reported by this Navigator's
- * {@link NavigationStateManager} as Map<String, String> where each key
- * represents a parameter in the state.
- *
- * Uses {@literal &} as parameter separator. If the state contains
- * {@literal #!view/foo&bar=baz} then this method will return a map
- * containing {@literal foo => ""} and {@literal bar => baz}.
- *
- * @return The parameters from the navigation state as a map
- * @see #getStateParameterMap(String)
- * @since 8.1
- */
- public Map<String, String> getStateParameterMap() {
- return getStateParameterMap(DEFAULT_STATE_PARAMETER_SEPARATOR);
- }
-
- /**
- * Returns the current navigation state reported by this Navigator's
- * {@link NavigationStateManager} as Map<String, String> where each key
- * represents a parameter in the state. The state parameter separator
- * character needs to be specified with the separator.
- *
- * @param separator
- * the string (typically one character) used to separate values
- * from each other
- * @return The parameters from the navigation state as a map
- * @see #getStateParameterMap()
- * @since 8.1
- */
- public Map<String, String> getStateParameterMap(String separator) {
- return parseStateParameterMap(Objects.requireNonNull(separator));
- }
-
- /**
- * Parses the state parameter to a map using the given separator string.
- *
- * @param separator
- * the string (typically one character) used to separate values
- * from each other
- * @return The navigation state as Map<String, String>.
- * @since 8.1
- */
- protected Map<String, String> parseStateParameterMap(String separator) {
- if (getState() == null || getState().isEmpty()) {
- return Collections.emptyMap();
- }
-
- String state = getState();
- int viewSeparatorLocation = state.indexOf(DEFAULT_VIEW_SEPARATOR);
-
- String parameterString;
- if (viewSeparatorLocation == -1) {
- parameterString = "";
- } else {
- parameterString = state.substring(viewSeparatorLocation + 1,
- state.length());
- }
- return parseParameterStringToMap(parameterString, separator);
- }
-
- /**
- * Parses the given parameter string to a map using the given separator
- * string.
- *
- * @param parameterString
- * the parameter string to parse
- * @param separator
- * the string (typically one character) used to separate values
- * from each other
- * @return The navigation state as Map<String, String>.
- * @since 8.1
- */
- protected Map<String, String> parseParameterStringToMap(
- String parameterString, String separator) {
- if (parameterString.isEmpty()) {
- return Collections.emptyMap();
- }
-
- Map<String, String> parameterMap = new HashMap<>();
- String[] parameters = parameterString.split(separator);
- for (String parameter : parameters) {
- String[] keyAndValue = parameter
- .split(DEFAULT_STATE_PARAMETER_KEY_VALUE_SEPARATOR);
- parameterMap.put(keyAndValue[0],
- keyAndValue.length > 1 ? keyAndValue[1] : "");
- }
-
- return parameterMap;
- }
-
- /**
- * Return the {@link ViewDisplay} used by the navigator.
- *
- * @return the ViewDisplay used for displaying views
- */
- public ViewDisplay getDisplay() {
- return display;
- }
-
- public UI getUI() {
- return ui;
- }
-
- /**
- * Return the currently active view.
- *
- * @since 7.6
- * @return current view
- */
- public View getCurrentView() {
- return currentView;
- }
-
- /**
- * Fires an event after the current view has changed.
- * <p>
- * Listeners are called in registration order.
- *
- * @param event
- * view change event (not null)
- */
- protected void fireAfterViewChange(ViewChangeEvent event) {
- // a copy of the listener list is needed to avoid
- // ConcurrentModificationException as a listener can add/remove
- // listeners
- for (ViewChangeListener l : new ArrayList<>(listeners)) {
- l.afterViewChange(event);
- }
- }
-
- /**
- * Registers a static, pre-initialized view instance for a view name.
- * <p>
- * Registering another view with a name that is already registered
- * overwrites the old registration of the same type.
- * <p>
- * Note that a view should not be shared between UIs (for instance, it
- * should not be a static field in a UI subclass).
- *
- * @param viewName
- * String that identifies a view (not null nor empty string)
- * @param view
- * {@link View} instance (not null)
- */
- public void addView(String viewName, View view) {
-
- // Check parameters
- if (viewName == null || view == null) {
- throw new IllegalArgumentException(
- "view and viewName must be non-null");
- }
-
- removeView(viewName);
- addProvider(new StaticViewProvider(viewName, view));
- }
-
- /**
- * Registers a view class for a view name.
- * <p>
- * Registering another view with a name that is already registered
- * overwrites the old registration of the same type.
- * <p>
- * A new view instance is created every time a view is requested.
- *
- * @param viewName
- * String that identifies a view (not null nor empty string)
- * @param viewClass
- * {@link View} class to instantiate when a view is requested
- * (not null)
- */
- public void addView(String viewName, Class<? extends View> viewClass) {
-
- // Check parameters
- if (viewName == null || viewClass == null) {
- throw new IllegalArgumentException(
- "view and viewClass must be non-null");
- }
-
- removeView(viewName);
- addProvider(new ClassBasedViewProvider(viewName, viewClass));
- }
-
- /**
- * Removes a view from navigator.
- * <p>
- * This method only applies to views registered using
- * {@link #addView(String, View)} or {@link #addView(String, Class)}.
- *
- * @param viewName
- * name of the view to remove
- */
- public void removeView(String viewName) {
- Iterator<ViewProvider> it = providers.iterator();
- while (it.hasNext()) {
- ViewProvider provider = it.next();
- if (provider instanceof StaticViewProvider) {
- StaticViewProvider staticProvider = (StaticViewProvider) provider;
- if (staticProvider.getViewName().equals(viewName)) {
- it.remove();
- }
- } else if (provider instanceof ClassBasedViewProvider) {
- ClassBasedViewProvider classBasedProvider = (ClassBasedViewProvider) provider;
- if (classBasedProvider.getViewName().equals(viewName)) {
- it.remove();
- }
- }
- }
- }
-
- /**
- * Registers a view provider (factory).
- * <p>
- * Providers are called in order of registration until one that can handle
- * the requested view name is found.
- *
- * @param provider
- * provider to register, not <code>null</code>
- * @throws IllegalArgumentException
- * if the provided view provider is <code>null</code>
- */
- public void addProvider(ViewProvider provider) {
- if (provider == null) {
- throw new IllegalArgumentException(
- "Cannot add a null view provider");
- }
- providers.add(provider);
- }
-
- /**
- * Unregister a view provider (factory).
- *
- * @param provider
- * provider to unregister
- */
- public void removeProvider(ViewProvider provider) {
- providers.remove(provider);
- }
-
- /**
- * Registers a view class that is instantiated when no other view matches
- * the navigation state. This implicitly sets an appropriate error view
- * provider and overrides any previous
- * {@link #setErrorProvider(ViewProvider)} call.
- * <p>
- * Note that an error view should not be shared between UIs (for instance,
- * it should not be a static field in a UI subclass).
- *
- * @param viewClass
- * The View class whose instance should be used as the error
- * view.
- */
- public void setErrorView(final Class<? extends View> viewClass) {
- setErrorProvider(new ViewProvider() {
- @Override
- public View getView(String viewName) {
- return ReflectTools.createInstance(viewClass);
- }
-
- @Override
- public String getViewName(String navigationState) {
- return navigationState;
- }
- });
- }
-
- /**
- * Registers a view that is displayed when no other view matches the
- * navigation state. This implicitly sets an appropriate error view provider
- * and overrides any previous {@link #setErrorProvider(ViewProvider)} call.
- *
- * @param view
- * The View that should be used as the error view.
- */
- public void setErrorView(final View view) {
- setErrorProvider(new ViewProvider() {
- @Override
- public View getView(String viewName) {
- return view;
- }
-
- @Override
- public String getViewName(String navigationState) {
- return navigationState;
- }
- });
- }
-
- /**
- * Registers a view provider that is queried for a view when no other view
- * matches the navigation state. An error view provider should match any
- * navigation state, but could return different views for different states.
- * Its <code>getViewName(String navigationState)</code> should return
- * <code>navigationState</code>.
- *
- * @param provider
- */
- public void setErrorProvider(ViewProvider provider) {
- errorProvider = provider;
- }
-
- /**
- * Listen to changes of the active view.
- * <p>
- * Registered listeners are invoked in registration order before (
- * {@link ViewChangeListener#beforeViewChange(ViewChangeEvent)
- * beforeViewChange()}) and after (
- * {@link ViewChangeListener#afterViewChange(ViewChangeEvent)
- * afterViewChange()}) a view change occurs.
- *
- * @param listener
- * Listener to invoke during a view change.
- * @since 8.0
- */
- public Registration addViewChangeListener(ViewChangeListener listener) {
- listeners.add(listener);
- return () -> listeners.remove(listener);
- }
-
- /**
- * Removes a view change listener.
- *
- * @param listener
- * Listener to remove.
- * @deprecated use a {@link Registration} object returned by
- * {@link #addViewChangeListener(ViewChangeListener)} to remove
- * a listener
- */
- @Deprecated
- public void removeViewChangeListener(ViewChangeListener listener) {
- listeners.remove(listener);
- }
-
- /**
- * Get view provider that handles the given {@code state}.
- *
- * @param state
- * state string
- * @return suitable provider
- */
- protected ViewProvider getViewProvider(String state) {
- String longestViewName = null;
- ViewProvider longestViewNameProvider = null;
- for (ViewProvider provider : providers) {
- String viewName = provider.getViewName(state);
- if (null != viewName && (longestViewName == null
- || viewName.length() > longestViewName.length())) {
- longestViewName = viewName;
- longestViewNameProvider = provider;
- }
- }
- return longestViewNameProvider;
- }
-
- /**
- * Creates view change event for given {@code view}, {@code viewName} and
- * {@code parameters}.
- *
- * @since 7.6.7
- */
- public void destroy() {
- stateManager.setNavigator(null);
- ui.setNavigator(null);
- }
-
- /**
- * Returns the current navigation state for which the
- * {@link #getCurrentView()} has been constructed. This may differ to
- * {@link #getState()} in case the URL has been changed on the browser and
- * the navigator wasn't yet given an opportunity to construct new view. The
- * state is in the form of
- * <code>current-view-name/optional/parameters</code>
- *
- * @return the current navigation state, may be {@code null}.
- */
- public String getCurrentNavigationState() {
- return currentNavigationState;
- }
- }
|