/*
 * 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 net.jini.discovery;

import com.sun.jini.discovery.Discovery;
import com.sun.jini.discovery.DiscoveryConstraints;
import com.sun.jini.discovery.UnicastResponse;
import com.sun.jini.discovery.UnicastSocketTimeout;
import com.sun.jini.discovery.internal.MultiIPDiscovery;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.MethodConstraints;
import net.jini.core.constraint.RemoteMethodControl;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.lookup.ServiceRegistrar;

/**
 * <code>LookupLocator</code> subclass which supports constraint operations
 * through the {@link RemoteMethodControl} interface.  The constraints of a
 * <code>ConstrainableLookupLocator</code> instance control how it performs
 * unicast discovery, and apply only to its {@link LookupLocator#getRegistrar()
 * getRegistrar()} and {@link LookupLocator#getRegistrar(int)
 * getRegistrar(int)} methods.  The constraints may also be used by other
 * utilities, such as {@link LookupLocatorDiscovery}, to determine how unicast
 * discovery should be performed on behalf of a given
 * <code>ConstrainableLookupLocator</code> instance.  Untrusted
 * <code>ConstrainableLookupLocator</code> instances can be verified using the
 * {@link ConstrainableLookupLocatorTrustVerifier} trust verifier.
 *
 * @author	Sun Microsystems, Inc.
 * @since 2.0
 *
 * @com.sun.jini.impl
 *
 * This class supports use of the following constraint types to control unicast
 * discovery behavior:
 * <ul>
 *   <li> {@link com.sun.jini.discovery.DiscoveryProtocolVersion}: this
 *        constraint can be used to control which version of the unicast
 *        discovery protocol is used.
 *   <li> {@link com.sun.jini.discovery.UnicastSocketTimeout}: this constraint
 *        can be used to control the read timeout set on sockets over which
 *        unicast discovery is performed.
 *   <li> {@link net.jini.core.constraint.ConnectionRelativeTime}:
 *        this constraint can be used to control the relative connection timeout
 *        set on sockets over which unicast discovery is performed.
 *    <li> {@link net.jini.core.constraint.ConnectionAbsoluteTime}:
 *        this constraint can be used to control the absolute connection timeout
 *        set on sockets over which unicast discovery is performed.
 * </ul>
 * <p>
 * In addition, the {@link com.sun.jini.discovery.MulticastMaxPacketSize} and
 * {@link com.sun.jini.discovery.MulticastTimeToLive} constraint types are
 * trivially supported, but do not have any effect on unicast discovery
 * operations.  Constraints other than those mentioned above are passed on to
 * the underlying implementations of versions 1 and 2 of the discovery
 * protocols.
 * <p>
 * An example of using constraints with <code>ConstrainableLookupLocator</code>
 * is:
 * <blockquote><pre>
 * new ConstrainableLookupLocator("target_host", 4160, new {@link net.jini.constraint.BasicMethodConstraints}(
 *     new {@link InvocationConstraints}(
 *         DiscoveryProtocolVersion.TWO, new UnicastSocketTimeout(120000))));
 * </pre></blockquote>
 * The resulting <code>ConstrainableLookupLocator</code> instance would (when
 * used) perform unicast discovery to the host <code>target_host</code> on port
 * 4160 using discovery protocol version 2, with a socket read timeout of
 * 120000 milliseconds unless one was explicitly specified using the {@link
 * #getRegistrar(int)} method.
 */
