Class: Treequel::Filter

Inherits:
Object
  • Object
show all
Extended by:
Loggability
Includes:
Constants::Patterns
Defined in:
lib/treequel/filter.rb

Overview

This is an object that is used to build an LDAP filter for Treequel::Branchsets.

Grammar (from RFC 2254) ==

filter     = "(" filtercomp ")"
filtercomp = and / or / not / item
and        = "&" filterlist
or         = "|" filterlist
not        = "!" filter
filterlist = 1*filter
item       = simple / present / substring / extensible
simple     = attr filtertype value
filtertype = equal / approx / greater / less
equal      = "="
approx     = "~="
greater    = ">="
less       = "<="
extensible = attr [":dn"] [":" matchingrule] ":=" value
             / [":dn"] ":" matchingrule ":=" value
present    = attr "=*"
substring  = attr "=" [initial] any [final]
initial    = value
any        = "*" *(value "*")
final      = value
attr       = AttributeDescription from Section 4.1.5 of [1]
matchingrule = MatchingRuleId from Section 4.1.9 of [1]
value      = AttributeValue from Section 4.1.6 of [1]

Defined Under Namespace

Classes: AndComponent, Component, FilterList, ItemComponent, NotComponent, OrComponent, PresentItemComponent, SimpleItemComponent, SubstringItemComponent

Constant Summary collapse

DEFAULT_EXPRESSION =

The default filter expression to use when searching if none is specified

[ :objectClass ]
LOGICAL_COMPONENTS =

The mapping of leftmost symbols in a boolean expression and the corresponding FilterComponent class.

{
	:or  => OrComponent,
	:|   => OrComponent,
	:and => AndComponent,
	:&   => AndComponent,
	:not => NotComponent,
	:"!" => NotComponent,
}
SEQUEL_FILTERTYPE_EQUIVALENTS =

An equivalence mapping of operation names from Sequel expressions into Treequel equivalents

{
	:like => :equal,
	:>=   => :greater,
	:<=   => :less,
}
UNSUPPORTED_SEQUEL_FILTERTYPES =

A list of filtertypes that come in as Sequel::Expressions; these generated nicer exception messages that just ‘unknown filtertype’

