/*
* Grapht, an open source dependency injector.
* Copyright 2014-2015 various contributors (see CONTRIBUTORS.txt)
* Copyright 2010-2014 Regents of the University of Minnesota
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.grouplens.grapht.util;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.lang.reflect.*;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* A serialization proxy for class instances. This serializable class encapsulates a simple
* representation for classes when serializing object graphs.
* <p>
* When using this class, classes are serialized as their binary name, as returned by
* {@link Class#getName()}. The name encodes array information, so this is adequate
* to fully reconstruct the class.
* </p>
*
* @author <a href="http://grouplens.org">GroupLens Research</a>
*/
@Immutable
public final class ClassProxy implements Serializable {
private static final long serialVersionUID = 1;
private static final Logger logger = LoggerFactory.getLogger(ClassProxy.class);
private final String className;
private final long checksum;
@Nullable
private transient volatile WeakReference<Class<?>> theClass;
private transient ClassLoader classLoader;
private ClassProxy(String name, long check) {
className = name;
checksum = check;
classLoader = ClassLoaders.inferDefault(ClassProxy.class);
}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
classLoader = ClassLoaders.inferDefault(ClassProxy.class);
}
/**
* Get the class name. This name does not include any array information.
* @return The class name.
*/
public String getClassName() {
return className;
}
@Override
public String toString() {
return "proxy of " + className;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ClassProxy) {
ClassProxy op = (ClassProxy) o;
return className.equals(op.className);
} else {
return false;
}
}
@Override
public int hashCode() {
return className.hashCode();
}
/**
* Resolve a class proxy to a class.
* @return The class represented by this proxy.
* @throws ClassNotFoundException if the class represented by this proxy cannot be found.
*/
public Class<?> resolve() throws ClassNotFoundException {
WeakReference<Class<?>> ref = theClass;
Class<?> cls = ref == null ? null : ref.get();
if (cls == null) {
if(className.equals("void")) {
// special case
cls = Void.TYPE;
} else {
cls = ClassUtils.getClass(classLoader, className);
}
long check = checksumClass(cls);
if (!isSerializationPermissive() && checksum != check) {
throw new ClassNotFoundException("checksum mismatch for " + cls.getName());
} else {
if (checksum != check) {
logger.warn("checksum mismatch for {}", cls);
}
theClass = new WeakReference<Class<?>>(cls);
}
}
return cls;
}
private static final Map<Class<?>, ClassProxy> proxyCache = new WeakHashMap<Class<?>, ClassProxy>();
/**
* Construct a class proxy for a class.
*
* @param cls The class.
* @return The class proxy.
*/
public static synchronized ClassProxy of(Class<?> cls) {
ClassProxy proxy = proxyCache.get(cls);
if (proxy == null) {
proxy = new ClassProxy(cls.getName(), checksumClass(cls));
proxy.theClass = new WeakReference<Class<?>>(cls);
proxyCache.put(cls, proxy);
}
return proxy;
}
private static final Charset UTF8 = Charset.forName("UTF-8");
public static boolean isSerializationPermissive() {
return Boolean.getBoolean("grapht.deserialization.permissive");
}
/**
* Compute a checksum for a class. These checksums are used to see if a class has changed
* its definition since being serialized.
* <p>
* The checksum used here is not cryptographically strong. It is intended only as a sanity
* check to detect incompatible serialization, not to robustly prevent tampering. The
* checksum algorithm currently is to compute an MD5 checksum over class member signatures
* and XOR the lower and upper halves of the checksum.
* </p>
*
* @param type The class to checksum.
* @return The
*/
private static long checksumClass(Class<?> type) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("JVM does not support MD5", e);
}
checksumClass(type, digest);
ByteBuffer buf = ByteBuffer.wrap(digest.digest());
long l1 = buf.getLong();
long l2 = buf.getLong();
return l1 ^ l2;
}
private static void checksumClass(Class<?> type, MessageDigest digest) {
// we compute a big hash of all the members of the class, and its superclasses.
List<String> members = new ArrayList<String>();
for (Constructor<?> c: type.getDeclaredConstructors()) {
if (isInjectionSensitive(c)) {
members.add(String.format("%s(%s)", c.getName(),
StringUtils.join(c.getParameterTypes(), ", ")));
}
}
for (Method m: type.getDeclaredMethods()) {
if (isInjectionSensitive(m)) {
members.add(String.format("%s(%s): %s", m.getName(),
StringUtils.join(m.getParameterTypes(), ", "),
m.getReturnType()));
}
}
for (Field f: type.getDeclaredFields()) {
if (isInjectionSensitive(f)) {
members.add(f.getName() + ":" + f.getType().getName());
}
}
Collections.sort(members);
Class<?> sup = type.getSuperclass();
if (sup != null) {
checksumClass(sup, digest);
}
for (String mem: members) {
digest.update(mem.getBytes(UTF8));
}
}
/**
* Check whether a member is injection-sensitive and should be checked for validity in
* deserialization.
*
* @param m The member.
* @param <M> The type of member (done so we can check multiple types).
* @return {@code true} if the member should be checksummed, {@code false} to ignore it.
*/
private static <M extends Member & AnnotatedElement>boolean isInjectionSensitive(M m) {
// static methods are not sensitive
if (Modifier.isStatic(m.getModifiers())) {
return false;
}
// private members w/o @Inject are not sensitive
if (Modifier.isPrivate(m.getModifiers()) && m.getAnnotation(Inject.class) == null) {
return false;
}
// public, protected, or @Inject - it's sensitive (be conservative)
return true;
}
}