aquarium 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,3 +1,53 @@
1
+ == Version 0.4.0
2
+
3
+ V0.4.0 adds specs to characterize and test advising Java classes when running on JRuby and adds several
4
+ API enhancements.
5
+
6
+ Bug fixes:
7
+ 17844 JRuby - Advising types, Aquarium thinks the type is a string
8
+ 17883 Workaround for JRUBY-2089
9
+ 18090 JoinPoint#invoke_original_join_point only works with :around advice
10
+
11
+ Enhancements:
12
+ 17834 Allow :class and :module (and variants) wherever :type is allowed
13
+ 17881 Add specs that exercise advising Java types and objects using JRuby
14
+
15
+ #17844 occurred because of the way JRuby encodes Java packages into modules. Aquarium now properly handles
16
+ JRuby types.
17
+
18
+ #17883 reflected a JRuby bug, so a workaround was required.
19
+
20
+ #17834 allows the user to substitute the words "class", "classes", "module" or "modules" anywhere the words
21
+ "type" and "types" are used in the API, since some users might naturally want to write aspects like this:
22
+
23
+ around :calls_to => :my_method, :in_class => MyClass do ...; end
24
+
25
+ However, there is no enforcement to ensure that "class" is only used for classes and "module" is only used
26
+ for modules, etc. Note: it's possible we'll enforce this in some future release, as a way of saying things
27
+ like "only advise classes that match ...", etc. Caveat Emptor!
28
+
29
+ For #18090, a bug prevented JoinPoint#invoke_original_join_point (which allows you to bypass all advices at
30
+ the join point) from working except for :around advice (and yes, the specs didn't cover this adequately -
31
+ gasp!). Now fixed.
32
+
33
+ For #17881, I created a separate set of specs for JRuby, so it's easier to run the "regular" Aquarium
34
+ specs using MRI and the JRuby-specific specs separately with JRuby. The new "jruby" directory contains a
35
+ Rakefile, another set of specs, and Java example code for the specs to use. The default Rakefile task
36
+ re-runs the main Aquarium spec suite using JRuby, to confirm that the suite passes successfully, then it
37
+ runs a set of different specs that load sample Java interfaces and classes into JRuby and then advises
38
+ them with Aquarium.
39
+
40
+ I found a few JRuby bugs and other behavior differences between MRI during this release. I was able to handle
41
+ them with modifications to the Aquarium code. If you "grep" the Aquarium "lib" and "spec" directories for the
42
+ word "jruby" (ignoring case), you'll find notes about these issues and the workarounds I implemented.
43
+
44
+ The separate JRuby spec suite shows what you can and cannot do with Java types. As a side benefit, it also
45
+ demonstrates how Java types, objects, and methods are mapped to Ruby. There are some important limitations,
46
+ however. See the jruby.html page on the website or the README for more details.
47
+
48
+ Note: JRuby 1.1RC2 was used.
49
+
50
+
1
51
  == Version 0.3.1
2
52
 
3
53
  V0.3.1 adds numerous performance improvements, especially in the RSpec suite, and some minor API additions.
data/README CHANGED
@@ -5,6 +5,7 @@ Aquarium is a toolkit for Aspect-Oriented Programming (AOP) whose goals include:
5
5
  * Management of concurrent aspects (i.e., those acting on the same "join points").
6
6
  * Adding and removing aspects dynamically.
7
7
  * A user-friendly DSL.
8
+ * Support for advising Java types through JRuby.
8
9
 
9
10
  === Why Is an AOP Framework Useful in Ruby?
10
11
 
@@ -35,6 +36,11 @@ Only around advice can prevent execution of the join point, except for the speci
35
36
  * Concurrent advice on type AND advice on objects of the type can't be removed cleanly.
36
37
  * The pointcut language is still limited, compared to AspectJ's. See also the comparison with AspectJ behavior next.
37
38
  * The API and wrapper DSL will probably evolve until the 1.0.0 release. Backwards compatibility will be maintained between releases as much as possible. When it is necessary to break backwards compatibility, translation tools will be provided, if possible.
