aquarium 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/Aquarium-IDEA.ipr +252 -0
  2. data/Aquarium-IDEA.iws +493 -0
  3. data/Aquarium.ipr +1 -1
  4. data/Aquarium.iws +133 -138
  5. data/CHANGES +63 -0
  6. data/ParseTreePlay.rb +25 -0
  7. data/README +55 -3
  8. data/RELEASE-PLAN +9 -1
  9. data/TODO.rb +175 -15
  10. data/examples/aspect_design_example.rb +13 -1
  11. data/examples/aspect_design_example_spec.rb +20 -2
  12. data/examples/introductions_example.rb +35 -0
  13. data/examples/introductions_example_spec.rb +37 -0
  14. data/examples/method_missing_example.rb +2 -1
  15. data/lib/aquarium/aspects/advice.rb +127 -74
  16. data/lib/aquarium/aspects/aspect.rb +139 -72
  17. data/lib/aquarium/aspects/default_objects_handler.rb +6 -4
  18. data/lib/aquarium/aspects/exclusion_handler.rb +15 -3
  19. data/lib/aquarium/aspects/join_point.rb +60 -55
  20. data/lib/aquarium/aspects/pointcut.rb +153 -124
  21. data/lib/aquarium/aspects/pointcut_composition.rb +1 -1
  22. data/lib/aquarium/dsl/aspect_dsl.rb +13 -5
  23. data/lib/aquarium/dsl/object_dsl.rb +4 -2
  24. data/lib/aquarium/extras/design_by_contract.rb +9 -5
  25. data/lib/aquarium/finders.rb +1 -0
  26. data/lib/aquarium/finders/finder_result.rb +13 -5
  27. data/lib/aquarium/finders/method_finder.rb +75 -70
  28. data/lib/aquarium/finders/pointcut_finder.rb +166 -0
  29. data/lib/aquarium/finders/type_finder.rb +104 -62
  30. data/lib/aquarium/utils/array_utils.rb +1 -1
  31. data/lib/aquarium/utils/invalid_options.rb +2 -0
  32. data/lib/aquarium/utils/name_utils.rb +3 -2
  33. data/lib/aquarium/utils/nil_object.rb +7 -3
  34. data/lib/aquarium/utils/options_utils.rb +38 -27
  35. data/lib/aquarium/utils/set_utils.rb +2 -2
  36. data/lib/aquarium/utils/type_utils.rb +11 -0
  37. data/lib/aquarium/version.rb +1 -1
  38. data/spec/aquarium/aspects/advice_spec.rb +147 -32
  39. data/spec/aquarium/aspects/aspect_invocation_spec.rb +252 -43
  40. data/spec/aquarium/aspects/aspect_spec.rb +148 -88
  41. data/spec/aquarium/aspects/aspect_with_nested_types_spec.rb +40 -34
  42. data/spec/aquarium/aspects/aspect_with_subtypes_spec.rb +39 -3
  43. data/spec/aquarium/aspects/join_point_spec.rb +190 -227
  44. data/spec/aquarium/aspects/pointcut_spec.rb +24 -1
  45. data/spec/aquarium/dsl/aspect_dsl_spec.rb +17 -17
  46. data/spec/aquarium/finders/method_finder_spec.rb +8 -2
  47. data/spec/aquarium/finders/pointcut_finder_spec.rb +193 -0
  48. data/spec/aquarium/finders/pointcut_finder_spec_test_classes.rb +90 -0
  49. data/spec/aquarium/finders/type_finder_spec.rb +17 -0
  50. data/spec/aquarium/finders/type_finder_with_descendents_and_ancestors_spec.rb +4 -4
  51. data/spec/aquarium/finders/type_finder_with_nested_types.rb +47 -0
  52. data/spec/aquarium/utils/nil_object_spec.rb +21 -0
  53. data/spec/aquarium/utils/type_utils_sample_nested_types.rb +51 -0
  54. data/spec/aquarium/utils/type_utils_spec.rb +18 -1
  55. metadata +13 -3
@@ -31,10 +31,28 @@ end
31
31
 
32
32
  include Aquarium::Aspects
33
33
 
34
- describe "An example of an aspect using a class-defined pointcut." do
34
+ # Two ways of referencing the pointcut are shown. The first assumes you know the particular
35
+ # pointcuts you care about. The second is more general; it uses the recently-introduced
36
+ # :named_pointcut feature to search for all pointcuts matching a name in a set of types.
37
+ describe "An example of an aspect referencing a particular class-defined pointcut." do
35
38
  it "should observe state changes in the class." do
36
39
  @new_state = nil
37
- observer = Aspect.new :after, :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, obj, *args|
40
+ observer = Aspect.new :after,
41
+ :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, obj, *args|
42
+ @new_state = obj.state
43
+ @new_state.should be_eql(*args)
44
+ end
45
+ object = Aquarium::ClassWithStateAndBehavior.new(:a1, :a2, :a3)
46
+ object.state = [:b1, :b2]
47
+ @new_state.should == [:b1, :b2]
48
+ observer.unadvise
49
+ end
50
+ end
51
+ describe "An example of an aspect searching for class-defined pointcuts." do
52
+ it "should observe state changes in the class." do
53
+ @new_state = nil
54
+ observer = Aspect.new :after,
55
+ :named_pointcuts => {:matching => /CHANGE/, :within_types => Aquarium::ClassWithStateAndBehavior} do |jp, obj, *args|
38
56
  @new_state = obj.state
