/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.aries.jndi.services;
import java.lang.ref.WeakReference;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.naming.NamingException;
import org.apache.aries.jndi.url.Activator;
import org.apache.aries.jndi.url.OsgiName;
import org.apache.aries.proxy.ProxyManager;
import org.apache.aries.proxy.UnableToProxyException;
import org.apache.aries.util.nls.MessageUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceException;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.jndi.JNDIConstants;
/**
* This helper provides access to services registered in the OSGi service registry.
* If a matching service cannot be located null may be returned. A caller should not
* expect to get the same object if multiple requests are made to this API. A caller
* should not expect to get a different object if multiple requests are made to this API.
* A caller should avoid caching the returned service. OSGi is a dynamic environment and
* the service may become unavailable while a reference to it is held. To minimize this
* risk the caller should hold onto the service for the minimum length of time.
*
* <p>This API should not be used from within an OSGi bundle. When in an OSGi environment
* the BundleContext for the bundle should be used to obtain the service.
* </p>
*/
public final class ServiceHelper
{
public static final class CacheClearoutListener implements BundleListener, ServiceListener
{
/** The cache to purge */
private final ConcurrentMap<ServiceKey, WeakReference<Object>> cache;
public CacheClearoutListener(ConcurrentMap<ServiceKey, WeakReference<Object>> pc)
{
cache = pc;
}
public void bundleChanged(BundleEvent event)
{
if (event.getType() == BundleEvent.STOPPED) {
Bundle b = event.getBundle();
Iterator<ServiceKey> keys = cache.keySet().iterator();
while (keys.hasNext()) {
ServiceKey key = keys.next();
if (key.requesting == b) keys.remove();
}
}
}
public void serviceChanged(ServiceEvent event)
{
if (event.getType() == ServiceEvent.UNREGISTERING) {
ServiceReference ref = event.getServiceReference();
Long serviceId = (Long) ref.getProperty(Constants.SERVICE_ID);
Bundle registeringBundle = ref.getBundle();
Iterator<ServiceKey> keys = cache.keySet().iterator();
while (keys.hasNext()) {
ServiceKey key = keys.next();
if (key.registering == registeringBundle && serviceId.equals(key.serviceId)) {
keys.remove();
break;
}
}
}
}
public void add(final BundleContext ctx, ServiceKey k)
{
// try to use the system bundle for our listener, if that fails we fall back to the calling context
BundleContext systemBundle = AccessController.doPrivileged(new PrivilegedAction<BundleContext>() {
public BundleContext run()
{
Bundle system = ctx.getBundle(0);
return system == null ? null : system.getBundleContext();
}
});
if (systemBundle == null) systemBundle = ctx;
systemBundle.addBundleListener(cacheClearoutListener);
systemBundle.addServiceListener(cacheClearoutListener);
}
}
private static final class ServiceKey
{
private final Bundle requesting;
private final Bundle registering;
private final Long serviceId;
private final int hash;
public ServiceKey(Bundle owningBundle, Bundle registeringBundle, Long property)
{
requesting = owningBundle;
registering = registeringBundle;
serviceId = property;
hash = serviceId.intValue() * 100003 + System.identityHashCode(requesting);
}
public int hashCode()
{
return hash;
}
public boolean equals(Object other)
{
if (other == this) return true;
if (other == null) return false;
if (other instanceof ServiceKey) {
ServiceKey otherKey = (ServiceKey) other;
return (otherKey.requesting == requesting && otherKey.serviceId.equals(serviceId));
}
return false;
}
}
private static class JNDIServiceDamper implements Callable<Object>
{
private BundleContext ctx;
private ServicePair pair;
private String interfaceName;
private String filter;
private boolean dynamic;
private int rebindTimeout;
public JNDIServiceDamper(BundleContext context, String i, String f, ServicePair service,
boolean d, int timeout)
{
ctx = context;
pair = service;
interfaceName = i;
filter = f;
dynamic = d;
rebindTimeout = timeout;
}
public Object call() throws NamingException {
if (pair == null || pair.ref.getBundle() == null) {
if (dynamic) {
pair = findService(ctx, interfaceName, filter);
if (pair == null && rebindTimeout > 0) {
long startTime = System.currentTimeMillis();
try {
while (pair == null && System.currentTimeMillis() - startTime < rebindTimeout) {
Thread.sleep(100);
pair = findService(ctx, interfaceName, filter);
}
} catch (InterruptedException e) {
}
}
} else {
pair = null;
}
}
if (pair == null) {
throw new ServiceException(interfaceName, ServiceException.UNREGISTERED);
}
return pair.service;
}
}
private static class ServicePair
{
private ServiceReference ref;
private Object service;
}
/** A cache of proxies returned to the client */
private static final ConcurrentMap<ServiceKey, WeakReference<Object>> proxyCache = new ConcurrentHashMap<ServiceKey, WeakReference<Object>>();
private static final CacheClearoutListener cacheClearoutListener = new CacheClearoutListener(proxyCache);
private static final MessageUtil MESSAGES = MessageUtil.createMessageUtil(ServiceHelper.class, "org.apache.aries.jndi.nls.jndiUrlMessages");
public static Object getService(BundleContext ctx, OsgiName lookupName, String id,
boolean dynamicRebind, Map<String, Object> env, boolean requireProxy) throws NamingException
{
String interfaceName = lookupName.getInterface();
String filter = lookupName.getFilter();
String serviceName = lookupName.getServiceName();
if (id != null) {
if (filter == null) {
filter = '(' + Constants.SERVICE_ID + '=' + id + ')';
} else {
filter = "(&(" + Constants.SERVICE_ID + '=' + id + ')' + filter + ')';
}
}
ServicePair pair = null;
if (!!!lookupName.isServiceNameBased()) {
pair = findService(ctx, interfaceName, filter);
}
if (pair == null) {
interfaceName = null;
if (id == null) {
filter = "(" + JNDIConstants.JNDI_SERVICENAME + "=" + serviceName + ')';
} else {
filter = "(&(" + Constants.SERVICE_ID + '=' + id + ")(" + JNDIConstants.JNDI_SERVICENAME
+ "=" + serviceName + "))";
}
pair = findService(ctx, interfaceName, filter);
}
Object result = null;
if (pair != null) {
if (requireProxy) {
Object obj = env.get(org.apache.aries.jndi.api.JNDIConstants.REBIND_TIMEOUT);
int timeout = 0;
if (obj instanceof String) {
timeout = Integer.parseInt((String)obj);
} else if (obj instanceof Integer) {
timeout = (Integer)obj;
}
result = proxy(interfaceName, filter, dynamicRebind, ctx, pair, timeout);
} else {
result = pair.service;
}
}
return result;
}
private static Object proxy(final String interface1, final String filter, final boolean rebind,
final BundleContext ctx, final ServicePair pair, final int timeout)
{
Object result = null;
Bundle owningBundle = ctx.getBundle();
ServiceKey k = new ServiceKey(owningBundle, pair.ref.getBundle(), (Long) pair.ref.getProperty(Constants.SERVICE_ID));
WeakReference<Object> proxyRef = proxyCache.get(k);
if (proxyRef != null) {
result = proxyRef.get();
if (result == null) {
proxyCache.remove(k, proxyRef);
}
}
if (result == null) {
result = AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run()
{
return proxyPrivileged(interface1, filter, rebind, ctx, pair, timeout);
}
});
proxyRef = new WeakReference<Object>(result);
// if we have two threads doing a put and then clashing we ignore it. The code to ensure only
// one wins is quite complex to save a few bytes of memory and millis of execution time.
proxyCache.putIfAbsent(k, proxyRef);
cacheClearoutListener.add(ctx, k);
}
return result;
}
private static Object proxyPrivileged(String interface1, String filter, boolean dynamicRebind, BundleContext ctx, ServicePair pair, int timeout)
{
String[] interfaces = null;
if (interface1 != null) {
interfaces = new String[] { interface1 };
} else {
interfaces = (String[]) pair.ref.getProperty(Constants.OBJECTCLASS);
}
List<Class<?>> clazz = new ArrayList<Class<?>>(interfaces.length);
// We load the interface classes the service is registered under using the defining bundle.
// This is ok because the service must be able to see the classes to be registered using them.
// We then check to see if isAssignableTo on the reference works for the owning bundle and
// the interface name and only use the interface if true is returned there.
// This might seem odd, but equinox and felix return true for isAssignableTo if the
// Bundle provided does not import the package. This is under the assumption the
// caller will then use reflection. The upshot of doing it this way is that a utility
// bundle can be created which centralizes JNDI lookups, but the service will be used
// by another bundle. It is true that class space consistency is less safe, but we
// are enabling a slightly odd use case anyway.
// August 13th 2013: We've found valid use cases in which a Bundle is exporting
// services that the Bundle itself cannot load. We deal with this rare case by
// noting the classes that we failed to load. If as a result we have no classes
// to proxy, we try those classes again but instead pull the Class objects off
// the service rather than from the bundle exporting that service.
Bundle serviceProviderBundle = pair.ref.getBundle();
Bundle owningBundle = ctx.getBundle();
ProxyManager proxyManager = Activator.getProxyManager();
Collection<String> classesNotFound = new ArrayList<String>();
for (String interfaceName : interfaces) {
try {
Class<?> potentialClass = serviceProviderBundle.loadClass(interfaceName);
if (pair.ref.isAssignableTo(owningBundle, interfaceName)) {
clazz.add(potentialClass);
}
} catch (ClassNotFoundException e) {
classesNotFound.add(interfaceName);
}
}
if (clazz.isEmpty() && !classesNotFound.isEmpty()) {
Class<?> ifacesOnService[] = ctx.getService(pair.ref).getClass().getInterfaces();
for (String interfaceName : classesNotFound) {
Class<?> thisClass = null;
for (Class<?> c : getAllInterfaces(ifacesOnService)) {
if (c.getName().equals(interfaceName)) {
thisClass = c;
break;
}
}
if (thisClass != null) {
if (pair.ref.isAssignableTo(owningBundle, interfaceName)) {
clazz.add(thisClass);
}
}
}
}
if (clazz.isEmpty()) {
throw new IllegalArgumentException(Arrays.asList(interfaces).toString());
}
Callable<Object> ih = new JNDIServiceDamper(ctx, interface1, filter, pair, dynamicRebind, timeout);
// The ClassLoader needs to be able to load the service interface
// classes so it needs to be
// wrapping the service provider bundle. The class is actually defined
// on this adapter.
try {
return proxyManager.createDelegatingProxy(serviceProviderBundle, clazz, ih, null);
} catch (UnableToProxyException e) {
throw new IllegalArgumentException(e);
} catch (RuntimeException e) {
throw new IllegalArgumentException(MESSAGES.getMessage("unable.to.create.proxy", pair.ref), e);
}
}
private static ServicePair findService(BundleContext ctx, String interface1, String filter)
throws NamingException
{
ServicePair p = null;
try {
ServiceReference[] refs = ctx.getServiceReferences(interface1, filter);
if (refs != null) {
// natural order is the exact opposite of the order we desire.
Arrays.sort(refs, new Comparator<ServiceReference>() {
public int compare(ServiceReference o1, ServiceReference o2)
{
return o2.compareTo(o1);
}
});
for (ServiceReference ref : refs) {
Object service = ctx.getService(ref);
if (service != null) {
p = new ServicePair();
p.ref = ref;
p.service = service;
break;
}
}
}
} catch (InvalidSyntaxException e) {
// If we get an invalid syntax exception we just ignore it. Null
// will be returned which
// is valid and that may result in a NameNotFoundException if that
// is the right thing to do
}
return p;
}
public static ServiceReference[] getServiceReferences(BundleContext ctx, String interface1,
String filter, String serviceName, Map<String, Object> env) throws NamingException
{
ServiceReference[] refs = null;
try {
refs = ctx.getServiceReferences(interface1, filter);
if (refs == null || refs.length == 0) {
refs = ctx.getServiceReferences((String) null, "(" + JNDIConstants.JNDI_SERVICENAME + "="
+ serviceName + ')');
}
} catch (InvalidSyntaxException e) {
throw (NamingException) new NamingException(e.getFilter()).initCause(e);
}
if (refs != null) {
// natural order is the exact opposite of the order we desire.
Arrays.sort(refs, new Comparator<ServiceReference>() {
public int compare(ServiceReference o1, ServiceReference o2)
{
return o2.compareTo(o1);
}
});
}
return refs;
}
public static Object getService(BundleContext ctx, ServiceReference ref)
{
Object service = ctx.getService(ref);
if (service == null) {
return null;
}
ServicePair pair = new ServicePair();
pair.ref = ref;
pair.service = service;
return proxy(null, null, false, ctx, pair, 0);
}
static Collection<Class<?>> getAllInterfaces (Class<?>[] baseInterfaces)
{
Set<Class<?>> result = new HashSet<Class<?>>();
for (Class<?> c : baseInterfaces) {
if (!c.equals(Object.class)) {
result.add (c);
Class<?> ifaces[] = c.getInterfaces();
if (ifaces.length != 0) {
result.addAll(getAllInterfaces(ifaces));
}
}
}
return result;
}
}