39
+ * There are limitations when advising Java types through JRuby. The separate RSpec suite in the "jruby" directory documentations the details on how to use Aquarium with JRuby-wrapped Java types and the limitations thereof. Here we summarize what works and what doesn't:
40
+ * Aquarium advice on a method in a Java type will only be invoked when the method is called directly from Ruby.
41
+ * To have the advice invoked when the method is called from either Java or Ruby, it is necessary to create a subclass of the Java type in Ruby and an override of the method, which can just call "super". Note that it will be necessary for instances of this Ruby type to be used throughout, not instances of a Java parent type.
42
+ * BUG #18325: If you have Ruby subclasses of Java types *and* you advise a Java method in the hierarchy using @:types_and_descendents => MyJavaBaseClassOrInterface@ *and* you call unadvise on the aspect, the advice "infrastructure" is not correctly removed from the Ruby types. Workaround: Only advise methods in Ruby subclasses of Java types where the method is explicitly overridden in the Ruby class. (The spec and the "Rubyforge bug report":http://rubyforge.org/tracker/index.php?func=detail&aid=18325&group_id=4281&atid=16494 provide examples.)
43
+ * BUG #18326: Normally, you can use either Java- or Ruby-style method names (e.g., "doSomething" vs. "do_something"), for Java types. However, if you write an aspect using the Java-style for a method name and a Ruby subclass of the Java type where the method is actually defined (i.e., the Ruby class doesn't override the method), it appears that the JoinPoint was advised, but the advice is never called. Workaround: Use the Ruby-style name in this scenario.
38
44
 
39
45
  === Differences With Other Ruby AOP Toolkits
40
46
 
@@ -44,13 +50,24 @@ The goal of Aquarium is to provide a superset of the functionality provided by t
44
50
 
45
51
  === Differences With AspectJ Behavior
46
52
 
47
- Many of AspectJ's behaviors that aren't currently supported are planned for future releases.
53
+ Many of AspectJ's features are not currently supported by Aquarium, but some of them are planned for future releases.
48
54
 
49
- * Attribute reading and writing join points are not supported. The :attributes and :attributes_options parameters for Aspect.new are "syntactic sugar" for the corresponding accessor methods.
50
- * At this time, the pointcut language supported by Aquarium is not nearly as feature-rich as AspectJ's language. For example, there are no runtime pointcut designators supported, such as "if" conditions and "cflow" (context flow). Most of AspectJ pointcut language features are planned, however.
55
+ * Attribute reading and writing join points are not supported. The :attributes and :attributes_options parameters (and their synonyms) for Aspect.new are actually "syntactic sugar" for the corresponding accessor methods.
56
+ * More advanced AspectJ pointcut language features are not supported, such as the runtime pointcut designators like "if" conditionals and "cflow" (context flow) and the compile time "within" and "withincode" designators. Most of AspectJ pointcut language features are planned, however.
51
57
  * While AspectJ provides "intertype declaration" capabilities (i.e., adding state and behavior to existing classes), Ruby's native metaprogramming support satisfies this need. There may be convenience hooks added in a future release, however.
52
58
  * User defined advice precedence is not supported. However, advice precedence is unambiguous; the last aspects created while modules are loaded at runtime have higher precedence than earlier aspects. Ensuring a particular order is not always easy, of course.
53
- * Unlike AspectJ, Aquarium can advise individual objects, can remove advice, and it supports named advice that can be defined separately from the aspects that use the advice. Aquarium can also advise ancestor (parent) types, not just derived (descendent) types of specified types.
59
+
60
+ However, Aquarium does have a few advantages over AspectJ, especially when advising Java types when running in JRuby.
61
+
62
+ * Aquarium can add and remove advice dynamically, at runtime.
63
+ * Aquarium can advise individual objects, not just classes.
64
+ * Aquarium can advise JDK classes. AspectJ can also do this, but not by default.
65
+ * Aquarium supports named advice that can be defined separately from the aspects that use the advice.
66
+ * Aquarium can advise ancestor (parent) types, not just derived (descendent) types of specified types.
67
+
68
+ Note: at the time of this writing (V0.4.0 release), there is an important limitation of Aquarium when used with java code; it appears that advice is only invoked if an advised method is called directly from Ruby code. If the advised method is called by other Java code, the advice is *not* invoked. Whether or not this limitation can be removed is under investigation.
69
+
70
+ Also, as of V0.4.0, the interaction behavior of Aquarium and AspectJ or Spring aspects has not been investigated.
54
71
 
55
72
  === Examples
56
73
 
@@ -142,7 +159,9 @@ You can specify a single type, a type name, a type regular expression, or an arr
142
159
  around :types => /A::.*Helper$/, ...
143
160
  around :types => [/A::.*Helper$/, /B::Foo.*/], ...
144
161
 
145
- Using the plural versions of the synonyms, with method specifications so they read better:
162
+ Everywhere "type" is used, you can substitute "class", "classes", "module", or "modules". Note that they are treated as synonyms; there is currently no enforcement that the values passed with ":class", for example, are actually classes, not modules.
163
+
164
+ Using the plural versions of the synonyms with method specifications sometimes read better:
146
165
 
147
166
  around :calls_to => :all_methods, :on_types => [A, B, ...], ...
