Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CCDM: allow page configuration in client mode #7050

Merged
merged 10 commits into from
Nov 29, 2019
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2000-2018 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.component.page;

import java.io.Serializable;

/**
* A marker class to configure the index.hml page when using client-side
* bootstrapping. This class supports the following annotations that affect the
* generated index.html page:
*
* <ul>
* <li>{@link Meta}: appends an HTML {@code <meta>} tag to the bottom of the
* {@code <head>} element</li>
* </ul>
*
* <p>
* There should be at max one class extending {@link VaadinAppShell} in the
* application.
* </p>
*
* <p>
* NOTE: the application shell class is the only valid target for the page
* configuration annotations listed above. The application would fail to start
* if any of these annotations is wrongly placed on a class other than the
* application shell class.
* <p>
*
* <code>
* &#64;Meta(name = "Author", content = "Donald Duck")
* public class AppShell extends VaadinAppShell {
* }
* </code>
*
* @since 3.0
*/
public abstract class VaadinAppShell implements Serializable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR Convert the abstract class "VaadinAppShell" into an interface. rule

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinResponse;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.startup.VaadinAppShellRegistry;

import elemental.json.JsonObject;
import elemental.json.impl.JsonUtil;
Expand All @@ -56,6 +58,7 @@ public boolean synchronizedHandleRequest(VaadinSession session,
Document indexDocument = getIndexHtmlDocument(request);

prependBaseHref(request, indexDocument);

if (request.getService().getBootstrapInitialPredicate()
.includeInitialUidl(request)) {
includeInitialUidl(session, request, response, indexDocument);
Expand All @@ -64,12 +67,18 @@ public boolean synchronizedHandleRequest(VaadinSession session,
// unless we detect a call to JavaScriptBootstrapUI.connectClient
session.setAttribute(SERVER_ROUTING, Boolean.TRUE);
}

configureErrorDialogStyles(indexDocument);

showWebpackErrors(indexDocument);

response.setContentType(CONTENT_TYPE_TEXT_HTML_UTF_8);

// modify the page based on the page config annotations (@Meta, etc)
VaadinContext context = session.getService().getContext();
VaadinAppShellRegistry.getInstance(context)
.modifyIndexHtmlResponse(indexDocument);

// modify the page based on registered IndexHtmlRequestListener:s
request.getService().modifyIndexHtmlResponse(
new IndexHtmlResponse(request, response, indexDocument));

Expand All @@ -88,6 +97,7 @@ private void includeInitialUidl(VaadinSession session,
Document indexDocument) {
JsonObject initial = getInitialJson(request, response, session);


Element elm = new Element("script");
elm.attr("initial", "");
elm.appendChild(new DataNode("window.Vaadin = {Flow : {initial: "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ default Set<Class<?>> getAnnotatedClasses(String className)

/**
* Get class loader which is used to find classes.
*
*
* @return the class loader which is used to find classes..
*/
ClassLoader getClassLoader();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.stream.Collectors;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.page.VaadinAppShell;
import com.vaadin.flow.router.ParentLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouteAlias;
Expand Down Expand Up @@ -107,10 +108,21 @@ protected String getErrorHint() {
* {@code clazz}
*/
protected String getClassAnnotations(Class<?> clazz) {
return getAnnotations().stream()
return getClassAnnotations(clazz, getAnnotations());
}

@SuppressWarnings("unchecked")
protected static String getClassAnnotations(Class<?> clazz,
List<Class<?>> annotations) {
return annotations.stream()
.filter(ann -> clazz
.isAnnotationPresent((Class<? extends Annotation>) ann))
.map(Class::getSimpleName).collect(Collectors.joining(", "));
.map(ann ->
// Prepend annotation name with '@'
"@" + ann.getName()
// Replace `$Container` ending when multiple annotations
.replaceFirst("^.*\\.([^$\\.]+).*$", "$1"))
.collect(Collectors.joining(", "));
}

private List<String> validateAnnotatedClasses(
Expand All @@ -130,6 +142,9 @@ private List<String> validateAnnotatedClasses(
offendingAnnotations.add(String.format(NON_PARENT_ALIAS,
clazz.getName(), getClassAnnotations(clazz)));
}
} else if (VaadinAppShell.class.isAssignableFrom(clazz)) {
// Annotations on the app shell classes are validated in
// VaadinAppShellInitializer
} else if (!RouterLayout.class.isAssignableFrom(clazz)) {
if (!Modifier.isAbstract(clazz.getModifiers())) {
handleNonRouterLayout(clazz)
Expand All @@ -141,7 +156,6 @@ private List<String> validateAnnotatedClasses(
clazz.getName(), getClassAnnotations(clazz)));
}
}

return offendingAnnotations;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ protected Class<?> validatePwaClass(Stream<Class<?>> routeClasses) {
validatePwa(route);

Route routeAnnotation = route.getAnnotation(Route.class);

if (!UI.class.equals(routeAnnotation.layout())) {
Class<? extends RouterLayout> topParentLayout = RouteUtil
.getTopParentLayout(route,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright 2000-2018 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.startup;

import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import javax.servlet.annotation.HandlesTypes;
import javax.servlet.annotation.WebListener;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.page.Meta;
import com.vaadin.flow.component.page.VaadinAppShell;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.server.InvalidApplicationConfigurationException;
import com.vaadin.flow.server.VaadinServlet;
import com.vaadin.flow.server.VaadinServletContext;
import com.vaadin.flow.server.startup.ServletDeployer.StubServletConfig;

/**
* Servlet initializer visiting {@link VaadinAppShell} configuration.
*
* @since 3.0
*/
@HandlesTypes({ VaadinAppShell.class, Meta.class, Meta.Container.class })
@WebListener
public class VaadinAppShellInitializer implements ServletContainerInitializer,
Serializable {

@Override
public void onStartup(Set<Class<?>> classes, ServletContext context)
throws ServletException {

Collection<? extends ServletRegistration> registrations = context
.getServletRegistrations().values();
if (registrations.isEmpty()) {
return;
}

DeploymentConfiguration config = StubServletConfig
.createDeploymentConfiguration(context,
registrations.iterator().next(), VaadinServlet.class);

init(classes, context, config);
}

/**
* Initializes the {@link VaadinAppShellRegistry} for the application.
*
* @param classes
* a set of classes that matches the {@link HandlesTypes} set in
* this class.
* @param context
* the servlet context.
* @param config
* the vaadin configuration for the application.
*/
@SuppressWarnings("unchecked")
public static void init(Set<Class<?>> classes, ServletContext context,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAJOR Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed. rule

DeploymentConfiguration config) {

if (!config.isClientSideMode()) {
return;
}

VaadinAppShellRegistry registry = VaadinAppShellRegistry
.getInstance(new VaadinServletContext(context));
registry.reset();

if (classes.isEmpty()) {
return;
}

List<String> offendingAnnotations = new ArrayList<>();

classes.stream()
// sort classes by putting VaadinAppShell in first position
.sorted((a, b) -> registry.isShell(a) ? -1
: registry.isShell(b) ? 1 : 0)
.forEach(clz -> {
if (registry.isShell(clz)) {
registry.setShell(
(Class<? extends VaadinAppShell>) clz);
} else {
String error = registry.validateClass(clz);
if (error != null) {
offendingAnnotations.add(error);
}
}
});

if (!offendingAnnotations.isEmpty()) {
if (registry.getShell() == null) {
String message = String.format(
VaadinAppShellRegistry.ERROR_HEADER_NO_SHELL,
String.join("\n ", offendingAnnotations));
LoggerFactory.getLogger(VaadinAppShellInitializer.class)
.error(message);
} else {
String message = String.format(
VaadinAppShellRegistry.ERROR_HEADER_OFFENDING,
registry.getShell(),
String.join("\n ", offendingAnnotations));
throw new InvalidApplicationConfigurationException(message);
}
}
}

/**
* Return the set of valid annotations in a {@link VaadinAppShell} class.
* This method is thought to be called from external plugins to decouple
* them.
*
* @return
*/
@SuppressWarnings("unchecked")
public static List<Class<? extends Annotation>> getValidAnnotations() {
HandlesTypes annotation = VaadinAppShellInitializer.class
.getAnnotation(HandlesTypes.class);
assert annotation != null;
List<Class<? extends Annotation>> ret = new ArrayList<>();
for (Class<?> clazz : annotation.value()) {
if (clazz.isAnnotation()) {
ret.add((Class<? extends Annotation>) clazz);
}
}
return ret;
}

/**
* Return the {@link VaadinAppShell} class. This method is thought to be
* called from external plugins to decouple them.
*
* @return
*/
public static List<Class<?>> getValidSupers() {
return Collections.singletonList(VaadinAppShell.class);
}
}