blob: b91a9a2a369c7280c70676bbaf0f085e18fce09c [file] [log] [blame]
/**
* Copyright 2003-2005 Arthur van Hoff, Rick Blair
*
* 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 org.apache.activemq.jmdns;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.TimerTask;
import java.util.Vector;
import java.util.logging.Logger;
/**
* JmDNS service information.
*
* @version %I%, %G%
* @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer
*/
public class ServiceInfo implements DNSListener
{
private static Logger logger = Logger.getLogger(ServiceInfo.class.toString());
public final static byte[] NO_VALUE = new byte[0];
JmDNS dns;
// State machine
/**
* The state of this service info.
* This is used only for services announced by JmDNS.
* <p/>
* For proper handling of concurrency, this variable must be
* changed only using methods advanceState(), revertState() and cancel().
*/
private DNSState state = DNSState.PROBING_1;
/**
* Task associated to this service info.
* Possible tasks are JmDNS.Prober, JmDNS.Announcer, JmDNS.Responder,
* JmDNS.Canceler.
*/
TimerTask task;
String type;
private String name;
String server;
int port;
int weight;
int priority;
byte text[];
Hashtable props;
InetAddress addr;
/**
* Construct a service description for registrating with JmDNS.
*
* @param type fully qualified service type name, such as <code>_http._tcp.local.</code>.
* @param name unqualified service instance name, such as <code>foobar</code>
* @param port the local port on which the service runs
* @param text string describing the service
*/
public ServiceInfo(String type, String name, int port, String text)
{
this(type, name, port, 0, 0, text);
}
/**
* Construct a service description for registrating with JmDNS.
*
* @param type fully qualified service type name, such as <code>_http._tcp.local.</code>.
* @param name unqualified service instance name, such as <code>foobar</code>
* @param port the local port on which the service runs
* @param weight weight of the service
* @param priority priority of the service
* @param text string describing the service
*/
public ServiceInfo(String type, String name, int port, int weight, int priority, String text)
{
this(type, name, port, weight, priority, (byte[]) null);
try
{
ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
writeUTF(out, text);
this.text = out.toByteArray();
}
catch (IOException e)
{
throw new RuntimeException("unexpected exception: " + e);
}
}
/**
* Construct a service description for registrating with JmDNS. The properties hashtable must
* map property names to either Strings or byte arrays describing the property values.
*
* @param type fully qualified service type name, such as <code>_http._tcp.local.</code>.
* @param name unqualified service instance name, such as <code>foobar</code>
* @param port the local port on which the service runs
* @param weight weight of the service
* @param priority priority of the service
* @param props properties describing the service
*/
public ServiceInfo(String type, String name, int port, int weight, int priority, Hashtable props)
{
this(type, name, port, weight, priority, new byte[0]);
if (props != null)
{
try
{
ByteArrayOutputStream out = new ByteArrayOutputStream(256);
for (Enumeration e = props.keys(); e.hasMoreElements();)
{
String key = (String) e.nextElement();
Object val = props.get(key);
ByteArrayOutputStream out2 = new ByteArrayOutputStream(100);
writeUTF(out2, key);
if (val instanceof String)
{
out2.write('=');
writeUTF(out2, (String) val);
}
else
{
if (val instanceof byte[])
{
out2.write('=');
byte[] bval = (byte[]) val;
out2.write(bval, 0, bval.length);
}
else
{
if (val != NO_VALUE)
{
throw new IllegalArgumentException("invalid property value: " + val);
}
}
}
byte data[] = out2.toByteArray();
out.write(data.length);
out.write(data, 0, data.length);
}
this.text = out.toByteArray();
}
catch (IOException e)
{
throw new RuntimeException("unexpected exception: " + e);
}
}
}
/**
* Construct a service description for registrating with JmDNS.
*
* @param type fully qualified service type name, such as <code>_http._tcp.local.</code>.
* @param name unqualified service instance name, such as <code>foobar</code>
* @param port the local port on which the service runs
* @param weight weight of the service
* @param priority priority of the service
* @param text bytes describing the service
*/
public ServiceInfo(String type, String name, int port, int weight, int priority, byte text[])
{
this.type = type;
this.name = name;
this.port = port;
this.weight = weight;
this.priority = priority;
this.text = text;
}
/**
* Construct a service record during service discovery.
*/
ServiceInfo(String type, String name)
{
if (!type.endsWith("."))
{
throw new IllegalArgumentException("type must be fully qualified DNS name ending in '.': " + type);
}
this.type = type;
this.name = name;
}
/**
* During recovery we need to duplicate service info to reregister them
*/
ServiceInfo(ServiceInfo info)
{
if (info != null)
{
this.type = info.type;
this.name = info.name;
this.port = info.port;
this.weight = info.weight;
this.priority = info.priority;
this.text = info.text;
}
}
/**
* Fully qualified service type name, such as <code>_http._tcp.local.</code> .
*/
public String getType()
{
return type;
}
/**
* Unqualified service instance name, such as <code>foobar</code> .
*/
public String getName()
{
return name;
}
/**
* Sets the service instance name.
*
* @param name unqualified service instance name, such as <code>foobar</code>
*/
void setName(String name)
{
this.name = name;
}
/**
* Fully qualified service name, such as <code>foobar._http._tcp.local.</code> .
*/
public String getQualifiedName()
{
return name + "." + type;
}
/**
* Get the name of the server.
*/
public String getServer()
{
return server;
}
/**
* Get the host address of the service (ie X.X.X.X).
*/
public String getHostAddress()
{
return (addr != null ? addr.getHostAddress() : "");
}
public InetAddress getAddress()
{
return addr;
}
/**
* Get the InetAddress of the service.
*/
public InetAddress getInetAddress()
{
return addr;
}
/**
* Get the port for the service.
*/
public int getPort()
{
return port;
}
/**
* Get the priority of the service.
*/
public int getPriority()
{
return priority;
}
/**
* Get the weight of the service.
*/
public int getWeight()
{
return weight;
}
/**
* Get the text for the serivce as raw bytes.
*/
public byte[] getTextBytes()
{
return text;
}
/**
* Get the text for the service. This will interpret the text bytes
* as a UTF8 encoded string. Will return null if the bytes are not
* a valid UTF8 encoded string.
*/
public String getTextString()
{
if ((text == null) || (text.length == 0) || ((text.length == 1) && (text[0] == 0)))
{
return null;
}
return readUTF(text, 0, text.length);
}
/**
* Get the URL for this service. An http URL is created by
* combining the address, port, and path properties.
*/
public String getURL()
{
return getURL("http");
}
/**
* Get the URL for this service. An URL is created by
* combining the protocol, address, port, and path properties.
*/
public String getURL(String protocol)
{
String url = protocol + "://" + getAddress() + ":" + getPort();
String path = getPropertyString("path");
if (path != null)
{
if (path.indexOf("://") >= 0)
{
url = path;
}
else
{
url += path.startsWith("/") ? path : "/" + path;
}
}
return url;
}
/**
* Get a property of the service. This involves decoding the
* text bytes into a property list. Returns null if the property
* is not found or the text data could not be decoded correctly.
*/
public synchronized byte[] getPropertyBytes(String name)
{
return (byte[]) getProperties().get(name);
}
/**
* Get a property of the service. This involves decoding the
* text bytes into a property list. Returns null if the property
* is not found, the text data could not be decoded correctly, or
* the resulting bytes are not a valid UTF8 string.
*/
public synchronized String getPropertyString(String name)
{
byte data[] = (byte[]) getProperties().get(name);
if (data == null)
{
return null;
}
if (data == NO_VALUE)
{
return "true";
}
return readUTF(data, 0, data.length);
}
/**
* Enumeration of the property names.
*/
public Enumeration getPropertyNames()
{
Hashtable props = getProperties();
return (props != null) ? props.keys() : new Vector().elements();
}
/**
* Write a UTF string with a length to a stream.
*/
void writeUTF(OutputStream out, String str) throws IOException
{
for (int i = 0, len = str.length(); i < len; i++)
{
int c = str.charAt(i);
if ((c >= 0x0001) && (c <= 0x007F))
{
out.write(c);
}
else
{
if (c > 0x07FF)
{
out.write(0xE0 | ((c >> 12) & 0x0F));
out.write(0x80 | ((c >> 6) & 0x3F));
out.write(0x80 | ((c >> 0) & 0x3F));
}
else
{
out.write(0xC0 | ((c >> 6) & 0x1F));
out.write(0x80 | ((c >> 0) & 0x3F));
}
}
}
}
/**
* Read data bytes as a UTF stream.
*/
String readUTF(byte data[], int off, int len)
{
StringBuffer buf = new StringBuffer();
for (int end = off + len; off < end;)
{
int ch = data[off++] & 0xFF;
switch (ch >> 4)
{
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
// 0xxxxxxx
break;
case 12:
case 13:
if (off >= len)
{
return null;
}
// 110x xxxx 10xx xxxx
ch = ((ch & 0x1F) << 6) | (data[off++] & 0x3F);
break;
case 14:
if (off + 2 >= len)
{
return null;
}
// 1110 xxxx 10xx xxxx 10xx xxxx
ch = ((ch & 0x0f) << 12) | ((data[off++] & 0x3F) << 6) | (data[off++] & 0x3F);
break;
default:
if (off + 1 >= len)
{
return null;
}
// 10xx xxxx, 1111 xxxx
ch = ((ch & 0x3F) << 4) | (data[off++] & 0x0f);
break;
}
buf.append((char) ch);
}
return buf.toString();
}
synchronized Hashtable getProperties()
{
if ((props == null) && (text != null))
{
Hashtable props = new Hashtable();
int off = 0;
while (off < text.length)
{
// length of the next key value pair
int len = text[off++] & 0xFF;
if ((len == 0) || (off + len > text.length))
{
props.clear();
break;
}
// look for the '='
int i = 0;
for (; (i < len) && (text[off + i] != '='); i++)
{
;
}
// get the property name
String name = readUTF(text, off, i);
if (name == null)
{
props.clear();
break;
}
if (i == len)
{
props.put(name, NO_VALUE);
}
else
{
byte value[] = new byte[len - ++i];
System.arraycopy(text, off + i, value, 0, len - i);
props.put(name, value);
off += len;
}
}
this.props = props;
}
return props;
}
// REMIND: Oops, this shouldn't be public!
/**
* JmDNS callback to update a DNS record.
*/
public void updateRecord(JmDNS jmdns, long now, DNSRecord rec)
{
if ((rec != null) && !rec.isExpired(now))
{
switch (rec.type)
{
case DNSConstants.TYPE_A: // IPv4
case DNSConstants.TYPE_AAAA: // IPv6 FIXME [PJYF Oct 14 2004] This has not been tested
if (rec.name.equals(server))
{
addr = ((DNSRecord.Address) rec).getAddress();
}
break;
case DNSConstants.TYPE_SRV:
if (rec.name.equals(getQualifiedName()))
{
DNSRecord.Service srv = (DNSRecord.Service) rec;
server = srv.server;
port = srv.port;
weight = srv.weight;
priority = srv.priority;
addr = null;
// changed to use getCache() instead - jeffs
// updateRecord(jmdns, now, (DNSRecord)jmdns.cache.get(server, TYPE_A, CLASS_IN));
updateRecord(jmdns, now, (DNSRecord) jmdns.getCache().get(server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN));
}
break;
case DNSConstants.TYPE_TXT:
if (rec.name.equals(getQualifiedName()))
{
DNSRecord.Text txt = (DNSRecord.Text) rec;
text = txt.text;
}
break;
}
// Future Design Pattern
// This is done, to notify the wait loop in method
// JmDNS.getServiceInfo(type, name, timeout);
if (hasData() && dns != null)
{
dns.handleServiceResolved(this);
dns = null;
}
synchronized (this)
{
notifyAll();
}
}
}
/**
* Returns true if the service info is filled with data.
*/
boolean hasData()
{
return server != null && addr != null && text != null;
}
// State machine
/**
* Sets the state and notifies all objects that wait on the ServiceInfo.
*/
synchronized void advanceState()
{
state = state.advance();
notifyAll();
}
/**
* Sets the state and notifies all objects that wait on the ServiceInfo.
*/
synchronized void revertState()
{
state = state.revert();
notifyAll();
}
/**
* Sets the state and notifies all objects that wait on the ServiceInfo.
*/
synchronized void cancel()
{
state = DNSState.CANCELED;
notifyAll();
}
/**
* Returns the current state of this info.
*/
DNSState getState()
{
return state;
}
public int hashCode()
{
return getQualifiedName().hashCode();
}
public boolean equals(Object obj)
{
return (obj instanceof ServiceInfo) && getQualifiedName().equals(((ServiceInfo) obj).getQualifiedName());
}
public String getNiceTextString()
{
StringBuffer buf = new StringBuffer();
for (int i = 0, len = text.length; i < len; i++)
{
if (i >= 20)
{
buf.append("...");
break;
}
int ch = text[i] & 0xFF;
if ((ch < ' ') || (ch > 127))
{
buf.append("\\0");
buf.append(Integer.toString(ch, 8));
}
else
{
buf.append((char) ch);
}
}
return buf.toString();
}
public String toString()
{
StringBuffer buf = new StringBuffer();
buf.append("service[");
buf.append(getQualifiedName());
buf.append(',');
buf.append(getAddress());
buf.append(':');
buf.append(port);
buf.append(',');
buf.append(getNiceTextString());
buf.append(']');
return buf.toString();
}
}