148
167
  around :calls_to => :all_methods, :in_types => [A, B, ...], ...
@@ -154,6 +173,8 @@ You can specify types and their descendents (subclasses or included modules) or
154
173
  around :types_and_ancestors = A, ...
155
174
  around :type_and_descendents = A, ...
156
175
  around :types_and_descendents = A, ...
176
+ around :classes_and_descendents = A, ...
177
+ around :modules_and_descendents = A, ...
157
178
 
158
179
  Some of the synonyms:
159
180
 
@@ -324,6 +345,7 @@ Rather than passing a block as the advice, you can pass a previously-created Pro
324
345
  around :type => [...], :methods => :all, :call => advice # synonym for advice.
325
346
  around :type => [...], :methods => :all, :invoke => advice # synonym for advice.
326
347
 
348
+ Finally, when running in JRuby, you can advise Java types! See the examples in the separate RSpec suite in the "jruby" directory.
327
349
 
328
350
  === Packages
329
351
 
@@ -353,7 +375,7 @@ do the following:
353
375
 
354
376
  == Running Aquarium's RSpec Specs
355
377
 
356
- In order to run Aquarium's full suite of specs (rake pre_commit) you must install the following gems:
378
+ In order to run Aquarium's full suite of specs (rake pre_commit) you must install the following gems and tools:
357
379
 
358
380
  * rake # Runs the build script
359
381
  * rspec # Used instead of Test::Unit for TDD
@@ -365,6 +387,7 @@ In order to run Aquarium's full suite of specs (rake pre_commit) you must instal
365
387
  * win32console # Required by the --colour switch if you're on Windows
366
388
  * meta_project # Required in order to make releases at RubyForge
367
389
  * heckle # Required if you use the --heckle switch
390
+ * jruby # Required if run the separate spec suite in the "jruby" directory
368
391
 
369
392
  Once those are all installed, you should be able to run the suite with the following steps:
370
393
 
@@ -376,6 +399,12 @@ or
376
399
 
377
400
  Note that Aquarium itself - once built - doesn't have any dependencies outside the Ruby core and stdlib.
378
401
 
402
+ If you want to run the tests for the JRuby support, you must also have JRuby 1.1RC2 or later installed. To run the specs for JRuby, use the command
403
+
404
+ * rake verify_jruby
405
+
406
+ This command runs the standard Aquarium specs using JRuby instead of MRI, then runs a separate set of specs in the "jruby/spec" directory which test Aquarium with Java classes inside JRuby.
407
+
379
408
  See http://aquarium.rubyforge.org for further documentation.
380
409
 
381
410
  === Acknowledgments
data/RELEASE-PLAN CHANGED
@@ -15,9 +15,9 @@ This change will obvious break existing aspects.
15
15
 
16
16
  === Versions 0.3+.0
17
17
 
18
- More refinements and simpliciations to the API and functionality, including much-needed redundancy
19
- reduction. I haven't used mocks much in the specs, but they are clearly needed now to improve
20
- performance of RSpec runs.
18
+ More refinements and simplifications to the API and functionality, including much-needed
19
+ redundancy reduction. I haven't used mocks much in the specs, but they would help improve the
20
+ performance of the RSpec runs.
21
21
 
22
22
  The main thrust of new feature work will be expanding the pointcut language to include
23
23
  conditionals and stack context constructs, as well as more intuitive ways of expressing sets of
@@ -39,10 +39,13 @@ How about something like the following?
39
39
  end
40
40
 
41
41
  I'm not sure it adds much (at this stage of thinking about it...) except that it could make
42
- composition of complete pointcuts easier.
42
+ composition of complex pointcuts easier.
43
+
44
+ I also want to ensure full support for running in JRuby. In particular, you should be able to
45
+ advise Java types!
43
46
 
44
47
  === Version 1.0.0
45
48
 
46
- Reasonable stable and full-featured API and DSL. Also, to justify Aquarium's existence ;), I want
49
+ Reasonably stable and full-featured API and DSL. Also, to justify Aquarium's existence ;), I want
47
50
  to produce some non-trivial examples of refactoring known APIs and demonstrating improved clarity,
48
51
  productivity, modularity, etc., etc.
data/Rakefile CHANGED
@@ -44,6 +44,12 @@ Spec::Rake::SpecTask.new('spec_html') do |t|
44
44
  t.spec_opts = ['--format html:../doc/output/report.html','--backtrace']
45
45
  end
46
46
 
47
+ desc "Run all specs without rcov"
48
+ Spec::Rake::SpecTask.new('spec_no_rcov') do |t|
49
+ t.spec_files = FileList['spec/**/*_spec.rb', 'examples/**/*_spec.rb']
50
+ t.spec_opts = ['--colour']
51
+ end
52
+
47
53
  desc 'Generate HTML documentation for website'
