From d90a680789e5c0aa0a88125ade904d8fb96a5809 Mon Sep 17 00:00:00 2001 From: Denis Anisimov Date: Fri, 4 Jun 2021 09:03:28 +0300 Subject: [PATCH 1/3] feat: reimplement @RouteScoped to keep beans for PreserveOnRefresh - presere beans for PreserveOnRefresh views - reimplement absent RouteScopeOwner semantic - do not allow to use (route) scope when it's not available fixes #369 --- .../routecontext/AbstractCountedView.java | 4 +- .../itest/routecontext/ErrorHandlerView.java | 44 ++- .../itest/routecontext/ErrorParentView.java | 6 +- .../cdi/itest/routecontext/ErrorView.java | 5 +- .../cdi/itest/routecontext/MasterView.java | 21 +- .../cdi/itest/routecontext/RootView.java | 17 +- .../vaadin/cdi/itest/RouteContextTest.java | 128 +++++++- .../com/vaadin/cdi/DeploymentValidator.java | 91 ++---- .../vaadin/cdi/annotation/RouteScoped.java | 45 +-- .../AbstractContextualStorageManager.java | 31 +- .../cdi/context/RouteScopedContext.java | 281 +++++++++++++++--- .../vaadin/cdi/DeploymentValidatorTest.java | 105 +++---- .../cdi/context/AbstractContextTest.java | 20 +- .../cdi/context/RouteContextNormalTest.java | 27 +- .../cdi/context/RouteContextPseudoTest.java | 33 +- .../cdi/context/TestNavigationTarget.java | 24 ++ 16 files changed, 620 insertions(+), 262 deletions(-) create mode 100644 vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedView.java index 75f40741..9dc46999 100644 --- a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedView.java +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedView.java @@ -16,11 +16,11 @@ package com.vaadin.cdi.itest.routecontext; -import com.vaadin.flow.component.html.Div; - import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import com.vaadin.flow.component.html.Div; + public abstract class AbstractCountedView extends Div implements CountedPerUI { @PostConstruct diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorHandlerView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorHandlerView.java index 3fcd8c7d..b799bf8c 100644 --- a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorHandlerView.java +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorHandlerView.java @@ -16,30 +16,60 @@ package com.vaadin.cdi.itest.routecontext; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import javax.servlet.http.HttpServletResponse; + import com.vaadin.cdi.annotation.RouteScopeOwner; import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.NativeButton; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.ErrorParameter; import com.vaadin.flow.router.HasErrorParameter; import com.vaadin.flow.router.ParentLayout; import com.vaadin.flow.router.RouterLink; -import javax.servlet.http.HttpServletResponse; - @RouteScoped @RouteScopeOwner(ErrorParentView.class) @ParentLayout(ErrorParentView.class) public class ErrorHandlerView extends AbstractCountedView - implements HasErrorParameter { + implements HasErrorParameter { public static final String PARENT = "parent"; + @Inject + @RouteScopeOwner(ErrorHandlerView.class) + private Instance buttonInjection; + + @Inject + @RouteScopeOwner(ErrorHandlerView.class) + private Instance divInjection; + + private boolean isSubDiv; + + private Component current; + @Override public int setErrorParameter(BeforeEnterEvent event, - ErrorParameter parameter) { - add( - new RouterLink(PARENT, ErrorParentView.class) - ); + ErrorParameter parameter) { + add(new RouterLink(PARENT, ErrorParentView.class)); + + NativeButton button = new NativeButton("switch content", ev -> { + remove(current); + if (isSubDiv) { + current = buttonInjection.get(); + } else { + current = divInjection.get(); + } + add(current); + isSubDiv = !isSubDiv; + }); + button.setId("switch-content"); + add(button); + current = buttonInjection.get(); + add(current); + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; } diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorParentView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorParentView.java index f09e9b98..6ee13ea5 100644 --- a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorParentView.java +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorParentView.java @@ -16,14 +16,16 @@ package com.vaadin.cdi.itest.routecontext; +import javax.annotation.PostConstruct; + +import com.vaadin.cdi.annotation.RouteScopeOwner; import com.vaadin.cdi.annotation.RouteScoped; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouterLayout; import com.vaadin.flow.router.RouterLink; -import javax.annotation.PostConstruct; - @RouteScoped +@RouteScopeOwner(ErrorParentView.class) @Route("error-layout") public class ErrorParentView extends AbstractCountedView implements RouterLayout { diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorView.java index e80899bc..380f1942 100644 --- a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorView.java +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorView.java @@ -23,12 +23,13 @@ @RouteScoped @Route("error") -public class ErrorView extends AbstractCountedView implements BeforeEnterObserver { +public class ErrorView extends AbstractCountedView + implements BeforeEnterObserver { @Override public void beforeEnter(BeforeEnterEvent event) { if (true) { - throw new NullPointerException(); + throw new CustomException(); } } diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/MasterView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/MasterView.java index 154fe436..713f6362 100644 --- a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/MasterView.java +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/MasterView.java @@ -16,6 +16,9 @@ package com.vaadin.cdi.itest.routecontext; +import javax.annotation.PostConstruct; +import javax.inject.Inject; + import com.vaadin.cdi.annotation.RouteScopeOwner; import com.vaadin.cdi.annotation.RouteScoped; import com.vaadin.flow.component.html.Div; @@ -27,9 +30,6 @@ import com.vaadin.flow.router.RouterLayout; import com.vaadin.flow.router.RouterLink; -import javax.annotation.PostConstruct; -import javax.inject.Inject; - @RouteScoped @Route("") @RoutePrefix("master") @@ -44,31 +44,20 @@ public class MasterView extends AbstractCountedView @Inject @RouteScopeOwner(MasterView.class) private AssignedBean assignedBean; - @Inject - @RouteScopeOwner(DetailApartView.class) - private ApartBean apartBean; private Label assignedLabel; - private Label apartLabel; @PostConstruct private void init() { assignedLabel = new Label(); assignedLabel.setId(ASSIGNED_BEAN_LABEL); - apartLabel = new Label(); - apartLabel.setId(APART_BEAN_LABEL); - add( - new Label("MASTER"), - new Div(assignedLabel), - new Div(apartLabel), + add(new Label("MASTER"), new Div(assignedLabel), new Div(new RouterLink(ASSIGNED, DetailAssignedView.class)), - new Div(new RouterLink(APART, DetailApartView.class)) - ); + new Div(new RouterLink(APART, DetailApartView.class))); } @Override public void afterNavigation(AfterNavigationEvent event) { assignedLabel.setText(assignedBean.getData()); - apartLabel.setText(apartBean.getData()); } } diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/RootView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/RootView.java index 5259cfea..41fc0fae 100644 --- a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/RootView.java +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/RootView.java @@ -16,39 +16,32 @@ package com.vaadin.cdi.itest.routecontext; +import javax.annotation.PostConstruct; + import com.vaadin.cdi.annotation.RouteScoped; -import com.vaadin.flow.component.UI; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Label; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouterLink; -import javax.annotation.PostConstruct; - -@Route("") +@Route(value = "", layout = MainLayout.class) @RouteScoped public class RootView extends AbstractCountedView { public static final String MASTER = "master"; public static final String REROUTE = "reroute"; public static final String POSTPONE = "postpone"; - public static final String UIID = "UIID"; public static final String EVENT = "event"; public static final String ERROR = "ERROR"; @PostConstruct private void init() { - Label uiIdLabel = new Label(UI.getCurrent().getUIId() + ""); - uiIdLabel.setId(UIID); - add( - new Div(uiIdLabel), - new Div(new Label("ROOT")), + add(new Div(new Label("ROOT")), new Div(new RouterLink(MASTER, MasterView.class)), new Div(new RouterLink(REROUTE, RerouteView.class)), new Div(new RouterLink(POSTPONE, PostponeView.class)), new Div(new RouterLink(EVENT, EventView.class)), - new Div(new RouterLink(ERROR, ErrorView.class)) - ); + new Div(new RouterLink(ERROR, ErrorView.class))); } } diff --git a/vaadin-cdi-itest/src/test/java/com/vaadin/cdi/itest/RouteContextTest.java b/vaadin-cdi-itest/src/test/java/com/vaadin/cdi/itest/RouteContextTest.java index d1180391..2e5086fb 100644 --- a/vaadin-cdi-itest/src/test/java/com/vaadin/cdi/itest/RouteContextTest.java +++ b/vaadin-cdi-itest/src/test/java/com/vaadin/cdi/itest/RouteContextTest.java @@ -23,19 +23,24 @@ import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; +import org.openqa.selenium.By; import com.vaadin.cdi.itest.routecontext.ApartBean; import com.vaadin.cdi.itest.routecontext.AssignedBean; +import com.vaadin.cdi.itest.routecontext.BeanNoOwner; +import com.vaadin.cdi.itest.routecontext.CustomExceptionSubButton; +import com.vaadin.cdi.itest.routecontext.CustomExceptionSubDiv; import com.vaadin.cdi.itest.routecontext.DetailApartView; import com.vaadin.cdi.itest.routecontext.DetailAssignedView; import com.vaadin.cdi.itest.routecontext.ErrorHandlerView; import com.vaadin.cdi.itest.routecontext.ErrorParentView; import com.vaadin.cdi.itest.routecontext.ErrorView; import com.vaadin.cdi.itest.routecontext.EventView; +import com.vaadin.cdi.itest.routecontext.MainLayout; import com.vaadin.cdi.itest.routecontext.MasterView; import com.vaadin.cdi.itest.routecontext.PostponeView; +import com.vaadin.cdi.itest.routecontext.PreserveOnRefreshBean; import com.vaadin.cdi.itest.routecontext.RerouteView; import com.vaadin.cdi.itest.routecontext.RootView; @@ -55,7 +60,7 @@ public static WebArchive deployment() { public void setUp() throws Exception { resetCounts(); open(""); - uiId = getText(RootView.UIID); + uiId = getText(MainLayout.UIID); assertConstructed(RootView.class, 1); assertDestroyed(RootView.class, 0); assertConstructed(RerouteView.class, 0); @@ -73,7 +78,6 @@ public void navigateFromRootToMasterReleasesRootInjectsEmptyBeans() throws IOException { follow(RootView.MASTER); assertTextEquals("", MasterView.ASSIGNED_BEAN_LABEL); - assertTextEquals("", MasterView.APART_BEAN_LABEL); assertConstructed(RootView.class, 1); assertDestroyed(RootView.class, 1); @@ -81,7 +85,7 @@ public void navigateFromRootToMasterReleasesRootInjectsEmptyBeans() assertDestroyed(MasterView.class, 0); assertConstructed(AssignedBean.class, 1); assertDestroyed(AssignedBean.class, 0); - assertConstructed(ApartBean.class, 1); + assertConstructed(ApartBean.class, 0); assertDestroyed(ApartBean.class, 0); assertConstructed(DetailApartView.class, 0); assertConstructed(DetailAssignedView.class, 0); @@ -92,7 +96,6 @@ public void navigationFromAssignedToMasterHoldsGroup() throws IOException { follow(RootView.MASTER); follow(MasterView.ASSIGNED); assertTextEquals("ASSIGNED", DetailAssignedView.BEAN_LABEL); - assertTextEquals("", MasterView.APART_BEAN_LABEL); follow(DetailAssignedView.MASTER); assertConstructed(MasterView.class, 1); @@ -102,7 +105,6 @@ public void navigationFromAssignedToMasterHoldsGroup() throws IOException { assertConstructed(DetailApartView.class, 0); assertTextEquals("ASSIGNED", MasterView.ASSIGNED_BEAN_LABEL); - assertTextEquals("", MasterView.APART_BEAN_LABEL); } @Test @@ -121,7 +123,6 @@ public void navigationFromApartToMasterReleasesGroup() throws IOException { assertDestroyed(DetailApartView.class, 1); assertTextEquals("", MasterView.ASSIGNED_BEAN_LABEL); - assertTextEquals("", MasterView.APART_BEAN_LABEL); } @Test @@ -158,7 +159,6 @@ public void eventObserved() { } @Test - @Ignore("Temprary disabled since it doesn't work with CCDM: https://github.com/vaadin/cdi/issues/314") public void errorHandlerIsScoped() throws IOException { follow(RootView.ERROR); assertConstructed(RootView.class, 1); @@ -187,8 +187,118 @@ public void errorHandlerIsScoped() throws IOException { assertRootViewIsDisplayed(); } + @Test + public void routeScopeDoesNotExist_injectionWithOwnerOutOfNavigationThrows_invalidViewIsNotRendered() { + follow(MainLayout.INVALID); + + Assert.assertFalse(isElementPresent(By.id("invalid-injection"))); + } + + @Test + public void beansWithNoOwner_preservedWithinTheSameRouteTarget_notPreservedAfterNavigation() + throws IOException { + follow(MainLayout.PARENT_NO_OWNER); + + assertConstructed(BeanNoOwner.class, 1); + assertDestroyed(BeanNoOwner.class, 0); + + follow("child"); + + assertDestroyed(BeanNoOwner.class, 0); + + follow("parent"); + + assertConstructed(BeanNoOwner.class, 2); + assertDestroyed(BeanNoOwner.class, 1); + } + + @Test + public void beanWithNoOwner_preservedWithinTheSameRoutingChain() + throws IOException { + follow(MainLayout.CHILD_NO_OWNER); + + assertConstructed(BeanNoOwner.class, 1); + assertDestroyed(BeanNoOwner.class, 0); + + findElement(By.id("reset")).click(); + + assertDestroyed(BeanNoOwner.class, 0); + } + + @Test + public void navigateToViewWhichThrows_beansInsideErrorViewArePreservedinScope() + throws IOException { + follow(RootView.ERROR); + + assertConstructed(CustomExceptionSubButton.class, 1); + assertDestroyed(CustomExceptionSubButton.class, 0); + + assertConstructed(CustomExceptionSubDiv.class, 0); + assertDestroyed(CustomExceptionSubDiv.class, 0); + + findElement(By.id("switch-content")).click(); + + assertDestroyed(CustomExceptionSubButton.class, 0); + assertConstructed(CustomExceptionSubDiv.class, 1); + assertDestroyed(CustomExceptionSubDiv.class, 0); + + findElement(By.id("switch-content")).click(); + + assertConstructed(CustomExceptionSubButton.class, 1); + assertConstructed(CustomExceptionSubDiv.class, 1); + assertDestroyed(CustomExceptionSubButton.class, 0); + assertDestroyed(CustomExceptionSubDiv.class, 0); + } + + @Test + public void routeScopedBeanIsDestroyedOnNavigationOutOfViewAfterPreserveOnRefresh() + throws IOException { + follow(MainLayout.PRESERVE); + + assertConstructed(PreserveOnRefreshBean.class, 1); + assertDestroyed(PreserveOnRefreshBean.class, 0); + + // refresh + getDriver().get(getDriver().getCurrentUrl()); + + // UI ID has to be updated: all bean creations/removals will be done + // now within the new UI + uiId = getText(MainLayout.UIID); + + // navigate out of the preserved view + follow(MainLayout.PARENT_NO_OWNER); + + assertDestroyed(PreserveOnRefreshBean.class, 1); + } + + @Test + public void preserveOnRefresh_beanIsNotDestroyed() throws IOException { + follow(MainLayout.PRESERVE); + + assertConstructed(PreserveOnRefreshBean.class, 1); + assertDestroyed(PreserveOnRefreshBean.class, 0); + + String beanData = findElement(By.id("preserve-on-refresh")).getText(); + + // refresh + getDriver().get(getDriver().getCurrentUrl()); + + // check that the bean has not been removed in the previous UI + assertDestroyed(PreserveOnRefreshBean.class, 0); + + // UI ID has to be updated: all bean creations/removals will be done + // now within the new UI + uiId = getText(MainLayout.UIID); + + // the bean should not be destroyed with the new UI as well + assertDestroyed(PreserveOnRefreshBean.class, 0); + + Assert.assertEquals(beanData, + findElement(By.id("preserve-on-refresh")).getText()); + } + private void assertRootViewIsDisplayed() { - assertTextEquals(uiId, RootView.UIID); + assertTextEquals(uiId, MainLayout.UIID); } private void assertConstructed(Class beanClass, int count) diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java index 015577ef..624e5c07 100644 --- a/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java @@ -16,20 +16,12 @@ package com.vaadin.cdi; -import com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode; -import com.vaadin.cdi.annotation.NormalRouteScoped; -import com.vaadin.cdi.annotation.RouteScopeOwner; -import com.vaadin.cdi.annotation.RouteScoped; -import com.vaadin.flow.component.Component; -import com.vaadin.flow.router.HasErrorParameter; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouterLayout; - import javax.enterprise.context.Dependent; import javax.enterprise.inject.spi.Annotated; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.inject.Inject; + import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Arrays; @@ -38,7 +30,15 @@ import java.util.Set; import java.util.function.Consumer; -import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.ABSENT_OWNER_OF_NON_ROUTE_COMPONENT; +import com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode; +import com.vaadin.cdi.annotation.NormalRouteScoped; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; + import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.NON_ROUTE_SCOPED_HAVE_OWNER; import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.NORMAL_SCOPED_COMPONENT; import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.OWNER_IS_NOT_ROUTE_COMPONENT; @@ -92,23 +92,20 @@ private Optional getRouteScopeOwner() { } /** - * Represents a deployment problem to be passed to the container. - * Message and stacktrace will appear in server log. - * It is not thrown, or caught. + * Represents a deployment problem to be passed to the container. Message + * and stacktrace will appear in server log. It is not thrown, or caught. */ static class DeploymentProblem extends Throwable { enum ErrorCode { - NORMAL_SCOPED_COMPONENT, - NON_ROUTE_SCOPED_HAVE_OWNER, - ABSENT_OWNER_OF_NON_ROUTE_COMPONENT, - OWNER_IS_NOT_ROUTE_COMPONENT + NORMAL_SCOPED_COMPONENT, NON_ROUTE_SCOPED_HAVE_OWNER, ABSENT_OWNER_OF_NON_ROUTE_COMPONENT, OWNER_IS_NOT_ROUTE_COMPONENT } private final Type baseType; private final ErrorCode errorCode; - private DeploymentProblem(String message, Type baseType, ErrorCode errorCode) { + private DeploymentProblem(String message, Type baseType, + ErrorCode errorCode) { super(message); this.baseType = baseType; this.errorCode = errorCode; @@ -159,10 +156,9 @@ public ErrorCode getErrorCode() { @Override public String getErrorMessage(BeanInfo beanInfo) { return String.format( - "Normal scoped Vaadin components are not supported. " + - "'%s' should not belong to a normal scope.", - beanInfo.getBaseType().getTypeName() - ); + "Normal scoped Vaadin components are not supported. " + + "'%s' should not belong to a normal scope.", + beanInfo.getBaseType().getTypeName()); } } @@ -172,10 +168,9 @@ private class OwnerIsNotRouteComponentValidator implements BeanValidator { @Override public boolean isInvalid(BeanInfo beanInfo) { return beanInfo.isRouteScoped() - && beanInfo.getRouteScopeOwner() - .map(RouteScopeOwner::value) - .filter(DeploymentValidator::isNonRouteComponent) - .isPresent(); + && beanInfo.getRouteScopeOwner().map(RouteScopeOwner::value) + .filter(DeploymentValidator::isNonRouteComponent) + .isPresent(); } @Override @@ -188,33 +183,7 @@ public String getErrorMessage(BeanInfo beanInfo) { return String.format( "'@%s' should define a route component on '%s'.", RouteScopeOwner.class.getSimpleName(), - beanInfo.getBaseType().getTypeName() - ); - } - - } - - private class AbsentOwnerOfNonRouteComponentValidator implements BeanValidator { - - @Override - public boolean isInvalid(BeanInfo beanInfo) { - return beanInfo.isRouteScoped() - && isNonRouteComponent(beanInfo.getBaseType()) - && !beanInfo.getRouteScopeOwner().isPresent(); - } - - @Override - public ErrorCode getErrorCode() { - return ABSENT_OWNER_OF_NON_ROUTE_COMPONENT; - } - - @Override - public String getErrorMessage(BeanInfo beanInfo) { - return String.format( - "'%s' is not a route component, need a '@%s'.", - beanInfo.getBaseType().getTypeName(), - RouteScopeOwner.class.getSimpleName() - ); + beanInfo.getBaseType().getTypeName()); } } @@ -239,8 +208,7 @@ public String getErrorMessage(BeanInfo beanInfo) { beanInfo.getBaseType().getTypeName(), RouteScoped.class.getSimpleName(), NormalRouteScoped.class.getSimpleName(), - RouteScopeOwner.class.getSimpleName() - ); + RouteScopeOwner.class.getSimpleName()); } } @@ -250,22 +218,19 @@ public String getErrorMessage(BeanInfo beanInfo) { private final List validators = Arrays.asList( new NormalScopedComponentValidator(), - new AbsentOwnerOfNonRouteComponentValidator(), new OwnerIsNotRouteComponentValidator(), - new NonRouteScopedHaveOwnerValidator() - ); + new NonRouteScopedHaveOwnerValidator()); void validate(Set infoSet, Consumer problemConsumer) { infoSet.forEach(info -> validateBean(info, problemConsumer)); } - private void validateBean(BeanInfo beanInfo, Consumer problemConsumer) { - validators.stream() - .filter(validator -> validator.isInvalid(beanInfo)) + private void validateBean(BeanInfo beanInfo, + Consumer problemConsumer) { + validators.stream().filter(validator -> validator.isInvalid(beanInfo)) .map(validator -> new DeploymentProblem( validator.getErrorMessage(beanInfo), - beanInfo.getBaseType(), - validator.getErrorCode())) + beanInfo.getBaseType(), validator.getErrorCode())) .forEach(problemConsumer); } diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScoped.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScoped.java index f3c64843..1e8a8748 100644 --- a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScoped.java +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScoped.java @@ -16,46 +16,49 @@ package com.vaadin.cdi.annotation; -import com.vaadin.flow.router.HasErrorParameter; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouterLayout; - import javax.inject.Scope; + import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; + /** * The lifecycle of a RouteScoped component is controlled by route navigation. *

