| // 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() { |
| Inet4Address[] ip4Aaddresses = this.getInet4Addresses(); |
| Inet6Address[] ip6Aaddresses = this.getInet6Addresses(); |
| String[] names = new String[ip4Aaddresses.length + ip6Aaddresses.length]; |
| for (int i = 0; i < ip4Aaddresses.length; i++) { |
| names[i] = ip4Aaddresses[i].getHostAddress(); |
| } |
| for (int i = 0; i < ip6Aaddresses.length; i++) { |
| names[i + ip4Aaddresses.length] = "[" + ip6Aaddresses[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(); |
| _props = null; // set it null for apply update text data |
| 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(); |
| } |
| |
| 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 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, this.getTextBytes())); |
| 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; |
| } |
| |
| } |