48
54
  task :webgen do
49
55
  Dir.chdir '../doc' do
@@ -152,7 +158,7 @@ end
152
158
  # ]
153
159
 
154
160
  desc "Build the website, but do not publish it"
155
- task :website => [:clobber, :verify_rcov, :spec_html, :webgen, :rdoc]
161
+ task :website => [:clobber, :verify_rcov, :verify_jruby, :spec_html, :webgen, :rdoc]
156
162
 
157
163
  task :rdoc_rails do
158
164
  Dir.chdir '../aquarium_on_rails' do
@@ -161,6 +167,16 @@ task :rdoc_rails do
161
167
  end
162
168
  end
163
169
 
170
+ desc "Verify that the Aquarium specs run under JRuby and that JRuby can advise Java types"
171
+ task :verify_jruby do
172
+ puts "Verifying JRuby Support"
173
+ Dir.chdir 'jruby' do
174
+ rake = (PLATFORM == "i386-mswin32") ? "rake.cmd" : "rake"
175
+ sh "jruby -S #{rake}"
176
+ end
177
+ end
178
+
179
+
164
180
  task :verify_user do
165
181
  raise "RUBYFORGE_USER environment variable not set!" unless ENV['RUBYFORGE_USER']
166
182
  end
data/UPGRADE CHANGED
@@ -1,3 +1,8 @@
1
+ == Updating to Aquarium-0.4.0
2
+
3
+ This release is fully backwards-compatible with previous releases. It expands the API slightly and adds
4
+ internal improvements to better support JRuby. There should be no upgrade issues.
5
+
1
6
  == Updating to Aquarium-0.3.1
2
7
 
3
8
  There should be no upgrade issues with this release. However, the enhancement #17565 now ensures that a
@@ -122,7 +122,7 @@ module Aquarium
122
122
  class BeforeAdviceChainNode < AdviceChainNode
123
123
  def initialize options = {}
124
124
  super(options) { |jp, obj, *args|
125
- before_jp = jp.make_current_context_join_point :advice_kind => :before
125
+ before_jp = jp.make_current_context_join_point :advice_kind => :before, :current_advice_node => self
126
126
  advice.call(before_jp, obj, *args)
127
127
  next_node.call(jp, obj, *args)
128
128
  }
@@ -133,7 +133,7 @@ module Aquarium
133
133
  def initialize options = {}
134
134
  super(options) { |jp, obj, *args|
135
135
  returned_value = next_node.call(jp, obj, *args)
136
- next_jp = jp.make_current_context_join_point :advice_kind => :after_returning, :returned_value => returned_value
136
+ next_jp = jp.make_current_context_join_point :advice_kind => :after_returning, :returned_value => returned_value, :current_advice_node => self
137
137
  advice.call(next_jp, obj, *args)
138
138
  next_jp.context.returned_value # allow advice to modify the returned value
139
139
  }
@@ -150,7 +150,7 @@ module Aquarium
150
150
  next_node.call(jp, obj, *args)
151
151
  rescue Object => raised_exception
152
152
  if after_raising_exceptions_list_includes raised_exception
153
- next_jp = jp.make_current_context_join_point :advice_kind => :after_raising, :raised_exception => raised_exception
153
+ next_jp = jp.make_current_context_join_point :advice_kind => :after_raising, :raised_exception => raised_exception, :current_advice_node => self
154
154
  advice.call(next_jp, obj, *args)
155
155
  raised_exception = next_jp.context.raised_exception # allow advice to modify raised exception
156
156
  end
@@ -177,11 +177,11 @@ module Aquarium
177
177
  # can allow the advice to change the exception that will be raised.
178
178
  begin
179
179
  returned_value = next_node.call(jp, obj, *args)
180
- next_jp = jp.make_current_context_join_point :advice_kind => :after, :returned_value => returned_value
180
+ next_jp = jp.make_current_context_join_point :advice_kind => :after, :returned_value => returned_value, :current_advice_node => self
181
181
  advice.call(next_jp, obj, *args)
182
182
  next_jp.context.returned_value # allow advice to modify the returned value
183
183
  rescue Object => raised_exception
184
- next_jp = jp.make_current_context_join_point :advice_kind => :after, :raised_exception => raised_exception
184
+ next_jp = jp.make_current_context_join_point :advice_kind => :after, :raised_exception => raised_exception, :current_advice_node => self
185
185
  advice.call(next_jp, obj, *args)
186
186
  raise next_jp.context.raised_exception
187
187
  end
