From 0776dfde32180b77bd15e25587f4046f5e342e98 Mon Sep 17 00:00:00 2001 From: beirtipol Date: Sat, 5 Dec 2020 20:42:22 +0000 Subject: [PATCH 1/3] Provide a helper utility which can recursively set the name on all java.awt.Component instances which have a blank 'name' property. --- .../assertj/swing/util/ComponentNamer.java | 115 ++++++++++ .../swing/util/ComponentNamerTest.java | 206 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java create mode 100644 assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java diff --git a/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java b/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java new file mode 100644 index 00000000..3b139a55 --- /dev/null +++ b/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java @@ -0,0 +1,115 @@ +/* + * 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. + * + * Copyright 2012-2020 the original author or authors. + */ +package org.assertj.swing.util; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +import java.awt.*; +import java.lang.reflect.Field; +import java.util.*; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Strategy for applying names to components which have not been given one at compilation time. Where possible, this + * will use the field name if the component is declared at class level. For anonymously declared fields, or for those + * declared in methods, a name will be generated based on the {@link Class#getSimpleName()} of the component appended + * with an incremented long. + * + * @author Beirti O'Nunain + */ +public class ComponentNamer { + private Container container; + private AtomicLong counter; + + private static final Map, List> DECLARED_FIELDS_BY_CLASS = new HashMap<>(); + + private ComponentNamer(Container container) { + this.container = container; + counter = new AtomicLong(1L); + } + + /** + * Create an instance of a ComponentNamer + * @param container to introspect + * @return a new instance of a ComponentNamer + */ + public static ComponentNamer namer(Container container) { + if (container == null) { + throw new IllegalArgumentException("Container cannot be null"); + } + return new ComponentNamer(container); + } + + /** + * Populate the 'name' field of any component with either its field name, if declared as a field, + * or with a generated String of the format 'field.getClass().getSimpleName() + "-" + Integer' + */ + public void setMissingNames() { + setMissingNames(container, new LinkedHashMap<>()); + } + + private void setMissingNames(Container container, Map parentFields) { + // @format:off + // First, find all declared fields in the component's hierarchy so we can use their declared names + getAllDeclaredFields(container.getClass()).forEach(f -> parentFields.put(getFieldValue(container, f), f.getName())); + // Now, recursively set names on the component hierarchy. + stream(container.getComponents()) + .forEach(e -> { + if (e instanceof Container) { + setMissingNames((Container) e, parentFields); + } + setMissingName(e, parentFields); + }); + // @format:on + + } + + private void setMissingName(Component component, Map allFields) { + if (component.getName() == null || component.getName().isEmpty()) { + if (allFields.containsKey(component)) { + component.setName(allFields.get(component)); + } else { + component.setName(component.getClass().getSimpleName() + "-" + counter.getAndIncrement()); + } + } + } + + private Object getFieldValue(Container container, Field f) { + try { + f.setAccessible(true); + return f.get(container); + } catch (IllegalAccessException e) { + return null; + } + } + + private List getAllDeclaredFields(Class clazz) { + List fields = DECLARED_FIELDS_BY_CLASS.get(clazz); + if (fields == null) { + fields = new ArrayList<>(); + + while (clazz != null) { + //@format:off + fields.addAll(stream(clazz.getDeclaredFields()) + .filter(f -> Component.class.isAssignableFrom(f.getDeclaringClass())) + .collect(toList())); + //@format:on + clazz = clazz.getSuperclass(); + } + DECLARED_FIELDS_BY_CLASS.put(clazz, fields); + } + return fields; + } +} diff --git a/assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java b/assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java new file mode 100644 index 00000000..8e9264ae --- /dev/null +++ b/assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java @@ -0,0 +1,206 @@ +/* + * 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. + * + * Copyright 2012-2020 the original author or authors. + */ +package org.assertj.swing.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.swing.edt.GuiActionRunner.execute; +import static org.assertj.swing.test.ExpectedException.none; +import static org.assertj.swing.util.ComponentNamer.namer; + +import java.awt.*; + +import javax.swing.*; + +import org.assertj.swing.fixture.ContainerFixture; +import org.assertj.swing.test.ExpectedException; +import org.assertj.swing.test.core.RobotBasedTestCase; +import org.assertj.swing.test.swing.TestWindow; +import org.junit.Rule; +import org.junit.Test; + +/** + * Tests and demonstrates application of {@link ComponentNamer#namer(Container)} + * + * @author Beirti O'Nunain + */ +public class ComponentNamerTest extends RobotBasedTestCase { + @Rule + public ExpectedException thrown = none(); + + private SimpleWindow simpleWindow; + private ContainerFixture simpleFixture; + + private NestedWindow nestedWindow; + private ContainerFixture nestedFixture; + + private SimpleWindowAlreadyNamed simpleAlreadyNamedWindow; + private ContainerFixture simpleAlreadyNamedFixture; + + private SimpleWindowAnonymousField simpleAnonymousFieldWindow; + private ContainerFixture simpleAnonymousFieldFixture; + + private ParentReferenceWindow parentReferenceWindow; + private ContainerFixture parentReferenceFixture; + + @Override + protected final void onSetUp() { + simpleWindow = SimpleWindow.createNew(getClass()); + simpleFixture = new ContainerFixture(robot, simpleWindow); + + nestedWindow = NestedWindow.createNew(getClass()); + nestedFixture = new ContainerFixture(robot, nestedWindow); + + simpleAlreadyNamedWindow = SimpleWindowAlreadyNamed.createNew(getClass()); + simpleAlreadyNamedFixture = new ContainerFixture(robot, simpleAlreadyNamedWindow); + + simpleAnonymousFieldWindow = SimpleWindowAnonymousField.createNew(getClass()); + simpleAnonymousFieldFixture = new ContainerFixture(robot, simpleAnonymousFieldWindow); + + parentReferenceWindow = ParentReferenceWindow.createNew(getClass()); + parentReferenceFixture = new ContainerFixture(robot, parentReferenceWindow); + } + + @Test + public void should_name_field_with_declared_name() { + robot.showWindow(simpleWindow); + namer(simpleWindow).setMissingNames(); + assertThat(simpleFixture.textBox("specialText").target()).isSameAs(simpleWindow.specialText); + } + + @Test + public void should_name_field_with_declared_name_on_nested_window() { + robot.showWindow(nestedWindow); + namer(nestedWindow).setMissingNames(); + assertThat(nestedFixture.textBox("specialText").target()).isSameAs(nestedWindow.specialText); + } + + @Test + public void should_not_rename_already_named_field() { + robot.showWindow(simpleAlreadyNamedWindow); + namer(simpleAlreadyNamedWindow).setMissingNames(); + assertThat(simpleAlreadyNamedFixture.textBox("notReallyThatSpecial").target()).isSameAs(simpleAlreadyNamedWindow.specialText); + } + + @Test + public void should_name_anonymous_field_with_generated_name() { + robot.showWindow(simpleAnonymousFieldWindow); + namer(simpleAnonymousFieldWindow).setMissingNames(); + simpleAnonymousFieldFixture.textBox("JTextField-2").requireText("b"); + simpleAnonymousFieldFixture.textBox("JTextField-1").requireText("a"); + } + + @Test + public void should_throw_IllegalArgumentException_if_attempting_to_name_null() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessageToContain("Container cannot be null"); + namer(null); + } + + @Test + public void should_handle_circular_reference() { + robot.showWindow(parentReferenceWindow); + namer(parentReferenceWindow).setMissingNames(); + parentReferenceFixture.textBox("JTextField-1").requireText("child"); + } + + @Test + public void multiple_calls_are_idempotent() { + robot.showWindow(simpleAnonymousFieldWindow); + // First naming attempt + ComponentNamer namer = namer(simpleAnonymousFieldWindow); + namer.setMissingNames(); + simpleAnonymousFieldFixture.textBox("JTextField-2").requireText("b"); + simpleAnonymousFieldFixture.textBox("JTextField-1").requireText("a"); + + // Second naming attempt + namer.setMissingNames(); + simpleAnonymousFieldFixture.textBox("JTextField-2").requireText("b"); + simpleAnonymousFieldFixture.textBox("JTextField-1").requireText("a"); + } + + private static class SimpleWindow extends TestWindow { + final JTextField specialText = new JTextField(); + + static SimpleWindow createNew(final Class testClass) { + return execute(() -> new SimpleWindow(testClass)); + } + + private SimpleWindow(Class testClass) { + super(testClass); + addComponents(specialText); + } + } + + private static class NestedWindow extends TestWindow { + final JTextField specialText = new JTextField(); + + static NestedWindow createNew(final Class testClass) { + return execute(() -> new NestedWindow(testClass)); + } + + private NestedWindow(Class testClass) { + super(testClass); + JPanel nestedPanel = new JPanel(); + nestedPanel.add(specialText); + addComponents(nestedPanel); + } + } + + private static class SimpleWindowAlreadyNamed extends TestWindow { + final JTextField specialText = new JTextField(); + + static SimpleWindowAlreadyNamed createNew(final Class testClass) { + return execute(() -> new SimpleWindowAlreadyNamed(testClass)); + } + + private SimpleWindowAlreadyNamed(Class testClass) { + super(testClass); + specialText.setName("notReallyThatSpecial"); + addComponents(specialText); + } + } + + private static class SimpleWindowAnonymousField extends TestWindow { + static SimpleWindowAnonymousField createNew(final Class testClass) { + return execute(() -> new SimpleWindowAnonymousField(testClass)); + } + + private SimpleWindowAnonymousField(Class testClass) { + super(testClass); + addComponents(new JTextField("a"), new JTextField("b")); + } + } + + private static class ParentReferenceWindow extends TestWindow { + static ParentReferenceWindow createNew(final Class testClass) { + return execute(() -> new ParentReferenceWindow(testClass)); + } + + private ChildReferenceWindow child; + + private ParentReferenceWindow(Class testClass) { + super(testClass); + child = new ChildReferenceWindow(this); + addComponents(child); + } + } + + private static class ChildReferenceWindow extends JPanel { + private final ParentReferenceWindow parentWindow; + + private ChildReferenceWindow(ParentReferenceWindow parentWindow) { + this.parentWindow = parentWindow; + add(new JTextField("child")); + } + } +} From 5d951d535c5a7805d34801a9cb6044099859803b Mon Sep 17 00:00:00 2001 From: beirtipol Date: Sun, 6 Dec 2020 14:39:52 +0000 Subject: [PATCH 2/3] Include strategies for overwriting existing names and forcing the use of generated names. --- .../assertj/swing/util/ComponentNamer.java | 34 +++++++++++++++++-- .../swing/util/ComponentNamerTest.java | 25 +++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java b/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java index 3b139a55..5d8a899f 100644 --- a/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java +++ b/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java @@ -35,6 +35,9 @@ public class ComponentNamer { private static final Map, List> DECLARED_FIELDS_BY_CLASS = new HashMap<>(); + private boolean overwriteExisting = false; + private boolean useGeneratedNamesOnly = false; + private ComponentNamer(Container container) { this.container = container; counter = new AtomicLong(1L); @@ -43,7 +46,7 @@ private ComponentNamer(Container container) { /** * Create an instance of a ComponentNamer * @param container to introspect - * @return a new instance of a ComponentNamer + * @return the namer for fluent coding */ public static ComponentNamer namer(Container container) { if (container == null) { @@ -52,6 +55,27 @@ public static ComponentNamer namer(Container container) { return new ComponentNamer(container); } + /** + * Overwrite the name of the field, even if it already has one. This is useful when dealing with 3rd party components + * that have unsuitable names. Combine with {@link ComponentNamer#useGeneratedNamesOnly} to force a consistent + * set of names for the components. + * @return the namer for fluent coding + */ + public ComponentNamer overwriteExisting() { + this.overwriteExisting = true; + return this; + } + + /** + * Only use generated names for the components. This is useful when the hierarchy of Containers contains components + * which have the same name set, or have declared fields with the same name. + * @return the namer for fluent coding + */ + public ComponentNamer useGeneratedNamesOnly() { + this.useGeneratedNamesOnly = true; + return this; + } + /** * Populate the 'name' field of any component with either its field name, if declared as a field, * or with a generated String of the format 'field.getClass().getSimpleName() + "-" + Integer' @@ -77,8 +101,8 @@ private void setMissingNames(Container container, Map parentFiel } private void setMissingName(Component component, Map allFields) { - if (component.getName() == null || component.getName().isEmpty()) { - if (allFields.containsKey(component)) { + if (shouldRenameComponent(component)) { + if (!useGeneratedNamesOnly && allFields.containsKey(component)) { component.setName(allFields.get(component)); } else { component.setName(component.getClass().getSimpleName() + "-" + counter.getAndIncrement()); @@ -86,6 +110,10 @@ private void setMissingName(Component component, Map allFields) } } + private boolean shouldRenameComponent(Component component) { + return overwriteExisting || (component.getName() == null || component.getName().isEmpty()); + } + private Object getFieldValue(Container container, Field f) { try { f.setAccessible(true); diff --git a/assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java b/assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java index 8e9264ae..4e2ae5a8 100644 --- a/assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java +++ b/assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java @@ -35,9 +35,9 @@ */ public class ComponentNamerTest extends RobotBasedTestCase { @Rule - public ExpectedException thrown = none(); + public ExpectedException thrown = none(); - private SimpleWindow simpleWindow; + private SimpleWindow simpleWindow; private ContainerFixture simpleFixture; private NestedWindow nestedWindow; @@ -49,8 +49,8 @@ public class ComponentNamerTest extends RobotBasedTestCase { private SimpleWindowAnonymousField simpleAnonymousFieldWindow; private ContainerFixture simpleAnonymousFieldFixture; - private ParentReferenceWindow parentReferenceWindow; - private ContainerFixture parentReferenceFixture; + private ParentReferenceWindow parentReferenceWindow; + private ContainerFixture parentReferenceFixture; @Override protected final void onSetUp() { @@ -66,7 +66,7 @@ protected final void onSetUp() { simpleAnonymousFieldWindow = SimpleWindowAnonymousField.createNew(getClass()); simpleAnonymousFieldFixture = new ContainerFixture(robot, simpleAnonymousFieldWindow); - parentReferenceWindow = ParentReferenceWindow.createNew(getClass()); + parentReferenceWindow = ParentReferenceWindow.createNew(getClass()); parentReferenceFixture = new ContainerFixture(robot, parentReferenceWindow); } @@ -91,6 +91,20 @@ public void should_not_rename_already_named_field() { assertThat(simpleAlreadyNamedFixture.textBox("notReallyThatSpecial").target()).isSameAs(simpleAlreadyNamedWindow.specialText); } + @Test + public void should_overwrite_named_field_if_asked() { + robot.showWindow(simpleAlreadyNamedWindow); + namer(simpleAlreadyNamedWindow).overwriteExisting().setMissingNames(); + assertThat(simpleAlreadyNamedFixture.textBox("specialText").target()).isSameAs(simpleAlreadyNamedWindow.specialText); + } + + @Test + public void should_use_generated_names_if_asked() { + robot.showWindow(simpleAlreadyNamedWindow); + namer(simpleAlreadyNamedWindow).overwriteExisting().useGeneratedNamesOnly().setMissingNames(); + assertThat(simpleAlreadyNamedFixture.textBox("JTextField-2").target()).isSameAs(simpleAlreadyNamedWindow.specialText); + } + @Test public void should_name_anonymous_field_with_generated_name() { robot.showWindow(simpleAnonymousFieldWindow); @@ -203,4 +217,5 @@ private ChildReferenceWindow(ParentReferenceWindow parentWindow) { add(new JTextField("child")); } } + } From 49f7d81f140e72bbc3db273904b4fc3c829a7ec7 Mon Sep 17 00:00:00 2001 From: beirtipol Date: Tue, 11 May 2021 12:46:30 +0100 Subject: [PATCH 3/3] Filter of declared fields was incorrectly including all fields where the parent was a component, rather than using the type of the actual field. --- .../src/main/java/org/assertj/swing/util/ComponentNamer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java b/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java index 5d8a899f..ed991778 100644 --- a/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java +++ b/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java @@ -131,7 +131,7 @@ private List getAllDeclaredFields(Class clazz) { while (clazz != null) { //@format:off fields.addAll(stream(clazz.getDeclaredFields()) - .filter(f -> Component.class.isAssignableFrom(f.getDeclaringClass())) + .filter(f -> Component.class.isAssignableFrom(f.getType())) .collect(toList())); //@format:on clazz = clazz.getSuperclass();