{
	:'~*' => %{LDAP doesn't support Regex filters},
	:'~'  => %{LDAP doesn't support Regex filters},
	:>    => %{LDAP doesn't support "greater-than"; use "greater-than-or-equal-to" (>=) instead},
	:<    => %{LDAP doesn't support "less-than"; use "less-than-or-equal-to" (<=) instead},
}

Constants included from Constants::Patterns

Constants::Patterns::ALPHA, Constants::Patterns::AMPERSAND, Constants::Patterns::ASSERTIONVALUE, Constants::Patterns::ASTERISK, Constants::Patterns::ATTRIBUTE_TYPE, Constants::Patterns::ATTRIBUTE_TYPE_AND_VALUE, Constants::Patterns::ATTRIBUTE_VALUE, Constants::Patterns::BASE64_CHAR, Constants::Patterns::BASE64_STRING, Constants::Patterns::COLON, Constants::Patterns::COMMA, Constants::Patterns::DESCR, Constants::Patterns::DIGIT, Constants::Patterns::DISTINGUISHED_NAME, Constants::Patterns::DN_ESCAPED, Constants::Patterns::DOLLAR, Constants::Patterns::DOT, Constants::Patterns::DQUOTE, Constants::Patterns::DSTRING, Constants::Patterns::EQUALS, Constants::Patterns::ESC, Constants::Patterns::ESCAPED, Constants::Patterns::EXCLAMATION, Constants::Patterns::EXTENSIONS, Constants::Patterns::FILL, Constants::Patterns::FOLD, Constants::Patterns::HEX, Constants::Patterns::HEXPAIR, Constants::Patterns::HEXSTRING, Constants::Patterns::HYPHEN, Constants::Patterns::KEYCHAR, Constants::Patterns::KEYSTRING, Constants::Patterns::KIND, Constants::Patterns::LANGLE, Constants::Patterns::LCURLY, Constants::Patterns::LDAP_ATTRIBUTE_DESCRIPTION, Constants::Patterns::LDAP_ATTRIBUTE_TYPE_DESCRIPTION, Constants::Patterns::LDAP_MATCHING_RULE_DESCRIPTION, Constants::Patterns::LDAP_MATCHING_RULE_USE_DESCRIPTION, Constants::Patterns::LDAP_MISORDERED_DESC_OBJECTCLASS_DESCRIPTION, Constants::Patterns::LDAP_MISORDERED_KIND_OBJECTCLASS_DESCRIPTION, Constants::Patterns::LDAP_MISORDERED_SYNTAX_ATTRIBUTE_TYPE_DESCRIPTION, Constants::Patterns::LDAP_OBJECTCLASS_DESCRIPTION, Constants::Patterns::LDAP_SUBSTRING_FILTER, Constants::Patterns::LDAP_SUBSTRING_FILTER_VALUE, Constants::Patterns::LDAP_SYNTAX_DESCRIPTION, Constants::Patterns::LDAP_TRAILING_KIND_OBJECTCLASS_DESCRIPTION, Constants::Patterns::LDAP_UNESCAPE_SQUOTE_ATTRIBUTE_TYPE_DESCRIPTION, Constants::Patterns::LDIF_ATTRIBUTE_DESCRIPTION, Constants::Patterns::LDIF_ATTRIBUTE_TYPE, Constants::Patterns::LDIF_ATTRTYPE_OPTION, Constants::Patterns::LDIF_ATTRTYPE_OPTIONS, Constants::Patterns::LDIF_ATTRVAL_SPEC, Constants::Patterns::LDIF_ATTR_TYPE_CHARS, Constants::Patterns::LDIF_OPT_CHAR, Constants::Patterns::LDIF_SAFE_CHAR, Constants::Patterns::LDIF_SAFE_INIT_CHAR, Constants::Patterns::LDIF_SAFE_STRING, Constants::Patterns::LDIF_VALUE_SPEC, Constants::Patterns::LDIGIT, Constants::Patterns::LEADCHAR, Constants::Patterns::LEADKEYCHAR, Constants::Patterns::LEN, Constants::Patterns::LPAREN, Constants::Patterns::LUTF1, Constants::Patterns::MALFORMED_DSTRING, Constants::Patterns::MALFORMED_QDSTRING, Constants::Patterns::NOIDLEN, Constants::Patterns::NORMAL, Constants::Patterns::NUL, Constants::Patterns::NUMBER, Constants::Patterns::NUMERICOID, Constants::Patterns::OCTET, Constants::Patterns::OID, Constants::Patterns::OIDLIST, Constants::Patterns::OIDS, Constants::Patterns::PAIR, Constants::Patterns::PLUS, Constants::Patterns::QDESCR, Constants::Patterns::QDESCRLIST, Constants::Patterns::QDESCRS, Constants::Patterns::QDSTRING, Constants::Patterns::QDSTRINGLIST, Constants::Patterns::QDSTRINGS, Constants::Patterns::QQ, Constants::Patterns::QS, Constants::Patterns::QUOTED_DESCR, Constants::Patterns::QUOTED_NUMERICOID, Constants::Patterns::QUTF1, Constants::Patterns::QUTF8, Constants::Patterns::RANGLE, Constants::Patterns::RCURLY, Constants::Patterns::RELATIVE_DISTINGUISHED_NAME, Constants::Patterns::RPAREN, Constants::Patterns::SEMI, Constants::Patterns::SEP, Constants::Patterns::SHARP, Constants::Patterns::SP, Constants::Patterns::SPACE, Constants::Patterns::SPECIAL, Constants::Patterns::SQUOTE, Constants::Patterns::STRING, Constants::Patterns::STRINGCHAR, Constants::Patterns::SUTF1, Constants::Patterns::TILDE, Constants::Patterns::TRAILCHAR, Constants::Patterns::TUTF1, Constants::Patterns::UNESCAPED, Constants::Patterns::URI_REF, Constants::Patterns::USAGE, Constants::Patterns::USCORE, Constants::Patterns::UTF0, Constants::Patterns::UTF1, Constants::Patterns::UTF1SUBSET, Constants::Patterns::UTF2, Constants::Patterns::UTF3, Constants::Patterns::UTF4, Constants::Patterns::UTF8, Constants::Patterns::UTFMB, Constants::Patterns::VALUEENCODING, Constants::Patterns::VERTBAR, Constants::Patterns::WSP, Constants::Patterns::XSTRING

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*expression_parts) ⇒ Filter

Create a new Treequel::Branchset::Filter with the specified expression.



675
676
677
678
679
680
681
# File 'lib/treequel/filter.rb', line 675

def initialize( *expression_parts )
	self.log.debug "New filter for expression: %p" % [ expression_parts ]
	@component = self.class.parse_expression( expression_parts )
	self.log.debug "  expression parsed into component: %p" % [ @component ]

	super()
end

Instance Attribute Details

#componentObject

The filtercomp part of the filter



689
690
691
# File 'lib/treequel/filter.rb', line 689

def component
  @component
end

Class Method Details

.parse_array_expression(expression) ⇒ Object

Turn the specified expression Array into a Treequel::Filter::Component object and return it.



462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
# File 'lib/treequel/filter.rb', line 462

def self::parse_array_expression( expression )
	self.log.debug "Parsing Array expression %p" % [ expression ]

	case

	# [ ] := '(objectClass=*)'
	when expression.empty?
		self.log.debug "  empty expression -> objectClass presence item component"
		return Treequel::Filter::PresentItemComponent.new

	# Collection of subfilters
	# [ [:uid, 'mahlon'], [:employeeNumber, 20202] ]
	when expression.all? {|elem| elem.is_a?(Array) }
		self.log.debug "  parsing array of subfilters"
		filters = expression.collect {|exp| Treequel::Filter.new(exp) }
		if filters.length > 1
			return Treequel::Filter::AndComponent.new( filters )
		else
			return filters.first
		end

	# Literal filters [ 'uid~=gung', 'l=bangkok' ]  := '(uid~=gung)(l=bangkok)'
	when expression.all? {|item| item.is_a?(String) }
		filters = expression.collect {|item| Treequel::Filter.new(item) }
		return Treequel::Filter::FilterList.new( filters )

	# Collection of subfilter objects
	when expression.all? {|elem| elem.is_a?(Treequel::Filter) }
		return Treequel::Filter::FilterList.new( expression )

	# [ :attribute ] := '(attribute=*)'
	when expression.length == 1
		return self.parse_expression( expression[0] )

	when expression[0].is_a?( Symbol )
		return self.parse_tuple_array_expression( expression )

	else
		raise Treequel::ExpressionError,
			"don't know how to turn %p into a filter component" % [ expression ]
	end

end

.parse_expression(expression) ⇒ Object

Turn the specified filter expression into a Treequel::Filter::Component object and return it.



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# File 'lib/treequel/filter.rb', line 423

def self::parse_expression( expression )
	self.log.debug "Parsing expression %p" % [ expression ]
	expression = expression[0] if expression.is_a?( Array ) && expression.length == 1

	case expression

	# String-literal filters
	when String
		return expression

	# 'Item' components
	when Array
		return self.parse_array_expression( expression )

	# Composite item components
	when Hash
		return self.parse_hash_expression( expression )

	# Unwrapped presence item filter
	when Symbol
		return Treequel::Filter::PresentItemComponent.new( expression )

	# Support Sequel expressions
	when Sequel::SQL::Expression
		return self.parse_sequel_expression( expression )

	# Filters and components can already act as components of other filters
	when Treequel::Filter, Treequel::Filter::Component
		return expression

	else
		raise Treequel::ExpressionError,
			"don't know how to turn %p into an filter component" % [ expression ]
	end
end

.parse_hash_expression(expression) ⇒ Object

Parse one or more tuples contained in a Hash into an ANDed set of Treequel::Filter::Components and return it.



509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
# File 'lib/treequel/filter.rb', line 509

def self::parse_hash_expression( expression )
	self.log.debug "Parsing Hash expression %p" % [ expression ]

	filterlist = expression.collect do |key, expr|
		self.log.debug "  adding %p => %p to the filter list" % [ key, expr ]
		if expr.respond_to?( :fetch )
			if expr.respond_to?( :length ) && expr.length > 1
				self.log.debug "    ORing together %d subfilters since %p has indices" %
					[ expr.length, expr ]
				subfilters = expr.collect {|val| Treequel::Filter.new(key, val) }
				Treequel::Filter.new( :or, subfilters )
			else
				self.log.debug "    unwrapping singular subfilter"
				Treequel::Filter.new([ key.to_sym, expr.first ])
			end
		else
			self.log.debug "    value is a scalar; creating a single filter"
			Treequel::Filter.new( key.to_sym, expr )
		end
	end

	if filterlist.length > 1
		return Treequel::Filter::AndComponent.new( *filterlist )
	else
		return filterlist.first
	end
end

.parse_item_component(attribute, value) ⇒ Object

Parse an item component from the specified attribute and value



587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
# File 'lib/treequel/filter.rb', line 587

def self::parse_item_component( attribute, value )
	self.log.debug "  tuple expression (%p=%p)-> item component" %
		[ attribute, value ]

	case
	when attribute.to_s.index( ':' )
		raise NotImplementedError, "extensible filters are not yet supported"
	when value == '*'
		return Treequel::Filter::PresentItemComponent.new( attribute )
	when value =~ LDAP_SUBSTRING_FILTER_VALUE
		return Treequel::Filter::SubstringItemComponent.new( attribute, value )
	else
		return Treequel::Filter::SimpleItemComponent.new( attribute, value )
	end
end

.parse_logical_array_expression(op, *components) ⇒ Object

Break down the given expression as a logical (AND, OR, or NOT) filter component and return it.



569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/treequel/filter.rb', line 569

def self::parse_logical_array_expression( op, *components )
	self.log.debug "Parsing logical %p expression with components: %p" %
		[ op, components ]

	compclass = LOGICAL_COMPONENTS[ op ] or
		raise "don't know what a %p condition is. I only know about: %p" %
			[ op, LOGICAL_COMPONENTS.keys ]

	filterlist = components.collect do |filterexp|
		self.log.debug "  making %p into a component" % [ filterexp ]
		Treequel::Filter.new( filterexp )
	end.flatten

	return compclass.new( *filterlist )
end

.parse_sequel_expression(expression) ⇒ Object

Parse a Sequel::SQL::Expression as a Treequel::Filter::Component and return it.



605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
# File 'lib/treequel/filter.rb', line 605

def self::parse_sequel_expression( expression )
	self.log.debug "  parsing Sequel expression: %p" % [ expression ]

	if expression.respond_to?( :op )
		op = expression.op.to_s.downcase.to_sym

		if equivalent = SEQUEL_FILTERTYPE_EQUIVALENTS[ op ]
			attribute, value = *expression.args

			# Turn :sn.like( 'bob' ) into (cn~=bob) 'cause it has no asterisks
			if op == :like
				if value.index( '*' )
					self.log.debug \
						"    turning a LIKE expression with an asterisk into a substring filter"
					return Treequel::Filter::SubstringItemComponent.new( attribute, value )
				else
					self.log.debug \
						"    turning a LIKE expression with no wildcards into an 'approx' filter"
					equivalent = :approx
				end
			end

			return Treequel::Filter::SimpleItemComponent.new( attribute, value, equivalent )

		elsif op == :'!='
			contents = Treequel::Filter.new( expression.args )
			return Treequel::Filter::NotComponent.new( contents )

		elsif op == :'not like'
			self.log.debug "  making a NOT LIKE expression out of: %p" % [ expression ]
			attribute, value = *expression.args
			component = nil

			if value.index( '*' )
				component = Treequel::Filter::SubstringItemComponent.new( attribute, value )
			else
				component = Treequel::Filter::SimpleItemComponent.new( attribute, value, :approx )
			end

			filter = Treequel::Filter.new( component )
			return Treequel::Filter::NotComponent.new( filter )

		elsif LOGICAL_COMPONENTS.key?( op )
			components = expression.args.collect do |comp|
				Treequel::Filter.new( comp )
			end

			return self.parse_logical_array_expression( op, components )

		elsif msg = UNSUPPORTED_SEQUEL_FILTERTYPES[ op ]
			raise Treequel::ExpressionError,
				"unsupported Sequel filter syntax %p: %s" %
				[ expression, msg ]
		else
			raise ScriptError,
				"  unhandled Sequel BooleanExpression: add handling for %p: %p" % [ op, expression ]
		end

	else
		raise Treequel::ExpressionError,
			"don't know how to turn %p into a component" % [ expression ]
	end
end

.parse_tuple_array_expression(expression) ⇒ Object

Parse a tuple of the form: [ Symbol, Object ] into a Treequel::Filter::Component and return it.



540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
# File 'lib/treequel/filter.rb', line 540

def self::parse_tuple_array_expression( expression )
	self.log.debug "Parsing tuple Array expression %p" % [ expression ]

	case expression[1]

	# [ :and/:or/:not, [:uid, 1] ]      := (&/|/!(uid=1))
	# [ :and/:or/:not, {:uid => 1} ]    := (&/|/!(uid=1))
	when Array, Hash
		return self.parse_logical_array_expression( *expression )

	when Range
		self.log.debug "  two ANDed item expressions from a Range"
		attribute = expression[0]
		range = expression[1]
		left = "#{attribute}>=#{range.begin}"
		right = "#{attribute}<=#{range.exclude_end? ? range.max : range.end}"
		return self.parse_logical_array_expression( :and, [left, right] )

	# [ :attribute, 'value' ]  := '(attribute=value)'
	# when String, Symbol, Numeric, Time
	else
		self.log.debug "  item expression from a %p" % [ expression[1].class ]
		return self.parse_item_component( *expression )
	end
end

Instance Method Details

#&(other_filter) ⇒ Object Also known as: +

Return a new Filter that is the AND filter of the receiver with other_filter.



731
732
733
734
735
# File 'lib/treequel/filter.rb', line 731

def &( other_filter )
	return other_filter if self.promiscuous?
	return self.dup if other_filter.promiscuous?
	return self.class.new( :and, [self, other_filter] )
end

#==(other_filter) ⇒ Object

Equality operator – returns true if other_filter is equivalent to the receiver.



725
726
727
# File 'lib/treequel/filter.rb', line 725

def ==( other_filter )
	return ( self.component == other_filter.component )
end

#inspectObject

Return a human-readable string representation of the filter suitable for debugging.



706
707
708
709
710
711
712
# File 'lib/treequel/filter.rb', line 706

def inspect
	return %{#<%s:0x%0x (%s)>} % [
		self.class.name,
		self.object_id * 2,
		self.component,
	]
end

#promiscuous?Boolean Also known as: is_promiscuous?

Returns true if the filter contains a single ‘present’ component for the objectClass attribute (which will match every entry)

Returns:

  • (Boolean)


717
718
719
# File 'lib/treequel/filter.rb', line 717

def promiscuous?
	return self.component.promiscuous?
end

#to_sObject

Return the Treequel::Branchset::Filter as a String.



693
694
695
696
697
698
699
700
701
# File 'lib/treequel/filter.rb', line 693

def to_s
	# self.log.debug "stringifying filter %p" % [ self ]
	filtercomp = self.component.to_s
	if filtercomp[0] == ?(
		return filtercomp
	else
		return '(' + filtercomp + ')'
	end
end

#|(other_filter) ⇒ Object

Return a new Filter that is the OR filter of the receiver with other_filter.



740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
# File 'lib/treequel/filter.rb', line 740

def |( other_filter )
	return other_filter if self.promiscuous?
	return self.dup if other_filter.promiscuous?

	# Collapse nested ORs into a single one with an additional alternation
	# if possible.
	if self.component.respond_to?( :add_alternation )
		self.log.debug "collapsing nested ORs..."
		newcomp = self.component.dup
		newcomp.add_alternation( other_filter )
		return self.class.new( newcomp )
	else
		return self.class.new( :or, [self, other_filter] )
	end
end