diff options
author | Henri Sara <hesara@vaadin.com> | 2015-04-16 09:19:16 +0300 |
---|---|---|
committer | Teemu Suo-Anttila <teemusa@vaadin.com> | 2015-04-27 13:18:22 +0300 |
commit | 6e46ed68d6cae41cb63560be9fa3ddd23bcf2e2f (patch) | |
tree | 7218936671851e5848e2592b61d9f9f0205b5428 | |
parent | f8404b9be9d8f599ae62d0be22aab3e25142796f (diff) | |
download | vaadin-framework-6e46ed68d6cae41cb63560be9fa3ddd23bcf2e2f.tar.gz vaadin-framework-6e46ed68d6cae41cb63560be9fa3ddd23bcf2e2f.zip |
Iterate over copies of listener lists (#17477)
Allows adding/removing view change listeners from listeners.
Change-Id: Idb2227e1423c0297887f01f6df03b74e633ad917
3 files changed, 177 insertions, 2 deletions
diff --git a/server/src/com/vaadin/navigator/Navigator.java b/server/src/com/vaadin/navigator/Navigator.java index ef5c61a294..65b3fec488 100644 --- a/server/src/com/vaadin/navigator/Navigator.java +++ b/server/src/com/vaadin/navigator/Navigator.java @@ -32,6 +32,7 @@ package com.vaadin.navigator; */ import java.io.Serializable; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -597,7 +598,10 @@ public class Navigator implements Serializable { * block the navigation operation */ protected boolean fireBeforeViewChange(ViewChangeEvent event) { - for (ViewChangeListener l : listeners) { + // a copy of the listener list is needed to avoid + // ConcurrentModificationException as a listener can add/remove + // listeners + for (ViewChangeListener l : new ArrayList<ViewChangeListener>(listeners)) { if (!l.beforeViewChange(event)) { return false; } @@ -647,7 +651,10 @@ public class Navigator implements Serializable { * view change event (not null) */ protected void fireAfterViewChange(ViewChangeEvent event) { - for (ViewChangeListener l : listeners) { + // a copy of the listener list is needed to avoid + // ConcurrentModificationException as a listener can add/remove + // listeners + for (ViewChangeListener l : new ArrayList<ViewChangeListener>(listeners)) { l.afterViewChange(event); } } diff --git a/uitest/src/com/vaadin/tests/navigator/NavigatorListenerModifiesListeners.java b/uitest/src/com/vaadin/tests/navigator/NavigatorListenerModifiesListeners.java new file mode 100644 index 0000000000..139dbaefb6 --- /dev/null +++ b/uitest/src/com/vaadin/tests/navigator/NavigatorListenerModifiesListeners.java @@ -0,0 +1,100 @@ +package com.vaadin.tests.navigator; + +import com.vaadin.navigator.Navigator; +import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.Label; +import com.vaadin.ui.VerticalLayout; + +public class NavigatorListenerModifiesListeners extends AbstractTestUI { + + private Navigator navigator; + + protected static final String LABEL_MAINVIEW_ID = "LABEL_MAINVIEW_ID"; + protected static final String LABEL_ANOTHERVIEW_ID = "LABEL_ANOTHERVIEW_ID"; + + // NOP view change listener + private class MyViewChangeListener implements ViewChangeListener { + @Override + public boolean beforeViewChange(ViewChangeEvent event) { + navigator.removeViewChangeListener(listener1); + navigator.addViewChangeListener(listener2); + return true; + } + + @Override + public void afterViewChange(ViewChangeEvent event) { + navigator.removeViewChangeListener(listener2); + navigator.addViewChangeListener(listener1); + } + } + + private MyViewChangeListener listener1 = new MyViewChangeListener(); + private MyViewChangeListener listener2 = new MyViewChangeListener(); + + @Override + protected void setup(VaadinRequest request) { + navigator = new Navigator(this, this); + navigator.addView(MainView.NAME, new MainView()); + navigator.addView(AnotherView.NAME, new AnotherView()); + navigator.addViewChangeListener(listener1); + navigator.navigateTo(MainView.NAME); + } + + class MainView extends VerticalLayout implements View { + + public static final String NAME = "mainview"; + + public MainView() { + Label label = new Label("MainView content"); + label.setId(LABEL_MAINVIEW_ID); + addComponent(label); + + Button buttonNavToAnotherView = new Button( + "Navigate to another view", new ClickListener() { + + @Override + public void buttonClick(ClickEvent event) { + navigator.navigateTo(AnotherView.NAME); + } + }); + addComponent(buttonNavToAnotherView); + } + + @Override + public void enter(ViewChangeEvent event) { + } + + } + + class AnotherView extends VerticalLayout implements View { + + public static final String NAME = "another"; + + public AnotherView() { + Label label = new Label("AnotherView content"); + label.setId(LABEL_ANOTHERVIEW_ID); + addComponent(label); + } + + @Override + public void enter(ViewChangeEvent event) { + } + } + + @Override + protected String getTestDescription() { + return "Adding and removing view change listeners from view change listeners should not cause a ConcurrentModificationException"; + } + + @Override + protected Integer getTicketNumber() { + return 17477; + } +}
\ No newline at end of file diff --git a/uitest/src/com/vaadin/tests/navigator/NavigatorListenerModifiesListenersTest.java b/uitest/src/com/vaadin/tests/navigator/NavigatorListenerModifiesListenersTest.java new file mode 100644 index 0000000000..d11ab790a8 --- /dev/null +++ b/uitest/src/com/vaadin/tests/navigator/NavigatorListenerModifiesListenersTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2014 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.tests.navigator; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.tests.tb3.SingleBrowserTest; + +public class NavigatorListenerModifiesListenersTest extends SingleBrowserTest { + + @Test + public void testIfConfirmBack() { + openTestURL(); + + // keep URL of main view + final String initialUrl = driver.getCurrentUrl(); + + // do it 2 times to verify that this is not broken after first time + for (int i = 0; i < 2; i++) { + // go to prompted view + WebElement button = $(ButtonElement.class).first(); + button.click(); + + // verify we are in another view and url is correct + waitForElementPresent(By + .id(NavigatorListenerModifiesListeners.LABEL_ANOTHERVIEW_ID)); + String currentUrl = driver.getCurrentUrl(); + assertEquals( + "Current URL should be equal to another view URL", + initialUrl + .replace( + NavigatorListenerModifiesListeners.MainView.NAME, + NavigatorListenerModifiesListeners.AnotherView.NAME), + currentUrl); + + // click back button + driver.navigate().back(); + + // verify we are in main view and url is correct + // without the fix for #17477, we get + // ConcurrentModificationException + waitForElementPresent(By + .id(NavigatorListenerModifiesListeners.LABEL_MAINVIEW_ID)); + currentUrl = driver.getCurrentUrl(); + assertEquals("Current URL should be equal to the initial view URL", + initialUrl, currentUrl); + } + } + +} |