1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
|
#--
# =============================================================================
# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu)
# All rights reserved.
#
# This source file is distributed as part of the Needle dependency injection
# library for Ruby. This file (and the library as a whole) may be used only as
# allowed by either the BSD license, or the Ruby license (or, by association
# with the Ruby license, the GPL). See the "doc" subdirectory of the Needle
# distribution for the texts of these licenses.
# -----------------------------------------------------------------------------
# needle website : http://needle.rubyforge.org
# project website: http://rubyforge.org/projects/needle
# =============================================================================
#++
require 'needle/errors'
module Needle
# This module encapsulates the functionality for building interceptor chains.
module InterceptorChainBuilder
# The context of a method invocation. This is used in an interceptor chain
# to encapsulate the elements of the current invocation.
# sym: the name of the method being invoked
# args: the argument list being passed to the method
# block: the reference to the block attached to the method invocation
# data: a hash that may be used by clients for storing arbitrary data in
# the context.
InvocationContext = Struct.new( :sym, :args, :block, :data )
# A single element in an interceptor chain. Each interceptor object is
# wrapped in an instance of one of these. Calling #process_next on a given
# chain element, invokes the #process method on the corresponding
# interceptor, with the next element in the chain being passed in.
class InterceptorChainElement
# Create a new InterceptorChainElement that wraps the given interceptor.
def initialize( interceptor )
@interceptor = interceptor
end
# Set the next element in the interceptor chain to the given object. This
# must be either an InterceptorChainElement instance of a
# ProxyObjectChainElement instance.
def next=( next_obj )
@next_obj = next_obj
end
# Invokes the #process method of the interceptor encapsulated by this
# object, with the _next_ element in the chain being passed to it.
def process_next( context )
if @next_obj.nil?
raise Bug,
"[BUG] interceptor chain should always terminate with proxy"
end
@interceptor.process( @next_obj, context )
end
end
# Encapsulates the end of an interceptor chain, which is the actual object
# being affected.
class ProxyObjectChainElement
# Create a new ProxyObjectChainElement that wraps the given object.
def initialize( obj )
@obj = obj
end
# Invoke the method represented by the context on the wrapped object.
def process_next( context )
@obj.__send__( context.sym, *context.args, &context.block )
end
end
# This is just a trivial proxy class that is used to wrap a service
# before the interceptors are applied to it. This additional level of
# abstraction prevents the need for mangling the names of the service's
# methods, and also offers those applications that need it the ability
# to invoke methods of the service without going through the interceptors.
#
# The proxy will be decorated with dynamically appended methods by the
# InterceptorChainBuilder#build method.
class InterceptedServiceProxy
# Create a new InterceptedServiceProxy that wraps the given interceptor
# chain.
def initialize( chain )
@chain = chain
end
end
# This will apply the given interceptors to the given service by first
# ordering the interceptors based on their relative priorities,
# and then dynamically modifying the service's methods so that the chain
# of interceptors sits in front of each of them.
#
# The modified service is returned.
def build( point, service, interceptors )
return service if interceptors.nil? || interceptors.empty?
ordered_list =
interceptors.sort { |a,b|
a.options[:priority] <=> b.options[:priority] }
chain = ProxyObjectChainElement.new( service )
ordered_list.reverse.each do |interceptor|
factory = interceptor.action.call( point.container )
instance = factory.new( point, interceptor.options )
element = InterceptorChainElement.new( instance )
element.next = chain
chain = element
end
# FIXME: should inherited methods of "Object" be interceptable?
methods_to_intercept = ( service.class.instance_methods( true ) -
Object.instance_methods +
service.class.instance_methods( false ) ).uniq
service = InterceptedServiceProxy.new( chain )
singleton = class << service; self; end
methods_to_intercept.each do |method|
next if method =~ /^__/
if singleton.instance_methods(false).include? method
singleton.send( :remove_method, method )
end
singleton.class_eval <<-EOF
def #{method}( *args, &block )
context = InvocationContext.new( :#{method}, args, block, Hash.new )
@chain.process_next( context )
end
EOF
end
# allow the interceptor to intercept methods not explicitly
# declared on the reciever.
if singleton.instance_methods(false).include? "method_missing"
singleton.send( :remove_method, :method_missing )
end
singleton.class_eval <<-EOF
def method_missing( sym, *args, &block )
context = InvocationContext.new( sym, args, block, Hash.new )
@chain.process_next( context )
end
EOF
return service
end
module_function :build
end
end
|