- * Every RouteScoped bean belongs to one router component owner. - * It can be a {@link Route @Route}, or a {@link RouterLayout}, - * or a {@link HasErrorParameter HasErrorParameter}. - * Beans are qualified by {@link RouteScopeOwner @RouteScopeOwner} - * to link with their owner. + * Every RouteScoped bean belongs to one router component owner. It can be a + * {@link Route @Route} component, or a {@link RouterLayout}, or a + * {@link HasErrorParameter HasErrorParameter}. Beans are qualified by + * {@link RouteScopeOwner @RouteScopeOwner} to link with their owner. *

* Until owner remains active, all beans owned by it remain in the scope. *

- * When a RouteScoped bean is a router component, - * an owner can be any ancestor {@link RouterLayout}, or the bean itself. - * Omitting the RouteScopeOwner annotation means owner is the bean itself. + * Without the {@link RouteScopeOwner} annotation the owner is the current route + * target component (dynamically calculated). With nested routing hierarchies, + * the target is the "leaf" or "bottom most" routing component. The beans are + * preserved as long as the owner component remains in the navigation chain. It + * means that the bean may be preserved even if the navigation target is changed + * (but the "initial" calculated owner is still in the navigation chain). *

* Injection with this annotation will create a direct reference to the object * rather than a proxy. *

