summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java8
-rw-r--r--documentation/advanced/advanced-navigator.asciidoc68
-rw-r--r--documentation/advanced/advanced-pushstate.asciidoc69
-rw-r--r--documentation/advanced/advanced-urifu.asciidoc7
-rw-r--r--documentation/application/application-declarative.asciidoc4
-rw-r--r--server/src/main/java/com/vaadin/navigator/Navigator.java117
-rw-r--r--server/src/main/java/com/vaadin/navigator/PushStateNavigation.java47
-rw-r--r--server/src/main/java/com/vaadin/server/Page.java6
-rw-r--r--server/src/main/java/com/vaadin/ui/UI.java81
-rw-r--r--server/src/test/java/com/vaadin/server/ConnectorResourceHandlerTest.java7
-rw-r--r--server/src/test/java/com/vaadin/server/VaadinSessionTest.java29
-rw-r--r--server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java107
-rw-r--r--server/src/test/java/com/vaadin/ui/UIInitRefreshTest.java14
-rw-r--r--server/src/test/java/com/vaadin/ui/UIThemeEscapingTest.java21
-rw-r--r--shared/src/main/java/com/vaadin/shared/ApplicationConstants.java9
-rw-r--r--uitest/src/main/java/com/vaadin/launcher/ApplicationRunnerServlet.java9
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/ui/PushStateAndReplaceState.java3
-rw-r--r--uitest/src/main/java/com/vaadin/tests/navigator/NavigatorViewBlocksBackButtonAction.java2
-rw-r--r--uitest/src/main/webapp/WEB-INF/web.xml14
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/combobox/ComboBoxPopupPositionEmbeddedInDivTest.java5
20 files changed, 561 insertions, 66 deletions
diff --git a/client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java b/client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java
index 5618cabb19..44f0b4253a 100644
--- a/client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java
+++ b/client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java
@@ -439,12 +439,14 @@ public class UIConnector extends AbstractSingleComponentContainerConnector
}
if (uidl.hasAttribute(UIConstants.ATTRIBUTE_PUSH_STATE)) {
- Browser.getWindow().getHistory().pushState(null, "",
+ Browser.getWindow().getHistory().pushState(null,
+ getState().pageState.title,
uidl.getStringAttribute(UIConstants.ATTRIBUTE_PUSH_STATE));
}
if (uidl.hasAttribute(UIConstants.ATTRIBUTE_REPLACE_STATE)) {
- Browser.getWindow().getHistory().replaceState(null, "", uidl
- .getStringAttribute(UIConstants.ATTRIBUTE_REPLACE_STATE));
+ Browser.getWindow().getHistory().replaceState(null,
+ getState().pageState.title, uidl.getStringAttribute(
+ UIConstants.ATTRIBUTE_REPLACE_STATE));
}
if (firstPaint) {
diff --git a/documentation/advanced/advanced-navigator.asciidoc b/documentation/advanced/advanced-navigator.asciidoc
index 7ef5b1ac36..00fedd635e 100644
--- a/documentation/advanced/advanced-navigator.asciidoc
+++ b/documentation/advanced/advanced-navigator.asciidoc
@@ -10,9 +10,9 @@ layout: page
Plain Vaadin applications do not have normal web page navigation as they usually
run on a single page, as all Ajax applications do. Quite commonly, however,
applications have different views between which the user should be able to
-navigate. The [classname]#Navigator# in Vaadin can be used for most cases of
-navigation. Views managed by the navigator automatically get a distinct URI
-fragment, which can be used to be able to bookmark the views and their states
+navigate. Also users often need to have direct links to specific views. The [classname]#Navigator# in Vaadin can be used for most cases of
+navigation. Views managed by the navigator automatically get a distinct URI,
+which can be used to be able to bookmark the views and their states
and to go back and forward in the browser history.
[[advanced.navigator.navigating]]
@@ -61,17 +61,9 @@ public class NavigatorUI extends UI {
}
----
-The [classname]#Navigator# automatically sets the URI fragment of the
-application URL. It also registers a [interfacename]#URIFragmentChangedListener#
-in the page
+The [classname]#Navigator# automatically parses the URI to identify and show the [interfacename]#View#. The browser navigation back and forward are also handled by it.
-ifdef::web[]
-(see <<dummy/../../../framework/advanced/advanced-urifu#advanced.urifu,"Managing
-URI
-Fragments">>)
-endif::web[]
- to show the view identified by the URI fragment if entered or navigated to in
-the browser. This also enables browser navigation history in the application.
+Starting from [literal]#++8.2.0.alpha2++# there is also PushState based navigation. This new way for the Navigator to manage URLs is described here as well. It is still under development so changes are expected. To enable this feature, add the [classname]#PushStateNavigation# annotation to your UI.
[[advanced.navigator.navigating.viewprovider]]
=== View Providers
@@ -95,7 +87,7 @@ You can handle view changes also by implementing a
[interfacename]#ViewChangeListener# and adding it to a [classname]#Navigator#.
When a view change occurs, a listener receives a [classname]#ViewChangeEvent#
object, which has references to the old and the activated view, the name of the
-activated view, as well as the fragment parameters.
+activated view, as well as the parameters (the part of of URI after the viewname).
@@ -104,7 +96,7 @@ activated view, as well as the fragment parameters.
Views can be any objects that implement the [interfacename]#View# interface.
When the [methodname]#navigateTo()# is called for the navigator, or the
-application is opened with the URI fragment associated with the view, the
+application is opened with the URI associated with the view, the
navigator switches to the view and calls its [methodname]#enter()# method.
To continue with the example, consider the following simple start view that just
@@ -143,21 +135,23 @@ latter method is that the view is attached to the view container as well as to
the UI at that time, which is not the case in the constructor.
-[[advanced.navigator.urifragment]]
-== Handling URI Fragment Path
+[[advanced.navigator.pathparam]]
+== Handling Path Parameters
-URI fragment part of a URL is the part after a hash [literal]#++#++# character.
-Is used for within-UI URLs, because it is the only part of the URL that can be
-changed with JavaScript from within a page without reloading the page. The URLs
-with URI fragments can be used for hyperlinking and bookmarking, as well as
-browser history, just like any other URLs. In addition, an exclamation mark
-[literal]#++#!++# after the hash marks that the page is a stateful AJAX page,
-which can be crawled by search engines. Crawling requires that the application
-also responds to special URLs to get the searchable content. URI fragments are
-managed by [classname]#Page#, which provides a low-level API.
+By default the URLs managed through the [classname]#Navigator# have a URI fragment
+that contains the identifier of the [interfacename]#View#. The URI fragment is
+separated from the rest of the URL by a [literal]#++#++# character.
-URI fragments can be used with [classname]#Navigator# in two ways: for
-navigating to a view and to a state within a view. The URI fragment accepted by
+If the [classname]#PushStateNavigation# annotation is present on the [classname]#UI#
+the HTML5 History API is used. When using the PushState, the identifier is separated
+from the root path by a [literal]#++/++# like a real URL.
+
+In addition to the View identifier, URI can contain additional parameters to be
+passed to views. The parameters are the part of the URI after the longest matching view identifier, separated by [literal]#++/++#. These parameters together with the identifier
+form the __navigation state__.
+
+The navigation state can be used with [classname]#Navigator# in two ways: for
+navigating to a view and to a state within a view. The navigation state accepted by
[methodname]#navigateTo()# can have the view name at the root, followed by
fragment parameters after a slash (" [literal]#++/++#"). These parameters are
passed to the [methodname]#enter()# method in the [interfacename]#View#.
@@ -202,7 +196,7 @@ public class MainView extends VerticalLayout implements View {
navigator.navigateTo(MAINVIEW + "/" + menuitem);
}
}
-
+
VerticalLayout menuContent;
Panel equalPanel;
Button logout;
@@ -224,19 +218,19 @@ public class MainView extends VerticalLayout implements View {
new ButtonListener("sheep")));
// Allow going back to the start
- logout.addClickListener(event -> // Java 8
+ logout.addClickListener(event ->
navigator.navigateTo(""));
- }
-
+ }
+
@DesignRoot
class AnimalViewer extends VerticalLayout {
Label watching;
Embedded pic;
Label back;
-
+
public AnimalViewer(String animal) {
Design.read(this);
-
+
watching.setValue("You are currently watching a " +
animal);
pic.setSource(new ThemeResource(
@@ -274,12 +268,8 @@ The animal sub-view would have the following declarative design:
----
The main view is shown in <<figure.advanced.navigator.mainview>>. At this point,
-the URL would be [literal]#++http://localhost:8080/myapp#!main/reindeer++#.
+the URL would be [literal]#++http://localhost:8080/myapp/main/reindeer++#.
[[figure.advanced.navigator.mainview]]
.Navigator Main View
image::img/navigator-mainview.png[]
-
-
-
-
diff --git a/documentation/advanced/advanced-pushstate.asciidoc b/documentation/advanced/advanced-pushstate.asciidoc
new file mode 100644
index 0000000000..a29ce865de
--- /dev/null
+++ b/documentation/advanced/advanced-pushstate.asciidoc
@@ -0,0 +1,69 @@
+---
+title: Mananipulating browser history
+order: 11
+layout: page
+---
+
+[[advanced.pushstate]]
+= Mananipulating browser history
+
+A major issue in AJAX applications is that as they run in a single web page.
+Bookmarking the application URL (or more generally the __URI__) can only
+bookmark the application, not an application state. This is a problem for many
+applications, such as product catalogs and discussion forums, in which it would
+be good to provide links to specific products or messages. The solution is to
+modify the URI of the browser using https://developer.mozilla.org/en-US/docs/Web/API/History_API[History APIs]
+[methodname]#pushState# or [methodname]#replaceState# functions, whenever developer
+wants to simulate a logical page change. There is a server side API for those
+methods and a mechanism to listen to changes in the client side URI in the
+[classname]#Page# object.
+
+Vaadin offers two ways to modify URIs: the high-level
+[classname]#Navigator# utility described in
+<<dummy/../../../framework/advanced/advanced-navigator#advanced.navigator,"Navigating
+in an Application">> and the low-level API described here.
+
+[[advanced.urifu.setting]]
+== Setting the URL displayed in the browser
+
+You can set the current fragment identifier with the
+[methodname]#pushState()# method in the [classname]#Page# object.
+
+
+[source, java]
+----
+Page.getCurrent().pushState("mars");
+----
+
+The parameter (both String and URI are supported) is resolved on the current URL. Both relative and absolute URIs are supported, but note that browsers accept only URLs of the same origin as the current URL.
+
+A call to _pushState_ creates a new entry to browsers history. If you wish to avoid this, and just replace the current URL in the browser, use the related [methodname]#replaceState# method.
+
+
+[[advanced.pushstate.popstate]]
+== Listening for "in-page" URI Changes
+
+If your application uses pushState to update the location and after that the user uses browsers back/forward button, a full page reload does not happen and the UIs init method is not called like when entering the page for the first time. To detect these change you can use [interfacename]#PopChangeListener#.
+
+For example, we could define the listener as follows in the [methodname]#init()#
+method of a UI class:
+
+
+[source, java]
+----
+public class MyUI extends UI {
+ @Override
+ protected void init(VaadinRequest request) {
+ getPage().addPopStateListener( e -> enter() );
+
+ // Read the initial URI fragment
+ enter();
+ }
+
+ void enter() {
+ URI location = getPage().getLocation();
+ ... initialize the UI ...
+ }
+}
+----
+
diff --git a/documentation/advanced/advanced-urifu.asciidoc b/documentation/advanced/advanced-urifu.asciidoc
index e245c8e727..e3b6b5c1ba 100644
--- a/documentation/advanced/advanced-urifu.asciidoc
+++ b/documentation/advanced/advanced-urifu.asciidoc
@@ -7,13 +7,16 @@ layout: page
[[advanced.urifu]]
= Managing URI Fragments
+NOTE: This chapter contains instructions how to manage URI fragments. As browser support for HTML5 History API has improved, developers should in most cases developers instead use real URIs with _pushState_ method. Read more from
+<<dummy/../../../framework/advanced/advanced-navigator#advanced.pushstate,"Manipulating Browser History">>.
+
A major issue in AJAX applications is that as they run in a single web page,
bookmarking the application URL (or more generally the __URI__) can only
bookmark the application, not an application state. This is a problem for many
applications, such as product catalogs and discussion forums, in which it would
be good to provide links to specific products or messages. Consequently, as
browsers remember the browsing history by URI, the history and the
-[guibutton]#Back# button do not normally work. The solution is to use the
+[guibutton]#Back# button do not normally work. The solution before HTML5 API was available was to use the
__fragment identifier__ part of the URI, which is separated from the primary
part (address + path + optional query parameters) of the URI with the hash (#)
character. For example:
@@ -31,7 +34,7 @@ the slash and the question mark.
Vaadin offers two ways to enable the use of URI fragments: the high-level
[classname]#Navigator# utility described in
<<dummy/../../../framework/advanced/advanced-navigator#advanced.navigator,"Navigating
-in an Application">> and the low-level API described here.
+in an Application">> (if the legacy [classname]#UriFragmentManager# is configured for the Navigator) and the low-level API described here.
[[advanced.urifu.setting]]
== Setting the URI Fragment
diff --git a/documentation/application/application-declarative.asciidoc b/documentation/application/application-declarative.asciidoc
index 9bffa4b371..c044b472ae 100644
--- a/documentation/application/application-declarative.asciidoc
+++ b/documentation/application/application-declarative.asciidoc
@@ -370,5 +370,5 @@ navigator.addView(MAINVIEW, new MainView());
----
See
-<<dummy/../../../framework/advanced/advanced-navigator#advanced.navigator.urifragment,"Handling
-URI Fragment Path">> for a complete example.
+<<dummy/../../../framework/advanced/advanced-navigator#advanced.navigator.pathparam,"Handling
+Path Parameters">> for a complete example.
diff --git a/server/src/main/java/com/vaadin/navigator/Navigator.java b/server/src/main/java/com/vaadin/navigator/Navigator.java
index 6b18cd4000..d65bad9db7 100644
--- a/server/src/main/java/com/vaadin/navigator/Navigator.java
+++ b/server/src/main/java/com/vaadin/navigator/Navigator.java
@@ -16,6 +16,7 @@
package com.vaadin.navigator;
import java.io.Serializable;
+import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -27,6 +28,7 @@ import java.util.Objects;
import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
import com.vaadin.server.Page;
+import com.vaadin.server.Page.PopStateEvent;
import com.vaadin.shared.Registration;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.ui.Component;
@@ -82,6 +84,95 @@ public class Navigator implements Serializable {
}
/**
+ * A {@link NavigationStateManager} using path info, HTML5 push state and
+ * {@link PopStateEvent}s to track views and enable listening to view
+ * changes. This manager can be enabled with UI annotation
+ * {@link PushStateNavigation}.
+ * <p>
+ * The part of path after UI's "root path" (UI's path without view
+ * identifier) is used as {@link View}s identifier. The rest of the path
+ * after the view name can be used by the developer for extra parameters for
+ * the View.
+ * <p>
+ * This class is mostly for internal use by Navigator, and is only public
+ * and static to enable testing.
+ *
+ * @since 8.2
+ */
+ public static class PushStateManager implements NavigationStateManager {
+ private Registration popStateListenerRegistration;
+ private UI ui;
+
+ /**
+ * Creates a new PushStateManager.
+ *
+ * @param ui
+ * the UI where the Navigator is attached to
+ */
+ public PushStateManager(UI ui) {
+ this.ui = ui;
+ }
+
+ @Override
+ public void setNavigator(Navigator navigator) {
+ if (popStateListenerRegistration != null) {
+ popStateListenerRegistration.remove();
+ popStateListenerRegistration = null;
+ }
+ if (navigator != null) {
+ popStateListenerRegistration = ui.getPage()
+ .addPopStateListener(e -> {
+ navigator.navigateTo(getState());
+ });
+ }
+ }
+
+ @Override
+ public String getState() {
+ // Get the current URL
+ URI location = ui.getPage().getLocation();
+ String path = location.getPath();
+ if (ui.getUiPathInfo() != null
+ && path.contains(ui.getUiPathInfo())) {
+ // Split the path from after the UI PathInfo
+ path = path.substring(path.indexOf(ui.getUiPathInfo())
+ + ui.getUiPathInfo().length());
+ } else if (path.startsWith(ui.getUiRootPath())) {
+ // Use the whole path after UI RootPath
+ String uiRootPath = ui.getUiRootPath();
+ path = path.substring(uiRootPath.length());
+ } else {
+ throw new IllegalStateException(getClass().getSimpleName()
+ + " is unable to determine the view path from the URL.");
+ }
+
+ if (path.startsWith("/")) {
+ // Strip leading '/'
+ path = path.substring(1);
+ }
+ return path;
+ }
+
+ @Override
+ public void setState(String state) {
+ StringBuilder sb = new StringBuilder(ui.getUiRootPath());
+ if (!ui.getUiRootPath().endsWith("/")) {
+ // make sure there is a '/' between the root path and the
+ // navigation state.
+ sb.append("/");
+ }
+ sb.append(state);
+ URI location = ui.getPage().getLocation();
+ if (location != null) {
+ ui.getPage().pushState(location.resolve(sb.toString()));
+ } else {
+ throw new IllegalStateException(
+ "The Page of the UI does not have a location.");
+ }
+ }
+ }
+
+ /**
* A {@link NavigationStateManager} using hashbang fragments in the Page
* location URI to track views and enable listening to view changes.
* <p>
@@ -92,6 +183,10 @@ public class Navigator implements Serializable {
* <p>
* This class is mostly for internal use by Navigator, and is only public
* and static to enable testing.
+ * <p>
+ * <strong>Note:</strong> Since 8.2 you can use {@link PushStateManager},
+ * which is based on HTML5 History API. To use it, add
+ * {@link PushStateNavigation} annotation to the UI.
*/
public static class UriFragmentManager implements NavigationStateManager {
private final Page page;
@@ -426,7 +521,7 @@ public class Navigator implements Serializable {
* The ViewDisplay used to display the views.
*/
public Navigator(UI ui, ViewDisplay display) {
- this(ui, new UriFragmentManager(ui.getPage()), display);
+ this(ui, null, display);
}
/**
@@ -494,7 +589,7 @@ public class Navigator implements Serializable {
this.ui = ui;
this.ui.setNavigator(this);
if (stateManager == null) {
- stateManager = new UriFragmentManager(ui.getPage());
+ stateManager = createNavigationStateManager(ui);
}
if (stateManager != null && this.stateManager != null
&& stateManager != this.stateManager) {
@@ -506,6 +601,24 @@ public class Navigator implements Serializable {
}
/**
+ * Creates a navigation state manager for given UI. This method should take
+ * into account any navigation related annotations.
+ *
+ * @param ui
+ * the ui
+ * @return the navigation state manager
+ *
+ * @since 8.2
+ */
+ protected NavigationStateManager createNavigationStateManager(UI ui) {
+ if (ui.getClass().getAnnotation(PushStateNavigation.class) != null) {
+ return new PushStateManager(ui);
+ }
+ // Fall back to old default
+ return new UriFragmentManager(ui.getPage());
+ }
+
+ /**
* Navigates to a view and initialize the view with given parameters.
* <p>
* The view string consists of a view name optionally followed by a slash
diff --git a/server/src/main/java/com/vaadin/navigator/PushStateNavigation.java b/server/src/main/java/com/vaadin/navigator/PushStateNavigation.java
new file mode 100644
index 0000000000..f1cab0391b
--- /dev/null
+++ b/server/src/main/java/com/vaadin/navigator/PushStateNavigation.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.navigator;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import com.vaadin.server.DeploymentConfiguration;
+import com.vaadin.server.Page.PopStateEvent;
+import com.vaadin.ui.UI;
+
+/**
+ * Annotation for {@link UI}s to enable the PushState navigation mode when
+ * initializing a {@link Navigator} for it. PushState navigation is an
+ * alternative way to handle URLs in the {@link Navigator}. It uses path info,
+ * HTML5 push state and {@link PopStateEvent}s to track views and enable
+ * listening to view changes.
+ * <p>
+ * <strong>Note:</strong> For PushState navigation to work, the
+ * {@link DeploymentConfiguration} parameter
+ * {@link DeploymentConfiguration#isSendUrlsAsParameters() SendUrlAsParameters}
+ * must not be set to {@code false}.
+ *
+ * @since 8.2
+ */
+@Retention(RUNTIME)
+@Target(TYPE)
+@Inherited
+public @interface PushStateNavigation {
+}
diff --git a/server/src/main/java/com/vaadin/server/Page.java b/server/src/main/java/com/vaadin/server/Page.java
index 4c880c2a6b..3e95de0c4b 100644
--- a/server/src/main/java/com/vaadin/server/Page.java
+++ b/server/src/main/java/com/vaadin/server/Page.java
@@ -1035,8 +1035,12 @@ public class Page implements Serializable {
* deployed in due to potential proxies, redirections and similar.
*
* @return The browser location URI.
+ * @throws IllegalStateException
+ * if the
+ * {@link DeploymentConfiguration#isSendUrlsAsParameters()} is
+ * set to {@code false}
*/
- public URI getLocation() {
+ public URI getLocation() throws IllegalStateException {
if (location == null && !uI.getSession().getConfiguration()
.isSendUrlsAsParameters()) {
throw new IllegalStateException("Location is not available as the "
diff --git a/server/src/main/java/com/vaadin/ui/UI.java b/server/src/main/java/com/vaadin/ui/UI.java
index ac4815f92d..5d27c1de74 100644
--- a/server/src/main/java/com/vaadin/ui/UI.java
+++ b/server/src/main/java/com/vaadin/ui/UI.java
@@ -44,6 +44,7 @@ import com.vaadin.event.UIEvents.PollEvent;
import com.vaadin.event.UIEvents.PollListener;
import com.vaadin.event.UIEvents.PollNotifier;
import com.vaadin.navigator.Navigator;
+import com.vaadin.navigator.PushStateNavigation;
import com.vaadin.server.ClientConnector;
import com.vaadin.server.ComponentSizeValidator;
import com.vaadin.server.ComponentSizeValidator.InvalidLayout;
@@ -196,7 +197,6 @@ public abstract class UI extends AbstractSingleComponentContainer
@Override
public void popstate(String uri) {
getPage().updateLocation(uri, true, true);
-
}
};
private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() {
@@ -261,8 +261,7 @@ public abstract class UI extends AbstractSingleComponentContainer
private WindowOrderRpc windowOrderRpc = new WindowOrderRpc() {
@Override
- public void windowOrderChanged(
- Map<Integer, Connector> windowOrders) {
+ public void windowOrderChanged(Map<Integer, Connector> windowOrders) {
Map<Integer, Window> orders = new LinkedHashMap<>();
for (Entry<Integer, Connector> entry : windowOrders.entrySet()) {
if (entry.getValue() instanceof Window) {
@@ -661,6 +660,10 @@ public abstract class UI extends AbstractSingleComponentContainer
private String embedId;
+ private String uiPathInfo;
+
+ private String uiRootPath;
+
private boolean mobileHtml5DndPolyfillLoaded;
/**
@@ -740,6 +743,36 @@ public abstract class UI extends AbstractSingleComponentContainer
getPage().init(request);
+ String uiPathInfo = (String) request
+ .getAttribute(ApplicationConstants.UI_ROOT_PATH);
+ if (uiPathInfo != null) {
+ setUiPathInfo(uiPathInfo);
+ }
+
+ if (getSession() != null && getSession().getConfiguration() != null
+ && getSession().getConfiguration().isSendUrlsAsParameters()
+ && getPage().getLocation() != null) {
+ // By default the root is the URL from client
+ String uiRootPath = getPage().getLocation().getPath();
+
+ if (uiPathInfo != null && uiRootPath.contains(uiPathInfo)) {
+ // String everything from the URL after uiPathInfo
+ // This will remove the navigation state from the URL
+ uiRootPath = uiRootPath.substring(0,
+ uiRootPath.indexOf(uiPathInfo) + uiPathInfo.length());
+ } else if (request.getPathInfo() != null) {
+ // uiRootPath does not match the uiPathInfo
+ // This can happen for example when embedding a Vaadin UI
+ String pathInfo = request.getPathInfo();
+ if (uiRootPath.endsWith(pathInfo)) {
+ uiRootPath = uiRootPath.substring(0,
+ uiRootPath.length() - pathInfo.length());
+ }
+ }
+ // Store the URL as the UI Root Path
+ setUiRootPath(uiRootPath);
+ }
+
// Call the init overridden by the application developer
init(request);
@@ -750,6 +783,48 @@ public abstract class UI extends AbstractSingleComponentContainer
}
}
+ private void setUiRootPath(String uiRootPath) {
+ this.uiRootPath = uiRootPath;
+ }
+
+ /**
+ * Gets the part of path (from browser's URL) that points to this UI.
+ * Basically the same as the value from {@link Page#getLocation()}, but
+ * without possible view identifiers or path parameters.
+ *
+ * @return the part of path (from browser's URL) that points to this UI,
+ * without possible view identifiers or path parameters
+ *
+ * @since 8.2
+ */
+ public String getUiRootPath() {
+ return uiRootPath;
+ }
+
+ private void setUiPathInfo(String uiPathInfo) {
+ this.uiPathInfo = uiPathInfo;
+ }
+
+ /**
+ * Gets the path info part of the request that is used to detect the UI.
+ * This is defined during UI init by certain {@link UIProvider UIProviders}
+ * that map different UIs to different URIs, like Vaadin Spring. This
+ * information is used by the {@link Navigator} when the {@link UI} is
+ * annotated with {@link PushStateNavigation}.
+ * <p>
+ * For example if the UI is accessed through
+ * {@code http://example.com/MyUI/mainview/parameter=1} the path info would
+ * be {@code /MyUI}.
+ *
+ * @return the path info part of the request; {@code null} if no request
+ * from client has been processed
+ *
+ * @since 8.2
+ */
+ public String getUiPathInfo() {
+ return uiPathInfo;
+ }
+
/**
* Initializes this UI. This method is intended to be overridden by
* subclasses to build the view and configure non-component functionality.
diff --git a/server/src/test/java/com/vaadin/server/ConnectorResourceHandlerTest.java b/server/src/test/java/com/vaadin/server/ConnectorResourceHandlerTest.java
index fd7dc4291d..57b8ed8ff4 100644
--- a/server/src/test/java/com/vaadin/server/ConnectorResourceHandlerTest.java
+++ b/server/src/test/java/com/vaadin/server/ConnectorResourceHandlerTest.java
@@ -39,10 +39,14 @@ public class ConnectorResourceHandlerTest {
request = control.createMock(VaadinRequest.class);
response = control.createMock(VaadinResponse.class);
+ DeploymentConfiguration dc = control
+ .createMock(DeploymentConfiguration.class);
VaadinService service = control.createMock(VaadinService.class);
EasyMock.expect(request.getPathInfo())
.andReturn("/APP/connector/0/1/2");
+ EasyMock.expect(request.getParameter("v-loc"))
+ .andReturn("http://localhost/");
control.replay();
@@ -53,13 +57,14 @@ public class ConnectorResourceHandlerTest {
protected void init(VaadinRequest request) {
}
};
- ui.doInit(request, 0, "");
session.lock();
try {
+ session.setConfiguration(dc);
session.setCommunicationManager(
new LegacyCommunicationManager(session));
ui.setSession(session);
+ ui.doInit(request, 0, "");
session.addUI(ui);
} finally {
session.unlock();
diff --git a/server/src/test/java/com/vaadin/server/VaadinSessionTest.java b/server/src/test/java/com/vaadin/server/VaadinSessionTest.java
index 5b5528e8e3..a3d0746149 100644
--- a/server/src/test/java/com/vaadin/server/VaadinSessionTest.java
+++ b/server/src/test/java/com/vaadin/server/VaadinSessionTest.java
@@ -32,6 +32,7 @@ import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -96,12 +97,22 @@ public class VaadinSessionTest implements Serializable {
}
};
+ IMocksControl control = EasyMock.createNiceControl();
+ DeploymentConfiguration dc = control
+ .createMock(DeploymentConfiguration.class);
+
session = new VaadinSession(mockService);
+
mockService.storeSession(session, mockWrappedSession);
ui = new MockPageUI();
- vaadinRequest = new VaadinServletRequest(
- EasyMock.createMock(HttpServletRequest.class), mockService) {
+ HttpServletRequest request = control
+ .createMock(HttpServletRequest.class);
+ EasyMock.expect(request.getParameter("v-loc"))
+ .andReturn("http://localhost/");
+ control.replay();
+
+ vaadinRequest = new VaadinServletRequest(request, mockService) {
@Override
public String getParameter(String name) {
if ("theme".equals(name) || "restartApplication".equals(name)
@@ -116,6 +127,14 @@ public class VaadinSessionTest implements Serializable {
}
@Override
+ public Object getAttribute(String name) {
+ if (name.equals("com.vaadin.server.UI_ROOT_PATH")) {
+ return "/";
+ }
+ return super.getAttribute(name);
+ }
+
+ @Override
public String getMethod() {
return "POST";
}
@@ -128,9 +147,10 @@ public class VaadinSessionTest implements Serializable {
};
+ session.setConfiguration(dc);
+ ui.setSession(session);
ui.doInit(vaadinRequest, session.getNextUIid(), null);
- ui.setSession(session);
session.addUI(ui);
}
@@ -240,9 +260,6 @@ public class VaadinSessionTest implements Serializable {
// VaadinSessionTest.this which isn't serializable
private static class MockPageUI extends UI {
Page page = new Page(this, getState(false).pageState) {
- @Override
- public void init(VaadinRequest request) {
- }
};
@Override
diff --git a/server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java b/server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java
index 636c226e66..019c13698d 100644
--- a/server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java
+++ b/server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java
@@ -36,6 +36,7 @@ import org.junit.Test;
import com.vaadin.navigator.NavigationStateManager;
import com.vaadin.navigator.Navigator;
+import com.vaadin.navigator.PushStateNavigation;
import com.vaadin.navigator.View;
import com.vaadin.navigator.ViewBeforeLeaveEvent;
import com.vaadin.navigator.ViewChangeListener;
@@ -138,6 +139,31 @@ public class NavigatorTest {
}
}
+ public static class TestNavigatorWithFragments extends Navigator {
+ public TestNavigatorWithFragments() {
+ super(createMockUI(), new NullFragmentManager(), new TestDisplay());
+ }
+
+ public TestNavigatorWithFragments(UI ui) {
+ super(ui, new UriFragmentManager(ui.getPage()),
+ EasyMock.createMock(ViewDisplay.class));
+ }
+
+ public View getView(String viewAndParameters) {
+ try {
+ navigateTo(viewAndParameters);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ return ((TestDisplay) getDisplay()).getCurrentView();
+ }
+
+ @Override
+ protected NavigationStateManager getStateManager() {
+ return super.getStateManager();
+ }
+ }
+
public static class ViewChangeTestListener implements ViewChangeListener {
private final LinkedList<ViewChangeEvent> referenceEvents = new LinkedList<>();
private final LinkedList<Boolean> referenceIsCheck = new LinkedList<>();
@@ -244,6 +270,14 @@ public class NavigatorTest {
private final Page page;
}
+ @PushStateNavigation
+ private static class TestPushStateUI extends TestUI {
+
+ TestPushStateUI(Page page) {
+ super(page);
+ }
+ }
+
private static class TestPage extends Page {
public TestPage() {
@@ -251,6 +285,31 @@ public class NavigatorTest {
}
@Override
+ public Registration addPopStateListener(PopStateListener listener) {
+ addPopstateCalled = true;
+ return () -> removePopstateCalled = true;
+ }
+
+ boolean addPopstateCalled() {
+ return addPopstateCalled;
+ }
+
+ boolean removePopstateCalled() {
+ return removePopstateCalled;
+ }
+
+ private boolean addPopstateCalled;
+
+ private boolean removePopstateCalled;
+ }
+
+ private static class TestPageWithUriFragments extends Page {
+
+ public TestPageWithUriFragments() {
+ super(null, null);
+ }
+
+ @Override
public Registration addUriFragmentChangedListener(
UriFragmentChangedListener listener) {
addUriFragmentCalled = true;
@@ -304,12 +363,13 @@ public class NavigatorTest {
return new Navigator(createMockUI(), manager, display);
}
- @Test(expected = NullPointerException.class)
+ @Test
public void testDestroy_unsetNavigatorInUIAndUriFragmentManager() {
- TestPage page = new TestPage();
+ TestPageWithUriFragments page = new TestPageWithUriFragments();
UI ui = new TestUI(page);
- TestNavigator navigator = new TestNavigator(ui);
+ TestNavigatorWithFragments navigator = new TestNavigatorWithFragments(
+ ui);
Assert.assertTrue("Add URI fragment Page method has not been called",
page.addUriFragmentCalled());
Assert.assertFalse("Unexpected remove URI fragment Page method call",
@@ -323,11 +383,44 @@ public class NavigatorTest {
Assert.assertNull("Navigator is not null in UI after destroy",
ui.getNavigator());
- page.setUriFragment("foobar", true);
+ try {
+ page.setUriFragment("foobar", true); // This should throw
+ Assert.fail(
+ "Expected null pointer exception after call uriFragmentChanged "
+ + "for destroyed navigator");
+ } catch (NullPointerException e) {
+ // All ok.
+ }
+ }
+
+ @Test
+ public void testDestroy_unsetNavigatorInUIAndPopstateManager() {
+ TestPage page = new TestPage();
+ UI ui = new TestPushStateUI(page);
+
+ TestNavigator navigator = new TestNavigator(ui);
+ Assert.assertTrue("Add URI fragment Page method has not been called",
+ page.addPopstateCalled());
+ Assert.assertFalse("Unexpected remove URI fragment Page method call",
+ page.removePopstateCalled());
+ Assert.assertNotNull("Navigator is null in UI", ui.getNavigator());
+
+ navigator.destroy();
+ Assert.assertTrue(
+ "Remove URI fragment Page method has not been called after destroy",
+ page.removePopstateCalled());
+ Assert.assertNull("Navigator is not null in UI after destroy",
+ ui.getNavigator());
+
+ try {
+ page.updateLocation("http://server/path/info", true, true);
- Assert.fail(
- "Expected null pointer exception after call uriFragmentChanged "
- + "for destroyed navigator");
+ Assert.fail(
+ "Expected null pointer exception after call uriFragmentChanged "
+ + "for destroyed navigator");
+ } catch (NullPointerException e) {
+ // All ok.
+ }
}
@Test
diff --git a/server/src/test/java/com/vaadin/ui/UIInitRefreshTest.java b/server/src/test/java/com/vaadin/ui/UIInitRefreshTest.java
index c4a44d85a6..65bbc59a5b 100644
--- a/server/src/test/java/com/vaadin/ui/UIInitRefreshTest.java
+++ b/server/src/test/java/com/vaadin/ui/UIInitRefreshTest.java
@@ -15,17 +15,21 @@
*/
package com.vaadin.ui;
+import java.util.Locale;
+
import org.easymock.EasyMock;
import org.easymock.IMocksControl;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import com.vaadin.server.DeploymentConfiguration;
import com.vaadin.server.Page.BrowserWindowResizeEvent;
import com.vaadin.server.Page.BrowserWindowResizeListener;
import com.vaadin.server.Page.UriFragmentChangedEvent;
import com.vaadin.server.Page.UriFragmentChangedListener;
import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinSession;
public class UIInitRefreshTest {
@@ -87,6 +91,7 @@ public class UIInitRefreshTest {
IMocksControl control = EasyMock.createNiceControl();
VaadinRequest initRequest = control.createMock(VaadinRequest.class);
+
EasyMock.expect(initRequest.getParameter("v-loc"))
.andReturn("http://example.com/#foo");
EasyMock.expect(initRequest.getParameter("v-cw")).andReturn("100");
@@ -98,9 +103,18 @@ public class UIInitRefreshTest {
EasyMock.expect(reinitRequest.getParameter("v-cw")).andReturn("200");
EasyMock.expect(reinitRequest.getParameter("v-ch")).andReturn("200");
+ VaadinSession session = control.createMock(VaadinSession.class);
+ DeploymentConfiguration dc = control
+ .createMock(DeploymentConfiguration.class);
+
+ EasyMock.expect(session.hasLock()).andStubReturn(true);
+ EasyMock.expect(session.getConfiguration()).andStubReturn(dc);
+ EasyMock.expect(session.getLocale()).andStubReturn(Locale.getDefault());
+
control.replay();
UI ui = new TestUI();
+ ui.setSession(session);
ui.doInit(initRequest, 0, "");
Assert.assertTrue(initCalled);
diff --git a/server/src/test/java/com/vaadin/ui/UIThemeEscapingTest.java b/server/src/test/java/com/vaadin/ui/UIThemeEscapingTest.java
index 7c19694eec..656dd96b30 100644
--- a/server/src/test/java/com/vaadin/ui/UIThemeEscapingTest.java
+++ b/server/src/test/java/com/vaadin/ui/UIThemeEscapingTest.java
@@ -21,10 +21,16 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import java.util.Locale;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
import org.junit.Before;
import org.junit.Test;
+import com.vaadin.server.DeploymentConfiguration;
import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinSession;
public class UIThemeEscapingTest {
@@ -33,13 +39,28 @@ public class UIThemeEscapingTest {
private void initUiWithTheme(String theme) {
VaadinRequest request = getRequestWithTheme(theme);
+ IMocksControl control = EasyMock.createNiceControl();
+ VaadinSession session = control.createMock(VaadinSession.class);
+ DeploymentConfiguration dc = control
+ .createMock(DeploymentConfiguration.class);
+
+ EasyMock.expect(session.hasLock()).andStubReturn(true);
+ EasyMock.expect(session.getConfiguration()).andStubReturn(dc);
+ EasyMock.expect(session.getLocale()).andStubReturn(Locale.getDefault());
+
+ control.replay();
+
+ ui.setSession(session);
+ ui.getPage().init(request);
ui.doInit(request, 1234, "foobar");
}
private VaadinRequest getRequestWithTheme(String theme) {
VaadinRequest request = mock(VaadinRequest.class);
+ // when(request.getParameter())
when(request.getParameter("theme")).thenReturn(theme);
+ when(request.getParameter("v-loc")).thenReturn("http://localhost/");
return request;
}
diff --git a/shared/src/main/java/com/vaadin/shared/ApplicationConstants.java b/shared/src/main/java/com/vaadin/shared/ApplicationConstants.java
index b3490a434c..4ce1ee0ac0 100644
--- a/shared/src/main/java/com/vaadin/shared/ApplicationConstants.java
+++ b/shared/src/main/java/com/vaadin/shared/ApplicationConstants.java
@@ -206,6 +206,15 @@ public class ApplicationConstants implements Serializable {
public static final String WIDGETSET_VERSION_ID = "wsver";
/**
+ * A request attribute name to store the part of pathInfo that was used to
+ * select the UI. Will be used by default Navigator to separate view
+ * identifiers from UI. This can be set by custom UI providers.
+ *
+ * @since 8.2
+ **/
+ public static final String UI_ROOT_PATH = "com.vaadin.server.UI_ROOT_PATH";
+
+ /**
* Content type to use for text/html responses (should always be UTF-8).
*/
public static final String CONTENT_TYPE_TEXT_HTML_UTF_8 = "text/html; charset=utf-8";
diff --git a/uitest/src/main/java/com/vaadin/launcher/ApplicationRunnerServlet.java b/uitest/src/main/java/com/vaadin/launcher/ApplicationRunnerServlet.java
index 6835b5fb0b..97689033d5 100644
--- a/uitest/src/main/java/com/vaadin/launcher/ApplicationRunnerServlet.java
+++ b/uitest/src/main/java/com/vaadin/launcher/ApplicationRunnerServlet.java
@@ -52,6 +52,7 @@ import com.vaadin.server.SystemMessages;
import com.vaadin.server.SystemMessagesInfo;
import com.vaadin.server.SystemMessagesProvider;
import com.vaadin.server.UIClassSelectionEvent;
+import com.vaadin.server.UICreateEvent;
import com.vaadin.server.UIProvider;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
@@ -59,6 +60,7 @@ import com.vaadin.server.VaadinServlet;
import com.vaadin.server.VaadinServletRequest;
import com.vaadin.server.VaadinServletService;
import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.ApplicationConstants;
import com.vaadin.tests.components.TestBase;
import com.vaadin.ui.UI;
import com.vaadin.util.CurrentInstance;
@@ -267,6 +269,13 @@ public class ApplicationRunnerServlet extends LegacyVaadinServlet {
public Class<? extends UI> getUIClass(UIClassSelectionEvent event) {
return (Class<? extends UI>) classToRun;
}
+
+ @Override
+ public UI createInstance(UICreateEvent event) {
+ event.getRequest().setAttribute(ApplicationConstants.UI_ROOT_PATH,
+ "/" + event.getUIClass().getName());
+ return super.createInstance(event);
+ }
}
// TODO Don't need to use a data object now that there's only one field
diff --git a/uitest/src/main/java/com/vaadin/tests/components/ui/PushStateAndReplaceState.java b/uitest/src/main/java/com/vaadin/tests/components/ui/PushStateAndReplaceState.java
index 22788f326c..962bcb95ca 100644
--- a/uitest/src/main/java/com/vaadin/tests/components/ui/PushStateAndReplaceState.java
+++ b/uitest/src/main/java/com/vaadin/tests/components/ui/PushStateAndReplaceState.java
@@ -2,6 +2,7 @@ package com.vaadin.tests.components.ui;
import java.net.URI;
+import com.vaadin.annotations.Title;
import com.vaadin.server.Page;
import com.vaadin.server.Page.PopStateEvent;
import com.vaadin.server.Page.PopStateListener;
@@ -13,6 +14,7 @@ import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Label;
import com.vaadin.ui.Notification;
+@Title("Original title")
public class PushStateAndReplaceState extends AbstractReindeerTestUI {
private final Label locationLabel = new Label();
@@ -48,6 +50,7 @@ public class PushStateAndReplaceState extends AbstractReindeerTestUI {
Button button = new Button(caption, new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
+ getPage().setTitle(caption);
if (replace.getValue()) {
getPage().replaceState(newUri);
} else {
diff --git a/uitest/src/main/java/com/vaadin/tests/navigator/NavigatorViewBlocksBackButtonAction.java b/uitest/src/main/java/com/vaadin/tests/navigator/NavigatorViewBlocksBackButtonAction.java
index 60be2f6e74..9a62729bd3 100644
--- a/uitest/src/main/java/com/vaadin/tests/navigator/NavigatorViewBlocksBackButtonAction.java
+++ b/uitest/src/main/java/com/vaadin/tests/navigator/NavigatorViewBlocksBackButtonAction.java
@@ -1,6 +1,7 @@
package com.vaadin.tests.navigator;
import com.vaadin.navigator.Navigator;
+import com.vaadin.navigator.PushStateNavigation;
import com.vaadin.navigator.View;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
@@ -13,6 +14,7 @@ import com.vaadin.ui.Label;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window;
+@PushStateNavigation
public class NavigatorViewBlocksBackButtonAction
extends AbstractReindeerTestUI {
diff --git a/uitest/src/main/webapp/WEB-INF/web.xml b/uitest/src/main/webapp/WEB-INF/web.xml
index b1ec909d21..1771a0bd98 100644
--- a/uitest/src/main/webapp/WEB-INF/web.xml
+++ b/uitest/src/main/webapp/WEB-INF/web.xml
@@ -151,6 +151,20 @@
<async-supported>true</async-supported>
</servlet>
+ <servlet>
+ <servlet-name>Navigator test</servlet-name>
+ <servlet-class>com.vaadin.server.VaadinServlet</servlet-class>
+ <init-param>
+ <param-name>UI</param-name>
+ <param-value>com.vaadin.tests.navigator.NavigatorViewBlocksBackButtonAction</param-value>
+ </init-param>
+ <async-supported>true</async-supported>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>Navigator test</servlet-name>
+ <url-pattern>/navigator-test/*</url-pattern>
+ </servlet-mapping>
<servlet-mapping>
<servlet-name>Embed App 1</servlet-name>
<url-pattern>/embed1/*</url-pattern>
diff --git a/uitest/src/test/java/com/vaadin/tests/components/combobox/ComboBoxPopupPositionEmbeddedInDivTest.java b/uitest/src/test/java/com/vaadin/tests/components/combobox/ComboBoxPopupPositionEmbeddedInDivTest.java
index 2845283c7a..682712dcbb 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/combobox/ComboBoxPopupPositionEmbeddedInDivTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/combobox/ComboBoxPopupPositionEmbeddedInDivTest.java
@@ -49,4 +49,9 @@ public class ComboBoxPopupPositionEmbeddedInDivTest extends MultiBrowserTest {
Assert.assertTrue("Popup should be left aligned with the combobox",
popupLocation.getX() == comboboxLocation.getX());
}
+
+ @Override
+ protected Class<?> getUIClass() {
+ return ComboBoxEmbeddedInDiv.class;
+ }
}