/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.sun.jini.outrigger;
import com.sun.jini.landlord.LeasedResource;
import com.sun.jini.logging.Levels;
import com.sun.jini.proxy.MarshalledWrapper;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.InvalidObjectException;
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.Modifier;
import java.net.MalformedURLException;
import java.rmi.MarshalException;
import java.rmi.UnmarshalException;
import java.rmi.server.RMIClassLoader;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Comparator;
import java.util.WeakHashMap;
import java.util.logging.Logger;
import net.jini.core.entry.Entry;
import net.jini.core.entry.UnusableEntryException;
import net.jini.id.Uuid;
import net.jini.id.UuidFactory;
import net.jini.io.MarshalledInstance;
import net.jini.loader.ClassLoading;
/**
* An <code>EntryRep</code> object contains a packaged
* <code>Entry</code> object for communication between the client and a
* <code>JavaSpace</code>.
*
* @author Sun Microsystems, Inc.
*
* @see JavaSpace
* @see Entry
*/
class EntryRep implements StorableResource, LeasedResource, Serializable {
static final long serialVersionUID = 3L;
/**
* The fields of the entry in marshalled form. Use <code>null</code>
* for <code>null</code> fields.
*/
private MarshalledInstance[] values;
private String[] superclasses; // class names of the superclasses
private long[] hashes; // superclass hashes
private long hash; // hash for the entry class
private String className; // the class ID of the entry
private String codebase; // the codebase for this entry class
private Uuid id; // space-relative storage id
private transient long expires;// expiration time
/**
* <code>true</code> if the last time this object was unmarshalled
* integrity was being enforced, <code>false</code> otherwise.
*/
private transient boolean integrity;
/** Comparator for sorting fields */
private static final FieldComparator comparator = new FieldComparator();
/**
* This object represents the passing of a <code>null</code>
* parameter as a template, which is designed to match any entry.
* When a <code>null</code> is passed, it is replaced with this
* rep, which is then handled specially in a few relevant places.
*/
private static final EntryRep matchAnyRep;
static {
try {
matchAnyRep = new EntryRep(new Entry() {
// keeps tests happy
static final long serialVersionUID = -4244768995726274609L;
}, false);
} catch (MarshalException e) {
throw new AssertionError(e);
}
}
/**
* The realClass object is transient because we neither need nor want
* it reconstituted on the other side. All we want is to be able to
* recreate it on the receiving client side. If it were not transient,
* not only would an unnecessary object creation occur, but it might
* force the download of the actual class to the server.
*/
private transient Class realClass; // real class of the contained object
/**
* Logger for logging information about operations carried out in
* the client. Note, we hard code "com.sun.jini.outrigger" so
* we don't drag in OutriggerServerImpl to outrigger-dl.jar.
*/
private static final Logger logger =
Logger.getLogger("com.sun.jini.outrigger.proxy");
/**
* Set this entry's generic data to be shared with the <code>other</code>
* object. Those fields that are object references that will be the same
* for all objects of the same type are shared this way.
* <p>
* Note that <code>codebase</code> is <em>not</em> shared. If it were,
* then the failure of one codebase could make all entries inaccessible.
* Each entry is usable insofar as the codebase under which it was
* written is usable.
*/
void shareWith(EntryRep other) {
className = other.className;
superclasses = other.superclasses;
hashes = other.hashes;
hash = other.hash;
}
/**
* Get the entry fields associated with the passed class and put
* them in a canonical order. The fields are sorted so that fields
* belonging to a superclasses are before fields belonging to
* subclasses and within a class fields are ordered
* lexicographically by their name.
*/
static private Field[] getFields(Class cl) {
final Field[] fields = cl.getFields();
Arrays.sort(fields, comparator);
return fields;
}
/**
* Cached hash values for all classes we encounter. Weak hash used
* in case the class is GC'ed from the client's VM.
*/
static private WeakHashMap classHashes;
/**
* Lookup the hash value for the given class. If it is not
* found in the cache, generate the hash for the class and
* save it.
*/
static synchronized private Long findHash(Class clazz,
boolean marshaling)
throws MarshalException, UnusableEntryException
{
if (classHashes == null)
classHashes = new WeakHashMap();
Long hash = (Long)classHashes.get(clazz);
// If hash not cached, calculate it for this class and,
// recursively, all superclasses
//
if (hash == null) {
try {
Field[] fields = getFields(clazz);
MessageDigest md = MessageDigest.getInstance("SHA");
DataOutputStream out =
new DataOutputStream(
new DigestOutputStream(new ByteArrayOutputStream(127),
md));
Class c = clazz.getSuperclass();
if (c != Object.class)
// recursive call
out.writeLong(findHash(c, marshaling).longValue());
// Hash only usable fields, this means that we do not
// detect changes in non-usable fields. This should be ok
// since those fields do not move between space and client.
//
for (int i = 0; i < fields.length; i++) {
if (!usableField(fields[i]))
continue;
out.writeUTF(fields[i].getName());
out.writeUTF(fields[i].getType().getName());
}
out.flush();
byte[] digest = md.digest();
long h = 0;
for (int i = Math.min(8, digest.length); --i >= 0; ) {
h += ((long)(digest[i] & 0xFF)) << (i * 8);
}
hash = new Long(h);
} catch (Exception e) {
if (marshaling)
throw throwNewMarshalException(
"Exception calculating entry class hash for " +
clazz, e);
else
throw throwNewUnusableEntryException(
"Exception calculating entry class hash for " +
clazz, e);
}
classHashes.put(clazz, hash);
}
return hash;
}
/**
* Create a serialized form of the entry. If <code>validate</code> is
* <code>true</code>, basic sanity checks are done on the class to
* ensure that it meets the requirements to be an <code>Entry</code>.
* <code>validate</code> is <code>false</code> only when creating the
* stand-in object for "match any", which is never actually marshalled
* on the wire and so which doesn't need to be "proper".
*/
private EntryRep(Entry entry, boolean validate) throws MarshalException {
realClass = entry.getClass();
if (validate)
ensureValidClass(realClass);
className = realClass.getName();
codebase = RMIClassLoader.getClassAnnotation(realClass);
/*
* Build up the per-field and superclass information through
* the reflection API.
*/
final Field[] fields = getFields(realClass);
int numFields = fields.length;
// collect the usable field values in vals[0..nvals-1]
MarshalledInstance[] vals = new MarshalledInstance[numFields];
int nvals = 0;
for (int fnum = 0; fnum < fields.length; fnum++) {
final Field field = fields[fnum];
if (!usableField(field))
continue;
final Object fieldValue;
try {
fieldValue = field.get(entry);
} catch (IllegalAccessException e) {
/* In general between using getFields() and
* ensureValidClass this should never happen, however
* there appear to be a few screw cases and
* IllegalArgumentException seems appropriate.
*/
final IllegalArgumentException iae =
new IllegalArgumentException("Couldn't access field " +
field);
iae.initCause(e);
throw throwRuntime(iae);
}
if (fieldValue == null) {
vals[nvals] = null;
} else {
try {
vals[nvals] = new MarshalledInstance(fieldValue);
} catch (IOException e) {
throw throwNewMarshalException(
"Can't marshal field " + field + " with value " +
fieldValue, e);
}
}
nvals++;
}
// copy the vals with the correct length
this.values = new MarshalledInstance[nvals];
System.arraycopy(vals, 0, this.values, 0, nvals);
try {
hash = findHash(realClass, true).longValue();
} catch (UnusableEntryException e) {
// Will never happen when we pass true to findHash
throw new AssertionError(e);
}
// Loop through the supertypes, making a list of all superclasses.
ArrayList sclasses = new ArrayList();
ArrayList shashes = new ArrayList();
for (Class c = realClass.getSuperclass();
c != Object.class;
c = c.getSuperclass())
{
try {
sclasses.add(c.getName());
shashes.add(findHash(c, true));
} catch (ClassCastException cce) {
break; // not Serializable
} catch (UnusableEntryException e) {
// Will never happen when we pass true to findHash
throw new AssertionError(e);
}
}
superclasses = (String[])sclasses.toArray(new String[sclasses.size()]);
hashes = new long[shashes.size()];
for (int i=0; i < hashes.length; i++) {
hashes[i] = ((Long)shashes.get(i)).longValue();
}
}
/**
* Create a serialized form of the entry with our object's
* relevant fields set.
*/
public EntryRep(Entry entry) throws MarshalException {
this(entry, true);
}
/** Used in recovery */
EntryRep() { }
/** Used to look up no-arg constructors. */
private static Class[] noArg = new Class[0];
/**
* Ensure that the entry class is valid, that is, that it has appropriate
* access. If not, throw <code>IllegalArgumentException</code>.
*/
private static void ensureValidClass(Class c) {
boolean ctorOK = false;
try {
if (!Modifier.isPublic(c.getModifiers())) {
throw throwRuntime(new IllegalArgumentException(
"entry class " + c.getName() + " not public"));
}
Constructor ctor = c.getConstructor(noArg);
ctorOK = Modifier.isPublic(ctor.getModifiers());
} catch (NoSuchMethodException e) {
ctorOK = false;
} catch (SecurityException e) {
ctorOK = false;
}
if (!ctorOK) {
throw throwRuntime(new IllegalArgumentException("entry class " +
c.getName() +" needs public no-arg constructor"));
}
}
/**
* The <code>EntryRep</code> that marks a ``match any'' request.
* This is used to represent a <code>null</code> template.
*/
static EntryRep matchAnyEntryRep() {
return matchAnyRep;
}
/**
* Return <code>true</code> if the given rep is that ``match any''
* <code>EntryRep</code>.
*/
private static boolean isMatchAny(EntryRep rep) {
return matchAnyRep.equals(rep);
}
/**
* Return the class name that is used by the ``match any'' EntryRep
*/
static String matchAnyClassName() {
return matchAnyRep.classFor();
}
/**
* Return an <code>Entry</code> object built out of this
* <code>EntryRep</code> This is used by the client-side proxy to
* convert the <code>EntryRep</code> it gets from the space server
* into the actual <code>Entry</code> object it represents.
*
* @throws UnusableEntryException
* One or more fields in the entry cannot be
* deserialized, or the class for the entry type
* itself cannot be deserialized.
*/
Entry entry() throws UnusableEntryException {
ObjectInputStream objIn = null;
try {
ArrayList badFields = null;
ArrayList except = null;
realClass = ClassLoading.loadClass(codebase, className,
null, integrity, null);
if (findHash(realClass, false).longValue() != hash)
throw throwNewUnusableEntryException(
new IncompatibleClassChangeError(realClass + " changed"));
Entry entryObj = (Entry) realClass.newInstance();
Field[] fields = getFields(realClass);
/*
* Loop through the fields, ensuring no primitives and
* checking for wildcards.
*/
int nvals = 0; // index into this.values[]
for (int i = 0; i < fields.length; i++) {
Throwable nested = null;
try {
if (!usableField(fields[i]))
continue;
final MarshalledInstance val = values[nvals++];
Object value = (val == null ? null : val.get(integrity));
fields[i].set(entryObj, value);
} catch (Throwable e) {
nested = e;
}
if (nested != null) { // some problem occurred
if (badFields == null) {
badFields = new ArrayList(fields.length);
except = new ArrayList(fields.length);
}
badFields.add(fields[i].getName());
except.add(nested);
}
}
/* See if any fields have vanished from the class,
* because of the hashing this should never happen but
* throwing an exception that provides more info
* (instead of AssertionError) seems harmless.
*/
if (nvals < values.length) {
throw throwNewUnusableEntryException(
entryObj, // should this be null?
null, // array of bad-field names
new Throwable[] { // array of exceptions
new IncompatibleClassChangeError(
"A usable field has been removed from " +
entryObj.getClass().getName() +
" since this EntryRep was created")
});
}
// if there were any bad fields, throw the exception
if (badFields != null) {
String[] bf =
(String[]) badFields.toArray(
new String[badFields.size()]);
Throwable[] ex =
(Throwable[]) except.toArray(new Throwable[bf.length]);
throw throwNewUnusableEntryException(entryObj, bf, ex);
}
// everything fine, return the entry
return entryObj;
} catch (InstantiationException e) {
/*
* If this happens outside a per-field deserialization then
* this is a complete failure The per-field ones are caught
* inside the per-field loop.
*/
throw throwNewUnusableEntryException(e);
} catch (ClassNotFoundException e) {
// see above
throw throwNewUnusableEntryException("Encountered a " +
"ClassNotFoundException while unmarshalling " + className, e);
} catch (IllegalAccessException e) {
// see above
throw throwNewUnusableEntryException(e);
} catch (RuntimeException e) {
// see above
throw throwNewUnusableEntryException("Encountered a " +
"RuntimeException while unmarshalling " + className, e);
} catch (MalformedURLException e) {
// see above
throw throwNewUnusableEntryException("Malformed URL " +
"associated with entry of type " + className, e);
} catch (MarshalException e) {
// because we call findHash() w/ false, should never happen
throw new AssertionError(e);
}
}
// inherit doc comment
public int hashCode() {
return className.hashCode();
}
/**
* To be equal, the other object must by an <code>EntryRep</code> for
* an object of the same class with the same values for each field.
* This is <em>not</em> a template match -- see <code>matches</code>.
*
* @see matches
*/
public boolean equals(Object o) {
// The other passed in was null--obviously not equal
if (o == null)
return false;
// The other passed in was ME--obviously I'm the same as me...
if (this == o)
return true;
if (!(o instanceof EntryRep))
return false;
EntryRep other = (EntryRep) o;
// If we're not the same class then we can't be equal
if (hash != other.hash)
return false;
/* Paranoid check just to make sure we can't get an
* IndexOutOfBoundsException. Should never happen.
*/
if (values.length != other.values.length)
return false;
/* OPTIMIZATION:
* If we have a case where one element is null and the corresponding
* element within the object we're comparing ourselves with is
* non-null (or vice-versa), we can stop right here and declare the
* two objects to be unequal. This is slightly faster than checking
* the bytes themselves.
* LOGIC: They've both got to be null or both have got to be
* non-null or we're out-of-here...
*/
for (int i = 0; i < values.length; i++) {
if ((values[i] == null) && (other.values[i] != null))
return false;
if ((values[i] != null) && (other.values[i] == null))
return false;
}
/* The most expensive tests we save for last.
* Because we've made the null/non-null check above, we can
* simplify our comparison here: if our element is non-null,
* we know the other value is non-null, too.
* If any equals() calls from these element comparisons come
* back false then return false. If they all succeed, we fall
* through and return true (they were equal).
*/
for (int i = 0; i < values.length; i++) {
// Short-circuit evaluation if null, compare otherwise.
if (values[i] != null && !values[i].equals(other.values[i]))
return false;
}
return true;
}
/**
* Return <code>true</code> if the field is to be used for the
* entry. That is, return <code>true</code> if the field isn't
* <code>transient</code>, <code>static</code>, or <code>final</code>.
* @throws IllegalArgumentException
* The field is not <code>transient</code>,
* <code>static</code>, or <code>final</code>, but
* is primitive and hence not a proper field for
* an <code>Entry</code>.
*/
static private boolean usableField(Field field) {
// ignore anything that isn't a public non-static mutable field
final int ignoreMods =
(Modifier.TRANSIENT | Modifier.STATIC | Modifier.FINAL);
if ((field.getModifiers() & ignoreMods) != 0)
return false;
// if it isn't ignorable, it has to be an object of some kind
if (field.getType().isPrimitive()) {
throw throwRuntime(new IllegalArgumentException(
"primitive field, " + field + ", not allowed in an Entry"));
}
return true;
}
/**
* Return the ID.
*/
Uuid id() {
return id;
}
/**
* Pick a random <code>Uuid</code> and set our id field to it.
* @throws IllegalStateException if this method has already
* been called.
*/
void pickID() {
if (id != null)
throw new IllegalStateException("pickID called more than once");
id = UuidFactory.generate();
}
/**
* Return the <code>MarshalledObject</code> for the given field.
*/
public MarshalledInstance value(int fieldNum) {
return values[fieldNum];
}
/**
* Return the number of fields in this kind of entry.
*/
public int numFields() {
if (values != null) {
return values.length;
} else {
return 0;
}
}
/**
* Return the class name for this entry.
*/
public String classFor() {
return className;
}
/**
* Return the array names of superclasses of this entry type.
*/
public String[] superclasses() {
return superclasses;
}
/**
* Return the hash of this entry type.
*/
long getHash() {
return hash;
}
/**
* Return the array of superclass hashes of this entry type.
*/
long[] getHashes() {
return hashes;
}
/**
* See if the other object matches the template object this
* represents. (Note that even though "this" is a template, it may
* have no wildcards -- a template can have all values.)
*/
boolean matches(EntryRep other) {
/*
* We use the fact that this is the template in several ways in
* the method implementation. For instance, in this next loop,
* we know that the real object must be at least my type, which
* means (a) the field types already match, and (b) it has at
* least as many fields as the this does.
*/
//Note: If this object is the MatchAny template then
// return true (all entries match MatchAny)
if (EntryRep.isMatchAny(this))
return true;
for (int f = 0; f < values.length; f++) {
if (values[f] == null) { // skip wildcards
continue;
}
if (!values[f].equals(other.values[f])) {
return false;
}
}
return true; // no mismatches, so must be OK
}
public String toString() {
return ("EntryRep[" + className + "]");
}
/**
* Return <code>true</code> if this entry represents an object that
* is at least the type of the <code>otherClass</code>.
*/
boolean isAtLeastA(String otherClass) {
if (otherClass.equals(matchAnyClassName()))
// The other is a null template, all entries are at least entry.
return true;
if (className.equals(otherClass))
return true;
for (int i = 0; i < superclasses.length; i++)
if (superclasses[i].equals(otherClass))
return true;
return false;
}
/** Comparator for sorting fields. Cribbed from Reggie */
private static class FieldComparator implements Comparator {
public FieldComparator() {}
/** Super before subclass, alphabetical within a given class */
public int compare(Object o1, Object o2) {
Field f1 = (Field)o1;
Field f2 = (Field)o2;
if (f1 == f2)
return 0;
if (f1.getDeclaringClass() == f2.getDeclaringClass())
return f1.getName().compareTo(f2.getName());
if (f1.getDeclaringClass().isAssignableFrom(
f2.getDeclaringClass()))
return -1;
return 1;
}
}
/**
* Use <code>readObject</code> method to capture whether or
* not integrity was being enforced when this object was
* unmarshalled, and to perform basic integrity checks.
*/
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject();
if (className == null)
throw new InvalidObjectException("null className");
if (values == null)
throw new InvalidObjectException("null values");
if (superclasses == null)
throw new InvalidObjectException("null superclasses");
if (hashes == null)
throw new InvalidObjectException("null hashes");
if (hashes.length != superclasses.length)
throw new InvalidObjectException("hashes.length (" +
hashes.length + ") does not equal superclasses.length (" +
superclasses.length + ")");
// get value for integrity flag
integrity = MarshalledWrapper.integrityEnforced(in);
}
/**
* We should always have data in the stream, if this method
* gets called there is something wrong.
*/
private void readObjectNoData() throws InvalidObjectException {
throw new
InvalidObjectException("SpaceProxy should always have data");
}
// -------------------------------------------------------
// Methods required by LeasedResource and StorableResource
// -------------------------------------------------------
// inherit doc comment from LeasedResource
public void setExpiration(long newExpiration) {
expires = newExpiration;
}
// inherit doc comment from LeasedResource
public long getExpiration() {
return expires;
}
// inherit doc comment from LeasedResource
// We use the Rep ID as the cookie
public Uuid getCookie() {
return id;
}
// -------------------------------------
// Methods required by StorableResource
// -------------------------------------
// inherit doc comment
public void store(ObjectOutputStream out) throws IOException {
final long bits0;
final long bits1;
if (id == null) {
bits0 = 0;
bits1 = 0;
} else {
bits0 = id.getMostSignificantBits();
bits1 = id.getLeastSignificantBits();
}
out.writeLong(bits0);
out.writeLong(bits1);
out.writeLong(expires);
out.writeObject(codebase);
out.writeObject(className);
out.writeObject(superclasses);
out.writeObject(values);
out.writeLong(hash);
out.writeObject(hashes);
}
// inherit doc comment
public void restore(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
final long bits0 = in.readLong();
final long bits1 = in.readLong();
if (bits0 == 0 && bits1 == 0) {
id = null;
} else {
id = UuidFactory.create(bits0, bits1);
}
expires = in.readLong();
codebase = (String)in.readObject();
className = (String)in.readObject();
superclasses = (String [])in.readObject();
values = (MarshalledInstance [])in.readObject();
hash = in.readLong();
hashes = (long[])in.readObject();
}
// Utility methods for throwing and logging exceptions
/** Log and throw a runtime exception */
private static RuntimeException throwRuntime(RuntimeException e) {
if (logger.isLoggable(Levels.FAILED)) {
logger.log(Levels.FAILED, e.getMessage(), e);
}
throw e;
}
/** Construct, log, and throw a new MarshalException */
private static MarshalException throwNewMarshalException(
String msg, Exception nested)
throws MarshalException
{
final MarshalException me = new MarshalException(msg, nested);
if (logger.isLoggable(Levels.FAILED)) {
logger.log(Levels.FAILED, msg, me);
}
throw me;
}
/**
* Construct, log, and throw a new UnusableEntryException
*/
private UnusableEntryException throwNewUnusableEntryException(
Entry partial, String[] badFields, Throwable[] exceptions)
throws UnusableEntryException
{
final UnusableEntryException uee =
new UnusableEntryException(partial, badFields, exceptions);
if (logger.isLoggable(Levels.FAILED)) {
logger.log(Levels.FAILED,
"failure constructing entry of type " + className, uee);
}
throw uee;
}
/**
* Construct, log, and throw a new UnusableEntryException, that
* raps a given exception.
*/
private static UnusableEntryException throwNewUnusableEntryException(
Throwable nested)
throws UnusableEntryException
{
final UnusableEntryException uee = new UnusableEntryException(nested);
if (logger.isLoggable(Levels.FAILED)) {
logger.log(Levels.FAILED, nested.getMessage(), uee);
}
throw uee;
}
/**
* Construct, log, and throw a new UnusableEntryException, that
* will rap a newly constructed UnmarshalException (that optional
* wraps a given exception).
*/
private static UnusableEntryException throwNewUnusableEntryException(
String msg, Exception nested)
throws UnusableEntryException
{
final UnmarshalException ue = new UnmarshalException(msg, nested);
final UnusableEntryException uee = new UnusableEntryException(ue);
if (logger.isLoggable(Levels.FAILED)) {
logger.log(Levels.FAILED, msg, uee);
}
throw uee;
}
}