001package jmri.util.zeroconf;
002
003import java.net.InetAddress;
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.HashMap;
007import java.util.List;
008import javax.jmdns.JmDNS;
009import javax.jmdns.ServiceInfo;
010import jmri.InstanceManager;
011
012/**
013 * ZeroConfService objects manage a zeroConf network service advertisement.
014 * <p>
015 * ZeroConfService objects encapsulate zeroConf network services created using
016 * JmDNS, providing methods to start and stop service advertisements and to
017 * query service state. Typical usage would be:
018 * <pre>
019 * ZeroConfService myService = ZeroConfService.create("_withrottle._tcp.local.", port);
020 * myService.publish();
021 * </pre> or, if you do not wish to retain the ZeroConfService object:
022 * <pre>
023 * ZeroConfService.create("_http._tcp.local.", port).publish();
024 * </pre> ZeroConfService objects can also be created with a HashMap of
025 * properties that are included in the TXT record for the service advertisement.
026 * This HashMap should remain small, but it could include information such as
027 * the default path (for a web server), a specific protocol version, or other
028 * information. Note that all service advertisements include the JMRI version,
029 * using the key "version", and the JMRI version numbers in a string
030 * "major.minor.test" with the key "jmri"
031 * <p>
032 * All ZeroConfServices are published with the computer's hostname as the mDNS
033 * hostname (unless it cannot be determined by JMRI), as well as the JMRI node
034 * name in the TXT record with the key "node".
035 * <p>
036 * All ZeroConfServices are automatically stopped when the JMRI application
037 * shuts down. Use {@link ZeroConfServiceManager#allServices()} to get a
038 * collection of all published ZeroConfService objects.
039 * <hr>
040 * This file is part of JMRI.
041 * <p>
042 * JMRI is free software; you can redistribute it and/or modify it under the
043 * terms of version 2 of the GNU General Public License as published by the Free
044 * Software Foundation. See the "COPYING" file for a copy of this license.
045 * <p>
046 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
047 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
048 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
049 *
050 * @author Randall Wood Copyright (C) 2011, 2013, 2018
051 * @see javax.jmdns.JmDNS
052 * @see javax.jmdns.ServiceInfo
053 */
054public class ZeroConfService {
055
056    // internal data members
057    private final HashMap<InetAddress, ServiceInfo> serviceInfos = new HashMap<>();
058    private ServiceInfo serviceInfo = null;
059    // static data objects
060    private final List<ZeroConfServiceListener> listeners = new ArrayList<>();
061    // API constants
062    public static final String IPv4 = "IPv4";
063    public static final String IPv6 = "IPv6";
064    public static final String LOOPBACK = "loopback";
065    public static final String LINKLOCAL = "linklocal";
066
067    /**
068     * Create a ZeroConfService with the minimal required settings. This method
069     * calls {@link #create(java.lang.String, int, java.util.HashMap)} with an
070     * empty props HashMap.
071     *
072     * @param type The service protocol
073     * @param port The port the service runs over
074     * @return A new unpublished ZeroConfService, or an existing service
075     * @see #create(java.lang.String, java.lang.String, int, int, int,
076     * java.util.HashMap)
077     */
078    public static ZeroConfService create(String type, int port) {
079        return InstanceManager.getDefault(ZeroConfServiceManager.class).create(type, port);
080    }
081
082    /**
083     * Create a ZeroConfService with an automatically detected server name. This
084     * method calls
085     * {@link #create(java.lang.String, java.lang.String, int, int, int, java.util.HashMap)}
086     * with the default weight and priority, and with the result of
087     * {@link jmri.web.server.WebServerPreferences#getRailroadName()}
088     * reformatted to replace dots and dashes with spaces.
089     *
090     * @param type       The service protocol
091     * @param port       The port the service runs over
092     * @param properties Additional information to be listed in service
093     *                   advertisement
094     * @return A new unpublished ZeroConfService, or an existing service
095     */
096    public static ZeroConfService create(String type, int port, HashMap<String, String> properties) {
097        return InstanceManager.getDefault(ZeroConfServiceManager.class).create(type, port, properties);
098    }
099
100    /**
101     * Create a ZeroConfService. The property <i>version</i> is added or
102     * replaced with the current JMRI version as its value. The property
103     * <i>jmri</i> is added or replaced with the JMRI major.minor.test version
104     * string as its value.
105     * <p>
106     * If a service with the same key as the new service is already published,
107     * the original service is returned unmodified.
108     *
109     * @param type       The service protocol
110     * @param name       The name of the JMRI server listed on client devices
111     * @param port       The port the service runs over
112     * @param weight     Default value is 0
113     * @param priority   Default value is 0
114     * @param properties Additional information to be listed in service
115     *                   advertisement
116     * @return A new unpublished ZeroConfService, or an existing service
117     */
118    public static ZeroConfService create(String type, String name, int port, int weight, int priority, HashMap<String, String> properties) {
119        return InstanceManager.getDefault(ZeroConfServiceManager.class).create(type, name, port, weight, priority, properties);
120    }
121
122    /**
123     * Create a ZeroConfService object.
124     *
125     * @param service the JmDNS service information
126     */
127    protected ZeroConfService(ServiceInfo service) {
128        this.serviceInfo = service;
129    }
130
131    /**
132     * Get the key of the ZeroConfService object. The key is fully qualified
133     * name of the service in all lowercase, for example
134     * {@code jmri._http.local }.
135     *
136     * @return The fully qualified name of the service
137     */
138    public String getKey() {
139        return this.getServiceInfo().getKey();
140    }
141
142    /**
143     * Get the name of the ZeroConfService object. The name can only be set when
144     * creating the object.
145     *
146     * @return The service name as reported by the
147     *         {@link javax.jmdns.ServiceInfo} object
148     */
149    public String getName() {
150        return this.getServiceInfo().getName();
151    }
152
153    /**
154     * Get the type of the ZeroConfService object. The type can only be set when
155     * creating the object.
156     *
157     * @return The service type as reported by the
158     *         {@link javax.jmdns.ServiceInfo} object
159     */
160    public String getType() {
161        return this.getServiceInfo().getType();
162    }
163
164    /**
165     * Get the ServiceInfo for the given address. Package private so can be
166     * managed by {@link jmri.util.zeroconf.ZeroConfServiceManager}, but not in
167     * public API.
168     *
169     * @param address the address associated with the ServiceInfo to get
170     * @return the ServiceInfo for the address or null if none exists
171     */
172    ServiceInfo getServiceInfo(InetAddress address) {
173        return serviceInfos.get(address);
174    }
175
176    /**
177     * Add the ServiceInfo for the given address. Package private so can be
178     * managed by {@link jmri.util.zeroconf.ZeroConfServiceManager}, but not in
179     * public API.
180     *
181     * @param address the address associated with the ServiceInfo to add
182     * @return the added ServiceInfo for the address
183     */
184    ServiceInfo addServiceInfo(InetAddress address) {
185        if (!this.serviceInfos.containsKey(address)) {
186            this.serviceInfos.put(address, this.getServiceInfo().clone());
187        }
188        return this.serviceInfos.get(address);
189    }
190
191    /**
192     * Remove the ServiceInfo for the given address. Package private so can be
193     * managed by {@link jmri.util.zeroconf.ZeroConfServiceManager}, but not in
194     * public API.
195     *
196     * @param address the address associated with the ServiceInfo to remove
197     */
198    void removeServiceInfo(InetAddress address) {
199        serviceInfos.remove(address);
200    }
201
202    /**
203     * Check if a ServiceInfo exists for the given address. Package private so
204     * can be managed by {@link jmri.util.zeroconf.ZeroConfServiceManager}, but
205     * not in public API.
206     *
207     * @param key the address associated with the ServiceInfo to check for
208     * @return true if the ServiceInfo exists; false otherwise
209     */
210    boolean containsServiceInfo(InetAddress key) {
211        return serviceInfos.containsKey(key);
212    }
213
214    /**
215     * Get the reference ServiceInfo for the object. This is the JmDNS
216     * implementation of a zeroConf service. The reference ServiceInfo is never
217     * actually registered with a JmDNS service, since registrations with a
218     * JmDNS service are unique per InetAddress.
219     *
220     * @return The getServiceInfo object.
221     */
222    public ServiceInfo getServiceInfo() {
223        return this.serviceInfo;
224    }
225
226    /**
227     * Get the state of the service.
228     *
229     * @return True if the service is being advertised, and false otherwise.
230     */
231    public boolean isPublished() {
232        return InstanceManager.getDefault(ZeroConfServiceManager.class).isPublished(this);
233    }
234
235    /**
236     * Start advertising the service.
237     */
238    public void publish() {
239        InstanceManager.getDefault(ZeroConfServiceManager.class).publish(this);
240    }
241
242    /**
243     * Stop advertising the service.
244     */
245    public void stop() {
246        InstanceManager.getDefault(ZeroConfServiceManager.class).stop(this);
247    }
248
249    public void addEventListener(ZeroConfServiceListener l) {
250        this.listeners.add(l);
251    }
252
253    public void removeEventListener(ZeroConfServiceListener l) {
254        this.listeners.remove(l);
255    }
256    
257    /**
258     * Get a list of the listeners for this service.
259     * @return the listeners or an empty list if none
260     */
261    public List<ZeroConfServiceListener> getListeners() {
262        return new ArrayList<>(listeners);
263    }
264}