require 'set' require 'aquarium/aspects/join_point' require 'aquarium/utils' require 'aquarium/extensions' require 'aquarium/finders/finder_result' require 'aquarium/finders/type_finder' require 'aquarium/finders/method_finder' require 'aquarium/aspects/default_object_handler' module Aquarium module Aspects # == Pointcut # Pointcuts are queries on JoinPoints combined with binding of context data to # that will be useful during advice execution. The Pointcut locates the join points # that match the input criteria, remembering the found join points as well as the # the criteria that yielded no matches (mostly useful for debugging Pointcut definitions) class Pointcut include Aquarium::Utils::ArrayUtils include Aquarium::Utils::HashUtils include Aquarium::Utils::SetUtils include DefaultObjectHandler attr_reader :specification # Construct a Pointcut for methods in types or objects. # Pointcut.new :type{s} => [...] | :object{s} => [...] \ # {, :method{s} => [], :method_options => [...], \ # :attribute{s} => [...], :attribute_options[...]} # where # the "{}" indicate optional elements. For example, you can use # :types or :type. # # <tt>:types => type || [type_list]</tt>:: # <tt>:type => type || [type_list]</tt>:: # One or an array of types, type names and/or type regular expessions to match. # # <tt>:objects => object || [object_list]</tt>:: # <tt>:object => object || [object_list]</tt>:: # Objects to match. # # <tt>:default_object => object</tt>:: # An "internal" flag used by AspectDSL#pointcut when no object or type is specified, # the value of :default_object will be used, if defined. AspectDSL#pointcut sets the # value to self, so that the user doesn't have to in the appropriate contexts. # This flag is subject to change, so don't use it explicitly! # # <tt>:methods => method || [method_list]</tt>:: # <tt>:method => method || [method_list]</tt>:: # One or an array of methods, method names and/or method regular expessions to match. # By default, unless :attributes are specified, searches for public instance methods # with the method option :suppress_ancestor_methods implied, unless explicit method # options are given. # # <tt>:method_options => [options]</tt>:: # One or more options supported by Aquarium::Finders::MethodFinder. The :suppress_ancestor_methods # option is most useful. # # <tt>:attributes => attribute || [attribute_list]</tt>:: # <tt>:attribute => attribute || [attribute_list]</tt>:: # One or an array of attribute names and/or regular expessions to match. # This is syntactic sugar for the corresponding attribute readers and/or writers # methods, as specified using the <tt>:attrbute_options</tt>. Any matches will be # joined with the matched :methods.</tt>. # # <tt>:attribute_options => [options]</tt>:: # One or more of <tt>:readers</tt>, <tt>:reader</tt> (synonymous), # <tt>:writers</tt>, and/or <tt>:writer</tt> (synonymous). By default, both # readers and writers are matched. def initialize options = {} init_specification options init_candidate_types init_candidate_objects init_join_points end attr_reader :join_points_matched, :join_points_not_matched, :specification, :candidate_types, :candidate_objects # Two Considered equivalent only if the same join points matched and not_matched sets are equal, # the specifications are equal, and the candidate types and candidate objects are equal. # if you care only about the matched join points, then just compare #join_points_matched def eql? other object_id == other.object_id || (specification == other.specification && candidate_types == other.candidate_types && candidate_objects == other.candidate_objects && join_points_matched == other.join_points_matched && join_points_not_matched == other.join_points_not_matched) end alias :== :eql? alias :=== :eql? def empty? return join_points_matched.empty? && join_points_not_matched.empty? end def inspect "Pointcut: {specification: #{specification.inspect}, candidate_types: #{candidate_types.inspect}, candidate_objects: #{candidate_objects.inspect}, join_points_matched: #{join_points_matched.inspect}, join_points_not_matched: #{join_points_not_matched.inspect}}" end alias to_s inspect def self.make_attribute_method_names attribute_name_regexps_or_names, attribute_options = [] readers = make_attribute_readers attribute_name_regexps_or_names return readers if read_only attribute_options writers = make_attribute_writers readers return writers if write_only attribute_options return readers + writers end protected attr_writer :join_points_matched, :join_points_not_matched, :specification, :candidate_types, :candidate_objects def init_specification options @specification = {} options ||= {} @specification[:method_options] = Set.new(make_array(options[:method_options])) @specification[:attribute_options] = Set.new(make_array(options[:attribute_options]) ) @specification[:types] = Set.new(make_array(options[:types], options[:type])) @specification[:objects] = Set.new(make_array(options[:objects], options[:object])) @specification[:default_object] = Set.new(make_array(options[:default_object])) use_default_object_if_defined unless (types_given? || objects_given?) @specification[:attributes] = Set.new(make_array(options[:attributes], options[:attribute])) raise Aquarium::Utils::InvalidOptions.new(":all is not yet supported for :attributes.") if @specification[:attributes] == Set.new([:all]) init_methods_specification options end def init_methods_specification options @specification[:methods] = Set.new(make_array(options[:methods], options[:method])) @specification[:methods].add(:all) if @specification[:methods].empty? and @specification[:attributes].empty? end def self.read_only attribute_options read_option(attribute_options) && !write_option(attribute_options) end def self.write_only attribute_options write_option(attribute_options) && !read_option(attribute_options) end def self.read_option attribute_options attribute_options.include?(:readers) || attribute_options.include?(:reader) end def self.write_option attribute_options attribute_options.include?(:writers) || attribute_options.include?(:writer) end %w[types objects methods attributes method_options attribute_options].each do |name| class_eval(<<-EOF, __FILE__, __LINE__) def #{name}_given @specification[:#{name}] end def #{name}_given? not (#{name}_given.nil? or #{name}_given.empty?) end EOF end private def init_candidate_types explicit_types, type_regexps_or_names = @specification[:types].partition do |type| type.kind_of?(Module) || type.kind_of?(Class) end @candidate_types = Aquarium::Finders::TypeFinder.new.find :types => type_regexps_or_names @candidate_types.append_matched(make_hash(explicit_types) {|x| Set.new([])}) # Append already-known types end def init_candidate_objects object_hash = {} @specification[:objects].each {|o| object_hash[o] = Set.new([])} @candidate_objects = Aquarium::Finders::FinderResult.new object_hash end def init_join_points @join_points_matched = Set.new @join_points_not_matched = Set.new results = find_methods_for_types make_all_method_names add_join_points @join_points_matched, results.matched, :type add_join_points @join_points_not_matched, results.not_matched, :type results = find_methods_for_objects make_all_method_names add_join_points @join_points_matched, results.matched, :object add_join_points @join_points_not_matched, results.not_matched, :object end def find_methods_for_types which_methods return Aquarium::Finders::FinderResult::NIL_OBJECT if candidate_types.matched.size == 0 Aquarium::Finders::MethodFinder.new.find :types => candidate_types.matched_keys, :methods => which_methods, :options => @specification[:method_options].to_a end def find_methods_for_objects which_methods return Aquarium::Finders::FinderResult::NIL_OBJECT if candidate_objects.matched.size == 0 Aquarium::Finders::MethodFinder.new.find :objects => candidate_objects.matched_keys, :methods => which_methods, :options => @specification[:method_options].to_a end def add_join_points which_join_points_list, results_hash, type_or_object_sym instance_method = @specification[:method_options].include?(:class) ? false : true results_hash.each_pair do |type_or_object, method_name_list| method_name_list.each do |method_name| which_join_points_list << Aquarium::Aspects::JoinPoint.new( type_or_object_sym => type_or_object, :method_name => method_name, :instance_method => instance_method) end end end def make_all_method_names @specification[:methods] + Pointcut.make_attribute_method_names(@specification[:attributes], @specification[:attribute_options]) end def self.make_attribute_readers attributes readers = attributes.map do |regexp_or_name| if regexp_or_name.kind_of? Regexp exp = remove_trailing_equals_and_or_dollar regexp_or_name.source Regexp.new(remove_leading_colon_or_at_sign(exp + '.*\b$')) else exp = remove_trailing_equals_and_or_dollar regexp_or_name.to_s remove_leading_colon_or_at_sign(exp.to_s) end end Set.new(readers.sort_by {|exp| exp.to_s}) end def self.make_attribute_writers attributes writers = attributes.map do |regexp_or_name| if regexp_or_name.kind_of? Regexp # remove the "\b$" from the end of the reader expression, if present. Regexp.new(remove_trailing_equals_and_or_dollar(regexp_or_name.source) + '=$') else regexp_or_name + '=' end end Set.new(writers.sort_by {|exp| exp.to_s}) end def self.remove_trailing_equals_and_or_dollar exp exp.gsub(/\=?\$?$/, '') end def self.remove_leading_colon_or_at_sign exp exp.gsub(/^\^?(@|:)/, '') end end end end