You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Navigator.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. package com.vaadin.navigator;
  2. /*
  3. @VaadinApache2LicenseForJavaFiles@
  4. */
  5. import java.io.Serializable;
  6. import java.util.Iterator;
  7. import java.util.LinkedList;
  8. import java.util.List;
  9. import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
  10. import com.vaadin.terminal.Page;
  11. import com.vaadin.terminal.Page.FragmentChangedEvent;
  12. import com.vaadin.terminal.Page.FragmentChangedListener;
  13. import com.vaadin.ui.Component;
  14. import com.vaadin.ui.ComponentContainer;
  15. import com.vaadin.ui.CssLayout;
  16. import com.vaadin.ui.CustomComponent;
  17. /**
  18. * Navigator utility that allows switching of views in a part of an application.
  19. *
  20. * The view switching can be based e.g. on URI fragments containing the view
  21. * name and parameters to the view. There are two types of parameters for views:
  22. * an optional parameter string that is included in the fragment (may be
  23. * bookmarkable).
  24. *
  25. * Views can be explicitly registered or dynamically generated and listening to
  26. * view changes is possible.
  27. *
  28. * Note that {@link Navigator} is not a component itself but comes with
  29. * {@link SimpleViewDisplay} which is a component that displays the selected
  30. * view as its contents.
  31. *
  32. * @author Vaadin Ltd
  33. * @since 7.0
  34. */
  35. public class Navigator implements Serializable {
  36. // TODO divert navigation e.g. if no permissions? Or just show another view
  37. // but keep URL? how best to intercept
  38. // TODO investigate relationship with TouchKit navigation support
  39. /**
  40. * Empty view component.
  41. */
  42. public static class EmptyView extends CssLayout implements View {
  43. /**
  44. * Create minimally sized empty view.
  45. */
  46. public EmptyView() {
  47. setWidth("0px");
  48. setHeight("0px");
  49. }
  50. @Override
  51. public void navigateTo(String fragmentParameters) {
  52. // nothing to do
  53. }
  54. }
  55. /**
  56. * Fragment manager using URI fragments of a Page to track views and enable
  57. * listening to view changes.
  58. *
  59. * This class is mostly for internal use by Navigator, and is only public
  60. * and static to enable testing.
  61. */
  62. public static class UriFragmentManager implements FragmentManager,
  63. FragmentChangedListener {
  64. private final Page page;
  65. private final Navigator navigator;
  66. /**
  67. * Create a new URIFragmentManager and attach it to listen to URI
  68. * fragment changes of a {@link Page}.
  69. *
  70. * @param page
  71. * page whose URI fragment to get and modify
  72. * @param navigator
  73. * {@link Navigator} to notify of fragment changes (using
  74. * {@link Navigator#navigateTo(String)}
  75. */
  76. public UriFragmentManager(Page page, Navigator navigator) {
  77. this.page = page;
  78. this.navigator = navigator;
  79. page.addListener(this);
  80. }
  81. @Override
  82. public String getFragment() {
  83. return page.getFragment();
  84. }
  85. @Override
  86. public void setFragment(String fragment) {
  87. page.setFragment(fragment, false);
  88. }
  89. @Override
  90. public void fragmentChanged(FragmentChangedEvent event) {
  91. UriFragmentManager.this.navigator.navigateTo(getFragment());
  92. }
  93. }
  94. /**
  95. * View display that is a component itself and replaces its contents with
  96. * the view.
  97. *
  98. * This display only supports views that are {@link Component}s themselves.
  99. * Attempting to display a view that is not a component causes an exception
  100. * to be thrown.
  101. *
  102. * By default, the view display has full size.
  103. */
  104. public static class SimpleViewDisplay extends CustomComponent implements
  105. ViewDisplay {
  106. /**
  107. * Create new {@link ViewDisplay} that is itself a component displaying
  108. * the view.
  109. */
  110. public SimpleViewDisplay() {
  111. setSizeFull();
  112. }
  113. @Override
  114. public void showView(View view) {
  115. if (view instanceof Component) {
  116. setCompositionRoot((Component) view);
  117. } else {
  118. throw new IllegalArgumentException("View is not a component: "
  119. + view);
  120. }
  121. }
  122. }
  123. /**
  124. * View display that replaces the contents of a {@link ComponentContainer}
  125. * with the active {@link View}.
  126. *
  127. * All components of the container are removed before adding the new view to
  128. * it.
  129. *
  130. * This display only supports views that are {@link Component}s themselves.
  131. * Attempting to display a view that is not a component causes an exception
  132. * to be thrown.
  133. */
  134. public static class ComponentContainerViewDisplay implements ViewDisplay {
  135. private final ComponentContainer container;
  136. /**
  137. * Create new {@link ViewDisplay} that updates a
  138. * {@link ComponentContainer} to show the view.
  139. */
  140. public ComponentContainerViewDisplay(ComponentContainer container) {
  141. this.container = container;
  142. }
  143. @Override
  144. public void showView(View view) {
  145. if (view instanceof Component) {
  146. container.removeAllComponents();
  147. container.addComponent((Component) view);
  148. } else {
  149. throw new IllegalArgumentException("View is not a component: "
  150. + view);
  151. }
  152. }
  153. }
  154. /**
  155. * View provider which supports mapping a single view name to a single
  156. * pre-initialized view instance.
  157. *
  158. * For most cases, ClassBasedViewProvider should be used instead of this.
  159. */
  160. public static class StaticViewProvider implements ViewProvider {
  161. private final String viewName;
  162. private final View view;
  163. /**
  164. * Create a new view provider which returns a pre-created view instance.
  165. *
  166. * @param viewName
  167. * name of the view (not null)
  168. * @param view
  169. * view instance to return (not null), reused on every
  170. * request
  171. */
  172. public StaticViewProvider(String viewName, View view) {
  173. this.viewName = viewName;
  174. this.view = view;
  175. }
  176. @Override
  177. public String getViewName(String viewAndParameters) {
  178. if (null == viewAndParameters) {
  179. return null;
  180. }
  181. if (viewAndParameters.startsWith(viewName)) {
  182. return viewName;
  183. }
  184. return null;
  185. }
  186. @Override
  187. public View getView(String viewName) {
  188. if (this.viewName.equals(viewName)) {
  189. return view;
  190. }
  191. return null;
  192. }
  193. /**
  194. * Get the view name for this provider.
  195. *
  196. * @return view name for this provider
  197. */
  198. public String getViewName() {
  199. return viewName;
  200. }
  201. }
  202. /**
  203. * View provider which maps a single view name to a class to instantiate for
  204. * the view.
  205. *
  206. * Note that the view class must be accessible by the class loader used by
  207. * the provider. This may require its visibility to be public.
  208. *
  209. * This class is primarily for internal use by {@link Navigator}.
  210. */
  211. public static class ClassBasedViewProvider implements ViewProvider {
  212. private final String viewName;
  213. private final Class<? extends View> viewClass;
  214. /**
  215. * Create a new view provider which creates new view instances based on
  216. * a view class.
  217. *
  218. * @param viewName
  219. * name of the views to create (not null)
  220. * @param viewClass
  221. * class to instantiate when a view is requested (not null)
  222. */
  223. public ClassBasedViewProvider(String viewName,
  224. Class<? extends View> viewClass) {
  225. if (null == viewName || null == viewClass) {
  226. throw new IllegalArgumentException(
  227. "View name and class should not be null");
  228. }
  229. this.viewName = viewName;
  230. this.viewClass = viewClass;
  231. }
  232. @Override
  233. public String getViewName(String viewAndParameters) {
  234. if (null == viewAndParameters) {
  235. return null;
  236. }
  237. if (viewAndParameters.equals(viewName)
  238. || viewAndParameters.startsWith(viewName + "/")) {
  239. return viewName;
  240. }
  241. return null;
  242. }
  243. @Override
  244. public View getView(String viewName) {
  245. if (this.viewName.equals(viewName)) {
  246. try {
  247. View view = viewClass.newInstance();
  248. return view;
  249. } catch (InstantiationException e) {
  250. // TODO error handling
  251. throw new RuntimeException(e);
  252. } catch (IllegalAccessException e) {
  253. // TODO error handling
  254. throw new RuntimeException(e);
  255. }
  256. }
  257. return null;
  258. }
  259. /**
  260. * Get the view name for this provider.
  261. *
  262. * @return view name for this provider
  263. */
  264. public String getViewName() {
  265. return viewName;
  266. }
  267. /**
  268. * Get the view class for this provider.
  269. *
  270. * @return {@link View} class
  271. */
  272. public Class<? extends View> getViewClass() {
  273. return viewClass;
  274. }
  275. }
  276. private final FragmentManager fragmentManager;
  277. private final ViewDisplay display;
  278. private View currentView = null;
  279. private List<ViewChangeListener> listeners = new LinkedList<ViewChangeListener>();
  280. private List<ViewProvider> providers = new LinkedList<ViewProvider>();
  281. /**
  282. * Create a navigator that is tracking the active view using URI fragments
  283. * of the current {@link Page} and replacing the contents of a
  284. * {@link ComponentContainer} with the active view.
  285. *
  286. * In case the container is not on the current page, use another
  287. * {@link Navigator#Navigator(Page, ViewDisplay)} with an explicitly created
  288. * {@link ComponentContainerViewDisplay}.
  289. *
  290. * All components of the container are removed each time before adding the
  291. * active {@link View}. Views must implement {@link Component} when using
  292. * this constructor.
  293. *
  294. * <p>
  295. * After all {@link View}s and {@link ViewProvider}s have been registered,
  296. * the application should trigger navigation to the current fragment using
  297. * e.g.
  298. *
  299. * <pre>
  300. * navigator.navigateTo(Page.getCurrent().getFragment());
  301. * </pre>
  302. *
  303. * @param container
  304. * ComponentContainer whose contents should be replaced with the
  305. * active view on view change
  306. */
  307. public Navigator(ComponentContainer container) {
  308. display = new ComponentContainerViewDisplay(container);
  309. fragmentManager = new UriFragmentManager(Page.getCurrent(), this);
  310. }
  311. /**
  312. * Create a navigator that is tracking the active view using URI fragments.
  313. *
  314. * <p>
  315. * After all {@link View}s and {@link ViewProvider}s have been registered,
  316. * the application should trigger navigation to the current fragment using
  317. * e.g.
  318. *
  319. * <pre>
  320. * navigator.navigateTo(Page.getCurrent().getFragment());
  321. * </pre>
  322. *
  323. * @param page
  324. * whose URI fragments are used
  325. * @param display
  326. * where to display the views
  327. */
  328. public Navigator(Page page, ViewDisplay display) {
  329. this.display = display;
  330. fragmentManager = new UriFragmentManager(page, this);
  331. }
  332. /**
  333. * Create a navigator.
  334. *
  335. * When a custom fragment manager is not needed, use the constructor
  336. * {@link #Navigator(Page, ViewDisplay)} which uses a URI fragment based
  337. * fragment manager.
  338. *
  339. * Note that navigation to the initial view must be performed explicitly by
  340. * the application after creating a Navigator using this constructor.
  341. *
  342. * @param fragmentManager
  343. * fragment manager keeping track of the active view and enabling
  344. * bookmarking and direct navigation
  345. * @param display
  346. * where to display the views
  347. */
  348. public Navigator(FragmentManager fragmentManager, ViewDisplay display) {
  349. this.display = display;
  350. this.fragmentManager = fragmentManager;
  351. }
  352. /**
  353. * Navigate to a view and initialize the view with given parameters.
  354. *
  355. * The view string consists of a view name optionally followed by a slash
  356. * and (fragment) parameters. ViewProviders are used to find and create the
  357. * correct type of view.
  358. *
  359. * If multiple providers return a matching view, the view with the longest
  360. * name is selected. This way, e.g. hierarchies of subviews can be
  361. * registered like "admin/", "admin/users", "admin/settings" and the longest
  362. * match is used.
  363. *
  364. * If the view being deactivated indicates it wants a confirmation for the
  365. * navigation operation, the user is asked for the confirmation.
  366. *
  367. * Registered {@link ViewChangeListener}s are called upon successful view
  368. * change.
  369. *
  370. * @param viewAndParameters
  371. * view name and parameters
  372. */
  373. public void navigateTo(String viewAndParameters) {
  374. String longestViewName = null;
  375. View viewWithLongestName = null;
  376. for (ViewProvider provider : providers) {
  377. String viewName = provider.getViewName(viewAndParameters);
  378. if (null != viewName
  379. && (longestViewName == null || viewName.length() > longestViewName
  380. .length())) {
  381. View view = provider.getView(viewName);
  382. if (null != view) {
  383. longestViewName = viewName;
  384. viewWithLongestName = view;
  385. }
  386. }
  387. }
  388. if (viewWithLongestName != null) {
  389. String parameters = null;
  390. if (viewAndParameters.length() > longestViewName.length() + 1) {
  391. parameters = viewAndParameters.substring(longestViewName
  392. .length() + 1);
  393. }
  394. navigateTo(viewWithLongestName, longestViewName, parameters);
  395. }
  396. // TODO if no view is found, what to do?
  397. }
  398. /**
  399. * Internal method activating a view, setting its parameters and calling
  400. * listeners.
  401. *
  402. * This method also verifies that the user is allowed to perform the
  403. * navigation operation.
  404. *
  405. * @param view
  406. * view to activate
  407. * @param viewName
  408. * (optional) name of the view or null not to set the fragment
  409. * @param fragmentParameters
  410. * parameters passed in the fragment for the view
  411. */
  412. protected void navigateTo(View view, String viewName,
  413. String fragmentParameters) {
  414. ViewChangeEvent event = new ViewChangeEvent(this, currentView, view,
  415. viewName, fragmentParameters);
  416. if (!isViewChangeAllowed(event)) {
  417. return;
  418. }
  419. if (null != viewName && getFragmentManager() != null) {
  420. String currentFragment = viewName;
  421. if (fragmentParameters != null) {
  422. currentFragment += "/" + fragmentParameters;
  423. }
  424. if (!currentFragment.equals(getFragmentManager().getFragment())) {
  425. getFragmentManager().setFragment(currentFragment);
  426. }
  427. }
  428. view.navigateTo(fragmentParameters);
  429. currentView = view;
  430. if (display != null) {
  431. display.showView(view);
  432. }
  433. fireViewChange(event);
  434. }
  435. /**
  436. * Check whether view change is allowed.
  437. *
  438. * All related listeners are called. The view change is blocked if any of
  439. * them wants to block the navigation operation.
  440. *
  441. * The view change listeners may also e.g. open a warning or question dialog
  442. * and save the parameters to re-initiate the navigation operation upon user
  443. * action.
  444. *
  445. * @param event
  446. * view change event (not null, view change not yet performed)
  447. * @return true if the view change should be allowed, false to silently
  448. * block the navigation operation
  449. */
  450. protected boolean isViewChangeAllowed(ViewChangeEvent event) {
  451. for (ViewChangeListener l : listeners) {
  452. if (!l.isViewChangeAllowed(event)) {
  453. return false;
  454. }
  455. }
  456. return true;
  457. }
  458. /**
  459. * Return the fragment manager that is used to get, listen to and manipulate
  460. * the URI fragment or other source of navigation information.
  461. *
  462. * @return fragment manager in use
  463. */
  464. protected FragmentManager getFragmentManager() {
  465. return fragmentManager;
  466. }
  467. /**
  468. * Returns the ViewDisplay used by the navigator. Unless another display is
  469. * specified, a {@link SimpleViewDisplay} (which is a {@link Component}) is
  470. * used by default.
  471. *
  472. * @return current ViewDisplay
  473. */
  474. public ViewDisplay getDisplay() {
  475. return display;
  476. }
  477. /**
  478. * Fire an event when the current view has changed.
  479. *
  480. * @param event
  481. * view change event (not null)
  482. */
  483. protected void fireViewChange(ViewChangeEvent event) {
  484. for (ViewChangeListener l : listeners) {
  485. l.navigatorViewChanged(event);
  486. }
  487. }
  488. /**
  489. * Register a static, pre-initialized view instance for a view name.
  490. *
  491. * Registering another view with a name that is already registered
  492. * overwrites the old registration of the same type.
  493. *
  494. * @param viewName
  495. * String that identifies a view (not null nor empty string)
  496. * @param view
  497. * {@link View} instance (not null)
  498. */
  499. public void addView(String viewName, View view) {
  500. // Check parameters
  501. if (viewName == null || view == null) {
  502. throw new IllegalArgumentException(
  503. "view and viewName must be non-null");
  504. }
  505. removeView(viewName);
  506. registerProvider(new StaticViewProvider(viewName, view));
  507. }
  508. /**
  509. * Register for a view name a view class.
  510. *
  511. * Registering another view with a name that is already registered
  512. * overwrites the old registration of the same type.
  513. *
  514. * A new view instance is created every time a view is requested.
  515. *
  516. * @param viewName
  517. * String that identifies a view (not null nor empty string)
  518. * @param viewClass
  519. * {@link View} class to instantiate when a view is requested
  520. * (not null)
  521. */
  522. public void addView(String viewName, Class<? extends View> viewClass) {
  523. // Check parameters
  524. if (viewName == null || viewClass == null) {
  525. throw new IllegalArgumentException(
  526. "view and viewClass must be non-null");
  527. }
  528. removeView(viewName);
  529. registerProvider(new ClassBasedViewProvider(viewName, viewClass));
  530. }
  531. /**
  532. * Remove view from navigator.
  533. *
  534. * This method only applies to views registered using
  535. * {@link #addView(String, View)} or {@link #addView(String, Class)}.
  536. *
  537. * @param viewName
  538. * name of the view to remove
  539. */
  540. public void removeView(String viewName) {
  541. Iterator<ViewProvider> it = providers.iterator();
  542. while (it.hasNext()) {
  543. ViewProvider provider = it.next();
  544. if (provider instanceof StaticViewProvider) {
  545. StaticViewProvider staticProvider = (StaticViewProvider) provider;
  546. if (staticProvider.getViewName().equals(viewName)) {
  547. it.remove();
  548. }
  549. } else if (provider instanceof ClassBasedViewProvider) {
  550. ClassBasedViewProvider classBasedProvider = (ClassBasedViewProvider) provider;
  551. if (classBasedProvider.getViewName().equals(viewName)) {
  552. it.remove();
  553. }
  554. }
  555. }
  556. }
  557. /**
  558. * Register a view provider (factory).
  559. *
  560. * Providers are called in order of registration until one that can handle
  561. * the requested view name is found.
  562. *
  563. * @param provider
  564. * provider to register
  565. */
  566. public void registerProvider(ViewProvider provider) {
  567. providers.add(provider);
  568. }
  569. /**
  570. * Unregister a view provider (factory).
  571. *
  572. * @param provider
  573. * provider to unregister
  574. */
  575. public void unregisterProvider(ViewProvider provider) {
  576. providers.remove(provider);
  577. }
  578. /**
  579. * Listen to changes of the active view.
  580. *
  581. * The listener will get notified after the view has changed.
  582. *
  583. * @param listener
  584. * Listener to invoke after view changes.
  585. */
  586. public void addListener(ViewChangeListener listener) {
  587. listeners.add(listener);
  588. }
  589. /**
  590. * Remove a view change listener.
  591. *
  592. * @param listener
  593. * Listener to remove.
  594. */
  595. public void removeListener(ViewChangeListener listener) {
  596. listeners.remove(listener);
  597. }
  598. }