treequel 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+