package com.vaadin.navigator; /* * Copyright 2011 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. */ import java.io.Serializable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; import com.vaadin.server.Page; import com.vaadin.server.Page.FragmentChangedEvent; import com.vaadin.server.Page.FragmentChangedListener; import com.vaadin.ui.Component; import com.vaadin.ui.ComponentContainer; import com.vaadin.ui.CssLayout; import com.vaadin.ui.CustomComponent; /** * 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 comes with * {@link SimpleViewDisplay} which is a component that displays the selected * view as its contents. * * @author Vaadin Ltd * @since 7.0 */ public class Navigator implements Serializable { // TODO divert navigation e.g. if no permissions? Or just show another view // but keep URL? how best to intercept // TODO investigate relationship with TouchKit navigation support /** * 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 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.
*/
public static class UriFragmentManager implements NavigationStateManager,
FragmentChangedListener {
private final Page page;
private final Navigator navigator;
/**
* Create 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
* @param navigator
* {@link Navigator} to notify of fragment changes (using
* {@link Navigator#navigateTo(String)}
*/
public UriFragmentManager(Page page, Navigator navigator) {
this.page = page;
this.navigator = navigator;
page.addFragmentChangedListener(this);
}
@Override
public String getState() {
String fragment = page.getFragment();
if (fragment.startsWith("!")) {
return page.getFragment().substring(1);
} else {
return "";
}
}
@Override
public void setState(String state) {
page.setFragment("!" + state, false);
}
@Override
public void fragmentChanged(FragmentChangedEvent event) {
UriFragmentManager.this.navigator.navigateTo(getState());
}
}
/**
* View display that is a component itself and replaces its contents with
* the 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.
*
* By default, the view display has full size.
*/
public static class SimpleViewDisplay extends CustomComponent implements
ViewDisplay {
/**
* Create new {@link ViewDisplay} that is itself a component displaying
* the view.
*/
public SimpleViewDisplay() {
setSizeFull();
}
@Override
public void showView(View view) {
if (view instanceof Component) {
setCompositionRoot((Component) view);
} else {
throw new IllegalArgumentException("View is not a component: "
+ view);
}
}
}
/**
* View display 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) {
if (view instanceof Component) {
container.removeAllComponents();
container.addComponent((Component) view);
} else {
throw new IllegalArgumentException("View is not a component: "
+ view);
}
}
}
/**
* View provider 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;
/**
* Create 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;
}
}
/**
* View provider 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)) {
try {
View view = viewClass.newInstance();
return view;
} catch (InstantiationException e) {
// TODO error handling
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
// TODO error handling
throw new RuntimeException(e);
}
}
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;
}
}
private final NavigationStateManager stateManager;
private final ViewDisplay display;
private View currentView = null;
private List
* After all {@link View}s and {@link ViewProvider}s have been registered,
* the application should trigger navigation to the current fragment using
* e.g.
*
*
* After all {@link View}s and {@link ViewProvider}s have been registered,
* the application should trigger navigation to the current fragment using
* e.g.
*
*
* Listeners are called in registration order. If any listener returns
*
* 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) {
for (ViewChangeListener l : 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;
}
/**
* Return the ViewDisplay used by the navigator. Unless another display is
* specified, a {@link SimpleViewDisplay} (which is a {@link Component}) is
* used by default.
*
* @return current ViewDisplay
*/
public ViewDisplay getDisplay() {
return display;
}
/**
* 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) {
for (ViewChangeListener l : listeners) {
l.afterViewChange(event);
}
}
/**
* Register 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.
*
* @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));
}
/**
* Register for a view name a view class.
*
* 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));
}
/**
* Remove 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
* navigator.navigateTo(Page.getCurrent().getFragment());
*
*
* @param container
* ComponentContainer whose contents should be replaced with the
* active view on view change
*/
public Navigator(ComponentContainer container) {
display = new ComponentContainerViewDisplay(container);
stateManager = new UriFragmentManager(Page.getCurrent(), this);
}
/**
* Create a navigator that is tracking the active view using URI fragments.
*
*
* navigator.navigateTo(Page.getCurrent().getFragment());
*
*
* @param page
* whose URI fragments are used
* @param display
* where to display the views
*/
public Navigator(Page page, ViewDisplay display) {
this.display = display;
stateManager = new UriFragmentManager(page, this);
}
/**
* Create a navigator.
*
* When a custom navigation state manager is not needed, use the constructor
* {@link #Navigator(Page, ViewDisplay)} which uses a URI fragment based
* state manager.
*
* Note that navigation to the initial view must be performed explicitly by
* the application after creating a Navigator using this constructor.
*
* @param stateManager
* {@link NavigationStateManager} keeping track of the active
* view and enabling bookmarking and direct navigation
* @param display
* {@ViewDisplay} used to display the views handled
* by this navigator
*/
public Navigator(NavigationStateManager stateManager, ViewDisplay display) {
this.display = display;
this.stateManager = stateManager;
}
/**
* Navigates to the current navigation state. This method should be called
* when all required {@link View}s, {@link ViewProvider}s, and
* {@link ViewChangeListener}s have been added to the navigator.
*/
public void navigate() {
navigateTo(getStateManager().getState());
}
/**
* Navigate 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
*/
public void navigateTo(String navigationState) {
String longestViewName = null;
View viewWithLongestName = null;
for (ViewProvider provider : providers) {
String viewName = provider.getViewName(navigationState);
if (null != viewName
&& (longestViewName == null || viewName.length() > longestViewName
.length())) {
View view = provider.getView(viewName);
if (null != view) {
longestViewName = viewName;
viewWithLongestName = view;
}
}
}
if (viewWithLongestName != null) {
String parameters = "";
if (navigationState.length() > longestViewName.length() + 1) {
parameters = navigationState
.substring(longestViewName.length() + 1);
}
navigateTo(viewWithLongestName, longestViewName, parameters);
}
// TODO if no view is found, what to do?
}
/**
* 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) {
ViewChangeEvent event = new ViewChangeEvent(this, currentView, view,
viewName, parameters);
if (!fireBeforeViewChange(event)) {
return;
}
if (null != viewName && getStateManager() != null) {
String navigationState = viewName;
if (!parameters.isEmpty()) {
navigationState += "/" + parameters;
}
if (!navigationState.equals(getStateManager().getState())) {
getStateManager().setState(navigationState);
}
}
view.enter(event);
currentView = view;
if (display != null) {
display.showView(view);
}
fireAfterViewChange(event);
}
/**
* Fires an event before an imminent view change.
* false
, the rest of the listeners are not called and the view
* change is blocked.
*