39
57
  @new_state.should be_eql(*args)
40
58
  end
@@ -0,0 +1,35 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'aquarium'
3
+
4
+ module Aquarium
5
+ module TypeFinderIntroductionExampleTargetModule1
6
+ end
7
+ module TypeFinderIntroductionExampleTargetModule2
8
+ end
9
+ class TypeFinderIntroductionExampleTargetClass1
10
+ end
11
+ class TypeFinderIntroductionExampleTargetClass2
12
+ end
13
+ module TypeFinderIntroductionExampleModule
14
+ def introduced_method; end
15
+ end
16
+ end
17
+
18
+ include Aquarium::Finders
19
+
20
+ # First, find the types
21
+
22
+ found = TypeFinder.new.find :types => /Aquarium::TypeFinderIntroductionExampleTarget/
23
+
24
+ # Now, iterate through them and "extend" them with the module defining the new behavior.
25
+
26
+ found.each {|t| t.extend Aquarium::TypeFinderIntroductionExampleModule }
27
+
28
+ # See if the "introduced" modules's method is there.
29
+
30
+ [Aquarium::TypeFinderIntroductionExampleTargetModule1,
31
+ Aquarium::TypeFinderIntroductionExampleTargetModule2,
32
+ Aquarium::TypeFinderIntroductionExampleTargetClass1,
33
+ Aquarium::TypeFinderIntroductionExampleTargetClass2].each do |t|
34
+ p "type #{t}, method there? #{t.methods.include?("introduced_method")}"
35
+ end
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/../spec/aquarium/spec_helper'
2
+ require 'aquarium'
3
+
4
+ # Example demonstrating how to use the TypeFinder class to convenient "introduce" new
5
+ # methods and attributes in a set of types, like you might do with AspectJ in Java.
6
+ # Of course, in Ruby, you can simply use Object#extend(module). However, if you want to
7
+ # do this in a cross-cutting way, TypeFinder. is convenient.
8
+
9
+ module Aquarium
10
+ module TypeFinderIntroductionExampleTargetModule1
11
+ end
12
+ module TypeFinderIntroductionExampleTargetModule2
13
+ end
14
+ class TypeFinderIntroductionExampleTargetClass1
15
+ end
16
+ class TypeFinderIntroductionExampleTargetClass2
17
+ end
18
+ module TypeFinderIntroductionExampleModule
19
+ def introduced_method; end
20
+ end
21
+ end
22
+
23
+ # include Aquarium::Finders
24
+
25
+ describe "Using TypeFinder to introduce modules in a set of other types" do
26
+ it "should extend each found type with the specified module if you use the finder result #each method" do
27
+ found = Aquarium::Finders::TypeFinder.new.find :types => /Aquarium::TypeFinderIntroductionExampleTarget/
28
+ found.each {|t| t.extend Aquarium::TypeFinderIntroductionExampleModule }
29
+ [Aquarium::TypeFinderIntroductionExampleTargetModule1,
30
+ Aquarium::TypeFinderIntroductionExampleTargetModule2,
31
+ Aquarium::TypeFinderIntroductionExampleTargetClass1,
32
+ Aquarium::TypeFinderIntroductionExampleTargetClass2].each do |t|
33
+ t.methods.should include('introduced_method')
34
+ end
35
+ end
36
+ end
37
+
@@ -31,7 +31,8 @@ echo1.say "hello", "world!"
31
31
  echo1.log "something", "interesting..."
32
32
  echo1.shout "theater", "in", "a", "crowded", "firehouse!"
33
33
 
34
- Aquarium::Aspects::Aspect.new :around, :calls_to => :method_missing, :for_type => Aquarium::Echo, do |join_point, obj, sym, *args|
34
+ Aquarium::Aspects::Aspect.new :around,
35
+ :calls_to => :method_missing, :for_type => Aquarium::Echo, do |join_point, obj, sym, *args|
35
36
  if sym == :log
36
37
  p "--- Sending to log: #{args.join(" ")}"
37
38
  else
@@ -8,6 +8,8 @@ module Aquarium
8
8
  module Aspects
9
9
  module Advice
10
10
 
11
+ UNKNOWN_ADVICE_KIND = "unknown"
12
+
11
13
  KINDS_IN_PRIORITY_ORDER = [:around, :before, :after, :after_returning, :after_raising]
12
14
 
13
15
  def self.kinds; KINDS_IN_PRIORITY_ORDER; end
@@ -17,6 +19,19 @@ module Aquarium
17
19
  KINDS_IN_PRIORITY_ORDER.index(x.to_sym) <=> KINDS_IN_PRIORITY_ORDER.index(y.to_sym)
18
20
  end.map {|x| x.to_sym}
19
21
  end
22
+
23
+ def self.compare_advice_kinds kind1, kind2
24
+ if kind1.nil?
25
+ return kind2.nil? ? 0 : -1
26
+ end
27
+ return 1 if kind2.nil?
28
+ if kind1.eql?(UNKNOWN_ADVICE_KIND)
29
+ return kind2.eql?(UNKNOWN_ADVICE_KIND) ? 0 : -1
30
+ else
31
+ return kind2.eql?(UNKNOWN_ADVICE_KIND) ? 1 : KINDS_IN_PRIORITY_ORDER.index(kind1) <=> KINDS_IN_PRIORITY_ORDER.index(kind2)
32
+ end
33
+ end
34
+
20
35
  end
21
36
 
