blob: 9bf19c0bd6cee95078f4a5ca48389611f0447ebe [file] [log] [blame]
/*
* 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.felix.connect.felix.framework;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.felix.connect.felix.framework.capabilityset.CapabilitySet;
import org.apache.felix.connect.felix.framework.capabilityset.SimpleFilter;
import org.apache.felix.connect.felix.framework.wiring.BundleCapabilityImpl;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.resource.Capability;
public class ServiceRegistry
{
/** Counter for the service id */
private final AtomicLong m_currentServiceId = new AtomicLong(1);
// Maps bundle to an array of service registrations.
private final ConcurrentMap<Bundle, List<ServiceRegistration<?>>> m_regsMap = new ConcurrentHashMap<Bundle, List<ServiceRegistration<?>>>();
// Capability set for all service registrations.
private final CapabilitySet m_regCapSet = new CapabilitySet(Collections.singletonList(Constants.OBJECTCLASS), false);
// Maps bundle to an array of usage counts.
private final ConcurrentMap<Bundle, UsageCount[]> m_inUseMap = new ConcurrentHashMap<Bundle, UsageCount[]>();
private final ServiceRegistryCallbacks m_callbacks;
private final HookRegistry hookRegistry = new HookRegistry();
public ServiceRegistry(final ServiceRegistryCallbacks callbacks)
{
m_callbacks = callbacks;
}
/**
* Get all service references for a bundle
* @param bundle
* @return List with all valid service references or {@code null}.
*/
public ServiceReference<?>[] getRegisteredServices(final Bundle bundle)
{
final List<ServiceRegistration<?>> regs = m_regsMap.get(bundle);
if (regs != null)
{
final List<ServiceReference<?>> refs = new ArrayList<ServiceReference<?>>(regs.size());
// this is a per bundle list, therefore synchronizing this should be fine
synchronized ( regs )
{
for (final ServiceRegistration<?> reg : regs)
{
try
{
refs.add(reg.getReference());
}
catch (final IllegalStateException ex)
{
// Don't include the reference as it is not valid anymore
}
}
}
return refs.toArray(new ServiceReference[refs.size()]);
}
return null;
}
/**
* Register a new service
*
* Caller must fire service event as this method is not doing it!
*
* @param bundle The bundle registering the service
* @param classNames The service class names
* @param svcObj The service object
* @param dict Optional service properties
* @return Service registration
*/
public ServiceRegistration<?> registerService(
final Bundle bundle,
final String[] classNames,
final Object svcObj,
final Dictionary<?,?> dict)
{
// Create the service registration.
final ServiceRegistrationImpl reg = new ServiceRegistrationImpl(
this, bundle, classNames, m_currentServiceId.getAndIncrement(), svcObj, dict);
// Keep track of registered hooks.
this.hookRegistry.addHooks(classNames, svcObj, reg.getReference());
// Get the bundles current registered services.
final List<ServiceRegistration<?>> newRegs = new ArrayList<ServiceRegistration<?>>();
List<ServiceRegistration<?>> regs = m_regsMap.putIfAbsent(bundle, newRegs);
if (regs == null)
{
regs = newRegs;
}
// this is a per bundle list, therefore synchronizing this should be fine
synchronized ( regs )
{
regs.add(reg);
}
m_regCapSet.addCapability((BundleCapabilityImpl) reg.getReference());
return reg;
}
/**
* Unregister a service
* @param bundle The bundle unregistering the service
* @param reg The service registration
*/
public void unregisterService(
final Bundle bundle,
final ServiceRegistration<?> reg)
{
// If this is a hook, it should be removed.
this.hookRegistry.removeHooks(reg.getReference());
// Now remove the registered service.
final List<ServiceRegistration<?>> regs = m_regsMap.get(bundle);
if (regs != null)
{
// this is a per bundle list, therefore synchronizing this should be fine
synchronized ( regs )
{
regs.remove(reg);
}
}
m_regCapSet.removeCapability((BundleCapabilityImpl) reg.getReference());
// Notify callback objects about unregistering service.
if (m_callbacks != null)
{
m_callbacks.serviceChanged(
new ServiceEvent(ServiceEvent.UNREGISTERING, reg.getReference()), null);
}
// Now forcibly unget the service object for all stubborn clients.
final ServiceReference<?> ref = reg.getReference();
ungetServices(ref);
// Invalidate registration
((ServiceRegistrationImpl) reg).invalidate();
// Bundles are allowed to get a reference while unregistering
// get fresh set of bundles (should be empty, but this is a sanity check)
ungetServices(ref);
// Now flush all usage counts as the registration is invalid
for (Bundle usingBundle : m_inUseMap.keySet())
{
flushUsageCount(usingBundle, ref, null);
}
}
private void ungetServices(final ServiceReference<?> ref)
{
final Bundle[] clients = getUsingBundles(ref);
for (int i = 0; (clients != null) && (i < clients.length); i++)
{
final UsageCount[] usages = m_inUseMap.get(clients[i]);
for (int x = 0; (usages != null) && (x < usages.length); x++)
{
if (usages[x].m_ref.equals(ref))
{
ungetService(clients[i], ref, (usages[x].m_prototype ? usages[x].getService() : null));
}
}
}
}
/**
* This method retrieves all services registrations for the specified bundle and
* invokes {@code ServiceRegistration.unregister()} on each one. This method is
* only called be the framework to clean up after a stopped bundle.
*
* @param bundle the bundle whose services should be unregistered.
**/
public void unregisterServices(final Bundle bundle)
{
// Simply remove all service registrations for the bundle.
final List<ServiceRegistration<?>> regs = m_regsMap.remove(bundle);
// Note, there is no race condition here with respect to the
// bundle registering more services, because its bundle context
// has already been invalidated by this point, so it would not
// be able to register more services.
// Unregister each service.
if (regs != null)
{
final List<ServiceRegistration<?>> copyRefs;
// there shouldn't be a need to sync, but just to be safe
// we create a copy array and use that for iterating
synchronized ( regs )
{
copyRefs = new ArrayList<ServiceRegistration<?>>(regs);
}
for (final ServiceRegistration<?> reg : copyRefs)
{
if (((ServiceRegistrationImpl) reg).isValid())
{
try
{
reg.unregister();
}
catch (final IllegalStateException e)
{
// Ignore exception if the service has already been unregistered
}
}
}
}
}
public Collection<Capability> getServiceReferences(final String className, SimpleFilter filter)
{
if ((className == null) && (filter == null))
{
// Return all services.
filter = new SimpleFilter(null, null, SimpleFilter.MATCH_ALL);
}
else if ((className != null) && (filter == null))
{
// Return services matching the class name.
filter = new SimpleFilter(Constants.OBJECTCLASS, className, SimpleFilter.EQ);
}
else if ((className != null) && (filter != null))
{
// Return services matching the class name and filter.
final List<SimpleFilter> filters = new ArrayList<SimpleFilter>(2);
filters.add(new SimpleFilter(Constants.OBJECTCLASS, className, SimpleFilter.EQ));
filters.add(filter);
filter = new SimpleFilter(null, filters, SimpleFilter.AND);
}
// else just use the specified filter.
return m_regCapSet.match(filter, false);
}
public ServiceReference<?>[] getServicesInUse(final Bundle bundle)
{
final UsageCount[] usages = m_inUseMap.get(bundle);
if (usages != null)
{
final ServiceReference<?>[] refs = new ServiceReference[usages.length];
for (int i = 0; i < refs.length; i++)
{
refs[i] = usages[i].m_ref;
}
return refs;
}
return null;
}
@SuppressWarnings("unchecked")
public <S> S getService(final Bundle bundle, final ServiceReference<S> ref, final boolean isServiceObjects)
{
// prototype scope is only possible if called from ServiceObjects
final boolean isPrototype = isServiceObjects && ref.getProperty(Constants.SERVICE_SCOPE) == Constants.SCOPE_PROTOTYPE;
UsageCount usage = null;
Object svcObj = null;
// Get the service registration.
final ServiceRegistrationImpl reg =
((ServiceRegistrationImpl.ServiceReferenceImpl) ref).getRegistration();
// We don't allow cycles when we call out to the service factory.
if ( reg.currentThreadMarked() )
{
throw new ServiceException(
"ServiceFactory.getService() resulted in a cycle.",
ServiceException.FACTORY_ERROR,
null);
}
try
{
reg.markCurrentThread();
// Make sure the service registration is still valid.
if (reg.isValid())
{
// Get the usage count, or create a new one. If this is a
// prototype, the we'll alway create a new one.
usage = obtainUsageCount(bundle, ref, null, isPrototype);
// Increment the usage count and grab the already retrieved
// service object, if one exists.
incrementToPositiveValue(usage.m_count);
svcObj = usage.getService();
if ( isServiceObjects )
{
incrementToPositiveValue(usage.m_serviceObjectsCount);
}
// If we have a usage count, but no service object, then we haven't
// cached the service object yet, so we need to create one.
if (usage != null)
{
ServiceHolder holder = null;
// There is a possibility that the holder is unset between the compareAndSet() and the get()
// below. If that happens get() returns null and we may have to set a new holder. This is
// why the below section is in a loop.
while (holder == null)
{
ServiceHolder h = new ServiceHolder();
if (usage.m_svcHolderRef.compareAndSet(null, h))
{
holder = h;
try {
svcObj = reg.getService(bundle);
holder.m_service = svcObj;
} finally {
holder.m_latch.countDown();
}
}
else
{
holder = usage.m_svcHolderRef.get();
if (holder != null)
{
boolean interrupted = false;
do
{
try
{
// Need to ensure that the other thread has obtained
// the service.
holder.m_latch.await();
if (interrupted)
{
Thread.currentThread().interrupt();
}
interrupted = false;
}
catch (InterruptedException e)
{
interrupted = true;
Thread.interrupted();
}
}
while (interrupted);
svcObj = holder.m_service;
}
}
// if someone concurrently changed the holder, loop again
if (holder != usage.m_svcHolderRef.get())
holder = null;
}
if (svcObj != null && isPrototype)
{
UsageCount existingUsage = obtainUsageCount(bundle, ref, svcObj, null);
if (existingUsage != null && existingUsage != usage)
{
flushUsageCount(bundle, ref, usage);
usage = existingUsage;
incrementToPositiveValue(usage.m_count);
if ( isServiceObjects )
{
incrementToPositiveValue(usage.m_serviceObjectsCount);
}
}
}
}
}
}
finally
{
reg.unmarkCurrentThread();
if (!reg.isValid() || (svcObj == null))
{
flushUsageCount(bundle, ref, usage);
}
}
return (S) svcObj;
}
// Increment the Atomic Long by 1, and ensure the result is at least 1.
// This method uses a loop, optimistic algorithm to do this in a threadsafe
// way without locks.
private void incrementToPositiveValue(AtomicLong al)
{
boolean success = false;
while (!success)
{
long oldVal = al.get();
long newVal = Math.max(oldVal + 1L, 1L);
checkCountOverflow(newVal);
success = al.compareAndSet(oldVal, newVal);
}
}
private void checkCountOverflow(long c)
{
if (c == Long.MAX_VALUE)
{
throw new ServiceException(
"The use count for the service overflowed.",
ServiceException.UNSPECIFIED,
null);
}
}
public boolean ungetService(final Bundle bundle, final ServiceReference<?> ref, final Object svcObj)
{
final ServiceRegistrationImpl reg =
((ServiceRegistrationImpl.ServiceReferenceImpl) ref).getRegistration();
if ( reg.currentThreadMarked() )
{
throw new IllegalStateException(
"ServiceFactory.ungetService() resulted in a cycle.");
}
try
{
// Mark the current thread to avoid cycles
reg.markCurrentThread();
// Get the usage count.
UsageCount usage = obtainUsageCount(bundle, ref, svcObj, null);
// If there are no cached services, then just return immediately.
if (usage == null)
{
return false;
}
// if this is a call from service objects and the service was not fetched from
// there, return false
if ( svcObj != null )
{
if (usage.m_serviceObjectsCount.decrementAndGet() < 0)
{
return false;
}
}
// If usage count will go to zero, then unget the service
// from the registration.
long count = usage.m_count.decrementAndGet();
try
{
if (count <= 0)
{
ServiceHolder holder = usage.m_svcHolderRef.get();
Object svc = holder != null ? holder.m_service : null;
if (svc != null)
{
// Check the count again to ensure that nobody else has just
// obtained the service again
if (usage.m_count.get() <= 0)
{
if (usage.m_svcHolderRef.compareAndSet(holder, null))
{
// Temporarily increase the usage again so that the
// service factory still sees the usage in the unget
usage.m_count.incrementAndGet();
try
{
// Remove reference from usages array.
reg.ungetService(bundle, svc);
}
finally
{
// now we can decrease the usage again
usage.m_count.decrementAndGet();
}
}
}
}
}
return count >= 0;
}
finally
{
if (!reg.isValid())
{
usage.m_svcHolderRef.set(null);
}
// If the registration is invalid or the usage count for a prototype
// reached zero, then flush it. Non-prototype services are not flushed
// on ungetService() when they reach 0 as this introduces a race
// condition of concurrently the same service is obtained via getService()
if (!reg.isValid() || (count <= 0 && svcObj != null))
{
flushUsageCount(bundle, ref, usage);
}
}
}
finally
{
reg.unmarkCurrentThread();
}
}
/**
* This is a utility method to release all services being
* used by the specified bundle.
* @param bundle the bundle whose services are to be released.
**/
public void ungetServices(final Bundle bundle)
{
UsageCount[] usages = m_inUseMap.get(bundle);
if (usages == null)
{
return;
}
// Note, there is no race condition here with respect to the
// bundle using more services, because its bundle context
// has already been invalidated by this point, so it would not
// be able to look up more services.
// Remove each service object from the
// service cache.
for (int i = 0; i < usages.length; i++)
{
if (usages[i].m_svcHolderRef.get() == null)
continue;
// Keep ungetting until all usage count is zero.
while (ungetService(bundle, usages[i].m_ref, usages[i].m_prototype ? usages[i].getService() : null))
{
// Empty loop body.
}
}
}
public Bundle[] getUsingBundles(ServiceReference<?> ref)
{
Bundle[] bundles = null;
for (Iterator<Map.Entry<Bundle, UsageCount[]>> iter = m_inUseMap.entrySet().iterator(); iter.hasNext(); )
{
Map.Entry<Bundle, UsageCount[]> entry = iter.next();
Bundle bundle = entry.getKey();
UsageCount[] usages = entry.getValue();
for (int useIdx = 0; useIdx < usages.length; useIdx++)
{
if (usages[useIdx].m_ref.equals(ref) && usages[useIdx].m_count.get() > 0)
{
// Add the bundle to the array to be returned.
if (bundles == null)
{
bundles = new Bundle[] { bundle };
}
else
{
Bundle[] nbs = new Bundle[bundles.length + 1];
System.arraycopy(bundles, 0, nbs, 0, bundles.length);
nbs[bundles.length] = bundle;
bundles = nbs;
}
}
}
}
return bundles;
}
void servicePropertiesModified(ServiceRegistration<?> reg, Dictionary<?,?> oldProps)
{
this.hookRegistry.updateHooks(reg.getReference());
if (m_callbacks != null)
{
m_callbacks.serviceChanged(
new ServiceEvent(ServiceEvent.MODIFIED, reg.getReference()), oldProps);
}
}
/**
* Obtain a UsageCount object, by looking for an existing one or creating a new one (if possible).
* This method tries to find a UsageCount object in the {@code m_inUseMap}. If one is found then
* this is returned, otherwise a UsageCount object will be created, but this can only be done if
* the {@code isPrototype} parameter is not {@code null}. If {@code isPrototype} is {@code TRUE}
* then a new UsageCount object will always be created.
* @param bundle The bundle using the service.
* @param ref The Service Reference.
* @param svcObj A Service Object, if applicable.
* @param isPrototype {@code TRUE} if we know that this is a prototype, {@ FALSE} if we know that
* it isn't. There are cases where we don't know (the pure lookup case), in that case use {@code null}.
* @return The UsageCount object if it could be obtained, or {@code null} otherwise.
*/
UsageCount obtainUsageCount(Bundle bundle, ServiceReference<?> ref, Object svcObj, Boolean isPrototype)
{
UsageCount usage = null;
// This method uses an optimistic concurrency mechanism with a conditional put/replace
// on the m_inUseMap. If this fails (because another thread made changes) this thread
// retries the operation. This is the purpose of the while loop.
boolean success = false;
while (!success)
{
UsageCount[] usages = m_inUseMap.get(bundle);
// If we know it's a prototype, then we always need to create a new usage count
if (!Boolean.TRUE.equals(isPrototype))
{
for (int i = 0; (usages != null) && (i < usages.length); i++)
{
if (usages[i].m_ref.equals(ref)
&& ((svcObj == null && !usages[i].m_prototype) || usages[i].getService() == svcObj))
{
return usages[i];
}
}
}
// We haven't found an existing usage count object so we need to create on. For this we need to
// know whether this is a prototype or not.
if (isPrototype == null)
{
// If this parameter isn't passed in we can't create a usage count.
return null;
}
// Add a new Usage Count.
usage = new UsageCount(ref, isPrototype);
if (usages == null)
{
UsageCount[] newUsages = new UsageCount[] { usage };
success = m_inUseMap.putIfAbsent(bundle, newUsages) == null;
}
else
{
UsageCount[] newUsages = new UsageCount[usages.length + 1];
System.arraycopy(usages, 0, newUsages, 0, usages.length);
newUsages[usages.length] = usage;
success = m_inUseMap.replace(bundle, usages, newUsages);
}
}
return usage;
}
/**
* Utility method to flush the specified bundle's usage count for the
* specified service reference. This should be called to completely
* remove the associated usage count object for the specified service
* reference. If the goal is to simply decrement the usage, then get
* the usage count and decrement its counter. This method will also
* remove the specified bundle from the "in use" map if it has no more
* usage counts after removing the usage count for the specified service
* reference.
* @param bundle The bundle whose usage count should be removed.
* @param ref The service reference whose usage count should be removed.
**/
void flushUsageCount(Bundle bundle, ServiceReference<?> ref, UsageCount uc)
{
// This method uses an optimistic concurrency mechanism with conditional modifications
// on the m_inUseMap. If this fails (because another thread made changes) this thread
// retries the operation. This is the purpose of the while loop.
boolean success = false;
while (!success)
{
UsageCount[] usages = m_inUseMap.get(bundle);
final UsageCount[] orgUsages = usages;
for (int i = 0; (usages != null) && (i < usages.length); i++)
{
if ((uc == null && usages[i].m_ref.equals(ref)) || (uc == usages[i]))
{
// If this is the only usage, then point to empty list.
if ((usages.length - 1) == 0)
{
usages = null;
}
// Otherwise, we need to do some array copying.
else
{
UsageCount[] newUsages = new UsageCount[usages.length - 1];
System.arraycopy(usages, 0, newUsages, 0, i);
if (i < newUsages.length)
{
System.arraycopy(
usages, i + 1, newUsages, i, newUsages.length - i);
}
usages = newUsages;
i--;
}
}
}
if (usages == orgUsages)
return; // no change in map
if (orgUsages != null)
{
if (usages != null)
success = m_inUseMap.replace(bundle, orgUsages, usages);
else
success = m_inUseMap.remove(bundle, orgUsages);
}
}
}
public HookRegistry getHookRegistry()
{
return this.hookRegistry;
}
static class UsageCount
{
final ServiceReference<?> m_ref;
final boolean m_prototype;
final AtomicLong m_count = new AtomicLong();
final AtomicLong m_serviceObjectsCount = new AtomicLong();
final AtomicReference<ServiceHolder> m_svcHolderRef = new AtomicReference<ServiceHolder>();
UsageCount(final ServiceReference<?> ref, final boolean isPrototype)
{
m_ref = ref;
m_prototype = isPrototype;
}
Object getService()
{
ServiceHolder sh = m_svcHolderRef.get();
return sh == null ? null : sh.m_service;
}
}
static class ServiceHolder
{
final CountDownLatch m_latch = new CountDownLatch(1);
volatile Object m_service;
}
public interface ServiceRegistryCallbacks
{
void serviceChanged(ServiceEvent event, Dictionary<?,?> oldProps);
}
}