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 2e953361cf8..86acb5f5956 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 @@ -15,17 +15,18 @@ */ package com.vaadin.flow.server.startup; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Optional; + import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.ServletException; import javax.servlet.ServletRegistration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +36,7 @@ import com.vaadin.flow.server.DeploymentConfigurationFactory; import com.vaadin.flow.server.VaadinServlet; import com.vaadin.flow.server.VaadinServletConfiguration; +import com.vaadin.flow.server.webcomponent.WebComponentConfigurationRegistry; /** * Context listener that automatically registers Vaadin servlets. @@ -65,8 +67,7 @@ public class ServletDeployer implements ServletContextListener { private static final String SKIPPING_AUTOMATIC_SERVLET_REGISTRATION_BECAUSE = "Skipping automatic servlet registration because"; - private static class StubServletConfig - implements ServletConfig { + private static class StubServletConfig implements ServletConfig { private final ServletContext context; private final ServletRegistration registration; @@ -144,7 +145,8 @@ private DeploymentConfiguration createDeploymentConfiguration( ServletConfig servletConfig, Class servletClass) { try { return DeploymentConfigurationFactory - .createPropertyDeploymentConfiguration(servletClass, servletConfig); + .createPropertyDeploymentConfiguration(servletClass, + servletConfig); } catch (ServletException e) { throw new IllegalStateException(String.format( "Failed to get deployment configuration data for servlet with name '%s' and class '%s'", @@ -153,9 +155,16 @@ private DeploymentConfiguration createDeploymentConfiguration( } private void createAppServlet(ServletContext context) { - if (!ApplicationRouteRegistry.getInstance(context).hasNavigationTargets()) { + boolean createServlet = ApplicationRouteRegistry.getInstance(context) + .hasNavigationTargets(); + + createServlet = createServlet || WebComponentConfigurationRegistry + .getInstance(context).hasExporters(); + + if (!createServlet) { getLogger().info( - "{} there are no navigation targets registered to the registry", + "{} there are no navigation targets registered to the " + + "route registry and there are no web component exporters", SKIPPING_AUTOMATIC_SERVLET_REGISTRATION_BECAUSE); return; } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/webcomponent/WebComponentConfigurationRegistry.java b/flow-server/src/main/java/com/vaadin/flow/server/webcomponent/WebComponentConfigurationRegistry.java index 789c7bc2e33..5479b0c7929 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/webcomponent/WebComponentConfigurationRegistry.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/webcomponent/WebComponentConfigurationRegistry.java @@ -15,7 +15,6 @@ */ package com.vaadin.flow.server.webcomponent; -import javax.servlet.ServletContext; import java.io.Serializable; import java.util.Collections; import java.util.HashMap; @@ -26,6 +25,8 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import javax.servlet.ServletContext; + import com.vaadin.flow.component.Component; import com.vaadin.flow.component.WebComponentExporter; import com.vaadin.flow.component.webcomponent.WebComponentConfiguration; @@ -47,10 +48,8 @@ public class WebComponentConfigurationRegistry implements Serializable { */ private final ReentrantLock configurationLock = new ReentrantLock(true); - private HashMap>> exporterClasses = null; - private HashMap> - builderCache = new HashMap<>(); + private HashMap>> exporterClasses = null; + private HashMap> builderCache = new HashMap<>(); /** * Protected constructor for internal OSGi extensions. @@ -63,23 +62,25 @@ protected WebComponentConfigurationRegistry() { * registered. * * @param tag - * custom element tag + * custom element tag * @return Optional containing a web component matching given tag */ - public Optional> getConfiguration(String tag) { + public Optional> getConfiguration( + String tag) { return Optional.ofNullable(getConfigurationInternal(tag)); } /** - * Retrieves {@link WebComponentConfigurationImpl} matching the {@code} tag. If the - * builder is not readily available, attempts to construct it from the - * web component exporter cache. + * Retrieves {@link WebComponentConfigurationImpl} matching the {@code} tag. + * If the builder is not readily available, attempts to construct it from + * the web component exporter cache. * * @param tag - * tag name of the web component + * tag name of the web component * @return {@link WebComponentConfigurationImpl} by the tag */ - protected WebComponentConfigurationImpl getConfigurationInternal(String tag) { + protected WebComponentConfigurationImpl getConfigurationInternal( + String tag) { WebComponentConfigurationImpl builder = null; configurationLock.lock(); try { @@ -97,11 +98,14 @@ protected WebComponentConfigurationImpl getConfigurationInt * Get an unmodifiable set containing all registered web component * configurations for a specific {@link Component} type. * - * @param componentClass type of the exported {@link Component} - * @param component - * @return set of {@link WebComponentConfiguration} or an empty set. + * @param componentClass + * type of the exported {@link Component} + * @param + * component + * @return set of {@link WebComponentConfiguration} or an empty set. */ - public Set> getConfigurationsByComponentType(Class componentClass) { + public Set> getConfigurationsByComponentType( + Class componentClass) { configurationLock.lock(); try { if (exporterClasses == null) { @@ -112,7 +116,7 @@ public Set> getConfigurations } return Collections.unmodifiableSet(builderCache.values().stream() .filter(b -> componentClass.equals(b.getComponentClass())) - .map(b -> (WebComponentConfiguration)b) + .map(b -> (WebComponentConfiguration) b) .collect(Collectors.toSet())); } finally { @@ -124,10 +128,10 @@ public Set> getConfigurations * Internal method for updating registry. * * @param exporters - * map of web component exporters to register + * map of web component exporters to register */ - protected void updateRegistry(Map>> exporters) { + protected void updateRegistry( + Map>> exporters) { configurationLock.lock(); try { if (exporters.isEmpty()) { @@ -151,11 +155,11 @@ protected void updateRegistry(Map>> exporters) { + public boolean setExporters( + Map>> exporters) { configurationLock.lock(); try { if (exporterClasses != null) { @@ -171,15 +175,20 @@ public boolean setExporters(Map> getConfigurations() { configurationLock.lock(); @@ -188,8 +197,8 @@ public Set> getConfigurations() { populateCacheWithMissingConfigurations(); } - return Collections.unmodifiableSet(new HashSet<>( - builderCache.values())); + return Collections + .unmodifiableSet(new HashSet<>(builderCache.values())); } finally { configurationLock.unlock(); } @@ -199,7 +208,7 @@ public Set> getConfigurations() { * Get WebComponentRegistry instance for given servlet context. * * @param servletContext - * servlet context to get registry for + * servlet context to get registry for * @return WebComponentRegistry instance */ public static WebComponentConfigurationRegistry getInstance( @@ -208,14 +217,14 @@ public static WebComponentConfigurationRegistry getInstance( Object attribute; synchronized (servletContext) { - attribute = servletContext - .getAttribute(WebComponentConfigurationRegistry.class.getName()); + attribute = servletContext.getAttribute( + WebComponentConfigurationRegistry.class.getName()); if (attribute == null) { attribute = createRegistry(servletContext); - servletContext - .setAttribute(WebComponentConfigurationRegistry.class.getName(), - attribute); + servletContext.setAttribute( + WebComponentConfigurationRegistry.class.getName(), + attribute); } } @@ -227,12 +236,14 @@ public static WebComponentConfigurationRegistry getInstance( } } - private static WebComponentConfigurationRegistry createRegistry(ServletContext context) { + private static WebComponentConfigurationRegistry createRegistry( + ServletContext context) { if (OSGiAccess.getInstance().getOsgiServletContext() == null) { return new WebComponentConfigurationRegistry(); } Object attribute = OSGiAccess.getInstance().getOsgiServletContext() - .getAttribute(WebComponentConfigurationRegistry.class.getName()); + .getAttribute( + WebComponentConfigurationRegistry.class.getName()); if (attribute != null && attribute instanceof OSGiWebComponentConfigurationRegistry) { return (WebComponentConfigurationRegistry) attribute; @@ -245,7 +256,8 @@ private static WebComponentConfigurationRegistry createRegistry(ServletContext c * Adds a builder to the builder cache if one is not already present, * exporters have been set, and a matching exporter is found. * - * @param tag name of the web component + * @param tag + * name of the web component */ protected void populateCacheByTag(String tag) { configurationLock.lock(); @@ -255,11 +267,12 @@ protected void populateCacheByTag(String tag) { return; } - Class> exporterClass - = exporterClasses.get(tag); + Class> exporterClass = exporterClasses + .get(tag); if (exporterClass != null) { - builderCache.put(tag, constructConfigurations(tag, exporterClass)); + builderCache.put(tag, + constructConfigurations(tag, exporterClass)); // remove the class reference from the data bank - it has // already been constructed and is no longer needed exporterClasses.remove(tag); @@ -287,22 +300,21 @@ protected boolean areAllConfigurationsAvailable() { configurationLock.lock(); try { - return exporterClasses != null && - exporterClasses.size() == 0; + return exporterClasses != null && exporterClasses.size() == 0; } finally { configurationLock.unlock(); } } protected WebComponentConfigurationImpl constructConfigurations( - String tag, Class> exporterClass) { + String tag, + Class> exporterClass) { - Instantiator instantiator = - VaadinService.getCurrent().getInstantiator(); + Instantiator instantiator = VaadinService.getCurrent() + .getInstantiator(); - WebComponentExporter exporter = - instantiator.getOrCreate(exporterClass); + WebComponentExporter exporter = instantiator + .getOrCreate(exporterClass); return new WebComponentConfigurationImpl<>(tag, exporter); } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/startup/ServletDeployerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/startup/ServletDeployerTest.java index d60fd3a240c..01fec699932 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/startup/ServletDeployerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/startup/ServletDeployerTest.java @@ -1,5 +1,28 @@ package com.vaadin.flow.server.startup; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.easymock.EasyMock.anyBoolean; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.anyString; +import static org.easymock.EasyMock.capture; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.mock; +import static org.easymock.EasyMock.newCapture; +import static org.easymock.EasyMock.replay; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import javax.servlet.Registration; import javax.servlet.Servlet; import javax.servlet.ServletConfig; @@ -8,40 +31,21 @@ import javax.servlet.ServletRegistration; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import com.vaadin.flow.component.Component; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouteConfiguration; -import com.vaadin.flow.server.Constants; -import com.vaadin.flow.server.RouteRegistry; -import com.vaadin.flow.server.VaadinServlet; import org.easymock.Capture; import org.easymock.CaptureType; import org.easymock.EasyMock; import org.junit.Before; import org.junit.Test; -import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; -import static org.easymock.EasyMock.anyBoolean; -import static org.easymock.EasyMock.anyObject; -import static org.easymock.EasyMock.anyString; -import static org.easymock.EasyMock.capture; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.mock; -import static org.easymock.EasyMock.newCapture; -import static org.easymock.EasyMock.replay; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.WebComponentExporter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteConfiguration; +import com.vaadin.flow.server.Constants; +import com.vaadin.flow.server.RouteRegistry; +import com.vaadin.flow.server.VaadinServlet; +import com.vaadin.flow.server.webcomponent.WebComponentConfigurationRegistry; public class ServletDeployerTest { private final ServletDeployer deployer = new ServletDeployer(); @@ -89,8 +93,17 @@ public void clearCaptures() { } @Test - public void automaticallyRegisterTwoServletsWhenNoServletsPresent() { - deployer.contextInitialized(getContextEvent(true)); + public void hasRoutes_automaticallyRegisterTwoServletsWhenNoServletsPresent() { + deployer.contextInitialized(getContextEvent(true, false)); + + assertMappingsCount(2); + assertMappingIsRegistered(ServletDeployer.class.getName(), "/*"); + assertMappingIsRegistered("frontendFilesServlet", "/frontend/*"); + } + + @Test + public void hasWebComponents_automaticallyRegisterTwoServletsWhenNoServletsPresent() { + deployer.contextInitialized(getContextEvent(false, true)); assertMappingsCount(2); assertMappingIsRegistered(ServletDeployer.class.getName(), "/*"); @@ -99,7 +112,7 @@ public void automaticallyRegisterTwoServletsWhenNoServletsPresent() { @Test public void doNotRegisterAnythingIfRegistrationIsDisabled() { - deployer.contextInitialized(getContextEvent(true, + deployer.contextInitialized(getContextEvent(true, true, getServletRegistration("testServlet", TestServlet.class, singletonList("/test/*"), singletonMap( @@ -110,8 +123,8 @@ public void doNotRegisterAnythingIfRegistrationIsDisabled() { } @Test - public void mainServletIsNotRegisteredWhenNoRoutesArePresent() { - deployer.contextInitialized(getContextEvent(false)); + public void noRoutes_noWebComponents_mainServletIsNotRegistered() { + deployer.contextInitialized(getContextEvent(false, false)); assertMappingsCount(1); assertMappingIsRegistered("frontendFilesServlet", "/frontend/*"); @@ -119,7 +132,7 @@ public void mainServletIsNotRegisteredWhenNoRoutesArePresent() { @Test public void mainServletIsNotRegisteredWhenVaadinServletIsPresent() { - deployer.contextInitialized(getContextEvent(true, + deployer.contextInitialized(getContextEvent(true, true, getServletRegistration("testServlet", TestVaadinServlet.class, singletonList("/test/*"), emptyMap()))); @@ -129,7 +142,7 @@ public void mainServletIsNotRegisteredWhenVaadinServletIsPresent() { @Test public void frontendServletIsNotRegisteredWhenProductionModeIsActive() { - deployer.contextInitialized(getContextEvent(true, + deployer.contextInitialized(getContextEvent(true, true, getServletRegistration("testServlet", TestServlet.class, singletonList("/test/*"), singletonMap( @@ -142,7 +155,7 @@ public void frontendServletIsNotRegisteredWhenProductionModeIsActive() { @Test public void frontendServletIsRegisteredWhenAtLeastOneServletHasDevelopmentMode() { - deployer.contextInitialized(getContextEvent(true, + deployer.contextInitialized(getContextEvent(true, true, getServletRegistration("testServlet1", TestServlet.class, singletonList("/test1/*"), singletonMap( Constants.SERVLET_PARAMETER_PRODUCTION_MODE, @@ -165,7 +178,7 @@ public void frontendServletIsRegisteredInProductionModeIfOriginalFrontendResourc params.put(Constants.USE_ORIGINAL_FRONTEND_RESOURCES, "true"); deployer.contextInitialized( - getContextEvent(true, getServletRegistration("test", + getContextEvent(true, true, getServletRegistration("test", TestServlet.class, emptyList(), params))); assertMappingsCount(2); @@ -175,7 +188,7 @@ public void frontendServletIsRegisteredInProductionModeIfOriginalFrontendResourc @Test public void servletIsNotRegisteredWhenAnotherHasTheSamePathMapping_mainServlet() { - deployer.contextInitialized(getContextEvent(true, + deployer.contextInitialized(getContextEvent(true, true, getServletRegistration("test", TestServlet.class, singletonList("/*"), Collections.emptyMap()))); @@ -185,7 +198,7 @@ public void servletIsNotRegisteredWhenAnotherHasTheSamePathMapping_mainServlet() @Test public void servletIsNotRegisteredWhenAnotherHasTheSamePathMapping_frontendServlet() { - deployer.contextInitialized(getContextEvent(true, + deployer.contextInitialized(getContextEvent(true, true, getServletRegistration("test", TestServlet.class, singletonList("/frontend/*"), Collections.emptyMap()))); @@ -223,7 +236,9 @@ private void assertMappingIsRegistered(String servletName, pathIndex, servletNameIndex); } + @SuppressWarnings({ "unchecked", "rawtypes" }) private ServletContextEvent getContextEvent(boolean addRoutes, + boolean addWebComponents, ServletRegistration... servletRegistrations) { ServletRegistration.Dynamic dynamicMock = mock( ServletRegistration.Dynamic.class); @@ -241,7 +256,7 @@ private ServletContextEvent getContextEvent(boolean addRoutes, // seems to be a compiler bug, since fails to compile with the actual // types specified (or being inlined) but works with raw type - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "serial" }) Map hack = Stream.of(servletRegistrations).collect( Collectors.toMap(Registration::getName, Function.identity())); expect(contextMock.getServletRegistrations()).andReturn(hack) @@ -256,16 +271,33 @@ private ServletContextEvent getContextEvent(boolean addRoutes, .forRegistry(registry); routeConfiguration.update(() -> { routeConfiguration.getHandledRegistry().clean(); - routeConfiguration.setAnnotatedRoute(ComponentWithRoute.class); + routeConfiguration.setAnnotatedRoute( + ComponentWithRoute.class); }); return registry; }).anyTimes(); - } else { - expect(contextMock.getAttribute(anyString())).andReturn(null) - .anyTimes(); - contextMock.setAttribute(anyObject(), anyObject()); - EasyMock.expectLastCall(); } + if (addWebComponents) { + expect(contextMock.getAttribute( + WebComponentConfigurationRegistry.class.getName())) + .andAnswer(() -> { + + WebComponentConfigurationRegistry registry = new WebComponentConfigurationRegistry() { + }; + registry.setExporters( + (Map) Collections.singletonMap("foo", + WebComponentExporter.class)); + + return registry; + }).anyTimes(); + + } + + expect(contextMock.getAttribute(anyString())).andReturn(null) + .anyTimes(); + contextMock.setAttribute(anyObject(), anyObject()); + contextMock.setAttribute(anyObject(), anyObject()); + EasyMock.expectLastCall(); expect(contextMock.getInitParameterNames()) .andReturn(Collections.emptyEnumeration()).anyTimes();