22
37
  # Supports Enumerable, but not the sorting methods, as this class is a linked list structure.
@@ -25,9 +40,7 @@ module Aquarium
25
40
  # in the case of around advice!
26
41
  class AdviceChainNode
27
42
  include Enumerable
28
- def initialize options = {}, &proc_block
29
- raise Aquarium::Utils::InvalidOptions.new("You must specify an advice block or Proc") if proc_block.nil?
30
- @proc = Proc.new &proc_block
43
+ def initialize options = {}
31
44
  # assign :next_node and :static_join_point so the attributes are always created
32
45
  options[:next_node] ||= nil
33
46
  options[:static_join_point] ||= nil
@@ -37,38 +50,32 @@ module Aquarium
37
50
  attr_accessor(:#{key})
38
51
  EOF
39
52
  end
53
+ end
40
54
 
41
- def call_advice jp, obj, *args
42
- case advice.arity
43
- when 0 then advice.call
44
- when 1 then advice.call jp
45
- when 2 then advice.call jp, obj
46
- else advice.call jp, obj, *args
47
- end
55
+ # Bug #19262 workaround: need to only pass jp argument if arity is 1.
56
+ def call_advice jp
57
+ if advice.arity == 1
58
+ advice.call jp
59
+ else
60
+ advice.call jp, jp.context.advised_object, *jp.context.parameters
48
61
  end
49
62
  end
50
63
 
51
- def call jp, obj, *args
52
- do_call @proc, "", jp, obj, *args
53
- end
54
-
55
- def invoke_original_join_point current_jp, obj, *args
56
- do_call last, "While executing the original join_point: ", current_jp, obj, *args
64
+ def call jp
65
+ begin
66
+ advice_wrapper jp
67
+ rescue Exception => e
68
+ handle_call_rescue e, "", jp
69
+ end
57
70
  end
58
71
 
59
- def do_call proc_to, error_message_prefix, jp, obj, *args
72
+ def invoke_original_join_point current_jp
60
73
  begin
61
- proc_to.call jp, obj, *args
62
- rescue => e
63
- class_or_instance_method_separater = jp.instance_method? ? "#" : "."
64
- context_message = error_message_prefix + "Exception raised while executing \"#{jp.context.advice_kind}\" advice for \"#{jp.type_or_object.inspect}#{class_or_instance_method_separater}#{jp.method_name}\": "
65
- backtrace = e.backtrace
66
- e2 = e.exception(context_message + e.message + " (join_point = #{jp.inspect})")
67
- e2.set_backtrace backtrace
68
- raise e2
74
+ last.advice_wrapper current_jp
75
+ rescue Exception => e
76
+ handle_call_rescue e, "While executing the original join_point: ", current_jp
69
77
  end
70
78
  end
71
- protected :do_call
72
79
 
73
80
  # Supports Enumerable
74
81
  def each
@@ -101,8 +108,30 @@ module Aquarium
101
108
 
102
109
  protected
103
110
 
104
- def invoking_object join_point
105
- join_point.instance_method? ? join_point.context.advised_object : join_point.target_type
111
+ #--
112
+ # For performance reasons, we don't clone the context.
113
+ # TODO: There are potential concurrency issues!
114
+ #++
115
+ def update_current_context jp
116
+ return if advice.arity == 0
117
+ @last_advice_kind = jp.context.advice_kind
118
+ @last_advice_node = jp.context.current_advice_node
119
+ jp.context.current_advice_node = self
120
+ end
121
+
122
+ def reset_current_context jp
123
+ return if advice.arity == 0
124
+ jp.context.advice_kind = @last_advice_kind
125
+ jp.context.current_advice_node = @last_advice_node
126
+ end
127
+
128
+ def handle_call_rescue ex, error_message_prefix, jp
129
+ class_or_instance_method_separater = jp.instance_method? ? "#" : "."
130
+ context_message = error_message_prefix + "Exception raised while executing \"#{jp.context.advice_kind}\" advice for \"#{jp.type_or_object.inspect}#{class_or_instance_method_separater}#{jp.method_name}\": "
131
+ backtrace = ex.backtrace
132
+ e2 = ex.exception(context_message + ex.message + " (join_point = #{jp.inspect})")
133
+ e2.set_backtrace backtrace
134
+ raise e2
106
135
  end
107
136
  end
108
137
 
@@ -113,33 +142,39 @@ module Aquarium
113
142
  # Note that we extract the block passed to the original method call, if any,
114
143
  # from the context and pass it to method invocation.
115
144
  def initialize options = {}
116
- super(options) { |jp, obj, *args|
117
- block_for_method = jp.context.block_for_method
118
- block_for_method.nil? ?
119
- invoking_object(jp).send(@alias_method_name, *args) :
120
- invoking_object(jp).send(@alias_method_name, *args, &block_for_method)
121
- }
145
+ super options
146
+ end
147
+ def advice_wrapper jp
148
+ jp.context.advised_object.send @alias_method_name, *jp.context.parameters, &jp.context.block_for_method
122
149
  end
123
150
  end
124
151
 
125
152
  class BeforeAdviceChainNode < AdviceChainNode
126
153
  def initialize options = {}
127
- super(options) { |jp, obj, *args|
128
- before_jp = jp.make_current_context_join_point :advice_kind => :before, :current_advice_node => self
129
- call_advice(before_jp, obj, *args)
130
- next_node.call(jp, obj, *args)
131
- }
154
+ super options
155
+ end
156
+ def advice_wrapper jp
157
+ update_current_context jp
158
+ jp.context.advice_kind = :before
159
+ call_advice jp
160
+ reset_current_context jp
161
+ next_node.call jp
132
162
  end
133
163
  end
134
164
 
135
165
  class AfterReturningAdviceChainNode < AdviceChainNode
136
166
  def initialize options = {}
137
- super(options) { |jp, obj, *args|
138
- returned_value = next_node.call(jp, obj, *args)
139
- next_jp = jp.make_current_context_join_point :advice_kind => :after_returning, :returned_value => returned_value, :current_advice_node => self
140
- call_advice(next_jp, obj, *args)
141
- next_jp.context.returned_value # allow advice to modify the returned value
142
- }
167
+ super options
168
+ end
169
+ def advice_wrapper jp
170
+ returned_value = next_node.call jp
171
+ update_current_context jp
172
+ jp.context.advice_kind = :after_returning
173
+ jp.context.returned_value = returned_value
174
+ call_advice jp
175
+ result = jp.context.returned_value # allow advice to modify the returned value
176
+ reset_current_context jp
177
+ result
143
178
  end
144
179
  end
145
180
 
@@ -148,18 +183,22 @@ module Aquarium
148
183
  class AfterRaisingAdviceChainNode < AdviceChainNode
149
184
  include Aquarium::Utils::ArrayUtils
150
185
  def initialize options = {}
151
- super(options) { |jp, obj, *args|
152
- begin
153
- next_node.call(jp, obj, *args)
154
- rescue Object => raised_exception
155
- if after_raising_exceptions_list_includes raised_exception
156
- next_jp = jp.make_current_context_join_point :advice_kind => :after_raising, :raised_exception => raised_exception, :current_advice_node => self
157
- call_advice(next_jp, obj, *args)
158
- raised_exception = next_jp.context.raised_exception # allow advice to modify raised exception
159
- end
160
- raise raised_exception
186
+ super options
187
+ end
188
+ def advice_wrapper jp
189
+ begin
190
+ next_node.call jp
191
+ rescue Object => raised_exception
192
+ if after_raising_exceptions_list_includes raised_exception
193
+ update_current_context jp
194
+ jp.context.advice_kind = :after_raising
195
+ jp.context.raised_exception = raised_exception
196
+ call_advice jp
197
+ raised_exception = jp.context.raised_exception # allow advice to modify the raised exception
198
+ reset_current_context jp
161
199
  end
162
- }
200
+ raise raised_exception
201
+ end
163
202
  end
164
203
 
165
204
  private
@@ -175,29 +214,43 @@ module Aquarium
175
214
 
176
215
  class AfterAdviceChainNode < AdviceChainNode
177
216
  def initialize options = {}
178
- super(options) { |jp, obj, *args|
179
- # call_advice is invoked in each bloc, rather than once in an "ensure" clause, so the invocation in the rescue class
180
- # can allow the advice to change the exception that will be raised.
181
- begin
182
- returned_value = next_node.call(jp, obj, *args)
183
- next_jp = jp.make_current_context_join_point :advice_kind => :after, :returned_value => returned_value, :current_advice_node => self
184
- call_advice(next_jp, obj, *args)
185
- next_jp.context.returned_value # allow advice to modify the returned value
186
- rescue Object => raised_exception
187
- next_jp = jp.make_current_context_join_point :advice_kind => :after, :raised_exception => raised_exception, :current_advice_node => self
188
- call_advice(next_jp, obj, *args)
189
- raise next_jp.context.raised_exception
190
- end
191
- }
217
+ super options
218
+ end
219
+ def advice_wrapper jp
220
+ # call_advice is invoked in each bloc, rather than once in an "ensure" clause, so the invocation in
221
+ # the rescue clause can allow the advice to change the exception that will be raised.
222
+ begin
223
+ returned_value = next_node.call jp
224
+ update_current_context jp
225
+ jp.context.advice_kind = :after
226
+ jp.context.returned_value = returned_value
227
+ call_advice jp
228
+ result = jp.context.returned_value # allow advice to modify the returned value
229
+ reset_current_context jp
230
+ result
231
+ rescue Object => raised_exception
232
+ update_current_context jp
233
+ jp.context.advice_kind = :after
234
+ jp.context.raised_exception = raised_exception
235
+ call_advice jp
236
+ raised_exception = jp.context.raised_exception # allow advice to modify the raised exception
237
+ reset_current_context jp
238
+ raise raised_exception
239
+ end
192
240
  end
193
241
  end
194
242
 
195
243
  class AroundAdviceChainNode < AdviceChainNode
196
244
  def initialize options = {}
197
- super(options) { |jp, obj, *args|
198
- around_jp = jp.make_current_context_join_point :advice_kind => :around, :proceed_proc => next_node, :current_advice_node => self
199
- call_advice(around_jp, obj, *args)
200
- }
245
+ super options
246
+ end
247
+ def advice_wrapper jp
248
+ update_current_context jp
249
+ jp.context.advice_kind = :around
250
+ jp.context.proceed_proc = next_node
251
+ result = call_advice jp
252
+ reset_current_context jp
253
+ result
201
254
  end
202
255
  end
203
256
 
@@ -1,5 +1,5 @@
1
1
  require 'aquarium/extensions'
2
- require 'aquarium/finders/type_finder'
2
+ require 'aquarium/finders'
3
3
  require 'aquarium/utils'
4
4
  require 'aquarium/aspects/advice'
5
5
  require 'aquarium/aspects/exclusion_handler'
@@ -12,14 +12,14 @@ module Aquarium
12
12
  module Aspects
13
13
 
14
14
  # == Aspect
15
- # Aspects "advise" one or more method invocations for one or more types or objects
15
+ # Aspect "advises" one or more method invocations for one or more types or objects
16
16
  # (including class methods on types). The corresponding advice is a Proc that is
17
17
  # invoked either before the join point, after it returns, after it raises an exception,
18
18
  # after either event, or around the join point, meaning the advice runs and it decides
19
19
  # when and if to invoke the advised method. (Hence, around advice can run code before
20
20
  # and after the join point call and it can "veto" the actual join point call).
21
21
  #
22
- # See also Aquarium::Aspects::DSL::AspectDsl for more information.
22
+ # See also Aquarium::DSL for more information.
23
23
  class Aspect
24
24
  include Advice
25
25
  include ExclusionHandler
@@ -35,27 +35,31 @@ module Aquarium
35
35
  ASPECT_CANONICAL_OPTIONS = {
36
36
  "advice" => %w[action do_action use_advice advise_with invoke call],
37
37
  "pointcuts" => %w[pointcut],
38
+ "named_pointcuts" => %w[named_pointcut],
38
39
  "exceptions" => %w[exception],
39
40
  "ignore_no_matching_join_points" => %[ignore_no_jps]
40
41
  }
41
- add_prepositional_option_variants_for "pointcuts", ASPECT_CANONICAL_OPTIONS
42
- add_exclude_options_for "pointcuts", ASPECT_CANONICAL_OPTIONS
42
+ ["pointcuts", "named_pointcuts"].each do |pc_option|
43
+ add_prepositional_option_variants_for pc_option, ASPECT_CANONICAL_OPTIONS
44
+ add_exclude_options_for pc_option, ASPECT_CANONICAL_OPTIONS
45
+ end
43
46
  CANONICAL_OPTIONS = Pointcut::CANONICAL_OPTIONS.merge ASPECT_CANONICAL_OPTIONS
44
47
 
45
48
  canonical_options_given_methods CANONICAL_OPTIONS
46
49
 
47
50
 
48
- # Aspect.new (:around | :before | :after | :after_returning | :after_raising ) \
49
- # (:pointcuts => [...]), | \
50
- # ((:types => [...] | :types_and_ancestors => [...] | :types_and_descendents => [...] \
51
- # :objects => [...]),
52
- # :methods => [], :method_options => [...], \
53
- # :attributes => [...], :attribute_options[...]), \
51
+ # Aspect.new (:around | :before | :after | :after_returning | :after_raising )
52
+ # (:pointcuts => [...]), :named_pointcuts => [...] |
53
+ # ((:types => [...] | :types_and_ancestors => [...] | :types_and_descendents => [...]
54
+ # | :types_and_nested_types | :objects => [...]),
55
+ # :methods => [], :method_options => [...],
56
+ # :attributes => [...], :attribute_options[...]),
54
57
  # (:advice = advice | do |join_point, obj, *args| ...; end)
55
58
  #
56
59
  # where the parameters often have many synonyms (mostly to support a "humane
57
60
  # interface") and they are interpreted as followed:
58
61
  #
62
+ # ==== Type of Advice
59
63
  # <tt>:around</tt>::
60
64
  # Invoke the specified advice "around" the join points. It is up to the advice
61
65
  # itself to call <tt>join_point.proceed</tt> (where <tt>join_point</tt> is the
@@ -73,36 +77,64 @@ module Aquarium
73
77
  # Invoke the specified advice after the join point returns successfully.
74
78
  #
75
79
  # <tt>:after_raising [=> exception || [exception_list]]</tt>::
76
- # <tt>:after_raising, :exceptions => (exception || [exception_list])</tt>::
77
- # <tt>:after_raising, :exception => (exception || [exception_list])</tt>::
78
80
  # Invoke the specified advice after the join point raises one of the specified exceptions.
79
- # If no exceptions are specified, the advice is invoked after any exception is raised.
81
+ # If no exceptions are specified, the advice is invoked after any exception is raised. An
82
+ # alternative syntax is <tt>:after_raising, :exception[s] => (exception || [exception_list])</tt>.
83
+ #
84
+ # ==== Advice
85
+ # The advice to invoke before, after, or around the join points. Only one advice may be specified.
86
+ # If a block is specified, the following options are ignored.
87
+ # * <tt>:advice => proc</tt>
88
+ # * <tt>:action => proc</tt>
89
+ # * <tt>:do_action => proc</tt>
90
+ # * <tt>:use_advice => proc</tt>
91
+ # * <tt>:advise_with => proc</tt>
92
+ # * <tt>:invoke => proc</tt>
93
+ # * <tt>:call => proc</tt>
94
+ #
95
+ # ==== Pointcuts
96
+ # A Pointcut, JoinPoint, or array of the same (Mixed is allowed.). Specifying pointcuts
97
+ # is mutually-exclusive with specifying them "indirectly" through :types, :objects,
98
+ # :methods, :attributes, :method_options, and :attribute_options parameters.
99
+ # * <tt>:pointcuts => pointcut || [pointcut_list]</tt>
100
+ # * <tt>:pointcut => pointcut || [pointcut_list]</tt>
101
+ # * <tt>:on_pointcut => pointcut || [pointcut_list]</tt>
102
+ # * <tt>:on_pointcuts => pointcut || [pointcut_list]</tt>
103
+ # * <tt>:in_pointcut => pointcut || [pointcut_list]</tt>
104
+ # * <tt>:in_pointcuts => pointcut || [pointcut_list]</tt>
105
+ # * <tt>:within_pointcut => pointcut || [pointcut_list]</tt>
106
+ # * <tt>:within_pointcuts => pointcut || [pointcut_list]</tt>
80
107
  #
81
- # <tt>:advice => proc</tt>::
82
- # <tt>:action => proc</tt>::
83
- # <tt>:do_action => proc</tt>::
84
- # <tt>:use_advice => proc</tt>::
85
- # <tt>:advise_with => proc</tt>::
86
- # <tt>:invoke => proc</tt>::
87
- # <tt>:call => proc</tt>::
88
- # The specified advice to be invoked. Only one advice may be specified. If a block is
89
- # specified, it is used instead.
108
+ # ==== Named Pointcuts
109
+ # Specify search criteria to locate Pointcuts defined as class constants and/or class variables.
110
+ # The options for the <tt>named_pointcuts</tt> parameter must form a hash and satisfy the
111
+ # requirements documented for Aquarium::Finders::PointcutFinder#find.
112
+ # Specifying named pointcuts is also mutually-exclusive with specifying Pointcuts "indirectly"
113
+ # through :types, :objects, :methods, :attributes, :method_options, and :attribute_options parameters.
114
+ # * <tt>:named_pointcuts => {PointcutFinder options}</tt>
115
+ # * <tt>:named_pointcut => {PointcutFinder options}</tt>
116
+ # * <tt>:on_named_pointcuts => {PointcutFinder options}</tt>
117
+ # * <tt>:on_named_pointcut => {PointcutFinder options}</tt>
118
+ # * <tt>:in_named_pointcuts => {PointcutFinder options}</tt>
119
+ # * <tt>:in_named_pointcut => {PointcutFinder options}</tt>
120
+ # * <tt>:within_named_pointcuts => {PointcutFinder options}</tt>
121
+ # * <tt>:within_named_pointcut => {PointcutFinder options}</tt>
90
122
  #
91
- # <tt>:pointcuts => pointcut || [pointcut_list]</tt>::
92
- # <tt>:pointcut => pointcut || [pointcut_list]</tt>::
93
- # <tt>:within_pointcut => pointcut || [pointcut_list]</tt>::
94
- # <tt>:within_pointcuts => pointcut || [pointcut_list]</tt>::
95
- # One or an array of Pointcut or JoinPoint objects. Mutually-exclusive with the :types, :objects,
96
- # :methods, :attributes, :method_options, and :attribute_options parameters.
123
+ # ==== Exclude Pointcuts
124
+ # Exclude the specified pointcuts. The <tt>exclude_</tt> prefix can be used with any of the
125
+ # <tt>:pointcuts</tt> and <tt>:named_pointcuts</tt> synonyms.
126
+ # * <tt>:exclude_pointcuts => pointcut || [pointcut_list]</tt>
127
+ # * <tt>:exclude_named_pointcuts => {PointcutFinder options}</tt>
97
128
  #
98
- # <tt>:ignore_no_matching_join_points => true | false</tt>
99
- # <tt>ignore_no_jps => true | false</tt>::
129
+ # ==== Miscellaneous Options
130
+ # <tt>:ignore_no_matching_join_points => true | false</tt>::
100
131
  # Do not issue a warning if no join points are actually matched by the aspect. By default, the value
101
132
  # is false, meaning that a WARN-level message will be written to the log. It is usually very helpful
102
- # to be warned when no matches occurred, for diagnostic purposes!
133
+ # to be warned when no matches occurred, for debugging purposes! A synonym for this option is
134
+ # <tt>ignore_no_jps => true | false</tt>.
103
135
  #
104
- # Aspect.new also accepts all the same options that Pointcut accepts, including the synonyms for :types,
105
- # :methods, etc. It also accepts the "universal" options documented in OptionsUtils.
136
+ # For other options <i>e.g.,</i> <tt>:types</tt>, <tt>:methods</tt>, Aspect#new accepts all the options
137
+ # that Pointcut#new accepts. It also accepts the "universal" options documented in Aquarium::Utils::OptionsUtils.
106
138
  def initialize *options, &block
107
139
  @first_option_that_was_method = []
108
140
  opts = rationalize options
@@ -149,7 +181,7 @@ module Aquarium
149
181
 
150
182
  alias :== :eql?
151
183
 
152
- protected
184
+ private
153
185
 
154
186
  def rationalize options
155
187
  return {} if options.nil? or options.empty?
@@ -194,7 +226,7 @@ module Aquarium
194
226
 
195
227
  def calculate_excluded_types
196
228
  type_finder_options = {}
197
- %w[types types_and_ancestors types_and_descendents].each do |opt|
229
+ %w[types types_and_ancestors types_and_descendents types_and_nested_types].each do |opt|
198
230
  type_finder_options[opt.intern] = @specification["exclude_#{opt}".intern] if @specification["exclude_#{opt}".intern]
199
231
  end
200
232
  excluded_types = Aquarium::Finders::TypeFinder.new.find type_finder_options
@@ -220,18 +252,10 @@ module Aquarium
220
252
  end
221
253
 
222
254
  def init_pointcuts
223
- pointcuts = []
224
- if pointcuts_given?
225
- pointcuts_given.each do |pointcut|
226
- if pointcut.kind_of? Pointcut
227
- pointcuts << pointcut
228
- elsif pointcut.kind_of? JoinPoint
229
- pointcuts << Pointcut.new(:join_point => pointcut)
230
- else # a hash of Pointcut.new options?
231
- pointcuts << Pointcut.new(pointcut)
232
- end
233
- end
234
- else
255
+ set_calculated_excluded_pointcuts determine_excluded_pointcuts
256
+ pointcuts = determine_specified_pointcuts
257
+ pointcuts += determine_named_pointcuts
258
+ if pointcuts.empty? # If no PCs specified, then the user must have specified :types, ...
235
259
  pc_options = {}
236
260
  Pointcut::CANONICAL_OPTIONS.keys.each do |pc_option|
237
261
  pco_sym = pc_option.intern
@@ -239,10 +263,38 @@ module Aquarium
239
263
  end
240
264
  pointcuts << Pointcut.new(pc_options)
241
265
  end
242
- @pointcuts = Set.new(remove_excluded_join_points_and_empty_pointcuts(pointcuts))
266
+ @pointcuts = Set.new(remove_excluded_join_points_and_pointcuts(pointcuts))
243
267
  warn_if_no_join_points_matched
244
268
  end
245
269
 
270
+ def determine_specified_pointcuts
271
+ pointcuts_given.inject([]) do |pointcuts, pointcut|
272
+ if pointcut.kind_of? Pointcut
273
+ pointcuts << pointcut
274
+ elsif pointcut.kind_of? JoinPoint
275
+ pointcuts << Pointcut.new(:join_point => pointcut)
276
+ else # a hash of Pointcut.new options?
277
+ pointcuts << Pointcut.new(pointcut)
278
+ end
279
+ pointcuts
280
+ end
281
+ end
282
+
283
+ def determine_named_pointcuts
284
+ named_pointcuts_given.inject([]) do |pointcuts, pointcut_spec|
285
+ found_pointcuts_results = Aquarium::Finders::PointcutFinder.new.find(pointcut_spec)
286
+ pointcuts += found_pointcuts_results.found_pointcuts
287
+ pointcuts
288
+ end
289
+ end
290
+
291
+ def determine_excluded_pointcuts
292
+ exclude_named_pointcuts_given.inject([]) do |excluded_pointcuts, pointcut_spec|
293
+ found_pointcuts_results = Aquarium::Finders::PointcutFinder.new.find(pointcut_spec)
294
+ excluded_pointcuts += found_pointcuts_results.found_pointcuts
295
+ end
296
+ end
297
+
246
298
  def warn_if_no_join_points_matched
247
299
  return unless should_warn_if_no_matching_join_points
248
300
  @pointcuts.each do |pc|
@@ -261,7 +313,7 @@ module Aquarium
261
313
  @specification[:ignore_no_matching_join_points].to_a.first == false
262
314
  end
263
315
 
264
- def remove_excluded_join_points_and_empty_pointcuts pointcuts
316
+ def remove_excluded_join_points_and_pointcuts pointcuts
265
317
  pointcuts.reject do |pc|
266
318
  pc.join_points_matched.delete_if do |jp|
267
319
  join_point_excluded? jp
@@ -293,13 +345,15 @@ module Aquarium
293
345
  end
294
346
 
295
347
  def add_advice_to_chain join_point, advice_kind, advice
348
+ # Note that we use the same static join point object throughout the chain. It is
349
+ # equal to the passed-in join_point, except for additional context information.
296
350
  start_of_advice_chain = Aspect.get_advice_chain join_point
297
351
  options = @specification.merge({
298
352
  :aspect => self,
299
353
  :advice_kind => advice_kind,
300
354
  :advice => advice,
301
355
  :next_node => start_of_advice_chain,
302
- :static_join_point => join_point})
356
+ :static_join_point => start_of_advice_chain.static_join_point})
303
357
  # New node is new start of chain.
