Skip to content

Commit

Permalink
First implementation of the @RequiresBest handler
Browse files Browse the repository at this point in the history
Always injects the service with the best ranking.
See #29
  • Loading branch information
tcalmant committed Nov 3, 2014
1 parent c5a0c9c commit 252f787
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 0 deletions.
3 changes: 3 additions & 0 deletions pelix/ipopo/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
HANDLER_REQUIRES = 'ipopo.requires'
""" The @Requires handler ID """

HANDLER_REQUIRES_BEST = 'ipopo.requires.best'
""" The @RequiresBest handler ID """

HANDLER_REQUIRES_MAP = 'ipopo.requires.map'
""" The @RequiresMap handler ID """

Expand Down
1 change: 1 addition & 0 deletions pelix/ipopo/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
BUILTIN_HANDLERS = ('pelix.ipopo.handlers.properties',
'pelix.ipopo.handlers.provides',
'pelix.ipopo.handlers.requires',
'pelix.ipopo.handlers.requiresbest',
'pelix.ipopo.handlers.requiresmap',
'pelix.ipopo.handlers.temporal')

Expand Down
83 changes: 83 additions & 0 deletions pelix/ipopo/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,89 @@ def __call__(self, clazz):
# ------------------------------------------------------------------------------


class RequiresBest(object):
"""
@RequiresBest decorator
Defines a requirement of the service with best ranking
"""
HANDLER_ID = constants.HANDLER_REQUIRES_BEST
""" ID of the handler configured by this decorator """

def __init__(self, field, specification, optional=False, spec_filter=None,
immediate_rebind=True):
"""
Sets up the requirement
:param field: The injected field
:param specification: The injected service specification
:param optional: If true, this injection is optional
:param spec_filter: An LDAP query to filter injected services upon
their properties
:param immediate_rebind: If True, the component won't be invalidated
then re-validated if a matching service is
available when the injected dependency is
unbound
:raise TypeError: A parameter has an invalid type
:raise ValueError: An error occurred while parsing the filter or an
argument is incorrect
"""
if not field:
raise ValueError("Empty field name.")

if not is_string(field):
raise TypeError("The field name must be a string, not {0}"
.format(type(field).__name__))

if ' ' in field:
raise ValueError("Field name can't contain spaces.")

self.__field = field

# Be sure that there is only one required specification
specifications = _get_specifications(specification)
self.__multi_specs = len(specifications) > 1

# Construct the requirement object
self.__requirement = Requirement(specifications[0], False,
optional, spec_filter,
immediate_rebind)

def __call__(self, clazz):
"""
Adds the requirement to the class iPOPO field
:param clazz: The class to decorate
:return: The decorated class
:raise TypeError: If *clazz* is not a type
"""
if not inspect.isclass(clazz):
raise TypeError("@RequiresBest can decorate only classes, not '{0}'"
.format(type(clazz).__name__))

if self.__multi_specs:
_logger.warning("Only one specification can be required: %s -> %s",
clazz.__name__, self.__field)

# Set up the property in the class
context = get_factory_context(clazz)
if context.completed:
# Do nothing if the class has already been manipulated
_logger.warning("@RequiresBest: Already manipulated class: %s",
get_method_description(clazz))
return clazz

# Store the requirement information
config = context.set_handler_default(self.HANDLER_ID, {})
config[self.__field] = self.__requirement

# Inject the field
setattr(clazz, self.__field, None)
return clazz

# ------------------------------------------------------------------------------