* There are some limitations when not using proxies. Circular referencing (that - * is, injecting A to B and B to A) will not work. - * Injecting into a larger scope will bind the instance - * from the currently active smaller scope, and will ignore smaller scope change. - * For example after being injected into session scope it will point to the same - * RouteScoped bean instance ( even it is destroyed ) regardless of UI, - * or any navigation change. + * is, injecting A to B and B to A) will not work. Injecting into a larger scope + * will bind the instance from the currently active smaller scope, and will + * ignore smaller scope change. For example after being injected into session + * scope it will point to the same RouteScoped bean instance ( even it is + * destroyed ) regardless of UI, or any navigation change. *

- * The sister annotation to this is the {@link NormalRouteScoped}. Both annotations - * reference the same underlying scope, so it is possible to get both a proxy - * and a direct reference to the same object by using different annotations. + * The sister annotation to this is the {@link NormalRouteScoped}. Both + * annotations reference the same underlying scope, so it is possible to get + * both a proxy and a direct reference to the same object by using different + * annotations. */ @Scope @Inherited diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/AbstractContextualStorageManager.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/AbstractContextualStorageManager.java index 181033f7..655a5b92 100644 --- a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/AbstractContextualStorageManager.java +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/AbstractContextualStorageManager.java @@ -16,12 +16,10 @@ package com.vaadin.cdi.context; -import org.apache.deltaspike.core.util.context.AbstractContext; -import org.apache.deltaspike.core.util.context.ContextualStorage; - import javax.annotation.PreDestroy; import javax.enterprise.inject.spi.BeanManager; import javax.inject.Inject; + import java.io.Serializable; import java.util.Collection; import java.util.Collections; @@ -30,15 +28,17 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.apache.deltaspike.core.util.context.AbstractContext; +import org.apache.deltaspike.core.util.context.ContextualStorage; + /** * Base class for manage and store ContextualStorages. * - * This class is responsible for - * - creating, and providing the ContextualStorage for a context key - * - destroying ContextualStorages + * This class is responsible for - creating, and providing the ContextualStorage + * for a context key - destroying ContextualStorages */ @SuppressWarnings("CdiManagedBeanInconsistencyInspection") -abstract class AbstractContextualStorageManager implements Serializable { +abstract class AbstractContextualStorageManager implements Serializable { @Inject private BeanManager beanManager; private final boolean concurrent; @@ -53,7 +53,8 @@ protected AbstractContextualStorageManager(boolean concurrent) { this.concurrent = concurrent; } - protected ContextualStorage getContextualStorage(K key, boolean createIfNotExist) { + protected ContextualStorage getContextualStorage(K key, + boolean createIfNotExist) { if (createIfNotExist) { return storageMap.computeIfAbsent(key, this::newContextualStorage); } else { @@ -61,12 +62,20 @@ protected ContextualStorage getContextualStorage(K key, boolean createIfNotExist } } + protected void relocate(K from, K to) { + ContextualStorage storage = storageMap.remove(from); + if (storage != null) { + storageMap.put(to, storage); + } + } + protected ContextualStorage newContextualStorage(K key) { - // Not required by the spec, but in reality beans are PassivationCapable. + // Not required by the spec, but in reality beans are + // PassivationCapable. // Even for non serializable bean classes. // CDI implementations use PassivationCapable beans, - // because injecting non serializable proxies might block serialization of - // bean instances in a passivation capable context. + // because injecting non serializable proxies might block serialization + // of bean instances in a passivation capable context. return new ContextualStorage(beanManager, concurrent, true); } diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java index efed71f3..31132bda 100644 --- a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java @@ -16,24 +16,41 @@ package com.vaadin.cdi.context; -import com.vaadin.cdi.annotation.NormalUIScoped; -import com.vaadin.cdi.annotation.RouteScopeOwner; -import com.vaadin.cdi.annotation.RouteScoped; -import com.vaadin.flow.router.AfterNavigationEvent; -import org.apache.deltaspike.core.api.provider.BeanProvider; -import org.apache.deltaspike.core.util.context.AbstractContext; -import org.apache.deltaspike.core.util.context.ContextualStorage; - import javax.enterprise.context.spi.Contextual; import javax.enterprise.event.Observes; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.PassivationCapable; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; import java.lang.annotation.Annotation; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; +import org.apache.deltaspike.core.api.provider.BeanProvider; +import org.apache.deltaspike.core.util.context.AbstractContext; +import org.apache.deltaspike.core.util.context.ContextualStorage; + +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.page.ExtendedClientDetails; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.server.VaadinSession; + import static javax.enterprise.event.Reception.IF_EXISTS; /** @@ -41,9 +58,9 @@ */ public class RouteScopedContext extends AbstractContext { - @NormalUIScoped + @VaadinSessionScoped public static class ContextualStorageManager - extends AbstractContextualStorageManager { + extends AbstractContextualStorageManager { public ContextualStorageManager() { // Session lock checked in VaadinSessionScopedContext while @@ -51,21 +68,178 @@ public ContextualStorageManager() { super(false); } - private void onAfterNavigation(@Observes(notifyObserver = IF_EXISTS) - AfterNavigationEvent event) { - Set activeChain = event.getActiveChain().stream() - .map(Object::getClass) - .collect(Collectors.toSet()); + @Override + protected ContextualStorage newContextualStorage(RouteStorageKey key) { + UI.getCurrent().addDetachListener( + event -> handleUIDetach(event.getUI(), key)); + return super.newContextualStorage(key); + } + + private void onAfterNavigation( + @Observes(notifyObserver = IF_EXISTS) AfterNavigationEvent event) { + Set> activeChain = event.getActiveChain().stream() + .map(Object::getClass).collect(Collectors.toSet()); + + destroyDescopedBeans(event.getLocationChangeEvent().getUI(), + activeChain); + + } - Set missingFromChain = getKeySet().stream() - .filter(routeCompClass -> !activeChain.contains(routeCompClass)) + private void onBeforeEnter(@Observes BeforeEnterEvent event) { + UI ui = event.getUI(); + ComponentUtil.setData(ui, NavigationData.class, new NavigationData( + event.getNavigationTarget(), event.getLayouts())); + + Set> activeChain = new HashSet<>(); + activeChain.add(event.getNavigationTarget()); + activeChain.addAll(event.getLayouts()); + + destroyDescopedBeans(ui, activeChain); + } + + private void destroyDescopedBeans(UI ui, + Set> navigationChain) { + String uiStoreId = getUIStoreId(ui); + + Set missingKeys = getKeySet().stream() + .filter(key -> key.getUIId().equals(uiStoreId)) + .filter(key -> !navigationChain.contains(key.getOwner())) .collect(Collectors.toSet()); - missingFromChain.forEach(this::destroy); + File file = new File("/Users/denis/test.log"); + try { + Files.write(file.toPath(), + Arrays.asList("missing keys " + missingKeys, + "keys : " + getKeySet(), + " active chain: " + navigationChain), + StandardOpenOption.APPEND); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + missingKeys.forEach(this::destroy); + } + + private void handleUIDetach(UI ui, RouteStorageKey key) { + UI uiAfterRefresh = findPreservingUI(ui); + if (uiAfterRefresh == null) { + destroy(key); + } else { + uiAfterRefresh.addDetachListener( + event -> handleUIDetach(event.getUI(), key)); + } + } + + private UI findPreservingUI(UI ui) { + VaadinSession session = ui.getSession(); + String windowName = getWindowName(ui); + for (UI sessionUi : session.getUIs()) { + if (sessionUi != ui && windowName != null + && windowName.equals(getWindowName(sessionUi))) { + return sessionUi; + } + } + return null; + } + + private static String getWindowName(UI ui) { + ExtendedClientDetails details = ui.getInternals() + .getExtendedClientDetails(); + if (details == null) { + return null; + } + return details.getWindowName(); + } + + private RouteStorageKey getKey(UI ui, Class owner) { + ExtendedClientDetails details = ui.getInternals() + .getExtendedClientDetails(); + RouteStorageKey key = new RouteStorageKey(owner, getUIStoreId(ui)); + if (details == null) { + ui.getPage().retrieveExtendedClientDetails( + det -> relocate(ui, key)); + } + return key; + } + + private void relocate(UI ui, RouteStorageKey key) { + relocate(key, + new RouteStorageKey(key.getOwner(), getUIStoreId(ui))); + } + + private String getUIStoreId(UI ui) { + ExtendedClientDetails details = ui.getInternals() + .getExtendedClientDetails(); + if (details == null) { + return "uid-" + ui.getUIId(); + } else { + return "win-" + getWindowName(ui); + } + } + + } + + private static class RouteStorageKey implements Serializable { + private final Class owner; + private final String uiId; + + private RouteStorageKey(Class owner, String uiId) { + this.owner = owner; + this.uiId = uiId; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RouteStorageKey)) { + return false; + } + if (obj == this) { + return true; + } + RouteStorageKey key = (RouteStorageKey) obj; + return owner.equals(key.owner) && uiId.equals(key.uiId); + } + + @Override + public int hashCode() { + return Objects.hash(owner, uiId); + } + + @Override + public String toString() { + return "[ ui-key='" + getUIId() + "', owner='" + getOwner() + "' ]"; + } + + Class getOwner() { + return owner; + } + + String getUIId() { + return uiId; } } + static class NavigationData { + private final Class navigationTarget; + private final List> layouts; + + NavigationData(Class navigationTarget, + List> layouts) { + this.navigationTarget = navigationTarget; + this.layouts = layouts; + } + + Class getNavigationTarget() { + return navigationTarget; + } + + List> getLayouts() { + return layouts; + } + } + private ContextualStorageManager contextManager; private Supplier isUIContextActive; private BeanManager beanManager; @@ -75,9 +249,9 @@ public RouteScopedContext(BeanManager beanManager) { } public void init(BeanManager beanManager, - Supplier isUIContextActive) { - contextManager = BeanProvider - .getContextualReference(beanManager, ContextualStorageManager.class, false); + Supplier isUIContextActive) { + contextManager = BeanProvider.getContextualReference(beanManager, + ContextualStorageManager.class, false); this.beanManager = beanManager; this.isUIContextActive = isUIContextActive; } @@ -94,29 +268,58 @@ public boolean isActive() { @Override protected ContextualStorage getContextualStorage(Contextual contextual, - boolean createIfNotExist) { - Class key = convertToKey(contextual); + boolean createIfNotExist) { + RouteStorageKey key = convertToKey(contextual); return contextManager.getContextualStorage(key, createIfNotExist); } - private Class convertToKey(Contextual contextual) { - if (!(contextual instanceof Bean)) { - if (contextual instanceof PassivationCapable) { - final String id = ((PassivationCapable) contextual).getId(); - contextual = beanManager.getPassivationCapableBean(id); - } else { - throw new IllegalArgumentException( - contextual.getClass().getName() - + " is not of type " + Bean.class.getName()); - } + private RouteStorageKey convertToKey(Contextual contextual) { + Bean bean = getBean(contextual); + UI ui = UI.getCurrent(); + Class owner = getOwner(ui, bean); + if (!navigationChainHasOwner(ui, owner)) { + throw new IllegalStateException(String.format( + "Route owner '%s' instance is not available in the " + + "active navigation components chain: the scope defined by the bean '%s' doesn't exist.", + owner, bean.getBeanClass())); } - final Bean bean = (Bean) contextual; - return bean.getQualifiers() - .stream() + return contextManager.getKey(ui, owner); + } + + private boolean navigationChainHasOwner(UI ui, Class owner) { + NavigationData data = ComponentUtil.getData(ui, NavigationData.class); + if (owner.equals(data.getNavigationTarget())) { + return true; + } + return data.getLayouts().stream() + .anyMatch(clazz -> clazz.equals(owner)); + } + + @SuppressWarnings("unchecked") + private Class getOwner(UI ui, Bean bean) { + return bean.getQualifiers().stream() .filter(annotation -> annotation instanceof RouteScopeOwner) - .map(annotation -> (Class) (((RouteScopeOwner) annotation).value())) - .findFirst() - .orElse(bean.getBeanClass()); + .map(annotation -> (Class) (((RouteScopeOwner) annotation) + .value())) + .findFirst().orElseGet(() -> getCurrentNavigationTarget(ui)); } + @SuppressWarnings("rawtypes") + private Class getCurrentNavigationTarget(UI ui) { + NavigationData data = ComponentUtil.getData(ui, NavigationData.class); + return data.getNavigationTarget(); + } + + private Bean getBean(Contextual contextual) { + if (contextual instanceof Bean) { + return (Bean) contextual; + } + if (contextual instanceof PassivationCapable) { + String id = ((PassivationCapable) contextual).getId(); + return beanManager.getPassivationCapableBean(id); + } else { + throw new IllegalArgumentException(contextual.getClass().getName() + + " is not of type " + Bean.class.getName()); + } + } } diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java index f530e250..fc6c51b7 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java @@ -16,6 +16,29 @@ package com.vaadin.cdi; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.Vetoed; +import javax.inject.Inject; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.vaadin.cdi.DeploymentValidator.BeanInfo; import com.vaadin.cdi.DeploymentValidator.DeploymentProblem; import com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode; @@ -31,27 +54,6 @@ import com.vaadin.flow.router.HasErrorParameter; import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouterLayout; -import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.enterprise.inject.Produces; -import javax.enterprise.inject.Vetoed; -import javax.inject.Inject; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.ABSENT_OWNER_OF_NON_ROUTE_COMPONENT; import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.NON_ROUTE_SCOPED_HAVE_OWNER; @@ -88,10 +90,6 @@ public static class NonRouteScopedHaveOwner { public static class OwnerIsNotRouteComponent { } - @RouteScoped - public static class AbsentOwnerOfNonRouteComponent { - } - @Route @RouteScoped @RouteScopeOwner(RouteTargetOfSelf.class) @@ -108,9 +106,11 @@ public static class TestRouterLayout extends Label implements RouterLayout { } @RouteScoped - public static class TestHasErrorParameter extends Label implements HasErrorParameter { + public static class TestHasErrorParameter extends Label + implements HasErrorParameter { @Override - public int setErrorParameter(BeforeEnterEvent event, ErrorParameter parameter) { + public int setErrorParameter(BeforeEnterEvent event, + ErrorParameter parameter) { return 0; } } @@ -163,8 +163,8 @@ public boolean equals(Object o) { return false; } ProblemId problemId = (ProblemId) o; - return errorCode == problemId.errorCode && - Objects.equals(baseType, problemId.baseType); + return errorCode == problemId.errorCode + && Objects.equals(baseType, problemId.baseType); } @Override @@ -174,10 +174,8 @@ public int hashCode() { @Override public String toString() { - return "ProblemId{" + - "errorCode=" + errorCode + - ", baseType=" + baseType + - '}'; + return "ProblemId{" + "errorCode=" + errorCode + ", baseType=" + + baseType + '}'; } } @@ -201,32 +199,25 @@ public void tearDown() { @Test public void validate_normalScopedProblems_collected() { - Set infoSet = createBeanSet( - PseudoScopedLabel.class, - NormalScopedBean.class, - NormalScopedLabel.class, + Set infoSet = createBeanSet(PseudoScopedLabel.class, + NormalScopedBean.class, NormalScopedLabel.class, ProducedNormalScopedComponent.class); validator.validateForTest(infoSet, problems::add); assertEquals(2, problems.size()); assertProblemExists(NORMAL_SCOPED_COMPONENT, NormalScopedLabel.class); - assertProblemExists(NORMAL_SCOPED_COMPONENT, ProducedNormalScopedComponent.class); + assertProblemExists(NORMAL_SCOPED_COMPONENT, + ProducedNormalScopedComponent.class); } @Test public void validate_routeScopedProblems_collected() { - Set infoSet = createBeanSet( - NonRouteScopedHaveOwner.class, - OwnerIsNotRouteComponent.class, - AbsentOwnerOfNonRouteComponent.class, - RouteTargetOfSelf.class, - TestRouteScopedTarget.class, - TestRouterLayout.class, - TestHasErrorParameter.class, - BeanOfHasErrorParameter.class, - BeanOfRouterLayout.class, - BeanOfRouteTarget.class, + Set infoSet = createBeanSet(NonRouteScopedHaveOwner.class, + OwnerIsNotRouteComponent.class, RouteTargetOfSelf.class, + TestRouteScopedTarget.class, TestRouterLayout.class, + TestHasErrorParameter.class, BeanOfHasErrorParameter.class, + BeanOfRouterLayout.class, BeanOfRouteTarget.class, RouteTargetOfRouterLayout.class, ProducedRouteScopedBeanWithoutOwner.class); @@ -237,8 +228,6 @@ public void validate_routeScopedProblems_collected() { OwnerIsNotRouteComponent.class); assertProblemExists(NON_ROUTE_SCOPED_HAVE_OWNER, NonRouteScopedHaveOwner.class); - assertProblemExists(ABSENT_OWNER_OF_NON_ROUTE_COMPONENT, - AbsentOwnerOfNonRouteComponent.class); assertProblemExists(ABSENT_OWNER_OF_NON_ROUTE_COMPONENT, ProducedRouteScopedBeanWithoutOwner.class); } @@ -255,18 +244,18 @@ private void assertProblemExists(ErrorCode errorCode, Type baseType) { private Set createBeanSet(Type... clazz) { Map map = getBeanInfoSetAsMap(); - return Arrays - .stream(clazz) - .map(map::get) - .collect(Collectors.toSet()); + return Arrays.stream(clazz).map(map::get).collect(Collectors.toSet()); } private Map getBeanInfoSetAsMap() { return beanInfoSetHolder.getInfoSet().stream() - // Weld causes duplicate key because of exposing weird things as Object. + // Weld causes duplicate key because of exposing weird things as + // Object. // Tests are not interested in it. - .filter(beanInfo -> !beanInfo.getBaseType().equals(Object.class)) - .collect(Collectors.toMap(BeanInfo::getBaseType, Function.identity())); + .filter(beanInfo -> !beanInfo.getBaseType() + .equals(Object.class)) + .collect(Collectors.toMap(BeanInfo::getBaseType, + Function.identity())); } @Produces diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/AbstractContextTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/AbstractContextTest.java index 4cce4da9..6297a32e 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/AbstractContextTest.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/AbstractContextTest.java @@ -16,15 +16,16 @@ package com.vaadin.cdi.context; +import javax.enterprise.context.ContextNotActiveException; + +import java.util.ArrayList; +import java.util.List; + import org.apache.deltaspike.core.api.provider.BeanProvider; import org.junit.After; import org.junit.Before; import org.junit.Test; -import javax.enterprise.context.ContextNotActiveException; -import java.util.ArrayList; -import java.util.List; - import static org.junit.Assert.assertEquals; public abstract class AbstractContextTest { @@ -97,12 +98,11 @@ public void destroy_beanExistsInContext_beanDestroyed() { protected UnderTestContext createContext() { UnderTestContext underTestContext = newContextUnderTest(); -/* - UnderTestContext implementations set fields - to Vaadin CurrentInstance. - Need to hold a hard reference to prevent possible GC, - because CurrentInstance works with weak reference. -*/ + /* + * UnderTestContext implementations set fields to Vaadin + * CurrentInstance. Need to hold a hard reference to prevent possible + * GC, because CurrentInstance works with weak reference. + */ contexts.add(underTestContext); return underTestContext; } diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextNormalTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextNormalTest.java index 7708b20b..d014e542 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextNormalTest.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextNormalTest.java @@ -16,14 +16,19 @@ package com.vaadin.cdi.context; -import com.vaadin.cdi.annotation.NormalRouteScoped; -import com.vaadin.flow.router.Route; +import java.util.Collections; + import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner; import org.junit.runner.RunWith; +import com.vaadin.cdi.annotation.NormalRouteScoped; +import com.vaadin.cdi.context.RouteScopedContext.NavigationData; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.router.Route; + @RunWith(CdiTestRunner.class) -public class RouteContextNormalTest - extends AbstractContextTest { +public class RouteContextNormalTest extends + AbstractContextTest { @NormalRouteScoped @Route("") @@ -38,7 +43,19 @@ protected Class getBeanType() { @Override protected UnderTestContext newContextUnderTest() { // Intentionally UI Under Test Context. Nothing else needed. - return new UIUnderTestContext(); + UIUnderTestContext context = new UIUnderTestContext() { + + @Override + public void activate() { + super.activate(); + + NavigationData data = new NavigationData( + TestNavigationTarget.class, Collections.emptyList()); + ComponentUtil.setData(getUi(), NavigationData.class, data); + } + }; + + return context; } @Override diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextPseudoTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextPseudoTest.java index 272b7994..bb151376 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextPseudoTest.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextPseudoTest.java @@ -16,14 +16,25 @@ package com.vaadin.cdi.context; -import com.vaadin.cdi.annotation.RouteScoped; -import com.vaadin.flow.router.Route; +import java.util.Collections; + import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner; import org.junit.runner.RunWith; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.context.RouteScopedContext.NavigationData; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.router.Route; + @RunWith(CdiTestRunner.class) -public class RouteContextPseudoTest - extends AbstractContextTest { +public class RouteContextPseudoTest extends + AbstractContextTest { + + @Override + public void setUp() { + // TODO Auto-generated method stub + super.setUp(); + } @RouteScoped @Route("") @@ -38,7 +49,19 @@ protected Class getBeanType() { @Override protected UnderTestContext newContextUnderTest() { // Intentionally UI Under Test Context. Nothing else needed. - return new UIUnderTestContext(); + UIUnderTestContext context = new UIUnderTestContext() { + + @Override + public void activate() { + super.activate(); + + NavigationData data = new NavigationData( + TestNavigationTarget.class, Collections.emptyList()); + ComponentUtil.setData(getUi(), NavigationData.class, data); + } + }; + + return context; } @Override diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java new file mode 100644 index 00000000..8c326d9b --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2021 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.cdi.context; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; + +@Tag(Tag.A) +class TestNavigationTarget extends Component { + +} \ No newline at end of file From 2a0f4e12212aaa19a7786f926326ca55faf39211 Mon Sep 17 00:00:00 2001 From: Denis Anisimov Date: Mon, 7 Jun 2021 12:34:44 +0300 Subject: [PATCH 2/3] chore: make minor corrections and add ITs --- .../cdi/itest/routecontext/BeanNoOwner.java | 29 ++ .../itest/routecontext/ChildNoOwnerView.java | 52 ++++ .../itest/routecontext/CustomException.java | 20 ++ .../CustomExceptionSubButton.java | 34 +++ .../routecontext/CustomExceptionSubDiv.java | 31 +++ .../cdi/itest/routecontext/InvalidView.java | 42 +++ .../cdi/itest/routecontext/MainLayout.java | 52 ++++ .../itest/routecontext/ParentNoOwnerView.java | 46 ++++ .../routecontext/PreserveOnRefreshBean.java | 34 +++ .../routecontext/PreserveOnRefreshView.java | 43 +++ .../com/vaadin/cdi/DeploymentValidator.java | 2 +- .../cdi/context/RouteScopedContext.java | 30 +-- .../vaadin/cdi/DeploymentValidatorTest.java | 37 +-- .../RouteContextualStorageManagerTest.java | 254 ++++++++++++++---- .../cdi/context/SessionUnderTestContext.java | 3 + .../cdi/context/TestNavigationTarget.java | 2 +- .../cdi/context/UIUnderTestContext.java | 7 + 17 files changed, 608 insertions(+), 110 deletions(-) create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/BeanNoOwner.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ChildNoOwnerView.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomException.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubButton.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubDiv.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/InvalidView.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayout.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ParentNoOwnerView.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshBean.java create mode 100644 vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshView.java diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/BeanNoOwner.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/BeanNoOwner.java new file mode 100644 index 00000000..8c866bb0 --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/BeanNoOwner.java @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.RouteScoped; + +@RouteScoped +public class BeanNoOwner extends AbstractCountedBean { + + public BeanNoOwner() { + setData(UUID.randomUUID().toString()); + } + +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ChildNoOwnerView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ChildNoOwnerView.java new file mode 100644 index 00000000..06ecd591 --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ChildNoOwnerView.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@Route(value = "child-no-owner", layout = ParentNoOwnerView.class) +public class ChildNoOwnerView extends Div { + + @Inject + private Instance instance; + + @Override + protected void onAttach(AttachEvent attachEvent) { + if (attachEvent.isInitialAttach()) { + RouterLink link = new RouterLink("parent", ParentNoOwnerView.class); + link.setId("to-parent"); + add(link); + + Div div = new Div(); + div.setId("child-info"); + div.getElement().getStyle().set("display", "block"); + div.setText(instance.get().getData()); + add(div); + + NativeButton button = new NativeButton("Reset bean instance", + event -> div.setText(instance.get().getData())); + add(button); + button.setId("reset"); + } + } +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomException.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomException.java new file mode 100644 index 00000000..88093b93 --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomException.java @@ -0,0 +1,20 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +public class CustomException extends RuntimeException { + +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubButton.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubButton.java new file mode 100644 index 00000000..4aee65c6 --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubButton.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.NativeButton; + +@RouteScoped +@RouteScopeOwner(ErrorHandlerView.class) +public class CustomExceptionSubButton extends AbstractCountedView { + + public CustomExceptionSubButton() { + NativeButton button = new NativeButton(); + button.setId("custom-exception-button"); + button.setText(UUID.randomUUID().toString()); + add(button); + } +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubDiv.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubDiv.java new file mode 100644 index 00000000..ebb48b79 --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubDiv.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; + +@RouteScoped +@RouteScopeOwner(ErrorHandlerView.class) +public class CustomExceptionSubDiv extends AbstractCountedView { + + public CustomExceptionSubDiv() { + setId("custom-exception-div"); + setText(UUID.randomUUID().toString()); + } +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/InvalidView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/InvalidView.java new file mode 100644 index 00000000..8bc54614 --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/InvalidView.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import javax.inject.Inject; + +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("invalid-injection") +public class InvalidView extends Div { + + @Inject + @RouteScopeOwner(ErrorParentView.class) + /* + * There is no a ErrorParentView in navigation: this injection has no scope. + * Pseudo-scope @RouteScoped is used here with the component to immediately + * get an exception, for normal scope proxy is created and the exception + * won't be thrown immediately. + */ + private ErrorParentView bean; + + public InvalidView() { + setId("invalid-injection"); + setText("This view should not be shown since the " + + "injection has no scope and an exception should be thrown"); + } +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayout.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayout.java new file mode 100644 index 00000000..4fd5203d --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayout.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Label; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.router.RouterLink; + +public class MainLayout extends Div implements RouterLayout { + + public static final String UIID = "UIID"; + + public static final String PRESERVE = "preserve"; + public static final String INVALID = "invalid"; + public static final String PARENT_NO_OWNER = "parent-no-owner"; + public static final String CHILD_NO_OWNER = "child-no-owner"; + + private Label uiIdLabel; + + public MainLayout() { + add(new RouterLink(PRESERVE, PreserveOnRefreshView.class), + new RouterLink(INVALID, InvalidView.class), + new RouterLink(PARENT_NO_OWNER, ParentNoOwnerView.class), + new RouterLink(CHILD_NO_OWNER, ChildNoOwnerView.class)); + ; + } + + @Override + protected void onAttach(AttachEvent attachEvent) { + if (uiIdLabel != null) { + remove(uiIdLabel); + } + uiIdLabel = new Label(attachEvent.getUI().getUIId() + ""); + uiIdLabel.setId(UIID); + add(uiIdLabel); + } +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ParentNoOwnerView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ParentNoOwnerView.java new file mode 100644 index 00000000..e9c5d28e --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/ParentNoOwnerView.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import javax.inject.Inject; + +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.router.RouterLink; + +@Route("parent-no-owner") +public class ParentNoOwnerView extends Div implements RouterLayout { + + @Inject + private BeanNoOwner bean; + + @Override + protected void onAttach(AttachEvent attachEvent) { + if (attachEvent.isInitialAttach()) { + RouterLink link = new RouterLink("child", ChildNoOwnerView.class); + link.setId("to-child"); + add(link); + + Div div = new Div(); + div.setId("parent-info"); + div.getElement().getStyle().set("display", "block"); + div.setText(bean.getData()); + add(div); + } + } +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshBean.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshBean.java new file mode 100644 index 00000000..bc03114f --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshBean.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import javax.annotation.PostConstruct; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; + +@RouteScoped +@RouteScopeOwner(PreserveOnRefreshView.class) +public class PreserveOnRefreshBean extends AbstractCountedBean { + + @PostConstruct + private void init() { + setData(UUID.randomUUID().toString()); + } + +} diff --git a/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshView.java b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshView.java new file mode 100644 index 00000000..bb2a8dcb --- /dev/null +++ b/vaadin-cdi-itest/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshView.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2021 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.cdi.itest.routecontext; + +import javax.enterprise.inject.Instance; +import javax.inject.Inject; + +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PreserveOnRefresh; +import com.vaadin.flow.router.Route; + +@PreserveOnRefresh +@Route(value = "preserve-on-refresh", layout = MainLayout.class) +public class PreserveOnRefreshView extends Div { + + @Inject + @RouteScopeOwner(PreserveOnRefreshView.class) + private Instance injection; + + public PreserveOnRefreshView() { + setId("preserve-on-refresh"); + } + + @Override + protected void onAttach(AttachEvent attachEvent) { + setText(injection.get().getData()); + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java index 624e5c07..feb44541 100644 --- a/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java @@ -98,7 +98,7 @@ private Optional getRouteScopeOwner() { static class DeploymentProblem extends Throwable { enum ErrorCode { - NORMAL_SCOPED_COMPONENT, NON_ROUTE_SCOPED_HAVE_OWNER, ABSENT_OWNER_OF_NON_ROUTE_COMPONENT, OWNER_IS_NOT_ROUTE_COMPONENT + NORMAL_SCOPED_COMPONENT, NON_ROUTE_SCOPED_HAVE_OWNER, OWNER_IS_NOT_ROUTE_COMPONENT } private final Type baseType; diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java index 31132bda..103dcf22 100644 --- a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java @@ -22,13 +22,8 @@ import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.PassivationCapable; -import java.io.File; -import java.io.IOException; import java.io.Serializable; import java.lang.annotation.Annotation; -import java.nio.file.Files; -import java.nio.file.StandardOpenOption; -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -106,18 +101,6 @@ private void destroyDescopedBeans(UI ui, .filter(key -> !navigationChain.contains(key.getOwner())) .collect(Collectors.toSet()); - File file = new File("/Users/denis/test.log"); - try { - Files.write(file.toPath(), - Arrays.asList("missing keys " + missingKeys, - "keys : " + getKeySet(), - " active chain: " + navigationChain), - StandardOpenOption.APPEND); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - missingKeys.forEach(this::destroy); } @@ -281,7 +264,7 @@ private RouteStorageKey convertToKey(Contextual contextual) { throw new IllegalStateException(String.format( "Route owner '%s' instance is not available in the " + "active navigation components chain: the scope defined by the bean '%s' doesn't exist.", - owner, bean.getBeanClass())); + owner, bean.getBeanClass().getName())); } return contextManager.getKey(ui, owner); } @@ -301,12 +284,19 @@ private Class getOwner(UI ui, Bean bean) { .filter(annotation -> annotation instanceof RouteScopeOwner) .map(annotation -> (Class) (((RouteScopeOwner) annotation) .value())) - .findFirst().orElseGet(() -> getCurrentNavigationTarget(ui)); + .findFirst() + .orElseGet(() -> getCurrentNavigationTarget(ui, bean)); } @SuppressWarnings("rawtypes") - private Class getCurrentNavigationTarget(UI ui) { + private Class getCurrentNavigationTarget(UI ui, Bean bean) { NavigationData data = ComponentUtil.getData(ui, NavigationData.class); + if (data == null) { + throw new IllegalStateException(String.format( + "There is no yet any navigation chain available, " + + "so bean '%s' has no scope and may not be injected", + bean.getBeanClass().getName())); + } return data.getNavigationTarget(); } diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java index fc6c51b7..e7da5172 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java @@ -42,7 +42,6 @@ import com.vaadin.cdi.DeploymentValidator.BeanInfo; import com.vaadin.cdi.DeploymentValidator.DeploymentProblem; import com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode; -import com.vaadin.cdi.annotation.NormalRouteScoped; import com.vaadin.cdi.annotation.NormalUIScoped; import com.vaadin.cdi.annotation.RouteScopeOwner; import com.vaadin.cdi.annotation.RouteScoped; @@ -55,10 +54,7 @@ import com.vaadin.flow.router.Route; import com.vaadin.flow.router.RouterLayout; -import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.ABSENT_OWNER_OF_NON_ROUTE_COMPONENT; -import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.NON_ROUTE_SCOPED_HAVE_OWNER; import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.NORMAL_SCOPED_COMPONENT; -import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.OWNER_IS_NOT_ROUTE_COMPONENT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -81,15 +77,6 @@ public static class NormalScopedBean { public static class ProducedNormalScopedComponent extends Component { } - @RouteScopeOwner(RouteTargetOfSelf.class) - public static class NonRouteScopedHaveOwner { - } - - @RouteScopeOwner(PseudoScopedLabel.class) - @RouteScoped - public static class OwnerIsNotRouteComponent { - } - @Route @RouteScoped @RouteScopeOwner(RouteTargetOfSelf.class) @@ -136,10 +123,6 @@ public static class RouteTargetOfRouterLayout { public static class BeanOfHasErrorParameter { } - @Vetoed - public static class ProducedRouteScopedBeanWithoutOwner { - } - private static class ProblemId { private final ErrorCode errorCode; private final Type baseType; @@ -213,23 +196,15 @@ public void validate_normalScopedProblems_collected() { @Test public void validate_routeScopedProblems_collected() { - Set infoSet = createBeanSet(NonRouteScopedHaveOwner.class, - OwnerIsNotRouteComponent.class, RouteTargetOfSelf.class, + Set infoSet = createBeanSet(RouteTargetOfSelf.class, TestRouteScopedTarget.class, TestRouterLayout.class, TestHasErrorParameter.class, BeanOfHasErrorParameter.class, BeanOfRouterLayout.class, BeanOfRouteTarget.class, - RouteTargetOfRouterLayout.class, - ProducedRouteScopedBeanWithoutOwner.class); + RouteTargetOfRouterLayout.class); validator.validateForTest(infoSet, problems::add); - assertEquals(4, problems.size()); - assertProblemExists(OWNER_IS_NOT_ROUTE_COMPONENT, - OwnerIsNotRouteComponent.class); - assertProblemExists(NON_ROUTE_SCOPED_HAVE_OWNER, - NonRouteScopedHaveOwner.class); - assertProblemExists(ABSENT_OWNER_OF_NON_ROUTE_COMPONENT, - ProducedRouteScopedBeanWithoutOwner.class); + assertEquals(0, problems.size()); } private void assertProblemExists(ErrorCode errorCode, Type baseType) { @@ -264,12 +239,6 @@ private ProducedNormalScopedComponent getProducedComponent() { return new ProducedNormalScopedComponent(); } - @Produces - @NormalRouteScoped - private ProducedRouteScopedBeanWithoutOwner getProducedRouteScopedBeanWithoutOwner() { - return new ProducedRouteScopedBeanWithoutOwner(); - } - private static Logger getLogger() { return LoggerFactory.getLogger(DeploymentValidatorTest.class); } diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextualStorageManagerTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextualStorageManagerTest.java index 684cc574..017c9b42 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextualStorageManagerTest.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextualStorageManagerTest.java @@ -16,88 +16,109 @@ package com.vaadin.cdi.context; -import com.vaadin.cdi.annotation.RouteScopeOwner; -import com.vaadin.cdi.annotation.RouteScoped; -import com.vaadin.flow.component.HasElement; -import com.vaadin.flow.dom.Element; -import com.vaadin.flow.router.AfterNavigationEvent; -import com.vaadin.flow.router.Route; +import javax.annotation.PreDestroy; +import javax.enterprise.event.Event; +import javax.inject.Inject; +import javax.inject.Provider; + +import java.util.Collections; + import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import javax.enterprise.event.Event; -import javax.inject.Inject; -import javax.inject.Provider; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.context.RouteScopedContext.NavigationData; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.HasElement; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.page.ExtendedClientDetails; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.LocationChangeEvent; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinSession; @RunWith(CdiTestRunner.class) public class RouteContextualStorageManagerTest { private static final String STATE = "hello"; - private abstract static class HasElementTestBean extends TestBean implements HasElement { + private abstract static class HasElementTestBean extends TestBean + implements HasElement { @Override public Element getElement() { return null; } } - @RouteScoped @Route("group1") - public static class Group1 extends HasElementTestBean { + public static class Group1 extends HasElementTestBean { } @RouteScoped @RouteScopeOwner(Group1.class) public static class MemberOfGroup1 extends HasElementTestBean { + + boolean isDestroyed; + + @PreDestroy + private void onDestroy() { + isDestroyed = true; + } } @RouteScoped + public static class NoOwnerBean extends HasElementTestBean { + + boolean isDestroyed; + + @PreDestroy + private void onDestroy() { + isDestroyed = true; + } + + } + @Route("group2") public static class Group2 extends HasElementTestBean { } - private UIUnderTestContext uiUnderTestContext; + @Route("") + public static class InitialRoute extends HasElementTestBean { - @Inject - private Provider group1; + } + + private UIUnderTestContext uiUnderTestContext; @Inject @RouteScopeOwner(Group1.class) private Provider memberOfGroup1; @Inject - private Provider group2; + private Provider noOwnerBean; + + @Inject + private Event beforeNavigationTrigger; @Inject private Event afterNavigationTrigger; - private List chain; - private AfterNavigationEvent event; + private BeforeEnterEvent event; + private AfterNavigationEvent afterEvent; + private LocationChangeEvent changeEvent; + + private NavigationData data = Mockito.mock(NavigationData.class); @Before public void setUp() { - uiUnderTestContext = new UIUnderTestContext(); - uiUnderTestContext.activate(); - - group1.get().setState(STATE); - group2.get().setState(STATE); - memberOfGroup1.get().setState(STATE); - - assertEquals(STATE, group1.get().getState()); - assertEquals(STATE, memberOfGroup1.get().getState()); - assertEquals(STATE, group2.get().getState()); - - chain = new ArrayList<>(); - event = Mockito.mock(AfterNavigationEvent.class); - Mockito.when(event.getActiveChain()).thenReturn(chain); + doSetUp(null, null); } @After @@ -105,34 +126,159 @@ public void tearDown() { uiUnderTestContext.tearDownAll(); } + @Test(expected = IllegalStateException.class) + public void onBeforeEnter_initialNavigationTarget_scopeDoesNotExist_Throws() { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) InitialRoute.class); + beforeNavigationTrigger.fire(event); + + memberOfGroup1.get(); + } + + @Test(expected = IllegalStateException.class) + public void afterNavigation_initialNavigationTarget_scopeDoesNotExist_Throws() { + Mockito.when(afterEvent.getActiveChain()) + .thenReturn(Collections.singletonList(new InitialRoute())); + afterNavigationTrigger.fire(afterEvent); + + memberOfGroup1.get(); + } + @Test - public void onAfterNavigation_chainIsEmpty_allDestroyed() { - afterNavigationTrigger.fire(event); + public void onBeforeEnter_group1Navigation_beansAreScoped() { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean = memberOfGroup1.get(); + bean.setState(STATE); + Assert.assertEquals(STATE, memberOfGroup1.get().getState()); - assertEquals("", group1.get().getState()); - assertEquals("", memberOfGroup1.get().getState()); - assertEquals("", group2.get().getState()); + noOwnerBean.get().setState(STATE); + Assert.assertEquals(STATE, noOwnerBean.get().getState()); } @Test - public void onAfterNavigation_chainDoesNotContainOwner_ownerDestroyedOtherRemained() { - chain.add(group1.get()); - afterNavigationTrigger.fire(event); + public void onBeforeEnter_group2NavigationAfterGroup1_beansAreDestroyed() { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); - assertEquals(STATE, group1.get().getState()); - assertEquals(STATE, memberOfGroup1.get().getState()); - assertEquals("", group2.get().getState()); + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + NoOwnerBean bean2 = noOwnerBean.get(); + bean2.setState(STATE); + + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group2.class); + beforeNavigationTrigger.fire(event); + + Assert.assertTrue(bean1.isDestroyed); + Assert.assertTrue(bean2.isDestroyed); + + // no owner bean is not preserved: the new one is created + Assert.assertNotEquals(STATE, noOwnerBean.get().getState()); } @Test - public void onAfterNavigation_chainDoesNotContainOwnerForAssigned_bothDestroyedOtherRemained() { - chain.add(group2.get()); - chain.add(memberOfGroup1.get()); - afterNavigationTrigger.fire(event); + public void afterNavigation_group2NavigationAftergroup1_beansAreDestroyed() { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + NoOwnerBean bean2 = noOwnerBean.get(); + bean2.setState(STATE); + + Mockito.when(afterEvent.getActiveChain()) + .thenReturn(Collections.singletonList(new Group2())); + afterNavigationTrigger.fire(afterEvent); - assertEquals("", group1.get().getState()); - assertEquals("", memberOfGroup1.get().getState()); - assertEquals(STATE, group2.get().getState()); + Assert.assertTrue(bean1.isDestroyed); + Assert.assertTrue(bean2.isDestroyed); } + @Test + public void preserveOnRefresh_anotherUIHasSameWindowName_beanIsPreserved() { + UI ui = doSetUp("foo", null); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + + // set another UI instance with the same window name into the context + doSetUp("foo", ui.getSession()); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + ComponentUtil.onComponentDetach(ui); + + Assert.assertFalse(bean1.isDestroyed); + Assert.assertEquals(STATE, memberOfGroup1.get().getState()); + } + + @Test + public void onBeforeEnter_anotherUIHasNoWindowName_beanIsDestroyedOnUiDestroy() { + UI ui = doSetUp("foo", null); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + + // set another UI instance with the same window name into the context + doSetUp(null, ui.getSession()); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + ComponentUtil.onComponentDetach(ui); + + Assert.assertTrue(bean1.isDestroyed); + Assert.assertNotEquals(STATE, memberOfGroup1.get().getState()); + } + + private UI doSetUp(String windowName, VaadinSession session) { + changeEvent = Mockito.mock(LocationChangeEvent.class); + + Mockito.when(data.getNavigationTarget()) + .thenReturn((Class) InitialRoute.class); + event = Mockito.mock(BeforeEnterEvent.class); + uiUnderTestContext = new UIUnderTestContext(session) { + + @Override + public void activate() { + super.activate(); + + if (windowName != null) { + ExtendedClientDetails details = Mockito + .mock(ExtendedClientDetails.class); + Mockito.when(details.getWindowName()) + .thenReturn(windowName); + getUi().getInternals().setExtendedClientDetails(details); + } + + ComponentUtil.setData(getUi(), NavigationData.class, data); + Mockito.when(changeEvent.getUI()).thenReturn(getUi()); + Mockito.when(event.getUI()) + .thenReturn(uiUnderTestContext.getUi()); + } + }; + uiUnderTestContext.activate(); + + UI ui = uiUnderTestContext.getUi(); + + uiUnderTestContext.getUi().getSession().addUI(ui); + + afterEvent = Mockito.mock(AfterNavigationEvent.class); + Mockito.when(afterEvent.getLocationChangeEvent()) + .thenReturn(changeEvent); + + return uiUnderTestContext.getUi(); + } } diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionUnderTestContext.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionUnderTestContext.java index e2a80a6a..d3e5a1f3 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionUnderTestContext.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionUnderTestContext.java @@ -56,6 +56,9 @@ private void mockSession() { when(session.getConfiguration()).thenReturn(configuration); Properties props = new Properties(); when(configuration.getInitParameters()).thenReturn(props); + + doCallRealMethod().when(session).addUI(Mockito.any()); + doCallRealMethod().when(session).getUIs(); } @Override diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java index 8c326d9b..28dc486e 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java @@ -21,4 +21,4 @@ @Tag(Tag.A) class TestNavigationTarget extends Component { -} \ No newline at end of file +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java index 4c889b6c..56985c86 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java @@ -27,6 +27,13 @@ public class UIUnderTestContext implements UnderTestContext { private static int uiIdNdx = 0; private static SessionUnderTestContext sessionContextUnderTest; + protected UIUnderTestContext() { + this(null); + } + + protected UIUnderTestContext(VaadinSession session) { + this.session = session; + } private void mockUI() { if (session == null) { From 66f90348a7e0a9c8555fc59b83bd0bc615670f1f Mon Sep 17 00:00:00 2001 From: Denis Anisimov Date: Mon, 7 Jun 2021 12:56:02 +0300 Subject: [PATCH 3/3] chore: correct test class --- .../test/java/com/vaadin/cdi/context/UIUnderTestContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java index 56985c86..c776c796 100644 --- a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java @@ -27,7 +27,7 @@ public class UIUnderTestContext implements UnderTestContext { private static int uiIdNdx = 0; private static SessionUnderTestContext sessionContextUnderTest; - protected UIUnderTestContext() { + public UIUnderTestContext() { this(null); }