304
358
  Aspect.set_advice_chain(join_point, AdviceChainNodeFactory.make_node(options))
305
359
  end
@@ -330,22 +384,31 @@ module Aquarium
330
384
  type_to_advise = Aspect.type_to_advise_for join_point
331
385
  # Note: Must set advice chain, a class variable on the type we're advising, FIRST.
332
386
  # Otherwise the class_eval that follows will assume the @@ advice chain belongs to Aspect!
333
- Aspect.set_advice_chain join_point, AdviceChainNodeFactory.make_node(
387
+ static_join_point = make_static_join_point join_point
388
+ advice_chain = AdviceChainNodeFactory.make_node(
334
389
  :aspect => nil, # Belongs to all aspects that might advise this join point!
335
390
  :advice_kind => :none,
336
391
  :alias_method_name => alias_method_name,
337
- :static_join_point => join_point)
338
- type_being_advised_text = join_point.instance_method? ? "self.class" : "self"
339
- unless Aspect.is_type_join_point?(join_point)
392
+ :static_join_point => static_join_point)
393
+ advice_chain_attr_sym = Aspect.make_advice_chain_attr_sym join_point
394
+ Aspect.set_advice_chain static_join_point, advice_chain
395
+ type_being_advised_text = static_join_point.instance_method? ? "self.class" : "self"
396
+ unless Aspect.is_type_join_point?(static_join_point)
340
397
  type_being_advised_text = "(class << self; self; end)"
