// 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.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.DNSRecord.Pointer;
import javax.jmdns.impl.DNSRecord.Service;
import javax.jmdns.impl.DNSRecord.Text;
import javax.jmdns.impl.constants.DNSRecordClass;
import javax.jmdns.impl.constants.DNSRecordType;
import javax.jmdns.impl.constants.DNSState;
import javax.jmdns.impl.tasks.DNSTask;
/**
* JmDNS service information.
*
* @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer
*/
public class ServiceInfoImpl extends ServiceInfo implements DNSListener, DNSStatefulObject {
private static Logger logger = Logger.getLogger(ServiceInfoImpl.class.getName());
private String _domain;
private String _protocol;
private String _application;
private String _name;
private String _subtype;
private String _server;
private int _port;
private int _weight;
private int _priority;
private byte _text[];
private Map<String, byte[]> _props;
private final Set<Inet4Address> _ipv4Addresses;
private final Set<Inet6Address> _ipv6Addresses;
private transient String _key;
private boolean _persistent;
private boolean _needTextAnnouncing;
private final ServiceInfoState _state;
private Delegate _delegate;
public static interface Delegate {
public void textValueUpdated(ServiceInfo target, byte[] value);
}
private final static class ServiceInfoState extends DNSStatefulObject.DefaultImplementation {
private static final long serialVersionUID = 1104131034952196820L;
private final ServiceInfoImpl _info;
/**
* @param info
*/
public ServiceInfoState(ServiceInfoImpl info) {
super();
_info = info;
}
@Override
protected void setTask(DNSTask task) {
super.setTask(task);
if ((this._task == null) && _info.needTextAnnouncing()) {
this.lock();
try {
if ((this._task == null) && _info.needTextAnnouncing()) {
if (this._state.isAnnounced()) {
this.setState(DNSState.ANNOUNCING_1);
if (this.getDns() != null) {
this.getDns().startAnnouncer();
}
}
_info.setNeedTextAnnouncing(false);
}
} finally {
this.unlock();
}
}
}
@Override
public void setDns(JmDNSImpl dns) {
super.setDns(dns);
}
}
/**
* @param type
* @param name
* @param subtype
* @param port
* @param weight
* @param priority
* @param persistent
* @param text
* @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, String)
*/
public ServiceInfoImpl(String type, String name, String subtype, int port, int weight, int priority, boolean persistent, String text) {
this(ServiceInfoImpl.decodeQualifiedNameMap(type, name, subtype), port, weight, priority, persistent, (byte[]) null);
_server = text;
try {
ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
writeUTF(out, text);
this._text = out.toByteArray();
} catch (IOException e) {
throw new RuntimeException("unexpected exception: " + e);
}
}
/**
* @param type
* @param name
* @param subtype
* @param port
* @param weight
* @param priority
* @param persistent
* @param props
* @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, Map)
*/
public ServiceInfoImpl(String type, String name, String subtype, int port, int weight, int priority, boolean persistent, Map<String, ?> props) {
this(ServiceInfoImpl.decodeQualifiedNameMap(type, name, subtype), port, weight, priority, persistent, textFromProperties(props));
}
/**
* @param type
* @param name
* @param subtype
* @param port
* @param weight
* @param priority
* @param persistent
* @param text
* @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, byte[])
*/
public ServiceInfoImpl(String type, String name, String subtype, int port, int weight, int priority, boolean persistent, byte text[]) {
this(ServiceInfoImpl.decodeQualifiedNameMap(type, name, subtype), port, weight, priority, persistent, text);
}
public ServiceInfoImpl(Map<Fields, String> qualifiedNameMap, int port, int weight, int priority, boolean persistent, Map<String, ?> props) {
this(qualifiedNameMap, port, weight, priority, persistent, textFromProperties(props));
}
ServiceInfoImpl(Map<Fields, String> qualifiedNameMap, int port, int weight, int priority, boolean persistent, String text) {
this(qualifiedNameMap, port, weight, priority, persistent, (byte[]) null);
_server = text;
try {
ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
writeUTF(out, text);
this._text = out.toByteArray();
} catch (IOException e) {
throw new RuntimeException("unexpected exception: " + e);
}
}
ServiceInfoImpl(Map<Fields, String> qualifiedNameMap, int port, int weight, int priority, boolean persistent, byte text[]) {
Map<Fields, String> map = ServiceInfoImpl.checkQualifiedNameMap(qualifiedNameMap);
this._domain = map.get(Fields.Domain);
this._protocol = map.get(Fields.Protocol);
this._application = map.get(Fields.Application);
this._name = map.get(Fields.Instance);
this._subtype = map.get(Fields.Subtype);
this._port = port;
this._weight = weight;
this._priority = priority;
this._text = text;
this.setNeedTextAnnouncing(false);
this._state = new ServiceInfoState(this);
this._persistent = persistent;
this._ipv4Addresses = Collections.synchronizedSet(new LinkedHashSet<Inet4Address>());
this._ipv6Addresses = Collections.synchronizedSet(new LinkedHashSet<Inet6Address>());
}
/**
* During recovery we need to duplicate service info to reregister them
*
* @param info
*/
ServiceInfoImpl(ServiceInfo info) {
this._ipv4Addresses = Collections.synchronizedSet(new LinkedHashSet<Inet4Address>());
this._ipv6Addresses = Collections.synchronizedSet(new LinkedHashSet<Inet6Address>());
if (info != null) {
this._domain = info.getDomain();
this._protocol = info.getProtocol();
this._application = info.getApplication();
this._name = info.getName();
this._subtype = info.getSubtype();
this._port = info.getPort();
this._weight = info.getWeight();
this._priority = info.getPriority();
this._text = info.getTextBytes();
this._persistent = info.isPersistent();
Inet6Address[] ipv6Addresses = info.getInet6Addresses();
for (Inet6Address address : ipv6Addresses) {
this._ipv6Addresses.add(address);
}
Inet4Address[] ipv4Addresses = info.getInet4Addresses();
for (Inet4Address address : ipv4Addresses) {
this._ipv4Addresses.add(address);
}
}
this._state = new ServiceInfoState(this);
}
public static Map<Fields, String> decodeQualifiedNameMap(String type, String name, String subtype) {
Map<Fields, String> qualifiedNameMap = decodeQualifiedNameMapForType(type);
qualifiedNameMap.put(Fields.Instance, name);
qualifiedNameMap.put(Fields.Subtype, subtype);
return checkQualifiedNameMap(qualifiedNameMap);
}
public static Map<Fields, String> decodeQualifiedNameMapForType(String type) {
int index;
String casePreservedType = type;
String aType = type.toLowerCase();
String application = aType;
String protocol = "";
String subtype = "";
String name = "";
String domain = "";
if (aType.contains("in-addr.arpa") || aType.contains("ip6.arpa")) {
index = (aType.contains("in-addr.arpa") ? aType.indexOf("in-addr.arpa") : aType.indexOf("ip6.arpa"));
name = removeSeparators(casePreservedType.substring(0, index));
domain = casePreservedType.substring(index);
application = "";
} else if ((!aType.contains("_")) && aType.contains(".")) {
index = aType.indexOf('.');
name = removeSeparators(casePreservedType.substring(0, index));
domain = removeSeparators(casePreservedType.substring(index));
application = "";
} else {
// First remove the name if it there.
if (!aType.startsWith("_") || aType.startsWith("_services")) {
index = aType.indexOf('.');
if (index > 0) {
// We need to preserve the case for the user readable name.
name = casePreservedType.substring(0, index);
if (index + 1 < aType.length()) {
aType = aType.substring(index + 1);
casePreservedType = casePreservedType.substring(index + 1);
}
}
}
index = aType.lastIndexOf("._");
if (index > 0) {
int start = index + 2;
int end = aType.indexOf('.', start);
protocol = casePreservedType.substring(start, end);
}
if (protocol.length() > 0) {
index = aType.indexOf("_" + protocol.toLowerCase() + ".");
int start = index + protocol.length() + 2;
int end = aType.length() - (aType.endsWith(".") ? 1 : 0);
domain = casePreservedType.substring(start, end);
application = casePreservedType.substring(0, index - 1);
}
index = application.toLowerCase().indexOf("._sub");
if (index > 0) {
int start = index + 5;
subtype = removeSeparators(application.substring(0, index));
application = application.substring(start);
}
}
final Map<Fields, String> qualifiedNameMap = new HashMap<Fields, String>(5);
qualifiedNameMap.put(Fields.Domain, removeSeparators(domain));
qualifiedNameMap.put(Fields.Protocol, protocol);
qualifiedNameMap.put(Fields.Application, removeSeparators(application));
qualifiedNameMap.put(Fields.Instance, name);
qualifiedNameMap.put(Fields.Subtype, subtype);
return qualifiedNameMap;
}
protected static Map<Fields, String> checkQualifiedNameMap(Map<Fields, String> qualifiedNameMap) {
Map<Fields, String> checkedQualifiedNameMap = new HashMap<Fields, String>(5);
// Optional domain
String domain = (qualifiedNameMap.containsKey(Fields.Domain) ? qualifiedNameMap.get(Fields.Domain) : "local");
if ((domain == null) || (domain.length() == 0)) {
domain = "local";
}
domain = removeSeparators(domain);
checkedQualifiedNameMap.put(Fields.Domain, domain);
// Optional protocol
String protocol = (qualifiedNameMap.containsKey(Fields.Protocol) ? qualifiedNameMap.get(Fields.Protocol) : "tcp");
if ((protocol == null) || (protocol.length() == 0)) {
protocol = "tcp";
}
protocol = removeSeparators(protocol);
checkedQualifiedNameMap.put(Fields.Protocol, protocol);
// Application
String application = (qualifiedNameMap.containsKey(Fields.Application) ? qualifiedNameMap.get(Fields.Application) : "");
if ((application == null) || (application.length() == 0)) {
application = "";
}
application = removeSeparators(application);
checkedQualifiedNameMap.put(Fields.Application, application);
// Instance
String instance = (qualifiedNameMap.containsKey(Fields.Instance) ? qualifiedNameMap.get(Fields.Instance) : "");
if ((instance == null) || (instance.length() == 0)) {
instance = "";
// throw new IllegalArgumentException("The instance name component of a fully qualified service cannot be empty.");
}
instance = removeSeparators(instance);
checkedQualifiedNameMap.put(Fields.Instance, instance);
// Optional Subtype
String subtype = (qualifiedNameMap.containsKey(Fields.Subtype) ? qualifiedNameMap.get(Fields.Subtype) : "");
if ((subtype == null) || (subtype.length() == 0)) {
subtype = "";
}
subtype = removeSeparators(subtype);
checkedQualifiedNameMap.put(Fields.Subtype, subtype);
return checkedQualifiedNameMap;
}
private static String removeSeparators(String name) {
if (name == null) {
return "";
}
String newName = name.trim();
if (newName.startsWith(".")) {
newName = newName.substring(1);
}
if (newName.startsWith("_")) {
newName = newName.substring(1);
}
if (newName.endsWith(".")) {
newName = newName.substring(0, newName.length() - 1);
}
return newName;
}
/**
* {@inheritDoc}
*/
@Override
public String getType() {
String domain = this.getDomain();
String protocol = this.getProtocol();
String application = this.getApplication();
return (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + ".";
}
/**
* {@inheritDoc}
*/
@Override
public String getTypeWithSubtype() {
String subtype = this.getSubtype();
return (subtype.length() > 0 ? "_" + subtype.toLowerCase() + "._sub." : "") + this.getType();
}
/**
* {@inheritDoc}
*/
@Override
public String getName() {
return (_name != null ? _name : "");
}
/**
* {@inheritDoc}
*/
@Override
public String getKey() {
if (this._key == null) {
this._key = this.getQualifiedName().toLowerCase();
}
return this._key;
}
/**
* Sets the service instance name.
*
* @param name
* unqualified service instance name, such as <code>foobar</code>
*/
void setName(String name) {
this._name = name;
this._key = null;
}
/**
* {@inheritDoc}
*/
@Override
public String getQualifiedName() {
String domain = this.getDomain();
String protocol = this.getProtocol();
String application = this.getApplication();
String instance = this.getName();
// String subtype = this.getSubtype();
// return (instance.length() > 0 ? instance + "." : "") + (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + (subtype.length() > 0 ? ",_" + subtype.toLowerCase() + "." : ".") : "") + domain
// + ".";
return (instance.length() > 0 ? instance + "." : "") + (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + ".";
}
/**
* @see javax.jmdns.ServiceInfo#getServer()
*/
@Override
public String getServer() {
return (_server != null ? _server : "");
}
/**
* @param server
* the server to set
*/
void setServer(String server) {
this._server = server;
}
/**
* {@inheritDoc}
*/
@Deprecated
@Override
public String getHostAddress() {
String[] names = this.getHostAddresses();
return (names.length > 0 ? names[0] : "");
}
/**
* {@inheritDoc}
*/
@Override
public String[] getHostAddresses() {
InetAddress[] addresses = this.getInetAddresses();
String[] names = new String[addresses.length];
for (int i = 0; i < addresses.length; i++) {
names[i] = addresses[i].getHostAddress();
}
return names;
}
/**
* @param addr
* the addr to add
*/
void addAddress(Inet4Address addr) {
_ipv4Addresses.add(addr);
}
/**
* @param addr
* the addr to add
*/
void addAddress(Inet6Address addr) {
_ipv6Addresses.add(addr);
}
/**
* {@inheritDoc}
*/
@Deprecated
@Override
public InetAddress getAddress() {
return this.getInetAddress();
}
/**
* {@inheritDoc}
*/
@Deprecated
@Override
public InetAddress getInetAddress() {
InetAddress[] addresses = this.getInetAddresses();
return (addresses.length > 0 ? addresses[0] : null);
}
/**
* {@inheritDoc}
*/
@Deprecated
@Override
public Inet4Address getInet4Address() {
Inet4Address[] addresses = this.getInet4Addresses();
return (addresses.length > 0 ? addresses[0] : null);
}
/**
* {@inheritDoc}
*/
@Deprecated
@Override
public Inet6Address getInet6Address() {
Inet6Address[] addresses = this.getInet6Addresses();
return (addresses.length > 0 ? addresses[0] : null);
}
/*
* (non-Javadoc)
* @see javax.jmdns.ServiceInfo#getInetAddresses()
*/
@Override
public InetAddress[] getInetAddresses() {
List<InetAddress> aList = new ArrayList<InetAddress>(_ipv4Addresses.size() + _ipv6Addresses.size());
aList.addAll(_ipv4Addresses);
aList.addAll(_ipv6Addresses);
return aList.toArray(new InetAddress[aList.size()]);
}
/*
* (non-Javadoc)
* @see javax.jmdns.ServiceInfo#getInet4Addresses()
*/
@Override
public Inet4Address[] getInet4Addresses() {
return _ipv4Addresses.toArray(new Inet4Address[_ipv4Addresses.size()]);
}
/*
* (non-Javadoc)
* @see javax.jmdns.ServiceInfo#getInet6Addresses()
*/
@Override
public Inet6Address[] getInet6Addresses() {
return _ipv6Addresses.toArray(new Inet6Address[_ipv6Addresses.size()]);
}
/**
* @see javax.jmdns.ServiceInfo#getPort()
*/
@Override
public int getPort() {
return _port;
}
/**
* @see javax.jmdns.ServiceInfo#getPriority()
*/
@Override
public int getPriority() {
return _priority;
}
/**
* @see javax.jmdns.ServiceInfo#getWeight()
*/
@Override
public int getWeight() {
return _weight;
}
/**
* @see javax.jmdns.ServiceInfo#getTextBytes()
*/
@Override
public byte[] getTextBytes() {
return (this._text != null && this._text.length > 0 ? this._text : DNSRecord.EMPTY_TXT);
}
/**
* {@inheritDoc}
*/
@Deprecated
@Override
public String getTextString() {
Map<String, byte[]> properties = this.getProperties();
for (String key : properties.keySet()) {
byte[] value = properties.get(key);
if ((value != null) && (value.length > 0)) {
return key + "=" + new String(value);
}
return key;
}
return "";
}
/*
* (non-Javadoc)
* @see javax.jmdns.ServiceInfo#getURL()
*/
@Deprecated
@Override
public String getURL() {
return this.getURL("http");
}
/*
* (non-Javadoc)
* @see javax.jmdns.ServiceInfo#getURLs()
*/
@Override
public String[] getURLs() {
return this.getURLs("http");
}
/*
* (non-Javadoc)
* @see javax.jmdns.ServiceInfo#getURL(java.lang.String)
*/
@Deprecated
@Override
public String getURL(String protocol) {
String[] urls = this.getURLs(protocol);
return (urls.length > 0 ? urls[0] : protocol + "://null:" + getPort());
}
/*
* (non-Javadoc)
* @see javax.jmdns.ServiceInfo#getURLs(java.lang.String)
*/
@Override
public String[] getURLs(String protocol) {
InetAddress[] addresses = this.getInetAddresses();
String[] urls = new String[addresses.length];
for (int i = 0; i < addresses.length; i++) {
String url = protocol + "://" + addresses[i].getHostAddress() + ":" + getPort();
String path = getPropertyString("path");
if (path != null) {
if (path.indexOf("://") >= 0) {
url = path;
} else {
url += path.startsWith("/") ? path : "/" + path;
}
}
urls[i] = url;
}
return urls;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized byte[] getPropertyBytes(String name) {
return this.getProperties().get(name);
}
/**
* {@inheritDoc}
*/
@Override
public synchronized String getPropertyString(String name) {
byte data[] = this.getProperties().get(name);
if (data == null) {
return null;
}
if (data == NO_VALUE) {
return "true";
}
return readUTF(data, 0, data.length);
}
/**
* {@inheritDoc}
*/
@Override
public Enumeration<String> getPropertyNames() {
Map<String, byte[]> properties = this.getProperties();
Collection<String> names = (properties != null ? properties.keySet() : Collections.<String> emptySet());
return new Vector<String>(names).elements();
}
/**
* {@inheritDoc}
*/
@Override
public String getApplication() {
return (_application != null ? _application : "");
}
/**
* {@inheritDoc}
*/
@Override
public String getDomain() {
return (_domain != null ? _domain : "local");
}
/**
* {@inheritDoc}
*/
@Override
public String getProtocol() {
return (_protocol != null ? _protocol : "tcp");
}
/**
* {@inheritDoc}
*/
@Override
public String getSubtype() {
return (_subtype != null ? _subtype : "");
}
/**
* {@inheritDoc}
*/
@Override
public Map<Fields, String> getQualifiedNameMap() {
Map<Fields, String> map = new HashMap<Fields, String>(5);
map.put(Fields.Domain, this.getDomain());
map.put(Fields.Protocol, this.getProtocol());
map.put(Fields.Application, this.getApplication());
map.put(Fields.Instance, this.getName());
map.put(Fields.Subtype, this.getSubtype());
return map;
}
/**
* Write a UTF string with a length to a stream.
*/
static 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) {
int offset = off;
StringBuffer buf = new StringBuffer();
for (int end = offset + len; offset < end;) {
int ch = data[offset++] & 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 (offset >= len) {
return null;
}
// 110x xxxx 10xx xxxx
ch = ((ch & 0x1F) << 6) | (data[offset++] & 0x3F);
break;
case 14:
if (offset + 2 >= len) {
return null;
}
// 1110 xxxx 10xx xxxx 10xx xxxx
ch = ((ch & 0x0f) << 12) | ((data[offset++] & 0x3F) << 6) | (data[offset++] & 0x3F);
break;
default:
if (offset + 1 >= len) {
return null;
}
// 10xx xxxx, 1111 xxxx
ch = ((ch & 0x3F) << 4) | (data[offset++] & 0x0f);
break;
}
buf.append((char) ch);
}
return buf.toString();
}
synchronized Map<String, byte[]> getProperties() {
if ((_props == null) && (this.getTextBytes() != null)) {
Hashtable<String, byte[]> properties = new Hashtable<String, byte[]>();
try {
int off = 0;
while (off < getTextBytes().length) {
// length of the next key value pair
int len = getTextBytes()[off++] & 0xFF;
if ((len == 0) || (off + len > getTextBytes().length)) {
properties.clear();
break;
}
// look for the '='
int i = 0;
for (; (i < len) && (getTextBytes()[off + i] != '='); i++) {
/* Stub */
}
// get the property name
String name = readUTF(getTextBytes(), off, i);
if (name == null) {
properties.clear();
break;
}
if (i == len) {
properties.put(name, NO_VALUE);
} else {
byte value[] = new byte[len - ++i];
System.arraycopy(getTextBytes(), off + i, value, 0, len - i);
properties.put(name, value);
off += len;
}
}
} catch (Exception exception) {
// We should get better logging.
logger.log(Level.WARNING, "Malformed TXT Field ", exception);
}
this._props = properties;
}
return (_props != null ? _props : Collections.<String, byte[]> emptyMap());
}
/**
* JmDNS callback to update a DNS record.
*
* @param dnsCache
* @param now
* @param rec
*/
@Override
public void updateRecord(DNSCache dnsCache, long now, DNSEntry rec) {
if ((rec instanceof DNSRecord) && !rec.isExpired(now)) {
boolean serviceUpdated = false;
switch (rec.getRecordType()) {
case TYPE_A: // IPv4
if (rec.getName().equalsIgnoreCase(this.getServer())) {
_ipv4Addresses.add((Inet4Address) ((DNSRecord.Address) rec).getAddress());
serviceUpdated = true;
}
break;
case TYPE_AAAA: // IPv6
if (rec.getName().equalsIgnoreCase(this.getServer())) {
_ipv6Addresses.add((Inet6Address) ((DNSRecord.Address) rec).getAddress());
serviceUpdated = true;
}
break;
case TYPE_SRV:
if (rec.getName().equalsIgnoreCase(this.getQualifiedName())) {
DNSRecord.Service srv = (DNSRecord.Service) rec;
boolean serverChanged = (_server == null) || !_server.equalsIgnoreCase(srv.getServer());
_server = srv.getServer();
_port = srv.getPort();
_weight = srv.getWeight();
_priority = srv.getPriority();
if (serverChanged) {
_ipv4Addresses.clear();
_ipv6Addresses.clear();
for (DNSEntry entry : dnsCache.getDNSEntryList(_server, DNSRecordType.TYPE_A, DNSRecordClass.CLASS_IN)) {
this.updateRecord(dnsCache, now, entry);
}
for (DNSEntry entry : dnsCache.getDNSEntryList(_server, DNSRecordType.TYPE_AAAA, DNSRecordClass.CLASS_IN)) {
this.updateRecord(dnsCache, now, entry);
}
// We do not want to trigger the listener in this case as it will be triggered if the address resolves.
} else {
serviceUpdated = true;
}
}
break;
case TYPE_TXT:
if (rec.getName().equalsIgnoreCase(this.getQualifiedName())) {
DNSRecord.Text txt = (DNSRecord.Text) rec;
_text = txt.getText();
serviceUpdated = true;
}
break;
case TYPE_PTR:
if ((this.getSubtype().length() == 0) && (rec.getSubtype().length() != 0)) {
_subtype = rec.getSubtype();
serviceUpdated = true;
}
break;
default:
break;
}
if (serviceUpdated && this.hasData()) {
JmDNSImpl dns = this.getDns();
if (dns != null) {
ServiceEvent event = ((DNSRecord) rec).getServiceEvent(dns);
event = new ServiceEventImpl(dns, event.getType(), event.getName(), this);
dns.handleServiceResolved(event);
}
}
// This is done, to notify the wait loop in method JmDNS.waitForInfoData(ServiceInfo info, int timeout);
synchronized (this) {
this.notifyAll();
}
}
}
/**
* Returns true if the service info is filled with data.
*
* @return <code>true</code> if the service info has data, <code>false</code> otherwise.
*/
@Override
public synchronized boolean hasData() {
return this.getServer() != null && this.hasInetAddress() && this.getTextBytes() != null && this.getTextBytes().length > 0;
// return this.getServer() != null && (this.getAddress() != null || (this.getTextBytes() != null && this.getTextBytes().length > 0));
}
private final boolean hasInetAddress() {
return _ipv4Addresses.size() > 0 || _ipv6Addresses.size() > 0;
}
// State machine
/**
* {@inheritDoc}
*/
@Override
public boolean advanceState(DNSTask task) {
return _state.advanceState(task);
}
/**
* {@inheritDoc}
*/
@Override
public boolean revertState() {
return _state.revertState();
}
/**
* {@inheritDoc}
*/
@Override
public boolean cancelState() {
return _state.cancelState();
}
/**
* {@inheritDoc}
*/
@Override
public boolean closeState() {
return this._state.closeState();
}
/**
* {@inheritDoc}
*/
@Override
public boolean recoverState() {
return this._state.recoverState();
}
/**
* {@inheritDoc}
*/
@Override
public void removeAssociationWithTask(DNSTask task) {
_state.removeAssociationWithTask(task);
}
/**
* {@inheritDoc}
*/
@Override
public void associateWithTask(DNSTask task, DNSState state) {
_state.associateWithTask(task, state);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAssociatedWithTask(DNSTask task, DNSState state) {
return _state.isAssociatedWithTask(task, state);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isProbing() {
return _state.isProbing();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAnnouncing() {
return _state.isAnnouncing();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAnnounced() {
return _state.isAnnounced();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCanceling() {
return this._state.isCanceling();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCanceled() {
return _state.isCanceled();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isClosing() {
return _state.isClosing();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isClosed() {
return _state.isClosed();
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForAnnounced(long timeout) {
return _state.waitForAnnounced(timeout);
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForCanceled(long timeout) {
return _state.waitForCanceled(timeout);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return getQualifiedName().hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
return (obj instanceof ServiceInfoImpl) && getQualifiedName().equals(((ServiceInfoImpl) obj).getQualifiedName());
}
/**
* {@inheritDoc}
*/
@Override
public String getNiceTextString() {
StringBuffer buf = new StringBuffer();
for (int i = 0, len = this.getTextBytes().length; i < len; i++) {
if (i >= 200) {
buf.append("...");
break;
}
int ch = getTextBytes()[i] & 0xFF;
if ((ch < ' ') || (ch > 127)) {
buf.append("\\0");
buf.append(Integer.toString(ch, 8));
} else {
buf.append((char) ch);
}
}
return buf.toString();
}
/*
* (non-Javadoc)
* @see javax.jmdns.ServiceInfo#clone()
*/
@Override
public ServiceInfoImpl clone() {
ServiceInfoImpl serviceInfo = new ServiceInfoImpl(this.getQualifiedNameMap(), _port, _weight, _priority, _persistent, _text);
Inet6Address[] ipv6Addresses = this.getInet6Addresses();
for (Inet6Address address : ipv6Addresses) {
serviceInfo._ipv6Addresses.add(address);
}
Inet4Address[] ipv4Addresses = this.getInet4Addresses();
for (Inet4Address address : ipv4Addresses) {
serviceInfo._ipv4Addresses.add(address);
}
return serviceInfo;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("[" + this.getClass().getSimpleName() + "@" + System.identityHashCode(this) + " ");
buf.append("name: '");
buf.append((this.getName().length() > 0 ? this.getName() + "." : "") + this.getTypeWithSubtype());
buf.append("' address: '");
InetAddress[] addresses = this.getInetAddresses();
if (addresses.length > 0) {
for (InetAddress address : addresses) {
buf.append(address);
buf.append(':');
buf.append(this.getPort());
buf.append(' ');
}
} else {
buf.append("(null):");
buf.append(this.getPort());
}
buf.append("' status: '");
buf.append(_state.toString());
buf.append(this.isPersistent() ? "' is persistent," : "',");
buf.append(" has ");
buf.append(this.hasData() ? "" : "NO ");
buf.append("data");
if (this.getTextBytes().length > 0) {
// buf.append("\n");
// buf.append(this.getNiceTextString());
Map<String, byte[]> properties = this.getProperties();
if (!properties.isEmpty()) {
buf.append("\n");
for (String key : properties.keySet()) {
buf.append("\t" + key + ": " + new String(properties.get(key)) + "\n");
}
} else {
buf.append(" empty");
}
}
buf.append(']');
return buf.toString();
}
private String removeLastDot(String s) {
if(s.endsWith(".")) {
return s.substring(0, s.length() - 1);
}
return s;
}
public Collection<DNSRecord> answers(boolean unique, int ttl, HostInfo localHost) {
List<DNSRecord> list = new ArrayList<DNSRecord>();
if (this.getSubtype().length() > 0) {
list.add(new Pointer(this.getTypeWithSubtype(), DNSRecordClass.CLASS_IN, DNSRecordClass.NOT_UNIQUE, ttl, this.getQualifiedName()));
}
list.add(new Text(this.getQualifiedName(), DNSRecordClass.CLASS_IN, unique, ttl, textBytesToValidTextBytes(this.getTextBytes())));
list.add(new Pointer(this.getType(), DNSRecordClass.CLASS_IN, DNSRecordClass.NOT_UNIQUE, ttl, this.getQualifiedName()));
list.add(new Service(this.getQualifiedName(), DNSRecordClass.CLASS_IN, unique, ttl, _priority, _weight, _port, localHost.getName()));
//list.add(new Text(this.getQualifiedName(), DNSRecordClass.CLASS_IN, unique, ttl, textBytesToValidTextBytes(this.getTextBytes())));
// for (Inet6Address adr : this.getInet6Addresses()) {
// list.add(new DNSRecord.IPv6Address(removeLastDot(this.getServer()), DNSRecordClass.CLASS_IN, false, ttl, adr));
// }
for (Inet4Address adr : this.getInet4Addresses()) {
list.add(new DNSRecord.IPv4Address(removeLastDot(this.getServer()), DNSRecordClass.CLASS_IN, false, ttl, adr));
}
list.add(new Pointer("_services._dns-sd._udp.local", DNSRecordClass.CLASS_IN, false, ttl, "_raop._tcp.local"));
return list;
}
/**
* {@inheritDoc}
*/
@Override
public void setText(byte[] text) throws IllegalStateException {
synchronized (this) {
this._text = text;
this._props = null;
this.setNeedTextAnnouncing(true);
}
}
/**
* {@inheritDoc}
*/
@Override
public void setText(Map<String, ?> props) throws IllegalStateException {
this.setText(textFromProperties(props));
}
/**
* This is used internally by the framework
*
* @param text
*/
void _setText(byte[] text) {
this._text = text;
this._props = null;
}
private static byte[] textFromProperties(Map<String, ?> props) {
byte[] text = null;
if (props != null) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream(256);
for (String key : props.keySet()) {
Object val = props.get(key);
ByteArrayOutputStream out2 = new ByteArrayOutputStream(100);
writeUTF(out2, key);
if (val == null) {
// Skip
} else if (val instanceof String) {
out2.write('=');
writeUTF(out2, (String) val);
} else if (val instanceof byte[]) {
byte[] bval = (byte[]) val;
if (bval.length > 0) {
out2.write('=');
out2.write(bval, 0, bval.length);
} else {
val = null;
}
} else {
throw new IllegalArgumentException("invalid property value: " + val);
}
byte data[] = out2.toByteArray();
if (data.length > 255) {
throw new IOException("Cannot have individual values larger that 255 chars. Offending value: " + key + (val != null ? "" : "=" + val));
}
out.write((byte) data.length);
out.write(data, 0, data.length);
}
text = out.toByteArray();
} catch (IOException e) {
throw new RuntimeException("unexpected exception: " + e);
}
}
return (text != null && text.length > 0 ? text : DNSRecord.EMPTY_TXT);
}
public void setDns(JmDNSImpl dns) {
this._state.setDns(dns);
}
/**
* {@inheritDoc}
*/
@Override
public JmDNSImpl getDns() {
return this._state.getDns();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isPersistent() {
return _persistent;
}
/**
* @param needTextAnnouncing
* the needTextAnnouncing to set
*/
public void setNeedTextAnnouncing(boolean needTextAnnouncing) {
this._needTextAnnouncing = needTextAnnouncing;
if (this._needTextAnnouncing) {
_state.setTask(null);
}
}
/**
* @return the needTextAnnouncing
*/
public boolean needTextAnnouncing() {
return _needTextAnnouncing;
}
/**
* @return the delegate
*/
Delegate getDelegate() {
return this._delegate;
}
/**
* @param delegate
* the delegate to set
*/
void setDelegate(Delegate delegate) {
this._delegate = delegate;
}
public static byte[] textBytesToValidTextBytes(byte[] bytes) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
String str = new String(bytes);
String[] sp = str.split(" ");
for (String s : sp) {
out.write(s.length());
try {
out.write(s.getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return out.toByteArray();
} finally {
try {
out.close();
} catch (IOException e) {
// ignore
}
}
}
}