Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component Name generator #255

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions assertj-swing/src/main/java/org/assertj/swing/util/ComponentNamer.java
Original file line number Diff line number Diff line change
@@ -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<Class<?>, List<Field>> 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<Object, String> 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<Object, String> 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<Field> getAllDeclaredFields(Class<?> clazz) {
List<Field> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}
}

}