class RequiresMap(object):
"""
@RequiresMap decorator
Expand Down
212 changes: 212 additions & 0 deletions pelix/ipopo/handlers/requiresbest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env python
# -- Content-Encoding: UTF-8 --
"""
RequiresBest handler implementation
:author: Thomas Calmant
:copyright: Copyright 2014, isandlaTech
:license: Apache License 2.0
:version: 0.5.9
:status: Beta
..
Copyright 2014 isandlaTech
Licensed 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.
"""

# Module version
__version_info__ = (0, 5, 9)
__version__ = ".".join(str(x) for x in __version_info__)

# Documentation strings format
__docformat__ = "restructuredtext en"

# ------------------------------------------------------------------------------

# Pelix beans
from pelix.constants import BundleActivator, SERVICE_RANKING

# iPOPO constants
import pelix.ipopo.constants as ipopo_constants
import pelix.ipopo.handlers.constants as constants
import pelix.ipopo.handlers.requires as requires

# ------------------------------------------------------------------------------


class _HandlerFactory(requires._HandlerFactory):
"""
Factory service for service registration handlers
"""
def get_handlers(self, component_context, instance):
"""
Sets up service providers for the given component
:param component_context: The ComponentContext bean
:param instance: The component instance
:return: The list of handlers associated to the given component
"""
# Extract information from the context
requirements = component_context.get_handler(
ipopo_constants.HANDLER_REQUIRES_BEST)
requires_filters = component_context.properties.get(
ipopo_constants.IPOPO_REQUIRES_FILTERS, None)

# Prepare requirements
requirements = self._prepare_requirements(
requirements, requires_filters)

# Set up the runtime dependency handlers
return [BestDependency(field, requirement)
for field, requirement in requirements.items()]


@BundleActivator
class _Activator(object):
"""
The bundle activator
"""
def __init__(self):
"""
Sets up members
"""
self._registration = None

def start(self, context):
"""
Bundle started
"""
# Set up properties
properties = {constants.PROP_HANDLER_ID:
ipopo_constants.HANDLER_REQUIRES_BEST}

# Register the handler factory service
self._registration = context.register_service(
constants.SERVICE_IPOPO_HANDLER_FACTORY,
_HandlerFactory(), properties)

def stop(self, context):
"""
Bundle stopped
"""
# Unregister the service
self._registration.unregister()
self._registration = None

# ------------------------------------------------------------------------------


class BestDependency(requires.SimpleDependency):
"""
Manages a simple dependency field
TODO: Allow to use a custom service reference comparator
"""
def __init__(self, field, requirement):
"""
Sets up members
"""
super(BestDependency, self).__init__(field, requirement)

# Current ranking
self._current_ranking = None

def clear(self):
"""
Cleans up the manager. The manager can't be used after this method has
been called
"""
self._current_ranking = None
super(BestDependency, self).clear()

def on_service_arrival(self, svc_ref):
"""
Called when a service has been registered in the framework
:param svc_ref: A service reference
"""
with self._lock:
new_ranking = svc_ref.get_property(SERVICE_RANKING)
if self._current_ranking is not None:
if new_ranking > self._current_ranking:
# New service with better ranking: use it
self._pending_ref = svc_ref
old_ref = self.reference
old_value = self._value

# Clean up like for a departure
self._current_ranking = None
self._value = None
self.reference = None

# Unbind (new binding will be done afterwards)
self._ipopo_instance.unbind(self, old_value, old_ref)
else:
# No ranking yet: inject the service
self.reference = svc_ref
self._value = self._context.get_service(svc_ref)
self._current_ranking = new_ranking
self._pending_ref = None

self._ipopo_instance.bind(self, self._value, self.reference)

def on_service_departure(self, svc_ref):
"""
Called when a service has been unregistered from the framework
:param svc_ref: A service reference
"""
with self._lock:
if svc_ref is self.reference:
# Injected service going away...
service = self._value

# Clear the instance values
self._current_ranking = None
self._value = None
self.reference = None

if self.requirement.immediate_rebind:
# Look for a replacement
self._pending_ref = self._context.get_service_reference(
self.requirement.specification,
self.requirement.filter)
else:
self._pending_ref = None

self._ipopo_instance.unbind(self, service, svc_ref)

def on_service_modify(self, svc_ref, old_properties):
"""
Called when a service has been modified in the framework
:param svc_ref: A service reference
:param old_properties: Previous properties values
"""
with self._lock:
if self.reference is None:
# A previously registered service now matches our filter
return self.on_service_arrival(svc_ref)
else:
# Check if the ranking changed the service to inject
best_ref = self._context.get_service_reference(
self.requirement.specification, self.requirement.filter)
if best_ref is self.reference:
# Still the best service: notify the property modification
self._ipopo_instance.update(self, self._value, svc_ref,
old_properties)
else:
# A new service is now the best: start a departure loop
self.on_service_departure(svc_ref)

0 comments on commit 252f787

Please sign in to comment.