blob: dda46d1c50a56e474bf10fd83c5befff3e660bab [file] [log] [blame]
//Copyright 2003-2005 Arthur van Hoff, Rick Blair
//Licensed under Apache License version 2.0
//Original license LGPL
package javax.jmdns.impl;
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;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.DNSRecord.Pointer;
import javax.jmdns.impl.DNSRecord.Service;
import javax.jmdns.impl.DNSRecord.Text;
/**
* JmDNS service information.
*
* @version %I%, %G%
* @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer
*/
public class ServiceInfoImpl extends ServiceInfo implements DNSListener
{
// private static Logger logger = Logger.getLogger(ServiceInfoImpl.class.getName());
private JmDNSImpl 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.
*/
private TimerTask task;
String type;
private String name;
String server;
int port;
int weight;
int priority;
private byte text[];
Hashtable props;
InetAddress addr;
/**
* @see javax.jmdns.ServiceInfo#create(String, String, int, String)
*/
public ServiceInfoImpl(String type, String name, int port, String text)
{
this(type, name, port, 0, 0, text);
}
/**
* @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, String)
*/
public ServiceInfoImpl(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);
byte [] data = out.toByteArray();
this.setText(new byte[data.length + 1]);
this.getText()[0] = (byte) data.length;
System.arraycopy(data, 0, this.getText(), 1, data.length);
}
catch (IOException e)
{
throw new RuntimeException("unexpected exception: " + e);
}
}
/**
* @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, Hashtable)
*/
public ServiceInfoImpl(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.setText(out.toByteArray());
}
catch (IOException e)
{
throw new RuntimeException("unexpected exception: " + e);
}
}
}
/**
* @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, byte[])
*/
public ServiceInfoImpl(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.setText(text);
}
/**
* Construct a service record during service discovery.
*/
ServiceInfoImpl(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
*/
ServiceInfoImpl(ServiceInfoImpl 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.setText(info.getText());
}
}
/**
* @see javax.jmdns.ServiceInfo#getType()
*/
public String getType()
{
return type;
}
/**
* @see javax.jmdns.ServiceInfo#getName()
*/
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;
}
/**
* @see javax.jmdns.ServiceInfo#getQualifiedName()
*/
public String getQualifiedName()
{
return name + "." + type;
}
/**
* @see javax.jmdns.ServiceInfo#getServer()
*/
public String getServer()
{
return server;
}
/**
* @see javax.jmdns.ServiceInfo#getHostAddress()
*/
public String getHostAddress()
{
return (addr != null ? addr.getHostAddress() : "");
}
public InetAddress getAddress()
{
return addr;
}
/**
* @see javax.jmdns.ServiceInfo#getInetAddress()
*/
public InetAddress getInetAddress()
{
return addr;
}
/**
* @see javax.jmdns.ServiceInfo#getPort()
*/
public int getPort()
{
return port;
}
/**
* @see javax.jmdns.ServiceInfo#getPriority()
*/
public int getPriority()
{
return priority;
}
/**
* @see javax.jmdns.ServiceInfo#getWeight()
*/
public int getWeight()
{
return weight;
}
/**
* @see javax.jmdns.ServiceInfo#getTextBytes()
*/
public byte[] getTextBytes()
{
return getText();
}
/**
* @see javax.jmdns.ServiceInfo#getTextString()
*/
public String getTextString()
{
if ((getText() == null) || (getText().length == 0) || ((getText().length == 1) && (getText()[0] == 0)))
{
return null;
}
return readUTF(getText(), 0, getText().length);
}
/**
* @see javax.jmdns.ServiceInfo#getURL()
*/
public String getURL()
{
return getURL("http");
}
/**
* @see javax.jmdns.ServiceInfo#getURL(java.lang.String)
*/
public String getURL(String protocol)
{
String url = protocol + "://" + getHostAddress() + ":" + getPort();
String path = getPropertyString("path");
if (path != null)
{
if (path.indexOf("://") >= 0)
{
url = path;
}
else
{
url += path.startsWith("/") ? path : "/" + path;
}
}
return url;
}
/**
* @see javax.jmdns.ServiceInfo#getPropertyBytes(java.lang.String)
*/
public synchronized byte[] getPropertyBytes(String name)
{
return (byte[]) getProperties().get(name);
}
/**
* @see javax.jmdns.ServiceInfo#getPropertyString(java.lang.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);
}
/**
* @see javax.jmdns.ServiceInfo#getPropertyNames()
*/
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) && (getText() != null))
{
Hashtable props = new Hashtable();
int off = 0;
while (off < getText().length)
{
// length of the next key value pair
int len = getText()[off++] & 0xFF;
if ((len == 0) || (off + len > getText().length))
{
props.clear();
break;
}
// look for the '='
int i = 0;
for (; (i < len) && (getText()[off + i] != '='); i++)
{
;
}
// get the property name
String name = readUTF(getText(), 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(getText(), off + i, value, 0, len - i);
props.put(name, value);
off += len;
}
}
this.props = props;
}
return props;
}
/**
* JmDNS callback to update a DNS record.
*/
public void updateRecord(JmDNSImpl 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;
setText(txt.text);
}
break;
}
// Future Design Pattern
// This is done, to notify the wait loop in method
// JmDNS.getServiceInfo(type, name, timeout);
if (hasData() && getDns() != null)
{
getDns().handleServiceResolved(this);
setDns(null);
}
synchronized (this)
{
notifyAll();
}
}
}
/**
* Returns true if the service info is filled with data.
*/
public boolean hasData()
{
return server != null && addr != null && getText() != null;
}
// State machine
/**
* Sets the state and notifies all objects that wait on the ServiceInfo.
*/
public 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.
*/
public DNSState getState()
{
return state;
}
public int hashCode()
{
return getQualifiedName().hashCode();
}
public boolean equals(Object obj)
{
return (obj instanceof ServiceInfoImpl) && getQualifiedName().equals(((ServiceInfoImpl) obj).getQualifiedName());
}
public String getNiceTextString()
{
StringBuffer buf = new StringBuffer();
for (int i = 0, len = getText().length; i < len; i++)
{
if (i >= 20)
{
buf.append("...");
break;
}
int ch = getText()[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();
}
public void addAnswers(DNSOutgoing out, int ttl, HostInfo localHost) throws IOException
{
out.addAnswer(new Pointer(type, DNSConstants.TYPE_PTR, DNSConstants.CLASS_IN, ttl,
getQualifiedName()), 0);
out.addAnswer(new Service(getQualifiedName(), DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN|DNSConstants.CLASS_UNIQUE,
ttl, priority, weight, port, localHost.getName()), 0);
out.addAnswer(new Text(getQualifiedName(), DNSConstants.TYPE_TXT, DNSConstants.CLASS_IN|DNSConstants.CLASS_UNIQUE,
ttl, getText()), 0);
}
public void setTask(TimerTask task)
{
this.task = task;
}
public TimerTask getTask()
{
return task;
}
public void setText(byte [] text)
{
this.text = text;
}
public byte [] getText()
{
return text;
}
public void setDns(JmDNSImpl dns)
{
this.dns = dns;
}
public JmDNSImpl getDns()
{
return dns;
}
public String getDomain()
{
String protocol = getProtocol();
int start = type.indexOf(protocol) + protocol.length() + 1;
int end = type.length() - 1;
return type.substring(start, end);
}
public String getProtocol()
{
int start = type.lastIndexOf("._") + 2;
int end = type.indexOf('.', start);
return type.substring(start, end);
}
}