1// Copyright 2003-2005 Arthur van Hoff, Rick Blair
2// Licensed under Apache License version 2.0
3// Original license LGPL
4
5package javax.jmdns.impl;
6
7import java.io.ByteArrayOutputStream;
8import java.io.IOException;
9import java.io.OutputStream;
10import java.net.Inet4Address;
11import java.net.Inet6Address;
12import java.net.InetAddress;
13import java.util.ArrayList;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.Enumeration;
17import java.util.HashMap;
18import java.util.Hashtable;
19import java.util.LinkedHashSet;
20import java.util.List;
21import java.util.Map;
22import java.util.Set;
23import java.util.Vector;
24import java.util.logging.Level;
25import java.util.logging.Logger;
26
27import javax.jmdns.ServiceEvent;
28import javax.jmdns.ServiceInfo;
29import javax.jmdns.impl.DNSRecord.Pointer;
30import javax.jmdns.impl.DNSRecord.Service;
31import javax.jmdns.impl.DNSRecord.Text;
32import javax.jmdns.impl.constants.DNSRecordClass;
33import javax.jmdns.impl.constants.DNSRecordType;
34import javax.jmdns.impl.constants.DNSState;
35import javax.jmdns.impl.tasks.DNSTask;
36
37/**
38 * JmDNS service information.
39 *
40 * @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer
41 */
42public class ServiceInfoImpl extends ServiceInfo implements DNSListener, DNSStatefulObject {
43    private static Logger           logger = Logger.getLogger(ServiceInfoImpl.class.getName());
44
45    private String                  _domain;
46    private String                  _protocol;
47    private String                  _application;
48    private String                  _name;
49    private String                  _subtype;
50    private String                  _server;
51    private int                     _port;
52    private int                     _weight;
53    private int                     _priority;
54    private byte                    _text[];
55    private Map<String, byte[]>     _props;
56    private final Set<Inet4Address> _ipv4Addresses;
57    private final Set<Inet6Address> _ipv6Addresses;
58
59    private transient String        _key;
60
61    private boolean                 _persistent;
62    private boolean                 _needTextAnnouncing;
63
64    private final ServiceInfoState  _state;
65
66    private Delegate                _delegate;
67
68    public static interface Delegate {
69
70        public void textValueUpdated(ServiceInfo target, byte[] value);
71
72    }
73
74    private final static class ServiceInfoState extends DNSStatefulObject.DefaultImplementation {
75
76        private static final long     serialVersionUID = 1104131034952196820L;
77
78        private final ServiceInfoImpl _info;
79
80        /**
81         * @param info
82         */
83        public ServiceInfoState(ServiceInfoImpl info) {
84            super();
85            _info = info;
86        }
87
88        @Override
89        protected void setTask(DNSTask task) {
90            super.setTask(task);
91            if ((this._task == null) && _info.needTextAnnouncing()) {
92                this.lock();
93                try {
94                    if ((this._task == null) && _info.needTextAnnouncing()) {
95                        if (this._state.isAnnounced()) {
96                            this.setState(DNSState.ANNOUNCING_1);
97                            if (this.getDns() != null) {
98                                this.getDns().startAnnouncer();
99                            }
100                        }
101                        _info.setNeedTextAnnouncing(false);
102                    }
103                } finally {
104                    this.unlock();
105                }
106            }
107        }
108
109        @Override
110        public void setDns(JmDNSImpl dns) {
111            super.setDns(dns);
112        }
113
114    }
115
116    /**
117     * @param type
118     * @param name
119     * @param subtype
120     * @param port
121     * @param weight
122     * @param priority
123     * @param persistent
124     * @param text
125     * @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, String)
126     */
127    public ServiceInfoImpl(String type, String name, String subtype, int port, int weight, int priority, boolean persistent, String text) {
128        this(ServiceInfoImpl.decodeQualifiedNameMap(type, name, subtype), port, weight, priority, persistent, (byte[]) null);
129        _server = text;
130        try {
131            ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
132            writeUTF(out, text);
133            this._text = out.toByteArray();
134        } catch (IOException e) {
135            throw new RuntimeException("unexpected exception: " + e);
136        }
137    }
138
139    /**
140     * @param type
141     * @param name
142     * @param subtype
143     * @param port
144     * @param weight
145     * @param priority
146     * @param persistent
147     * @param props
148     * @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, Map)
149     */
150    public ServiceInfoImpl(String type, String name, String subtype, int port, int weight, int priority, boolean persistent, Map<String, ?> props) {
151        this(ServiceInfoImpl.decodeQualifiedNameMap(type, name, subtype), port, weight, priority, persistent, textFromProperties(props));
152    }
153
154    /**
155     * @param type
156     * @param name
157     * @param subtype
158     * @param port
159     * @param weight
160     * @param priority
161     * @param persistent
162     * @param text
163     * @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, byte[])
164     */
165    public ServiceInfoImpl(String type, String name, String subtype, int port, int weight, int priority, boolean persistent, byte text[]) {
166        this(ServiceInfoImpl.decodeQualifiedNameMap(type, name, subtype), port, weight, priority, persistent, text);
167    }
168
169    public ServiceInfoImpl(Map<Fields, String> qualifiedNameMap, int port, int weight, int priority, boolean persistent, Map<String, ?> props) {
170        this(qualifiedNameMap, port, weight, priority, persistent, textFromProperties(props));
171    }
172
173    ServiceInfoImpl(Map<Fields, String> qualifiedNameMap, int port, int weight, int priority, boolean persistent, String text) {
174        this(qualifiedNameMap, port, weight, priority, persistent, (byte[]) null);
175        _server = text;
176        try {
177            ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
178            writeUTF(out, text);
179            this._text = out.toByteArray();
180        } catch (IOException e) {
181            throw new RuntimeException("unexpected exception: " + e);
182        }
183    }
184
185    ServiceInfoImpl(Map<Fields, String> qualifiedNameMap, int port, int weight, int priority, boolean persistent, byte text[]) {
186        Map<Fields, String> map = ServiceInfoImpl.checkQualifiedNameMap(qualifiedNameMap);
187
188        this._domain = map.get(Fields.Domain);
189        this._protocol = map.get(Fields.Protocol);
190        this._application = map.get(Fields.Application);
191        this._name = map.get(Fields.Instance);
192        this._subtype = map.get(Fields.Subtype);
193
194        this._port = port;
195        this._weight = weight;
196        this._priority = priority;
197        this._text = text;
198        this.setNeedTextAnnouncing(false);
199        this._state = new ServiceInfoState(this);
200        this._persistent = persistent;
201        this._ipv4Addresses = Collections.synchronizedSet(new LinkedHashSet<Inet4Address>());
202        this._ipv6Addresses = Collections.synchronizedSet(new LinkedHashSet<Inet6Address>());
203    }
204
205    /**
206     * During recovery we need to duplicate service info to reregister them
207     *
208     * @param info
209     */
210    ServiceInfoImpl(ServiceInfo info) {
211        this._ipv4Addresses = Collections.synchronizedSet(new LinkedHashSet<Inet4Address>());
212        this._ipv6Addresses = Collections.synchronizedSet(new LinkedHashSet<Inet6Address>());
213        if (info != null) {
214            this._domain = info.getDomain();
215            this._protocol = info.getProtocol();
216            this._application = info.getApplication();
217            this._name = info.getName();
218            this._subtype = info.getSubtype();
219            this._port = info.getPort();
220            this._weight = info.getWeight();
221            this._priority = info.getPriority();
222            this._text = info.getTextBytes();
223            this._persistent = info.isPersistent();
224            Inet6Address[] ipv6Addresses = info.getInet6Addresses();
225            for (Inet6Address address : ipv6Addresses) {
226                this._ipv6Addresses.add(address);
227            }
228            Inet4Address[] ipv4Addresses = info.getInet4Addresses();
229            for (Inet4Address address : ipv4Addresses) {
230                this._ipv4Addresses.add(address);
231            }
232        }
233        this._state = new ServiceInfoState(this);
234    }
235
236    public static Map<Fields, String> decodeQualifiedNameMap(String type, String name, String subtype) {
237        Map<Fields, String> qualifiedNameMap = decodeQualifiedNameMapForType(type);
238
239        qualifiedNameMap.put(Fields.Instance, name);
240        qualifiedNameMap.put(Fields.Subtype, subtype);
241
242        return checkQualifiedNameMap(qualifiedNameMap);
243    }
244
245    public static Map<Fields, String> decodeQualifiedNameMapForType(String type) {
246        int index;
247
248        String casePreservedType = type;
249
250        String aType = type.toLowerCase();
251        String application = aType;
252        String protocol = "";
253        String subtype = "";
254        String name = "";
255        String domain = "";
256
257        if (aType.contains("in-addr.arpa") || aType.contains("ip6.arpa")) {
258            index = (aType.contains("in-addr.arpa") ? aType.indexOf("in-addr.arpa") : aType.indexOf("ip6.arpa"));
259            name = removeSeparators(casePreservedType.substring(0, index));
260            domain = casePreservedType.substring(index);
261            application = "";
262        } else if ((!aType.contains("_")) && aType.contains(".")) {
263            index = aType.indexOf('.');
264            name = removeSeparators(casePreservedType.substring(0, index));
265            domain = removeSeparators(casePreservedType.substring(index));
266            application = "";
267        } else {
268            // First remove the name if it there.
269            if (!aType.startsWith("_") || aType.startsWith("_services")) {
270                index = aType.indexOf('.');
271                if (index > 0) {
272                    // We need to preserve the case for the user readable name.
273                    name = casePreservedType.substring(0, index);
274                    if (index + 1 < aType.length()) {
275                        aType = aType.substring(index + 1);
276                        casePreservedType = casePreservedType.substring(index + 1);
277                    }
278                }
279            }
280
281            index = aType.lastIndexOf("._");
282            if (index > 0) {
283                int start = index + 2;
284                int end = aType.indexOf('.', start);
285                protocol = casePreservedType.substring(start, end);
286            }
287            if (protocol.length() > 0) {
288                index = aType.indexOf("_" + protocol.toLowerCase() + ".");
289                int start = index + protocol.length() + 2;
290                int end = aType.length() - (aType.endsWith(".") ? 1 : 0);
291                domain = casePreservedType.substring(start, end);
292                application = casePreservedType.substring(0, index - 1);
293            }
294            index = application.toLowerCase().indexOf("._sub");
295            if (index > 0) {
296                int start = index + 5;
297                subtype = removeSeparators(application.substring(0, index));
298                application = application.substring(start);
299            }
300        }
301
302        final Map<Fields, String> qualifiedNameMap = new HashMap<Fields, String>(5);
303        qualifiedNameMap.put(Fields.Domain, removeSeparators(domain));
304        qualifiedNameMap.put(Fields.Protocol, protocol);
305        qualifiedNameMap.put(Fields.Application, removeSeparators(application));
306        qualifiedNameMap.put(Fields.Instance, name);
307        qualifiedNameMap.put(Fields.Subtype, subtype);
308
309        return qualifiedNameMap;
310    }
311
312    protected static Map<Fields, String> checkQualifiedNameMap(Map<Fields, String> qualifiedNameMap) {
313        Map<Fields, String> checkedQualifiedNameMap = new HashMap<Fields, String>(5);
314
315        // Optional domain
316        String domain = (qualifiedNameMap.containsKey(Fields.Domain) ? qualifiedNameMap.get(Fields.Domain) : "local");
317        if ((domain == null) || (domain.length() == 0)) {
318            domain = "local";
319        }
320        domain = removeSeparators(domain);
321        checkedQualifiedNameMap.put(Fields.Domain, domain);
322        // Optional protocol
323        String protocol = (qualifiedNameMap.containsKey(Fields.Protocol) ? qualifiedNameMap.get(Fields.Protocol) : "tcp");
324        if ((protocol == null) || (protocol.length() == 0)) {
325            protocol = "tcp";
326        }
327        protocol = removeSeparators(protocol);
328        checkedQualifiedNameMap.put(Fields.Protocol, protocol);
329        // Application
330        String application = (qualifiedNameMap.containsKey(Fields.Application) ? qualifiedNameMap.get(Fields.Application) : "");
331        if ((application == null) || (application.length() == 0)) {
332            application = "";
333        }
334        application = removeSeparators(application);
335        checkedQualifiedNameMap.put(Fields.Application, application);
336        // Instance
337        String instance = (qualifiedNameMap.containsKey(Fields.Instance) ? qualifiedNameMap.get(Fields.Instance) : "");
338        if ((instance == null) || (instance.length() == 0)) {
339            instance = "";
340            // throw new IllegalArgumentException("The instance name component of a fully qualified service cannot be empty.");
341        }
342        instance = removeSeparators(instance);
343        checkedQualifiedNameMap.put(Fields.Instance, instance);
344        // Optional Subtype
345        String subtype = (qualifiedNameMap.containsKey(Fields.Subtype) ? qualifiedNameMap.get(Fields.Subtype) : "");
346        if ((subtype == null) || (subtype.length() == 0)) {
347            subtype = "";
348        }
349        subtype = removeSeparators(subtype);
350        checkedQualifiedNameMap.put(Fields.Subtype, subtype);
351
352        return checkedQualifiedNameMap;
353    }
354
355    private static String removeSeparators(String name) {
356        if (name == null) {
357            return "";
358        }
359        String newName = name.trim();
360        if (newName.startsWith(".")) {
361            newName = newName.substring(1);
362        }
363        if (newName.startsWith("_")) {
364            newName = newName.substring(1);
365        }
366        if (newName.endsWith(".")) {
367            newName = newName.substring(0, newName.length() - 1);
368        }
369        return newName;
370    }
371
372    /**
373     * {@inheritDoc}
374     */
375    @Override
376    public String getType() {
377        String domain = this.getDomain();
378        String protocol = this.getProtocol();
379        String application = this.getApplication();
380        return (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + ".";
381    }
382
383    /**
384     * {@inheritDoc}
385     */
386    @Override
387    public String getTypeWithSubtype() {
388        String subtype = this.getSubtype();
389        return (subtype.length() > 0 ? "_" + subtype.toLowerCase() + "._sub." : "") + this.getType();
390    }
391
392    /**
393     * {@inheritDoc}
394     */
395    @Override
396    public String getName() {
397        return (_name != null ? _name : "");
398    }
399
400    /**
401     * {@inheritDoc}
402     */
403    @Override
404    public String getKey() {
405        if (this._key == null) {
406            this._key = this.getQualifiedName().toLowerCase();
407        }
408        return this._key;
409    }
410
411    /**
412     * Sets the service instance name.
413     *
414     * @param name
415     *            unqualified service instance name, such as <code>foobar</code>
416     */
417    void setName(String name) {
418        this._name = name;
419        this._key = null;
420    }
421
422    /**
423     * {@inheritDoc}
424     */
425    @Override
426    public String getQualifiedName() {
427        String domain = this.getDomain();
428        String protocol = this.getProtocol();
429        String application = this.getApplication();
430        String instance = this.getName();
431        // String subtype = this.getSubtype();
432        // return (instance.length() > 0 ? instance + "." : "") + (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + (subtype.length() > 0 ? ",_" + subtype.toLowerCase() + "." : ".") : "") + domain
433        // + ".";
434        return (instance.length() > 0 ? instance + "." : "") + (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + ".";
435    }
436
437    /**
438     * @see javax.jmdns.ServiceInfo#getServer()
439     */
440    @Override
441    public String getServer() {
442        return (_server != null ? _server : "");
443    }
444
445    /**
446     * @param server
447     *            the server to set
448     */
449    void setServer(String server) {
450        this._server = server;
451    }
452
453    /**
454     * {@inheritDoc}
455     */
456    @Deprecated
457    @Override
458    public String getHostAddress() {
459        String[] names = this.getHostAddresses();
460        return (names.length > 0 ? names[0] : "");
461    }
462
463    /**
464     * {@inheritDoc}
465     */
466    @Override
467    public String[] getHostAddresses() {
468        Inet4Address[] ip4Aaddresses = this.getInet4Addresses();
469        Inet6Address[] ip6Aaddresses = this.getInet6Addresses();
470        String[] names = new String[ip4Aaddresses.length + ip6Aaddresses.length];
471        for (int i = 0; i < ip4Aaddresses.length; i++) {
472            names[i] = ip4Aaddresses[i].getHostAddress();
473        }
474        for (int i = 0; i < ip6Aaddresses.length; i++) {
475            names[i + ip4Aaddresses.length] = "[" + ip6Aaddresses[i].getHostAddress() + "]";
476        }
477        return names;
478    }
479
480    /**
481     * @param addr
482     *            the addr to add
483     */
484    void addAddress(Inet4Address addr) {
485        _ipv4Addresses.add(addr);
486    }
487
488    /**
489     * @param addr
490     *            the addr to add
491     */
492    void addAddress(Inet6Address addr) {
493        _ipv6Addresses.add(addr);
494    }
495
496    /**
497     * {@inheritDoc}
498     */
499    @Deprecated
500    @Override
501    public InetAddress getAddress() {
502        return this.getInetAddress();
503    }
504
505    /**
506     * {@inheritDoc}
507     */
508    @Deprecated
509    @Override
510    public InetAddress getInetAddress() {
511        InetAddress[] addresses = this.getInetAddresses();
512        return (addresses.length > 0 ? addresses[0] : null);
513    }
514
515    /**
516     * {@inheritDoc}
517     */
518    @Deprecated
519    @Override
520    public Inet4Address getInet4Address() {
521        Inet4Address[] addresses = this.getInet4Addresses();
522        return (addresses.length > 0 ? addresses[0] : null);
523    }
524
525    /**
526     * {@inheritDoc}
527     */
528    @Deprecated
529    @Override
530    public Inet6Address getInet6Address() {
531        Inet6Address[] addresses = this.getInet6Addresses();
532        return (addresses.length > 0 ? addresses[0] : null);
533    }
534
535    /*
536     * (non-Javadoc)
537     * @see javax.jmdns.ServiceInfo#getInetAddresses()
538     */
539    @Override
540    public InetAddress[] getInetAddresses() {
541        List<InetAddress> aList = new ArrayList<InetAddress>(_ipv4Addresses.size() + _ipv6Addresses.size());
542        aList.addAll(_ipv4Addresses);
543        aList.addAll(_ipv6Addresses);
544        return aList.toArray(new InetAddress[aList.size()]);
545    }
546
547    /*
548     * (non-Javadoc)
549     * @see javax.jmdns.ServiceInfo#getInet4Addresses()
550     */
551    @Override
552    public Inet4Address[] getInet4Addresses() {
553        return _ipv4Addresses.toArray(new Inet4Address[_ipv4Addresses.size()]);
554    }
555
556    /*
557     * (non-Javadoc)
558     * @see javax.jmdns.ServiceInfo#getInet6Addresses()
559     */
560    @Override
561    public Inet6Address[] getInet6Addresses() {
562        return _ipv6Addresses.toArray(new Inet6Address[_ipv6Addresses.size()]);
563    }
564
565    /**
566     * @see javax.jmdns.ServiceInfo#getPort()
567     */
568    @Override
569    public int getPort() {
570        return _port;
571    }
572
573    /**
574     * @see javax.jmdns.ServiceInfo#getPriority()
575     */
576    @Override
577    public int getPriority() {
578        return _priority;
579    }
580
581    /**
582     * @see javax.jmdns.ServiceInfo#getWeight()
583     */
584    @Override
585    public int getWeight() {
586        return _weight;
587    }
588
589    /**
590     * @see javax.jmdns.ServiceInfo#getTextBytes()
591     */
592    @Override
593    public byte[] getTextBytes() {
594        return (this._text != null && this._text.length > 0 ? this._text : DNSRecord.EMPTY_TXT);
595    }
596
597    /**
598     * {@inheritDoc}
599     */
600    @Deprecated
601    @Override
602    public String getTextString() {
603        Map<String, byte[]> properties = this.getProperties();
604        for (String key : properties.keySet()) {
605            byte[] value = properties.get(key);
606            if ((value != null) && (value.length > 0)) {
607                return key + "=" + new String(value);
608            }
609            return key;
610        }
611        return "";
612    }
613
614    /*
615     * (non-Javadoc)
616     * @see javax.jmdns.ServiceInfo#getURL()
617     */
618    @Deprecated
619    @Override
620    public String getURL() {
621        return this.getURL("http");
622    }
623
624    /*
625     * (non-Javadoc)
626     * @see javax.jmdns.ServiceInfo#getURLs()
627     */
628    @Override
629    public String[] getURLs() {
630        return this.getURLs("http");
631    }
632
633    /*
634     * (non-Javadoc)
635     * @see javax.jmdns.ServiceInfo#getURL(java.lang.String)
636     */
637    @Deprecated
638    @Override
639    public String getURL(String protocol) {
640        String[] urls = this.getURLs(protocol);
641        return (urls.length > 0 ? urls[0] : protocol + "://null:" + getPort());
642    }
643
644    /*
645     * (non-Javadoc)
646     * @see javax.jmdns.ServiceInfo#getURLs(java.lang.String)
647     */
648    @Override
649    public String[] getURLs(String protocol) {
650        InetAddress[] addresses = this.getInetAddresses();
651        String[] urls = new String[addresses.length];
652        for (int i = 0; i < addresses.length; i++) {
653            String url = protocol + "://" + addresses[i].getHostAddress() + ":" + getPort();
654            String path = getPropertyString("path");
655            if (path != null) {
656                if (path.indexOf("://") >= 0) {
657                    url = path;
658                } else {
659                    url += path.startsWith("/") ? path : "/" + path;
660                }
661            }
662            urls[i] = url;
663        }
664        return urls;
665    }
666
667    /**
668     * {@inheritDoc}
669     */
670    @Override
671    public synchronized byte[] getPropertyBytes(String name) {
672        return this.getProperties().get(name);
673    }
674
675    /**
676     * {@inheritDoc}
677     */
678    @Override
679    public synchronized String getPropertyString(String name) {
680        byte data[] = this.getProperties().get(name);
681        if (data == null) {
682            return null;
683        }
684        if (data == NO_VALUE) {
685            return "true";
686        }
687        return readUTF(data, 0, data.length);
688    }
689
690    /**
691     * {@inheritDoc}
692     */
693    @Override
694    public Enumeration<String> getPropertyNames() {
695        Map<String, byte[]> properties = this.getProperties();
696        Collection<String> names = (properties != null ? properties.keySet() : Collections.<String> emptySet());
697        return new Vector<String>(names).elements();
698    }
699
700    /**
701     * {@inheritDoc}
702     */
703    @Override
704    public String getApplication() {
705        return (_application != null ? _application : "");
706    }
707
708    /**
709     * {@inheritDoc}
710     */
711    @Override
712    public String getDomain() {
713        return (_domain != null ? _domain : "local");
714    }
715
716    /**
717     * {@inheritDoc}
718     */
719    @Override
720    public String getProtocol() {
721        return (_protocol != null ? _protocol : "tcp");
722    }
723
724    /**
725     * {@inheritDoc}
726     */
727    @Override
728    public String getSubtype() {
729        return (_subtype != null ? _subtype : "");
730    }
731
732    /**
733     * {@inheritDoc}
734     */
735    @Override
736    public Map<Fields, String> getQualifiedNameMap() {
737        Map<Fields, String> map = new HashMap<Fields, String>(5);
738
739        map.put(Fields.Domain, this.getDomain());
740        map.put(Fields.Protocol, this.getProtocol());
741        map.put(Fields.Application, this.getApplication());
742        map.put(Fields.Instance, this.getName());
743        map.put(Fields.Subtype, this.getSubtype());
744        return map;
745    }
746
747    /**
748     * Write a UTF string with a length to a stream.
749     */
750    static void writeUTF(OutputStream out, String str) throws IOException {
751        for (int i = 0, len = str.length(); i < len; i++) {
752            int c = str.charAt(i);
753            if ((c >= 0x0001) && (c <= 0x007F)) {
754                out.write(c);
755            } else {
756                if (c > 0x07FF) {
757                    out.write(0xE0 | ((c >> 12) & 0x0F));
758                    out.write(0x80 | ((c >> 6) & 0x3F));
759                    out.write(0x80 | ((c >> 0) & 0x3F));
760                } else {
761                    out.write(0xC0 | ((c >> 6) & 0x1F));
762                    out.write(0x80 | ((c >> 0) & 0x3F));
763                }
764            }
765        }
766    }
767
768    /**
769     * Read data bytes as a UTF stream.
770     */
771    String readUTF(byte data[], int off, int len) {
772        int offset = off;
773        StringBuffer buf = new StringBuffer();
774        for (int end = offset + len; offset < end;) {
775            int ch = data[offset++] & 0xFF;
776            switch (ch >> 4) {
777                case 0:
778                case 1:
779                case 2:
780                case 3:
781                case 4:
782                case 5:
783                case 6:
784                case 7:
785                    // 0xxxxxxx
786                    break;
787                case 12:
788                case 13:
789                    if (offset >= len) {
790                        return null;
791                    }
792                    // 110x xxxx 10xx xxxx
793                    ch = ((ch & 0x1F) << 6) | (data[offset++] & 0x3F);
794                    break;
795                case 14:
796                    if (offset + 2 >= len) {
797                        return null;
798                    }
799                    // 1110 xxxx 10xx xxxx 10xx xxxx
800                    ch = ((ch & 0x0f) << 12) | ((data[offset++] & 0x3F) << 6) | (data[offset++] & 0x3F);
801                    break;
802                default:
803                    if (offset + 1 >= len) {
804                        return null;
805                    }
806                    // 10xx xxxx, 1111 xxxx
807                    ch = ((ch & 0x3F) << 4) | (data[offset++] & 0x0f);
808                    break;
809            }
810            buf.append((char) ch);
811        }
812        return buf.toString();
813    }
814
815    synchronized Map<String, byte[]> getProperties() {
816        if ((_props == null) && (this.getTextBytes() != null)) {
817            Hashtable<String, byte[]> properties = new Hashtable<String, byte[]>();
818            try {
819                int off = 0;
820                while (off < getTextBytes().length) {
821                    // length of the next key value pair
822                    int len = getTextBytes()[off++] & 0xFF;
823                    if ((len == 0) || (off + len > getTextBytes().length)) {
824                        properties.clear();
825                        break;
826                    }
827                    // look for the '='
828                    int i = 0;
829                    for (; (i < len) && (getTextBytes()[off + i] != '='); i++) {
830                        /* Stub */
831                    }
832
833                    // get the property name
834                    String name = readUTF(getTextBytes(), off, i);
835                    if (name == null) {
836                        properties.clear();
837                        break;
838                    }
839                    if (i == len) {
840                        properties.put(name, NO_VALUE);
841                    } else {
842                        byte value[] = new byte[len - ++i];
843                        System.arraycopy(getTextBytes(), off + i, value, 0, len - i);
844                        properties.put(name, value);
845                        off += len;
846                    }
847                }
848            } catch (Exception exception) {
849                // We should get better logging.
850                logger.log(Level.WARNING, "Malformed TXT Field ", exception);
851            }
852            this._props = properties;
853        }
854        return (_props != null ? _props : Collections.<String, byte[]> emptyMap());
855    }
856
857    /**
858     * JmDNS callback to update a DNS record.
859     *
860     * @param dnsCache
861     * @param now
862     * @param rec
863     */
864    @Override
865    public void updateRecord(DNSCache dnsCache, long now, DNSEntry rec) {
866        if ((rec instanceof DNSRecord) && !rec.isExpired(now)) {
867            boolean serviceUpdated = false;
868            switch (rec.getRecordType()) {
869                case TYPE_A: // IPv4
870                    if (rec.getName().equalsIgnoreCase(this.getServer())) {
871                        _ipv4Addresses.add((Inet4Address) ((DNSRecord.Address) rec).getAddress());
872                        serviceUpdated = true;
873                    }
874                    break;
875                case TYPE_AAAA: // IPv6
876                    if (rec.getName().equalsIgnoreCase(this.getServer())) {
877                        _ipv6Addresses.add((Inet6Address) ((DNSRecord.Address) rec).getAddress());
878                        serviceUpdated = true;
879                    }
880                    break;
881                case TYPE_SRV:
882                    if (rec.getName().equalsIgnoreCase(this.getQualifiedName())) {
883                        DNSRecord.Service srv = (DNSRecord.Service) rec;
884                        boolean serverChanged = (_server == null) || !_server.equalsIgnoreCase(srv.getServer());
885                        _server = srv.getServer();
886                        _port = srv.getPort();
887                        _weight = srv.getWeight();
888                        _priority = srv.getPriority();
889                        if (serverChanged) {
890                            _ipv4Addresses.clear();
891                            _ipv6Addresses.clear();
892                            for (DNSEntry entry : dnsCache.getDNSEntryList(_server, DNSRecordType.TYPE_A, DNSRecordClass.CLASS_IN)) {
893                                this.updateRecord(dnsCache, now, entry);
894                            }
895                            for (DNSEntry entry : dnsCache.getDNSEntryList(_server, DNSRecordType.TYPE_AAAA, DNSRecordClass.CLASS_IN)) {
896                                this.updateRecord(dnsCache, now, entry);
897                            }
898                            // We do not want to trigger the listener in this case as it will be triggered if the address resolves.
899                        } else {
900                            serviceUpdated = true;
901                        }
902                    }
903                    break;
904                case TYPE_TXT:
905                    if (rec.getName().equalsIgnoreCase(this.getQualifiedName())) {
906                        DNSRecord.Text txt = (DNSRecord.Text) rec;
907                        _text = txt.getText();
908                        _props = null; // set it null for apply update text data
909                        serviceUpdated = true;
910                    }
911                    break;
912                case TYPE_PTR:
913                    if ((this.getSubtype().length() == 0) && (rec.getSubtype().length() != 0)) {
914                        _subtype = rec.getSubtype();
915                        serviceUpdated = true;
916                    }
917                    break;
918                default:
919                    break;
920            }
921            if (serviceUpdated && this.hasData()) {
922                JmDNSImpl dns = this.getDns();
923                if (dns != null) {
924                    ServiceEvent event = ((DNSRecord) rec).getServiceEvent(dns);
925                    event = new ServiceEventImpl(dns, event.getType(), event.getName(), this);
926                    dns.handleServiceResolved(event);
927                }
928            }
929            // This is done, to notify the wait loop in method JmDNS.waitForInfoData(ServiceInfo info, int timeout);
930            synchronized (this) {
931                this.notifyAll();
932            }
933        }
934    }
935
936    /**
937     * Returns true if the service info is filled with data.
938     *
939     * @return <code>true</code> if the service info has data, <code>false</code> otherwise.
940     */
941    @Override
942    public synchronized boolean hasData() {
943        return this.getServer() != null && this.hasInetAddress() && this.getTextBytes() != null && this.getTextBytes().length > 0;
944        // return this.getServer() != null && (this.getAddress() != null || (this.getTextBytes() != null && this.getTextBytes().length > 0));
945    }
946
947    private final boolean hasInetAddress() {
948        return _ipv4Addresses.size() > 0 || _ipv6Addresses.size() > 0;
949    }
950
951    // State machine
952
953    /**
954     * {@inheritDoc}
955     */
956    @Override
957    public boolean advanceState(DNSTask task) {
958        return _state.advanceState(task);
959    }
960
961    /**
962     * {@inheritDoc}
963     */
964    @Override
965    public boolean revertState() {
966        return _state.revertState();
967    }
968
969    /**
970     * {@inheritDoc}
971     */
972    @Override
973    public boolean cancelState() {
974        return _state.cancelState();
975    }
976
977    /**
978     * {@inheritDoc}
979     */
980    @Override
981    public boolean closeState() {
982        return this._state.closeState();
983    }
984
985    /**
986     * {@inheritDoc}
987     */
988    @Override
989    public boolean recoverState() {
990        return this._state.recoverState();
991    }
992
993    /**
994     * {@inheritDoc}
995     */
996    @Override
997    public void removeAssociationWithTask(DNSTask task) {
998        _state.removeAssociationWithTask(task);
999    }
1000
1001    /**
1002     * {@inheritDoc}
1003     */
1004    @Override
1005    public void associateWithTask(DNSTask task, DNSState state) {
1006        _state.associateWithTask(task, state);
1007    }
1008
1009    /**
1010     * {@inheritDoc}
1011     */
1012    @Override
1013    public boolean isAssociatedWithTask(DNSTask task, DNSState state) {
1014        return _state.isAssociatedWithTask(task, state);
1015    }
1016
1017    /**
1018     * {@inheritDoc}
1019     */
1020    @Override
1021    public boolean isProbing() {
1022        return _state.isProbing();
1023    }
1024
1025    /**
1026     * {@inheritDoc}
1027     */
1028    @Override
1029    public boolean isAnnouncing() {
1030        return _state.isAnnouncing();
1031    }
1032
1033    /**
1034     * {@inheritDoc}
1035     */
1036    @Override
1037    public boolean isAnnounced() {
1038        return _state.isAnnounced();
1039    }
1040
1041    /**
1042     * {@inheritDoc}
1043     */
1044    @Override
1045    public boolean isCanceling() {
1046        return this._state.isCanceling();
1047    }
1048
1049    /**
1050     * {@inheritDoc}
1051     */
1052    @Override
1053    public boolean isCanceled() {
1054        return _state.isCanceled();
1055    }
1056
1057    /**
1058     * {@inheritDoc}
1059     */
1060    @Override
1061    public boolean isClosing() {
1062        return _state.isClosing();
1063    }
1064
1065    /**
1066     * {@inheritDoc}
1067     */
1068    @Override
1069    public boolean isClosed() {
1070        return _state.isClosed();
1071    }
1072
1073    /**
1074     * {@inheritDoc}
1075     */
1076    @Override
1077    public boolean waitForAnnounced(long timeout) {
1078        return _state.waitForAnnounced(timeout);
1079    }
1080
1081    /**
1082     * {@inheritDoc}
1083     */
1084    @Override
1085    public boolean waitForCanceled(long timeout) {
1086        return _state.waitForCanceled(timeout);
1087    }
1088
1089    /**
1090     * {@inheritDoc}
1091     */
1092    @Override
1093    public int hashCode() {
1094        return getQualifiedName().hashCode();
1095    }
1096
1097    /**
1098     * {@inheritDoc}
1099     */
1100    @Override
1101    public boolean equals(Object obj) {
1102        return (obj instanceof ServiceInfoImpl) && getQualifiedName().equals(((ServiceInfoImpl) obj).getQualifiedName());
1103    }
1104
1105    /**
1106     * {@inheritDoc}
1107     */
1108    @Override
1109    public String getNiceTextString() {
1110        StringBuffer buf = new StringBuffer();
1111        for (int i = 0, len = this.getTextBytes().length; i < len; i++) {
1112            if (i >= 200) {
1113                buf.append("...");
1114                break;
1115            }
1116            int ch = getTextBytes()[i] & 0xFF;
1117            if ((ch < ' ') || (ch > 127)) {
1118                buf.append("\\0");
1119                buf.append(Integer.toString(ch, 8));
1120            } else {
1121                buf.append((char) ch);
1122            }
1123        }
1124        return buf.toString();
1125    }
1126
1127    /*
1128     * (non-Javadoc)
1129     * @see javax.jmdns.ServiceInfo#clone()
1130     */
1131    @Override
1132    public ServiceInfoImpl clone() {
1133        ServiceInfoImpl serviceInfo = new ServiceInfoImpl(this.getQualifiedNameMap(), _port, _weight, _priority, _persistent, _text);
1134        Inet6Address[] ipv6Addresses = this.getInet6Addresses();
1135        for (Inet6Address address : ipv6Addresses) {
1136            serviceInfo._ipv6Addresses.add(address);
1137        }
1138        Inet4Address[] ipv4Addresses = this.getInet4Addresses();
1139        for (Inet4Address address : ipv4Addresses) {
1140            serviceInfo._ipv4Addresses.add(address);
1141        }
1142        return serviceInfo;
1143    }
1144
1145    /**
1146     * {@inheritDoc}
1147     */
1148    @Override
1149    public String toString() {
1150        StringBuilder buf = new StringBuilder();
1151        buf.append("[" + this.getClass().getSimpleName() + "@" + System.identityHashCode(this) + " ");
1152        buf.append("name: '");
1153        buf.append((this.getName().length() > 0 ? this.getName() + "." : "") + this.getTypeWithSubtype());
1154        buf.append("' address: '");
1155        InetAddress[] addresses = this.getInetAddresses();
1156        if (addresses.length > 0) {
1157            for (InetAddress address : addresses) {
1158                buf.append(address);
1159                buf.append(':');
1160                buf.append(this.getPort());
1161                buf.append(' ');
1162            }
1163        } else {
1164            buf.append("(null):");
1165            buf.append(this.getPort());
1166        }
1167        buf.append("' status: '");
1168        buf.append(_state.toString());
1169        buf.append(this.isPersistent() ? "' is persistent," : "',");
1170        buf.append(" has ");
1171        buf.append(this.hasData() ? "" : "NO ");
1172        buf.append("data");
1173        if (this.getTextBytes().length > 0) {
1174            // buf.append("\n");
1175            // buf.append(this.getNiceTextString());
1176            Map<String, byte[]> properties = this.getProperties();
1177            if (!properties.isEmpty()) {
1178                buf.append("\n");
1179                for (String key : properties.keySet()) {
1180                    buf.append("\t" + key + ": " + new String(properties.get(key)) + "\n");
1181                }
1182            } else {
1183                buf.append(" empty");
1184            }
1185        }
1186        buf.append(']');
1187        return buf.toString();
1188    }
1189
1190    public Collection<DNSRecord> answers(boolean unique, int ttl, HostInfo localHost) {
1191        List<DNSRecord> list = new ArrayList<DNSRecord>();
1192        if (this.getSubtype().length() > 0) {
1193            list.add(new Pointer(this.getTypeWithSubtype(), DNSRecordClass.CLASS_IN, DNSRecordClass.NOT_UNIQUE, ttl, this.getQualifiedName()));
1194        }
1195        list.add(new Pointer(this.getType(), DNSRecordClass.CLASS_IN, DNSRecordClass.NOT_UNIQUE, ttl, this.getQualifiedName()));
1196        list.add(new Service(this.getQualifiedName(), DNSRecordClass.CLASS_IN, unique, ttl, _priority, _weight, _port, localHost.getName()));
1197        list.add(new Text(this.getQualifiedName(), DNSRecordClass.CLASS_IN, unique, ttl, this.getTextBytes()));
1198        return list;
1199    }
1200
1201    /**
1202     * {@inheritDoc}
1203     */
1204    @Override
1205    public void setText(byte[] text) throws IllegalStateException {
1206        synchronized (this) {
1207            this._text = text;
1208            this._props = null;
1209            this.setNeedTextAnnouncing(true);
1210        }
1211    }
1212
1213    /**
1214     * {@inheritDoc}
1215     */
1216    @Override
1217    public void setText(Map<String, ?> props) throws IllegalStateException {
1218        this.setText(textFromProperties(props));
1219    }
1220
1221    /**
1222     * This is used internally by the framework
1223     *
1224     * @param text
1225     */
1226    void _setText(byte[] text) {
1227        this._text = text;
1228        this._props = null;
1229    }
1230
1231    private static byte[] textFromProperties(Map<String, ?> props) {
1232        byte[] text = null;
1233        if (props != null) {
1234            try {
1235                ByteArrayOutputStream out = new ByteArrayOutputStream(256);
1236                for (String key : props.keySet()) {
1237                    Object val = props.get(key);
1238                    ByteArrayOutputStream out2 = new ByteArrayOutputStream(100);
1239                    writeUTF(out2, key);
1240                    if (val == null) {
1241                        // Skip
1242                    } else if (val instanceof String) {
1243                        out2.write('=');
1244                        writeUTF(out2, (String) val);
1245                    } else if (val instanceof byte[]) {
1246                        byte[] bval = (byte[]) val;
1247                        if (bval.length > 0) {
1248                            out2.write('=');
1249                            out2.write(bval, 0, bval.length);
1250                        } else {
1251                            val = null;
1252                        }
1253                    } else {
1254                        throw new IllegalArgumentException("invalid property value: " + val);
1255                    }
1256                    byte data[] = out2.toByteArray();
1257                    if (data.length > 255) {
1258                        throw new IOException("Cannot have individual values larger that 255 chars. Offending value: " + key + (val != null ? "" : "=" + val));
1259                    }
1260                    out.write((byte) data.length);
1261                    out.write(data, 0, data.length);
1262                }
1263                text = out.toByteArray();
1264            } catch (IOException e) {
1265                throw new RuntimeException("unexpected exception: " + e);
1266            }
1267        }
1268        return (text != null && text.length > 0 ? text : DNSRecord.EMPTY_TXT);
1269    }
1270
1271    public void setDns(JmDNSImpl dns) {
1272        this._state.setDns(dns);
1273    }
1274
1275    /**
1276     * {@inheritDoc}
1277     */
1278    @Override
1279    public JmDNSImpl getDns() {
1280        return this._state.getDns();
1281    }
1282
1283    /**
1284     * {@inheritDoc}
1285     */
1286    @Override
1287    public boolean isPersistent() {
1288        return _persistent;
1289    }
1290
1291    /**
1292     * @param needTextAnnouncing
1293     *            the needTextAnnouncing to set
1294     */
1295    public void setNeedTextAnnouncing(boolean needTextAnnouncing) {
1296        this._needTextAnnouncing = needTextAnnouncing;
1297        if (this._needTextAnnouncing) {
1298            _state.setTask(null);
1299        }
1300    }
1301
1302    /**
1303     * @return the needTextAnnouncing
1304     */
1305    public boolean needTextAnnouncing() {
1306        return _needTextAnnouncing;
1307    }
1308
1309    /**
1310     * @return the delegate
1311     */
1312    Delegate getDelegate() {
1313        return this._delegate;
1314    }
1315
1316    /**
1317     * @param delegate
1318     *            the delegate to set
1319     */
1320    void setDelegate(Delegate delegate) {
1321        this._delegate = delegate;
1322    }
1323
1324}
1325