341
398
  end
342
- type_to_advise2 = join_point.instance_method? ? type_to_advise : (class << type_to_advise; self; end)
343
- type_to_advise2.class_eval(<<-EOF, __FILE__, __LINE__)
344
- #{def_eigenclass_method_text join_point}
345
- #{alias_original_method_text alias_method_name, join_point, type_being_advised_text}
399
+ metatype_to_advise = static_join_point.instance_method? ? type_to_advise : (class << type_to_advise; self; end)
400
+ metatype_to_advise.class_eval(<<-EOF, __FILE__, __LINE__)
401
+ #{def_eigenclass_method_text static_join_point}
402
+ #{alias_original_method_text alias_method_name, static_join_point, type_being_advised_text}
346
403
  EOF
347
404
  end
348
405
 
406
+ def make_static_join_point join_point
407
+ static_jp = join_point.dup
408
+ static_jp.context.advice_kind = advice_kinds_given
409
+ static_jp
410
+ end
411
+
349
412
  # When advising an instance, create an override method that gets advised instead of the types method.
350
413
  # Otherwise, all objects will be advised!
351
414
  # Note: this also solves bug #15202.
@@ -368,6 +431,13 @@ module Aquarium
368
431
  join_point.target_type ? join_point.target_type : (class << join_point.target_object; self; end)