@@ -192,7 +192,7 @@ module Aquarium
192
192
  class AroundAdviceChainNode < AdviceChainNode
193
193
  def initialize options = {}
194
194
  super(options) { |jp, obj, *args|
195
- around_jp = jp.make_current_context_join_point :advice_kind => :around, :proceed_proc => next_node
195
+ around_jp = jp.make_current_context_join_point :advice_kind => :around, :proceed_proc => next_node, :current_advice_node => self
196
196
  advice.call(around_jp, obj, *args)
197
197
  }
198
198
  end
@@ -224,7 +224,9 @@ module Aquarium
224
224
  return
225
225
  end
226
226
  end
227
- logger.warn "Warning: No join points were matched. The options specified were #{@original_options.inspect}"
227
+ msg = "Warning: No join points were matched. The options specified were #{@original_options.inspect}."
228
+ msg += " The resulting specification was #{@specification.inspect}." if logger.debug?
229
+ logger.warn msg
228
230
  end
229
231
 
230
232
  def should_warn_if_no_matching_join_points
@@ -246,7 +248,6 @@ module Aquarium
246
248
  advice = @advice.to_proc
247
249
  @pointcuts.each do |pointcut|
248
250
  interesting_join_points(pointcut).each do |join_point|
249
- attr_name = Aspect.make_advice_chain_attr_sym(join_point)
250
251
  add_advice_framework(join_point) if need_advice_framework?(join_point)
251
252
  Advice.sort_by_priority_order(specified_advice_kinds).reverse.each do |advice_kind|
252
253
  add_advice_to_chain join_point, advice_kind, advice
@@ -319,19 +320,6 @@ module Aquarium
319
320
  EOF
320
321
  end
321
322
 
322
- def static_method_prefix join_point
323
- if join_point.instance_method?
324
- "@@type_being_advised = self"
325
- else
326
- "@@type_being_advised = self"
327
- "class << self"
328
- end
329
- end
330
-
331
- def static_method_suffix join_point
332
- join_point.instance_method? ? "" : "end"
333
- end
334
-
335
323
  # When advising an instance, create an override method that gets advised instead of the types method.
336
324
  # Otherwise, all objects will be advised!
337
325
  # Note: this also solves bug #15202.
@@ -383,6 +371,7 @@ module Aquarium
383
371
  end
384
372
 
385
373
  def remove_advice_for_aspect_at join_point
374
+ return unless Aspect.advice_chain_exists? join_point
386
375
  prune_nodes_in_advice_chain_for join_point
387
376
  advice_chain = Aspect.get_advice_chain join_point
388
377
  remove_advice_framework_for(join_point) if advice_chain.empty?
@@ -415,14 +404,19 @@ module Aquarium
415
404
  def restore_original_method_text join_point
416
405
  alias_method_name = (Aspect.make_saved_method_name join_point).intern
417
406
  <<-EOF
418
- #{static_method_prefix join_point}
407
+ #{join_point.instance_method? ? "" : "class << self"}
419
408
  #{unalias_original_method_text alias_method_name, join_point}
420
409
  #{undef_eigenclass_method_text join_point}
421
- #{static_method_suffix join_point}
410
+ #{join_point.instance_method? ? "" : "end"}
422
411
  EOF
423
412
  end
424
413
 
425
414
  # TODO optimize calls to these *_advice_chain methods from other private methods.
415
+ def self.advice_chain_exists? join_point
416
+ advice_chain_attr_sym = self.make_advice_chain_attr_sym join_point
417
+ type_to_advise_for(join_point).class_variable_defined? advice_chain_attr_sym
418
+ end
419
+
426
420
  def self.set_advice_chain join_point, advice_chain
427
421
  advice_chain_attr_sym = self.make_advice_chain_attr_sym join_point
428
422
  type_to_advise_for(join_point).send :class_variable_set, advice_chain_attr_sym, advice_chain
@@ -12,7 +12,7 @@ module Aquarium
12
12
  class ContextNotDefined < Exception; end
13
13
 
14
14
  class Context
15
- attr_accessor :advice_kind, :advised_object, :parameters, :block_for_method, :returned_value, :raised_exception, :proceed_proc
15
+ attr_accessor :advice_kind, :advised_object, :parameters, :block_for_method, :returned_value, :raised_exception, :proceed_proc, :current_advice_node
16
16
 
17
17
  alias :target_object :advised_object
18
18
  alias :target_object= :advised_object=
@@ -44,17 +44,17 @@ module Aquarium
44
44
 
45
45
  def proceed enclosing_join_point, *args, &block
