package com.vaadin.tests.server;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Assert;
import org.junit.Test;
import com.vaadin.ui.Component;
public class ClassesSerializableTest {
/**
* JARs that will be scanned for classes to test, in addition to classpath
* directories.
*/
private static final String JAR_PATTERN = ".*vaadin.*\\.jar";
private static final String[] BASE_PACKAGES = { "com.vaadin" };
private static final String[] EXCLUDED_PATTERNS = {
"com\\.vaadin\\.demo\\..*", //
"com\\.vaadin\\.external\\.org\\.apache\\.commons\\.fileupload\\..*", //
"com\\.vaadin\\.launcher\\..*", //
"com\\.vaadin\\.client\\..*", //
"com\\.vaadin\\.server\\.widgetsetutils\\..*", //
"com\\.vaadin\\.server\\.themeutils\\..*", //
"com\\.vaadin\\.tests\\..*", // exclude automated tests
"com\\.vaadin\\.tools\\..*", //
"com\\.vaadin\\.ui\\.themes\\..*", //
// exact class level filtering
"com\\.vaadin\\.event\\.FieldEvents", //
"com\\.vaadin\\.event\\.LayoutEvents", //
"com\\.vaadin\\.event\\.MouseEvents", //
"com\\.vaadin\\.event\\.UIEvents", //
"com\\.vaadin\\.server\\.VaadinPortlet", //
"com\\.vaadin\\.server\\.MockServletConfig", //
"com\\.vaadin\\.server\\.MockServletContext", //
"com\\.vaadin\\.server\\.Constants", //
"com\\.vaadin\\.server\\.VaadinServiceClassLoaderUtil", //
"com\\.vaadin\\.server\\.VaadinServiceClassLoaderUtil\\$GetClassLoaderPrivilegedAction", //
"com\\.vaadin\\.server\\.communication\\.FileUploadHandler\\$SimpleMultiPartInputStream", //
"com\\.vaadin\\.server\\.communication\\.PushRequestHandler.*",
"com\\.vaadin\\.server\\.communication\\.PushHandler.*", // PushHandler
"com\\.vaadin\\.server\\.communication\\.DateSerializer", //
"com\\.vaadin\\.server\\.communication\\.JSONSerializer", //
// and its inner classes do not need to be serializable
"com\\.vaadin\\.util\\.SerializerHelper", // fully static
// class level filtering, also affecting nested classes and
// interfaces
"com\\.vaadin\\.server\\.LegacyCommunicationManager.*", //
"com\\.vaadin\\.buildhelpers.*", //
"com\\.vaadin\\.util\\.EncodeUtil.*", //
"com\\.vaadin\\.util\\.ReflectTools.*", //
"com\\.vaadin\\.data\\.provider\\.HierarchyMapper\\$TreeLevelQuery",
"com\\.vaadin\\.data\\.util\\.ReflectTools.*", //
"com\\.vaadin\\.data\\.util\\.JsonUtil.*", //
"com\\.vaadin\\.data\\.util.BeanItemContainerGenerator.*",
"com\\.vaadin\\.data\\.util\\.sqlcontainer\\.connection\\.MockInitialContextFactory",
"com\\.vaadin\\.data\\.util\\.sqlcontainer\\.DataGenerator",
"com\\.vaadin\\.data\\.util\\.sqlcontainer\\.FreeformQueryUtil",
// the JSR-303 constraint interpolation context
"com\\.vaadin\\.data\\.validator\\.BeanValidator\\$1", //
"com\\.vaadin\\.sass.*", //
"com\\.vaadin\\.testbench.*", //
"com\\.vaadin\\.util\\.CurrentInstance\\$1", //
"com\\.vaadin\\.server\\.AbstractClientConnector\\$1", //
"com\\.vaadin\\.server\\.AbstractClientConnector\\$1\\$1", //
"com\\.vaadin\\.server\\.JsonCodec\\$1", //
"com\\.vaadin\\.server\\.communication\\.PushConnection", //
"com\\.vaadin\\.server\\.communication\\.AtmospherePushConnection.*", //
"com\\.vaadin\\.util\\.ConnectorHelper", //
"com\\.vaadin\\.server\\.VaadinSession\\$FutureAccess", //
"com\\.vaadin\\.external\\..*", //
"com\\.vaadin\\.util\\.WeakValueMap.*", //
"com\\.vaadin\\.themes\\.valoutil\\.BodyStyleName", //
"com\\.vaadin\\.server\\.communication\\.JSR356WebsocketInitializer.*", //
"com\\.vaadin\\.screenshotbrowser\\.ScreenshotBrowser.*", //
"com\\.vaadin\\.osgi.*",//
"com\\.vaadin\\.server\\.osgi.*"
};
/**
* Tests that all the relevant classes and interfaces under
* {@link #BASE_PACKAGES} implement Serializable.
*
* @throws Exception
*/
@Test
public void testClassesSerializable() throws Exception {
List<String> rawClasspathEntries = getRawClasspathEntries();
List<String> classes = new ArrayList<>();
for (String location : rawClasspathEntries) {
classes.addAll(findServerClasses(location));
}
ArrayList<Field> nonSerializableFunctionFields = new ArrayList<>();
ArrayList<Class<?>> nonSerializableClasses = new ArrayList<>();
for (String className : classes) {
Class<?> cls = Class.forName(className);
// Don't add classes that have a @Ignore annotation on the class
if (isTestClass(cls)) {
continue;
}
// report fields that use lambda types that won't be serializable
// (also in syntehtic classes)
Stream.of(cls.getDeclaredFields())
.filter(field -> isFunctionalType(field.getGenericType()))
.forEach(nonSerializableFunctionFields::add);
// skip annotations and synthetic classes
if (cls.isAnnotation() || cls.isSynthetic()) {
continue;
}
if (Component.class.isAssignableFrom(cls) && !cls.isInterface()
&& !Modifier.isAbstract(cls.getModifiers())) {
serializeAndDeserialize(cls);
}
// report non-serializable classes and interfaces
if (!Serializable.class.isAssignableFrom(cls)) {
if (cls.getSuperclass() == Object.class
&& cls.getInterfaces().length == 1) {
// Single interface implementors
Class<?> iface = cls.getInterfaces()[0];
if (iface == Runnable.class) {
// Ignore Runnables used with access()
continue;
} else if (iface == Comparator.class) {
// Ignore inline comparators
continue;
}
}
nonSerializableClasses.add(cls);
// TODO easier to read when testing
// System.err.println(cls);
}
}
// useful failure message including all non-serializable classes and
// interfaces
if (!nonSerializableClasses.isEmpty()) {
failSerializableClasses(nonSerializableClasses);
}
if (!nonSerializableFunctionFields.isEmpty()) {
failSerializableFields(nonSerializableFunctionFields);
}
}
private void serializeAndDeserialize(Class<?> clazz)
throws IOException, ClassNotFoundException, InstantiationException,
IllegalAccessException, IllegalArgumentException,
InvocationTargetException {
Optional<Constructor<?>> defaultCtor = Stream
.of(clazz.getDeclaredConstructors())
.filter(ctor -> ctor.getParameterCount() == 0).findFirst();
if (!defaultCtor.isPresent()) {
return;
}
defaultCtor.get().setAccessible(true);
Object instance = defaultCtor.get().newInstance();
serializeAndDeserialize(instance);
}
public static <T> T serializeAndDeserialize(T instance)
throws IOException, ClassNotFoundException {
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(instance);
byte[] data = bs.toByteArray();
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(data));
@SuppressWarnings("unchecked")
T readObject = (T) in.readObject();
return readObject;
}
private void failSerializableFields(
ArrayList<Field> nonSerializableFunctionFields) {
String nonSerializableString = nonSerializableFunctionFields.stream()
.map(field -> String.format("%s.%s",
field.getDeclaringClass().getName(), field.getName()))
.collect(Collectors.joining(", "));
Assert.fail("Fields with functional types that are not serializable: "
+ nonSerializableString);
}
private void failSerializableClasses(
ArrayList<Class<?>> nonSerializableClasses) {
String nonSerializableString = "";
Iterator<Class<?>> it = nonSerializableClasses.iterator();
while (it.hasNext()) {
Class<?> c = it.next();
nonSerializableString += ", " + c.getName();
if (c.isAnonymousClass()) {
nonSerializableString += "(super: ";
nonSerializableString += c.getSuperclass().getName();
nonSerializableString += ", interfaces: ";
for (Class<?> i : c.getInterfaces()) {
nonSerializableString += i.getName();
nonSerializableString += ",";
}
nonSerializableString += ")";
}
}
Assert.fail(
"Serializable not implemented by the following classes and interfaces: "
+ nonSerializableString);
}
private static boolean isFunctionalType(Type type) {
return type.getTypeName().contains("java.util.function");
}
private boolean isTestClass(Class<?> cls) {
if (cls.getEnclosingClass() != null
&& isTestClass(cls.getEnclosingClass())) {
return true;
}
// Test classes with a @Test annotation on some method
for (Method method : cls.getMethods()) {
if (method.isAnnotationPresent(Test.class)) {
return true;
}
}
return false;
}
/**
* Lists all class path entries by splitting the class path string.
*
* Adapted from ClassPathExplorer.getRawClasspathEntries(), but without
* filtering.
*
* @return List of class path segment strings
*/
private final static List<String> getRawClasspathEntries() {
// try to keep the order of the classpath
List<String> locations = new ArrayList<>();
String pathSep = System.getProperty("path.separator");
String classpath = System.getProperty("java.class.path");
if (classpath.startsWith("\"")) {
classpath = classpath.substring(1);
}
if (classpath.endsWith("\"")) {
classpath = classpath.substring(0, classpath.length() - 1);
}
String[] split = classpath.split(pathSep);
locations.addAll(Arrays.asList(split));
return locations;
}
/**
* Finds the server side classes/interfaces under a class path entry -
* either a directory or a JAR that matches {@link #JAR_PATTERN}.
*
* Only classes under {@link #BASE_PACKAGES} are considered, and those
* matching {@link #EXCLUDED_PATTERNS} are filtered out.
*
* @param classpathEntry
* @return
* @throws IOException
*/
private List<String> findServerClasses(String classpathEntry)
throws IOException {
Collection<String> classes = new ArrayList<>();
File file = new File(classpathEntry);
if (file.isDirectory()) {
classes = findClassesInDirectory(null, file);
} else if (file.getName().matches(JAR_PATTERN)) {
classes = findClassesInJar(file);
} else {
System.out.println("Ignoring " + classpathEntry);
return Collections.emptyList();
}
List<String> filteredClasses = new ArrayList<>();
for (String className : classes) {
boolean ok = false;
for (String basePackage : BASE_PACKAGES) {
if (className.startsWith(basePackage + ".")) {
ok = true;
break;
}
}
for (String excludedPrefix : EXCLUDED_PATTERNS) {
if (className.matches(excludedPrefix)) {
ok = false;
break;
}
}
// Don't add test classes
if (className.contains("Test")) {
ok = false;
}
if (ok) {
filteredClasses.add(className);
}
}
return filteredClasses;
}
/**
* Lists class names (based on .class files) in a JAR file.
*
* @param file
* a valid JAR file
* @return collection of fully qualified class names in the JAR
* @throws IOException
*/
private Collection<String> findClassesInJar(File file) throws IOException {
Collection<String> classes = new ArrayList<>();
try (JarFile jar = new JarFile(file)) {
Enumeration<JarEntry> e = jar.entries();
while (e.hasMoreElements()) {
JarEntry entry = e.nextElement();
if (entry.getName().endsWith(".class")) {
String nameWithoutExtension = entry.getName()
.replaceAll("\\.class", "");
String className = nameWithoutExtension.replace('/', '.');
classes.add(className);
}
}
}
return classes;
}
/**
* Lists class names (based on .class files) in a directory (a package path
* root).
*
* @param parentPackage
* parent package name or null at root of hierarchy, used by
* recursion
* @param parent
* File representing the directory to scan
* @return collection of fully qualified class names in the directory
*/
private final static Collection<String> findClassesInDirectory(
String parentPackage, File parent) {
if (parent.isHidden()
|| parent.getPath().contains(File.separator + ".")) {
return Collections.emptyList();
}
if (parentPackage == null) {
parentPackage = "";
} else {
parentPackage += ".";
}
Collection<String> classNames = new ArrayList<>();
// add all directories recursively
File[] files = parent.listFiles();
for (File child : files) {
if (child.isDirectory()) {
classNames.addAll(findClassesInDirectory(
parentPackage + child.getName(), child));
} else if (child.getName().endsWith(".class")) {
classNames.add(parentPackage.replace(File.separatorChar, '.')
+ child.getName().replaceAll("\\.class", ""));
}
}
return classNames;
}
}