summaryrefslogtreecommitdiffstats
path: root/src/com/vaadin/navigator/Navigator.java
blob: 2c340adaa28b681e52f23884a25344b28911dd81 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
package com.vaadin.navigator;

/*
 @VaadinApache2LicenseForJavaFiles@
 */

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.terminal.Page;
import com.vaadin.terminal.Page.FragmentChangedEvent;
import com.vaadin.terminal.Page.FragmentChangedListener;
import com.vaadin.ui.Component;
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");
        }

        public void navigateTo(String fragmentParameters) {
            // nothing to do
        }
    }

    /**
     * Fragment manager using URI fragments of a Page 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 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.addListener(this);
        }

        public String getFragment() {
            return page.getFragment();
        }

        public void setFragment(String fragment) {
            page.setFragment(fragment, false);
        }

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

        public String getViewName(String viewAndParameters) {
            if (null == viewAndParameters) {
                return null;
            }
            if (viewAndParameters.startsWith(viewName)) {
                return viewName;
            }
            return null;
        }

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

        public String getViewName(String viewAndParameters) {
            if (null == viewAndParameters) {
                return null;
            }
            if (viewAndParameters.equals(viewName)
                    || viewAndParameters.startsWith(viewName + "/")) {
                return viewName;
            }
            return null;
        }

        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 FragmentManager fragmentManager;
    private final ViewDisplay display;
    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.
     * 
     * <p>
     * After all {@link View}s and {@link ViewProvider}s have been registered,
     * the application should trigger navigation to the current fragment using
     * e.g.
     * 
     * <pre>
     * navigator.navigateTo(Page.getCurrent().getFragment());
     * </pre>
     * 
     * @param page
     *            whose URI fragments are used
     * @param display
     *            where to display the views
     */
    public Navigator(Page page, ViewDisplay display) {
        this.display = display;
        fragmentManager = new UriFragmentManager(page, this);
    }

    /**
     * Create a navigator that is tracking the active view using URI fragments.
     * By default, a {@link SimpleViewDisplay} is used and can be obtained using
     * {@link #getDisplay()}.
     * 
     * <p>
     * After all {@link View}s and {@link ViewProvider}s have been registered,
     * the application should trigger navigation to the current fragment using
     * e.g.
     * 
     * <pre>
     * navigator.navigateTo(Page.getCurrent().getFragment());
     * </pre>
     * 
     * @param page
     *            whose URI fragments are used
     */
    public Navigator(Page page) {
        display = new SimpleViewDisplay();
        fragmentManager = new UriFragmentManager(page, this);
    }

    /**
     * Create a navigator.
     * 
     * When a custom fragment manager is not needed, use the constructor
     * {@link #Navigator(Page, ViewDisplay)} which uses a URI fragment based
     * fragment manager.
     * 
     * Note that navigation to the initial view must be performed explicitly by
     * the application after creating a Navigator using this constructor.
     * 
     * @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 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 viewAndParameters
     *            view name and parameters
     */
    public void navigateTo(String viewAndParameters) {
        String longestViewName = null;
        View viewWithLongestName = null;
        for (ViewProvider provider : providers) {
            String viewName = provider.getViewName(viewAndParameters);
            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 = null;
            if (viewAndParameters.length() > longestViewName.length() + 1) {
                parameters = viewAndParameters.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 set the fragment
     * @param fragmentParameters
     *            parameters passed in the fragment for the view
     */
    protected void navigateTo(View view, String viewName,
            String fragmentParameters) {
        ViewChangeEvent event = new ViewChangeEvent(this, currentView, view,
                viewName, fragmentParameters);
        if (!isViewChangeAllowed(event)) {
            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);
        currentView = view;

        if (display != null) {
            display.showView(view);
        }

        fireViewChange(event);
    }

    /**
     * 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 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 isViewChangeAllowed(ViewChangeEvent event) {
        for (ViewChangeListener l : listeners) {
            if (!l.isViewChangeAllowed(event)) {
                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;
    }

    /**
     * Returns 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;
    }

    /**
     * Fire an event when the current view has changed.
     * 
     * @param event
     *            view change event (not null)
     */
    protected void fireViewChange(ViewChangeEvent event) {
        for (ViewChangeListener l : listeners) {
            l.navigatorViewChanged(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);
        registerProvider(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);
        registerProvider(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<ViewProvider> it = providers.iterator();
        while (it.hasNext()) {
            ViewProvider provider = it.next();
            if (provider instanceof StaticViewProvider) {
                StaticViewProvider staticProvider = (StaticViewProvider) provider;
                if (staticProvider.getViewName().equals(viewName)) {
                    it.remove();
                }
            } else if (provider instanceof ClassBasedViewProvider) {
                ClassBasedViewProvider classBasedProvider = (ClassBasedViewProvider) provider;
                if (classBasedProvider.getViewName().equals(viewName)) {
                    it.remove();
                }
            }
        }
    }

    /**
     * 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);
    }

}