From e5b5ccd6055815fb7daf9c4323362acfc7f26f25 Mon Sep 17 00:00:00 2001 From: Sun Zhe <31067185+ZheSun88@users.noreply.github.com> Date: Tue, 10 Jul 2018 14:15:56 +0300 Subject: [PATCH] Support @Meta annotation (#4378) --- .../com/vaadin/flow/component/page/Meta.java | 68 ++++++++++ .../vaadin/flow/server/BootstrapHandler.java | 4 + .../vaadin/flow/server/BootstrapUtils.java | 33 +++++ .../flow/server/BootstrapHandlerTest.java | 119 +++++++++++++++--- 4 files changed, 208 insertions(+), 16 deletions(-) create mode 100644 flow-server/src/main/java/com/vaadin/flow/component/page/Meta.java diff --git a/flow-server/src/main/java/com/vaadin/flow/component/page/Meta.java b/flow-server/src/main/java/com/vaadin/flow/component/page/Meta.java new file mode 100644 index 00000000000..cc7e6bf16b0 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/page/Meta.java @@ -0,0 +1,68 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines a meta tag with customized name and content that will be added to the + * HTML of the host page of a UI class. + * + * @author Vaadin Ltd + * @since 1.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Repeatable(Meta.Container.class) +public @interface Meta { + /** + * Gets the custom tag name. + * + * @return the custom tag name + */ + String name(); + + /** + * Gets the custom tag content. + * + * @return the custom tag content + */ + String content(); + + /** + * Internal annotation to enable use of multiple {@link Meta} annotations. + */ + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Documented + public @interface Container { + /** + * Internally used to enable use of multiple {@link Meta} annotations. + * + * @return an array of the Meta annotations + */ + Meta[] value(); + } +} 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 d404b25d05f..314aa749ca0 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 @@ -694,6 +694,10 @@ private static void setupMetaAndTitle(Element head, .attr(CONTENT_ATTRIBUTE, BootstrapUtils .getViewportContent(context).orElse(Viewport.DEFAULT)); + if (!BootstrapUtils.getMetaTargets(context).isEmpty()) { + BootstrapUtils.getMetaTargets(context).forEach((name,content)->head.appendElement(META_TAG) + .attr("name",name).attr(CONTENT_ATTRIBUTE,content)); + } resolvePageTitle(context).ifPresent(title -> { if (!title.isEmpty()) { head.appendElement("title").appendText(title); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapUtils.java b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapUtils.java index 496e09dbefc..4d79da40922 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapUtils.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapUtils.java @@ -21,6 +21,7 @@ import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -38,6 +39,7 @@ import com.vaadin.flow.component.dependency.HtmlImport; import com.vaadin.flow.component.page.BodySize; import com.vaadin.flow.component.page.Inline; +import com.vaadin.flow.component.page.Meta; import com.vaadin.flow.component.page.Viewport; import com.vaadin.flow.internal.ReflectTools; import com.vaadin.flow.router.AfterNavigationEvent; @@ -112,6 +114,37 @@ static Optional getViewportContent( .map(Viewport::value); } + /** + * Returns the map which contains name and content of the customized meta + * tag for the target route chain that was navigated to, specified with + * {@link Meta} on the {@link Route} class or the {@link ParentLayout} of + * the route. + * + * @param context + * the bootstrap context + * @return the map contains name and content value string for the customized + * meta tag + */ + static Map getMetaTargets( + BootstrapHandler.BootstrapContext context) { + List metaAnnotations = context.getPageConfigurationAnnotations(Meta.class); + boolean illegalValue = false; + Map map = new HashMap<>(); + for (Meta meta : metaAnnotations) { + if (!meta.name().isEmpty() && !meta.content().isEmpty()) { + map.put(meta.name(), meta.content()); + } else { + illegalValue = true; + break; + } + } + if (illegalValue) { + throw new IllegalStateException( + "Meta tags added via Meta annotation contain null value on name or content attribute."); + } + return map; + } + /** * Get initial page settings if a {@link PageConfigurator} is found for the * current component tree after navigation has resolved. diff --git a/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java index 8836edb9f05..dd99c23e62f 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java @@ -1,6 +1,7 @@ package com.vaadin.flow.server; import javax.servlet.http.HttpServletRequest; + import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -13,6 +14,17 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.io.IOUtils; +import org.hamcrest.CoreMatchers; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +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.Component; import com.vaadin.flow.component.Html; import com.vaadin.flow.component.Tag; @@ -23,6 +35,7 @@ import com.vaadin.flow.component.dependency.StyleSheet; import com.vaadin.flow.component.page.BodySize; import com.vaadin.flow.component.page.Inline; +import com.vaadin.flow.component.page.Meta; import com.vaadin.flow.component.page.TargetElement; import com.vaadin.flow.component.page.Viewport; import com.vaadin.flow.internal.UsageStatistics; @@ -44,18 +57,9 @@ import com.vaadin.flow.theme.AbstractTheme; import com.vaadin.flow.theme.Theme; import com.vaadin.tests.util.MockDeploymentConfiguration; -import org.apache.commons.io.IOUtils; -import org.hamcrest.CoreMatchers; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mockito; import static org.hamcrest.Matchers.is; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; @@ -1480,6 +1484,82 @@ public void uiInitialization_changingListenersOnEventWorks() { secondInit.getUI(), uiReference.get()); } + @Route("") + @Tag(Tag.DIV) + @Meta(name = "apple-mobile-web-app-capable", content = "yes") + @Meta(name = "apple-mobile-web-app-status-bar-style", content = "black") + public static class MetaAnnotations extends Component { + } + + @Test + public void addMultiMetaTagViaMetaAnnotation_MetaSizeCorrect_ContentCorrect() + throws InvalidRouteConfigurationException { + initUI(testUI, createVaadinRequest(), + Collections.singleton(MetaAnnotations.class)); + + Document page = BootstrapHandler.getBootstrapPage( + new BootstrapContext(request, null, session, testUI)); + + Element head = page.head(); + Elements metas = head.getElementsByTag("meta"); + + Assert.assertEquals(5, metas.size()); + Element meta = metas.get(0); + assertEquals("Content-Type", meta.attr("http-equiv")); + assertEquals("text/html; charset=utf-8", meta.attr("content")); + + meta = metas.get(1); + assertEquals("X-UA-Compatible", meta.attr("http-equiv")); + assertEquals("IE=edge", meta.attr("content")); + + meta = metas.get(2); + assertEquals(BootstrapHandler.VIEWPORT, meta.attr("name")); + assertEquals(Viewport.DEFAULT, + meta.attr(BootstrapHandler.CONTENT_ATTRIBUTE)); + + meta = metas.get(3); + assertEquals("apple-mobile-web-app-status-bar-style", + meta.attr("name")); + assertEquals("black", + meta.attr(BootstrapHandler.CONTENT_ATTRIBUTE)); + + meta = metas.get(4); + assertEquals("apple-mobile-web-app-capable", meta.attr("name")); + assertEquals("yes", + meta.attr(BootstrapHandler.CONTENT_ATTRIBUTE)); + } + + @Route("") + @Tag(Tag.DIV) + @Meta(name = "", content = "yes") + public static class MetaAnnotationsContainsNull extends Component { + } + + @Test(expected = IllegalStateException.class) + public void AnnotationContainsNullValue_ExceptionThrown() + throws InvalidRouteConfigurationException { + initUI(testUI, createVaadinRequest(), + Collections.singleton(MetaAnnotationsContainsNull.class)); + + Document page = BootstrapHandler.getBootstrapPage( + new BootstrapContext(request, null, session, testUI)); + } + + @Tag(Tag.DIV) + @Meta(name = "apple-mobile-web-app-capable", content = "yes") + public static class MetaAnnotationsWithoutRoute extends Component { + } + + @Test(expected = InvalidRouteConfigurationException.class) + public void AnnotationsWithoutRoute_ExceptionThrown() + throws InvalidRouteConfigurationException { + initUI(testUI, createVaadinRequest(), + Collections.singleton(MetaAnnotationsWithoutRoute.class)); + + Document page = BootstrapHandler.getBootstrapPage( + new BootstrapContext(request, null, session, testUI)); + } + private void assertStringEquals(String message, String expected, String actual) { Assert.assertThat(message, @@ -1573,18 +1653,25 @@ public void viewportAnnotationOverridesDefault() throws Exception { @Test public void testUIConfiguration_usingPageSettings() throws Exception { - Assert.assertTrue("By default loading indicator is themed", testUI.getLoadingIndicatorConfiguration().isApplyDefaultTheme()); + Assert.assertTrue("By default loading indicator is themed", testUI + .getLoadingIndicatorConfiguration().isApplyDefaultTheme()); - initUI(testUI, createVaadinRequest(), Collections.singleton(InitialPageConfiguratorRoute.class)); + initUI(testUI, createVaadinRequest(), + Collections.singleton(InitialPageConfiguratorRoute.class)); Document page = BootstrapHandler.getBootstrapPage( new BootstrapContext(request, null, session, testUI)); - Assert.assertFalse("Default indicator theme is not themed anymore", testUI.getLoadingIndicatorConfiguration().isApplyDefaultTheme()); + Assert.assertFalse("Default indicator theme is not themed anymore", + testUI.getLoadingIndicatorConfiguration() + .isApplyDefaultTheme()); - Assert.assertEquals(InitialPageConfiguratorRoute.SECOND_DELAY, testUI.getLoadingIndicatorConfiguration().getSecondDelay()); + Assert.assertEquals(InitialPageConfiguratorRoute.SECOND_DELAY, + testUI.getLoadingIndicatorConfiguration().getSecondDelay()); - Assert.assertEquals(PushMode.MANUAL, testUI.getPushConfiguration().getPushMode()); + Assert.assertEquals(PushMode.MANUAL, + testUI.getPushConfiguration().getPushMode()); - Assert.assertTrue(testUI.getReconnectDialogConfiguration().isDialogModal()); + Assert.assertTrue( + testUI.getReconnectDialogConfiguration().isDialogModal()); } }