| /** |
| * 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(); |
| } |
| } |