001/**
002 * Copyright 2003-2005 Arthur van Hoff, Rick Blair
003 *
004 * Licensed to the Apache Software Foundation (ASF) under one or more
005 * contributor license agreements.  See the NOTICE file distributed with
006 * this work for additional information regarding copyright ownership.
007 * The ASF licenses this file to You under the Apache License, Version 2.0
008 * (the "License"); you may not use this file except in compliance with
009 * the License.  You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package org.apache.activemq.jmdns;
020
021import java.io.ByteArrayOutputStream;
022import java.io.IOException;
023import java.io.OutputStream;
024import java.net.InetAddress;
025import java.util.Enumeration;
026import java.util.Hashtable;
027import java.util.TimerTask;
028import java.util.Vector;
029import java.util.logging.Logger;
030
031/**
032 * JmDNS service information.
033 *
034 * @version %I%, %G%
035 * @author      Arthur van Hoff, Jeff Sonstein, Werner Randelshofer
036 */
037public class ServiceInfo implements DNSListener
038{
039    private static Logger logger = Logger.getLogger(ServiceInfo.class.toString());
040    public final static byte[] NO_VALUE = new byte[0];
041    JmDNS dns;
042    
043    // State machine
044    /**
045     * The state of this service info.
046     * This is used only for services announced by JmDNS.
047     * <p/>
048     * For proper handling of concurrency, this variable must be
049     * changed only using methods advanceState(), revertState() and cancel().
050     */
051    private DNSState state = DNSState.PROBING_1;
052
053    /**
054     * Task associated to this service info.
055     * Possible tasks are JmDNS.Prober, JmDNS.Announcer, JmDNS.Responder,
056     * JmDNS.Canceler.
057     */
058    TimerTask task;
059
060    String type;
061    private String name;
062    String server;
063    int port;
064    int weight;
065    int priority;
066    byte text[];
067    Hashtable props;
068    InetAddress addr;
069
070
071    /**
072     * Construct a service description for registrating with JmDNS.
073     *
074     * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>.
075     * @param name unqualified service instance name, such as <code>foobar</code>
076     * @param port the local port on which the service runs
077     * @param text string describing the service
078     */
079    public ServiceInfo(String type, String name, int port, String text)
080    {
081        this(type, name, port, 0, 0, text);
082    }
083
084    /**
085     * Construct a service description for registrating with JmDNS.
086     *
087     * @param type     fully qualified service type name, such as <code>_http._tcp.local.</code>.
088     * @param name     unqualified service instance name, such as <code>foobar</code>
089     * @param port     the local port on which the service runs
090     * @param weight   weight of the service
091     * @param priority priority of the service
092     * @param text     string describing the service
093     */
094    public ServiceInfo(String type, String name, int port, int weight, int priority, String text)
095    {
096        this(type, name, port, weight, priority, (byte[]) null);
097        try
098        {
099            ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
100            writeUTF(out, text);
101            this.text = out.toByteArray();
102        }
103        catch (IOException e)
104        {
105            throw new RuntimeException("unexpected exception: " + e);
106        }
107    }
108
109    /**
110     * Construct a service description for registrating with JmDNS. The properties hashtable must
111     * map property names to either Strings or byte arrays describing the property values.
112     *
113     * @param type     fully qualified service type name, such as <code>_http._tcp.local.</code>.
114     * @param name     unqualified service instance name, such as <code>foobar</code>
115     * @param port     the local port on which the service runs
116     * @param weight   weight of the service
117     * @param priority priority of the service
118     * @param props    properties describing the service
119     */
120    public ServiceInfo(String type, String name, int port, int weight, int priority, Hashtable props)
121    {
122        this(type, name, port, weight, priority, new byte[0]);
123        if (props != null)
124        {
125            try
126            {
127                ByteArrayOutputStream out = new ByteArrayOutputStream(256);
128                for (Enumeration e = props.keys(); e.hasMoreElements();)
129                {
130                    String key = (String) e.nextElement();
131                    Object val = props.get(key);
132                    ByteArrayOutputStream out2 = new ByteArrayOutputStream(100);
133                    writeUTF(out2, key);
134                    if (val instanceof String)
135                    {
136                        out2.write('=');
137                        writeUTF(out2, (String) val);
138                    }
139                    else
140                    {
141                        if (val instanceof byte[])
142                        {
143                            out2.write('=');
144                            byte[] bval = (byte[]) val;
145                            out2.write(bval, 0, bval.length);
146                        }
147                        else
148                        {
149                            if (val != NO_VALUE)
150                            {
151                                throw new IllegalArgumentException("invalid property value: " + val);
152                            }
153                        }
154                    }
155                    byte data[] = out2.toByteArray();
156                    out.write(data.length);
157                    out.write(data, 0, data.length);
158                }
159                this.text = out.toByteArray();
160            }
161            catch (IOException e)
162            {
163                throw new RuntimeException("unexpected exception: " + e);
164            }
165        }
166    }
167
168    /**
169     * Construct a service description for registrating with JmDNS.
170     *
171     * @param type     fully qualified service type name, such as <code>_http._tcp.local.</code>.
172     * @param name     unqualified service instance name, such as <code>foobar</code>
173     * @param port     the local port on which the service runs
174     * @param weight   weight of the service
175     * @param priority priority of the service
176     * @param text     bytes describing the service
177     */
178    public ServiceInfo(String type, String name, int port, int weight, int priority, byte text[])
179    {
180        this.type = type;
181        this.name = name;
182        this.port = port;
183        this.weight = weight;
184        this.priority = priority;
185        this.text = text;
186    }
187
188    /**
189     * Construct a service record during service discovery.
190     */
191    ServiceInfo(String type, String name)
192    {
193        if (!type.endsWith("."))
194        {
195            throw new IllegalArgumentException("type must be fully qualified DNS name ending in '.': " + type);
196        }
197
198        this.type = type;
199        this.name = name;
200    }
201
202    /**
203     * During recovery we need to duplicate service info to reregister them
204     */
205    ServiceInfo(ServiceInfo info)
206    {
207        if (info != null)
208        {
209            this.type = info.type;
210            this.name = info.name;
211            this.port = info.port;
212            this.weight = info.weight;
213            this.priority = info.priority;
214            this.text = info.text;
215        }
216    }
217
218    /**
219     * Fully qualified service type name, such as <code>_http._tcp.local.</code> .
220     */
221    public String getType()
222    {
223        return type;
224    }
225
226    /**
227     * Unqualified service instance name, such as <code>foobar</code> .
228     */
229    public String getName()
230    {
231        return name;
232    }
233
234    /**
235     * Sets the service instance name.
236     *
237     * @param name unqualified service instance name, such as <code>foobar</code>
238     */
239    void setName(String name)
240    {
241        this.name = name;
242    }
243
244    /**
245     * Fully qualified service name, such as <code>foobar._http._tcp.local.</code> .
246     */
247    public String getQualifiedName()
248    {
249        return name + "." + type;
250    }
251
252    /**
253     * Get the name of the server.
254     */
255    public String getServer()
256    {
257        return server;
258    }
259
260    /**
261     * Get the host address of the service (ie X.X.X.X).
262     */
263    public String getHostAddress()
264    {
265        return (addr != null ? addr.getHostAddress() : "");
266    }
267
268    public InetAddress getAddress()
269    {
270        return addr;
271    }
272
273    /**
274     * Get the InetAddress of the service.
275     */
276    public InetAddress getInetAddress()
277    {
278        return addr;
279    }
280
281    /**
282     * Get the port for the service.
283     */
284    public int getPort()
285    {
286        return port;
287    }
288
289    /**
290     * Get the priority of the service.
291     */
292    public int getPriority()
293    {
294        return priority;
295    }
296
297    /**
298     * Get the weight of the service.
299     */
300    public int getWeight()
301    {
302        return weight;
303    }
304
305    /**
306     * Get the text for the serivce as raw bytes.
307     */
308    public byte[] getTextBytes()
309    {
310        return text;
311    }
312
313    /**
314     * Get the text for the service. This will interpret the text bytes
315     * as a UTF8 encoded string. Will return null if the bytes are not
316     * a valid UTF8 encoded string.
317     */
318    public String getTextString()
319    {
320        if ((text == null) || (text.length == 0) || ((text.length == 1) && (text[0] == 0)))
321        {
322            return null;
323        }
324        return readUTF(text, 0, text.length);
325    }
326
327    /**
328     * Get the URL for this service. An http URL is created by
329     * combining the address, port, and path properties.
330     */
331    public String getURL()
332    {
333        return getURL("http");
334    }
335
336    /**
337     * Get the URL for this service. An URL is created by
338     * combining the protocol, address, port, and path properties.
339     */
340    public String getURL(String protocol)
341    {
342        String url = protocol + "://" + getAddress() + ":" + getPort();
343        String path = getPropertyString("path");
344        if (path != null)
345        {
346            if (path.indexOf("://") >= 0)
347            {
348                url = path;
349            }
350            else
351            {
352                url += path.startsWith("/") ? path : "/" + path;
353            }
354        }
355        return url;
356    }
357
358    /**
359     * Get a property of the service. This involves decoding the
360     * text bytes into a property list. Returns null if the property
361     * is not found or the text data could not be decoded correctly.
362     */
363    public synchronized byte[] getPropertyBytes(String name)
364    {
365        return (byte[]) getProperties().get(name);
366    }
367
368    /**
369     * Get a property of the service. This involves decoding the
370     * text bytes into a property list. Returns null if the property
371     * is not found, the text data could not be decoded correctly, or
372     * the resulting bytes are not a valid UTF8 string.
373     */
374    public synchronized String getPropertyString(String name)
375    {
376        byte data[] = (byte[]) getProperties().get(name);
377        if (data == null)
378        {
379            return null;
380        }
381        if (data == NO_VALUE)
382        {
383            return "true";
384        }
385        return readUTF(data, 0, data.length);
386    }
387
388    /**
389     * Enumeration of the property names.
390     */
391    public Enumeration getPropertyNames()
392    {
393        Hashtable props = getProperties();
394        return (props != null) ? props.keys() : new Vector().elements();
395    }
396
397    /**
398     * Write a UTF string with a length to a stream.
399     */
400    void writeUTF(OutputStream out, String str) throws IOException
401    {
402        for (int i = 0, len = str.length(); i < len; i++)
403        {
404            int c = str.charAt(i);
405            if ((c >= 0x0001) && (c <= 0x007F))
406            {
407                out.write(c);
408            }
409            else
410            {
411                if (c > 0x07FF)
412                {
413                    out.write(0xE0 | ((c >> 12) & 0x0F));
414                    out.write(0x80 | ((c >> 6) & 0x3F));
415                    out.write(0x80 | ((c >> 0) & 0x3F));
416                }
417                else
418                {
419                    out.write(0xC0 | ((c >> 6) & 0x1F));
420                    out.write(0x80 | ((c >> 0) & 0x3F));
421                }
422            }
423        }
424    }
425
426    /**
427     * Read data bytes as a UTF stream.
428     */
429    String readUTF(byte data[], int off, int len)
430    {
431        StringBuffer buf = new StringBuffer();
432        for (int end = off + len; off < end;)
433        {
434            int ch = data[off++] & 0xFF;
435            switch (ch >> 4)
436            {
437                case 0:
438                case 1:
439                case 2:
440                case 3:
441                case 4:
442                case 5:
443                case 6:
444                case 7:
445                    // 0xxxxxxx
446                    break;
447                case 12:
448                case 13:
449                    if (off >= len)
450                    {
451                        return null;
452                    }
453                    // 110x xxxx   10xx xxxx
454                    ch = ((ch & 0x1F) << 6) | (data[off++] & 0x3F);
455                    break;
456                case 14:
457                    if (off + 2 >= len)
458                    {
459                        return null;
460                    }
461                    // 1110 xxxx  10xx xxxx  10xx xxxx
462                    ch = ((ch & 0x0f) << 12) | ((data[off++] & 0x3F) << 6) | (data[off++] & 0x3F);
463                    break;
464                default:
465                    if (off + 1 >= len)
466                    {
467                        return null;
468                    }
469                    // 10xx xxxx,  1111 xxxx
470                    ch = ((ch & 0x3F) << 4) | (data[off++] & 0x0f);
471                    break;
472            }
473            buf.append((char) ch);
474        }
475        return buf.toString();
476    }
477
478    synchronized Hashtable getProperties()
479    {
480        if ((props == null) && (text != null))
481        {
482            Hashtable props = new Hashtable();
483            int off = 0;
484            while (off < text.length)
485            {
486                // length of the next key value pair
487                int len = text[off++] & 0xFF;
488                if ((len == 0) || (off + len > text.length))
489                {
490                    props.clear();
491                    break;
492                }
493                // look for the '='
494                int i = 0;
495                for (; (i < len) && (text[off + i] != '='); i++)
496                {
497                    ;
498                }
499
500                // get the property name
501                String name = readUTF(text, off, i);
502                if (name == null)
503                {
504                    props.clear();
505                    break;
506                }
507                if (i == len)
508                {
509                    props.put(name, NO_VALUE);
510                }
511                else
512                {
513                    byte value[] = new byte[len - ++i];
514                    System.arraycopy(text, off + i, value, 0, len - i);
515                    props.put(name, value);
516                    off += len;
517                }
518            }
519            this.props = props;
520        }
521        return props;
522    }
523    
524    // REMIND: Oops, this shouldn't be public!
525    /**
526     * JmDNS callback to update a DNS record.
527     */
528    public void updateRecord(JmDNS jmdns, long now, DNSRecord rec)
529    {
530        if ((rec != null) && !rec.isExpired(now))
531        {
532            switch (rec.type)
533            {
534                case DNSConstants.TYPE_A:               // IPv4
535                case DNSConstants.TYPE_AAAA:    // IPv6 FIXME [PJYF Oct 14 2004] This has not been tested
536                    if (rec.name.equals(server))
537                    {
538                        addr = ((DNSRecord.Address) rec).getAddress();
539
540                    }
541                    break;
542                case DNSConstants.TYPE_SRV:
543                    if (rec.name.equals(getQualifiedName()))
544                    {
545                        DNSRecord.Service srv = (DNSRecord.Service) rec;
546                        server = srv.server;
547                        port = srv.port;
548                        weight = srv.weight;
549                        priority = srv.priority;
550                        addr = null;
551                        // changed to use getCache() instead - jeffs
552                        // updateRecord(jmdns, now, (DNSRecord)jmdns.cache.get(server, TYPE_A, CLASS_IN));
553                        updateRecord(jmdns, now, (DNSRecord) jmdns.getCache().get(server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN));
554                    }
555                    break;
556                case DNSConstants.TYPE_TXT:
557                    if (rec.name.equals(getQualifiedName()))
558                    {
559                        DNSRecord.Text txt = (DNSRecord.Text) rec;
560                        text = txt.text;
561                    }
562                    break;
563            }
564            // Future Design Pattern
565            // This is done, to notify the wait loop in method
566            // JmDNS.getServiceInfo(type, name, timeout);
567            if (hasData() && dns != null)
568            {
569                dns.handleServiceResolved(this);
570                dns = null;
571            }
572            synchronized (this)
573            {
574                notifyAll();
575            }
576        }
577    }
578
579    /**
580     * Returns true if the service info is filled with data.
581     */
582    boolean hasData()
583    {
584        return server != null && addr != null && text != null;
585    }
586    
587    
588    // State machine
589    /**
590     * Sets the state and notifies all objects that wait on the ServiceInfo.
591     */
592    synchronized void advanceState()
593    {
594        state = state.advance();
595        notifyAll();
596    }
597
598    /**
599     * Sets the state and notifies all objects that wait on the ServiceInfo.
600     */
601    synchronized void revertState()
602    {
603        state = state.revert();
604        notifyAll();
605    }
606
607    /**
608     * Sets the state and notifies all objects that wait on the ServiceInfo.
609     */
610    synchronized void cancel()
611    {
612        state = DNSState.CANCELED;
613        notifyAll();
614    }
615
616    /**
617     * Returns the current state of this info.
618     */
619    DNSState getState()
620    {
621        return state;
622    }
623
624
625    public int hashCode()
626    {
627        return getQualifiedName().hashCode();
628    }
629
630    public boolean equals(Object obj)
631    {
632        return (obj instanceof ServiceInfo) && getQualifiedName().equals(((ServiceInfo) obj).getQualifiedName());
633    }
634
635    public String getNiceTextString()
636    {
637        StringBuffer buf = new StringBuffer();
638        for (int i = 0, len = text.length; i < len; i++)
639        {
640            if (i >= 20)
641            {
642                buf.append("...");
643                break;
644            }
645            int ch = text[i] & 0xFF;
646            if ((ch < ' ') || (ch > 127))
647            {
648                buf.append("\\0");
649                buf.append(Integer.toString(ch, 8));
650            }
651            else
652            {
653                buf.append((char) ch);
654            }
655        }
656        return buf.toString();
657    }
658
659    public String toString()
660    {
661        StringBuffer buf = new StringBuffer();
662        buf.append("service[");
663        buf.append(getQualifiedName());
664        buf.append(',');
665        buf.append(getAddress());
666        buf.append(':');
667        buf.append(port);
668        buf.append(',');
669        buf.append(getNiceTextString());
670        buf.append(']');
671        return buf.toString();
672    }
673}