/*
* 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 java.io.ObjectInputStream;
import java.io.Serializable;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.rmi.MarshalException;
import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jini.admin.Administrable;
import net.jini.core.entry.Entry;
import net.jini.core.entry.UnusableEntryException;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.lease.Lease;
import net.jini.core.transaction.Transaction;
import net.jini.core.transaction.TransactionException;
import net.jini.entry.UnusableEntriesException;
import net.jini.space.JavaSpace05;
import net.jini.space.MatchSet;
import net.jini.id.Uuid;
import net.jini.id.UuidFactory;
import net.jini.id.ReferentUuid;
import net.jini.id.ReferentUuids;
import net.jini.security.Security;
import com.sun.jini.landlord.LandlordLease;
/**
* This class is the client-side proxy for the Outrigger
* implementation of a JavaSpaces<sup><font size=-2>TM</font></sup>
* service. <code>OutriggerServerImpl</code> implements the
* <code>OutriggerSpace</code> interface, and each
* <code>SpaceProxy2</code> object holds a reference to the remote
* OutriggerSpace server it represents to the client. The client makes
* calls from the <code>JavaSpace</code> interface, which the
* <code>SpaceProxy2</code> translates into appropriate
* <code>OutriggerSpace</code> calls to the
* <code>OutriggerServerImpl</code> server.
*
* @author Sun Microsystems, Inc.
*
* @see OutriggerSpace
*/
class SpaceProxy2 implements JavaSpace05, Administrable, ReferentUuid,
Serializable
{
static final long serialVersionUID = 1L;
/**
* The remote server this proxy works with.
* Package protected so it can be read by subclasses and proxy verifier
* @serial
*/
final OutriggerServer space;
/**
* The <code>Uuid</code> that identifies the space this proxy is for.
* Package protected so it can be read by subclasses and proxy verifier
* @serial
*/
final Uuid spaceUuid;
/**
* The value to use for maxServerQueryTimeout if no
* local value is provided. Package protected so it can be read by
* subclasses.
* @serial
*/
final long serverMaxServerQueryTimeout;
/**
* Maximum time any sub-query should be allowed to run for.
*/
private transient long maxServerQueryTimeout;
/**
* Value (as a long) of the
* <code>com.sun.jini.outrigger.maxServerQueryTimeout</code>
* property in this VM, or a non-positive number if it is not set.
*/
private static final long maxServerQueryTimeoutPropertyValue =
getMaxServerQueryTimeoutPropertyValue();
/**
* 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");
// --------------------------------------------------
// Construction
// --------------------------------------------------
/**
* Create a new <code>SpaceProxy2</code> for the given space.
* @param space The <code>OutriggerServer</code> for the
* space.
* @param spaceUuid The universally unique ID for the
* space
* @param serverMaxServerQueryTimeout The value this proxy
* should use for the <code>maxServerQueryTimeout</code>
* if no local value is provided.
* @throws NullPointerException if <code>space</code> or
* <code>spaceUuid</code> is <code>null</code>.
* @throws IllegalArgumentException if
* <code>serverMaxServerQueryTimeout</code> is not
* larger than zero.
*/
SpaceProxy2(OutriggerServer space, Uuid spaceUuid,
long serverMaxServerQueryTimeout)
{
if (space == null)
throw new NullPointerException("space must be non-null");
if (spaceUuid == null)
throw new NullPointerException("spaceUuid must be non-null");
if (serverMaxServerQueryTimeout <= 0)
throw new
IllegalArgumentException("serverMaxServerQueryTimeout " +
"must be positive");
this.space = space;
this.spaceUuid = spaceUuid;
this.serverMaxServerQueryTimeout = serverMaxServerQueryTimeout;
setMaxServerQueryTimeout();
}
public String toString() {
return getClass().getName() + " for " + spaceUuid +
" (through " + space + ")";
}
// inherit doc comment
public boolean equals(Object other) {
return ReferentUuids.compare(this, other);
}
// inherit doc comment
public int hashCode() {
return spaceUuid.hashCode();
}
public Uuid getReferentUuid() {
return spaceUuid;
}
/**
* Safely read the value of
* <code>com.sun.jini.outrigger.maxServerQueryTimeout</code>.
* If it can't be read return -1.
*/
private static long getMaxServerQueryTimeoutPropertyValue() {
try {
final String propValue =
(String)Security.doPrivileged(
new ReadProperityPrivilegedAction(
"com.sun.jini.outrigger.maxServerQueryTimeout"));
if (propValue == null)
return -1;
// Note, if we throw NumberFormatException we return -1.
return Long.parseLong(propValue);
} catch (Throwable t) {
return -1;
}
}
/**
* PrivilegedAction for reading a property. Returns a string.
*/
private static class ReadProperityPrivilegedAction
implements PrivilegedAction
{
/** Property to read */
final private String propName;
/**
* Construct a ReadProperityPrivilegedAction that will read
* the specified property.
*/
ReadProperityPrivilegedAction(String propName) {
this.propName = propName;
}
public Object run() {
try {
return System.getProperty(propName);
} catch (SecurityException e) {
return null;
}
}
}
/**
* Set <code>maxServerQueryTimeout</code> based on the values
* of <code>serverMaxServerQueryTimeout</code> and
* <code>maxServerQueryTimeoutPropertyValue</code>.
*/
private void setMaxServerQueryTimeout() {
/* If the com.sun.jini.outrigger.maxServerQueryTimeout property
* was set in this VM, override the value set by the server
* when we were created.
*/
if (maxServerQueryTimeoutPropertyValue > 0)
maxServerQueryTimeout = maxServerQueryTimeoutPropertyValue;
else if (serverMaxServerQueryTimeout > 0)
maxServerQueryTimeout = serverMaxServerQueryTimeout;
else
/* should never get here, the constructor and readObject
* check to make sure that serverMaxServerQueryTimeout
* is positive.
*/
throw new
AssertionError("serverMaxServerQueryTimeout invalid:" +
serverMaxServerQueryTimeout);
if (logger.isLoggable(Level.CONFIG)) {
logger.log(Level.CONFIG,
"Outrigger proxy using {0} ms for maxServerQueryTimeout",
new Long(maxServerQueryTimeout));
}
}
/**
* Read this object back setting the <code>maxServerQueryTimeout</code>
* field and validate state.
*/
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject();
if (space == null)
throw new InvalidObjectException("null server reference");
if (spaceUuid == null)
throw new InvalidObjectException("null Uuid");
if (serverMaxServerQueryTimeout <= 0)
throw new
InvalidObjectException("Bad serverMaxServerQueryTimeout " +
"value:" + serverMaxServerQueryTimeout);
setMaxServerQueryTimeout();
}
/**
* 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("SpaceProxy2 should always have data");
}
// --------------------------------------------------
// JavaSpace method implementations
// --------------------------------------------------
// inherit doc comment
public Lease write(Entry entry, Transaction txn, long lease)
throws TransactionException, RemoteException
{
if (entry == null)
throw new NullPointerException("Cannot write null Entry");
long[] leaseData = space.write(repFor(entry), txn, lease);
if (leaseData == null || leaseData.length != 3)
throw new AssertionError("space.write returned malformed data" +
leaseData);
return newLease(UuidFactory.create(leaseData[1], leaseData[2]),
leaseData[0]);
}
public Entry read(Entry tmpl, Transaction txn, long timeout)
throws UnusableEntryException, TransactionException,
InterruptedException, RemoteException
{
// Figure out the max time this query should last
final long endTime = calcEndTime(timeout);
long remaining = timeout;
OutriggerServer.QueryCookie queryCookie = null;
// Loop util timeout or we get an answer (call at least once!)
do {
final long serverTimeout =
Math.min(remaining, maxServerQueryTimeout);
logQuery("read", serverTimeout, queryCookie, remaining);
final Object rslt =
space.read(repFor(tmpl), txn, serverTimeout, queryCookie);
if (rslt == null) {
// should never get null from a non-ifExists query
throw new AssertionError("space.read() returned null");
} else if (rslt instanceof EntryRep) {
// Got an answer, return it
return entryFrom((EntryRep)rslt);
} else if (rslt instanceof OutriggerServer.QueryCookie) {
/* Will still want to go on if there is time, but pass
* the new cookie
*/
queryCookie = (OutriggerServer.QueryCookie)rslt;
} else {
throw new AssertionError(
"Unexpected return type from space.read()");
}
/* Update remaining and loop, checking to see if the timeout has
* expired.
*/
remaining = endTime - System.currentTimeMillis();
} while (remaining > 0);
/* If we get here then there must not have been an entry available
* to us before the endTime.
*/
return null;
}
// inherit doc comment, use internal routine for common code
public Entry readIfExists(Entry tmpl, Transaction txn, long timeout)
throws UnusableEntryException, TransactionException,
InterruptedException, RemoteException
{
// Figure out the max time this query should last
final long endTime = calcEndTime(timeout);
long remaining = timeout;
OutriggerServer.QueryCookie queryCookie = null;
// Loop util timeout or we get an answer (call at least once!)
do {
final long serverTimeout =
Math.min(remaining, maxServerQueryTimeout);
logQuery("readIfExists", serverTimeout, queryCookie, remaining);
final Object rslt =
space.readIfExists(repFor(tmpl), txn, serverTimeout,
queryCookie);
if (rslt == null) {
// Must be no matches in the space at all
return null;
} else if (rslt instanceof EntryRep) {
// Got an answer, return it
return entryFrom((EntryRep)rslt);
} else if (rslt instanceof OutriggerServer.QueryCookie) {
/* Will still want to go on if there is time, but pass
* the new cookie
*/
queryCookie = (OutriggerServer.QueryCookie)rslt;
} else {
throw new AssertionError(
"Unexpected return type from space.readIfExists()");
}
/* Update remaining and loop, checking to see if the timeout has
* expired.
*/
remaining = endTime - System.currentTimeMillis();
} while (remaining > 0);
/* If we get here then there must not have been an entry available
* to us before the endTime.
*/
return null;
}
// inherit doc comment, use internal routine for common code
public Entry take(Entry tmpl, Transaction txn, long timeout)
throws UnusableEntryException, TransactionException,
InterruptedException, RemoteException
{
// Figure out the max time this query should last
final long endTime = calcEndTime(timeout);
long remaining = timeout;
OutriggerServer.QueryCookie queryCookie = null;
// Loop util timeout or we get an answer (call at least once!)
do {
final long serverTimeout =
Math.min(remaining, maxServerQueryTimeout);
logQuery("take", serverTimeout, queryCookie, remaining);
final Object rslt =
space.take(repFor(tmpl), txn, serverTimeout, queryCookie);
if (rslt == null) {
// should never get null from a non-ifExists query
throw new AssertionError("space.take() returned null");
} else if (rslt instanceof EntryRep) {
// Got an answer, return it
return entryFrom((EntryRep)rslt);
} else if (rslt instanceof OutriggerServer.QueryCookie) {
/* Will still want to go on if there is time, but pass
* the new cookie
*/
queryCookie = (OutriggerServer.QueryCookie)rslt;
} else {
throw new AssertionError(
"Unexpected return type from space.take()");
}
/* Update remaining and loop, checking to see if the timeout has
* expired.
*/
remaining = endTime - System.currentTimeMillis();
} while (remaining > 0);
/* If we get here then there must not have been an entry available
* to us before the endTime.
*/
return null;
}
// inherit doc comment, use internal routine for common code
public Entry takeIfExists(Entry tmpl, Transaction txn, long timeout)
throws UnusableEntryException, TransactionException,
InterruptedException, RemoteException
{
// Figure out the max time this query should last
final long endTime = calcEndTime(timeout);
long remaining = timeout;
OutriggerServer.QueryCookie queryCookie = null;
// Loop util timeout or we get an answer (call at least once!)
do {
final long serverTimeout =
Math.min(remaining, maxServerQueryTimeout);
logQuery("takeIfExists", serverTimeout, queryCookie, remaining);
final Object rslt =
space.takeIfExists(repFor(tmpl), txn, serverTimeout,
queryCookie);
if (rslt == null) {
// Must be no matches in the space at all
return null;
} else if (rslt instanceof EntryRep) {
// Got an answer, return it
return entryFrom((EntryRep)rslt);
} else if (rslt instanceof OutriggerServer.QueryCookie) {
/* Will still want to go on if there is time, but pass
* the new cookie
*/
queryCookie = (OutriggerServer.QueryCookie)rslt;
} else {
throw new AssertionError(
"Unexpected return type from space.takeIfExists()");
}
/* Update remaining and loop, checking to see if the timeout has
* expired.
*/
remaining = endTime - System.currentTimeMillis();
} while (remaining > 0);
/* If we get here then there must not have been an entry available
* to us before the endTime.
*/
return null;
}
// inherit doc comment
public Entry snapshot(Entry entry) throws MarshalException {
if (entry == null)
return null;
else
return new SnapshotRep(entry);
}
// inherit doc comment
public EventRegistration
notify(Entry tmpl, Transaction txn, RemoteEventListener listener,
long lease, MarshalledObject handback)
throws TransactionException, RemoteException
{
return space.notify(repFor(tmpl), txn, listener, lease, handback);
}
public List write(List entries, Transaction txn, List leaseDurations)
throws RemoteException, TransactionException
{
final long[] leases = new long[leaseDurations.size()];
int j = 0;
for (Iterator i=leaseDurations.iterator(); i.hasNext(); ) {
final Object l = i.next();
if (l == null)
throw new NullPointerException(
"leaseDurations contatins a null element");
if (!(l instanceof Long))
throw new IllegalArgumentException(
"leaseDurations contatins an element which is not a Long");
leases[j++] = ((Long)l).longValue();
}
long[] leaseData = space.write(repFor(entries, "entries"), txn, leases);
if (leaseData == null)
throw new AssertionError("space.write<multiple> returned null");
final List rslt = new ArrayList(leaseData.length/3);
try {
int m=0;
while (m<leaseData.length) {
final long duration = leaseData[m++];
final long high = leaseData[m++];
final long low = leaseData[m++];
final Uuid uuid = UuidFactory.create(high, low);
rslt.add(newLease(uuid, duration));
}
} catch (ArrayIndexOutOfBoundsException e) {
throw new
AssertionError("space.write<multiple> returned malformed data");
}
return rslt;
}
public Collection take(Collection tmpls, Transaction txn,
long timeout, long maxEntries)
throws UnusableEntriesException, TransactionException, RemoteException
{
// Figure out the max time this query should last
final long endTime = calcEndTime(timeout);
long remaining = timeout;
OutriggerServer.QueryCookie queryCookie = null;
final EntryRep[] treps = repFor(tmpls, "tmpls");
final int limit;
if (maxEntries < 1) {
throw new IllegalArgumentException("maxEntries must be positive");
} else if (maxEntries <= Integer.MAX_VALUE) {
limit = (int)maxEntries;
} else {
limit = Integer.MAX_VALUE; // ok to return fewer than requested
}
// Loop util timeout or we get an answer (call at least once!)
do {
final long serverTimeout =
Math.min(remaining, maxServerQueryTimeout);
logQuery("take(multiple)", serverTimeout, queryCookie, remaining);
final Object rslt =
space.take(treps, txn, serverTimeout, limit, queryCookie);
if (rslt == null) {
// should never get null from a non-ifExists query
throw new AssertionError("space.take<multiple>() returned null");
} else if (rslt instanceof EntryRep[]) {
EntryRep[] reps = (EntryRep[])rslt;
// Got an answer, return it
final Collection entries = new LinkedList();
Collection exceptions = null;
for (int i=0;i<reps.length;i++) {
try {
entries.add(entryFrom(reps[i]));
} catch (UnusableEntryException e) {
if (exceptions == null)
exceptions = new LinkedList();
exceptions.add(e);
}
}
if (exceptions == null) {
return entries;
} else {
throw new UnusableEntriesException(
"some of the removed entries could not be unmarshalled",
entries, exceptions);
}
} else if (rslt instanceof OutriggerServer.QueryCookie) {
/* Will still want to go on if there is time, but pass
* the new cookie
*/
queryCookie = (OutriggerServer.QueryCookie)rslt;
} else {
throw new AssertionError(
"Unexpected return type from space.take<multiple>()");
}
/* Update remaining and loop, checking to see if the timeout has
* expired.
*/
remaining = endTime - System.currentTimeMillis();
} while (remaining > 0);
/* If we get here then there must not have been any entries available
* to us before the endTime.
*/
return Collections.EMPTY_LIST;
}
public EventRegistration
registerForAvailabilityEvent(Collection tmpls,
Transaction txn,
boolean visibilityOnly,
RemoteEventListener listener,
long leaseDuration,
MarshalledObject handback)
throws TransactionException, RemoteException
{
return space.registerForAvailabilityEvent(
repFor(tmpls, "tmpls"), txn, visibilityOnly, listener,
leaseDuration, handback);
}
// inherit doc comment
public MatchSet contents(Collection tmpls,
Transaction txn,
long leaseDuration,
long maxEntries)
throws RemoteException, TransactionException
{
final MatchSetData msd =
space.contents(repFor(tmpls, "tmpls"), txn, leaseDuration, maxEntries);
return new MatchSetProxy(msd, this, space);
}
/* We break up lease creation into two methods. newLease takes
* care of converting from duration to expiration and we should
* never need to override (so we make it final), while
* constructLease takes care of invoking the right constructor for
* the given context (e.g. constrainable v. not)
*/
/** Create a new lease with the specified id and initial duration */
final protected Lease newLease(Uuid uuid, long duration) {
long expiration = duration + System.currentTimeMillis();
// We added two positive numbers, so if the result is negative
// we must have overflowed, so use Long.MAX_VALUE
if (expiration < 0)
expiration = Long.MAX_VALUE;
return constructLease(uuid, expiration);
}
/** Create a new lease with the specified id and initial expiration */
protected Lease constructLease(Uuid uuid, long expiration) {
return new LandlordLease(uuid, space, spaceUuid, expiration);
}
// --------------------------------------------------
// Administrable methods implementation
// --------------------------------------------------
// inherit doc comment
public Object getAdmin() throws RemoteException {
return space.getAdmin();
}
// --------------------------------------------------
// Private implementation
// --------------------------------------------------
/**
* Utility method to calculate the absolute end time of a query.
* @param timeout relative timeout of query.
* @return timeout plus the current time, or
* <code>Long.MAX_VALUE</code> if timeout plus the current time
* is larger than <code>Long.MAX_VALUE</code>.
*/
private long calcEndTime(long timeout) {
if (timeout < 0)
throw new IllegalArgumentException("timeout must be non-negative");
final long now = System.currentTimeMillis();
if (Long.MAX_VALUE - timeout <= now)
return Long.MAX_VALUE;
else
return now + timeout;
}
static EntryRep[] repFor(Collection entries, String argName)
throws MarshalException
{
final EntryRep[] reps = new EntryRep[entries.size()];
int j = 0;
for (Iterator i=entries.iterator(); i.hasNext(); ) {
final Object e = i.next();
if (!(e == null || e instanceof Entry))
throw new IllegalArgumentException(
argName + " contatins an element which is not an Entry");
reps[j++] = repFor((Entry)e);
}
return reps;
}
/**
* Return an <code>EntryRep</code> object for the given
* <code>Entry</code>.
*/
static EntryRep repFor(Entry entry) throws MarshalException {
if (entry == null)
return null;
if (entry instanceof SnapshotRep) // snapshots are pre-calculated
return ((SnapshotRep) entry).rep();
return new EntryRep(entry);
}
/**
* Return an entry generated from the given rep.
*/
static Entry entryFrom(EntryRep rep) throws UnusableEntryException {
if (rep == null)
return null;
return rep.entry();
}
/** Log query call to server */
private void logQuery(String op, long serverTimeout,
OutriggerServer.QueryCookie cookie, long remaining)
{
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Outrigger calling {0} on server with " +
"timeout of {1} ms for serverTimeout, using QueryCookie " +
"{2}, {3} ms remaining on query",
new Object[] {op, new Long(serverTimeout), cookie,
new Long(remaining)});
}
}
}