46
46
  raise ProceedMethodNotAvailable.new("It looks like you tried to call \"JoinPoint#proceed\" (or \"JoinPoint::Context#proceed\") from within advice that isn't \"around\" advice. Only around advice can call proceed. (Specific error: JoinPoint#proceed cannot be called because no \"@proceed_proc\" attribute was set on the corresponding JoinPoint::Context object.)") if @proceed_proc.nil?
47
- do_invoke :call, enclosing_join_point, *args, &block
47
+ do_invoke proceed_proc, :call, enclosing_join_point, *args, &block
48
48
  end
49
49
 
50
50
  def invoke_original_join_point enclosing_join_point, *args, &block
51
- do_invoke :invoke_original_join_point, enclosing_join_point, *args, &block
51
+ do_invoke current_advice_node, :invoke_original_join_point, enclosing_join_point, *args, &block
52
52
  end
53
53
 
54
- def do_invoke method, enclosing_join_point, *args, &block
54
+ def do_invoke proc_to_send, method, enclosing_join_point, *args, &block
55
55
  args = parameters if (args.nil? or args.size == 0)
56
56
  enclosing_join_point.context.block_for_method = block if block
57
- proceed_proc.send method, enclosing_join_point, advised_object, *args
57
+ proc_to_send.send method, enclosing_join_point, advised_object, *args
58
58
  end
59
59
  protected :do_invoke
60
60
 
@@ -196,25 +196,36 @@ module Aquarium
196
196
  alias to_s inspect
197
197
 
198
198
  CANONICAL_OPTIONS = {
199
- "types" => %w[type for_type for_types on_type on_types in_type in_types within_type within_types],
200
- "objects" => %w[object for_object for_objects on_object on_objects in_object in_objects within_object within_objects],
201
- "join_points" => %w[join_point for_join_point for_join_points on_join_point on_join_points within_join_point within_join_points],
199
+ "types" => %w[type class classes module modules],
200
+ "types_and_descendents" => %w[type_and_descendents class_and_descendents classes_and_descendents module_and_descendents modules_and_descendents],
201
+ "types_and_ancestors" => %w[type_and_ancestors class_and_ancestors classes_and_ancestors module_and_ancestors modules_and_ancestors],
202
+ "objects" => %w[object],
203
+ "join_points" => %w[join_point],
202
204
  "methods" => %w[method within_method within_methods calling invoking calls_to invocations_of sending_message_to sending_messages_to],
203
205
  "attributes" => %w[attribute accessing],
204
206
  "method_options" => %w[method_option restricting_methods_to],
205
207
  "attribute_options" => %w[attribute_option],
206
- "types_and_descendents" => %w[type_and_descendents on_type_and_descendents on_types_and_descendents within_type_and_descendents within_types_and_descendents],
207
- "types_and_ancestors" => %w[type_and_ancestors on_type_and_ancestors on_types_and_ancestors within_type_and_ancestors within_types_and_ancestors],
208
208
  "default_objects" => %w[default_object]
209
209
  }
210
- %w[types objects join_points methods types_and_descendents types_and_ancestors].each do |key|
211
- CANONICAL_OPTIONS["exclude_#{key}"] = CANONICAL_OPTIONS[key].map {|x| "exclude_#{x}"}
210
+ %w[types types_and_descendents types_and_ancestors objects join_points ].each do |thing|
211
+ roots = CANONICAL_OPTIONS[thing].dup + [thing]
212
+ CANONICAL_OPTIONS["exclude_#{thing}"] = roots.map {|x| "exclude_#{x}"}
213
+ %w[for on in within].each do |prefix|
214
+ roots.each do |root|
215
+ CANONICAL_OPTIONS[thing] << "#{prefix}_#{root}"
216
+ end
217
+ end
212
218
  end
213
219
  CANONICAL_OPTIONS["methods"].dup.each do |synonym|
214
220
  CANONICAL_OPTIONS["methods"] << "#{synonym}_methods_matching"
215
221
  end
216
- CANONICAL_OPTIONS["exclude_pointcuts"] = %w[exclude_pointcut exclude_on_pointcut exclude_on_pointcuts exclude_within_pointcut exclude_within_pointcuts]
217
-
222
+ CANONICAL_OPTIONS["exclude_methods"] = []
223
+ CANONICAL_OPTIONS["methods"].each do |synonym|
224
+ CANONICAL_OPTIONS["exclude_methods"] << "exclude_#{synonym}"
225
+ end
226
+ CANONICAL_OPTIONS["exclude_pointcuts"] = ["exclude_pointcut"] +
227
+ %w[for on in within].map {|prefix| ["exclude_#{prefix}_pointcuts", "exclude_#{prefix}_pointcut"]}.flatten
228
+
218
229
  ATTRIBUTE_OPTIONS = %w[reading writing changing]
