1/**
2 *
3 */
4package javax.jmdns.impl;
5
6import java.io.IOException;
7import java.net.InetAddress;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.Collections;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.List;
14import java.util.Map;
15import java.util.Set;
16import java.util.Timer;
17import java.util.TimerTask;
18import java.util.concurrent.ConcurrentHashMap;
19import java.util.concurrent.ConcurrentMap;
20import java.util.concurrent.ExecutorService;
21import java.util.concurrent.Executors;
22import java.util.concurrent.TimeUnit;
23import java.util.logging.Level;
24import java.util.logging.Logger;
25
26import javax.jmdns.JmDNS;
27import javax.jmdns.JmmDNS;
28import javax.jmdns.NetworkTopologyDiscovery;
29import javax.jmdns.NetworkTopologyEvent;
30import javax.jmdns.NetworkTopologyListener;
31import javax.jmdns.ServiceInfo;
32import javax.jmdns.ServiceListener;
33import javax.jmdns.ServiceTypeListener;
34import javax.jmdns.impl.constants.DNSConstants;
35
36/**
37 * This class enable multihomming mDNS. It will open a mDNS per IP address of the machine.
38 *
39 * @author Cédrik Lime, Pierre Frisch
40 */
41public class JmmDNSImpl implements JmmDNS, NetworkTopologyListener, ServiceInfoImpl.Delegate {
42    private static Logger                            logger = Logger.getLogger(JmmDNSImpl.class.getName());
43
44    private final Set<NetworkTopologyListener>       _networkListeners;
45
46    /**
47     * Every JmDNS created.
48     */
49    private final ConcurrentMap<InetAddress, JmDNS>  _knownMDNS;
50
51    /**
52     * This enable the service info text update.
53     */
54    private final ConcurrentMap<String, ServiceInfo> _services;
55
56    private final ExecutorService                    _ListenerExecutor;
57
58    private final ExecutorService                    _jmDNSExecutor;
59
60    private final Timer                              _timer;
61
62    /**
63     *
64     */
65    public JmmDNSImpl() {
66        super();
67        _networkListeners = Collections.synchronizedSet(new HashSet<NetworkTopologyListener>());
68        _knownMDNS = new ConcurrentHashMap<InetAddress, JmDNS>();
69        _services = new ConcurrentHashMap<String, ServiceInfo>(20);
70        _ListenerExecutor = Executors.newSingleThreadExecutor();
71        _jmDNSExecutor = Executors.newCachedThreadPool();
72        _timer = new Timer("Multihommed mDNS.Timer", true);
73        (new NetworkChecker(this, NetworkTopologyDiscovery.Factory.getInstance())).start(_timer);
74    }
75
76    /*
77     * (non-Javadoc)
78     * @see java.io.Closeable#close()
79     */
80    @Override
81    public void close() throws IOException {
82        if (logger.isLoggable(Level.FINER)) {
83            logger.finer("Cancelling JmmDNS: " + this);
84        }
85        _timer.cancel();
86        _ListenerExecutor.shutdown();
87        // We need to cancel all the DNS
88        ExecutorService executor = Executors.newCachedThreadPool();
89        for (final JmDNS mDNS : _knownMDNS.values()) {
90            executor.submit(new Runnable() {
91                /**
92                 * {@inheritDoc}
93                 */
94                @Override
95                public void run() {
96                    try {
97                        mDNS.close();
98                    } catch (IOException exception) {
99                        // JmDNS never throws this is only because of the closeable interface
100                    }
101                }
102            });
103        }
104        executor.shutdown();
105        try {
106            executor.awaitTermination(DNSConstants.CLOSE_TIMEOUT, TimeUnit.MILLISECONDS);
107        } catch (InterruptedException exception) {
108            logger.log(Level.WARNING, "Exception ", exception);
109        }
110        _knownMDNS.clear();
111    }
112
113    /*
114     * (non-Javadoc)
115     * @see javax.jmdns.JmmDNS#getNames()
116     */
117    @Override
118    public String[] getNames() {
119        Set<String> result = new HashSet<String>();
120        for (JmDNS mDNS : _knownMDNS.values()) {
121            result.add(mDNS.getName());
122        }
123        return result.toArray(new String[result.size()]);
124    }
125
126    /*
127     * (non-Javadoc)
128     * @see javax.jmdns.JmmDNS#getHostNames()
129     */
130    @Override
131    public String[] getHostNames() {
132        Set<String> result = new HashSet<String>();
133        for (JmDNS mDNS : _knownMDNS.values()) {
134            result.add(mDNS.getHostName());
135        }
136        return result.toArray(new String[result.size()]);
137    }
138
139    /*
140     * (non-Javadoc)
141     * @see javax.jmdns.JmmDNS#getInetAddresses()
142     */
143    @Override
144    public InetAddress[] getInetAddresses() throws IOException {
145        Set<InetAddress> result = new HashSet<InetAddress>();
146        for (JmDNS mDNS : _knownMDNS.values()) {
147            result.add(mDNS.getInetAddress());
148        }
149        return result.toArray(new InetAddress[result.size()]);
150    }
151
152    /*
153     * (non-Javadoc)
154     * @see javax.jmdns.JmmDNS#getInterfaces()
155     */
156    @Override
157    @Deprecated
158    public InetAddress[] getInterfaces() throws IOException {
159        Set<InetAddress> result = new HashSet<InetAddress>();
160        for (JmDNS mDNS : _knownMDNS.values()) {
161            result.add(mDNS.getInterface());
162        }
163        return result.toArray(new InetAddress[result.size()]);
164    }
165
166    /*
167     * (non-Javadoc)
168     * @see javax.jmdns.JmmDNS#getServiceInfos(java.lang.String, java.lang.String)
169     */
170    @Override
171    public ServiceInfo[] getServiceInfos(String type, String name) {
172        return this.getServiceInfos(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
173    }
174
175    /*
176     * (non-Javadoc)
177     * @see javax.jmdns.JmmDNS#getServiceInfos(java.lang.String, java.lang.String, long)
178     */
179    @Override
180    public ServiceInfo[] getServiceInfos(String type, String name, long timeout) {
181        return this.getServiceInfos(type, name, false, timeout);
182    }
183
184    /*
185     * (non-Javadoc)
186     * @see javax.jmdns.JmmDNS#getServiceInfos(java.lang.String, java.lang.String, boolean)
187     */
188    @Override
189    public ServiceInfo[] getServiceInfos(String type, String name, boolean persistent) {
190        return this.getServiceInfos(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
191    }
192
193    /*
194     * (non-Javadoc)
195     * @see javax.jmdns.JmmDNS#getServiceInfos(java.lang.String, java.lang.String, boolean, long)
196     */
197    @Override
198    public ServiceInfo[] getServiceInfos(final String type, final String name, final boolean persistent, final long timeout) {
199        // We need to run this in parallel to respect the timeout.
200        final Set<ServiceInfo> result = Collections.synchronizedSet(new HashSet<ServiceInfo>(_knownMDNS.size()));
201        ExecutorService executor = Executors.newCachedThreadPool();
202        for (final JmDNS mDNS : _knownMDNS.values()) {
203            executor.submit(new Runnable() {
204                /**
205                 * {@inheritDoc}
206                 */
207                @Override
208                public void run() {
209                    result.add(mDNS.getServiceInfo(type, name, persistent, timeout));
210                }
211            });
212        }
213        executor.shutdown();
214        try {
215            executor.awaitTermination(timeout, TimeUnit.MILLISECONDS);
216        } catch (InterruptedException exception) {
217            logger.log(Level.WARNING, "Exception ", exception);
218        }
219        return result.toArray(new ServiceInfo[result.size()]);
220    }
221
222    /*
223     * (non-Javadoc)
224     * @see javax.jmdns.JmmDNS#requestServiceInfo(java.lang.String, java.lang.String)
225     */
226    @Override
227    public void requestServiceInfo(String type, String name) {
228        this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT);
229    }
230
231    /*
232     * (non-Javadoc)
233     * @see javax.jmdns.JmmDNS#requestServiceInfo(java.lang.String, java.lang.String, boolean)
234     */
235    @Override
236    public void requestServiceInfo(String type, String name, boolean persistent) {
237        this.requestServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT);
238    }
239
240    /*
241     * (non-Javadoc)
242     * @see javax.jmdns.JmmDNS#requestServiceInfo(java.lang.String, java.lang.String, long)
243     */
244    @Override
245    public void requestServiceInfo(String type, String name, long timeout) {
246        this.requestServiceInfo(type, name, false, timeout);
247    }
248
249    /*
250     * (non-Javadoc)
251     * @see javax.jmdns.JmmDNS#requestServiceInfo(java.lang.String, java.lang.String, boolean, long)
252     */
253    @Override
254    public void requestServiceInfo(final String type, final String name, final boolean persistent, final long timeout) {
255        // We need to run this in parallel to respect the timeout.
256        for (final JmDNS mDNS : _knownMDNS.values()) {
257            _jmDNSExecutor.submit(new Runnable() {
258                /**
259                 * {@inheritDoc}
260                 */
261                @Override
262                public void run() {
263                    mDNS.requestServiceInfo(type, name, persistent, timeout);
264                }
265            });
266        }
267    }
268
269    /*
270     * (non-Javadoc)
271     * @see javax.jmdns.JmmDNS#addServiceTypeListener(javax.jmdns.ServiceTypeListener)
272     */
273    @Override
274    public void addServiceTypeListener(ServiceTypeListener listener) throws IOException {
275        for (JmDNS mDNS : _knownMDNS.values()) {
276            mDNS.addServiceTypeListener(listener);
277        }
278    }
279
280    /*
281     * (non-Javadoc)
282     * @see javax.jmdns.JmmDNS#removeServiceTypeListener(javax.jmdns.ServiceTypeListener)
283     */
284    @Override
285    public void removeServiceTypeListener(ServiceTypeListener listener) {
286        for (JmDNS mDNS : _knownMDNS.values()) {
287            mDNS.removeServiceTypeListener(listener);
288        }
289    }
290
291    /*
292     * (non-Javadoc)
293     * @see javax.jmdns.JmmDNS#addServiceListener(java.lang.String, javax.jmdns.ServiceListener)
294     */
295    @Override
296    public void addServiceListener(String type, ServiceListener listener) {
297        for (JmDNS mDNS : _knownMDNS.values()) {
298            mDNS.addServiceListener(type, listener);
299        }
300    }
301
302    /*
303     * (non-Javadoc)
304     * @see javax.jmdns.JmmDNS#removeServiceListener(java.lang.String, javax.jmdns.ServiceListener)
305     */
306    @Override
307    public void removeServiceListener(String type, ServiceListener listener) {
308        for (JmDNS mDNS : _knownMDNS.values()) {
309            mDNS.removeServiceListener(type, listener);
310        }
311    }
312
313    /*
314     * (non-Javadoc)
315     * @see javax.jmdns.impl.ServiceInfoImpl.Delegate#textValueUpdated(javax.jmdns.ServiceInfo, byte[])
316     */
317    @Override
318    public void textValueUpdated(ServiceInfo target, byte[] value) {
319        synchronized (_services) {
320            for (JmDNS mDNS : _knownMDNS.values()) {
321                ServiceInfo info = ((JmDNSImpl) mDNS).getServices().get(target.getQualifiedName());
322                if (info != null) {
323                    info.setText(value);
324                } else {
325                    logger.warning("We have a mDNS that does not know about the service info being updated.");
326                }
327            }
328        }
329    }
330
331    /*
332     * (non-Javadoc)
333     * @see javax.jmdns.JmmDNS#registerService(javax.jmdns.ServiceInfo)
334     */
335    @Override
336    public void registerService(ServiceInfo info) throws IOException {
337        // This is really complex. We need to clone the service info for each DNS but then we loose the ability to update it.
338        synchronized (_services) {
339            for (JmDNS mDNS : _knownMDNS.values()) {
340                mDNS.registerService(info.clone());
341            }
342            ((ServiceInfoImpl) info).setDelegate(this);
343            _services.put(info.getQualifiedName(), info);
344        }
345    }
346
347    /*
348     * (non-Javadoc)
349     * @see javax.jmdns.JmmDNS#unregisterService(javax.jmdns.ServiceInfo)
350     */
351    @Override
352    public void unregisterService(ServiceInfo info) {
353        synchronized (_services) {
354            for (JmDNS mDNS : _knownMDNS.values()) {
355                mDNS.unregisterService(info);
356            }
357            ((ServiceInfoImpl) info).setDelegate(null);
358            _services.remove(info.getQualifiedName());
359        }
360    }
361
362    /*
363     * (non-Javadoc)
364     * @see javax.jmdns.JmmDNS#unregisterAllServices()
365     */
366    @Override
367    public void unregisterAllServices() {
368        synchronized (_services) {
369            for (JmDNS mDNS : _knownMDNS.values()) {
370                mDNS.unregisterAllServices();
371            }
372            _services.clear();
373        }
374    }
375
376    /*
377     * (non-Javadoc)
378     * @see javax.jmdns.JmmDNS#registerServiceType(java.lang.String)
379     */
380    @Override
381    public void registerServiceType(String type) {
382        for (JmDNS mDNS : _knownMDNS.values()) {
383            mDNS.registerServiceType(type);
384        }
385    }
386
387    /*
388     * (non-Javadoc)
389     * @see javax.jmdns.JmmDNS#list(java.lang.String)
390     */
391    @Override
392    public ServiceInfo[] list(String type) {
393        return this.list(type, DNSConstants.SERVICE_INFO_TIMEOUT);
394    }
395
396    /*
397     * (non-Javadoc)
398     * @see javax.jmdns.JmmDNS#list(java.lang.String, long)
399     */
400    @Override
401    public ServiceInfo[] list(final String type, final long timeout) {
402        // We need to run this in parallel to respect the timeout.
403        final Set<ServiceInfo> result = Collections.synchronizedSet(new HashSet<ServiceInfo>(_knownMDNS.size() * 5));
404        ExecutorService executor = Executors.newCachedThreadPool();
405        for (final JmDNS mDNS : _knownMDNS.values()) {
406            executor.submit(new Runnable() {
407                /**
408                 * {@inheritDoc}
409                 */
410                @Override
411                public void run() {
412                    result.addAll(Arrays.asList(mDNS.list(type, timeout)));
413                }
414            });
415        }
416        executor.shutdown();
417        try {
418            executor.awaitTermination(timeout, TimeUnit.MILLISECONDS);
419        } catch (InterruptedException exception) {
420            logger.log(Level.WARNING, "Exception ", exception);
421        }
422        return result.toArray(new ServiceInfo[result.size()]);
423    }
424
425    /*
426     * (non-Javadoc)
427     * @see javax.jmdns.JmmDNS#listBySubtype(java.lang.String)
428     */
429    @Override
430    public Map<String, ServiceInfo[]> listBySubtype(String type) {
431        return this.listBySubtype(type, DNSConstants.SERVICE_INFO_TIMEOUT);
432    }
433
434    /*
435     * (non-Javadoc)
436     * @see javax.jmdns.JmmDNS#listBySubtype(java.lang.String, long)
437     */
438    @Override
439    public Map<String, ServiceInfo[]> listBySubtype(final String type, final long timeout) {
440        Map<String, List<ServiceInfo>> map = new HashMap<String, List<ServiceInfo>>(5);
441        for (ServiceInfo info : this.list(type, timeout)) {
442            String subtype = info.getSubtype();
443            if (!map.containsKey(subtype)) {
444                map.put(subtype, new ArrayList<ServiceInfo>(10));
445            }
446            map.get(subtype).add(info);
447        }
448
449        Map<String, ServiceInfo[]> result = new HashMap<String, ServiceInfo[]>(map.size());
450        for (String subtype : map.keySet()) {
451            List<ServiceInfo> infoForSubType = map.get(subtype);
452            result.put(subtype, infoForSubType.toArray(new ServiceInfo[infoForSubType.size()]));
453        }
454
455        return result;
456    }
457
458    /*
459     * (non-Javadoc)
460     * @see javax.jmdns.JmmDNS#addNetworkTopologyListener(javax.jmdns.NetworkTopologyListener)
461     */
462    @Override
463    public void addNetworkTopologyListener(NetworkTopologyListener listener) {
464        _networkListeners.add(listener);
465    }
466
467    /*
468     * (non-Javadoc)
469     * @see javax.jmdns.JmmDNS#removeNetworkTopologyListener(javax.jmdns.NetworkTopologyListener)
470     */
471    @Override
472    public void removeNetworkTopologyListener(NetworkTopologyListener listener) {
473        _networkListeners.remove(listener);
474    }
475
476    /*
477     * (non-Javadoc)
478     * @see javax.jmdns.JmmDNS#networkListeners()
479     */
480    @Override
481    public NetworkTopologyListener[] networkListeners() {
482        return _networkListeners.toArray(new NetworkTopologyListener[_networkListeners.size()]);
483    }
484
485    /*
486     * (non-Javadoc)
487     * @see javax.jmdns.NetworkTopologyListener#inetAddressAdded(javax.jmdns.NetworkTopologyEvent)
488     */
489    @Override
490    public void inetAddressAdded(NetworkTopologyEvent event) {
491        InetAddress address = event.getInetAddress();
492        try {
493            synchronized (this) {
494                if (!_knownMDNS.containsKey(address)) {
495                    _knownMDNS.put(address, JmDNS.create(address));
496                    final NetworkTopologyEvent jmdnsEvent = new NetworkTopologyEventImpl(_knownMDNS.get(address), address);
497                    for (final NetworkTopologyListener listener : this.networkListeners()) {
498                        _ListenerExecutor.submit(new Runnable() {
499                            /**
500                             * {@inheritDoc}
501                             */
502                            @Override
503                            public void run() {
504                                listener.inetAddressAdded(jmdnsEvent);
505                            }
506                        });
507                    }
508                }
509            }
510        } catch (Exception e) {
511            logger.warning("Unexpected unhandled exception: " + e);
512        }
513    }
514
515    /*
516     * (non-Javadoc)
517     * @see javax.jmdns.NetworkTopologyListener#inetAddressRemoved(javax.jmdns.NetworkTopologyEvent)
518     */
519    @Override
520    public void inetAddressRemoved(NetworkTopologyEvent event) {
521        InetAddress address = event.getInetAddress();
522        try {
523            synchronized (this) {
524                if (_knownMDNS.containsKey(address)) {
525                    JmDNS mDNS = _knownMDNS.remove(address);
526                    mDNS.close();
527                    final NetworkTopologyEvent jmdnsEvent = new NetworkTopologyEventImpl(mDNS, address);
528                    for (final NetworkTopologyListener listener : this.networkListeners()) {
529                        _ListenerExecutor.submit(new Runnable() {
530                            /**
531                             * {@inheritDoc}
532                             */
533                            @Override
534                            public void run() {
535                                listener.inetAddressRemoved(jmdnsEvent);
536                            }
537                        });
538                    }
539                }
540            }
541        } catch (Exception e) {
542            logger.warning("Unexpected unhandled exception: " + e);
543        }
544    }
545
546    /**
547     * Checks the network state.<br/>
548     * If the network change, this class will reconfigure the list of DNS do adapt to the new configuration.
549     */
550    static class NetworkChecker extends TimerTask {
551        private static Logger                  logger1 = Logger.getLogger(NetworkChecker.class.getName());
552
553        private final NetworkTopologyListener  _mmDNS;
554
555        private final NetworkTopologyDiscovery _topology;
556
557        private Set<InetAddress>               _knownAddresses;
558
559        public NetworkChecker(NetworkTopologyListener mmDNS, NetworkTopologyDiscovery topology) {
560            super();
561            this._mmDNS = mmDNS;
562            this._topology = topology;
563            _knownAddresses = Collections.synchronizedSet(new HashSet<InetAddress>());
564        }
565
566        public void start(Timer timer) {
567            timer.schedule(this, 0, DNSConstants.NETWORK_CHECK_INTERVAL);
568        }
569
570        /**
571         * {@inheritDoc}
572         */
573        @Override
574        public void run() {
575            try {
576                InetAddress[] curentAddresses = _topology.getInetAddresses();
577                Set<InetAddress> current = new HashSet<InetAddress>(curentAddresses.length);
578                for (InetAddress address : curentAddresses) {
579                    current.add(address);
580                    if (!_knownAddresses.contains(address)) {
581                        final NetworkTopologyEvent event = new NetworkTopologyEventImpl(_mmDNS, address);
582                        _mmDNS.inetAddressAdded(event);
583                    }
584                }
585                for (InetAddress address : _knownAddresses) {
586                    if (!current.contains(address)) {
587                        final NetworkTopologyEvent event = new NetworkTopologyEventImpl(_mmDNS, address);
588                        _mmDNS.inetAddressRemoved(event);
589                    }
590                }
591                _knownAddresses = current;
592            } catch (Exception e) {
593                logger1.warning("Unexpected unhandled exception: " + e);
594            }
595        }
596
597    }
598
599}
600