001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.transport.discovery.zeroconf;
018
019import java.io.IOException;
020import java.net.InetAddress;
021import java.net.UnknownHostException;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.Map;
025import java.util.concurrent.CopyOnWriteArrayList;
026
027import javax.jmdns.JmDNS;
028import javax.jmdns.ServiceEvent;
029import javax.jmdns.ServiceInfo;
030import javax.jmdns.ServiceListener;
031
032import org.apache.activemq.command.DiscoveryEvent;
033import org.apache.activemq.transport.discovery.DiscoveryAgent;
034import org.apache.activemq.transport.discovery.DiscoveryListener;
035import org.apache.activemq.util.JMSExceptionSupport;
036import org.apache.activemq.util.MapHelper;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * A {@link DiscoveryAgent} using <a
042 * href="http://www.zeroconf.org/">Zeroconf</a> via the <a
043 * href="http://jmdns.sf.net/">jmDNS</a> library
044 */
045public class ZeroconfDiscoveryAgent implements DiscoveryAgent, ServiceListener {
046    private static final Logger LOG = LoggerFactory.getLogger(ZeroconfDiscoveryAgent.class);
047
048    private static final String TYPE_SUFFIX = "ActiveMQ-5.";
049
050    private JmDNS jmdns;
051    private InetAddress localAddress;
052    private String localhost;
053    private int weight;
054    private int priority;
055    private String typeSuffix = TYPE_SUFFIX;
056
057    private DiscoveryListener listener;
058    private String group = "default";
059    private final CopyOnWriteArrayList<ServiceInfo> serviceInfos =
060        new CopyOnWriteArrayList<ServiceInfo>();
061
062    // DiscoveryAgent interface
063    // -------------------------------------------------------------------------
064    @Override
065    public void start() throws Exception {
066        if (group == null) {
067            throw new IOException("You must specify a group to discover");
068        }
069        String type = getType();
070        if (!type.endsWith(".")) {
071            LOG.warn("The type '{}' should end with '.' to be a valid Rendezvous type", type);
072            type += ".";
073        }
074        try {
075            // force lazy construction
076            getJmdns();
077            if (listener != null) {
078                LOG.info("Discovering service of type: {}", type);
079                jmdns.addServiceListener(type, this);
080            }
081        } catch (IOException e) {
082            JMSExceptionSupport.create("Failed to start JmDNS service: " + e, e);
083        }
084    }
085
086    @Override
087    public void stop() {
088        if (jmdns != null) {
089            for (Iterator<ServiceInfo> iter = serviceInfos.iterator(); iter.hasNext();) {
090                ServiceInfo si = iter.next();
091                jmdns.unregisterService(si);
092            }
093
094            // Close it down async since this could block for a while.
095            final JmDNS closeTarget = jmdns;
096            Thread thread = new Thread() {
097                @Override
098                public void run() {
099                    try {
100                        if (JmDNSFactory.onClose(getLocalAddress())) {
101                            closeTarget.close();
102                        };
103                    } catch (IOException e) {
104                        LOG.debug("Error closing JmDNS {}. This exception will be ignored.", getLocalhost(), e);
105                    }
106                }
107            };
108
109            thread.setDaemon(true);
110            thread.start();
111
112            jmdns = null;
113        }
114    }
115
116    @Override
117    public void registerService(String name) throws IOException {
118        ServiceInfo si = createServiceInfo(name, new HashMap<String, Object>());
119        serviceInfos.add(si);
120        getJmdns().registerService(si);
121    }
122
123    // ServiceListener interface
124    // -------------------------------------------------------------------------
125    public void addService(JmDNS jmDNS, String type, String name) {
126        LOG.debug("addService with type: {} name: {}", type, name);
127        if (listener != null) {
128            listener.onServiceAdd(new DiscoveryEvent(name));
129        }
130        jmDNS.requestServiceInfo(type, name);
131    }
132
133    public void removeService(JmDNS jmDNS, String type, String name) {
134        LOG.debug("removeService with type: {} name: {}", type, name);
135        if (listener != null) {
136            listener.onServiceRemove(new DiscoveryEvent(name));
137        }
138    }
139
140    @Override
141    public void serviceAdded(ServiceEvent event) {
142        addService(event.getDNS(), event.getType(), event.getName());
143    }
144
145    @Override
146    public void serviceRemoved(ServiceEvent event) {
147        removeService(event.getDNS(), event.getType(), event.getName());
148    }
149
150    @Override
151    public void serviceResolved(ServiceEvent event) {
152    }
153
154    public void resolveService(JmDNS jmDNS, String type, String name, ServiceInfo serviceInfo) {
155    }
156
157    public int getPriority() {
158        return priority;
159    }
160
161    public void setPriority(int priority) {
162        this.priority = priority;
163    }
164
165    public int getWeight() {
166        return weight;
167    }
168
169    public void setWeight(int weight) {
170        this.weight = weight;
171    }
172
173    public JmDNS getJmdns() throws IOException {
174        if (jmdns == null) {
175            jmdns = createJmDNS();
176        }
177        return jmdns;
178    }
179
180    public void setJmdns(JmDNS jmdns) {
181        this.jmdns = jmdns;
182    }
183
184    public InetAddress getLocalAddress() throws UnknownHostException {
185        if (localAddress == null) {
186            localAddress = createLocalAddress();
187        }
188        return localAddress;
189    }
190
191    public void setLocalAddress(InetAddress localAddress) {
192        this.localAddress = localAddress;
193    }
194
195    public String getLocalhost() {
196        return localhost;
197    }
198
199    public void setLocalhost(String localhost) {
200        this.localhost = localhost;
201    }
202
203    // Implementation methods
204    // -------------------------------------------------------------------------
205    protected ServiceInfo createServiceInfo(String name, Map map) {
206        int port = MapHelper.getInt(map, "port", 0);
207        String type = getType();
208        LOG.debug("Registering service type: {} name: {} details: {}", new Object[]{type, name, map});
209        return ServiceInfo.create(type, name + "." + type, port, weight, priority, "");
210    }
211
212    protected JmDNS createJmDNS() throws IOException {
213        return JmDNSFactory.create(getLocalAddress());
214    }
215
216    protected InetAddress createLocalAddress() throws UnknownHostException {
217        if (localhost != null) {
218            return InetAddress.getByName(localhost);
219        }
220        return InetAddress.getLocalHost();
221    }
222
223    @Override
224    public void setDiscoveryListener(DiscoveryListener listener) {
225        this.listener = listener;
226    }
227
228    public String getGroup() {
229        return group;
230    }
231
232    public void setGroup(String group) {
233        this.group = group;
234    }
235
236    public void setType(String typeSuffix) {
237        this.typeSuffix = typeSuffix;
238    }
239
240    public String getType() {
241        if (typeSuffix == null || typeSuffix.isEmpty()) {
242            typeSuffix = TYPE_SUFFIX;
243        }
244
245        return "_" + group + "." + typeSuffix;
246    }
247
248    @Override
249    public void serviceFailed(DiscoveryEvent event) throws IOException {
250        // TODO: is there a way to notify the JmDNS that the service failed?
251    }
252}