public final class ConstrainableLookupLocator
    extends LookupLocator implements RemoteMethodControl
{
    private static final long serialVersionUID = 7061417093114347317L;

    private static final int DEFAULT_TIMEOUT = 60 * 1000;
    private static final Method getRegistrarMethod;
    private static final Method getRegistrarTimeoutMethod;
    static {
	try {
	    // REMIND: lookup methods on ConstrainableLookupLocator instead?
	    getRegistrarMethod = LookupLocator.class.getMethod(
		"getRegistrar", new Class[0]);
	    getRegistrarTimeoutMethod = LookupLocator.class.getMethod(
		"getRegistrar", new Class[]{ int.class });
	} catch (NoSuchMethodException e) {
	    throw new AssertionError(e);
	}
    }

    /**
     * The client-side method constraints for unicast discovery.
     * @serial
     */
    private final MethodConstraints constraints;

    /**
     * Constructs a new <code>ConstrainableLookupLocator</code> instance which
     * can be used to perform unicast discovery to the host and port named by
     * the given URL with the provided constraints applied. This constructor
     * invokes its superclass {@link LookupLocator#LookupLocator(String)}
     * constructor.  Any exceptions thrown by the superclass constructor are
     * rethrown.
     * <p>
     * The <code>url</code> must be a valid URL of scheme <code>"jini"</code> as
     * described in <code>LookupLocator(String)</code>.  A <code>null
     * constraints</code> value is interpreted as mapping both
     * <code>getRegistrar</code> methods to empty constraints.
     *
     * @param url the URL to use
     * @param constraints the constraints to apply to unicast discovery, or
     * <code>null</code>
     * @throws MalformedURLException if <code>url</code> cannot be parsed
     * @throws NullPointerException if <code>url</code> is <code>null</code>
     */
    public ConstrainableLookupLocator(String url,
				      MethodConstraints constraints)
	throws MalformedURLException
    {
	super(url);
	this.constraints = constraints;
    }

    /**
     * Constructs a new <code>ConstrainableLookupLocator</code> instance which
     * can be used to perform unicast discovery to the given host and port with
     * the provided constraints applied.  This constructor invokes its
     * superclass {@link LookupLocator#LookupLocator(String, int)}
     * constructor.  Any exceptions thrown by the superclass constructor are
     * rethrown.
     *
     * <p>A <code>null constraints</code> value is interpreted as mapping both
     * <code>getRegistrar</code> methods to empty constraints. The
     * <code>host</code> and <code>port</code> must satisfy the requirements of
     * the <code>LookupLocator(String, int)</code> constructor.
     *
     * @param host the name of the host to contact
     * @param port the number of the port to connect to
     * @param constraints the constraints to apply to unicast discovery, or
     * <code>null</code>
     * @throws NullPointerException if <code>host</code> is <code>null</code>
     * @throws IllegalArgumentException if the port and host do not meet the
     * requirements of <code>LookupLocator(String, int)</code>.
     */
    public ConstrainableLookupLocator(String host,
				      int port,
				      MethodConstraints constraints)
    {
	super(host, port);
	this.constraints = constraints;
    }

    @Override
    public int hashCode() {
	int hash = super.hashCode();
	hash = 79 * hash + (this.constraints != null ? this.constraints.hashCode() : 0);
	return hash;
    }
    
    public boolean equals(Object o){
	if ( this == o ) return true;
	if ( o == null ) return false;
	if ( !(super.equals(o))) return false;
	if ( o instanceof ConstrainableLookupLocator ) {
	    ConstrainableLookupLocator that = (ConstrainableLookupLocator) o;
	    if ( constraints != null ) {
		if ( constraints.equals(that.constraints) ) return true;
	    } else if ( constraints == that.constraints) return true;
	}
	return false;
    }

    /**
     * Performs unicast discovery as specified by 
     * <code>LookupLocator.getRegistrar()</code> with the following differences.
     * <ul>
     * <li> It applies the supplied constraints (if any) for this method.
     * <li> It does not invoke the <code>getRegistrar(int)</code> method.
     * <li> If no timeout constraints are specified, this method assumes a
     * default timeout of 60 seconds.  A non default timeout can only be
     * specified through the supplied constraints, the
     * <code>net.jini.discovery.timeout</code> system property is ignored.
     * </ul>
     * <code>ConstrainableLookupLocator</code> implements this method to use the
     * values of the <code>host</code> and <code>port</code> field in
     * determining the host and port to connect to.
     * @throws net.jini.io.UnsupportedConstraintException if the
     * discovery-related constraints contain conflicts, or otherwise cannot be
     * processed
     */
    public ServiceRegistrar getRegistrar()
	throws IOException, ClassNotFoundException
    {
	return getRegistrar((constraints != null) ?
	    constraints.getConstraints(getRegistrarMethod) :
	    InvocationConstraints.EMPTY);
    }

    /**
     * Performs unicast discovery as specified by 
     * <code>LookupLocator.getRegistrar(int)</code>, additionally applying the
     * supplied discovery constraints. The <code>timeout</code> is considered a
     * requirement with respect to other constraints specified for this
     * instance.
     * @throws net.jini.io.UnsupportedConstraintException if the
     * discovery-related constraints contain conflicts, or otherwise cannot be
     * processed
     */
    public ServiceRegistrar getRegistrar(int timeout)
	throws IOException, ClassNotFoundException
    {
	InvocationConstraints ic = (constraints != null) ?
	    constraints.getConstraints(getRegistrarTimeoutMethod) :
	    InvocationConstraints.EMPTY;
	Collection reqs = new ArrayList(ic.requirements());
	reqs.add(new UnicastSocketTimeout(timeout));
	return getRegistrar(new InvocationConstraints(reqs, ic.preferences()));
    }

    /**
     * Returns a string representation of this object.
     * @return the string representation
     */
    public String toString() {
	return "ConstrainableLookupLocator[[" + super.toString() + "], [" +
	       constraints + "]]";
    }
    
    // documentation inherited from RemoteMethodControl.setConstraints
    public RemoteMethodControl setConstraints(MethodConstraints constraints) {
	return new ConstrainableLookupLocator(host, port, constraints);
    }

    // documentation inherited from RemoteMethodControl.getConstraints
    public MethodConstraints getConstraints() {
	return constraints;
    }

    private ServiceRegistrar getRegistrar(InvocationConstraints constraints)
	throws IOException, ClassNotFoundException
    {
	UnicastResponse resp = new MultiIPDiscovery() {	    
	    protected UnicastResponse performDiscovery(Discovery disco,
		    DiscoveryConstraints dc,
		    Socket s)
		throws IOException, ClassNotFoundException
	    {
		return disco.doUnicastDiscovery(
		    s, dc.getUnfulfilledConstraints(), null, null, null);
	    }
	}.getResponse(host, port, constraints);
	return resp.getRegistrar();
    }
}