1/**
2 *
3 */
4package javax.jmdns.impl;
5
6import java.util.EventListener;
7import java.util.concurrent.ConcurrentHashMap;
8import java.util.concurrent.ConcurrentMap;
9import java.util.logging.Logger;
10
11import javax.jmdns.JmDNS;
12import javax.jmdns.ServiceEvent;
13import javax.jmdns.ServiceInfo;
14import javax.jmdns.ServiceListener;
15import javax.jmdns.ServiceTypeListener;
16
17/**
18 * This class track the status of listener.<br/>
19 * The main purpose of this class is to collapse consecutive events so that we can guarantee the correct call back sequence.
20 *
21 * @param <T>
22 *            listener type
23 */
24public class ListenerStatus<T extends EventListener> {
25
26    public static class ServiceListenerStatus extends ListenerStatus<ServiceListener> {
27        private static Logger                            logger = Logger.getLogger(ServiceListenerStatus.class.getName());
28
29        private final ConcurrentMap<String, ServiceInfo> _addedServices;
30
31        /**
32         * @param listener
33         *            listener being tracked.
34         * @param synch
35         *            true if that listener can be called asynchronously
36         */
37        public ServiceListenerStatus(ServiceListener listener, boolean synch) {
38            super(listener, synch);
39            _addedServices = new ConcurrentHashMap<String, ServiceInfo>(32);
40        }
41
42        /**
43         * A service has been added.<br/>
44         * <b>Note:</b>This event is only the service added event. The service info associated with this event does not include resolution information.<br/>
45         * To get the full resolved information you need to listen to {@link #serviceResolved(ServiceEvent)} or call {@link JmDNS#getServiceInfo(String, String, long)}
46         *
47         * <pre>
48         *  ServiceInfo info = event.getDNS().getServiceInfo(event.getType(), event.getName())
49         * </pre>
50         * <p>
51         * Please note that service resolution may take a few second to resolve.
52         * </p>
53         *
54         * @param event
55         *            The ServiceEvent providing the name and fully qualified type of the service.
56         */
57        void serviceAdded(ServiceEvent event) {
58            String qualifiedName = event.getName() + "." + event.getType();
59            if (null == _addedServices.putIfAbsent(qualifiedName, event.getInfo().clone())) {
60                this.getListener().serviceAdded(event);
61                ServiceInfo info = event.getInfo();
62                if ((info != null) && (info.hasData())) {
63                    this.getListener().serviceResolved(event);
64                }
65            } else {
66                logger.finer("Service Added called for a service already added: " + event);
67            }
68        }
69
70        /**
71         * A service has been removed.
72         *
73         * @param event
74         *            The ServiceEvent providing the name and fully qualified type of the service.
75         */
76        void serviceRemoved(ServiceEvent event) {
77            String qualifiedName = event.getName() + "." + event.getType();
78            if (_addedServices.remove(qualifiedName, _addedServices.get(qualifiedName))) {
79                this.getListener().serviceRemoved(event);
80            } else {
81                logger.finer("Service Removed called for a service already removed: " + event);
82            }
83        }
84
85        /**
86         * A service has been resolved. Its details are now available in the ServiceInfo record.<br/>
87         * <b>Note:</b>This call back will never be called if the service does not resolve.<br/>
88         *
89         * @param event
90         *            The ServiceEvent providing the name, the fully qualified type of the service, and the service info record.
91         */
92        synchronized void serviceResolved(ServiceEvent event) {
93            ServiceInfo info = event.getInfo();
94            if ((info != null) && (info.hasData())) {
95                String qualifiedName = event.getName() + "." + event.getType();
96                ServiceInfo previousServiceInfo = _addedServices.get(qualifiedName);
97                if (!_sameInfo(info, previousServiceInfo)) {
98                    if (null == previousServiceInfo) {
99                        if (null == _addedServices.putIfAbsent(qualifiedName, info.clone())) {
100                            this.getListener().serviceResolved(event);
101                        }
102                    } else {
103                        if (_addedServices.replace(qualifiedName, previousServiceInfo, info.clone())) {
104                            this.getListener().serviceResolved(event);
105                        }
106                    }
107                } else {
108                    logger.finer("Service Resolved called for a service already resolved: " + event);
109                }
110            } else {
111                logger.warning("Service Resolved called for an unresolved event: " + event);
112
113            }
114        }
115
116        private static final boolean _sameInfo(ServiceInfo info, ServiceInfo lastInfo) {
117            if (info == null) return false;
118            if (lastInfo == null) return false;
119            if (!info.equals(lastInfo)) return false;
120            byte[] text = info.getTextBytes();
121            byte[] lastText = lastInfo.getTextBytes();
122            if (text.length != lastText.length) return false;
123            for (int i = 0; i < text.length; i++) {
124                if (text[i] != lastText[i]) return false;
125            }
126            return true;
127        }
128
129        /*
130         * (non-Javadoc)
131         * @see java.lang.Object#toString()
132         */
133        @Override
134        public String toString() {
135            StringBuilder aLog = new StringBuilder(2048);
136            aLog.append("[Status for ");
137            aLog.append(this.getListener().toString());
138            if (_addedServices.isEmpty()) {
139                aLog.append(" no type event ");
140            } else {
141                aLog.append(" (");
142                for (String service : _addedServices.keySet()) {
143                    aLog.append(service + ", ");
144                }
145                aLog.append(") ");
146            }
147            aLog.append("]");
148            return aLog.toString();
149        }
150
151    }
152
153    public static class ServiceTypeListenerStatus extends ListenerStatus<ServiceTypeListener> {
154        private static Logger                       logger = Logger.getLogger(ServiceTypeListenerStatus.class.getName());
155
156        private final ConcurrentMap<String, String> _addedTypes;
157
158        /**
159         * @param listener
160         *            listener being tracked.
161         * @param synch
162         *            true if that listener can be called asynchronously
163         */
164        public ServiceTypeListenerStatus(ServiceTypeListener listener, boolean synch) {
165            super(listener, synch);
166            _addedTypes = new ConcurrentHashMap<String, String>(32);
167        }
168
169        /**
170         * A new service type was discovered.
171         *
172         * @param event
173         *            The service event providing the fully qualified type of the service.
174         */
175        void serviceTypeAdded(ServiceEvent event) {
176            if (null == _addedTypes.putIfAbsent(event.getType(), event.getType())) {
177                this.getListener().serviceTypeAdded(event);
178            } else {
179                logger.finest("Service Type Added called for a service type already added: " + event);
180            }
181        }
182
183        /**
184         * A new subtype for the service type was discovered.
185         *
186         * <pre>
187         * &lt;sub&gt;._sub.&lt;app&gt;.&lt;protocol&gt;.&lt;servicedomain&gt;.&lt;parentdomain&gt;.
188         * </pre>
189         *
190         * @param event
191         *            The service event providing the fully qualified type of the service with subtype.
192         */
193        void subTypeForServiceTypeAdded(ServiceEvent event) {
194            if (null == _addedTypes.putIfAbsent(event.getType(), event.getType())) {
195                this.getListener().subTypeForServiceTypeAdded(event);
196            } else {
197                logger.finest("Service Sub Type Added called for a service sub type already added: " + event);
198            }
199        }
200
201        /*
202         * (non-Javadoc)
203         * @see java.lang.Object#toString()
204         */
205        @Override
206        public String toString() {
207            StringBuilder aLog = new StringBuilder(2048);
208            aLog.append("[Status for ");
209            aLog.append(this.getListener().toString());
210            if (_addedTypes.isEmpty()) {
211                aLog.append(" no type event ");
212            } else {
213                aLog.append(" (");
214                for (String type : _addedTypes.keySet()) {
215                    aLog.append(type + ", ");
216                }
217                aLog.append(") ");
218            }
219            aLog.append("]");
220            return aLog.toString();
221        }
222
223    }
224
225    public final static boolean SYNCHONEOUS  = true;
226    public final static boolean ASYNCHONEOUS = false;
227
228    private final T             _listener;
229
230    private final boolean       _synch;
231
232    /**
233     * @param listener
234     *            listener being tracked.
235     * @param synch
236     *            true if that listener can be called asynchronously
237     */
238    public ListenerStatus(T listener, boolean synch) {
239        super();
240        _listener = listener;
241        _synch = synch;
242    }
243
244    /**
245     * @return the listener
246     */
247    public T getListener() {
248        return _listener;
249    }
250
251    /**
252     * Return <cod>true</code> if the listener must be called synchronously.
253     *
254     * @return the synch
255     */
256    public boolean isSynchronous() {
257        return _synch;
258    }
259
260    /*
261     * (non-Javadoc)
262     * @see java.lang.Object#hashCode()
263     */
264    @Override
265    public int hashCode() {
266        return this.getListener().hashCode();
267    }
268
269    /*
270     * (non-Javadoc)
271     * @see java.lang.Object#equals(java.lang.Object)
272     */
273    @Override
274    public boolean equals(Object obj) {
275        return (obj instanceof ListenerStatus) && this.getListener().equals(((ListenerStatus<?>) obj).getListener());
276    }
277
278    /*
279     * (non-Javadoc)
280     * @see java.lang.Object#toString()
281     */
282    @Override
283    public String toString() {
284        return "[Status for " + this.getListener().toString() + "]";
285    }
286}
287