/*
* Copyright 2007-2010 Sun Microsystems, Inc.
*
* This file is part of Project Darkstar Server.
*
* Project Darkstar Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation and
* distributed hereunder to you.
*
* Project Darkstar Server 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, see <http://www.gnu.org/licenses/>.
*
* --
*/
package com.sun.sgs.impl.service.data;
import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.ObjectIOException;
import com.sun.sgs.impl.util.Int30;
import com.sun.sgs.service.Transaction;
import com.sun.sgs.service.store.ClassInfoNotFoundException;
import com.sun.sgs.service.store.DataStore;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Manages information about class descriptors used to serialize managed
* objects. This class caches information about class descriptors and class
* IDs that it obtains from the data store. The cache is filled on demand, and
* uses soft and weak references, so it can be cleared by the GC as needed.
*/
final class ClassesTable {
/*
* TBD: Maybe use futures to avoid simultaneous requests for the same
* class? -tjb@sun.com (05/21/2007)
*/
/** The data store used to store class information. */
private final DataStore store;
/*
* Note that using concurrent maps turned out to be too inefficient to be
* useful. Because the maps are likely to quickly include all of the
* entries needed for steady state operation, supporting fast concurrent
* reads via a shared read lock seems like a better approach than
* permitting concurrent modifications. -tjb@sun.com (05/18/2007)
*/
/**
* Maps class IDs to class descriptors and associated information. Use
* soft references to the descriptors since they are still useful if
* unreferenced, but can be reconstructed if needed.
*/
private final Map<Integer, ClassDescInfo> classDescMap =
new HashMap<Integer, ClassDescInfo>();
/** Reference queue for cleared ObjectStreamClass soft references. */
private final ReferenceQueue<ObjectStreamClass> refQueue =
new ReferenceQueue<ObjectStreamClass>();
/**
* Maps class descriptors to class descriptors and associated information,
* including class IDs. Use weak references to the descriptors since they
* are compared by identity, and so are not useful if no longer referenced.
*/
private final Map<ObjectStreamClass, ClassDescInfo> classIdMap =
new WeakHashMap<ObjectStreamClass, ClassDescInfo>();
/** Lock this lock when accessing classDescMap and classIdMap. */
private final ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* A soft reference holder for class descriptor objects, and associated
* information, stored in classDescMap.
*/
private static class ClassDescInfo
extends SoftReference<ObjectStreamClass>
{
/** The associated class ID key. */
final int classId;
/**
* The name of the class, so that we can identify the class even if the
* reference to the associated class descriptor has been cleared.
*/
private final String className;
/**
* Whether the class is a ManagedObject class with a writeReplace
* method.
*/
private final boolean hasWriteReplace;
/**
* Whether the class is a ManagedObject class with a readResolve
* method.
*/
private final boolean hasReadResolve;
/**
* The name of the first non-serializable superclass if it lacks an
* accessible no-argument constructor, or {@code null} if there is no
* such class. Note that it is illegal to deserialize an instance of a
* class with such a superclass, but serialization does not enforce
* that restriction.
*/
private final String missingConstructorSuperclass;
/** Creates an instance of this class. */
ClassDescInfo(int classId,
ObjectStreamClass classDesc,
ReferenceQueue<ObjectStreamClass> queue)
{
super(classDesc, queue);
this.classId = classId;
Class<?> cl = classDesc.forClass();
className = cl.getName();
boolean isManagedObject = ManagedObject.class.isAssignableFrom(cl);
hasWriteReplace =
isManagedObject && hasSerializationMethod(cl, "writeReplace");
hasReadResolve =
isManagedObject && hasSerializationMethod(cl, "readResolve");
missingConstructorSuperclass =
computeMissingConstructorSuperclass(cl);
}
/**
* Removes entries from the map that have been queued after being
* garbage collected.
*/
static void processQueue(ReferenceQueue<ObjectStreamClass> queue,
Map<Integer, ?> map)
{
ClassDescInfo ref;
/*
* Reference queues don't provide a way to specify that the queue
* contains a particular subclass of reference, so the unchecked
* assignment can't be avoided. -tjb@sun.com (05/18/2007)
*/
while ((ref = (ClassDescInfo) (Object) queue.poll()) != null) {
map.remove(ref.classId);
}
}
/** Checks if the class can be instantiated. */
void checkInstantiable() throws IOException {
if (hasWriteReplace) {
throw new IOException(
"Managed objects must not define a Serialization " +
"writeReplace method: " + className);
} else if (hasReadResolve) {
throw new IOException(
"Managed objects must not define a Serialization " +
"readResolve method: " + className);
} else if (missingConstructorSuperclass != null) {
throw new IOException(
"Class " + className + " has a superclass without an" +
" accessible no-argument constructor: " +
missingConstructorSuperclass);
}
}
}
/** Creates an instance with the specified data store. */
ClassesTable(DataStore store) {
this.store = store;
}
/**
* Returns an implementation of ClassSerialization that uses this table to
* lookup class descriptors, uses the specified transaction when making
* requests of the data store, and uses Int30 to represent class IDs.
*/
ClassSerialization createClassSerialization(final Transaction txn) {
return new ClassSerialization() {
public void writeClassDescriptor(ObjectStreamClass classDesc,
ObjectOutputStream out)
throws IOException
{
Int30.write(getClassId(txn, classDesc), out);
}
public void checkInstantiable(ObjectStreamClass classDesc)
throws IOException
{
getClassDescInfo(txn, classDesc).checkInstantiable();
}
public ObjectStreamClass readClassDescriptor(ObjectInputStream in)
throws ClassNotFoundException, IOException
{
return getClassDesc(txn, Int30.read(in));
}
};
}
/**
* Returns the class ID associated with a class descriptor.
*
* @param txn the transaction under which the operation should take place
* @param classDesc the class descriptor
* @return the class ID
* @throws ObjectIOException if a problem occurs serializing the class
* descriptor
* @throws TransactionAbortedException if the data store was consulted and
* the transaction was aborted due to a lock conflict or timeout
* @throws TransactionNotActiveException if the data store was consulted
* and the transaction is not active
* @throws IllegalStateException if the data store was consulted and the
* operation failed because of a problem with the current
* transaction
*/
int getClassId(Transaction txn, ObjectStreamClass classDesc) {
return getClassDescInfo(txn, classDesc).classId;
}
/**
* Returns the information associated with a class descriptor.
* @param txn the transaction under which the operation should take place
* @param classDesc the class descriptor
* @return the information about the class descriptor
* @throws ObjectIOException if a problem occurs serializing the class
* descriptor
* @throws TransactionAbortedException if the data store was consulted and
* the transaction was aborted due to a lock conflict or timeout
* @throws TransactionNotActiveException if the data store was consulted
* and the transaction is not active
* @throws IllegalStateException if the data store was consulted and the
* operation failed because of a problem with the current
* transaction
*/
private ClassDescInfo getClassDescInfo(
Transaction txn, ObjectStreamClass classDesc)
{
lock.readLock().lock();
try {
ClassDescInfo info = classIdMap.get(classDesc);
if (info != null) {
return info;
}
} finally {
lock.readLock().unlock();
}
int classId = store.getClassId(txn, getClassInfo(classDesc));
if (classId > Int30.MAX_VALUE) {
throw new IllegalStateException(
"Allocating more than " + Int30.MAX_VALUE +
" classes is not supported");
}
return updateMaps(classId, classDesc).classDescInfo;
}
/**
* Updates the maps to refer to the specified class ID and class
* descriptor. Returns the descriptor, and the associated information,
* that ends up mapped to the class ID. Note that the descriptor returned
* may be different from the one passed in if another one was obtained
* concurrently.
*/
private UpdateMapsResult updateMaps(Integer classId,
ObjectStreamClass classDesc)
{
lock.writeLock().lock();
try {
ClassDescInfo.processQueue(refQueue, classDescMap);
ClassDescInfo info = classDescMap.get(classId);
ObjectStreamClass existing = (info != null) ? info.get() : null;
if (existing == null) {
info = new ClassDescInfo(classId, classDesc, refQueue);
classDescMap.put(classId, info);
}
if (!classIdMap.containsKey(classDesc)) {
classIdMap.put(classDesc, info);
}
return new UpdateMapsResult(
(existing != null) ? existing : classDesc, info);
} finally {
lock.writeLock().unlock();
}
}
/**
* Stores the ObjectStreamClass and ClassDescInfo returned by a call to
* updateMaps. The return value needs to contain both to insure that it
* maintains a hard reference to the ObjectStreamClass.
*/
private static class UpdateMapsResult {
final ObjectStreamClass classDesc;
final ClassDescInfo classDescInfo;
UpdateMapsResult(ObjectStreamClass classDesc,
ClassDescInfo classDescInfo)
{
this.classDesc = classDesc;
this.classDescInfo = classDescInfo;
}
}
/**
* Returns the class descriptor associated with a class ID.
*
* @param txn the transaction under which the operation should take place
* @param classId the class ID
* @return the class descriptor
* @throws ObjectIOException if a problem occurs deserializing the class
* descriptor, including if the class ID is not found
* @throws TransactionAbortedException if the data store was consulted and
* the transaction was aborted due to a lock conflict or timeout
* @throws TransactionNotActiveException if the data store was consulted
* and the transaction is not active
* @throws IllegalStateException if the data store was consulted and the
* operation failed because of a problem with the current
* transaction
*/
private ObjectStreamClass getClassDesc(Transaction txn, int classId) {
lock.readLock().lock();
try {
SoftReference<ObjectStreamClass> ref = classDescMap.get(classId);
if (ref != null) {
ObjectStreamClass classDesc = ref.get();
if (classDesc != null) {
return classDesc;
}
}
} finally {
lock.readLock().unlock();
}
try {
UpdateMapsResult result = updateMaps(
classId, getClassDesc(store.getClassInfo(txn, classId)));
return result.classDesc;
} catch (ClassInfoNotFoundException e) {
throw new ObjectIOException(
"Problem deserializing class descriptor: " + e.getMessage(),
e, false);
}
}
/**
* Converts a class descriptor into its serialized form.
*
* @param classDesc the class descriptor
* @return the class information
* @throws ObjectIOException if a problem occurs serializing the class
* descriptor
*/
private static byte[] getClassInfo(ObjectStreamClass classDesc) {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream objectOut = null;
try {
objectOut = new ObjectOutputStream(byteOut);
objectOut.writeObject(classDesc);
objectOut.flush();
return byteOut.toByteArray();
} catch (IOException e) {
throw new ObjectIOException(
"Problem serializing class descriptor: " + e.getMessage(),
e, false);
} finally {
if (objectOut != null) {
try {
objectOut.close();
} catch (IOException e) {
}
}
}
}
/**
* Converts a class descriptor's serialized form into the descriptor.
*
* @param classInfo the class information
* @return the class descriptor
* @throws ObjectIOException if a problem occurs deserializing the class
* descriptor
*/
private static ObjectStreamClass getClassDesc(byte[] classInfo) {
ObjectInputStream in = null;
Exception exception;
try {
in = new ObjectInputStream(new ByteArrayInputStream(classInfo));
ObjectStreamClass classDesc = (ObjectStreamClass) in.readObject();
return classDesc;
} catch (ClassNotFoundException e) {
exception = e;
} catch (IOException e) {
exception = e;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
}
}
}
throw new ObjectIOException(
"Problem obtaining class descriptor: " + exception.getMessage(),
exception, false);
}
/**
* Returns whether the class defines an inherited method used by
* Serialization.
*/
private static boolean hasSerializationMethod(
Class<?> forClass, String methodName)
{
Method method = null;
Class<?> cl = forClass;
while (cl != null) {
try {
method = cl.getDeclaredMethod(methodName);
break;
} catch (NoSuchMethodException e) {
cl = cl.getSuperclass();
}
}
if (method == null || method.getReturnType() != Object.class) {
return false;
}
int mods = method.getModifiers();
if (Modifier.isStatic(mods) || Modifier.isAbstract(mods)) {
return false;
} else if (Modifier.isPublic(mods) || Modifier.isProtected(mods)) {
return true;
} else if (Modifier.isPrivate(mods)) {
return forClass == cl;
} else {
return samePackage(forClass, cl);
}
}
/** Checks if the two classes are in the same package. */
private static boolean samePackage(Class<?> c1, Class<?> c2) {
return c1.getClassLoader() == c2.getClassLoader() &&
getPackageName(c1).equals(getPackageName(c2));
}
/** Returns the package name of the class. */
private static String getPackageName(Class<?> cl) {
String name = cl.getName();
int pos = name.lastIndexOf('[');
if (pos >= 0) {
name = name.substring(pos + 2);
}
pos = name.lastIndexOf('.');
return (pos < 0) ? "" : name.substring(0, pos);
}
/**
* Returns the name of the first non-serializable superclass if it lacks an
* accessible no-arguments constructor, else null.
*/
private static String computeMissingConstructorSuperclass(Class<?> cl) {
assert cl != null;
Class<?> instantiatedClass = cl;
while (Serializable.class.isAssignableFrom(cl)) {
cl = cl.getSuperclass();
if (cl == null) {
return null;
}
}
try {
Constructor constructor = cl.getDeclaredConstructor();
int modifiers = constructor.getModifiers();
if (Modifier.isPublic(modifiers) ||
Modifier.isProtected(modifiers) ||
(!Modifier.isPrivate(modifiers) &&
samePackage(cl, instantiatedClass)))
{
return null;
} else {
return cl.getName();
}
} catch (NoSuchMethodException e) {
return cl.getName();
}
}
}