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     */
019    package org.apache.activemq.jmdns;
020    
021    import java.io.ByteArrayOutputStream;
022    import java.io.IOException;
023    import java.io.OutputStream;
024    import java.net.InetAddress;
025    import java.util.Enumeration;
026    import java.util.Hashtable;
027    import java.util.TimerTask;
028    import java.util.Vector;
029    import 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     */
037    public 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    }