369
432
  end
370
433
 
434
+ #--
435
+ # By NOT dup'ing the join_point, we save about 25% on the overhead! However, we
436
+ # compromise thread safety, primarily because the join_point's context object will be changed.
437
+ # TODO Refactor context out of static join point part.
438
+ # Note that we have to assign the parameters and block to the context object in case
439
+ # the advice calls "proceed" or "invoke_original_join_point" without arguments.
440
+ #++
371
441
  def alias_original_method_text alias_method_name, join_point, type_being_advised_text
372
442
  target_self = join_point.instance_method? ? "self" : join_point.target_type.name
373
443
  advice_chain_attr_sym = Aspect.make_advice_chain_attr_sym join_point
@@ -375,13 +445,11 @@ module Aquarium
375
445
  alias_method :#{alias_method_name}, :#{join_point.method_name}
376
446
  def #{join_point.method_name} *args, &block_for_method
377
447
  advice_chain = #{type_being_advised_text}.send :class_variable_get, "#{advice_chain_attr_sym}"
378
- static_join_point = advice_chain.static_join_point
379
- advice_join_point = static_join_point.make_current_context_join_point(
380
- :advice_kind => #{advice_kinds_given.inspect},
381
- :advised_object => #{target_self},
382
- :parameters => args,
383
- :block_for_method => block_for_method)
384
- advice_chain.call advice_join_point, #{target_self}, *args
448
+ join_point = advice_chain.static_join_point
449
+ join_point.context.parameters = args
450
+ join_point.context.block_for_method = block_for_method
451
+ join_point.context.advised_object = #{target_self}
452
+ advice_chain.call join_point
385
453
  end
