/*
* Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved.
*
* 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.
*/
package com.hazelcast.internal.serialization.impl;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.hazelcast.internal.serialization.DataSerializerHook;
import com.hazelcast.nio.serialization.DataSerializable;
import com.hazelcast.nio.serialization.DataSerializableFactory;
import com.hazelcast.nio.serialization.IdentifiedDataSerializable;
import com.hazelcast.nio.serialization.SerializableByConvention;
import com.hazelcast.nio.serialization.BinaryInterface;
import com.hazelcast.spi.AbstractLocalOperation;
import com.hazelcast.spi.annotation.PrivateApi;
import com.hazelcast.test.HazelcastParallelClassRunner;
import com.hazelcast.test.annotation.QuickTest;
import com.hazelcast.util.ConcurrentReferenceHashMap;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.Collections;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import static com.hazelcast.test.ReflectionsHelper.REFLECTIONS;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Tests to verify serializable classes conventions are observed. Each conventions test scans the classpath (excluding
* test classes) and tests <b>concrete</b> classes which implement (directly or transitively) {@code Serializable} or
* {@code DataSerializable} interface, then verifies that it's either annotated with {@link BinaryInterface},
* is excluded from conventions tests by being annotated with {@link SerializableByConvention} or
* they also implement {@code IdentifiedDataSerializable}.
* Additionally, tests whether IDS instanced obtained from DS factories
* have the same ID as the one reported by their `getId` method and that F_ID/ID combinations are unique.
*/
@RunWith(HazelcastParallelClassRunner.class)
@Category({QuickTest.class})
public class DataSerializableConventionsTest {
// subclasses of classes in the white list are not taken into account for conventions tests, as they
// inherit Serializable from a parent class and cannot implement IdentifiedDataSerializable due to
// unavailability of default constructor.
private static final Set<Class> SERIALIZABLE_WHITE_LIST;
static {
Set<Class> whiteList = new HashSet<Class>();
whiteList.add(EventObject.class);
whiteList.add(Throwable.class);
whiteList.add(Permission.class);
whiteList.add(PermissionCollection.class);
SERIALIZABLE_WHITE_LIST = Collections.unmodifiableSet(whiteList);
}
/**
* Verifies that any class which is {@link DataSerializable} and is not annotated with {@link BinaryInterface}
* is also an {@link IdentifiedDataSerializable}.
*/
@Test
public void test_dataSerializableClasses_areIdentifiedDataSerializable() {
Set<Class<? extends DataSerializable>> dataSerializableClasses = REFLECTIONS.getSubTypesOf(DataSerializable.class);
Set<Class<? extends IdentifiedDataSerializable>> allIdDataSerializableClasses
= REFLECTIONS.getSubTypesOf(IdentifiedDataSerializable.class);
dataSerializableClasses.removeAll(allIdDataSerializableClasses);
// also remove IdentifiedDataSerializable itself
dataSerializableClasses.remove(IdentifiedDataSerializable.class);
// do not check abstract classes & interfaces
filterNonConcreteClasses(dataSerializableClasses);
// locate all classes annotated with BinaryInterface and remove those as well
Set<?> allAnnotatedClasses = REFLECTIONS.getTypesAnnotatedWith(BinaryInterface.class, true);
dataSerializableClasses.removeAll(allAnnotatedClasses);
// exclude @SerializableByConvention classes
Set<?> serializableByConventions = REFLECTIONS.getTypesAnnotatedWith(SerializableByConvention.class, true);
dataSerializableClasses.removeAll(serializableByConventions);
if (dataSerializableClasses.size() > 0) {
SortedSet<String> nonCompliantClassNames = new TreeSet<String>();
for (Object o : dataSerializableClasses) {
nonCompliantClassNames.add(o.toString());
}
System.out.println("The following classes are DataSerializable while they should be IdentifiedDataSerializable:");
// failure - output non-compliant classes to standard output and fail the test
for (String s : nonCompliantClassNames) {
System.out.println(s);
}
fail("There are " + dataSerializableClasses.size() + " classes which are DataSerializable, not @BinaryInterface-"
+ "annotated and are not IdentifiedDataSerializable.");
}
}
/**
* Verifies that any class which is {@link Serializable} and is not annotated with {@link BinaryInterface}
* is also an {@link IdentifiedDataSerializable}.
*/
@Test
public void test_serializableClasses_areIdentifiedDataSerializable() {
Set<Class<? extends Serializable>> serializableClasses = REFLECTIONS.getSubTypesOf(Serializable.class);
Set<Class<? extends IdentifiedDataSerializable>> allIdDataSerializableClasses
= REFLECTIONS.getSubTypesOf(IdentifiedDataSerializable.class);
serializableClasses.removeAll(allIdDataSerializableClasses);
// do not check abstract classes & interfaces
filterNonConcreteClasses(serializableClasses);
// locate all classes annotated with BinaryInterface and remove those as well
Set<?> allAnnotatedClasses = REFLECTIONS.getTypesAnnotatedWith(BinaryInterface.class, true);
serializableClasses.removeAll(allAnnotatedClasses);
// exclude @SerializableByConvention classes
Set<?> serializableByConventions = REFLECTIONS.getTypesAnnotatedWith(SerializableByConvention. class , true);
serializableClasses.removeAll(serializableByConventions);
if (serializableClasses.size() > 0) {
SortedSet<String> nonCompliantClassNames = new TreeSet<String>();
for (Object o : serializableClasses) {
if (!inheritsFromWhiteListedClass((Class) o)) {
nonCompliantClassNames.add(o.toString());
}
}
if (!nonCompliantClassNames.isEmpty()) {
System.out.println("The following classes are Serializable and should be IdentifiedDataSerializable:");
// failure - output non-compliant classes to standard output and fail the test
for (String s : nonCompliantClassNames) {
System.out.println(s);
}
fail("There are " + nonCompliantClassNames.size() + " classes which are Serializable, not @BinaryInterface-"
+ "annotated and are not IdentifiedDataSerializable.");
}
}
}
/**
* Fails when {@link IdentifiedDataSerializable} classes:
* - do not have a default no-args constructor
* - factoryId/id pairs are not unique per class
*/
@Test
public void test_identifiedDataSerializables_haveUniqueFactoryAndTypeId() throws Exception {
Set<String> classesWithInstantiationProblems = new TreeSet<String>();
Set<String> classesThrowingUnsupportedOperationException = new TreeSet<String>();
Multimap<Integer, Integer> factoryToTypeId = HashMultimap.create();
Set<Class<? extends IdentifiedDataSerializable>> identifiedDataSerializables = getIDSConcreteClasses();
for (Class<? extends IdentifiedDataSerializable> klass : identifiedDataSerializables) {
// exclude classes which are known to be meant for local use only
if (!AbstractLocalOperation.class.isAssignableFrom(klass)) {
// wrap all of this in try-catch, as it is legitimate for some classes to throw UnsupportedOperationException
try {
Constructor<? extends IdentifiedDataSerializable> ctor = klass.getDeclaredConstructor();
ctor.setAccessible(true);
IdentifiedDataSerializable instance = ctor.newInstance();
int factoryId = instance.getFactoryId();
int typeId = instance.getId();
if (factoryToTypeId.containsEntry(factoryId, typeId)) {
fail("Factory-Type ID pair {" + factoryId + ", " + typeId + "} from " + klass.toString() + " is already"
+ " registered in another type.");
} else {
factoryToTypeId.put(factoryId, typeId);
}
} catch (UnsupportedOperationException e) {
// expected from local operation classes not meant for serialization
// gather those and print them to system.out for information at end of test
classesThrowingUnsupportedOperationException.add(klass.getName());
} catch (InstantiationException e) {
classesWithInstantiationProblems.add(klass.getName() + " failed with " + e.getMessage());
} catch (NoSuchMethodException e) {
classesWithInstantiationProblems.add(klass.getName() + " failed with " + e.getMessage());
}
}
}
if (!classesThrowingUnsupportedOperationException.isEmpty()) {
System.out.println("INFO: " + classesThrowingUnsupportedOperationException.size() + " classes threw"
+ " UnsupportedOperationException in getFactoryId/getId invocation:");
for (String className : classesThrowingUnsupportedOperationException) {
System.out.println(className);
}
}
if (!classesWithInstantiationProblems.isEmpty()) {
System.out.println("There are " + classesWithInstantiationProblems.size() + " classes which threw an exception while"
+ " attempting to invoke a default no-args constructor. See console output for exception details."
+ " List of problematic classes:");
for (String className : classesWithInstantiationProblems) {
System.out.println(className);
}
fail("There are " + classesWithInstantiationProblems.size() + " classes which threw an exception while"
+ " attempting to invoke a default no-args constructor. See test output for exception details.");
}
}
/**
* Locates {@link IdentifiedDataSerializable} classes via reflection, iterates over them and asserts an instance created by
* a factory is of the same classes as an instance created via reflection.
*/
@Test
public void test_identifiedDataSerializables_areInstancesOfSameClass_whenConstructedFromFactory() throws Exception {
Set<Class<? extends DataSerializerHook>> dsHooks = REFLECTIONS.getSubTypesOf(DataSerializerHook.class);
Map<Integer, DataSerializableFactory> factories = new HashMap<Integer, DataSerializableFactory>();
for (Class<? extends DataSerializerHook> hookClass : dsHooks) {
DataSerializerHook dsHook = hookClass.newInstance();
DataSerializableFactory factory = dsHook.createFactory();
factories.put(dsHook.getFactoryId(), factory);
}
Set<Class<? extends IdentifiedDataSerializable>> identifiedDataSerializables = getIDSConcreteClasses();
for (Class<? extends IdentifiedDataSerializable> klass : identifiedDataSerializables) {
if (AbstractLocalOperation.class.isAssignableFrom(klass)) {
continue;
}
// wrap all of this in try-catch, as it is legitimate for some classes to throw UnsupportedOperationException
try {
Constructor<? extends IdentifiedDataSerializable> ctor = klass.getDeclaredConstructor();
ctor.setAccessible(true);
IdentifiedDataSerializable instance = ctor.newInstance();
int factoryId = instance.getFactoryId();
int typeId = instance.getId();
if (!factories.containsKey(factoryId)) {
fail("Factory with ID " + factoryId + " declared in " + klass + " not found. Is such a factory ID "
+ "registered?");
}
IdentifiedDataSerializable instanceFromFactory = factories.get(factoryId).create(typeId);
assertNotNull("Factory with ID " + factoryId + " returned null for type with ID " + typeId, instanceFromFactory);
assertTrue("Factory with ID " + factoryId + " instantiated an object of " + instanceFromFactory.getClass() +
" while expected type was " + instance.getClass(),
instanceFromFactory.getClass().equals(instance.getClass()));
} catch (UnsupportedOperationException ignored) {
// expected from local operation classes not meant for serialization
}
}
}
/**
* Returns all concrete classes which implement {@link IdentifiedDataSerializable} located by
* {@link com.hazelcast.test.ReflectionsHelper#REFLECTIONS}.
*
* @return a set of all {@link IdentifiedDataSerializable} classes
*/
private Set<Class<? extends IdentifiedDataSerializable>> getIDSConcreteClasses() {
Set<Class<? extends IdentifiedDataSerializable>> identifiedDataSerializables
= REFLECTIONS.getSubTypesOf(IdentifiedDataSerializable.class);
filterNonConcreteClasses(identifiedDataSerializables);
return identifiedDataSerializables;
}
/**
* Removes abstract classes and interfaces from given Set in-place.
*/
private void filterNonConcreteClasses(Set classes) {
Iterator<Class> iterator = classes.iterator();
while (iterator.hasNext()) {
Class<?> klass = iterator.next();
if (klass.isInterface() || Modifier.isAbstract(klass.getModifiers())) {
iterator.remove();
}
}
}
/**
* @param klass
* @param inheritedClass
* @return {@code true} when klass has a superclass that implements or is itself of type {@code inheritedClass}
*/
private boolean inheritsClassFromPublicClass(Class klass, Class inheritedClass) {
// check interfaces implemented by klass: if one of these implements inheritedClass and is public, then true
Class[] interfaces = klass.getInterfaces();
if (interfaces != null) {
for (Class implementedInterface : interfaces) {
if (implementedInterface.equals(inheritedClass)) {
return false;
} else if (inheritedClass.isAssignableFrom(implementedInterface) && isPublicClass(implementedInterface)) {
return true;
}
}
}
// use hierarchyIteratingClass to iterate up the klass hierarchy
Class hierarchyIteratingClass = klass;
while (hierarchyIteratingClass.getSuperclass() != null) {
if (hierarchyIteratingClass.getSuperclass().equals(inheritedClass)) {
return true;
}
if (inheritedClass.isAssignableFrom(hierarchyIteratingClass.getSuperclass()) &&
isPublicClass(hierarchyIteratingClass.getSuperclass())) {
return true;
}
hierarchyIteratingClass = hierarchyIteratingClass.getSuperclass();
}
return false;
}
private boolean isPublicClass(Class klass) {
return !klass.getName().contains(".impl.") && !klass.getName().contains(".internal.")
&& klass.getAnnotation(PrivateApi.class) == null;
}
private static boolean inheritsFromWhiteListedClass(Class klass) {
for (Class superclass : SERIALIZABLE_WHITE_LIST) {
if (superclass.isAssignableFrom(klass)) {
return true;
}
}
return false;
}
}