diff --git a/.gitignore b/.gitignore
index 67d0e3a1d66..92027547aaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,7 +43,6 @@ flow-tests/**/package.json
flow-tests/**/webpack*.js
flow-tests/**/tsconfig.json
flow-tests/**/pnpm-lock.yaml
-flow-tests/**/types.d.ts
yarn.lock
flow-client/src/main/resources/META-INF/resources/frontend/FlowClient.js
diff --git a/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java b/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java
index ea038c27d6e..c3ac9542258 100644
--- a/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java
+++ b/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java
@@ -20,8 +20,6 @@
import com.google.web.bindery.event.shared.UmbrellaException;
-import com.google.gwt.core.client.Scheduler;
-
import com.vaadin.client.bootstrap.ErrorMessage;
import elemental.client.Browser;
@@ -58,12 +56,8 @@ public SystemErrorHandler(Registry registry) {
* message details or null if there are no details
*/
public void handleSessionExpiredError(String details) {
- // Run asynchronously to guarantee that all executions in the Uidl are
- // done (#7581)
- Scheduler.get()
- .scheduleDeferred(() -> handleUnrecoverableError(details,
- registry.getApplicationConfiguration()
- .getSessionExpiredError()));
+ handleUnrecoverableError(details, registry.getApplicationConfiguration()
+ .getSessionExpiredError());
}
/**
diff --git a/flow-client/src/main/java/com/vaadin/client/WidgetUtil.java b/flow-client/src/main/java/com/vaadin/client/WidgetUtil.java
index 5a251769c03..f519085ed3e 100644
--- a/flow-client/src/main/java/com/vaadin/client/WidgetUtil.java
+++ b/flow-client/src/main/java/com/vaadin/client/WidgetUtil.java
@@ -126,7 +126,7 @@ public static String toPrettyJson(JsonValue json) {
* {@code value}.
*
* If {@code value} is {@code null} then {@code attribute} is removed,
- * otherwise {@code value.toString()} is set as its value.
+ * otherwise {@code value} is set as its value.
*
* @param element
* the DOM element owning attribute
@@ -136,11 +136,11 @@ public static String toPrettyJson(JsonValue json) {
* the value to update
*/
public static void updateAttribute(Element element, String attribute,
- Object value) {
+ String value) {
if (value == null) {
DomApi.wrap(element).removeAttribute(attribute);
} else {
- DomApi.wrap(element).setAttribute(attribute, value.toString());
+ DomApi.wrap(element).setAttribute(attribute, value);
}
}
@@ -227,7 +227,7 @@ public static native boolean hasJsProperty(Object object, String name)
/**
* Checks if the given value is explicitly undefined. null
* values returns false.
- *
+ *
* @param property
* the value to be verified
* @return true is the value is explicitly undefined,
diff --git a/flow-client/src/main/java/com/vaadin/client/flow/binding/SimpleElementBindingStrategy.java b/flow-client/src/main/java/com/vaadin/client/flow/binding/SimpleElementBindingStrategy.java
index 45af2f858c1..22c679852dc 100644
--- a/flow-client/src/main/java/com/vaadin/client/flow/binding/SimpleElementBindingStrategy.java
+++ b/flow-client/src/main/java/com/vaadin/client/flow/binding/SimpleElementBindingStrategy.java
@@ -24,6 +24,7 @@
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
+import com.vaadin.client.ApplicationConfiguration;
import com.vaadin.client.Command;
import com.vaadin.client.Console;
import com.vaadin.client.ElementUtil;
@@ -289,9 +290,9 @@ private native void bindPolymerModelProperties(StateNode node,
private native void hookUpPolymerElement(StateNode node, Element element)
/*-{
var self = this;
-
+
var originalPropertiesChanged = element._propertiesChanged;
-
+
if (originalPropertiesChanged) {
element._propertiesChanged = function (currentProps, changedProps, oldProps) {
$entry(function () {
@@ -300,16 +301,16 @@ private native void hookUpPolymerElement(StateNode node, Element element)
originalPropertiesChanged.apply(this, arguments);
};
}
-
-
+
+
var tree = node.@com.vaadin.client.flow.StateNode::getTree()();
-
+
var originalReady = element.ready;
-
+
element.ready = function (){
originalReady.apply(this, arguments);
@com.vaadin.client.PolymerUtils::fireReadyEvent(*)(element);
-
+
// The _propertiesChanged method which is replaced above for the element
// doesn't do anything for items in dom-repeat.
// Instead it's called with some meaningful info for the dom-repeat element.
@@ -318,7 +319,7 @@ private native void hookUpPolymerElement(StateNode node, Element element)
// which changes this method for any dom-repeat instance.
var replaceDomRepeatPropertyChange = function(){
var domRepeat = element.root.querySelector('dom-repeat');
-
+
if ( domRepeat ){
// If the dom-repeat element is in the DOM then
// this method should not be executed anymore. The logic below will replace
@@ -332,12 +333,12 @@ private native void hookUpPolymerElement(StateNode node, Element element)
// if dom-repeat is found => replace _propertiesChanged method in the prototype and mark it as replaced.
if ( !domRepeat.constructor.prototype.$propChangedModified){
domRepeat.constructor.prototype.$propChangedModified = true;
-
+
var changed = domRepeat.constructor.prototype._propertiesChanged;
-
+
domRepeat.constructor.prototype._propertiesChanged = function(currentProps, changedProps, oldProps){
changed.apply(this, arguments);
-
+
var props = Object.getOwnPropertyNames(changedProps);
var items = "items.";
var i;
@@ -358,7 +359,7 @@ private native void hookUpPolymerElement(StateNode node, Element element)
if( currentPropsItem && currentPropsItem.nodeId ){
var nodeId = currentPropsItem.nodeId;
var value = currentPropsItem[propertyName];
-
+
// this is an attempt to find the template element
// which is not available as a context in the protype method
var host = this.__dataHost;
@@ -369,7 +370,7 @@ private native void hookUpPolymerElement(StateNode node, Element element)
while( !host.localName || host.__dataHost ){
host = host.__dataHost;
}
-
+
$entry(function () {
@SimpleElementBindingStrategy::handleListItemPropertyChange(*)(nodeId, host, propertyName, value, tree);
})();
@@ -380,7 +381,7 @@ private native void hookUpPolymerElement(StateNode node, Element element)
};
}
};
-
+
// dom-repeat doesn't have to be in DOM even if template has it
// such situation happens if there is dom-if e.g. which evaluates to false initially.
// in this case dom-repeat is not yet in the DOM tree until dom-if becomes true
@@ -395,7 +396,7 @@ private native void hookUpPolymerElement(StateNode node, Element element)
element.addEventListener('dom-change',replaceDomRepeatPropertyChange);
}
}
-
+
}-*/;
private static void handleListItemPropertyChange(double nodeId,
@@ -629,7 +630,10 @@ private void updateVisibility(JsArray listeners,
private void updateVisibility(Element element, NodeMap visibilityData,
Boolean visibility) {
storeInitialHiddenAttribute(element, visibilityData);
- WidgetUtil.updateAttribute(element, HIDDEN_ATTRIBUTE, visibility);
+ updateAttributeValue(
+ visibilityData.getNode().getTree().getRegistry()
+ .getApplicationConfiguration(),
+ element, HIDDEN_ATTRIBUTE, visibility);
}
private void restoreInitialHiddenAttribute(Element element,
@@ -637,8 +641,10 @@ private void restoreInitialHiddenAttribute(Element element,
MapProperty initialVisibility = storeInitialHiddenAttribute(element,
visibilityData);
if (initialVisibility.hasValue()) {
- WidgetUtil.updateAttribute(element, HIDDEN_ATTRIBUTE,
- initialVisibility.getValue());
+ updateAttributeValue(
+ visibilityData.getNode().getTree().getRegistry()
+ .getApplicationConfiguration(),
+ element, HIDDEN_ATTRIBUTE, initialVisibility.getValue());
}
}
@@ -733,7 +739,10 @@ private void updateStyleProperty(MapProperty mapProperty, Element element) {
private void updateAttribute(MapProperty mapProperty, Element element) {
String name = mapProperty.getName();
- WidgetUtil.updateAttribute(element, name, mapProperty.getValue());
+ updateAttributeValue(
+ mapProperty.getMap().getNode().getTree().getRegistry()
+ .getApplicationConfiguration(),
+ element, name, mapProperty.getValue());
}
private EventRemover bindChildren(BindingContext context) {
@@ -1352,6 +1361,35 @@ private EventRemover bindClientCallableMethods(BindingContext context) {
(Element) context.htmlNode, context.node);
}
+ private static void updateAttributeValue(
+ ApplicationConfiguration configuration, Element element,
+ String attribute, Object value) {
+ if (value == null || value instanceof String) {
+ WidgetUtil.updateAttribute(element, attribute, (String) value);
+ } else {
+ JsonValue jsonValue = WidgetUtil.crazyJsoCast(value);
+ if (JsonType.OBJECT.equals(jsonValue.getType())) {
+ JsonObject object = (JsonObject) jsonValue;
+ assert object.hasKey(
+ NodeProperties.URI_ATTRIBUTE) : "Implementation error: JsonObject is recieved as an attribute value for '"
+ + attribute + "' but it has no "
+ + NodeProperties.URI_ATTRIBUTE + " key";
+ String uri = object.getString(NodeProperties.URI_ATTRIBUTE);
+ if (configuration.isWebComponentMode()) {
+ String baseUri = configuration.getServiceUrl();
+ baseUri = baseUri.endsWith("/") ? baseUri : baseUri + "/";
+ WidgetUtil.updateAttribute(element, attribute,
+ baseUri + uri);
+ } else {
+ WidgetUtil.updateAttribute(element, attribute, uri);
+ }
+ } else {
+ WidgetUtil.updateAttribute(element, attribute,
+ value.toString());
+ }
+ }
+ }
+
private static EventExpression getOrCreateExpression(
String expressionString) {
if (expressionCache == null) {
diff --git a/flow-client/src/main/resources/META-INF/resources/frontend/VaadinDevmodeGizmo.js b/flow-client/src/main/resources/META-INF/resources/frontend/VaadinDevmodeGizmo.js
index fa62e4aa112..e6694a96fd1 100644
--- a/flow-client/src/main/resources/META-INF/resources/frontend/VaadinDevmodeGizmo.js
+++ b/flow-client/src/main/resources/META-INF/resources/frontend/VaadinDevmodeGizmo.js
@@ -344,9 +344,54 @@ class VaadinDevmodeGizmo extends LitElement {
this.messages.push(msg);
}
- demoteNotification() {
- if (this.notification) {
- this.showMessage(this.notification);
+ dismissNotification(id) {
+ const index = this.findNotificationIndex(id);
+ if (index !== -1 && !this.notifications[index].deleted) {
+ const notification = this.notifications[index];
+
+ // user is explicitly dismissing a notification---after that we won't bug them with it
+ if (notification.dontShowAgain && notification.persistentId && !VaadinDevmodeGizmo.notificationDismissed(notification.persistentId)) {
+ let dismissed = window.localStorage.getItem(VaadinDevmodeGizmo.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE);
+ if (dismissed === null) {
+ dismissed = notification.persistentId;
+ } else {
+ dismissed = dismissed + ',' + notification.persistentId;
+ }
+ window.localStorage.setItem(VaadinDevmodeGizmo.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE, dismissed);
+ }
+
+ notification.deleted = true;
+ this.showMessage(notification.type, notification.message, notification.details, notification.link);
+
+ const self = this;
+ // give some time for the animation
+ setTimeout(() => {
+ const index = self.findNotificationIndex(id);
+ if (index != -1) {
+ this.notifications.splice(index, 1);
+ this.requestUpdate();
+ }
+ }, this.__transitionDuration);
+ }
+ }
+
+ findNotificationIndex(id) {
+ let index = -1;
+ this.notifications.some((notification, idx) => {
+ if (notification.id === id) {
+ index = idx;
+ return true;
+ }
+ });
+ return index;
+ }
+
+ toggleDontShowAgain(id) {
+ const index = this.notifications.findIndex(notification => notification.id === id);
+ if (index !== -1 && !this.notifications[index].deleted) {
+ const notification = this.notifications[index];
+ notification.dontShowAgain = !notification.dontShowAgain;
+ this.requestUpdate();
}
this.showNotification(null);
}
diff --git a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtEventHandlerTest.java b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtEventHandlerTest.java
index 6b133627ac3..72dd917d900 100644
--- a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtEventHandlerTest.java
+++ b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtEventHandlerTest.java
@@ -21,6 +21,7 @@
import java.util.function.Consumer;
import java.util.function.Supplier;
+import com.vaadin.client.ApplicationConfiguration;
import com.vaadin.client.ClientEngineTestBase;
import com.vaadin.client.Registry;
import com.vaadin.client.WidgetUtil;
@@ -66,6 +67,8 @@ protected void gwtSetUp() throws Exception {
registry = new Registry() {
{
set(ConstantPool.class, new ConstantPool());
+ set(ApplicationConfiguration.class,
+ new ApplicationConfiguration());
}
};
@@ -138,7 +141,8 @@ public void testClientCallablePromises() {
delayTestFinish(100);
}
- private static native void addThen(Object promise, Consumer callback)
+ private static native void addThen(Object promise,
+ Consumer callback)
/*-{
promise.then($entry(function(value) {
callback.@Consumer::accept(*)(value);
diff --git a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtMultipleBindingTest.java b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtMultipleBindingTest.java
index b6455c78626..73a88166079 100644
--- a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtMultipleBindingTest.java
+++ b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtMultipleBindingTest.java
@@ -15,6 +15,7 @@
*/
package com.vaadin.client.flow;
+import com.vaadin.client.ApplicationConfiguration;
import com.vaadin.client.ClientEngineTestBase;
import com.vaadin.client.ExistingElementMap;
import com.vaadin.client.Registry;
@@ -91,6 +92,8 @@ protected void gwtSetUp() throws Exception {
{
set(ConstantPool.class, new ConstantPool());
set(ExistingElementMap.class, new ExistingElementMap());
+ set(ApplicationConfiguration.class,
+ new ApplicationConfiguration());
}
};
diff --git a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtPropertyElementBinderTest.java b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtPropertyElementBinderTest.java
index 2cdb5b8e5f6..e263576fb3a 100644
--- a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtPropertyElementBinderTest.java
+++ b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtPropertyElementBinderTest.java
@@ -18,6 +18,7 @@
import java.util.ArrayList;
import java.util.List;
+import com.vaadin.client.ApplicationConfiguration;
import com.vaadin.client.ClientEngineTestBase;
import com.vaadin.client.ExistingElementMap;
import com.vaadin.client.InitialPropertiesHandler;
@@ -58,6 +59,7 @@ private static class TestRegistry extends Registry {
ExistingElementMap existingElementMap) {
this.constantPool = constantPool;
this.existingElementMap = existingElementMap;
+ set(ApplicationConfiguration.class, new ApplicationConfiguration());
}
@Override
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java b/flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java
index e9bc2c30456..33b3859b7f8 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java
@@ -63,7 +63,11 @@ default boolean removeClassName(String className) {
* null to remove all class names
*/
default void setClassName(String className) {
- getElement().setAttribute("class", className);
+ if(className == null) {
+ getElement().removeAttribute("class");
+ } else {
+ getElement().setAttribute("class", className);
+ }
}
/**
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Text.java b/flow-server/src/main/java/com/vaadin/flow/component/Text.java
index 84878ca38b8..507889e25da 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/Text.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/Text.java
@@ -19,6 +19,15 @@
/**
* A component which encapsulates the given text in a text node.
+ *
+ * Text node doesn't support setting any attribute or property so you may not
+ * use Element API (and {@link Text} doesn't provide any such contract) for
+ * setting attribute/property. It implies that you may not style this component
+ * as well. Any attempt to set attribute/property value throws an exception. The
+ * only available API for a {@link Text} component is set a text.
+ *
+ * If you need a text component which can be styled then check {@code Span}
+ * class (from {@code flow-html-components}) module.
*
* @author Vaadin Ltd
* @since 1.0
@@ -59,4 +68,39 @@ public String getText() {
return getElement().getText();
}
+ @Override
+ protected void set(PropertyDescriptor descriptor, T value) {
+ throw new UnsupportedOperationException("Cannot set '"
+ + descriptor.getPropertyName() + "' property to the "
+ + getClass().getSimpleName() + " component because it doesn't "
+ + "represent an HTML Element but a text Node on the client side.");
+ }
+
+ /**
+ * The method is not supported for the {@link Text} class.
+ *
+ * Always throws an {@link UnsupportedOperationException}.
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void setId(String id) {
+ super.setId(id);
+ }
+
+ /**
+ * The method is not supported for the {@link Text} class.
+ *
+ * Always throws an {@link UnsupportedOperationException}.
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ throw new UnsupportedOperationException("Cannot change "
+ + getClass().getSimpleName()
+ + " component visibility because it doesn't "
+ + "represent an HTML Element but a text Node on the client side.");
+ }
+
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java
index ee28d9e4a86..11f7a723115 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java
@@ -177,30 +177,23 @@ private boolean renderViewForRoute(Location location) {
if (!shouldHandleNavigation(location)) {
return false;
}
- try {
- getInternals().setLastHandledNavigation(location);
- Optional navigationState = this.getRouter()
- .resolveNavigationTarget(location);
- if (navigationState.isPresent()) {
- // There is a valid route in flow.
- return handleNavigation(location, navigationState.get());
- } else {
- // When route does not exist, try to navigate to current route
- // in order to check if current view can be left before showing
- // the error page
- if (navigateToPlaceholder(location)) {
- return true;
- }
-
- // Route does not exist, and current view does not prevent
- // navigation
- // thus an error page is shown
- handleErrorNavigation(location);
+ getInternals().setLastHandledNavigation(location);
+ Optional navigationState = this.getRouter().resolveNavigationTarget(location);
+ if (navigationState.isPresent()) {
+ // There is a valid route in flow.
+ return handleNavigation(location, navigationState.get());
+ } else {
+ // When route does not exist, try to navigate to current route
+ // in order to check if current view can be left before showing
+ // the error page
+ if (navigateToPlaceholder(location)) {
+ return true;
}
- } catch (Exception exception) {
- return handleExceptionNavigation(location, exception);
- } finally {
- getInternals().clearLastHandledNavigation();
+
+ // Route does not exist, and current view does not prevent
+ // navigation
+ // thus an error page is shown
+ handleErrorNavigation(location);
}
return false;
}
@@ -224,35 +217,39 @@ private String removeLastSlash(String route) {
return route.replaceFirst("/+$", "");
}
- private boolean handleNavigation(Location location,
- NavigationState navigationState) {
- NavigationEvent navigationEvent = new NavigationEvent(getRouter(),
- location, this, NavigationTrigger.CLIENT_SIDE);
-
- NavigationStateRenderer clientNavigationStateRenderer = new NavigationStateRenderer(
- navigationState);
-
- clientNavigationStateRenderer.handle(navigationEvent);
-
- isUnknownRoute = false;
- hasForwardTo = false;
- // true if has forwardTo in server-views
- if (!getInternals().getActiveRouterTargetsChain().isEmpty()
- && getInternals().getActiveRouterTargetsChain().get(0).getClass().getAnnotation(Route.class) != null
- && !getInternals().getActiveRouterTargetsChain().get(0).getClass().getAnnotation(Route.class)
- .value().contains(getInternals().getActiveViewLocation().getPathWithQueryParameters())) {
- // true if the forwardTo target is client-view
- isUnknownRoute = !this.getRouter()
- .resolveNavigationTarget(new Location(removeFirstSlash(this.getInternals()
- .getActiveViewLocation().getPathWithQueryParameters()))).isPresent();
- if (isUnknownRoute) {
- forwardToUrl = this.getInternals().getActiveViewLocation().getPathWithQueryParameters();
+ private boolean handleNavigation(Location location, NavigationState navigationState) {
+ try {
+ NavigationEvent navigationEvent = new NavigationEvent(getRouter(), location, this,
+ NavigationTrigger.CLIENT_SIDE);
+
+ NavigationStateRenderer clientNavigationStateRenderer = new NavigationStateRenderer(navigationState);
+
+ clientNavigationStateRenderer.handle(navigationEvent);
+
+ isUnknownRoute = false;
+ hasForwardTo = false;
+ // true if has forwardTo in server-views
+ if (!getInternals().getActiveRouterTargetsChain().isEmpty()
+ && getInternals().getActiveRouterTargetsChain().get(0).getClass().getAnnotation(Route.class) != null
+ && !getInternals().getActiveRouterTargetsChain().get(0).getClass().getAnnotation(Route.class)
+ .value().contains(getInternals().getActiveViewLocation().getPathWithQueryParameters())) {
+ // true if the forwardTo target is client-view
+ isUnknownRoute = !this.getRouter().resolveNavigationTarget(new Location(
+ removeFirstSlash(this.getInternals().getActiveViewLocation().getPathWithQueryParameters())))
+ .isPresent();
+ if (isUnknownRoute) {
+ forwardToUrl = this.getInternals().getActiveViewLocation().getPathWithQueryParameters();
+ }
+ hasForwardTo = true;
}
- hasForwardTo = true;
- }
- adjustPageTitle();
+ adjustPageTitle();
- return getInternals().getContinueNavigationAction() != null;
+ return getInternals().getContinueNavigationAction() != null;
+ } catch (Exception exception) {
+ return handleExceptionNavigation(location, exception);
+ } finally {
+ getInternals().clearLastHandledNavigation();
+ }
}
private boolean handleExceptionNavigation(Location location, Exception exception) {
diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java
index a67cf08bed8..b74d1a5ce2c 100644
--- a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java
+++ b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java
@@ -34,6 +34,9 @@
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.shared.Registration;
+import elemental.json.Json;
+import elemental.json.JsonObject;
+
/**
* Map for element attribute values.
*
@@ -65,8 +68,7 @@ public ElementAttributeMap(StateNode node) {
* the value
*/
public void set(String attribute, String value) {
- unregisterResource(attribute);
- put(attribute, value);
+ doSet(attribute, value);
}
/**
@@ -103,7 +105,19 @@ public Serializable remove(String attribute) {
*/
@Override
public String get(String attribute) {
- return (String) super.get(attribute);
+ Serializable value = super.get(attribute);
+ if (value == null || value instanceof String) {
+ return (String) value;
+ } else {
+ // If the value is not a string then current impl only uses
+ // JsonObject
+ assert value instanceof JsonObject;
+ JsonObject object = (JsonObject) value;
+ // The only object which may be set by the current imlp contains
+ // "uri" attribute, only this situation is expected here.
+ assert object.hasKey(NodeProperties.URI_ATTRIBUTE);
+ return object.getString(NodeProperties.URI_ATTRIBUTE);
+ }
}
/**
@@ -142,7 +156,11 @@ private void doSetResource(String attribute,
} else {
targetUri = StreamResourceRegistry.getURI(resource);
}
- set(attribute, targetUri.toASCIIString());
+ JsonObject object = Json.createObject();
+ object.put(NodeProperties.URI_ATTRIBUTE, targetUri.toASCIIString());
+ // don't use sring as a value, but wrap it into an object to let know
+ // the client side about specific nature of the value
+ doSet(attribute, object);
}
private void ensurePendingRegistrations() {
@@ -225,6 +243,11 @@ public void execute() {
}));
}
+ private void doSet(String attribute, Serializable value) {
+ unregisterResource(attribute);
+ put(attribute, value);
+ }
+
private void unsetResource(String attribute) {
ensureResourceRegistrations();
StreamRegistration registration = resourceRegistrations.get(attribute);
diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeProperties.java b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeProperties.java
index 893b51e39c4..dd4008ab675 100644
--- a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeProperties.java
+++ b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeProperties.java
@@ -87,6 +87,14 @@ public final class NodeProperties {
*/
public static final String VISIBILITY_HIDDEN_PROPERTY = "hidden";
+ /**
+ * The property in Json object which marks the object as special value
+ * transmitting URI (not just any string).
+ *
+ * Used in the {@link ElementAttributeMap}.
+ */
+ public static final String URI_ATTRIBUTE = "uri";
+
private NodeProperties() {
}
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java
index 5211755ab4c..0cd47637cfd 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java
@@ -898,7 +898,7 @@ private Element createInlineJavaScriptElement(
// https://developer.mozilla.org/en/docs/Web/HTML/Element/script
Element wrapper = createJavaScriptElement(null, false);
wrapper.appendChild(
- new DataNode(javaScriptContents, wrapper.baseUri()));
+ new DataNode(javaScriptContents));
return wrapper;
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java b/flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java
index 01271e2447f..bf9c2d4f82b 100755
--- a/flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java
@@ -45,37 +45,34 @@
public class DefaultDeploymentConfiguration
extends PropertyDeploymentConfiguration {
- private static final String SEPARATOR = "\n=======================================================================";
- private static final String HEADER = "\n=================== Vaadin DeploymentConfiguration ====================\n";
+ public static final String NOT_PRODUCTION_MODE_INFO = "\nVaadin is running in DEBUG MODE.\n"
+ + "When deploying application for production, remember to disable debug features. See more from https://vaadin.com/docs/";
- public static final String NOT_PRODUCTION_MODE_INFO = " Vaadin is running in DEBUG MODE.\n"
- + " When deploying application for production, remember to disable debug features. See more from https://vaadin.com/docs/";
+ public static final String NOT_PRODUCTION_MODE_WARNING = "\nWARNING: Vaadin is running in DEBUG MODE with debug features enabled, but with a prebuild frontend bundle (production ready).\n"
+ + "When deploying application for production, disable debug features by enabling production mode!\n"
+ + "See more from https://vaadin.com/docs/v14/flow/production/tutorial-production-mode-basic.html";
- public static final String NOT_PRODUCTION_MODE_WARNING = " WARNING: Vaadin is running in DEBUG MODE with debug features enabled, but with a prebuild frontend bundle (production ready).\n"
- + " When deploying application for production, disable debug features by enabling production mode!\n"
- + " See more from https://vaadin.com/docs/v14/flow/production/tutorial-production-mode-basic.html";
-
- public static final String WARNING_V14_BOOTSTRAP = " Using deprecated Vaadin 14 bootstrap mode.\n"
- + " Client-side views written in TypeScript are not supported. Vaadin 15+ enables client-side and server-side views.\n"
- + " See https://vaadin.com/docs/v15/flow/typescript/starting-the-app.html for more information.";
+ public static final String WARNING_V14_BOOTSTRAP = "Using deprecated Vaadin 14 bootstrap mode.\n"
+ + "Client-side views written in TypeScript are not supported. Vaadin 15+ enables client-side and server-side views.\n"
+ + "See https://vaadin.com/docs/v15/flow/typescript/starting-the-app.html for more information.";
// not a warning anymore, but keeping variable name to avoid breaking anything
- public static final String WARNING_V15_BOOTSTRAP = "%n Using Vaadin 15+ bootstrap mode.%n %s%n %s";
+ public static final String WARNING_V15_BOOTSTRAP = "Using Vaadin 15+ bootstrap mode.%n %s%n %s";
- private static final String DEPLOYMENT_WARNINGS = " Following issues were discovered with deployment configuration:";
+ private static final String DEPLOYMENT_WARNINGS = "Following issues were discovered with deployment configuration:";
- public static final String WARNING_XSRF_PROTECTION_DISABLED = " WARNING: Cross-site request forgery protection is disabled!";
+ public static final String WARNING_XSRF_PROTECTION_DISABLED = "WARNING: Cross-site request forgery protection is disabled!";
- public static final String WARNING_HEARTBEAT_INTERVAL_NOT_NUMERIC = " WARNING: heartbeatInterval has been set to a non integer value."
+ public static final String WARNING_HEARTBEAT_INTERVAL_NOT_NUMERIC = "WARNING: heartbeatInterval has been set to a non integer value."
+ "\n The default of 5min will be used.";
- public static final String WARNING_PUSH_MODE_NOT_RECOGNIZED = " WARNING: pushMode has been set to an unrecognized value.\n"
- + " The permitted values are \"disabled\", \"manual\",\n"
- + " and \"automatic\". The default of \"disabled\" will be used.";
+ public static final String WARNING_PUSH_MODE_NOT_RECOGNIZED = "WARNING: pushMode has been set to an unrecognized value.\n"
+ + "The permitted values are \"disabled\", \"manual\",\n"
+ + "and \"automatic\". The default of \"disabled\" will be used.";
- private static final String INDEX_NOT_FOUND = " '%s' is not found from '%s'.%n"
- + " Generating a default one in '%s%s'. "
- + " Move it to the '%s' folder if you want to customize it.";
+ private static final String INDEX_NOT_FOUND = "'%s' is not found from '%s'.%n"
+ + "Generating a default one in '%s%s'. "
+ + "Move it to the '%s' folder if you want to customize it.";
/**
* Default value for {@link #getHeartbeatInterval()} = {@value} .
@@ -159,18 +156,13 @@ private void logMessages() {
Logger logger = LoggerFactory.getLogger(getClass().getName());
if (!warnings.isEmpty()) {
- warnings.add(0, HEADER);
- warnings.add(1, DEPLOYMENT_WARNINGS);
- warnings.add("\n");
+ warnings.add(0, DEPLOYMENT_WARNINGS);
// merging info messages to warnings for now
warnings.addAll(info);
- warnings.add(SEPARATOR);
if (logger.isWarnEnabled()) {
logger.warn(String.join("\n", warnings));
}
} else if (!info.isEmpty()) {
- info.add(0, HEADER);
- info.add(SEPARATOR);
if (logger.isInfoEnabled()) {
logger.info(String.join("\n", info));
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java b/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java
index e166e2f1e32..f27ccd08ce0 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java
@@ -28,6 +28,7 @@
import java.io.InputStreamReader;
import java.io.Serializable;
import java.io.UncheckedIOException;
+import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
@@ -35,13 +36,13 @@
import java.util.List;
import java.util.stream.Collectors;
-import com.vaadin.flow.server.frontend.FrontendUtils;
+import org.slf4j.LoggerFactory;
+
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;
import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
-import org.slf4j.LoggerFactory;
/**
* Registry for PWA data.
@@ -59,6 +60,8 @@
* @since 1.2
*/
public class PwaRegistry implements Serializable {
+
+ private static final String META_INF_RESOURCES = "/META-INF/resources";
private static final String HEADLESS_PROPERTY = "java.awt.headless";
private static final String APPLE_STARTUP_IMAGE = "apple-touch-startup-image";
private static final String APPLE_IMAGE_MEDIA = "(device-width: %dpx) and (device-height: %dpx) "
@@ -97,18 +100,21 @@ public PwaRegistry(PWA pwa, ServletContext servletContext)
// Build pwa elements only if they are enabled
if (pwaConfiguration.isEnabled()) {
- URL logo = servletContext
- .getResource(pwaConfiguration.relIconPath());
- URL offlinePage = servletContext
- .getResource(pwaConfiguration.relOfflinePath());
+ URL logo = getResourceUrl(servletContext,
+ pwaConfiguration.relIconPath());
+
+ URL offlinePage = getResourceUrl(servletContext,
+ pwaConfiguration.relOfflinePath());
// Load base logo from servlet context if available
// fall back to local image if unavailable
BufferedImage baseImage = getBaseImage(logo);
if (baseImage == null) {
- LoggerFactory.getLogger(PwaRegistry.class).error("Image is not found or can't be loaded: " + logo);
+ LoggerFactory.getLogger(PwaRegistry.class).error(
+ "Image is not found or can't be loaded: " + logo);
} else {
- // Pick top-left pixel as fill color if needed for image resizing
+ // Pick top-left pixel as fill color if needed for image
+ // resizing
int bgColor = baseImage.getRGB(0, 0);
// initialize icons
@@ -131,6 +137,19 @@ public PwaRegistry(PWA pwa, ServletContext servletContext)
}
}
+ private URL getResourceUrl(ServletContext context, String path)
+ throws MalformedURLException {
+ URL resourceUrl = context.getResource(path);
+ if (resourceUrl == null) {
+ // this is a workaround specific for Spring default static resources
+ // location: see #8705
+ String cpPath = path.startsWith("/") ? META_INF_RESOURCES + path
+ : META_INF_RESOURCES + "/" + path;
+ resourceUrl = PwaRegistry.class.getResource(cpPath);
+ }
+ return resourceUrl;
+ }
+
private List initializeIcons(BufferedImage baseImage,
int bgColor) {
for (PwaIcon icon : getIconTemplates(pwaConfiguration.getIconPath())) {
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/UnsupportedBrowserHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/UnsupportedBrowserHandler.java
index c38ecb64ed4..cb6554104f8 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/UnsupportedBrowserHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/UnsupportedBrowserHandler.java
@@ -18,6 +18,8 @@
import java.io.IOException;
import java.io.Writer;
+import com.vaadin.flow.shared.ApplicationConstants;
+
/**
* A {@link RequestHandler} that presents an informative page if the browser in
* use is unsupported.
@@ -104,6 +106,9 @@ protected void writeBrowserTooOldPage(VaadinRequest request,
Writer page = response.getWriter();
WebBrowser browser = VaadinSession.getCurrent().getBrowser();
+ response.setContentType(
+ ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8);
+
// @formatter:off
page.write(
""
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java
index e7a901df2bd..b34dd19fbe2 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java
@@ -39,6 +39,8 @@
import elemental.json.JsonObject;
import elemental.json.impl.JsonUtil;
+import java.io.File;
+
import static com.vaadin.flow.component.internal.JavaScriptBootstrapUI.SERVER_ROUTING;
import static com.vaadin.flow.shared.ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8;
import static com.vaadin.flow.shared.ApplicationConstants.CSRF_TOKEN;
@@ -172,10 +174,16 @@ private static Document getIndexHtmlDocument(VaadinRequest request)
}
String frontendDir = FrontendUtils.getProjectFrontendDir(
request.getService().getDeploymentConfiguration());
+ String indexHtmlFilePath;
+ if(frontendDir.endsWith(File.separator)) {
+ indexHtmlFilePath = frontendDir + "index.html";
+ } else {
+ indexHtmlFilePath = frontendDir + File.separatorChar + "index.html";
+ }
String message = String
- .format("Failed to load content of '%1$sindex.html'."
- + "It is required to have '%1$sindex.html' file when "
- + "using client side bootstrapping.", frontendDir);
+ .format("Failed to load content of '%1$s'. "
+ + "It is required to have '%1$s' file when "
+ + "using client side bootstrapping.", indexHtmlFilePath);
throw new IOException(message);
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java
index 9a6369cedbb..1c576e68013 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java
@@ -281,8 +281,6 @@ static Map getDefaultDevDependencies() {
defaults.put("webpack-merge", "4.2.2");
defaults.put("raw-loader", "4.0.0");
- defaults.put("terser", "4.6.7");
-
// Forcing chokidar version for now until new babel version is available
// check out https://github.com/babel/babel/issues/11488
defaults.put("chokidar", "^3.4.0");
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/startup/NavigationTargetFilter.java b/flow-server/src/main/java/com/vaadin/flow/server/startup/NavigationTargetFilter.java
index 6a479994309..d3e330236a0 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/startup/NavigationTargetFilter.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/startup/NavigationTargetFilter.java
@@ -27,7 +27,7 @@
* {@link ServiceLoader}. This means that all implementations must have a
* zero-argument constructor and the fully qualified name of the implementation
* class must be listed on a separate line in a
- * META-INF/services/cocom.vaadin.flow.server.startup.NavigationTargetFilter
+ * META-INF/services/com.vaadin.flow.server.startup.NavigationTargetFilter
* file present in the jar file containing the implementation class.
*
* @author Vaadin Ltd
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/startup/ServletDeployer.java b/flow-server/src/main/java/com/vaadin/flow/server/startup/ServletDeployer.java
index 237e1a9dd90..c2a75271088 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/startup/ServletDeployer.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/startup/ServletDeployer.java
@@ -73,7 +73,6 @@
*/
public class ServletDeployer implements ServletContextListener {
private static final String SKIPPING_AUTOMATIC_SERVLET_REGISTRATION_BECAUSE = "Skipping automatic servlet registration because";
- private static final String SEPARATOR = "=======================================================================";
private enum VaadinServletCreation {
NO_CREATION, SERVLET_EXISTS, SERVLET_CREATED;
@@ -261,12 +260,11 @@ private void logServletCreation(VaadinServletCreation servletCreation,
}
/**
- * Prints to sysout a notification to the user that the application is to be
- * opened in the browser.
+ * Prints to sysout a notification to the user that the application has been deployed.
*
* This method is public so that it can be called in add-ons that map
* servlet automatically but don't use this class for that.
- *
+ *
* @param servletContext
* the deployed servlet context
* @param servletAutomaticallyCreated
@@ -281,16 +279,14 @@ public static void logAppStartupToConsole(ServletContext servletContext,
String contextPath = servletContext.getContextPath();
contextPath = contextPath.isEmpty() ? "/" : contextPath;
- String url = String.format("http://localhost:8080%s", contextPath);
FrontendUtils.console(FrontendUtils.BRIGHT_BLUE, String.format(
- "%n%s%n%n Vaadin application has started in DEBUG MODE and is available by opening %s in the browser.%n%n NOTE: the server HTTP port may vary - see server log output.%n%n%s%n",
- SEPARATOR, url, SEPARATOR));
+ "Vaadin application has been deployed and started to the context path \"%s\".%n",
+ contextPath));
} else {
// if the user has mapped their own servlet, they will know where to
// find it
FrontendUtils.console(FrontendUtils.BRIGHT_BLUE, String.format(
- "%n%s%n%nVaadin application has started in DEBUG MODE and is available in the browser.%n%n%s%n",
- SEPARATOR, SEPARATOR));
+ "Vaadin application has been deployed and started.%n"));
}
}
diff --git a/flow-server/src/main/resources/webpack.generated.js b/flow-server/src/main/resources/webpack.generated.js
index 3437ecd7c97..3dde9df8372 100644
--- a/flow-server/src/main/resources/webpack.generated.js
+++ b/flow-server/src/main/resources/webpack.generated.js
@@ -214,8 +214,11 @@ function collectChunks(statsJson, acceptedChunks) {
const slimModule = {
id: module.id,
name: module.name,
- source: module.source,
+ source: module.source
};
+ if(module.modules) {
+ slimModule.modules = collectSubModules(module);
+ }
modules.push(slimModule);
});
const slimChunk = {
@@ -245,27 +248,40 @@ function collectModules(statsJson, acceptedChunks) {
statsJson.modules.forEach(function (module) {
// Add module if module chunks contain an accepted chunk and the module is generated-flow-imports.js module
if (module.chunks.filter(key => acceptedChunks.includes(key)).length > 0
- && (module.name.includes("generated-flow-imports.js") || module.name.includes("generated-flow-imports-fallback.js"))) {
- let subModules = [];
- // Create sub modules only if they are available
- if (module.modules) {
- module.modules.forEach(function (module) {
- const subModule = {
- name: module.name,
- source: module.source
- };
- subModules.push(subModule);
- });
- }
+ && (module.name.includes("generated-flow-imports.js") || module.name.includes("generated-flow-imports-fallback.js"))) {
const slimModule = {
id: module.id,
name: module.name,
- source: module.source,
- modules: subModules
+ source: module.source
};
+ if(module.modules) {
+ slimModule.modules = collectSubModules(module);
+ }
modules.push(slimModule);
}
});
}
return modules;
}
+
+/**
+ * Collect any modules under a module (aka. submodules);
+ *
+ * @param module module to get submodules for
+ */
+function collectSubModules(module) {
+ let modules = [];
+ module.modules.forEach(function (submodule) {
+ if (submodule.source) {
+ const slimModule = {
+ name: submodule.name,
+ source: submodule.source,
+ };
+ if(submodule.id) {
+ slimModule.id = submodule.id;
+ }
+ modules.push(slimModule);
+ }
+ });
+ return modules;
+}
diff --git a/flow-server/src/test/java/com/vaadin/flow/component/HasStyleTest.java b/flow-server/src/test/java/com/vaadin/flow/component/HasStyleTest.java
index 1047b73a6d7..9c498b8015f 100644
--- a/flow-server/src/test/java/com/vaadin/flow/component/HasStyleTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/component/HasStyleTest.java
@@ -90,6 +90,11 @@ public void setClassName() {
assertClasses(component);
component.setClassName("");
assertClasses(component);
+
+ component.setClassName("removeMe");
+ // setting null to classname should remove class name attribute
+ component.setClassName(null);
+ assertClasses(component);
}
@Test
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java b/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java
new file mode 100644
index 00000000000..cc45abec010
--- /dev/null
+++ b/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2000-2020 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.flow.server;
+
+import javax.servlet.ServletContext;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+@PWA(name = "foo", shortName = "bar")
+public class PwaRegistryTest {
+
+ @Test
+ public void pwaIconIsGeneratedBasedOnClasspathIcon_servletContextHasNoResources()
+ throws IOException {
+ ServletContext context = Mockito.mock(ServletContext.class);
+ // PWA annotation has default value for "iconPath" but servlet context
+ // has no resource for that path, in that case the ClassPath URL will be
+ // checked which is "META-INF/resources/icons/icon.png" (this path
+ // available is in the test resources folder). The icon in this path
+ // differs from the default icon and set of icons will be generated
+ // based on it
+ PwaRegistry registry = new PwaRegistry(
+ PwaRegistryTest.class.getAnnotation(PWA.class), context);
+ List icons = registry.getIcons();
+ // This icon has width 32 and it's generated based on a custom icon (see
+ // above)
+ PwaIcon pwaIcon = icons.stream().filter(icon -> icon.getWidth() == 32)
+ .findFirst().get();
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ pwaIcon.write(stream);
+ // the default image has 47 on the position 36
+ Assert.assertEquals(26, stream.toByteArray()[36]);
+ }
+}
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/UnsupportedBrowserHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/UnsupportedBrowserHandlerTest.java
index 340ce4e04fc..8d819841c0c 100644
--- a/flow-server/src/test/java/com/vaadin/flow/server/UnsupportedBrowserHandlerTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/server/UnsupportedBrowserHandlerTest.java
@@ -9,6 +9,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
+import com.vaadin.flow.shared.ApplicationConstants;
import com.vaadin.tests.util.MockDeploymentConfiguration;
public class UnsupportedBrowserHandlerTest {
@@ -72,6 +73,9 @@ public void testUnsupportedBrowserHandler_tooOldBrowser_returnsUnsupportedBrowse
Assert.assertTrue("Unsupported browser page not used",
pageCapture.getValue().contains(
"I'm sorry, but your browser is not supported"));
+
+ Mockito.verify(response).setContentType(
+ ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8);
}
@Test
@@ -94,6 +98,15 @@ public void testUnsupportedBrowserHandler_validBrowserWithForceReloadCookie_does
Mockito.verify(writer, Mockito.never()).write(Mockito.anyString());
}
+ @Test
+ public void writeBrowserTooOldPage_setContentType() throws IOException {
+ initMocks(true, true);
+ handler.writeBrowserTooOldPage(request, response);
+
+ Mockito.verify(response).setContentType(
+ ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8);
+ }
+
@After
public void tearDown() {
VaadinSession.setCurrent(null);
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java
index 11caa930571..9a455f1dfe6 100644
--- a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java
@@ -21,6 +21,7 @@
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -36,6 +37,7 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
@@ -46,6 +48,7 @@
import com.vaadin.flow.server.DevModeHandler;
import com.vaadin.flow.server.MockServletServiceSessionSetup;
import com.vaadin.flow.server.VaadinResponse;
+import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinServletRequest;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.frontend.FrontendUtils;
@@ -57,9 +60,13 @@
import elemental.json.JsonObject;
import static com.vaadin.flow.component.internal.JavaScriptBootstrapUI.SERVER_ROUTING;
+import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES;
import static com.vaadin.flow.server.DevModeHandlerTest.createStubWebpackTcpListener;
+import static com.vaadin.flow.server.frontend.FrontendUtils.INDEX_HTML;
import static com.vaadin.flow.server.frontend.NodeUpdateTestUtil.createStubWebpackServer;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
public class IndexHtmlRequestHandlerTest {
private MockServletServiceSessionSetup mocks;
@@ -73,6 +80,8 @@ public class IndexHtmlRequestHandlerTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+ @Rule
+ public ExpectedException exceptionRule = ExpectedException.none();
@Before
public void setUp() throws Exception {
@@ -103,6 +112,41 @@ public void serveIndexHtml_requestWithRootPath_serveContentFromTemplate()
indexHtml.contains(".v-system-error"));
}
+ @Test
+ public void serveNotFoundIndexHtml_requestWithRootPath_failsWithIOException()
+ throws IOException {
+ VaadinServletRequest vaadinServletRequest = createVaadinRequest("/");
+ VaadinService vaadinService = vaadinServletRequest.getService();
+
+ // Finding index.html URL
+ String indexHtmlPathInProductionMode = VAADIN_SERVLET_RESOURCES
+ + INDEX_HTML;
+ URL url = vaadinService.getClassLoader().getResource(indexHtmlPathInProductionMode);
+
+ assertNotNull(url);
+ File indexHtmlFile = new File(url.getPath());
+ File indexHtmlFileTmp = new File(url.getPath() + "_tmp");
+ try {
+ // Renaming file to simulate the absence of index.html
+ boolean renamed = indexHtmlFile.renameTo(indexHtmlFileTmp);
+ assertTrue(renamed);
+
+ String expectedError = "Failed to load content of './frontend/index.html'. " +
+ "It is required to have './frontend/index.html' file " +
+ "when using client side bootstrapping.";
+
+ exceptionRule.expect(IOException.class);
+ exceptionRule.expectMessage(expectedError);
+
+ indexHtmlRequestHandler.synchronizedHandleRequest(session,
+ vaadinServletRequest, response);
+ } finally {
+ // Restoring index.html
+ boolean renamed = indexHtmlFileTmp.renameTo(indexHtmlFile);
+ assertTrue(renamed);
+ }
+ }
+
@Test
public void serveIndexHtml_requestWithRootPath_hasBaseHrefElement()
throws IOException {
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java
index 07fcb9833b0..6315f1e01ef 100644
--- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java
@@ -180,14 +180,6 @@ public void updateDefaultDependencies_newerVersionsAreNotChanged()
.getObject(NodeUpdater.DEV_DEPENDENCIES).getString("webpack"));
}
- @Test
- public void assertTerserVersion() throws IOException {
- final JsonObject packageJson = nodeUpdater.getPackageJson();
- nodeUpdater.updateDefaultDependencies(packageJson);
- Assert.assertEquals("4.6.7", packageJson
- .getObject(NodeUpdater.DEV_DEPENDENCIES).getString("terser"));
- }
-
private String getPolymerVersion(JsonObject object) {
JsonObject deps = object.get("dependencies");
String version = deps.getString("@polymer/polymer");
diff --git a/flow-server/src/test/resources/META-INF/resources/icons/icon.png b/flow-server/src/test/resources/META-INF/resources/icons/icon.png
new file mode 100644
index 00000000000..e30218e22dc
Binary files /dev/null and b/flow-server/src/test/resources/META-INF/resources/icons/icon.png differ
diff --git a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedException.java b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedException.java
new file mode 100644
index 00000000000..3257affeccc
--- /dev/null
+++ b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedException.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2000-2020 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.flow.ccdmtest;
+
+public class UnauthenticatedException extends RuntimeException
+{
+}
diff --git a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedExceptionHandler.java b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedExceptionHandler.java
new file mode 100644
index 00000000000..72faed2ccd8
--- /dev/null
+++ b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedExceptionHandler.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2020 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.flow.ccdmtest;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.router.BeforeEnterEvent;
+import com.vaadin.flow.router.ErrorParameter;
+import com.vaadin.flow.router.HasErrorParameter;
+
+import javax.servlet.http.HttpServletResponse;
+
+
+@Tag(Tag.DIV)
+public class UnauthenticatedExceptionHandler
+ extends Component
+ implements HasErrorParameter
+{
+
+ @Override
+ public int setErrorParameter(BeforeEnterEvent event,
+ ErrorParameter
+ parameter) {
+ setId("errorView");
+ getElement().setText("Tried to navigate to a view without being authenticated");
+ return HttpServletResponse.SC_UNAUTHORIZED;
+ }
+}
diff --git a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewThrowsException.java b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewThrowsException.java
new file mode 100644
index 00000000000..ef84f2c6999
--- /dev/null
+++ b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewThrowsException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2020 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.flow.ccdmtest;
+
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.router.HasDynamicTitle;
+import com.vaadin.flow.router.Route;
+
+@Route("view-throws-exception")
+public class ViewThrowsException extends Div implements HasDynamicTitle {
+
+ public ViewThrowsException() {
+ Span textField = new Span("You should not see this page, you cannot go back to the main page");
+
+ add(textField);
+ }
+
+ @Override
+ public String getPageTitle() {
+ // Use backend information
+ throw new UnauthenticatedException();
+ }
+}
diff --git a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewWithServerViewButton.java b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewWithServerViewButton.java
index 84bc4c9cbc1..cc687eb845a 100644
--- a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewWithServerViewButton.java
+++ b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewWithServerViewButton.java
@@ -27,6 +27,12 @@ public ViewWithServerViewButton() {
NativeButton serverViewButton = new NativeButton("Server view",
e -> UI.getCurrent().navigate("serverview"));
serverViewButton.setId("serverViewButton");
- add(serverViewButton);
+
+ NativeButton serverViewThrowsExcpetionButton =
+ new NativeButton("Go to a server view that thorws exception",
+ e -> UI.getCurrent().navigate(ViewThrowsException.class));
+ serverViewThrowsExcpetionButton.setId("serverViewThrowsExcpetionButton");
+
+ add(serverViewButton, serverViewThrowsExcpetionButton);
}
}
diff --git a/flow-tests/test-ccdm/src/test/java/com/vaadin/flow/ccdmtest/ServerSideNavigationExceptionHandlingIT.java b/flow-tests/test-ccdm/src/test/java/com/vaadin/flow/ccdmtest/ServerSideNavigationExceptionHandlingIT.java
new file mode 100644
index 00000000000..7b26ad1da5f
--- /dev/null
+++ b/flow-tests/test-ccdm/src/test/java/com/vaadin/flow/ccdmtest/ServerSideNavigationExceptionHandlingIT.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-2020 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.flow.ccdmtest;
+
+import java.util.List;
+
+import com.vaadin.flow.testutil.ChromeBrowserTest;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+public class ServerSideNavigationExceptionHandlingIT extends ChromeBrowserTest {
+
+ private void openTestUrl(String url) {
+ getDriver().get(getRootURL() + "/foo" + url);
+ waitForDevServer();
+ }
+
+ @Test
+ public void should_showErrorView_when_targetViewThrowsException() {
+ openVaadinRouter("/");
+
+ findAnchor("view-with-server-view-button").click();
+
+ // Navigate to a server-side view that throws exception.
+ findElement(By.id("serverViewThrowsExcpetionButton")).click();
+
+ assertView("errorView", "Tried to navigate to a view without being authenticated", "view-throws-exception");
+ }
+
+ protected final void openVaadinRouter(String url) {
+ openTestUrl(url);
+
+ waitForElementPresent(By.id("loadVaadinRouter"));
+ findElement(By.id("loadVaadinRouter")).click();
+ waitForElementPresent(By.id("outlet"));
+ }
+
+ protected final WebElement findAnchor(String href) {
+ final List anchors = findElements(By.tagName("a"));
+ for (WebElement element : anchors) {
+ if (element.getAttribute("href").endsWith(href)) {
+ return element;
+ }
+ }
+
+ return null;
+ }
+
+ protected final void assertView(String viewId, String assertViewText, String assertViewRoute) {
+ waitForElementPresent(By.id(viewId));
+ final WebElement serverViewDiv = findElement(By.id(viewId));
+
+ Assert.assertEquals(assertViewText, serverViewDiv.getText());
+ assertCurrentRoute(assertViewRoute);
+ }
+
+ protected final void assertCurrentRoute(String route) {
+ final String currentUrl = getDriver().getCurrentUrl();
+ Assert.assertTrue(String.format("Expecting route '%s', but url is '%s'",
+ route, currentUrl), currentUrl.endsWith(route));
+ }
+
+
+}
diff --git a/flow-tests/test-embedding/embedding-test-assets/src/main/java/com/vaadin/flow/webcomponent/ClientSelectComponent.java b/flow-tests/test-embedding/embedding-test-assets/src/main/java/com/vaadin/flow/webcomponent/ClientSelectComponent.java
index 0f3be9e2e97..9b46a85a496 100644
--- a/flow-tests/test-embedding/embedding-test-assets/src/main/java/com/vaadin/flow/webcomponent/ClientSelectComponent.java
+++ b/flow-tests/test-embedding/embedding-test-assets/src/main/java/com/vaadin/flow/webcomponent/ClientSelectComponent.java
@@ -15,6 +15,7 @@
*/
package com.vaadin.flow.webcomponent;
+import java.io.ByteArrayInputStream;
import java.util.Optional;
import com.vaadin.flow.component.AbstractField;
@@ -22,8 +23,10 @@
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.JsModule;
+import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
+import com.vaadin.flow.server.StreamResource;
@JsModule("./src/Dependency.js")
public class ClientSelectComponent extends Div {
@@ -42,6 +45,12 @@ public ClientSelectComponent() {
select.addValueChangeListener(this::setValue);
add(select, message);
+ Anchor getPdf = new Anchor();
+ getPdf.setText("Download PDF");
+ getPdf.setId("link");
+ getPdf.setHref(createPdfStreamResource());
+ getPdf.getElement().setAttribute("download", true);
+ add(getPdf);
}
@Override
@@ -49,6 +58,13 @@ protected void onAttach(AttachEvent attachEvent) {
add(new DepElement());
}
+ private StreamResource createPdfStreamResource() {
+ StreamResource streamResource = new StreamResource("label.pdf",
+ () -> new ByteArrayInputStream(new byte[] { 1 }));
+ streamResource.setContentType("application/pdf");
+ return streamResource;
+ }
+
private void setValue(
AbstractField.ComponentValueChangeEvent