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