treequel 1.0.0

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 (74) hide show
  1. data/ChangeLog +354 -0
  2. data/LICENSE +27 -0
  3. data/README +66 -0
  4. data/Rakefile +345 -0
  5. data/Rakefile.local +43 -0
  6. data/bin/treeirb +14 -0
  7. data/bin/treequel +229 -0
  8. data/examples/company-directory.rb +112 -0
  9. data/examples/ldap-monitor.rb +143 -0
  10. data/examples/ldap-monitor/public/css/master.css +328 -0
  11. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  12. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  13. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  14. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  15. data/examples/ldap-monitor/public/images/plug.png +0 -0
  16. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  17. data/examples/ldap-monitor/public/images/tick.png +0 -0
  18. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  19. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  20. data/examples/ldap-monitor/views/backends.erb +41 -0
  21. data/examples/ldap-monitor/views/connections.erb +74 -0
  22. data/examples/ldap-monitor/views/databases.erb +39 -0
  23. data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
  24. data/examples/ldap-monitor/views/index.erb +14 -0
  25. data/examples/ldap-monitor/views/layout.erb +35 -0
  26. data/examples/ldap-monitor/views/listeners.erb +30 -0
  27. data/examples/ldap_state.rb +62 -0
  28. data/lib/treequel.rb +145 -0
  29. data/lib/treequel/branch.rb +589 -0
  30. data/lib/treequel/branchcollection.rb +204 -0
  31. data/lib/treequel/branchset.rb +360 -0
  32. data/lib/treequel/constants.rb +604 -0
  33. data/lib/treequel/directory.rb +541 -0
  34. data/lib/treequel/exceptions.rb +32 -0
  35. data/lib/treequel/filter.rb +704 -0
  36. data/lib/treequel/mixins.rb +325 -0
  37. data/lib/treequel/schema.rb +245 -0
  38. data/lib/treequel/schema/attributetype.rb +252 -0
  39. data/lib/treequel/schema/ldapsyntax.rb +96 -0
  40. data/lib/treequel/schema/matchingrule.rb +124 -0
  41. data/lib/treequel/schema/matchingruleuse.rb +124 -0
  42. data/lib/treequel/schema/objectclass.rb +289 -0
  43. data/lib/treequel/sequel_integration.rb +26 -0
  44. data/lib/treequel/utils.rb +169 -0
  45. data/rake/191_compat.rb +26 -0
  46. data/rake/dependencies.rb +76 -0
  47. data/rake/helpers.rb +434 -0
  48. data/rake/hg.rb +261 -0
  49. data/rake/manual.rb +782 -0
  50. data/rake/packaging.rb +135 -0
  51. data/rake/publishing.rb +318 -0
  52. data/rake/rdoc.rb +30 -0
  53. data/rake/style.rb +62 -0
  54. data/rake/svn.rb +668 -0
  55. data/rake/testing.rb +187 -0
  56. data/rake/verifytask.rb +64 -0
  57. data/rake/win32.rb +190 -0
  58. data/spec/lib/constants.rb +93 -0
  59. data/spec/lib/helpers.rb +100 -0
  60. data/spec/treequel/branch_spec.rb +569 -0
  61. data/spec/treequel/branchcollection_spec.rb +213 -0
  62. data/spec/treequel/branchset_spec.rb +376 -0
  63. data/spec/treequel/directory_spec.rb +487 -0
  64. data/spec/treequel/filter_spec.rb +482 -0
  65. data/spec/treequel/mixins_spec.rb +330 -0
  66. data/spec/treequel/schema/attributetype_spec.rb +237 -0
  67. data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
  68. data/spec/treequel/schema/matchingrule_spec.rb +158 -0
  69. data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
  70. data/spec/treequel/schema/objectclass_spec.rb +262 -0
  71. data/spec/treequel/schema_spec.rb +118 -0
  72. data/spec/treequel/utils_spec.rb +49 -0
  73. data/spec/treequel_spec.rb +179 -0
  74. metadata +169 -0
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'treequel'
4
+
5
+
6
+ # A collection of exceptions for Treequel.
7
+ #
8
+ # == Authors
9
+ #
10
+ # * Michael Granger <ged@FaerieMUD.org>
11
+ #
12
+ # :include: LICENSE
13
+ #
14
+ #--
15
+ #
16
+ # Please see the file LICENSE in the base directory for licensing details.
17
+ #
18
+ module Treequel
19
+
20
+ ### The base Treequel exception type
21
+ class Error < ::RuntimeError; end
22
+
23
+ ### Schema parsing errors
24
+ class ParseError < Treequel::Error; end
25
+
26
+ ### Exception type raised when an expression cannot be parsed from the
27
+ ### arguments given to Treequel::Filter.new
28
+ class ExpressionError < Treequel::Error; end
29
+
30
+ end # module Treequel
31
+
32
+
@@ -0,0 +1,704 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ldap'
4
+
5
+ require 'treequel'
6
+ require 'treequel/branchset'
7
+ require 'treequel/exceptions'
8
+ require 'treequel/sequel_integration'
9
+
10
+
11
+ # This is an object that is used to build an LDAP filter for Treequel::Branchsets.
12
+ #
13
+ # == Grammar (from RFC 2254) ==
14
+ #
15
+ # filter = "(" filtercomp ")"
16
+ # filtercomp = and / or / not / item
17
+ # and = "&" filterlist
18
+ # or = "|" filterlist
19
+ # not = "!" filter
20
+ # filterlist = 1*filter
21
+ # item = simple / present / substring / extensible
22
+ # simple = attr filtertype value
23
+ # filtertype = equal / approx / greater / less
24
+ # equal = "="
25
+ # approx = "~="
26
+ # greater = ">="
27
+ # less = "<="
28
+ # extensible = attr [":dn"] [":" matchingrule] ":=" value
29
+ # / [":dn"] ":" matchingrule ":=" value
30
+ # present = attr "=*"
31
+ # substring = attr "=" [initial] any [final]
32
+ # initial = value
33
+ # any = "*" *(value "*")
34
+ # final = value
35
+ # attr = AttributeDescription from Section 4.1.5 of [1]
36
+ # matchingrule = MatchingRuleId from Section 4.1.9 of [1]
37
+ # value = AttributeValue from Section 4.1.6 of [1]
38
+ #
39
+ #
40
+ # == Authors
41
+ #
42
+ # * Michael Granger <ged@FaerieMUD.org>
43
+ # * Mahlon E. Smith <mahlon@martini.nu>
44
+ #
45
+ # :include: LICENSE
46
+ #
47
+ #--
48
+ #
49
+ # Please see the file LICENSE in the base directory for licensing details.
50
+ #
51
+ class Treequel::Filter
52
+ include Treequel::Loggable,
53
+ Treequel::Constants::Patterns
54
+
55
+ ### Filter list component of a Treequel::Filter.
56
+ class FilterList
57
+ include Treequel::Loggable
58
+
59
+ ### Create a new filter list with the given +filters+ in it.
60
+ def initialize( *filters )
61
+ @filters = filters.flatten
62
+ end
63
+
64
+ ######
65
+ public
66
+ ######
67
+
68
+ # The filters in the FilterList
69
+ attr_reader :filters
70
+
71
+
72
+ ### Return the FilterList as a string.
73
+ def to_s
74
+ return self.filters.collect {|f| f.to_s }.join
75
+ end
76
+
77
+ end # class FilterList
78
+
79
+
80
+ ### An abstract class for filter components.
81
+ class Component
82
+ include Treequel::Loggable
83
+
84
+ # Hide this class's new method
85
+ private_class_method :new
86
+
87
+ ### Inherited hook: re-expose inheriting class's .new method
88
+ def self::inherited( klass )
89
+ klass.module_eval( 'public_class_method :new' )
90
+ super
91
+ end
92
+
93
+
94
+ ### Stringify the component.
95
+ ### :TODO: If this doesn't end up being a refactored version of all of its
96
+ ### subclasses's #to_s methods, test that it needs overriding.
97
+ def to_s
98
+ raise NotImplementedError, "%s does not provide an implementation of #to_s" %
99
+ [ self.class.name ]
100
+ end
101
+
102
+
103
+ ### Return a human-readable string representation of the component suitable
104
+ ### for debugging.
105
+ def inspect
106
+ return %Q{#<%s:0x%0x "%s">} % [
107
+ self.class.name,
108
+ self.object_id * 2,
109
+ self.to_s,
110
+ ]
111
+ end
112
+
113
+ ### Components are non-promiscuous (don't match everything) by default.
114
+ def promiscuous?
115
+ return false
116
+ end
117
+
118
+ end # class Component
119
+
120
+
121
+ ### A filtercomp that negates the filter it contains.
122
+ class NotComponent < Treequel::Filter::Component
123
+
124
+ ### Create an negation component of the specified +filter+.
125
+ def initialize( filter )
126
+ @filter = filter
127
+ end
128
+
129
+ ### Return the stringified filter form of the receiver.
130
+ def to_s
131
+ return '!' + @filter.to_s
132
+ end
133
+
134
+ end
135
+
136
+
137
+ ### An 'and' filter component
138
+ class AndComponent < Treequel::Filter::Component
139
+
140
+ ### Create a new 'and' filter component with the given +filterlist+.
141
+ def initialize( *filterlist )
142
+ @filterlist = Treequel::Filter::FilterList.new( filterlist )
143
+ super()
144
+ end
145
+
146
+ # The list of filters to AND together in the filter string
147
+ attr_reader :filterlist
148
+
149
+ ### Stringify the item
150
+ def to_s
151
+ return '&' + @filterlist.to_s
152
+ end
153
+
154
+ end # AndComponent
155
+
156
+
157
+ ### An 'or' filter component
158
+ class OrComponent < Treequel::Filter::Component
159
+
160
+ ### Create a new 'or' filter component with the given +filterlist+.
161
+ def initialize( *filterlist )
162
+ @filterlist = Treequel::Filter::FilterList.new( filterlist )
163
+ super()
164
+ end
165
+
166
+ # The list of filters to OR together in the filter string
167
+ attr_reader :filterlist
168
+
169
+ ### Stringify the item
170
+ def to_s
171
+ return '|' + @filterlist.to_s
172
+ end
173
+
174
+ end # class OrComponent
175
+
176
+
177
+ ### An 'item' filter component
178
+ class ItemComponent < Treequel::Filter::Component; end
179
+
180
+
181
+ ### A simple (attribute=value) component
182
+ ### simple = attr filtertype value
183
+ ### filtertype = equal / approx / greater / less
184
+ ### equal = "="
185
+ ### approx = "~="
186
+ ### greater = ">="
187
+ ### less = "<="
188
+ class SimpleItemComponent < Treequel::Filter::ItemComponent
189
+
190
+ # The valid values for +filtertype+ and the equivalent operator
191
+ FILTERTYPE_OP = {
192
+ :equal => '=',
193
+ :approx => '~=',
194
+ :greater => '>=',
195
+ :less => '<=',
196
+ }
197
+ FILTERTYPE_OP.freeze
198
+
199
+ # Inverse of the FILTERTYPE_OP mapping (symbol -> name)
200
+ FILTEROP_NAMES = FILTERTYPE_OP.invert.freeze
201
+
202
+ # A regex that matches any of the 'simple' operators.
203
+ FILTER_SPLIT_REGEXP = Regexp.new( '(' + FILTERTYPE_OP.values.join('|') + ')' )
204
+ FILTER_SPLIT_REGEXP.freeze
205
+
206
+
207
+ ### Parse a new SimpleItemComponent from the specified +literal+.
208
+ def self::parse_from_string( literal )
209
+ parts = literal.split( FILTER_SPLIT_REGEXP, 3 )
210
+ unless parts.length == 3
211
+ raise Treequel::ExpressionError,
212
+ "unable to parse %p as a string literal" % [ literal ]
213
+ end
214
+
215
+ attribute, operator, value = *parts
216
+ filtertype = FILTEROP_NAMES[ operator ]
217
+
218
+ return self.new( attribute, value, filtertype )
219
+ end
220
+
221
+
222
+ ### Create a new 'simple' item filter component with the given
223
+ ### +attribute+, +filtertype+, and +value+. The +filtertype+ should
224
+ ### be one of: :equal, :approx, :greater, :less
225
+ def initialize( attribute, value, filtertype=:equal )
226
+ self.log.debug "creating a new %s %s for %p and %p" %
227
+ [ filtertype, self.class.name, attribute, value ]
228
+
229
+ filtertype = filtertype.to_s.downcase.to_sym
230
+
231
+ if FILTERTYPE_OP.key?( filtertype )
232
+ # no-op
233
+ elsif FILTEROP_NAMES.key?( filtertype.to_s )
234
+ filtertype = FILTEROP_NAMES[ filtertype.to_s ]
235
+ else
236
+ raise Treequel::ExpressionError,
237
+ "invalid simple item operator %p" % [ filtertype ]
238
+ end
239
+
240
+ @attribute = attribute
241
+ @value = value
242
+ @filtertype = filtertype
243
+ end
244
+
245
+
246
+ ######
247
+ public
248
+ ######
249
+
250
+ # The name of the item's attribute
251
+ attr_accessor :attribute
252
+
253
+ # The item's value
254
+ attr_accessor :value
255
+
256
+ # The item's filter type (one of FILTERTYPE_OP.keys)
257
+ attr_accessor :filtertype
258
+
259
+
260
+ ### The operator that is associated with the item's +filtertype+.
261
+ def filtertype_op
262
+ FILTERTYPE_OP[ self.filtertype.to_sym ]
263
+ end
264
+
265
+
266
+ ### Stringify the component
267
+ def to_s
268
+ return [ self.attribute, self.filtertype_op, self.value ].join
269
+ end
270
+
271
+ end # class SimpleItemComponent
272
+
273
+
274
+ ### A 'present' (attribute=*) component
275
+ class PresentItemComponent < Treequel::Filter::ItemComponent
276
+
277
+ # The default attribute to test for presence if none is specified
278
+ DEFAULT_ATTRIBUTE = :objectClass
279
+
280
+ ### Create a new 'presence' item filter component for the given +attribute+.
281
+ def initialize( attribute=DEFAULT_ATTRIBUTE )
282
+ @attribute = attribute
283
+ end
284
+
285
+
286
+ ### Stringify the component
287
+ def to_s
288
+ return @attribute.to_s + '=*'
289
+ end
290
+
291
+
292
+ ### Returns true, indicating that this component in a filter will match every
293
+ ### entry if its attribute is 'objectClass'.
294
+ def promiscuous?
295
+ return @attribute.to_sym == DEFAULT_ATTRIBUTE
296
+ end
297
+
298
+ end # class PresentItemComponent
299
+
300
+
301
+ ### A 'substring' (attribute=foo*) component
302
+ class SubstringItemComponent < Treequel::Filter::ItemComponent
303
+ include Treequel::Constants::Patterns
304
+
305
+
306
+ ### Parse the substring item from the given +literal+.
307
+ def self::parse_from_string( literal )
308
+ match = LDAP_SUBSTRING_FILTER.match( literal ) or
309
+ raise Treequel::ExpressionError,
310
+ "unable to parse %p as a substring literal" % [ literal ]
311
+
312
+ Treequel.logger.debug " parsed substring literal as: %p" % [ match.captures ]
313
+ return self.new( *(match.captures.values_at(1,3,2)) )
314
+ end
315
+
316
+
317
+ #############################################################
318
+ ### I N S T A N C E M E T H O D S
319
+ #############################################################
320
+
321
+ ### Create a new 'presence' item filter component for the given +attribute+.
322
+ def initialize( attribute, pattern, options=nil )
323
+ @attribute = attribute
324
+ @pattern = pattern
325
+ @options = options
326
+
327
+ super()
328
+ end
329
+
330
+
331
+ ######
332
+ public
333
+ ######
334
+
335
+ # The name of the attribute to match against
336
+ attr_accessor :attribute
337
+
338
+ # The pattern to match (if the index exists in the directory)
339
+ attr_accessor :pattern
340
+
341
+ # The attribute options
342
+ attr_accessor :options
343
+
344
+
345
+ ### Stringify the component
346
+ def to_s
347
+ return self.attribute.to_s + self.options.to_s + '=' + self.pattern
348
+ end
349
+
350
+ end # class SubstringItemComponent
351
+
352
+
353
+
354
+ #################################################################
355
+ ### F I L T E R C O N S T A N T S
356
+ #################################################################
357
+
358
+ # The default filter expression to use when searching if none is specified
359
+ DEFAULT_EXPRESSION = [ :objectClass ]
360
+ DEFAULT_EXPRESSION.freeze
361
+
362
+ # The mapping of leftmost symbols in a boolean expression and the
363
+ # corresponding FilterComponent class.
364
+ LOGICAL_COMPONENTS = {
365
+ :or => OrComponent,
366
+ :| => OrComponent,
367
+ :and => AndComponent,
368
+ :& => AndComponent,
369
+ :not => NotComponent,
370
+ :"!" => NotComponent,
371
+ }
372
+
373
+ # An equivalence mapping of operation names from Sequel expressions into
374
+ # Treequel equivalents
375
+ SEQUEL_FILTERTYPE_EQUIVALENTS = {
376
+ :like => :equal,
377
+ :>= => :greater,
378
+ :<= => :less,
379
+ }
380
+ SEQUEL_FILTERTYPE_EQUIVALENTS.freeze
381
+
382
+ # A list of filtertypes that come in as Sequel::Expressions; these generated nicer
383
+ # exception messages that just 'unknown filtertype'
384
+ UNSUPPORTED_SEQUEL_FILTERTYPES = {
385
+ :'~*' => %{LDAP doesn't support Regex filters},
386
+ :'~' => %{LDAP doesn't support Regex filters},
387
+ :> => %{LDAP doesn't support "greater-than"; use "greater-than-or-equal-to" (>=) instead},
388
+ :< => %{LDAP doesn't support "less-than"; use "less-than-or-equal-to" (<=) instead},
389
+ }
390
+
391
+
392
+
393
+ ### Turn the specified filter +expression+ into a Treequel::Filter::Component
394
+ ### object and return it.
395
+ def self::parse_expression( expression )
396
+ Treequel.logger.debug "Parsing expression %p" % [ expression ]
397
+ expression = expression[0] if expression.is_a?( Array ) && expression.length == 1
398
+
399
+ case expression
400
+
401
+ # String-literal filters
402
+ when String
403
+ return expression
404
+
405
+ # 'Item' components
406
+ when Array
407
+ return self.parse_array_expression( expression )
408
+
409
+ # Composite item components
410
+ when Hash
411
+ return self.parse_hash_expression( expression )
412
+
413
+ # Unwrapped presence item filter
414
+ when Symbol
415
+ return Treequel::Filter::PresentItemComponent.new( expression )
416
+
417
+ # Support Sequel expressions
418
+ when Sequel::SQL::Expression
419
+ return self.parse_sequel_expression( expression )
420
+
421
+ # Filters and components can already act as components of other filters
422
+ when Treequel::Filter, Treequel::Filter::Component
423
+ return expression
424
+
425
+ else
426
+ raise Treequel::ExpressionError,
427
+ "don't know how to turn %p into an filter component" % [ expression ]
428
+ end
429
+ end
430
+
431
+
432
+ ### Turn the specified expression Array into a Treequel::Filter::Component object
433
+ ### and return it.
434
+ def self::parse_array_expression( expression )
435
+ Treequel.logger.debug "Parsing Array expression %p" % [ expression ]
436
+
437
+ case
438
+
439
+ # [ ] := '(objectClass=*)'
440
+ when expression.empty?
441
+ Treequel.logger.debug " empty expression -> objectClass presence item component"
442
+ return Treequel::Filter::PresentItemComponent.new
443
+
444
+ # Collection of subfilters
445
+ # [ [:uid, 'mahlon'], [:employeeNumber, 20202] ]
446
+ when expression.all? {|elem| elem.is_a?(Array) }
447
+ Treequel.logger.debug " parsing array of subfilters"
448
+ filters = expression.collect {|exp| Treequel::Filter.new(exp) }
449
+ if filters.length > 1
450
+ return Treequel::Filter::AndComponent.new( filters )
451
+ else
452
+ return filters.first
453
+ end
454
+
455
+ # Literal filters [ 'uid~=gung', 'l=bangkok' ] := '(uid~=gung)(l=bangkok)'
456
+ when expression.all? {|item| item.is_a?(String) }
457
+ filters = expression.collect {|item| Treequel::Filter.new(item) }
458
+ return Treequel::Filter::FilterList.new( filters )
459
+
460
+ # Collection of subfilter objects
461
+ when expression.all? {|elem| elem.is_a?(Treequel::Filter) }
462
+ return Treequel::Filter::FilterList.new( expression )
463
+
464
+ # [ :attribute ] := '(attribute=*)'
465
+ when expression.length == 1
466
+ return self.parse_expression( expression[0] )
467
+
468
+ when expression[0].is_a?( Symbol )
469
+ return self.parse_tuple_array_expression( expression )
470
+
471
+ else
472
+ raise Treequel::ExpressionError,
473
+ "don't know how to turn %p into a filter component" % [ expression ]
474
+ end
475
+
476
+ end
477
+
478
+
479
+ ### Parse one or more tuples contained in a Hash into an ANDed set of
480
+ ### Treequel::Filter::Components and return it.
481
+ def self::parse_hash_expression( expression )
482
+ Treequel.logger.debug "Parsing Hash expression %p" % [ expression ]
483
+
484
+ filterlist = expression.collect do |key, expr|
485
+ Treequel.logger.debug " adding %p => %p to the filter list" % [ key, expr ]
486
+ if expr.respond_to?( :fetch )
487
+ Treequel.logger.debug " ORing together %d subfilters since %p has indices" %
488
+ [ expr.length, expr ]
489
+ subfilters = expr.collect {|val| Treequel::Filter.new(key, val) }
490
+ Treequel::Filter.new( :or, subfilters )
491
+ else
492
+ Treequel.logger.debug " value is a scalar; creating a single filter"
493
+ Treequel::Filter.new( key, expr )
494
+ end
495
+ end
496
+
497
+ if filterlist.length > 1
498
+ return Treequel::Filter::AndComponent.new( *filterlist )
499
+ else
500
+ return filterlist.first
501
+ end
502
+ end
503
+
504
+
505
+ ### Parse a tuple of the form: [ Symbol, Object ] into a Treequel::Filter::Component
506
+ ### and return it.
507
+ def self::parse_tuple_array_expression( expression )
508
+ Treequel.logger.debug "Parsing tuple Array expression %p" % [ expression ]
509
+
510
+ case expression[1]
511
+
512
+ # [ :and/:or/:not, [:uid, 1] ] := (&/|/!(uid=1))
513
+ # [ :and/:or/:not, {:uid => 1} ] := (&/|/!(uid=1))
514
+ when Array, Hash
515
+ return self.parse_logical_array_expression( *expression )
516
+
517
+ when Range
518
+ Treequel.logger.debug " two ANDed item expressions from a Range"
519
+ attribute = expression[0]
520
+ range = expression[1]
521
+ left = "#{attribute}>=#{range.begin}"
522
+ right = "#{attribute}<=#{range.exclude_end? ? range.max : range.end}"
523
+ return self.parse_logical_array_expression( :and, [left, right] )
524
+
525
+ # [ :attribute, 'value' ] := '(attribute=value)'
526
+ # when String, Symbol, Numeric, Time
527
+ else
528
+ Treequel.logger.debug " item expression from a %p" % [ expression[1].class ]
529
+ return self.parse_item_component( *expression )
530
+ end
531
+ end
532
+
533
+
534
+ ### Break down the given +expression+ as a logical (AND, OR, or NOT)
535
+ ### filter component and return it.
536
+ def self::parse_logical_array_expression( op, *components )
537
+ Treequel.logger.debug "Parsing logical %p expression with components: %p" %
538
+ [ op, components ]
539
+
540
+ compclass = LOGICAL_COMPONENTS[ op ] or
541
+ raise "don't know what a %p condition is. I only know about: %p" %
542
+ [ op, LOGICAL_COMPONENTS.keys ]
543
+
544
+ filterlist = components.collect do |filterexp|
545
+ Treequel.logger.debug " making %p into a component" % [ filterexp ]
546
+ Treequel::Filter.new( filterexp )
547
+ end.flatten
548
+
549
+ return compclass.new( *filterlist )
550
+ end
551
+
552
+
553
+ ### Parse an item component from the specified +attribute+ and +value+
554
+ def self::parse_item_component( attribute, value )
555
+ Treequel.logger.debug " tuple expression (%p=%p)-> item component" %
556
+ [ attribute, value ]
557
+
558
+ case
559
+ when attribute.to_s.index( ':' )
560
+ raise NotImplementedError, "extensible filters are not yet supported"
561
+ when value == '*'
562
+ return Treequel::Filter::PresentItemComponent.new( attribute )
563
+ when value =~ LDAP_SUBSTRING_FILTER_VALUE
564
+ return Treequel::Filter::SubstringItemComponent.new( attribute, value )
565
+ else
566
+ return Treequel::Filter::SimpleItemComponent.new( attribute, value )
567
+ end
568
+ end
569
+
570
+
571
+ ### Parse a Sequel::SQL::Expression as a Treequel::Filter::Component and return it.
572
+ def self::parse_sequel_expression( expression )
573
+ Treequel.logger.debug " parsing Sequel expression: %p" % [ expression ]
574
+
575
+ if expression.respond_to?( :op )
576
+ op = expression.op.to_s.downcase.to_sym
577
+
578
+ if equivalent = SEQUEL_FILTERTYPE_EQUIVALENTS[ op ]
579
+ attribute, value = *expression.args
580
+
581
+ # Turn :sn.like( 'bob' ) into (cn~=bob) 'cause it has no asterisks
582
+ if op == :like && value !~ /\*/
583
+ Treequel.logger.debug \
584
+ " turning a LIKE expression with no wildcards into an 'approx' filter"
585
+ equivalent = :approx
586
+ end
587
+
588
+ return Treequel::Filter::SimpleItemComponent.new( attribute, value, equivalent )
589
+
590
+ elsif op == :'!='
591
+ contents = Treequel::Filter.new( expression.args )
592
+ return Treequel::Filter::NotComponent.new( contents )
593
+
594
+ elsif op == :'not like'
595
+ equivalent = nil
596
+ attribute, value = *expression.args
597
+ Treequel.logger.debug " making a NOT LIKE expression out of: %p" % [ expression ]
598
+
599
+ if value !~ /\*/
600
+ Treequel.logger.debug \
601
+ " turning a NOT LIKE expression with no wildcards into an 'approx' filter"
602
+ equivalent = :approx
603
+ else
604
+ equivalent = :equal
605
+ end
606
+
607
+ comp = Treequel::Filter::SimpleItemComponent.new( attribute, value, equivalent )
608
+ filter = Treequel::Filter.new( comp )
609
+ return Treequel::Filter::NotComponent.new( filter )
610
+
611
+ elsif LOGICAL_COMPONENTS.key?( op )
612
+ components = expression.args.collect do |comp|
613
+ Treequel::Filter.new( comp )
614
+ end
615
+
616
+ return self.parse_logical_array_expression( op, components )
617
+
618
+ elsif msg = UNSUPPORTED_SEQUEL_FILTERTYPES[ op ]
619
+ raise Treequel::ExpressionError,
620
+ "unsupported Sequel filter syntax %p: %s" %
621
+ [ expression, msg ]
622
+ else
623
+ raise ScriptError,
624
+ " unhandled Sequel BooleanExpression: add handling for %p: %p" % [ op, expression ]
625
+ end
626
+
627
+ else
628
+ raise Treequel::ExpressionError,
629
+ "don't know how to turn %p into a component" % [ expression ]
630
+ end
631
+ end
632
+
633
+
634
+ #################################################################
635
+ ### I N S T A N C E M E T H O D S
636
+ #################################################################
637
+
638
+ ### Create a new Treequel::Branchset::Filter with the specified +expression+.
639
+ def initialize( *expression_parts )
640
+ @component = self.class.parse_expression( expression_parts )
641
+ self.log.debug "created a filter with component: %p" % [ @component ]
642
+
643
+ super()
644
+ end
645
+
646
+
647
+ ######
648
+ public
649
+ ######
650
+
651
+ # The filtercomp part of the filter
652
+ attr_accessor :component
653
+
654
+
655
+ ### Return the Treequel::Branchset::Filter as a String.
656
+ def to_s
657
+ # self.log.debug "stringifying filter %p" % [ self ]
658
+ filtercomp = self.component.to_s
659
+ if filtercomp[0] == ?(
660
+ return filtercomp
661
+ else
662
+ return '(' + filtercomp + ')'
663
+ end
664
+ end
665
+
666
+
667
+ ### Return a human-readable string representation of the filter suitable
668
+ ### for debugging.
669
+ def inspect
670
+ return %{#<%s:0x%0x (%s)>} % [
671
+ self.class.name,
672
+ self.object_id * 2,
673
+ self.component,
674
+ ]
675
+ end
676
+
677
+
678
+ ### Returns +true+ if the filter contains a single 'present' component for
679
+ ### the objectClass attribute (which will match every entry)
680
+ def promiscuous?
681
+ return self.component.promiscuous?
682
+ end
683
+ alias_method :is_promiscuous?, :promiscuous?
684
+
685
+
686
+ ### Equality operator -- returns +true+ if +other_filter+ is equivalent
687
+ ### to the receiver.
688
+ def ==( other_filter )
689
+ return ( self.component == other_filter.component )
690
+ end
691
+
692
+
693
+ ### AND two filters together
694
+ def &( other_filter )
695
+ return other_filter if self.promiscuous?
696
+ return self.dup if other_filter.promiscuous?
697
+ return self.class.new( :and, [self, other_filter] )
698
+ end
699
+ alias_method :+, :&
700
+
701
+
702
+
703
+ end # class Treequel::Filter
704
+