package org.jdesktop.beans;
import static org.hamcrest.CoreMatchers.is;
import static org.jdesktop.test.SerializableSupport.serialize;
import static org.jdesktop.test.matchers.Matchers.equivalentTo;
import static org.jdesktop.test.matchers.Matchers.property;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.RETURNS_MOCKS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import java.awt.Insets;
import java.beans.BeanInfo;
import java.beans.EventSetDescriptor;
import java.beans.Introspector;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.exceptions.verification.NoInteractionsWanted;
@SuppressWarnings("nls")
public abstract class AbstractBeanInfoTest<T> {
protected Logger logger = Logger.getLogger(getClass().getName());
protected T instance;
private BeanInfo beanInfo;
private Map<Class<?>, Object> listeners;
@Before
public void setUp() throws Exception {
instance = createInstance();
beanInfo = Introspector.getBeanInfo(instance.getClass());
listeners = new HashMap<Class<?>, Object>();
for (EventSetDescriptor descriptor : beanInfo.getEventSetDescriptors()) {
Class<?> eventClass = descriptor.getListenerType();
Object listener = mock(eventClass);
descriptor.getAddListenerMethod().invoke(instance, listener);
listeners.put(eventClass, listener);
}
}
protected abstract T createInstance();
@Test
public final void testBoundProperties() throws Exception {
for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
if (descriptor.isBound()) {
if (descriptor.isHidden()) {
continue;
}
if (descriptor.getWriteMethod() == null) {
//special-case this read-only property
if ("UIClassID".equals(descriptor.getName())) {
return;
}
fail("bound read-only property: " + descriptor.getName());
}
Class<?> propertyType = descriptor.getPropertyType();
if (isUnhandledType(propertyType)) {
//TODO log?
continue;
}
Object defaultValue = descriptor.getReadMethod().invoke(instance);
Object newValue = getNewValue(propertyType, defaultValue);
descriptor.getWriteMethod().invoke(instance, newValue);
PropertyChangeListener pcl = (PropertyChangeListener) listeners.get(PropertyChangeListener.class);
verify(pcl).propertyChange(argThat(is(property(descriptor.getName(), defaultValue, newValue))));
reset(pcl);
}
}
}
private boolean isUnhandledType(Class<?> type) {
return type == null;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected Object getNewValue(Class<?> propertyType, Object defaultValue) {
Object result = null;
if (propertyType.isArray()) {
int length = defaultValue == null ? 1 : ((Object[]) defaultValue).length + 1;
result = Array.newInstance(propertyType.getComponentType(), length);
} else if (propertyType.isEnum()) {
EnumSet set = EnumSet.allOf((Class<? extends Enum>) propertyType);
int size = set.size();
if (size == 1) {
result = defaultValue == null ? set.iterator().next() : null;
} else {
int ordinal = ((Enum) defaultValue).ordinal();
ordinal = ordinal == size - 1 ? 0 : ordinal + 1;
Iterator iter = set.iterator();
for (int i = 0; i < ordinal + 1; i++) {
result = iter.next();
}
}
} else if (propertyType.isPrimitive()) {
//help short circuit all of these checks
if (propertyType == boolean.class) {
result = Boolean.FALSE.equals(defaultValue);
} else if (propertyType == int.class) {
result = ((Integer) defaultValue) + 1;
} else if (propertyType == double.class) {
result = ((Double) defaultValue) + 1d;
} else if (propertyType == float.class) {
result = ((Float) defaultValue) + 1f;
}
} else if (propertyType == String.class) {
result = "original string: " + defaultValue;
} else if (propertyType == Insets.class) {
if (new Insets(0, 0, 0, 0).equals(defaultValue)) {
result = new Insets(1, 1, 1, 1);
} else {
result = mock(propertyType);
}
} else {
result = mock(propertyType, RETURNS_MOCKS);
}
return result;
}
/**
* A simple serialization check. Ensures that the reconstituted object is equivalent to the
* original.
*/
@Test
public void testSerialization() {
if (!Serializable.class.isInstance(instance)) {
return;
}
T serialized = serialize(instance);
assertThat(serialized, is(equivalentTo(instance)));
}
@After
public void tearDown() {
for (Object listener : listeners.values()) {
try {
// TODO need a way to handle components that have contained components,
// like JXComboBox, that cause spurious container events
verifyNoMoreInteractions(listener);
} catch (NoInteractionsWanted logAndIgnore) {
logger.log(Level.WARNING, "unexpected listener notification", logAndIgnore);
}
}
}
}