386
454
  #{join_point.visibility.to_s} :#{join_point.method_name}
387
455
  private :#{alias_method_name}
@@ -437,7 +505,6 @@ module Aquarium
437
505
  EOF
438
506
  end
439
507
 
440
- # TODO optimize calls to these *_advice_chain methods from other private methods.
441
508
  def self.advice_chain_exists? join_point
442
509
  advice_chain_attr_sym = self.make_advice_chain_attr_sym join_point
443
510
  type_to_advise_for(join_point).class_variable_defined? advice_chain_attr_sym
@@ -481,11 +548,11 @@ module Aquarium
481
548
  end
482
549
 
483
550
  def some_type_object_join_point_or_pc_option_given?
484
- pointcuts_given? or join_points_given? or some_type_option_given? or objects_given?
551
+ pointcuts_given? or named_pointcuts_given? or join_points_given? or some_type_option_given? or objects_given?
485
552
  end
486
553
 
487
554
  def some_type_option_given?
488
- types_given? or types_and_ancestors_given? or types_and_descendents_given?
555
+ types_given? or types_and_ancestors_given? or types_and_descendents_given? or types_and_nested_types_given?
489
556
  end
490
557
 
491
558
  def self.determine_type_or_object join_point
@@ -515,10 +582,10 @@ module Aquarium
515
582
  bad_options(":after_returning can't be used with :after_raising.") if options_given? :after_returning, :after_raising
516
583
  bad_options(":exceptions can't be specified except with :after_raising.") if exceptions_given? and not specified_advice_kinds.include?(:after_raising)
517
584
  unless some_type_object_join_point_or_pc_option_given? or default_objects_given?
518
- bad_options("At least one of :pointcut(s), :join_point(s), :type(s), :type(s)_and_ancestors, :type(s)_and_descendents, or :object(s) is required.")
585
+ bad_options("At least one of :pointcut(s), :named_pointcut(s), :join_point(s), :type(s), :type(s)_and_ancestors, :type(s)_and_descendents, :type(s)_and_nested_types, or :object(s) is required.")
519
586
  end
520
- if pointcuts_given? and (some_type_option_given? or objects_given?)
521
- bad_options("Can't specify both :pointcut(s) and one or more of :type(s), and/or :object(s).")
587
+ if (pointcuts_given? or named_pointcuts_given?) and (some_type_option_given? or objects_given?)
588
+ bad_options("Can't specify both :pointcut(s) or :named_pointcut(s) and one or more of :type(s), and/or :object(s).")
522
589
  end
523
590
  unless noop
524
591
  if (not @specification[:advice].nil?) && @specification[:advice].size > 1