219
230
 
220
231
  ALL_ALLOWED_OPTIONS = ATTRIBUTE_OPTIONS +
@@ -339,8 +350,18 @@ module Aquarium
339
350
  def init_join_points
340
351
  @join_points_matched = Set.new
341
352
  @join_points_not_matched = Set.new
342
- find_join_points_for :type, (candidate_types - candidate_types_excluded), make_all_method_names
343
- find_join_points_for :object, candidate_objects, make_all_method_names
353
+ types = candidate_types - candidate_types_excluded
354
+ method_names = make_method_names
355
+ attribute_method_names = make_attribute_method_names
356
+ unless types.empty?
357
+ find_join_points_for(:type, types, method_names) unless method_names.empty?
358
+ find_join_points_for(:type, types, attribute_method_names) unless attribute_method_names.empty?
359
+ end
360
+ unless candidate_objects.empty?
361
+ find_join_points_for(:object, candidate_objects, method_names) unless method_names.empty?
362
+ find_join_points_for(:object, candidate_objects, attribute_method_names) unless attribute_method_names.empty?
363
+ end
364
+ subtract_attribute_writers if attributes_read_only?
344
365
  add_join_points_for_candidate_join_points
345
366
  remove_excluded_join_points
346
367
  end
@@ -364,7 +385,7 @@ module Aquarium
364
385
  def find_methods_for type_or_object_sym, candidates, which_methods
365
386
  return Aquarium::Finders::FinderResult::NIL_OBJECT if candidates.matched.size == 0
366
387
  Aquarium::Finders::MethodFinder.new.find type_or_object_sym => candidates.matched_keys,
367
- :methods => which_methods,
388
+ :methods => which_methods.to_a,
368
389
  :exclude_methods => @specification[:exclude_methods],
369
390
  :options => method_options
370
391
  end
@@ -385,64 +406,71 @@ module Aquarium
385
406
  end
386
407
  end
387
408
 
409
+ def subtract_attribute_writers
410
+ @join_points_matched.reject! do |jp|
411
+ jp.method_name.to_s[-1..-1] == '='
412
+ end
413
+ end
414
+
388
415
  def is_instance_methods?
389
416
  not @specification[:method_options].include? :class
390
417
  end
391
418
 
392
- def make_all_method_names
393
- @specification[:methods] +
394
- make_attribute_method_names(@specification[:attributes], @specification[:attribute_options]) -
395
- @specification[:exclude_methods]
419
+ def make_method_names
420
+ @specification[:methods] - @specification[:exclude_methods]
396
421
  end
397
422
 
398
- def make_attribute_method_names attribute_name_regexps_or_names, attribute_options = []
399
- readers = make_attribute_readers attribute_name_regexps_or_names
400
- return readers if read_only attribute_options
423
+ def make_attribute_method_names
424
+ readers = make_attribute_readers
425
+ return readers if attributes_read_only?
401
426
 
402
427
  writers = make_attribute_writers readers
403
- return writers if write_only attribute_options
428
+ return writers if attributes_write_only?
404
429
  return readers + writers
405
430
  end
406
431
 
407
- def make_attribute_readers attributes
408
- readers = attributes.map do |regexp_or_name|
432
+ # Because Ruby 1.8 regexp library doesn't support negative look behinds, we really
433
+ # can't set the regular expression to exclude a trailing = reliably. Instead,
434
+ # #init_join_points above will remove any writer methods, if necessary.
435
+ def make_attribute_readers
436
+ readers = @specification[:attributes].map do |regexp_or_name|
437
+ expr1 = regexp_or_name.kind_of?(Regexp) ? regexp_or_name.source : regexp_or_name.to_s
438
+ expr = remove_trailing_equals_and_or_dollar(remove_leading_colon_or_at_sign(expr1))
409
439
  if regexp_or_name.kind_of? Regexp
410
- exp = remove_trailing_equals_and_or_dollar regexp_or_name.source
411
- Regexp.new(remove_leading_colon_or_at_sign(exp + '.*\b$'))
440
+ Regexp.new(remove_leading_colon_or_at_sign(expr))
412
441
  else
413
- exp = remove_trailing_equals_and_or_dollar regexp_or_name.to_s
414
- remove_leading_colon_or_at_sign(exp.to_s)
442
+ expr
415
443
  end
416
444
  end
417
445
  Set.new(readers.sort_by {|exp| exp.to_s})
418
446
  end
419
-
420
- def make_attribute_writers attributes
421
- writers = attributes.map do |regexp_or_name|
447
+
448
+ def make_attribute_writers reader_methods
449
+ writers = reader_methods.map do |regexp_or_name|
450
+ expr = regexp_or_name.kind_of?(Regexp) ? regexp_or_name.source : regexp_or_name.to_s
422
451
  if regexp_or_name.kind_of? Regexp
