diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentMetaData.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentMetaData.java index a2c69e08b63..c860450d55e 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentMetaData.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/ComponentMetaData.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import com.vaadin.flow.component.Component; @@ -34,6 +35,7 @@ import com.vaadin.flow.component.dependency.Uses; import com.vaadin.flow.internal.AnnotationReader; import com.vaadin.flow.internal.ReflectTools; +import com.vaadin.flow.shared.ui.LoadMode; /** * Immutable meta data related to a component class. @@ -48,11 +50,11 @@ public class ComponentMetaData { * Framework internal class, thus package-private. */ public static class DependencyInfo { - private final List htmlImports = new ArrayList<>(); + private final List htmlImports = new ArrayList<>(); private final List javaScripts = new ArrayList<>(); private final List styleSheets = new ArrayList<>(); - List getHtmlImports() { + List getHtmlImports() { return Collections.unmodifiableList(htmlImports); } @@ -66,6 +68,27 @@ List getStyleSheets() { } + public static class HtmlImportDependency { + + private final Collection uris; + + private final LoadMode loadMode; + + private HtmlImportDependency(Collection uris, + LoadMode loadMode) { + this.uris = Collections.unmodifiableCollection(uris); + this.loadMode = loadMode; + } + + public Collection getUris() { + return uris; + } + + public LoadMode getLoadMode() { + return loadMode; + } + } + /** * Synchronized properties defined for a {@link Component} class. *

@@ -126,8 +149,8 @@ private static DependencyInfo findDependencies( scannedClasses.add(componentClass); - dependencyInfo.htmlImports.addAll( - AnnotationReader.getHtmlImportAnnotations(componentClass)); + dependencyInfo.htmlImports + .addAll(getHtmlImportDependencies(componentClass)); dependencyInfo.javaScripts.addAll( AnnotationReader.getJavaScriptAnnotations(componentClass)); dependencyInfo.styleSheets.addAll( @@ -169,6 +192,22 @@ public DependencyInfo getDependencyInfo() { return dependencyInfo; } + private static Collection getHtmlImportDependencies( + Class componentClass) { + return AnnotationReader.getHtmlImportAnnotations(componentClass) + .stream().map(ComponentMetaData::getHtmlImportDependencies) + .collect(Collectors.toList()); + } + + private static HtmlImportDependency getHtmlImportDependencies( + HtmlImport htmlImport) { + String value = htmlImport.value(); + HtmlDependencyParser parser = new HtmlDependencyParser(value); + + return new HtmlImportDependency(parser.parseDependencies(), + htmlImport.loadMode()); + } + /** * Scans the class for {@link Synchronize} annotations and gathers the data. * diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/HtmlDependencyParser.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/HtmlDependencyParser.java new file mode 100644 index 00000000000..dfb07bb67aa --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/HtmlDependencyParser.java @@ -0,0 +1,165 @@ +/* + * Copyright 2000-2017 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.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.server.VaadinServlet; +import com.vaadin.flow.server.VaadinSession; + +/** + * Html import dependencies parser. + *

+ * It takes the an HTML import url as a root and parse the content recursively + * collecting html import dependencies. + * + * @author Vaadin Ltd + * + */ +public class HtmlDependencyParser { + + static class HtmlDependenciesCache { + private final Set dependencies = new HashSet<>(); + + void addDependency(String url) { + dependencies.add(url); + } + + boolean hasDependency(String url) { + return dependencies.contains(url); + } + } + + private final String root; + + /** + * Creates a new instance using the given {@code uri} as a root. + * + * @param uri + * HTML import uri + */ + public HtmlDependencyParser(String uri) { + this.root = uri; + } + + Collection parseDependencies() { + Set dependencies = new HashSet<>(); + + parseDependencies(root, dependencies); + + return dependencies; + } + + private void parseDependencies(String path, Set dependencies) { + if (dependencies.contains(path)) { + return; + } + dependencies.add(path); + + VaadinSession session = VaadinSession.getCurrent(); + VaadinServlet servlet = VaadinServlet.getCurrent(); + if (servlet == null || session == null) { + /* + * Cannot happen in runtime. + * + * But not all unit tests set it. Let's just don't proceed further. + */ + return; + } + + assert session.hasLock(); + HtmlDependenciesCache cache = session + .getAttribute(HtmlDependenciesCache.class); + if (cache == null) { + cache = new HtmlDependenciesCache(); + session.setAttribute(HtmlDependenciesCache.class, cache); + } + + String resolvedResource = servlet.resolveResource(path); + + if (cache.hasDependency(resolvedResource)) { + return; + } + cache.addDependency(resolvedResource); + + try (InputStream content = servlet.getServletContext() + .getResourceAsStream(resolvedResource)) { + if (content == null) { + getLogger().info( + "Can't find resource '%s' via the servlet context", + path); + } else { + parseHtmlImports(content, path) + .map(uri -> resolveUri(uri, path)) + .forEach(uri -> parseDependencies(uri, dependencies)); + } + } catch (IOException exception) { + // ignore exception on close() + getLogger().debug("Couldn't close template input stream", + exception); + } + } + + private String resolveUri(String relative, String base) { + if (relative.startsWith("/")) { + return relative; + } + try { + URI uri = new URI(base); + return uri.resolve(relative).toString(); + } catch (URISyntaxException exception) { + getLogger().debug( + "Couldn't make URI for {}. The path {} will be used as is.", + base, relative, exception); + } + return relative; + } + + private Stream parseHtmlImports(InputStream content, String path) { + assert content != null; + try { + Document parsedDocument = Jsoup.parse(content, + StandardCharsets.UTF_8.name(), ""); + + return parsedDocument.getElementsByTag("link").stream() + .filter(link -> link.hasAttr("rel") && link.hasAttr("href")) + .filter(link -> link.attr("rel").equals("import")) + .map(link -> link.attr("href")); + } catch (IOException exception) { + getLogger().info( + "Can't parse the template declared using '%s' path", path, + exception); + } + return Stream.empty(); + } + + private Logger getLogger() { + return LoggerFactory.getLogger(HtmlDependencyParser.class); + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java index ef6089009cc..64ee89d85a2 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java @@ -38,6 +38,7 @@ import com.vaadin.flow.component.dependency.JavaScript; import com.vaadin.flow.component.dependency.StyleSheet; import com.vaadin.flow.component.internal.ComponentMetaData.DependencyInfo; +import com.vaadin.flow.component.internal.ComponentMetaData.HtmlImportDependency; import com.vaadin.flow.component.page.Page; import com.vaadin.flow.component.page.Page.ExecutionCanceler; import com.vaadin.flow.dom.Element; @@ -779,16 +780,21 @@ public void addComponentDependencies( Page page = ui.getPage(); DependencyInfo dependencies = ComponentUtil .getDependencies(componentClass); - dependencies.getHtmlImports().forEach(html -> page - .addHtmlImport(getHtmlImportValue(html), html.loadMode())); + dependencies.getHtmlImports().stream() + .forEach(html -> addHtmlImport(html, page)); dependencies.getJavaScripts() .forEach(js -> page.addJavaScript(js.value(), js.loadMode())); dependencies.getStyleSheets().forEach(styleSheet -> page .addStyleSheet(styleSheet.value(), styleSheet.loadMode())); } - private String getHtmlImportValue(HtmlImport html) { - String importValue = html.value(); + private void addHtmlImport(HtmlImportDependency dependency, Page page) { + dependency.getUris().stream() + .forEach(uri -> page.addHtmlImport(getHtmlImportValue(uri), + dependency.getLoadMode())); + } + + private String getHtmlImportValue(String importValue) { if (theme != null) { return VaadinServlet.getCurrent().getUrlTranslation(theme, importValue); @@ -864,7 +870,7 @@ public void setContinueNavigationAction( /** * Gets the application id tied with this UI. Different applications in the * same page have different unique ids. - * + * * @return the id of the application tied with this UI */ public String getAppId() { diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinServlet.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinServlet.java index 7af3370ca47..6fb531e74ed 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinServlet.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinServlet.java @@ -15,12 +15,6 @@ */ package com.vaadin.flow.server; -import javax.servlet.ServletConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; import java.net.MalformedURLException; @@ -34,6 +28,13 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.slf4j.LoggerFactory; import com.vaadin.flow.component.UI; @@ -373,9 +374,9 @@ protected boolean serveStaticOrWebJarRequest(HttpServletRequest request, staticFileServer.serveStaticResource(request, response); return true; } - - return webJarServer != null && webJarServer - .tryServeWebJarResource(request, response); + + return webJarServer != null + && webJarServer.tryServeWebJarResource(request, response); } /** @@ -529,7 +530,7 @@ private boolean ensureCookiesEnabled(VaadinServletRequest request, * * @deprecated As of 7.0. Will likely change or be removed in a future * version - * + * * @return current application URL */ @Deprecated @@ -579,7 +580,8 @@ public void destroy() { * Escapes characters to html entities. An exception is made for some "safe * characters" to keep the text somewhat readable. * - * @param unsafe non-escaped string + * @param unsafe + * non-escaped string * @return a safe string to be added inside an html tag * * @deprecated As of 7.0. Will likely change or be removed in a future @@ -633,6 +635,48 @@ public String getUrlTranslation(AbstractTheme theme, } } + /** + * Resolves the given {@code url} resource using vaadin URI resolver. + * + * @param url + * the resource to resolve + * @return resolved resource + */ + public String resolveResource(String url) { + VaadinRequest request = VaadinRequest.getCurrent(); + VaadinSession session = VaadinSession.getCurrent(); + if (request == null || session == null) { + /* + * Cannot happen in runtime. + * + * But not all unit tests set it. Let's just return null. + */ + return null; + } + + VaadinUriResolverFactory uriResolverFactory = session + .getAttribute(VaadinUriResolverFactory.class); + + String resolvedUrl = uriResolverFactory.toServletContextPath(request, + url); + + if (webJarServer != null) { + Optional webJarUrl = webJarServer + .getWebJarResourcePath(resolvedUrl); + try { + if (webJarUrl.isPresent() && getServletContext() + .getResource(webJarUrl.get()) != null) { + return webJarUrl.get(); + } + } catch (MalformedURLException exception) { + LoggerFactory.getLogger(VaadinServlet.class).trace( + "Failed to parse url {}.", webJarUrl.get(), exception); + } + } + + return resolvedUrl; + } + private final String computeUrlTranslation(AbstractTheme theme, String urlToTranslate) { String translatedUrl = theme.translateUrl(urlToTranslate); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/webjar/WebJarServer.java b/flow-server/src/main/java/com/vaadin/flow/server/webjar/WebJarServer.java index facb49092d0..c09be55ccd9 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/webjar/WebJarServer.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/webjar/WebJarServer.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.Serializable; import java.net.URL; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -76,7 +77,6 @@ public WebJarServer(DeploymentConfiguration deploymentConfiguration) { ApplicationConstants.CONTEXT_PROTOCOL_PREFIX.length()) + "bower_components/"; urlPattern = Pattern.compile("^([/.]?[/..]*)" + prefix); - } /** @@ -113,7 +113,7 @@ public boolean tryServeWebJarResource(HttpServletRequest request, /** * Check if a file is existent in a WebJar. - * + * * @param filePathInContext * servlet context path for file * @param servletContext @@ -124,6 +124,23 @@ public boolean tryServeWebJarResource(HttpServletRequest request, */ public boolean hasWebJarResource(String filePathInContext, ServletContext servletContext) throws IOException { + Optional webJarPath = getWebJarResourcePath(filePathInContext); + if (!webJarPath.isPresent()) { + return false; + } + + return servletContext.getResource(webJarPath.get()) != null; + } + + /** + * Gets web jar resource path if it exists. + * + * @param filePathInContext + * servlet context path for file + * @return an optional web jar resource path, or an empty optional if the + * resource is not web jar resource + */ + public Optional getWebJarResourcePath(String filePathInContext) { String webJarPath = null; Matcher matcher = urlPattern.matcher(filePathInContext); @@ -132,11 +149,7 @@ public boolean hasWebJarResource(String filePathInContext, webJarPath = getWebJarPath( filePathInContext.substring(matcher.group(1).length())); } - if (webJarPath == null) { - return false; - } - - return servletContext.getResource(webJarPath) != null; + return Optional.ofNullable(webJarPath); } private String getWebJarPath(String path) { diff --git a/flow-server/src/test/java/com/vaadin/flow/component/internal/HtmlDependencyParserTest.java b/flow-server/src/test/java/com/vaadin/flow/component/internal/HtmlDependencyParserTest.java new file mode 100644 index 00000000000..dcb9233fdf8 --- /dev/null +++ b/flow-server/src/test/java/com/vaadin/flow/component/internal/HtmlDependencyParserTest.java @@ -0,0 +1,185 @@ +/* + * Copyright 2000-2017 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.internal; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +import javax.servlet.ServletContext; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.flow.component.internal.HtmlDependencyParser.HtmlDependenciesCache; +import com.vaadin.flow.internal.CurrentInstance; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.VaadinServlet; +import com.vaadin.flow.server.VaadinServletService; +import com.vaadin.flow.server.VaadinSession; + +import net.jcip.annotations.NotThreadSafe; + +@NotThreadSafe +public class HtmlDependencyParserTest { + + private VaadinSession session; + private VaadinServlet servlet; + private ServletContext context; + private VaadinServletService service; + + @Before + public void setUp() { + service = Mockito.mock(VaadinServletService.class); + CurrentInstance.set(VaadinService.class, service); + + servlet = Mockito.mock(VaadinServlet.class); + Mockito.when(service.getServlet()).thenReturn(servlet); + + session = Mockito.mock(VaadinSession.class); + CurrentInstance.set(VaadinSession.class, session); + + context = Mockito.mock(ServletContext.class); + + Mockito.when(servlet.getServletContext()).thenReturn(context); + Mockito.when(session.hasLock()).thenReturn(true); + } + + @After + public void tearDown() { + CurrentInstance.clearAll(); + } + + @Test + public void mainImportDoesntHaveContentToParse_mainInportIsReturnedAndNoExceptions() { + String root = "foo.html"; + HtmlDependencyParser parser = new HtmlDependencyParser(root); + Collection dependencies = parser.parseDependencies(); + Assert.assertTrue("Dependencies parser doesn't return the root URI", + dependencies.contains(root)); + } + + @Test + public void oneLevelDependency_variousURIs_URIsAreCollectedCorrectly() { + String root = "baz/foo.html"; + HtmlDependencyParser parser = new HtmlDependencyParser(root); + + String resolvedRoot = "baz/bar/" + root; + Mockito.when(servlet.resolveResource(root)).thenReturn(resolvedRoot); + + String importContent = "" + + "" + + "" + + ""; + InputStream stream = new ByteArrayInputStream( + importContent.getBytes(StandardCharsets.UTF_8)); + Mockito.when(context.getResourceAsStream(resolvedRoot)) + .thenReturn(stream); + + Collection dependencies = parser.parseDependencies(); + + Assert.assertEquals(5, dependencies.size()); + + Assert.assertTrue("Dependencies parser doesn't return the root URI", + dependencies.contains(root)); + Assert.assertTrue( + "Dependencies parser doesn't return the simple relative URI (same parent)", + dependencies.contains("baz/relative1.html")); + Assert.assertTrue( + "Dependencies parser doesn't return the realtive URI which is located in the parent folder", + dependencies.contains("relative2.html")); + Assert.assertTrue( + "Dependencies parser doesn't return the realtive URI which is located sub folder", + dependencies.contains("baz/sub/relative3.html")); + Assert.assertTrue("Dependencies parser doesn't return the absolute URI", + dependencies.contains("/absolute.html")); + } + + @Test + public void nestedDependencyLevels_variousURIs_URIsAreCollectedCorrectly() { + String root = "foo.html"; + HtmlDependencyParser parser = new HtmlDependencyParser(root); + + String resolvedRoot = "baz/bar/" + root; + Mockito.when(servlet.resolveResource(root)).thenReturn(resolvedRoot); + + String importContent = ""; + InputStream stream = new ByteArrayInputStream( + importContent.getBytes(StandardCharsets.UTF_8)); + Mockito.when(context.getResourceAsStream(resolvedRoot)) + .thenReturn(stream); + + String resolvedRelative = "baz/bar/relative.html"; + Mockito.when(servlet.resolveResource("relative.html")) + .thenReturn(resolvedRelative); + + InputStream relativeContent = new ByteArrayInputStream( + "" + .getBytes(StandardCharsets.UTF_8)); + Mockito.when(context.getResourceAsStream(resolvedRelative)) + .thenReturn(relativeContent); + + Collection dependencies = parser.parseDependencies(); + + Assert.assertEquals(3, dependencies.size()); + + Assert.assertTrue("Dependencies parser doesn't return the root URI", + dependencies.contains(root)); + Assert.assertTrue( + "Dependencies parser doesn't return the simple relative URI (same parent)", + dependencies.contains("relative.html")); + Assert.assertTrue( + "Dependencies parser doesn't return the realtive URI which is located in the parent folder", + dependencies.contains("relative1.html")); + } + + @Test + public void dependenciesAreCached() { + String root = "foo.html"; + HtmlDependencyParser parser = new HtmlDependencyParser(root); + + String resolvedRoot = "baz/bar/" + root; + Mockito.when(servlet.resolveResource(root)).thenReturn(resolvedRoot); + + String importContent = ""; + InputStream stream = new ByteArrayInputStream( + importContent.getBytes(StandardCharsets.UTF_8)); + Mockito.when(context.getResourceAsStream(resolvedRoot)) + .thenReturn(stream); + + HtmlDependenciesCache cache = new HtmlDependenciesCache(); + Mockito.when(session.getAttribute(HtmlDependenciesCache.class)) + .thenReturn(cache); + + Collection dependencies = parser.parseDependencies(); + + Assert.assertEquals(2, dependencies.size()); + Mockito.verify(context).getResourceAsStream(resolvedRoot); + + // call one more time + dependencies = parser.parseDependencies(); + // this time only root resource should be returned + Assert.assertEquals(1, dependencies.size()); + // and there shouldn't be one more call for reading the content of the + // import + Mockito.verify(context).getResourceAsStream(resolvedRoot); + } + +} diff --git a/flow-server/src/test/java/com/vaadin/flow/server/VaadinServletTest.java b/flow-server/src/test/java/com/vaadin/flow/server/VaadinServletTest.java index a8ff0dc53cb..9502b7e09bb 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/VaadinServletTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/VaadinServletTest.java @@ -15,13 +15,66 @@ */ package com.vaadin.flow.server; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; -import com.vaadin.flow.server.VaadinServlet; +import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.internal.CurrentInstance; +import net.jcip.annotations.NotThreadSafe; + +@NotThreadSafe public class VaadinServletTest { + private VaadinRequest request; + private VaadinSession session; + private ServletContext context; + private VaadinUriResolverFactory factory; + private VaadinServletService service; + + private VaadinServlet servlet = new VaadinServlet() { + @Override + public ServletContext getServletContext() { + return context; + } + + @Override + protected VaadinServletService createServletService() { + return service; + } + }; + + @Before + public void setUp() { + request = Mockito.mock(VaadinRequest.class); + CurrentInstance.set(VaadinRequest.class, request); + session = Mockito.mock(VaadinSession.class); + CurrentInstance.set(VaadinSession.class, session); + + context = Mockito.mock(ServletContext.class); + factory = Mockito.mock(VaadinUriResolverFactory.class); + + context = Mockito.mock(ServletContext.class); + + Mockito.when(session.getAttribute(VaadinUriResolverFactory.class)) + .thenReturn(factory); + } + + @After + public void tearDown() { + CurrentInstance.clearAll(); + } + @Test public void testGetLastPathParameter() { Assert.assertEquals("", @@ -59,4 +112,48 @@ public void testGetLastPathParameter() { Assert.assertEquals("", VaadinServlet .getLastPathParameter("http://myhost.com/a;hello/;b=1,c=2/")); } + + @Test + public void resolveResource_noWebJars_resolveViaResolverFactory() { + String path = "foo"; + String resolved = "bar"; + Mockito.when(factory.toServletContextPath(request, path)) + .thenReturn(resolved); + + Assert.assertEquals(resolved, servlet.resolveResource(path)); + } + + @Test + public void resolveResource_webJarResource_resolvedAsWebJarsResource() + throws ServletException, MalformedURLException { + String path = "foo"; + String frontendPrefix = "context://baz/"; + String resolved = "/baz/bower_components/"; + Mockito.when(factory.toServletContextPath(request, path)) + .thenReturn(resolved); + + service = Mockito.mock(VaadinServletService.class); + DeploymentConfiguration configuration = Mockito + .mock(DeploymentConfiguration.class); + + Mockito.when(configuration.getDevelopmentFrontendPrefix()) + .thenReturn(frontendPrefix); + + Mockito.when(configuration.areWebJarsEnabled()).thenReturn(true); + + Mockito.when(service.getDeploymentConfiguration()) + .thenReturn(configuration); + + ServletConfig config = Mockito.mock(ServletConfig.class); + servlet.init(config); + + CurrentInstance.set(VaadinRequest.class, request); + CurrentInstance.set(VaadinSession.class, session); + + URL url = new URL("http://example.com"); + String webjars = "/webjars/"; + Mockito.when(context.getResource(webjars)).thenReturn(url); + + Assert.assertEquals(webjars, servlet.resolveResource(path)); + } } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java index fa452c086aa..1da29646535 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/UidlWriterTest.java @@ -44,6 +44,7 @@ import org.junit.After; import org.junit.Test; +import org.mockito.Mockito; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Tag; @@ -67,7 +68,6 @@ import com.vaadin.flow.server.VaadinServletService; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.VaadinUriResolverFactory; -import com.vaadin.flow.server.communication.UidlWriter; import com.vaadin.flow.shared.ApplicationConstants; import com.vaadin.flow.shared.ui.Dependency; import com.vaadin.flow.shared.ui.LoadMode; @@ -446,8 +446,14 @@ private void assertInlineDependencies(List inlineDependencies, } private UI initializeUIForDependenciesTest(UI ui) { + ServletContext context = Mockito.mock(ServletContext.class); VaadinServletService service = new VaadinServletService( - new VaadinServlet(), new MockDeploymentConfiguration()) { + new VaadinServlet() { + @Override + public ServletContext getServletContext() { + return context; + } + }, new MockDeploymentConfiguration()) { RouterInterface router = new com.vaadin.flow.router.legacy.Router(); @Override diff --git a/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/template/ThemedTemplateView.java b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/template/ThemedTemplateView.java new file mode 100644 index 00000000000..92481b4cf57 --- /dev/null +++ b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/template/ThemedTemplateView.java @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2017 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.uitest.ui.template; + +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.HtmlImport; +import com.vaadin.flow.component.polymertemplate.PolymerTemplate; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.templatemodel.TemplateModel; +import com.vaadin.flow.theme.AbstractTheme; +import com.vaadin.flow.theme.Theme; +import com.vaadin.flow.uitest.ui.template.ThemedTemplateView.CustomTheme; + +@Tag("themed-template") +@HtmlImport("frontend://com/vaadin/flow/uitest/ui/template/ThemedTemplate.html") +@Route(value = "com.vaadin.flow.uitest.ui.template.ThemedTemplateView") +@Theme(CustomTheme.class) +public class ThemedTemplateView extends PolymerTemplate { + + public static class CustomTheme implements AbstractTheme { + + @Override + public String getBaseUrl() { + String pkg = ThemedTemplateView.class.getPackage().getName(); + int index = pkg.lastIndexOf('.'); + pkg = pkg.substring(0, index); + return "/" + pkg.replace('.', '/'); + } + + @Override + public String getThemeUrl() { + return getBaseUrl() + "/custom-theme"; + } + + } +} diff --git a/flow-tests/test-root-context/src/main/webapp/frontend/absolute.html b/flow-tests/test-root-context/src/main/webapp/frontend/absolute.html new file mode 100644 index 00000000000..534eb5d56c9 --- /dev/null +++ b/flow-tests/test-root-context/src/main/webapp/frontend/absolute.html @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/relative2.html b/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/relative2.html new file mode 100644 index 00000000000..915cd9c682a --- /dev/null +++ b/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/relative2.html @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/template/ThemedTemplate.html b/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/template/ThemedTemplate.html new file mode 100644 index 00000000000..901e2e4b6ef --- /dev/null +++ b/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/template/ThemedTemplate.html @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/template/relative1.html b/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/template/relative1.html new file mode 100644 index 00000000000..917e4100e5f --- /dev/null +++ b/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/custom-theme/template/relative1.html @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/template/ThemedTemplate.html b/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/template/ThemedTemplate.html new file mode 100644 index 00000000000..fa56e68eead --- /dev/null +++ b/flow-tests/test-root-context/src/main/webapp/frontend/com/vaadin/flow/uitest/ui/template/ThemedTemplate.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/template/ThemedTemplateIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/template/ThemedTemplateIT.java new file mode 100644 index 00000000000..de55c93a973 --- /dev/null +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/template/ThemedTemplateIT.java @@ -0,0 +1,92 @@ +/* + * Copyright 2000-2017 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.uitest.ui.template; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.flow.testutil.ChromeBrowserTest; + +public class ThemedTemplateIT extends ChromeBrowserTest { + + @Test + public void themedUrlsAreAdded() { + open(); + + // check that all imported templates are available in the DOM + WebElement template = findElement(By.tagName("themed-template")); + + Assert.assertTrue("The main template has no simple child Div inside it", + isPresentInShadowRoot(template, By.id("div"))); + Assert.assertTrue( + "The main template has no sub template which is imported by " + + "relative URL referring to the resource in the same folder", + isPresentInShadowRoot(template, By.id("relative1"))); + Assert.assertTrue( + "The main template has no sub template which is imported by " + + "relative URL referring to the resource in the parent folder", + isPresentInShadowRoot(template, By.id("relative2"))); + Assert.assertTrue( + "The main template has no sub template which is imported by " + + "absolute URL", + isPresentInShadowRoot(template, By.id("absolute"))); + + WebElement head = findElement(By.tagName("head")); + List links = head.findElements(By.tagName("link")); + Set hrefs = links.stream() + .map(link -> link.getAttribute("href")).filter(Objects::nonNull) + .map(this::getFilePath).collect(Collectors.toSet()); + + Assert.assertTrue( + "The themed HTML file for the template is not added as an HMTL import to the head", + hrefs.contains( + "/frontend/com/vaadin/flow/uitest/ui/custom-theme/template/ThemedTemplate.html")); + + Assert.assertTrue( + "The themed HTML file for the simple relative file (same location as the template file) " + + "is not added as an HMTL import to the head", + hrefs.contains( + "/frontend/com/vaadin/flow/uitest/ui/custom-theme/template/relative1.html")); + Assert.assertTrue( + "The themed HTML file for the relative file (located in the parent folder of the template file) " + + "is not added as an HMTL import to the head", + hrefs.contains( + "/frontend/com/vaadin/flow/uitest/ui/custom-theme/relative2.html")); + Assert.assertTrue( + "The themed HTML file for the absolute file " + + "is not added as an HMTL import to the head", + hrefs.contains("/frontend/absolute.html")); + } + + private String getFilePath(String url) { + try { + URL location = new URL(url); + return location.getFile(); + } catch (MalformedURLException e) { + throw new RuntimeException("Unexpected URL string '" + url + "'", + e); + } + } +}