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.
- data/ChangeLog +354 -0
- data/LICENSE +27 -0
- data/README +66 -0
- data/Rakefile +345 -0
- data/Rakefile.local +43 -0
- data/bin/treeirb +14 -0
- data/bin/treequel +229 -0
- data/examples/company-directory.rb +112 -0
- data/examples/ldap-monitor.rb +143 -0
- data/examples/ldap-monitor/public/css/master.css +328 -0
- data/examples/ldap-monitor/public/images/card_small.png +0 -0
- data/examples/ldap-monitor/public/images/chain_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
- data/examples/ldap-monitor/public/images/plug.png +0 -0
- data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
- data/examples/ldap-monitor/public/images/tick.png +0 -0
- data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
- data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
- data/examples/ldap-monitor/views/backends.erb +41 -0
- data/examples/ldap-monitor/views/connections.erb +74 -0
- data/examples/ldap-monitor/views/databases.erb +39 -0
- data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
- data/examples/ldap-monitor/views/index.erb +14 -0
- data/examples/ldap-monitor/views/layout.erb +35 -0
- data/examples/ldap-monitor/views/listeners.erb +30 -0
- data/examples/ldap_state.rb +62 -0
- data/lib/treequel.rb +145 -0
- data/lib/treequel/branch.rb +589 -0
- data/lib/treequel/branchcollection.rb +204 -0
- data/lib/treequel/branchset.rb +360 -0
- data/lib/treequel/constants.rb +604 -0
- data/lib/treequel/directory.rb +541 -0
- data/lib/treequel/exceptions.rb +32 -0
- data/lib/treequel/filter.rb +704 -0
- data/lib/treequel/mixins.rb +325 -0
- data/lib/treequel/schema.rb +245 -0
- data/lib/treequel/schema/attributetype.rb +252 -0
- data/lib/treequel/schema/ldapsyntax.rb +96 -0
- data/lib/treequel/schema/matchingrule.rb +124 -0
- data/lib/treequel/schema/matchingruleuse.rb +124 -0
- data/lib/treequel/schema/objectclass.rb +289 -0
- data/lib/treequel/sequel_integration.rb +26 -0
- data/lib/treequel/utils.rb +169 -0
- data/rake/191_compat.rb +26 -0
- data/rake/dependencies.rb +76 -0
- data/rake/helpers.rb +434 -0
- data/rake/hg.rb +261 -0
- data/rake/manual.rb +782 -0
- data/rake/packaging.rb +135 -0
- data/rake/publishing.rb +318 -0
- data/rake/rdoc.rb +30 -0
- data/rake/style.rb +62 -0
- data/rake/svn.rb +668 -0
- data/rake/testing.rb +187 -0
- data/rake/verifytask.rb +64 -0
- data/rake/win32.rb +190 -0
- data/spec/lib/constants.rb +93 -0
- data/spec/lib/helpers.rb +100 -0
- data/spec/treequel/branch_spec.rb +569 -0
- data/spec/treequel/branchcollection_spec.rb +213 -0
- data/spec/treequel/branchset_spec.rb +376 -0
- data/spec/treequel/directory_spec.rb +487 -0
- data/spec/treequel/filter_spec.rb +482 -0
- data/spec/treequel/mixins_spec.rb +330 -0
- data/spec/treequel/schema/attributetype_spec.rb +237 -0
- data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
- data/spec/treequel/schema/matchingrule_spec.rb +158 -0
- data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
- data/spec/treequel/schema/objectclass_spec.rb +262 -0
- data/spec/treequel/schema_spec.rb +118 -0
- data/spec/treequel/utils_spec.rb +49 -0
- data/spec/treequel_spec.rb +179 -0
- 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
|
+
|