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