423
- # remove the "\b$" from the end of the reader expression, if present.
424
- Regexp.new(remove_trailing_equals_and_or_dollar(regexp_or_name.source) + '=$')
452
+ Regexp.new(expr+'.*=$')
425
453
  else
426
- regexp_or_name + '='
454
+ expr + '='
427
455
  end
428
456
  end
429
457
  Set.new(writers.sort_by {|exp| exp.to_s})
430
458
  end
431
459
 
432
- def read_only attribute_options
433
- read_option(attribute_options) && !write_option(attribute_options)
460
+ def attributes_read_only?
461
+ read_option && !write_option
434
462
  end
435
463
 
436
- def write_only attribute_options
437
- write_option(attribute_options) && !read_option(attribute_options)
464
+ def attributes_write_only?
465
+ write_option && !read_option
438
466
  end
439
467
 
440
- def read_option attribute_options
441
- attribute_options.include?(:readers) or attribute_options.include?(:reader)
468
+ def read_option
469
+ @specification[:attribute_options].include?(:readers) or @specification[:attribute_options].include?(:reader)
442
470
  end
443
471
 
444
- def write_option attribute_options
445
- attribute_options.include?(:writers) or attribute_options.include?(:writer)
472
+ def write_option
473
+ @specification[:attribute_options].include?(:writers) or @specification[:attribute_options].include?(:writer)
446
474
  end
447
475
 
448
476
  def method_options
@@ -108,6 +108,7 @@ module Aquarium
108
108
 
109
109
  NIL_OBJECT = MethodFinder.new unless const_defined?(:NIL_OBJECT)
110
110
 
111
+ # TODO move Pointcut's options to here.
111
112
  CANONICAL_OPTIONS = {
112
113
  "types" => %w[type for_type for_types on_type on_types in_type in_types within_type within_types],
113
114
  "objects" => %w[object for_object for_objects on_object on_objects in_object in_objects within_object within_objects],
@@ -118,6 +118,7 @@ module Aquarium
118
118
  result - excluded
119
119
  end
120
120
 
121
+
121
122
  protected
122
123
 
123
124
  def handle_errors unknown_options, input_type_nil
@@ -152,8 +153,8 @@ module Aquarium
152
153
  result
153
154
  end
154
155
 
155
- def find_namespace_matched expression, option
156
- expr = expression.kind_of?(Regexp) ? expression.source : expression.to_s
156
+ def find_namespace_matched regexp, option
157
+ expr = regexp.source
157
158
  return nil if expr.empty?
158
159
  found_types = [Object]
159
160
  split_expr = expr.split("::")
@@ -162,22 +163,25 @@ module Aquarium
162
163
  found_types = find_next_types found_types, subexp, (index == 0), (index == (split_expr.size - 1))
163
164
  break if found_types.size == 0
164
165
  end
166
+ # JRuby returns types that aren't actually defined by the enclosing namespace.
167
+ # As a sanity check, reject types whose names don't match the full regexp.
168
+ found_types.reject! {|t| t.name !~ regexp}
165
169
  if found_types.size > 0
166
170
  finish_and_make_successful_result found_types, option
167
171
  else
168
- make_failed_result expression
172
+ make_failed_result regexp
169
173
  end
170
174
  end
171
175
 
172
176
  # For a name (not a regular expression), return the corresponding type.
173
177
  # (Adapted from the RubyQuiz #113 solution by James Edward Gray II)
178
+ # See also this blog: http://blog.sidu.in/2008/02/loading-classes-from-strings-in-ruby.html
179
+ # I discovered that eval works fine with JRuby wrapper classes, while splitting on '::' and
180
+ # calling const_get on each module fails!
174
181
  def find_by_name type_name, option
175
- name = type_name.to_s # in case it's a symbol...
176
- return nil if name.nil? || name.strip.empty?
177
- name.strip!
178
182
  begin
179
- found = [name.split("::").inject(Object) { |parent, const| parent.const_get(const) }]
180
- finish_and_make_successful_result found, option
183
+ found = eval type_name.to_s, binding, __FILE__, __LINE__
184
+ finish_and_make_successful_result [found], option
181
185
  rescue NameError
182
186
  make_failed_result type_name
183
187
  end
@@ -7,6 +7,7 @@ module Aquarium
7
7
  module DefaultLogger
8
8
 
9
9
  @@default_logger = Logger.new STDERR
10
+ @@default_logger.level = Logger::Severity::WARN
10
11
 
11
12
  def self.logger
12
13
  @@default_logger