com.vaadin.navigator.Navigator Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* 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.
*
* 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).
*
* Views can be explicitly registered or dynamically generated and listening to
* view changes is possible.
*
* 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}.
*
* 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.
*
* 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.
*
* 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.
*
* This class is mostly for internal use by Navigator, and is only public
* and static to enable testing.
*
* Note: 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}.
*
* All components of the container are removed before adding the new view to
* it.
*
* 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}.
*
* 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.
*
* Note that the view class must be accessible by the class loader used by
* the provider. This may require its visibility to be public.
*
* 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 listeners = new LinkedList<>();
private List 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.
*
* All components of the container are removed each time before adding the
* active {@link View}. Views must implement {@link Component} when using
* this constructor.
*
* 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.
*
* Views must implement {@link Component} when using this constructor.
*
* 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.
*
* 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.
*
* When a custom navigation state manager is not needed, use one of the
* other constructors which use a URI fragment based state manager.
*
* 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.
*
* When a custom navigation state manager is not needed, use null to create
* a default one based on URI fragments.
*
* 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.
*
* 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.
*
* 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.
*
* If the view being deactivated indicates it wants a confirmation for the
* navigation operation, the user is asked for the confirmation.
*
* Registered {@link ViewChangeListener}s are called upon successful view
* change.
*
* @param navigationState
* view name and parameters
*
* @throws IllegalArgumentException
* if navigationState
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.
*
* 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.
*
* 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.
*
* 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.
*
* Invoked after the current view has confirmed that leaving is ok.
*
* 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.
*
* Listeners are called in registration order. If any listener returns
* false
, the rest of the listeners are not called and the view
* change is blocked.
*
* 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}.
*
* 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 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 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 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 parseParameterStringToMap(
String parameterString, String separator) {
if (parameterString.isEmpty()) {
return Collections.emptyMap();
}
Map 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.
*
* 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.
*
* Registering another view with a name that is already registered
* overwrites the old registration of the same type.
*
* 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.
*
* Registering another view with a name that is already registered
* overwrites the old registration of the same type.
*
* 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.
*
* 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 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).
*
* Providers are called in order of registration until one that can handle
* the requested view name is found.
*
* @param provider
* provider to register, not null
* @throws IllegalArgumentException
* if the provided view provider is null
*/
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.
*
* 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 getViewName(String navigationState)
should return
* navigationState
.
*
* @param provider
*/
public void setErrorProvider(ViewProvider provider) {
errorProvider = provider;
}
/**
* Listen to changes of the active view.
*
* 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
* current-view-name/optional/parameters
*
* @return the current navigation state, may be {@code null}.
*/
public String getCurrentNavigationState() {
return currentNavigationState;
}
}