/*
* 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.core.discovery;
import com.sun.jini.jeri.internal.runtime.Util;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.rmi.MarshalledObject;
import java.security.AccessController;
import java.security.PrivilegedAction;
import javax.net.SocketFactory;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.discovery.ConstrainableLookupLocator;
import net.jini.discovery.LookupLocatorDiscovery;
/**
* LookupLocator supports unicast discovery, using only version 1 of the
* unicast discovery protocol. It's main purpose now is to contain a host name
* and port number.
* It is used as a parameter in LookupLocatorDiscovery constructors.
* LookupLocatorDiscovery has methods to perform Discovery using either
* version 1 or 2 with constraints.
* ConstrainableLookupLocator is a subclass which uses discovery V1 or V2
* and enables the use of constraints.
*
* @author Sun Microsystems, Inc.
*
* @since 1.0
* @see LookupLocatorDiscovery
* @see ConstrainableLookupLocator
*/
public class LookupLocator implements Serializable {
private static final long serialVersionUID = 1448769379829432795L;
/**
* The port for both unicast and multicast boot requests.
*/
private static final short discoveryPort = 4160;
/**
* The current version of the unicast discovery protocol.
*/
private static final int protoVersion = 1;
/**
* The name of the host at which to perform discovery.
*
* @serial
*/
protected String host;
/**
* The port number on the host at which to perform discovery.
*
* @serial
*/
protected int port;
/**
* The socket factory that this <code>LookupLocator</code> uses to
* create <code>Socket</code> objects.
*
* @serial
**/
protected SocketFactory sf;
/**
* The timeout after which we give up waiting for a response from
* the lookup service.
*/
static final int defaultTimeout =
((Integer)AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
Integer timeout = new Integer(60 * 1000);
try {
Integer val = Integer.getInteger(
"net.jini.discovery.timeout",
timeout);
return (val.intValue() < 0 ? timeout : val);
} catch (SecurityException e) {
return timeout;
}
}
})).intValue();
/**
* Construct a new <code>LookupLocator</code> object, set up to perform
* discovery to the given URL. The <code>host</code> and <code>port</code>
* fields will be populated with the <i>host</i> and <i>port</i>
* components of the input URL. No host name resolution is attempted.
* <p>
* The syntax of the URL must be that of a <i>hierarchical</i>
* {@link java.net.URI} with a <i>server-based naming authority</i>.
* Requirements for the components are as follows:
* <ul>
* <li> A <i>scheme</i> of <code>"jini"</code> must be present.
* <li> A <i>server-based naming authority</i> must be
* present; <i>user-info</i> must not be present. The <i>port</i>, if
* present, must be between 1 and 65535 (both included). If no port is
* specified, a default value of <code>4160</code> is used.
* <li> A <i>path</i> if present, must be
* <code>"/"</code>; <i>path segment</i>s must not be present.
* <li> There must not be any other components present.
* </ul>
* <p>
* The four allowed forms of the URL are thus:
* <ul>
* <li> <code>jini://</code><i>host</i>
* <li> <code>jini://</code><i>host</i><code>/</code>
* <li> <code>jini://</code><i>host</i><code>:</code><i>port</i>
* <li>
* <code>jini://</code><i>host</i><code>:</code><i>port</i><code>/</code>
*
* @param url the URL to use
* @throws MalformedURLException <code>url</code> could not be parsed
* @throws NullPointerException if <code>url</code> is <code>null</code>
*/
public LookupLocator(String url) throws MalformedURLException {
if (url == null) {
throw new NullPointerException("url is null");
}
URI uri = null;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
MalformedURLException mue =
new MalformedURLException("URI parsing failure: " + url);
mue.initCause(e);
throw mue;
}
if (!uri.isAbsolute()) {
throw new MalformedURLException("no scheme specified: " + url);
}
if (uri.isOpaque()) {
throw new MalformedURLException("not a hierarchical url: " + url);
}
if (!uri.getScheme().toLowerCase().equals("jini")) {
throw new MalformedURLException(
"Invalid URL scheme: " + url);
}
String uriPath = uri.getPath();
if ((uriPath.length() != 0) && (!uriPath.equals("/"))) {
throw new MalformedURLException(
"URL path contains path segments: " + url);
}
if (uri.getQuery() != null) {
throw new MalformedURLException(
"invalid character, '?', in URL: " + url);
}
if (uri.getFragment() != null) {
throw new MalformedURLException(
"invalid character, '#', in URL: " + url);
}
// Make sure that it is a server-based authority, if the authority
// component exists
try {
uri = uri.parseServerAuthority();
if (uri.getUserInfo() != null) {
throw new MalformedURLException(
"invalid character, '@', in URL host: " + url);
}
if ((host = uri.getHost()) == null) {
// authority component does not exist - not a hierarchical URL
throw new MalformedURLException(
"Not a hierarchical URL: " + url);
}
port = uri.getPort();
if (port == -1) {
port = discoveryPort;
}
} catch (URISyntaxException e) {
handle3986Authority(uri);
}
if ((port <= 0) || (port >= 65536)) {
throw new MalformedURLException("port number out of range: " + url);
}
}
/**
* Construct a new <code>LookupLocator</code> object, set to perform unicast
* discovery to the input <code>host</code> and <code>port</code>. The
* <code>host</code> and <code>port</code> fields will be populated with the
* <code>host</code> and <code>port</code> arguments. No host name
* resolution is attempted.
* <p>The <code>host</code>
* argument must meet any one of the following syntactical requirements:
* <ul>
* <li>A host as required by a <i>server-based naming authority</i> in
* section 3.2.2 of <a href="http://www.ietf.org/rfc/rfc2396.txt">
* <i>RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax</i></a>
* <li>A literal IPv6 address as defined by
* <a href="http://www.ietf.org/rfc/rfc2732.txt">
* <i>RFC 2732: Format for Literal IPv6 Addresses in URL's</i></a>
* <li>A literal IPv6 address as defined by
* <a href="http://www.ietf.org/rfc/rfc3513.txt">
* <i>RFC 3513: Internet Protocol Version 6 (IPv6) Addressing Architecture
* </i></a>
* </ul>
*
* @param host the name of the host to contact
* @param port the number of the port to connect to
* @throws IllegalArgumentException if <code>port</code> is not between
* 1 and 65535 (both included) or if <code>host</code> cannot be parsed.
* @throws NullPointerException if <code>host</code> is <code>null</code>
*/
public LookupLocator(String host, int port) {
if (host == null)
throw new NullPointerException("null host");
if (port <= 0 || port >= 65536)
throw new IllegalArgumentException("port number out of range");
URI uri;
try {
// Use URI to validate the host.
// We pass in the port to handle the case where the host is in the
// form of a valid IPv6 address with a port appended to it.
uri = new URI(null, null, host, port, null, null, null);
if (uri.getUserInfo() != null) {
throw new IllegalArgumentException(
"invalid character, '@', in host: " + host);
}
this.host = host;
this.port = port;
} catch (URISyntaxException e) {
uri = try3986Authority(host, port);
assert ((this.port > 0) && (this.port < 65536));
}
String uriPath = uri.getPath();
if (uriPath.length() != 0) {
throw new IllegalArgumentException(
"invalid character, '/', in host: " + host);
}
if (uri.getQuery() != null) {
throw new IllegalArgumentException(
"invalid character, '?', in host: " + host);
}
if (uri.getFragment() != null) {
throw new IllegalArgumentException(
"invalid character, '#', in host: " + host);
}
}
/**
* Construct a new <code>LookupLocator</code> object, set to perform unicast
* discovery to the input <code>host</code> and <code>port</code> using the socketFactory.
* The
* <code>host</code>, <code>port</code> and <code>sf</code> fields will be populated with the
* <code>host</code>, <code>port</code> and <code>sf</code> arguments. No host name
* resolution is attempted.
* <p>The <code>host</code>
* argument must meet any one of the following syntactical requirements:
* <ul>
* <li>A host as required by a <i>server-based naming authority</i> in
* section 3.2.2 of <a href="http://www.ietf.org/rfc/rfc2396.txt">
* <i>RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax</i></a>
* <li>A literal IPv6 address as defined by
* <a href="http://www.ietf.org/rfc/rfc2732.txt">
* <i>RFC 2732: Format for Literal IPv6 Addresses in URL's</i></a>
* <li>A literal IPv6 address as defined by
* <a href="http://www.ietf.org/rfc/rfc3513.txt">
* <i>RFC 3513: Internet Protocol Version 6 (IPv6) Addressing Architecture
* </i></a>
* </ul>
*
* @param host the name of the host to contact
* @param port the number of the port to connect to
* @param sf the factory to use for creating the socket
* @throws IllegalArgumentException if <code>port</code> is not between
* 1 and 65535 (both included) or if <code>host</code> cannot be parsed.
* @throws NullPointerException if <code>host</code> is <code>null</code>
*/
public LookupLocator(String host, int port, SocketFactory sf)
{
this(host,port);
this.sf = sf ;
}
/**
* Returns the name of the host that this instance should contact.
* <code>LookupLocator</code> implements this method to return
* the <code>host</code> field.
*
* @return a String representing the host value
*/
public String getHost() {
return host;
}
/**
* Returns the number of the port to which this instance should connect.
* <code>LookupLocator</code> implements this method to return the
* <code>port</code> field.
*
* @return an int representing the port value
*/
public int getPort() {
return port;
}
/**
* Perform unicast discovery and return the ServiceRegistrar
* object for the given lookup service. Unicast discovery is
* performed anew each time this method is called.
* <code>LookupLocator</code> implements this method to simply invoke
* {@link #getRegistrar(int)} with a timeout value, which is determined
* by the value of the <code>net.jini.discovery.timeout</code> system
* property. If the property is set, is not negative, and can be parsed as
* an <code>Integer</code>, the value of the property is used as the timeout
* value. Otherwise, a default value of <code>60</code> seconds is assumed.
*
* @return the ServiceRegistrar for the lookup service denoted by
* this LookupLocator object
* @throws IOException an error occurred during discovery
* @throws ClassNotFoundException if a class required to unmarshal the
* <code>ServiceRegistrar</code> proxy cannot be found
*/
public ServiceRegistrar getRegistrar()
throws IOException, ClassNotFoundException
{
return getRegistrar(defaultTimeout);
}
/**
* Perform unicast discovery and return the ServiceRegistrar
* object for the given lookup service, with the given discovery timeout.
* Unicast discovery is performed anew each time this method is called.
* <code>LookupLocator</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.
*
* <p>
* If a connection can be established to start unicast discovery
* but the remote end fails to respond within the given time
* limit, an exception is thrown.
*
* @param timeout the maximum time to wait for a response, in
* milliseconds. A value of <code>0</code> specifies an infinite timeout.
* @return the ServiceRegistrar for the lookup service denoted by
* this LookupLocator object
* @throws IOException an error occurred during discovery
* @throws ClassNotFoundException if a class required to unmarshal the
* <code>ServiceRegistrar</code> proxy cannot be found
* @throws IllegalArgumentException if <code>timeout</code> is negative
*/
public ServiceRegistrar getRegistrar(int timeout)
throws IOException, ClassNotFoundException
{
InetAddress[] addrs = null;
try {
addrs = InetAddress.getAllByName(host);
} catch (UnknownHostException uhe) {
// Cannot resolve the host name, maybe the socket implementation
// can do it for us.
Socket sock ;
if( sf == null ) {
sock = new Socket(host, port);
} else {
sock = sf.createSocket(host, port);
}
return getRegistrarFromSocket(sock, timeout);
}
IOException ioEx = null;
SecurityException secEx = null;
ClassNotFoundException cnfEx = null;
for (int i = 0; i < addrs.length; i++) {
try {
Socket sock ;
if( sf == null ) {
sock = new Socket(addrs[i], port);
} else {
sock = sf.createSocket(addrs[i], port);
}
return getRegistrarFromSocket(sock, timeout);
} catch (ClassNotFoundException ex) {
cnfEx = ex;
} catch (IOException ex) {
ioEx = ex;
} catch (SecurityException ex) {
secEx = ex;
}
}
// All our attempts failed. Throw ClassNotFoundException, IOException,
// SecurityException in that order of preference.
if (cnfEx != null) {
throw cnfEx;
}
if (ioEx != null) {
throw ioEx;
}
assert (secEx != null);
throw secEx;
}
// Convenience method to do unicast discovery on a socket
private static ServiceRegistrar getRegistrarFromSocket(Socket sock,
int timeout)
throws IOException, ClassNotFoundException
{
try {
sock.setSoTimeout(timeout);
try {
sock.setTcpNoDelay(true);
} catch (SocketException e) {
// ignore possible failures and proceed anyway
}
try {
sock.setKeepAlive(true);
} catch (SocketException e) {
// ignore possible failures and proceed anyway
}
DataOutputStream dstr =
new DataOutputStream(sock.getOutputStream());
dstr.writeInt(protoVersion);
dstr.flush();
ObjectInputStream istr =
new ObjectInputStream(sock.getInputStream());
ServiceRegistrar registrar =
(ServiceRegistrar)((MarshalledObject)istr.readObject()).get();
for (int grpCount = istr.readInt(); --grpCount >= 0; ) {
istr.readUTF(); // ensure proper format, then discard
}
return registrar;
} finally {
try {
sock.close();
} catch (IOException e) {
// ignore
}
}
}
/**
* Return the string form of this LookupLocator, as a URL of scheme
* <code>"jini"</code>.
*/
public String toString() {
if (port != discoveryPort)
return "jini://" + getHost0(host) + ":" + port + "/";
return "jini://" + getHost0(host) + "/";
}
/**
* Two locators are equal if they have the same <code>host</code> and
* <code>port</code> fields. The case of the <code>host</code> is ignored.
* Alternative forms of the same IPv6 addresses for the <code>host</code>
* value are treated as being unequal.
*/
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof LookupLocator) {
LookupLocator oo = (LookupLocator) o;
return port == oo.port && host.equalsIgnoreCase(oo.host) &&
Util.sameClassAndEquals(sf, oo.sf);
}
return false;
}
/**
* Returns a hash code value calculated from the <code>host</code> and
* <code>port</code> field values.
*/
public int hashCode() {
return host.toLowerCase().hashCode() ^ port ^
(sf != null ? sf.hashCode() : 0);
}
// Checks if the host is an RFC 3513 IPv6 literal and converts it into
// RFC 2732 form.
private static String getHost0(String host) {
if ((host.indexOf(':') >= 0) && (host.charAt(0) != '[')) {
// This is a 3513 form IPv6 literal
return '[' + host + ']';
} else {
return host;
}
}
private URI try3986Authority(String host, int port)
{
try {
URI u = new URI("jini://" + host + ":" + port);
handle3986Authority(u);
return u;
} catch (URISyntaxException use) {
// has to be an invalid host
IllegalArgumentException iae =
new IllegalArgumentException("syntax error in host: " +
host);
iae.initCause(use);
throw iae;
} catch (MalformedURLException mue) {
IllegalArgumentException iae =
new IllegalArgumentException("syntax error in host: " +
host);
iae.initCause(mue);
throw iae;
}
}
private void handle3986Authority(URI uri) throws MalformedURLException {
assert (!uri.isOpaque());
String authority;
if ((authority = uri.getAuthority()) == null) {
throw new MalformedURLException("Missing authority: " + uri);
}
if (authority.indexOf('@') != -1) {
throw new MalformedURLException("invalid character, '@', in host: "
+ uri);
}
parseHostPort(authority, uri);
}
private void parseHostPort(String authority, URI uri)
throws MalformedURLException
{
int index = authority.lastIndexOf(':');
if (index == -1) {
port = discoveryPort;
host = authority;
return;
}
// Check for any other colons
if (authority.indexOf(':') != index) {
throw new MalformedURLException(": not allowed in host name: "
+ uri);
}
String portString = authority.substring(index + 1);
int portInt;
if (portString.length() == 0) {
throw new MalformedURLException("invalid port in authority: " +
uri);
} else {
try {
portInt = Integer.parseInt(portString);
} catch (NumberFormatException ne) {
MalformedURLException mue = new MalformedURLException(
"invalid port in authority: " + uri);
mue.initCause(ne);
throw mue;
}
}
port = portInt;
host = authority.substring(0, index);
if (host.length() == 0) {
throw new MalformedURLException("zero length host name: " + uri);
}
}
}