]> source.dussan.org Git - vaadin-framework.git/commitdiff
Implement first version of view and navigation APIs (#8859).
authorHenri Sara <hesara@vaadin.com>
Thu, 24 May 2012 10:07:19 +0000 (13:07 +0300)
committerHenri Sara <hesara@vaadin.com>
Thu, 24 May 2012 10:07:33 +0000 (13:07 +0300)
src/com/vaadin/navigator/FragmentManager.java [new file with mode: 0644]
src/com/vaadin/navigator/Navigator.java [new file with mode: 0644]
src/com/vaadin/navigator/View.java [new file with mode: 0644]
src/com/vaadin/navigator/ViewChangeListener.java [new file with mode: 0644]
src/com/vaadin/navigator/ViewDisplay.java [new file with mode: 0644]
src/com/vaadin/navigator/ViewProvider.java [new file with mode: 0644]
tests/server-side/com/vaadin/tests/server/navigator/ClassBasedViewProviderTest.java [new file with mode: 0644]
tests/server-side/com/vaadin/tests/server/navigator/NavigatorTest.java [new file with mode: 0644]
tests/server-side/com/vaadin/tests/server/navigator/UriFragmentManagerTest.java [new file with mode: 0644]

diff --git a/src/com/vaadin/navigator/FragmentManager.java b/src/com/vaadin/navigator/FragmentManager.java
new file mode 100644 (file)
index 0000000..f1fd90e
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+/**
+ * Fragment manager that handles interaction between Navigator and URI fragments
+ * or other similar view identification and bookmarking system.
+ * 
+ * Alternative implementations can be created for HTML5 pushState, for portlet
+ * URL navigation and other similar systems.
+ * 
+ * This interface is mostly for internal use by {@link Navigator}.
+ * 
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface FragmentManager extends Serializable {
+    /**
+     * Return the current fragment (location string) including view name and any
+     * optional parameters.
+     * 
+     * @return current view and parameter string, not null
+     */
+    public String getFragment();
+
+    /**
+     * Set the current fragment (location string) in the application URL or
+     * similar location, including view name and any optional parameters.
+     * 
+     * @param fragment
+     *            new view and parameter string, not null
+     */
+    public void setFragment(String fragment);
+}
\ No newline at end of file
diff --git a/src/com/vaadin/navigator/Navigator.java b/src/com/vaadin/navigator/Navigator.java
new file mode 100644 (file)
index 0000000..d0bba58
--- /dev/null
@@ -0,0 +1,582 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.vaadin.ui.Component;
+import com.vaadin.ui.CssLayout;
+import com.vaadin.ui.CustomComponent;
+import com.vaadin.ui.Root;
+import com.vaadin.ui.Root.FragmentChangedEvent;
+import com.vaadin.ui.Root.FragmentChangedListener;
+
+/**
+ * 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) and optional internal parameters that are not.
+ * 
+ * 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 to TouchKit navigation support
+
+    /**
+     * Empty view component.
+     */
+    public static class EmptyView extends CssLayout implements View {
+        public void init() {
+            setWidth("0px");
+            setHeight("0px");
+        }
+
+        public void navigateTo(String fragmentParameters,
+                Object... internalParameters) {
+            // nothing to do
+        }
+    }
+
+    /**
+     * Fragment manager using URI fragments of a Root to track views and enable
+     * listening to view changes.
+     * 
+     * This class is mostly for internal use by Navigator, and is only public
+     * and static to enable testing.
+     */
+    public static class UriFragmentManager implements FragmentManager,
+            FragmentChangedListener {
+        private final Root root;
+        private final Navigator navigator;
+
+        /**
+         * Create a new URIFragmentManager and attach it to listen to URI
+         * fragment changes of a {@link Root}.
+         * 
+         * @param root
+         *            root whose URI fragment to get and modify
+         * @param navigator
+         *            {@link Navigator} to notify of fragment changes (using
+         *            {@link Navigator#navigateTo(String, Object...)}
+         */
+        public UriFragmentManager(Root root, Navigator navigator) {
+            this.root = root;
+            this.navigator = navigator;
+
+            root.addListener(this);
+        }
+
+        public String getFragment() {
+            return root.getFragment();
+        }
+
+        public void setFragment(String fragment) {
+            // TODO ", false" ???
+            root.setFragment(fragment);
+        }
+
+        public void fragmentChanged(FragmentChangedEvent event) {
+            UriFragmentManager.this.navigator.navigateTo(getFragment());
+        }
+    }
+
+    /**
+     * 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();
+        }
+
+        public void showView(View view) {
+            if (view instanceof Component) {
+                setCompositionRoot((Component) view);
+            } else {
+                throw new IllegalArgumentException("View is not a component: "
+                        + view);
+            }
+        }
+    }
+
+    /**
+     * View provider which uses a map from view name to pre-created and
+     * registered view instances.
+     */
+    public static class RegisteredViewProvider implements ViewProvider {
+
+        private HashMap<String, View> viewNameToView = new HashMap<String, View>();
+
+        public String getViewName(String viewAndParameters) {
+            if (null == viewAndParameters) {
+                return null;
+            }
+            for (String viewName : viewNameToView.keySet()) {
+                if (viewAndParameters.equals(viewName)
+                        || viewAndParameters.startsWith(viewName + "/")) {
+                    return viewName;
+                }
+            }
+            return null;
+        }
+
+        public View getView(String viewName) {
+            return viewNameToView.get(viewName);
+        }
+
+        /**
+         * Register a view for a view name.
+         * 
+         * Registering another view with a name that is already registered
+         * overwrites the old registration.
+         * 
+         * @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 || viewName.length() == 0) {
+                throw new IllegalArgumentException(
+                        "view and viewName must be non-null and not empty");
+            }
+
+            viewNameToView.put(viewName, view);
+        }
+
+        /**
+         * Remove view from navigator.
+         * 
+         * @param viewName
+         *            name of the view to remove
+         */
+        public void removeView(String viewName) {
+            viewNameToView.remove(viewName);
+        }
+    }
+
+    /**
+     * View provider which uses a map from view name to the class to instantiate
+     * for the view.
+     * 
+     * Views that have been created are cached and reused when the same view
+     * name is requested again.
+     * 
+     * Note that the view class must be accessible by the class loader used by
+     * the provider. This may require its visibility to be public.
+     */
+    public static class ClassBasedViewProvider implements ViewProvider {
+
+        private HashMap<String, Class<? extends View>> viewNameToClass = new HashMap<String, Class<? extends View>>();
+        private HashMap<Class<? extends View>, String> classToViewName = new HashMap<Class<? extends View>, String>();
+        /**
+         * Already opened (cached) views that can be reopened or reused with new
+         * parameters.
+         */
+        private HashMap<Class<? extends View>, View> classToView = new HashMap<Class<? extends View>, View>();
+
+        public String getViewName(String viewAndParameters) {
+            if (null == viewAndParameters) {
+                return null;
+            }
+            for (String viewName : viewNameToClass.keySet()) {
+                if (viewAndParameters.equals(viewName)
+                        || viewAndParameters.startsWith(viewName + "/")) {
+                    return viewName;
+                }
+            }
+            return null;
+        }
+
+        public View getView(String viewName) {
+            Class<? extends View> newViewClass = viewNameToClass.get(viewName);
+            if (null == newViewClass) {
+                return null;
+            }
+            if (!classToView.containsKey(newViewClass)) {
+                try {
+                    View view = newViewClass.newInstance();
+                    view.init();
+                    classToView.put(newViewClass, view);
+                } catch (InstantiationException e) {
+                    // TODO error handling
+                    throw new RuntimeException(e);
+                } catch (IllegalAccessException e) {
+                    // TODO error handling
+                    throw new RuntimeException(e);
+                }
+            }
+            // return already cached view
+            final View v = classToView.get(newViewClass);
+            return v;
+        }
+
+        /**
+         * Register a view class for a view name.
+         * 
+         * @param viewName
+         *            String that identifies a view (not null nor empty string)
+         * @param viewClass
+         *            Component class that implements Navigator.View interface
+         *            (not null)
+         */
+        public void addView(String viewName, Class<? extends View> viewClass) {
+
+            // Check parameters
+            if (viewName == null || viewClass == null || viewName.length() == 0) {
+                throw new IllegalArgumentException(
+                        "viewClass and viewName must be non-null and not empty");
+            }
+
+            if (!View.class.isAssignableFrom(viewClass)) {
+                throw new IllegalArgumentException(
+                        "viewClass must implement Navigator.View");
+            }
+
+            if (viewNameToClass.containsKey(viewName)) {
+                if (viewNameToClass.get(viewName) == viewClass) {
+                    return;
+                }
+
+                throw new IllegalArgumentException(viewNameToClass
+                        .get(viewName).getName()
+                        + " is already mapped to '"
+                        + viewName + "'");
+            }
+
+            if (classToViewName.containsKey(viewClass)) {
+                throw new IllegalArgumentException(
+                        "Each view class can only be added to Navigator with one view name");
+            }
+
+            viewNameToClass.put(viewName, viewClass);
+            classToViewName.put(viewClass, viewName);
+        }
+
+        /**
+         * Remove view from navigator.
+         * 
+         * @param viewName
+         *            name of the view to remove
+         */
+        public void removeView(String viewName) {
+            Class<? extends View> c = viewNameToClass.get(viewName);
+            if (c != null) {
+                viewNameToClass.remove(viewName);
+                classToViewName.remove(c);
+                classToView.remove(c);
+            }
+        }
+
+        /**
+         * Get the view name for given view implementation class.
+         * 
+         * @param viewClass
+         *            Class that implements the view.
+         * @return view name for which the view class is registered, null if
+         *         none
+         */
+        public String getViewName(Class<? extends View> viewClass) {
+            return classToViewName.get(viewClass);
+        }
+
+        /**
+         * Get the view class for given view name.
+         * 
+         * @param viewName
+         *            view name to get view for
+         * @return View that corresponds to the name
+         */
+        public Class<? extends View> getViewClass(String viewName) {
+            return viewNameToClass.get(viewName);
+        }
+    }
+
+    private final FragmentManager fragmentManager;
+    private final ViewDisplay display;
+    private String mainViewName = null;
+    private View currentView = null;
+    private List<ViewChangeListener> listeners = new LinkedList<ViewChangeListener>();
+    private List<ViewProvider> providers = new LinkedList<ViewProvider>();
+
+    /**
+     * Create a navigator that is tracking the active view using URI fragments.
+     * 
+     * @param root
+     *            whose URI fragments are used
+     * @param display
+     *            where to display the views
+     */
+    public Navigator(Root root, ViewDisplay display) {
+        this.display = display;
+        fragmentManager = new UriFragmentManager(root, this);
+    }
+
+    /**
+     * Create a navigator.
+     * 
+     * When a custom fragment manager is not needed, use the constructor
+     * {@link #Navigator(Root, ViewDisplay)} which uses a URI fragment based
+     * fragment manager.
+     * 
+     * @param fragmentManager
+     *            fragment manager keeping track of the active view and enabling
+     *            bookmarking and direct navigation
+     * @param display
+     *            where to display the views
+     */
+    public Navigator(FragmentManager fragmentManager, ViewDisplay display) {
+        this.display = display;
+        this.fragmentManager = fragmentManager;
+    }
+
+    /**
+     * 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 (fragment) parameters. ViewProviders are used to find and create the
+     * correct type of view.
+     * 
+     * If the view being left indicates it wants a confirmation for the
+     * navigation operation, the user is asked for the confirmation.
+     * 
+     * @param viewAndParameters
+     *            view name and parameters
+     * @param internalParameters
+     *            parameters that are only passed when switching views
+     *            explicitly on the server side, not bookmarkable
+     */
+    public void navigateTo(String viewAndParameters,
+            Object... internalParameters) {
+        if ("".equals(viewAndParameters)) {
+            viewAndParameters = mainViewName;
+        }
+        for (ViewProvider provider : providers) {
+            String viewName = provider.getViewName(viewAndParameters);
+            if (null != viewName) {
+                String parameters = null;
+                if (viewAndParameters.length() > viewName.length() + 1) {
+                    parameters = viewAndParameters
+                            .substring(viewName.length() + 1);
+                }
+                View view = provider.getView(viewName);
+                if (null != view) {
+                    navigateTo(view, viewName, parameters, internalParameters);
+                    // stop after a view is found
+                    return;
+                }
+            }
+        }
+        // TODO if no view is found, use main view or do nothing?
+    }
+
+    /**
+     * 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 set the fragment
+     * @param fragmentParameters
+     *            parameters passed in the fragment for the view
+     * @param internalParameters
+     *            parameters that are only passed when switching views
+     *            explicitly on the server side, not bookmarkable
+     */
+    protected void navigateTo(View view, String viewName,
+            String fragmentParameters, Object... internalParameters) {
+        if (!isViewChangeAllowed(currentView, view, viewName,
+                fragmentParameters, internalParameters)) {
+            return;
+        }
+
+        if (null != viewName && getFragmentManager() != null) {
+            String currentFragment = viewName;
+            if (fragmentParameters != null) {
+                currentFragment += "/" + fragmentParameters;
+            }
+            if (!currentFragment.equals(getFragmentManager().getFragment())) {
+                getFragmentManager().setFragment(currentFragment);
+            }
+        }
+
+        view.navigateTo(fragmentParameters, internalParameters);
+        View previousView = currentView;
+        currentView = view;
+
+        if (display != null) {
+            display.showView(view);
+        }
+
+        fireViewChange(previousView);
+    }
+
+    /**
+     * Check whether view change is allowed.
+     * 
+     * All related listeners are called. The view change is blocked if any of
+     * them wants to block the navigation operation.
+     * 
+     * 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 previous
+     *            the view being deactivated
+     * @param next
+     *            the view being activated
+     * @param viewName
+     *            name of the view being activated
+     * @param fragmentParameters
+     *            parameters passed in the fragment for the view
+     * @param internalParameters
+     *            parameters that are only passed on the server side, not
+     *            bookmarkable
+     * @return true if the view change should be allowed, false to silently
+     *         block the navigation operation
+     */
+    protected boolean isViewChangeAllowed(View previous, View next,
+            String viewName, String fragmentParameters,
+            Object[] internalParameters) {
+        for (ViewChangeListener l : listeners) {
+            if (!l.isViewChangeAllowed(previous, next, viewName,
+                    fragmentParameters, internalParameters)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Return the fragment manager that is used to get, listen to and manipulate
+     * the URI fragment or other source of navigation information.
+     * 
+     * @return fragment manager in use
+     */
+    protected FragmentManager getFragmentManager() {
+        return fragmentManager;
+    }
+
+    /**
+     * Get the main view.
+     * 
+     * Main view is the default view shown to user when he opens application
+     * without specifying view name.
+     * 
+     * @return name of the main view.
+     */
+    public String getMainView() {
+        return mainViewName;
+    }
+
+    /**
+     * Set the main view.
+     * 
+     * Main view is the default view shown to user when he opens application
+     * without specifying view name. If the {@link Navigator} does not have a
+     * current view, the main view is automatically selected when set.
+     * 
+     * @param mainViewName
+     *            name of the main view.
+     */
+    public void setMainView(String mainViewName) {
+        this.mainViewName = mainViewName;
+        // TODO should this be done?
+        if (currentView == null) {
+            navigateTo(mainViewName);
+        }
+    }
+
+    /**
+     * Fire an event when the current view has changed.
+     * 
+     * @param previousView
+     */
+    protected void fireViewChange(View previousView) {
+        for (ViewChangeListener l : listeners) {
+            l.navigatorViewChanged(previousView, currentView);
+        }
+    }
+
+    /**
+     * Register 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
+     */
+    public void registerProvider(ViewProvider provider) {
+        providers.add(provider);
+    }
+
+    /**
+     * Unregister a view provider (factory).
+     * 
+     * @param provider
+     *            provider to unregister
+     */
+    public void unregisterProvider(ViewProvider provider) {
+        providers.remove(provider);
+    }
+
+    /**
+     * Listen to changes of the active view.
+     * 
+     * The listener will get notified after the view has changed.
+     * 
+     * @param listener
+     *            Listener to invoke after view changes.
+     */
+    public void addListener(ViewChangeListener listener) {
+        listeners.add(listener);
+    }
+
+    /**
+     * Remove a view change listener.
+     * 
+     * @param listener
+     *            Listener to remove.
+     */
+    public void removeListener(ViewChangeListener listener) {
+        listeners.remove(listener);
+    }
+
+}
diff --git a/src/com/vaadin/navigator/View.java b/src/com/vaadin/navigator/View.java
new file mode 100644 (file)
index 0000000..efa13fb
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+import com.vaadin.ui.Component;
+
+/**
+ * Interface for all views controlled by the navigator.
+ * 
+ * Each view added to the navigator must implement this interface. Typically, a
+ * view is a {@link Component}.
+ * 
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface View extends Serializable {
+
+    /**
+     * Init view.
+     * 
+     * Convenience method which navigator calls just before the view is rendered
+     * for the first time. This is called only once in the lifetime of each view
+     * instance.
+     */
+    public void init();
+
+    /**
+     * This view is navigated to.
+     * 
+     * This method is always called before the view is shown on screen. If there
+     * is any additional id to data what should be shown in the view, it is also
+     * optionally passed as parameter.
+     * 
+     * TODO fragmentParameters null if no parameters or empty string?
+     * 
+     * @param fragmentParameters
+     *            parameters to the view or null if none given. This is the
+     *            string that appears e.g. in URI after "viewname/"
+     * @param internalParameters
+     *            parameters given directly to
+     *            {@link Navigator#navigateTo(String, Object...)}, not passed
+     *            via the fragment and not preserved in bookmarks
+     */
+    public void navigateTo(String fragmentParameters,
+            Object... internalParameters);
+}
\ No newline at end of file
diff --git a/src/com/vaadin/navigator/ViewChangeListener.java b/src/com/vaadin/navigator/ViewChangeListener.java
new file mode 100644 (file)
index 0000000..9ac48e2
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+/**
+ * Interface for listening to View changes before and after they occur.
+ * 
+ * Implementations of this interface can also block navigation between views
+ * before it is performed.
+ * 
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface ViewChangeListener extends Serializable {
+
+    /**
+     * Check whether changing the view is permissible.
+     * 
+     * This method may also e.g. open a "save" dialog or question about the
+     * change, which may re-initiate the navigation operation after user action.
+     * 
+     * If this listener does not want to block the view change (e.g. does not
+     * know the view in question), it should return true. If any listener
+     * returns false, the view change is not allowed.
+     * 
+     * TODO move to separate interface?
+     * 
+     * @param previous
+     *            view that is being deactivated
+     * @param next
+     *            view that is being activated
+     * @param viewName
+     *            name of the new view that is being activated
+     * @param fragmentParameters
+     *            fragment parameters (potentially bookmarkable) for the new
+     *            view
+     * @param internalParameters
+     *            internal parameters for the new view, not visible in the
+     *            browser
+     * @return true if the view change should be allowed or this listener does
+     *         not care about the view change, false to block the change
+     */
+    public boolean isViewChangeAllowed(View previous, View next,
+            String viewName, String fragmentParameters,
+            Object... internalParameters);
+
+    /**
+     * Invoked after the view has changed. Be careful for deadlocks if you
+     * decide to change the view again in the listener.
+     * 
+     * @param previous
+     *            Preview view before the change.
+     * @param current
+     *            New view after the change.
+     */
+    public void navigatorViewChanged(View previous, View current);
+
+}
\ No newline at end of file
diff --git a/src/com/vaadin/navigator/ViewDisplay.java b/src/com/vaadin/navigator/ViewDisplay.java
new file mode 100644 (file)
index 0000000..6016951
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+/**
+ * Interface for displaying a view in an appropriate location.
+ * 
+ * The view display can be a component/layout itself or can modify a separate
+ * layout.
+ * 
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface ViewDisplay extends Serializable {
+    /**
+     * Remove previously shown view and show the newly selected view in its
+     * place.
+     * 
+     * The parameters for the view have been set before this method is called.
+     * 
+     * @param view
+     *            new view to show
+     */
+    public void showView(View view);
+}
\ No newline at end of file
diff --git a/src/com/vaadin/navigator/ViewProvider.java b/src/com/vaadin/navigator/ViewProvider.java
new file mode 100644 (file)
index 0000000..4d9d22a
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+/**
+ * A provider for view instances that can return pre-registered views or
+ * dynamically create new views.
+ * 
+ * If multiple providers are used, {@link #getViewName(String)} of each is
+ * called (in registration order) until one of them returns a non-null value.
+ * The {@link #getView(String)} method of that provider is then used.
+ * 
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface ViewProvider extends Serializable {
+    /**
+     * Extract the view name from a combined view name and parameter string.
+     * This method should return a view name if and only if this provider
+     * handles creation of such views.
+     * 
+     * @param viewAndParameters
+     *            string with view name and its fragment parameters (if given),
+     *            not null
+     * @return view name if the view is handled by this provider, null otherwise
+     */
+    public String getViewName(String viewAndParameters);
+
+    /**
+     * Create or return a pre-created instance of a view.
+     * 
+     * The parameters for the view are set separately by the navigator when the
+     * view is activated.
+     * 
+     * @param viewName
+     *            name of the view, not null
+     * @return newly created view (null if none available for the view name)
+     */
+    public View getView(String viewName);
+}
\ No newline at end of file
diff --git a/tests/server-side/com/vaadin/tests/server/navigator/ClassBasedViewProviderTest.java b/tests/server-side/com/vaadin/tests/server/navigator/ClassBasedViewProviderTest.java
new file mode 100644 (file)
index 0000000..818229a
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.tests.server.navigator;
+
+import junit.framework.TestCase;
+
+import com.vaadin.navigator.Navigator.ClassBasedViewProvider;
+import com.vaadin.navigator.View;
+import com.vaadin.ui.Label;
+
+public class ClassBasedViewProviderTest extends TestCase {
+
+    private ClassBasedViewProvider provider;
+
+    public static class TestView extends Label implements View {
+        public boolean initialized = false;
+        public String parameters = null;
+
+        public void init() {
+            initialized = true;
+        }
+
+        public void navigateTo(String parameters, Object... internalParameters) {
+            this.parameters = parameters;
+        }
+
+    }
+
+    public static class TestView2 extends TestView {
+
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        provider = new ClassBasedViewProvider();
+    }
+
+    public void testAddViewWithNullName() throws Exception {
+        try {
+            provider.addView(null, TestView.class);
+            fail("Should not be able to add view with null name");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    public void testAddViewWithEmptyStringName() throws Exception {
+        try {
+            provider.addView("", TestView.class);
+            fail("Should not be able to add view with empty name");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    public void testAddViewNull() throws Exception {
+        try {
+            provider.addView("test", null);
+            fail("Should not be able to add null view");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    public void testAddViewSameName() throws Exception {
+        try {
+            provider.addView("test", TestView.class);
+            provider.addView("test", TestView2.class);
+            fail("Should not be able to add two views with same name");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    public void testAddViewSameClass() throws Exception {
+        try {
+            provider.addView("test", TestView.class);
+            provider.addView("test2", TestView.class);
+            fail("Should not be able to add same view class with two different names");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    public void testGetViewNameForNullString() throws Exception {
+        assertNull(
+                "Found view name for null view string in an empty view provider",
+                provider.getViewName((String) null));
+
+        provider.addView("test", TestView.class);
+        assertNull("Found view name for null view string",
+                provider.getViewName((String) null));
+    }
+
+    public void testGetViewNameForNullClass() throws Exception {
+        assertNull("Found view name for null class",
+                provider.getViewName((Class<View>) null));
+    }
+
+    public void testGetViewNameForEmptyString() throws Exception {
+        assertNull(
+                "Found view name for empty view string in an empty provider",
+                provider.getViewName(""));
+        provider.addView("test", TestView.class);
+        assertNull("Found view name for empty view string",
+                provider.getViewName(""));
+    }
+
+    public void testGetViewNameForClass() throws Exception {
+        provider.addView("test", TestView.class);
+        assertEquals("No view name found for view class", "test",
+                provider.getViewName(TestView.class));
+    }
+
+    public void testGetViewNameWithParameters() throws Exception {
+        provider.addView("test", TestView.class);
+        assertEquals("Incorrect view name found for view string", "test",
+                provider.getViewName("test"));
+        assertEquals(
+                "Incorrect view name found for view string ending with slash",
+                "test", provider.getViewName("test/"));
+        assertEquals(
+                "Incorrect view name found for view string with parameters",
+                "test", provider.getViewName("test/params/are/here"));
+    }
+
+    public void testGetViewNameMultipleRegisteredWithParameters()
+            throws Exception {
+        provider.addView("test", TestView.class);
+        provider.addView("test2", TestView2.class);
+        assertEquals("Incorrect view name found for view string", "test",
+                provider.getViewName("test/test2/params"));
+    }
+
+    public void testGetViewNameNestedNames() throws Exception {
+        provider.addView("test/subview", TestView2.class);
+        provider.addView("test", TestView.class);
+        assertEquals("Incorrect view name found for subview string",
+                "test/subview", provider.getViewName("test/subview"));
+        assertEquals(
+                "Incorrect view name found for subview string with empty parameters",
+                "test/subview", provider.getViewName("test/subview/"));
+        assertEquals(
+                "Incorrect view name found for subview string with parameters",
+                "test/subview", provider.getViewName("test/subview/parameters"));
+        assertEquals("Incorrect view name found for top level view string",
+                "test", provider.getViewName("test"));
+        assertEquals(
+                "Incorrect view name found for top level view string with empty parameters",
+                "test", provider.getViewName("test/"));
+        assertEquals(
+                "Incorrect view name found for top level view string with parameters starting like subview name",
+                "test", provider.getViewName("test/subviewnothere"));
+    }
+
+    public void testGetViewClass() throws Exception {
+        assertNull("View class found for empty view provider",
+                provider.getViewClass("test"));
+        provider.addView("test", TestView.class);
+        assertEquals("View class not found", TestView.class,
+                provider.getViewClass("test"));
+        assertNull("View class found for unregistered view name",
+                provider.getViewClass("test2"));
+    }
+
+    public void testGetViewSimple() throws Exception {
+        assertNull("Found view in an empty view provider",
+                provider.getViewName("test"));
+
+        provider.addView("test", TestView.class);
+        View view = provider.getView("test");
+        assertNotNull("Did not get view from a provider", view);
+        assertEquals("Incorrect view type", TestView.class, view.getClass());
+        assertTrue("View not initialized", ((TestView) view).initialized);
+    }
+
+    public void testGetViewMultipleRegistered() throws Exception {
+        provider.addView("test", TestView.class);
+        provider.addView("test2", TestView2.class);
+        assertEquals("Incorrect view type", TestView.class,
+                provider.getView("test").getClass());
+        assertEquals("Incorrect view type", TestView2.class,
+                provider.getView("test2").getClass());
+        assertEquals("Incorrect view type", TestView.class,
+                provider.getView("test").getClass());
+    }
+
+    public void testRemoveView() throws Exception {
+        provider.addView("test", TestView.class);
+        assertNotNull("Did not get view from a provider",
+                provider.getView("test"));
+        provider.removeView("test");
+        assertNull("View class found for removed view name",
+                provider.getViewClass("test"));
+        assertNull("View name found for removed view",
+                provider.getViewName(TestView.class));
+        // cached view?
+        assertNull(
+                "Received view instance from a provider after removing view type",
+                provider.getView("test"));
+    }
+
+    public void testGetViewCached() throws Exception {
+        provider.addView("test", TestView.class);
+        View view1 = provider.getView("test");
+        View view2 = provider.getView("test");
+        assertSame("View instance not cached", view1, view2);
+    }
+
+}
diff --git a/tests/server-side/com/vaadin/tests/server/navigator/NavigatorTest.java b/tests/server-side/com/vaadin/tests/server/navigator/NavigatorTest.java
new file mode 100644 (file)
index 0000000..d6a7cc6
--- /dev/null
@@ -0,0 +1,252 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.tests.server.navigator;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+
+import com.vaadin.navigator.FragmentManager;
+import com.vaadin.navigator.Navigator;
+import com.vaadin.navigator.View;
+import com.vaadin.navigator.ViewChangeListener;
+import com.vaadin.navigator.ViewDisplay;
+import com.vaadin.navigator.ViewProvider;
+
+public class NavigatorTest extends TestCase {
+
+    // TODO test internal parameters (and absence of them)
+    // TODO test listeners blocking navigation, multiple listeners
+
+    public void testBasicNavigation() {
+        IMocksControl control = EasyMock.createControl();
+        FragmentManager manager = control.createMock(FragmentManager.class);
+        ViewDisplay display = control.createMock(ViewDisplay.class);
+        ViewProvider provider = control.createMock(ViewProvider.class);
+        View view1 = control.createMock(View.class);
+        View view2 = control.createMock(View.class);
+
+        // prepare mocks: what to expect
+        EasyMock.expect(provider.getViewName("test1")).andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("");
+        view1.navigateTo(null);
+        display.showView(view1);
+        manager.setFragment("test1");
+
+        EasyMock.expect(provider.getViewName("test2/")).andReturn("test2");
+        EasyMock.expect(provider.getView("test2")).andReturn(view2);
+        view2.init();
+        EasyMock.expect(manager.getFragment()).andReturn("view1");
+        view2.navigateTo(null);
+        display.showView(view2);
+        manager.setFragment("test2");
+
+        EasyMock.expect(provider.getViewName("test1/params"))
+                .andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("view2");
+        view1.navigateTo("params");
+        display.showView(view1);
+        manager.setFragment("test1/params");
+
+        control.replay();
+
+        // create and test navigator
+        Navigator navigator = new Navigator(manager, display);
+        navigator.registerProvider(provider);
+
+        navigator.navigateTo("test1");
+        navigator.navigateTo("test2/");
+        navigator.navigateTo("test1/params");
+    }
+
+    public void testMainView() {
+        IMocksControl control = EasyMock.createControl();
+        FragmentManager manager = control.createMock(FragmentManager.class);
+        ViewDisplay display = control.createMock(ViewDisplay.class);
+        ViewProvider provider = control.createMock(ViewProvider.class);
+        View view1 = control.createMock(View.class);
+        View view2 = control.createMock(View.class);
+
+        // prepare mocks: what to expect
+        EasyMock.expect(provider.getViewName("test1")).andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("");
+        view1.navigateTo(null);
+        display.showView(view1);
+        manager.setFragment("test1");
+
+        EasyMock.expect(provider.getViewName("test2")).andReturn("test2");
+        EasyMock.expect(provider.getView("test2")).andReturn(view2);
+        view2.init();
+        EasyMock.expect(manager.getFragment()).andReturn("view1");
+        view2.navigateTo(null);
+        display.showView(view2);
+        manager.setFragment("test2");
+
+        EasyMock.expect(provider.getViewName("test1")).andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("");
+        view1.navigateTo(null);
+        display.showView(view1);
+        manager.setFragment("test1");
+
+        EasyMock.expect(provider.getViewName("test1/params"))
+                .andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("view2");
+        view1.navigateTo("params");
+        display.showView(view1);
+        manager.setFragment("test1/params");
+
+        control.replay();
+
+        // create and test navigator
+        Navigator navigator = new Navigator(manager, display);
+        navigator.registerProvider(provider);
+        // this also triggers navigation
+        navigator.setMainView("test1");
+
+        navigator.navigateTo("test2");
+        navigator.navigateTo("");
+        navigator.navigateTo("test1/params");
+    }
+
+    public void testListeners() {
+        IMocksControl control = EasyMock.createControl();
+        FragmentManager manager = control.createMock(FragmentManager.class);
+        ViewDisplay display = control.createMock(ViewDisplay.class);
+        ViewProvider provider = control.createMock(ViewProvider.class);
+        View view1 = control.createMock(View.class);
+        View view2 = control.createMock(View.class);
+        ViewChangeListener listener = control
+                .createMock(ViewChangeListener.class);
+
+        // prepare mocks: what to expect
+        EasyMock.expect(provider.getViewName("test1")).andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("");
+        EasyMock.expect(
+                listener.isViewChangeAllowed(null, view1, "test1", null,
+                        new Object[0])).andReturn(true);
+        view1.navigateTo(null);
+        display.showView(view1);
+        manager.setFragment("test1");
+        listener.navigatorViewChanged(null, view1);
+
+        EasyMock.expect(provider.getViewName("test2")).andReturn("test2");
+        EasyMock.expect(provider.getView("test2")).andReturn(view2);
+        view2.init();
+        EasyMock.expect(manager.getFragment()).andReturn("view1");
+        EasyMock.expect(
+                listener.isViewChangeAllowed(view1, view2, "test2", null,
+                        new Object[0])).andReturn(true);
+        view2.navigateTo(null);
+        display.showView(view2);
+        manager.setFragment("test2");
+        listener.navigatorViewChanged(view1, view2);
+
+        control.replay();
+
+        // create and test navigator
+        Navigator navigator = new Navigator(manager, display);
+        navigator.registerProvider(provider);
+        navigator.addListener(listener);
+
+        navigator.navigateTo("test1");
+        navigator.navigateTo("test2");
+    }
+
+    public void testBlockNavigation() {
+        IMocksControl control = EasyMock.createControl();
+        FragmentManager manager = control.createMock(FragmentManager.class);
+        ViewDisplay display = control.createMock(ViewDisplay.class);
+        ViewProvider provider = control.createMock(ViewProvider.class);
+        View view1 = control.createMock(View.class);
+        View view2 = control.createMock(View.class);
+        ViewChangeListener listener1 = control
+                .createMock(ViewChangeListener.class);
+        ViewChangeListener listener2 = control
+                .createMock(ViewChangeListener.class);
+
+        // prepare mocks: what to expect
+        // first listener blocks first view change
+        EasyMock.expect(provider.getViewName("test1")).andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("");
+        EasyMock.expect(
+                listener1.isViewChangeAllowed(null, view1, "test1", null,
+                        new Object[0])).andReturn(false);
+
+        // second listener blocks second view change
+        EasyMock.expect(provider.getViewName("test1/test")).andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("");
+        EasyMock.expect(
+                listener1.isViewChangeAllowed(null, view1, "test1", "test",
+                        new Object[0])).andReturn(true);
+        EasyMock.expect(
+                listener2.isViewChangeAllowed(null, view1, "test1", "test",
+                        new Object[0])).andReturn(false);
+
+        // both listeners allow view change
+        EasyMock.expect(provider.getViewName("test1/bar")).andReturn("test1");
+        EasyMock.expect(provider.getView("test1")).andReturn(view1);
+        view1.init();
+        EasyMock.expect(manager.getFragment()).andReturn("");
+        EasyMock.expect(
+                listener1.isViewChangeAllowed(null, view1, "test1", "bar",
+                        new Object[0])).andReturn(true);
+        EasyMock.expect(
+                listener2.isViewChangeAllowed(null, view1, "test1", "bar",
+                        new Object[0])).andReturn(true);
+        view1.navigateTo("bar");
+        display.showView(view1);
+        manager.setFragment("test1/bar");
+        listener1.navigatorViewChanged(null, view1);
+        listener2.navigatorViewChanged(null, view1);
+
+        // both listeners allow view change from non-null view
+        EasyMock.expect(provider.getViewName("test2")).andReturn("test2");
+        EasyMock.expect(provider.getView("test2")).andReturn(view2);
+        view2.init();
+        EasyMock.expect(manager.getFragment()).andReturn("view1");
+        EasyMock.expect(
+                listener1.isViewChangeAllowed(view1, view2, "test2", null,
+                        new Object[0])).andReturn(true);
+        EasyMock.expect(
+                listener2.isViewChangeAllowed(view1, view2, "test2", null,
+                        new Object[0])).andReturn(true);
+        view2.navigateTo(null);
+        display.showView(view2);
+        manager.setFragment("test2");
+        listener1.navigatorViewChanged(view1, view2);
+        listener2.navigatorViewChanged(view1, view2);
+
+        control.replay();
+
+        // create and test navigator
+        Navigator navigator = new Navigator(manager, display);
+        navigator.registerProvider(provider);
+        navigator.addListener(listener1);
+        navigator.addListener(listener2);
+
+        navigator.navigateTo("test1");
+        navigator.navigateTo("test1/test");
+        navigator.navigateTo("test1/bar");
+        navigator.navigateTo("test2");
+    }
+
+}
diff --git a/tests/server-side/com/vaadin/tests/server/navigator/UriFragmentManagerTest.java b/tests/server-side/com/vaadin/tests/server/navigator/UriFragmentManagerTest.java
new file mode 100644 (file)
index 0000000..cfbf50f
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.tests.server.navigator;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+
+import com.vaadin.navigator.Navigator;
+import com.vaadin.navigator.Navigator.UriFragmentManager;
+import com.vaadin.ui.Root;
+import com.vaadin.ui.Root.FragmentChangedEvent;
+
+public class UriFragmentManagerTest extends TestCase {
+
+    public void testGetSetFragment() {
+        Root root = EasyMock.createMock(Root.class);
+        UriFragmentManager manager = new UriFragmentManager(root, null);
+
+        // prepare mock
+        EasyMock.expect(root.getFragment()).andReturn("");
+        root.setFragment("test");
+        EasyMock.expect(root.getFragment()).andReturn("test");
+        EasyMock.replay(root);
+
+        // test manager using the mock
+        assertEquals("Incorrect fragment value", "", manager.getFragment());
+        manager.setFragment("test");
+        assertEquals("Incorrect fragment value", "test", manager.getFragment());
+    }
+
+    public void testListener() {
+        // create mocks
+        IMocksControl control = EasyMock.createControl();
+        Navigator navigator = control.createMock(Navigator.class);
+        Root root = control.createMock(Root.class);
+
+        UriFragmentManager manager = new UriFragmentManager(root, navigator);
+
+        EasyMock.expect(root.getFragment()).andReturn("test");
+        navigator.navigateTo("test");
+        control.replay();
+
+        FragmentChangedEvent event = root.new FragmentChangedEvent(root,
+                "oldtest");
+        manager.fragmentChanged(event);
+    }
+}