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..ed991778 --- /dev/null +++ b/assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java @@ -0,0 +1,143 @@ +/* + * 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 boolean overwriteExisting = false; + private boolean useGeneratedNamesOnly = false; + + private ComponentNamer(Container container) { + this.container = container; + counter = new AtomicLong(1L); + } + + /** + * Create an instance of a ComponentNamer + * @param container to introspect + * @return the namer for fluent coding + */ + public static ComponentNamer namer(Container container) { + if (container == null) { + throw new IllegalArgumentException("Container cannot be null"); + } + 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' + */ + 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 (shouldRenameComponent(component)) { + if (!useGeneratedNamesOnly && allFields.containsKey(component)) { + component.setName(allFields.get(component)); + } else { + component.setName(component.getClass().getSimpleName() + "-" + counter.getAndIncrement()); + } + } + } + + private boolean shouldRenameComponent(Component component) { + return overwriteExisting || (component.getName() == null || component.getName().isEmpty()); + } + + 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.getType())) + .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..4e2ae5a8 --- /dev/null +++ b/assertj-swing/src/test/java/org/assertj/swing/util/ComponentNamerTest.java @@ -0,0 +1,221 @@ +/* + * 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_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); + 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")); + } + } + +}