/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */
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.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.space.JavaSpace;

import net.jini.id.Uuid;
import net.jini.id.ReferentUuid;
import net.jini.id.ReferentUuids;
import net.jini.security.Security;

/**
 * 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>SpaceProxy</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>SpaceProxy</code> translates into appropriate
 * <code>OutriggerSpace</code> calls to the
 * <code>OutriggerServerImpl</code> server.
 *
 * @author Sun Microsystems, Inc.
 *
 * @see OutriggerSpace
 */
class SpaceProxy implements JavaSpace, Administrable, ReferentUuid,
			    Serializable 
{
    static final long serialVersionUID = 2L;

    /**
     * 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>SpaceProxy</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.
     */
    SpaceProxy(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("SpaceProxy 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");
	return space.write(repFor(entry), txn, lease);
    }

    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
    {
	if (listener == null)
	    throw new NullPointerException("Passed null listener for " +
					   "event registration");
	return space.notify(repFor(tmpl), txn, listener, lease, handback);
    }

    // --------------------------------------------------
    //          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) {
	final long now = System.currentTimeMillis();
	if (Long.MAX_VALUE - timeout <= now)
	    return Long.MAX_VALUE;
	else
	    return now + timeout;
    }
     
    /**
     * 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)});
	    }
    }
}
