blob: 702926bb68c72d445e28289232357f73b086a137 [file] [log] [blame]
/****************************************************************************
* Copyright (c) 2007 Composent, Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Composent, Inc. - initial API and implementation
*****************************************************************************/
package org.eclipse.ecf.provider.jmdns.container;
import java.io.*;
import java.net.URI;
import java.util.*;
import javax.jmdns.*;
import javax.jmdns.ServiceInfo;
import org.eclipse.core.runtime.Assert;
import org.eclipse.ecf.core.ContainerConnectException;
import org.eclipse.ecf.core.events.*;
import org.eclipse.ecf.core.identity.ID;
import org.eclipse.ecf.core.identity.IDFactory;
import org.eclipse.ecf.core.security.IConnectContext;
import org.eclipse.ecf.core.util.*;
import org.eclipse.ecf.discovery.*;
import org.eclipse.ecf.discovery.identity.*;
import org.eclipse.ecf.discovery.service.IDiscoveryService;
import org.eclipse.ecf.internal.provider.jmdns.*;
import org.eclipse.ecf.provider.jmdns.identity.JMDNSNamespace;
public class JMDNSDiscoveryContainer extends AbstractDiscoveryContainerAdapter implements IDiscoveryService, ServiceListener, ServiceTypeListener {
private static final String SCHEME_PROPERTY = "jmdns.ptcl"; //$NON-NLS-1$
private static final String URI_PATH_PROPERTY = "path"; //$NON-NLS-1$
private static final String NAMING_AUTHORITY_PROPERTY = "jmdns.namingauthority"; //$NON-NLS-1$
public static final int DEFAULT_REQUEST_TIMEOUT = 3000;
private static int instanceCount = 0;
JmDNS jmdns = null;
private ID targetID = null;
List serviceTypes = null;
/**
* Map of IServiceInfos (maps to SRV type records) discovered by mDNS.
* mDNS defines a two stage process where a client first discovers the PTR (announced by the advertiser) and then upon
* request from the application layers above resolves the PTR into a SRV.
* Deregistration is only send for PTR records, not for SRV records (SRV records have a very low TTL anyway).
*/
final Map services = Collections.synchronizedMap(new HashMap());
boolean disposed = false;
final Object lock = new Object();
SimpleFIFOQueue queue = null;
Thread notificationThread = null;
/**
* @since 4.0
*/
public JMDNSDiscoveryContainer() {
super(JMDNSNamespace.NAME, new DiscoveryContainerConfig(IDFactory.getDefault().createStringID(JMDNSDiscoveryContainer.class.getName() + ";" + instanceCount++))); //$NON-NLS-1$ //$NON-NLS-2$
serviceTypes = new ArrayList();
}
/****************** IContainer methods **************************/
/* (non-Javadoc)
* @see org.eclipse.ecf.core.IContainer#getConnectedID()
*/
public ID getConnectedID() {
return this.targetID;
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.AbstractDiscoveryContainerAdapter#dispose()
*/
public void dispose() {
synchronized (lock) {
super.dispose();
disposed = true;
}
}
/* (non-Javadoc)
* @see org.eclipse.ecf.core.IContainer#connect(org.eclipse.ecf.core.identity.ID, org.eclipse.ecf.core.security.IConnectContext)
*/
public void connect(final ID targetID1, final IConnectContext joinContext) throws ContainerConnectException {
synchronized (lock) {
if (disposed)
throw new ContainerConnectException("Container has been disposed"); //$NON-NLS-1$
if (this.targetID != null)
throw new ContainerConnectException("Already connected"); //$NON-NLS-1$
this.targetID = (targetID1 == null) ? getConfig().getID() : targetID1;
fireContainerEvent(new ContainerConnectingEvent(this.getID(), this.targetID, joinContext));
initializeQueue();
try {
this.jmdns = JmDNS.create();
jmdns.addServiceTypeListener(this);
} catch (final IOException e) {
Trace.catching(JMDNSPlugin.PLUGIN_ID, JMDNSDebugOptions.EXCEPTIONS_CATCHING, this.getClass(), "connect", e); //$NON-NLS-1$
if (this.jmdns != null) {
jmdns.close();
jmdns = null;
}
throw new ContainerConnectException("Cannot create JmDNS instance", e); //$NON-NLS-1$
}
fireContainerEvent(new ContainerConnectedEvent(this.getID(), this.targetID));
}
}
private void initializeQueue() {
queue = new SimpleFIFOQueue();
notificationThread = new Thread(new Runnable() {
public void run() {
while (!disposed || queue.isStopped()) {
if (Thread.currentThread().isInterrupted())
break;
final Runnable runnable = (Runnable) queue.dequeue();
if (Thread.currentThread().isInterrupted() || runnable == null)
break;
try {
runnable.run();
} catch (final Throwable t) {
JMDNSPlugin plugin = JMDNSPlugin.getDefault();
if (plugin != null) {
plugin.logException("handleRuntimeException", t); //$NON-NLS-1$
}
}
}
}
}, "JMDNS Discovery Thread"); //$NON-NLS-1$
notificationThread.start();
}
/* (non-Javadoc)
* @see org.eclipse.ecf.core.IContainer#disconnect()
*/
public void disconnect() {
synchronized (lock) {
if (getConnectedID() == null || disposed) {
return;
}
final ID connectedID = getConnectedID();
fireContainerEvent(new ContainerDisconnectingEvent(this.getID(), connectedID));
queue.close();
notificationThread.interrupt();
notificationThread = null;
this.targetID = null;
serviceTypes.clear();
jmdns.close();
jmdns = null;
fireContainerEvent(new ContainerDisconnectedEvent(this.getID(), connectedID));
}
}
/************************* IDiscoveryContainerAdapter methods *********************/
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.IDiscoveryContainerAdapter#getServiceInfo(org.eclipse.ecf.discovery.identity.IServiceID)
*/
public IServiceInfo getServiceInfo(final IServiceID service) {
Assert.isNotNull(service);
synchronized (lock) {
try {
// ECF discovery API defines identity to be the service type and the URI (location)
// see https://bugs.eclipse.org/266723
final ServiceInfo[] serviceInfos = jmdns.list(service.getServiceTypeID().getInternal());
for (int i = 0; i < serviceInfos.length; i++) {
ServiceInfo serviceInfo = serviceInfos[i];
IServiceInfo iServiceInfo = createIServiceInfoFromServiceInfo(serviceInfo);
Assert.isNotNull(iServiceInfo);
Assert.isNotNull(iServiceInfo.getServiceID());
if (iServiceInfo.getServiceID().equals(service)) {
return iServiceInfo;
}
}
return null;
} catch (final Exception e) {
Trace.catching(JMDNSPlugin.PLUGIN_ID, JMDNSDebugOptions.EXCEPTIONS_CATCHING, this.getClass(), "getServiceInfo", e); //$NON-NLS-1$
return null;
}
}
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.IDiscoveryContainerAdapter#getServices()
*/
public IServiceInfo[] getServices() {
synchronized (lock) {
final IServiceTypeID[] serviceTypeArray = getServiceTypes();
final List results = new ArrayList();
for (int i = 0; i < serviceTypeArray.length; i++) {
final IServiceTypeID stid = serviceTypeArray[i];
if (stid != null)
results.addAll(Arrays.asList(getServices(stid)));
}
return (IServiceInfo[]) results.toArray(new IServiceInfo[] {});
}
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.IDiscoveryContainerAdapter#getServices(org.eclipse.ecf.discovery.identity.IServiceTypeID)
*/
public IServiceInfo[] getServices(final IServiceTypeID type) {
Assert.isNotNull(type);
final List serviceInfos = new ArrayList();
synchronized (lock) {
// We don't know the naming authority yet (it's part of the service properties)
for (final Iterator itr = serviceTypes.iterator(); itr.hasNext();) {
final IServiceTypeID serviceType = (IServiceTypeID) itr.next();
if (Arrays.equals(serviceType.getServices(), type.getServices()) && Arrays.equals(serviceType.getProtocols(), type.getProtocols()) && Arrays.equals(serviceType.getScopes(), type.getScopes())) {
final ServiceInfo[] infos = jmdns.list(type.getInternal());
for (int i = 0; i < infos.length; i++) {
try {
if (infos[i] != null) {
final IServiceInfo si = createIServiceInfoFromServiceInfo(infos[i]);
if (si != null)
serviceInfos.add(si);
}
} catch (final Exception e) {
Trace.catching(JMDNSPlugin.PLUGIN_ID, JMDNSDebugOptions.EXCEPTIONS_CATCHING, this.getClass(), "getServices", e); //$NON-NLS-1$
}
}
}
}
return (IServiceInfo[]) serviceInfos.toArray(new IServiceInfo[] {});
}
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.IDiscoveryContainerAdapter#getServiceTypes()
*/
public IServiceTypeID[] getServiceTypes() {
synchronized (lock) {
return (IServiceTypeID[]) serviceTypes.toArray(new IServiceTypeID[] {});
}
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.IDiscoveryContainerAdapter#registerService(org.eclipse.ecf.discovery.IServiceInfo)
*/
public void registerService(final IServiceInfo serviceInfo) {
Assert.isNotNull(serviceInfo);
final ServiceInfo svcInfo = createServiceInfoFromIServiceInfo(serviceInfo);
checkServiceInfo(svcInfo);
try {
jmdns.registerService(svcInfo);
} catch (final IOException e) {
throw new ECFRuntimeException("Exception registering service", e); //$NON-NLS-1$
}
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.IDiscoveryContainerAdapter#unregisterService(org.eclipse.ecf.discovery.IServiceInfo)
*/
public void unregisterService(final IServiceInfo serviceInfo) {
Assert.isNotNull(serviceInfo);
final ServiceInfo si = createServiceInfoFromIServiceInfo(serviceInfo);
jmdns.unregisterService(si);
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.AbstractDiscoveryContainerAdapter#unregisterAllServices()
*/
public void unregisterAllServices() {
jmdns.unregisterAllServices();
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.AbstractDiscoveryContainerAdapter#purgeCache()
*/
public IServiceInfo[] purgeCache() {
synchronized (lock) {
serviceTypes.clear();
}
return super.purgeCache();
}
/**************************** JMDNS listeners ***********************************/
private void runInThread(final Runnable runnable) {
queue.enqueue(runnable);
}
/* (non-Javadoc)
* @see javax.jmdns.ServiceTypeListener#serviceTypeAdded(javax.jmdns.ServiceEvent)
*/
public void serviceTypeAdded(final ServiceEvent arg0) {
Trace.trace(JMDNSPlugin.PLUGIN_ID, "serviceTypeAdded(" + arg0.getType() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
arg0.getDNS().addServiceListener(arg0.getType(), JMDNSDiscoveryContainer.this);
}
void fireTypeDiscovered(final IServiceTypeID serviceType) {
fireServiceTypeDiscovered(new ServiceTypeContainerEvent(serviceType, getID()));
}
/* (non-Javadoc)
* @see javax.jmdns.ServiceListener#serviceAdded(javax.jmdns.ServiceEvent)
*/
public void serviceAdded(final ServiceEvent arg0) {
Trace.trace(JMDNSPlugin.PLUGIN_ID, "serviceAdded(" + arg0.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
runInThread(new Runnable() {
public void run() {
final String serviceType = arg0.getType();
final String serviceName = arg0.getName();
IServiceInfo aServiceInfo = null;
synchronized (lock) {
if (getConnectedID() == null || disposed) {
return;
}
// explicitly get the service to determine the naming authority (part of the service properties)
try {
final ServiceInfo info = arg0.getDNS().getServiceInfo(serviceType, serviceName);
aServiceInfo = createIServiceInfoFromServiceInfo(info);
services.put(serviceType + serviceName, aServiceInfo);
serviceTypes.add(aServiceInfo.getServiceID().getServiceTypeID());
} catch (final Exception e) {
Trace.trace(JMDNSPlugin.PLUGIN_ID, "Failed to resolve in serviceAdded(" + arg0.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
return;
}
}
fireTypeDiscovered(aServiceInfo.getServiceID().getServiceTypeID());
fireDiscovered(aServiceInfo);
}
});
}
/* (non-Javadoc)
* @see javax.jmdns.ServiceListener#serviceRemoved(javax.jmdns.ServiceEvent)
*/
public void serviceRemoved(final ServiceEvent arg0) {
Trace.trace(JMDNSPlugin.PLUGIN_ID, "serviceRemoved(" + arg0.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
runInThread(new Runnable() {
public void run() {
if (getConnectedID() == null || disposed) {
return;
}
final String serviceType = arg0.getType();
final String serviceName = arg0.getName();
IServiceInfo aServiceInfo = (IServiceInfo) services.remove(serviceType + serviceName);
if (aServiceInfo == null) {
Trace.trace(JMDNSPlugin.PLUGIN_ID, "Failed to resolve in serviceRemoved(" + arg0.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
return;
}
fireUndiscovered(aServiceInfo);
}
});
}
void fireUndiscovered(final IServiceInfo serviceInfo) {
fireServiceUndiscovered(new ServiceContainerEvent(serviceInfo, getID()));
}
/* (non-Javadoc)
* @see javax.jmdns.ServiceListener#serviceResolved(javax.jmdns.ServiceEvent)
*/
public void serviceResolved(final ServiceEvent arg0) {
Trace.trace(JMDNSPlugin.PLUGIN_ID, "serviceResolved(" + arg0.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
}
void fireDiscovered(final IServiceInfo serviceInfo) {
fireServiceDiscovered(new ServiceContainerEvent(serviceInfo, getID()));
}
/*******************************************/
private void checkServiceInfo(final ServiceInfo serviceInfo) {
final String serviceName = serviceInfo.getName();
if (serviceName == null)
throw new ECFRuntimeException("Service name cannot be null"); //$NON-NLS-1$
}
IServiceInfo createIServiceInfoFromServiceInfo(final ServiceInfo serviceInfo) throws Exception {
Assert.isNotNull(serviceInfo);
final int priority = serviceInfo.getPriority();
final int weight = serviceInfo.getWeight();
final Properties props = new Properties();
String uriProtocol = null;
String uriPath = null;
String namingAuthority = IServiceTypeID.DEFAULT_NA;
for (final Enumeration e = serviceInfo.getPropertyNames(); e.hasMoreElements();) {
final String key = (String) e.nextElement();
if (SCHEME_PROPERTY.equals(key)) {
uriProtocol = serviceInfo.getPropertyString(key);
} else if (NAMING_AUTHORITY_PROPERTY.equals(key)) {
namingAuthority = serviceInfo.getPropertyString(key);
} else if (URI_PATH_PROPERTY.equals(key)) {
uriPath = serviceInfo.getPropertyString(key);
} else {
final byte[] bytes = serviceInfo.getPropertyBytes(key);
try {
final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
final Object object = in.readObject();
in.close();
props.put(key, object);
} catch (final StreamCorruptedException ioe) {
props.put(key, serviceInfo.getPropertyString(key));
} catch (final EOFException eofe) { // not all byte[] are serialized objs (e.g. a native service)
props.put(key, serviceInfo.getPropertyString(key));
}
}
}
// proto
final String proto = serviceInfo.getProtocol();
// scopes
final String domain = serviceInfo.getDomain();
final String[] scopes = new String[] {domain};
// uri
String authority = serviceInfo.getHostAddress() + ":" + serviceInfo.getPort();
final URI uri = new URI(uriProtocol == null ? proto : uriProtocol, authority, uriPath, null, null);
// service type
String st = serviceInfo.getType();
final int end = st.indexOf(proto);
String[] types = StringUtils.split(st.substring(1, end), "._");
final IServiceTypeID sID = ServiceIDFactory.getDefault().createServiceTypeID(getServicesNamespace(), types, scopes, new String[] {proto}, namingAuthority);
// service name
final String name = serviceInfo.getName();
return new org.eclipse.ecf.discovery.ServiceInfo(uri, name, sID, priority, weight, new ServiceProperties(props));
}
private ServiceInfo createServiceInfoFromIServiceInfo(final IServiceInfo serviceInfo) {
if (serviceInfo == null)
return null;
final IServiceID sID = serviceInfo.getServiceID();
final Hashtable props = new Hashtable();
final IServiceProperties svcProps = serviceInfo.getServiceProperties();
if (svcProps != null) {
for (final Enumeration e = svcProps.getPropertyNames(); e.hasMoreElements();) {
final String key = (String) e.nextElement();
final Object val = svcProps.getProperty(key);
if (val instanceof String) {
props.put(key, val);
} else if (val instanceof Serializable) {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
final ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(val);
out.close();
} catch (final IOException e1) {
e1.printStackTrace();
}
final byte[] buf = bos.toByteArray();
props.put(key, buf);
// } else if (svcProps.getPropertyBytes(key) != null) {
// byte[] bytes = svcProps.getPropertyBytes(key);
// props.put(key, bytes);
} else if (val != null) {
props.put(key, val.toString());
}
}
}
// Add URI scheme to props
final URI location = serviceInfo.getServiceID().getLocation();
if (location != null) {
props.put(SCHEME_PROPERTY, location.getScheme());
props.put(URI_PATH_PROPERTY, location.getPath());
}
props.put(NAMING_AUTHORITY_PROPERTY, serviceInfo.getServiceID().getServiceTypeID().getNamingAuthority());
final ServiceInfo si = ServiceInfo.create(sID.getServiceTypeID().getInternal(), serviceInfo.getServiceName(), location.getPort(), serviceInfo.getWeight(), serviceInfo.getPriority(), props);
return si;
}
/* (non-Javadoc)
* @see org.eclipse.ecf.discovery.AbstractDiscoveryContainerAdapter#getContainerName()
*/
public String getContainerName() {
return JMDNSPlugin.NAME;
}
}