diff --git a/pelix/framework.py b/pelix/framework.py index edc31d16..45a97c8c 100644 --- a/pelix/framework.py +++ b/pelix/framework.py @@ -896,7 +896,8 @@ def install_visiting(self, path, visitor, prefix=None): return bundles, failed - def register_service(self, bundle, clazz, service, properties, send_event): + def register_service(self, bundle, clazz, service, properties, send_event, + factory=False, prototype=False): """ Registers a service and calls the listeners @@ -904,6 +905,9 @@ def register_service(self, bundle, clazz, service, properties, send_event): :param clazz: Name(s) of the interface(s) implemented by service :param properties: Service properties :param send_event: If not, doesn't trigger a service registered event + :param factory: If True, the given service is a service factory + :param prototype: If True, the given service is a prototype service + factory (the factory argument is considered True) :return: A ServiceRegistration object :raise BundleException: An error occurred while registering the service """ @@ -938,8 +942,8 @@ def register_service(self, bundle, clazz, service, properties, send_event): classes.append(svc_clazz) # Make the service registration - registration = self._registry.register(bundle, classes, properties, - service) + registration = self._registry.register( + bundle, classes, properties, service, factory, prototype) # Update the bundle registration information bundle._registered_service(registration) @@ -1405,7 +1409,8 @@ def install_visiting(self, path, visitor): """ return self.__framework.install_visiting(path, visitor) - def register_service(self, clazz, service, properties, send_event=True): + def register_service(self, clazz, service, properties, send_event=True, + factory=False, prototype=False): """ Registers a service @@ -1413,11 +1418,15 @@ def register_service(self, clazz, service, properties, send_event=True): :param service: The service instance :param properties: The services properties (dictionary) :param send_event: If not, doesn't trigger a service registered event + :param factory: If True, the given service is a service factory + :param prototype: If True, the given service is a prototype service + factory (the factory argument is considered True) :return: A ServiceRegistration object :raise BundleException: An error occurred while registering the service """ return self.__framework.register_service( - self.__bundle, clazz, service, properties, send_event) + self.__bundle, clazz, service, properties, send_event, + factory, prototype) def remove_bundle_listener(self, listener): """ diff --git a/pelix/internals/registry.py b/pelix/internals/registry.py index 1a2ce1d2..2f16c5d5 100644 --- a/pelix/internals/registry.py +++ b/pelix/internals/registry.py @@ -32,7 +32,8 @@ # Pelix beans from pelix.constants import OBJECTCLASS, SERVICE_ID, SERVICE_RANKING, \ - SERVICE_BUNDLEID, SERVICE_SCOPE, SCOPE_SINGLETON, BundleException + SERVICE_BUNDLEID, SERVICE_SCOPE, SCOPE_SINGLETON, SCOPE_BUNDLE, \ + SCOPE_PROTOTYPE, BundleException from pelix.internals.events import ServiceEvent # Pelix utility modules @@ -220,6 +221,23 @@ def get_property_keys(self): with self._props_lock: return tuple(self.__properties.keys()) + def is_factory(self): + """ + Returns True if this reference points to a service factory + + :return: True if the service provides from a factory + """ + return self.__properties[SERVICE_SCOPE] in \ + (SCOPE_BUNDLE, SCOPE_PROTOTYPE) + + def is_prototype(self): + """ + Returns True if this reference points to a prototype service factory + + :return: True if the service provides from a prototype factory + """ + return self.__properties[SERVICE_SCOPE] == SCOPE_PROTOTYPE + def unused_by(self, bundle): """ Indicates that this reference is not being used anymore by the given @@ -686,6 +704,9 @@ def __init__(self, framework, logger=None): # Service reference -> Service instance self.__svc_registry = {} + # Service reference -> (Service factory, Service Registration) + self.__svc_factories = {} + # Specification -> Service references[] (always sorted) self.__svc_specs = {} @@ -707,12 +728,14 @@ def clear(self): """ with self.__svc_lock: self.__svc_registry.clear() + self.__svc_factories.clear() self.__svc_specs.clear() self.__bundle_svc.clear() self.__bundle_imports.clear() self.__pending_services.clear() - def register(self, bundle, classes, properties, svc_instance): + def register(self, bundle, classes, properties, svc_instance, + factory, prototype): """ Registers a service. @@ -720,6 +743,9 @@ def register(self, bundle, classes, properties, svc_instance): :param classes: The classes implemented by the service :param properties: The properties associated to the service :param svc_instance: The instance of the service + :param factory: If True, the given service is a service factory + :param prototype: If True, the given service is a prototype service + factory (the factory argument is considered True) :return: The ServiceRegistration object """ with self.__svc_lock: @@ -729,7 +755,14 @@ def register(self, bundle, classes, properties, svc_instance): properties[OBJECTCLASS] = classes properties[SERVICE_ID] = service_id properties[SERVICE_BUNDLEID] = bundle.get_bundle_id() - properties[SERVICE_SCOPE] = SCOPE_SINGLETON + + # Compute service scope + if prototype: + properties[SERVICE_SCOPE] = SCOPE_PROTOTYPE + elif factory: + properties[SERVICE_SCOPE] = SCOPE_BUNDLE + else: + properties[SERVICE_SCOPE] = SCOPE_SINGLETON # Force to have a valid service ranking try: @@ -745,6 +778,10 @@ def register(self, bundle, classes, properties, svc_instance): self.__framework, svc_ref, properties, self.__sort_registry) # Store service information + if prototype or factory: + self.__svc_factories[svc_ref] = (svc_instance, svc_registration) + + # Also store factories, as they must appear like any other service self.__svc_registry[svc_ref] = svc_instance for spec in classes: @@ -807,6 +844,11 @@ def unregister(self, svc_ref): # Get the service instance service = self.__svc_registry.pop(svc_ref) + # Remove the service factory + if svc_ref.is_factory(): + # TODO: notify the factory ? + del self.__svc_factories[svc_ref] + for spec in svc_ref.get_property(OBJECTCLASS): spec_services = self.__svc_specs[spec] # Use bisect to remove the reference (faster) @@ -954,6 +996,9 @@ def get_service(self, bundle, reference): :return: The requested service :raise BundleException: The service could not be found """ + if reference.is_factory(): + return self.__get_service_from_factory(bundle, reference) + with self.__svc_lock: # Be sure to have the instance try: @@ -969,6 +1014,28 @@ def get_service(self, bundle, reference): raise BundleException("Service not found (reference: {0})" .format(reference)) + def __get_service_from_factory(self, bundle, reference): + """ + Returns a service instance from a service factory or a prototype + service factory + + :param bundle: The bundle requiring the service + :param reference: A reference pointing to a factory + :return: The requested service + :raise BundleException: The service could not be found + """ + with self.__svc_lock: + try: + # TODO: increase usage counter + # TODO: only one call per factory per bundle + # TODO; handle prototypes + factory, svc_reg = self.__svc_factories[reference] + return factory.get_service(bundle, svc_reg) + except KeyError: + # Not found + raise BundleException("Service not found (reference: {0})" + .format(reference)) + def unget_service(self, bundle, reference): """ Removes the usage of a service by a bundle @@ -988,6 +1055,12 @@ def unget_service(self, bundle, reference): # Unknown reference return False else: + # Service factory + # TODO: better handling + if reference.is_factory(): + factory, svc_reg = self.__svc_factories[reference] + factory.unget_service(bundle, svc_reg) + # Clean up if not imports: del self.__bundle_imports[bundle] diff --git a/tests/framework/service_factory_bundle.py b/tests/framework/service_factory_bundle.py new file mode 100644 index 00000000..9514eac8 --- /dev/null +++ b/tests/framework/service_factory_bundle.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -- Content-Encoding: UTF-8 -- +""" +Simple bundle registering a service + +:author: Thomas Calmant +""" + +__version__ = (1, 0, 0) + +SVC = "greetings" + +from pelix.constants import BundleActivator + + +class Service: + """ + Service per bundle + """ + def __init__(self, bundle_id): + self.__id = bundle_id + + def show(self): + return self.__id + + +class ServiceFactoryTest: + """ + Simple test service + """ + def get_service(self, bundle, registration): + """ + Provide a new service + """ + return Service(bundle.get_bundle_id()) + + +@BundleActivator +class ActivatorService: + """ + Test activator + """ + def __init__(self): + """ + Constructor + """ + self.context = None + self.factory = None + self.reg = None + + def start(self, context): + """ + Bundle started + """ + self.context = context + + # Register the service + self.factory = ServiceFactoryTest() + self.reg = context.register_service( + SVC, self.factory, {"test": True, "answer": 0}, + factory=True) + + def stop(self, context): + """ + Bundle stopped + """ + self.reg.unregister() + self.reg = None diff --git a/tests/framework/test_service_factories.py b/tests/framework/test_service_factories.py new file mode 100644 index 00000000..2e4ae612 --- /dev/null +++ b/tests/framework/test_service_factories.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -- Content-Encoding: UTF-8 -- +""" +Pelix framework test module. Tests the framework, bundles handling, service +handling and events. + +:author: Thomas Calmant +""" + +# Pelix +from pelix.framework import FrameworkFactory, Bundle, BundleException, \ + BundleContext, ServiceReference +import pelix.constants + +# Standard library +try: + import unittest2 as unittest +except ImportError: + import unittest + +# ------------------------------------------------------------------------------ + +__version__ = "1.0.0" + +# ------------------------------------------------------------------------------ + + +class ServicesTest(unittest.TestCase): + """ + Pelix services registry tests + """ + def setUp(self): + """ + Called before each test. Initiates a framework and loads the current + module as the first bundle + """ + self.test_bundle_name = "tests.framework.service_factory_bundle" + + self.framework = FrameworkFactory.get_framework() + self.framework.start() + + def tearDown(self): + """ + Called after each test + """ + self.framework.stop() + FrameworkFactory.delete_framework() + + def test_factory(self): + """ + Tests the basic behaviour of a service factory + """ + context = self.framework.get_bundle_context() + bundle = context.install_bundle(self.test_bundle_name) + module = bundle.get_module() + bundle.start() + + svc_ref = context.get_service_reference(module.SVC) + svc = context.get_service(svc_ref) + self.assertEqual(svc.show(), 0) + context.unget_service(svc_ref) + + svc = context.get_service(svc_ref) + self.assertEqual(svc.show(), 0) + context.unget_service(svc_ref) + + bc = bundle.get_bundle_context() + svc = bc.get_service(svc_ref) + self.assertEqual(svc.show(), bundle.get_bundle_id()) + bc.unget_service(svc_ref) + +# ------------------------------------------------------------------------------ + +if __name__ == "__main__": + # Set logging level + import logging + logging.basicConfig(level=logging.DEBUG) + + unittest.main()