aquarium 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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