treequel 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/ChangeLog +130 -1
  2. data/Rakefile +8 -3
  3. data/Rakefile.local +2 -0
  4. data/bin/treeirb +6 -2
  5. data/bin/treequel +5 -4
  6. data/lib/treequel/branch.rb +133 -72
  7. data/lib/treequel/branchcollection.rb +16 -5
  8. data/lib/treequel/branchset.rb +37 -6
  9. data/lib/treequel/constants.rb +12 -0
  10. data/lib/treequel/directory.rb +96 -41
  11. data/lib/treequel/filter.rb +42 -1
  12. data/lib/treequel/model/objectclass.rb +111 -0
  13. data/lib/treequel/model.rb +363 -0
  14. data/lib/treequel/monkeypatches.rb +65 -0
  15. data/lib/treequel/schema/attributetype.rb +108 -18
  16. data/lib/treequel/schema/ldapsyntax.rb +15 -0
  17. data/lib/treequel/schema/matchingrule.rb +24 -0
  18. data/lib/treequel/schema/matchingruleuse.rb +24 -0
  19. data/lib/treequel/schema/objectclass.rb +70 -5
  20. data/lib/treequel/schema/table.rb +5 -15
  21. data/lib/treequel/schema.rb +64 -1
  22. data/lib/treequel.rb +5 -5
  23. data/rake/documentation.rb +27 -0
  24. data/rake/hg.rb +14 -2
  25. data/rake/manual.rb +1 -1
  26. data/spec/lib/constants.rb +9 -7
  27. data/spec/lib/control_behavior.rb +1 -0
  28. data/spec/lib/matchers.rb +1 -0
  29. data/spec/treequel/branch_spec.rb +229 -20
  30. data/spec/treequel/branchcollection_spec.rb +73 -39
  31. data/spec/treequel/branchset_spec.rb +59 -8
  32. data/spec/treequel/control_spec.rb +1 -0
  33. data/spec/treequel/controls/contentsync_spec.rb +1 -0
  34. data/spec/treequel/controls/pagedresults_spec.rb +1 -0
  35. data/spec/treequel/controls/sortedresults_spec.rb +1 -0
  36. data/spec/treequel/directory_spec.rb +46 -10
  37. data/spec/treequel/filter_spec.rb +28 -5
  38. data/spec/treequel/mixins_spec.rb +7 -14
  39. data/spec/treequel/model/objectclass_spec.rb +330 -0
  40. data/spec/treequel/model_spec.rb +433 -0
  41. data/spec/treequel/monkeypatches_spec.rb +118 -0
  42. data/spec/treequel/schema/attributetype_spec.rb +116 -0
  43. data/spec/treequel/schema/ldapsyntax_spec.rb +8 -0
  44. data/spec/treequel/schema/matchingrule_spec.rb +6 -1
  45. data/spec/treequel/schema/matchingruleuse_spec.rb +5 -0
  46. data/spec/treequel/schema/objectclass_spec.rb +41 -3
  47. data/spec/treequel/schema/table_spec.rb +31 -18
  48. data/spec/treequel/schema_spec.rb +13 -16
  49. data/spec/treequel_spec.rb +22 -0
  50. data.tar.gz.sig +1 -0
  51. metadata +40 -7
  52. metadata.gz.sig +0 -0
  53. data/spec/treequel/utils_spec.rb +0 -49
data/ChangeLog CHANGED
@@ -1,4 +1,133 @@
1
- 261[tip] 0188c2ba5b7e 2010-07-07 23:21 -0700 ged
1
+ 304[tip] 92c28b14730a 2010-09-20 13:51 -0700 ged
2
+ Added tag 1.1.0 for changeset b415e0fce774
3
+
4
+ 303[1.1.0] b415e0fce774 2010-09-20 13:51 -0700 ged
5
+ Added signature for changeset 4ba782a3a7e4
6
+
7
+ 302 4ba782a3a7e4 2010-09-17 09:09 -0700 ged
8
+ Include mixins for inherited objectClasses, too.
9
+
10
+ 301 f075a27a35eb 2010-09-17 09:03 -0700 ged
11
+ Updated build system
12
+
13
+ 300 c77bac0bf816 2010-09-17 09:02 -0700 ged
14
+ Manual updates for the Model section (paired with Mahlon)
15
+
16
+ 299 1b375c5dc123 2010-09-15 16:46 -0700 ged
17
+ Use the #syntax method instead of the syntax_oid in #inspect output so inherited syntaxes are shown
18
+
19
+ 298 0786411cd707 2010-09-15 16:43 -0700 ged
20
+ Use the system LDAP config if no URI is given
21
+
22
+ 297 d3a6c89d8004 2010-09-15 16:42 -0700 ged
23
+ Un-spam debugging on entry lookup
24
+
25
+ 296 2101482df41f 2010-09-15 16:41 -0700 ged
26
+ Overriding #inspect in Treequel::Model to show the list of extensions applied to the entry
27
+
28
+ 295 a384412c0e6f 2010-08-25 16:56 -0700 ged
29
+ Rescue the right error in Treequel::Model#method_missing and add a spec for the negative case.
30
+
31
+ 294 b220a83d31b5 2010-08-25 16:48 -0700 ged
32
+ Load the entry from Model's #method_missing to catch calls to methods that are added via
33
+
34
+ 293 2753f8405caf 2010-08-23 17:12 -0700 ged
35
+ Fixing object/attribute mapping for attributes whose types that inherit their syntax from their superclass
36
+
37
+ 292 2e2b035d3721 2010-08-23 15:19 -0700 ged
38
+ Coverage improvements, manual work
39
+
40
+ 291 693a163bd068 2010-08-23 08:36 -0700 ged
41
+ Started catching the manual up to 1.1.0.
42
+
43
+ 290 df0b0594cb62 2010-08-23 08:36 -0700 ged
44
+ Made Treequel::Branch#delete with no arguments delete the entry
45
+
46
+ 289 31fc0ea1ea31 2010-08-19 16:35 -0700 ged
47
+ Fixes for Ruby 1.9.2.
48
+
49
+ 288 7c7d3e2034b0 2010-08-19 07:21 -0700 ged
50
+ Build system update; fixed ruby-termios dependency
51
+
52
+ 287 f9151315d37f 2010-08-18 17:35 -0700 ged
53
+ Implement Branchset operators
54
+
55
+ 286 cd3ef7e3ffb0 2010-08-16 17:20 -0700 ged
56
+ Reworked operational attributes to use the 'USAGE' attribute at Mahlon's suggestion.
57
+
58
+ 285 dfb032f5e5c1 2010-08-16 17:19 -0700 ged
59
+ Fixes for Treequel::Model instantiation and lookup.
60
+
61
+ 284 844ee21c6916 2010-08-16 17:05 -0700 ged
62
+ Made Treequel::Schema::Table Enumerable
63
+
64
+ 283 ff2629196b4e 2010-08-06 16:11 -0700 ged
65
+ Don't add the objectClass attribute when searching through Treequel::Model if
66
+
67
+ 282 d4e58760ee34 2010-08-06 15:02 -0700 ged
68
+ Handle Sequel's Ruby1.9 Symbol-operator workaround in filter attributes
69
+
70
+ 281 d60c6b83db01 2010-08-06 15:02 -0700 ged
71
+ Also output empty-string attributes as non-binary
72
+
73
+ 280 bb182f705fe5 2010-08-05 14:07 -0700 ged
74
+ Unwrap base64ed LDIF lines before wrapping them to the new line length.
75
+
76
+ 279 32084630894f 2010-08-05 11:14 -0700 ged
77
+ Fix LDIF-generation for really reals.
78
+
79
+ 278 aabbab99b093 2010-08-05 10:40 -0700 ged
80
+ Optimizations, logging cleanup.
81
+
82
+ 277 7a4f2e5bef0a 2010-08-02 08:45 -0700 ged
83
+ Bugfixes.
84
+
85
+ 276 2f741e5294bf 2010-07-29 14:34 -0700 ged
86
+ Filter component symmetry, Model refactor
87
+
88
+ 275 bc0bc3aea136 2010-07-28 09:06 -0700 ged
89
+ Added support for Sequel-style #or: branchset.filter( :something ).or( :somethingelse ).
90
+
91
+ 274 30091794b910 2010-07-28 07:14 -0700 ged
92
+ Added Treequel::Model#respond_to?
93
+
94
+ 273 e9c908b1f426 2010-07-27 08:05 -0700 ged
95
+ Bugfix, make object conversion work for setting attributes too.
96
+
97
+ 272 30ba889041a8 2010-07-26 20:04 -0700 ged
98
+ Bugfixes, coverage of LDIF-generation.
99
+
100
+ 271 a6dd7b685a6a 2010-07-16 18:32 -0700 ged
101
+ Fixing the "cd .." special case
102
+
103
+ 270 2793fa802dad 2010-07-14 20:16 -0700 ged
104
+ Untaint objectClasses passed to Treequel::Model::mixins_for_objectclasses.
105
+
106
+ 269 b76dd9ca1f3f 2010-07-13 19:14 -0700 ged
107
+ The workaround for two-param syntax mapping Procs didn't work after all. Modifying
108
+
109
+ 268 70cc87a200ad 2010-07-13 19:08 -0700 ged
110
+ Added schema-object roundtripping, with a script/specs to test it.
111
+
112
+ 267 47e865bfef9e 2010-07-13 12:31 -0700 ged
113
+ Added the ability to set the default results class on a per-directory basis.
114
+
115
+ 266 c61373e3dc49 2010-07-13 08:03 -0700 ged
116
+ Fixes for Apache DS and bugfix in Treequel::Model.
117
+
118
+ 265 601003ff3077 2010-07-10 17:16 -0700 ged
119
+ Treequel::Model bugfix
120
+
121
+ 264 a76065cfeba0 2010-07-09 07:19 -0700 ged
122
+ Adding Treequel::Model, bumping version to 1.1.0.
123
+
124
+ 263:262,261 6dd6cb37f5ce 2010-07-09 07:16 -0700 ged
125
+ Merged with 47bb9cd7af3b
126
+
127
+ 262:258 3f4134d20d9d 2010-07-08 09:40 -0700 ged
128
+ Delegate the Treequel::Directory#root_dse method through its #conn
129
+
130
+ 261 0188c2ba5b7e 2010-07-07 23:21 -0700 ged
2
131
  Added tag 1.0.4 for changeset 0ec4ff0ce67f
3
132
 
4
133
  260[1.0.4] 0ec4ff0ce67f 2010-07-07 23:21 -0700 ged
data/Rakefile CHANGED
@@ -18,6 +18,7 @@ BEGIN {
18
18
  libdir = basedir + "lib"
19
19
  extdir = libdir + Config::CONFIG['sitearch']
20
20
 
21
+ $LOAD_PATH.unshift( basedir.to_s ) unless $LOAD_PATH.include?( basedir.to_s )
21
22
  $LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
22
23
  $LOAD_PATH.unshift( extdir.to_s ) unless $LOAD_PATH.include?( extdir.to_s )
23
24
  }
@@ -76,7 +77,7 @@ elsif VERSION_FILE.exist?
76
77
  PKG_VERSION = VERSION_FILE.read[ /VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, 1 ]
77
78
  end
78
79
 
79
- PKG_VERSION = '0.0.0' unless defined?( PKG_VERSION )
80
+ PKG_VERSION = '0.0.0' unless defined?( PKG_VERSION ) && !PKG_VERSION.nil?
80
81
 
81
82
  PKG_FILE_NAME = "#{PKG_NAME.downcase}-#{PKG_VERSION}"
82
83
  GEM_FILE_NAME = "#{PKG_FILE_NAME}.gem"
@@ -169,7 +170,7 @@ include RakefileHelpers
169
170
 
170
171
  # Set the build ID if the mercurial executable is available
171
172
  if hg = which( 'hg' )
172
- id = IO.read('|-') or exec hg.to_s, 'id', '-n'
173
+ id = `#{hg} id -n`.chomp
173
174
  PKG_BUILD = "pre%03d" % [(id.chomp[ /^[[:xdigit:]]+/ ] || '1')]
174
175
  else
175
176
  PKG_BUILD = 'pre000'
@@ -226,9 +227,9 @@ DEVELOPMENT_DEPENDENCIES = {
226
227
  'text-format' => '>= 1.0.0',
227
228
  'tmail' => '>= 1.2.3.1',
228
229
  'diff-lcs' => '>= 1.1.2',
229
- 'termios' => '>= 0.9.4',
230
230
  'columnize' => '>= 0.3.1',
231
231
  'diff-lcs' => '>= 1.1.2',
232
+ 'ruby-termios' => '>= 0.9.6',
232
233
  'ruby-terminfo' => '>= 0.1.1',
233
234
  }
234
235
 
@@ -277,6 +278,10 @@ GEMSPEC = Gem::Specification.new do |gem|
277
278
  gem.files = RELEASE_FILES
278
279
  gem.test_files = SPEC_FILES
279
280
 
281
+ # signing key and certificate chain
282
+ gem.signing_key = '/Volumes/Keys/ged-private_gem_key.pem'
283
+ gem.cert_chain = [File.expand_path('~/.gem/ged-public_gem_cert.pem')]
284
+
280
285
  DEPENDENCIES.each do |name, version|
281
286
  version = '>= 0' if version.length.zero?
282
287
  gem.add_runtime_dependency( name, version )
data/Rakefile.local CHANGED
@@ -13,6 +13,8 @@ require 'pp'
13
13
 
14
14
  MANUAL_PAGES = Pathname.glob( MANUALDIR + '**/*.page' )
15
15
 
16
+ RCOV_EXCLUDES.replace( RCOV_EXCLUDES + ',sequel_integration\.rb' )
17
+
16
18
  task :local => :check_manual
17
19
  task :manual => :check_manual
18
20
 
data/bin/treeirb CHANGED
@@ -6,8 +6,12 @@ require 'irb/cmd/nop'
6
6
  require 'treequel'
7
7
 
8
8
 
9
- uri = ARGV.shift or raise "usage: #$0 [LDAPURI]"
10
- $dir = Treequel.directory( uri )
9
+ if uri = ARGV.shift
10
+ $dir = Treequel.directory( uri )
11
+ else
12
+ $dir = Treequel.directory_from_config
13
+ end
14
+
11
15
  $stderr.puts "Directory is in $dir:", ' ' + $dir.inspect
12
16
 
13
17
  IRB.start( $0 )
data/bin/treequel CHANGED
@@ -571,7 +571,7 @@ class Treequel::Shell
571
571
  # Calcuate column widths
572
572
  oclen = children.map do |subbranch|
573
573
  subbranch.include_operational_attrs = true
574
- subbranch[:structuralObjectClass].length
574
+ subbranch[:structuralObjectClass] ? subbranch[:structuralObjectClass].length : 0
575
575
  end.max
576
576
 
577
577
  # Set up sorting by collecting all the requested sort criteria as Proc objects which
@@ -610,7 +610,7 @@ class Treequel::Shell
610
610
  return
611
611
  end
612
612
 
613
- return self.parent_command if rdn == '..'
613
+ return self.parent_command( options ) if rdn == '..'
614
614
 
615
615
  raise "invalid RDN %p" % [ rdn ] unless RELATIVE_DISTINGUISHED_NAME.match( rdn )
616
616
 
@@ -868,9 +868,10 @@ class Treequel::Shell
868
868
  metadatalen = oclen + 16 + 6 # oc + timestamp + whitespace
869
869
  maxdesclen = self.columns - metadatalen - rdn.length - 5
870
870
 
871
+ modtime = branch[:modifyTimestamp] || branch[:createTimestamp]
871
872
  return "%#{oclen}s %s %s%s %s" % [
872
- branch[:structuralObjectClass],
873
- branch[:modifyTimestamp].strftime('%Y-%m-%d %H:%M'),
873
+ branch[:structuralObjectClass] || '',
874
+ modtime.strftime('%Y-%m-%d %H:%M'),
874
875
  rdn,
875
876
  branch[:hasSubordinates] ? '/' : '',
876
877
  single_line_description( branch, maxdesclen )
@@ -69,7 +69,8 @@ class Treequel::Branch
69
69
  ### @param [String] dn The DN of the entry the Branch is wrapping.
70
70
  ### @param [LDAP::Entry, Hash] entry The entry object if it's already been fetched.
71
71
  def initialize( directory, dn, entry=nil )
72
- raise ArgumentError, "invalid DN" unless dn.match( Patterns::DISTINGUISHED_NAME )
72
+ raise ArgumentError, "invalid DN" unless
73
+ dn.match( Patterns::DISTINGUISHED_NAME ) || dn.empty?
73
74
  raise ArgumentError, "can't cast a %s to an LDAP::Entry" % [entry.class.name] unless
74
75
  entry.nil? || entry.is_a?( Hash )
75
76
 
@@ -104,7 +105,7 @@ class Treequel::Branch
104
105
 
105
106
  # Whether or not to include operational attributes when fetching the Branch's entry
106
107
  predicate_attr :include_operational_attrs
107
-
108
+ alias_method :include_operational_attributes?, :include_operational_attrs?
108
109
 
109
110
  ### Change the DN the Branch uses to look up its entry.
110
111
  ###
@@ -124,6 +125,7 @@ class Treequel::Branch
124
125
  self.clear_caches
125
126
  @include_operational_attrs = new_setting ? true : false
126
127
  end
128
+ alias_method :include_operational_attributes=, :include_operational_attrs=
127
129
 
128
130
 
129
131
  ### Return the attribute/s which make up this Branch's RDN.
@@ -181,6 +183,7 @@ class Treequel::Branch
181
183
  ### @return [String]
182
184
  def parent_dn
183
185
  return nil if self.dn == self.directory.base_dn
186
+ return '' if self.dn.index( ',' ).nil?
184
187
  return self.split_dn( 2 ).last
185
188
  end
186
189
 
@@ -260,35 +263,21 @@ class Treequel::Branch
260
263
  end
261
264
 
262
265
 
263
- ### Make LDIF for the given +attribute+ and its +values+, wrapping at the given
264
- ### +width+.
265
- ###
266
- ### @param [String] attribute the attribute
267
- ### @param [Array<String>] values the values for the given +attribute+
268
- ### @param [Fixnum] width the maximum width of the lines to return
269
- def ldif_for_attr( attribute, values, width )
270
- ldif = ''
271
-
272
- Array( values ).each do |val|
273
- line = "#{attribute}:"
266
+ ### Return the Branch as a Hash.
267
+ ### @see Treequel::Branch#[]
268
+ ### @return [Hash] the entry as a Hash with converted values
269
+ def to_hash
270
+ entry = self.entry || self.valid_attributes_hash
271
+ self.log.debug " making a Hash from an entry: %p" % [ entry ]
274
272
 
275
- if val =~ /^#{LDIF_SAFE_STRING}$/
276
- line << ' ' << val.to_s
273
+ return entry.keys.inject({}) do |hash, attribute|
274
+ if attribute == 'dn'
275
+ hash[ attribute ] = self.dn
277
276
  else
278
- line << ': ' << [ val ].pack( 'm' ).chomp
277
+ hash[ attribute ] = self[ attribute ]
279
278
  end
280
-
281
- # calculate how many times the line needs to be split, then add any
282
- # additional splits that need to be added because of the additional
283
- # fold characters
284
- splits = ( line.length / width )
285
- splits += ( splits * LDIF_FOLD_SEPARATOR.length ) / width
286
- splits.times {|i| line[ width * (i+1), 0 ] = LDIF_FOLD_SEPARATOR }
287
-
288
- ldif << line << "\n"
279
+ hash
289
280
  end
290
-
291
- return ldif
292
281
  end
293
282
 
294
283
 
@@ -298,32 +287,9 @@ class Treequel::Branch
298
287
  attrsym = attrname.to_sym
299
288
 
300
289
  unless @values.key?( attrsym )
301
- directory = self.directory
302
- entry = self.entry or return nil
303
- return nil unless (( value = entry[attrsym.to_s] ))
304
-
305
- self.log.debug " value is not cached; checking its attributeType"
306
- if attribute = directory.schema.attribute_types[ attrsym ]
307
- self.log.debug " attribute exists; checking the entry for a value"
308
-
309
- syntax_oid = attribute.syntax_oid
310
-
311
- if attribute.single?
312
- self.log.debug " attributeType is SINGLE; unwrapping the Array"
313
- @values[ attrsym ] = directory.convert_syntax_value( syntax_oid, value.first )
314
- else
315
- self.log.debug " attributeType is not SINGLE; keeping the Array"
316
- @values[ attrsym ] = value.collect do |raw|
317
- directory.convert_syntax_value( syntax_oid, raw )
318
- end
319
- @values[ attrsym ].freeze if @values[ attrsym ].is_a?( Array )
320
- end
321
-
322
- else
323
- self.log.info "no attributeType for %p" % [ attrsym ]
324
- @values[ attrsym ] = value
325
- @values[ attrsym ].freeze
326
- end
290
+ value = self.get_converted_object( attrsym )
291
+ value.freeze if value.respond_to?( :freeze )
292
+ @values[ attrsym ] = value
327
293
  else
328
294
  self.log.debug " value is cached."
329
295
  end
@@ -349,6 +315,7 @@ class Treequel::Branch
349
315
  ### @param [Object] value the attribute value
350
316
  def []=( attrname, value )
351
317
  value = [ value ] unless value.is_a?( Array )
318
+ value.collect! {|val| self.get_converted_attribute(attrname, val) }
352
319
  self.log.debug "Modifying %s to %p" % [ attrname, value ]
353
320
  self.directory.modify( self, attrname.to_s => value )
354
321
  @values.delete( attrname.to_sym )
@@ -384,19 +351,25 @@ class Treequel::Branch
384
351
  ###
385
352
  ### @return [TrueClass] if the delete succeeded
386
353
  def delete( *attributes )
387
- self.log.debug "Deleting attributes: %p" % [ attributes ]
388
- mods = attributes.flatten.collect do |attribute|
389
- if attribute.is_a?( Hash )
390
- attribute.collect do |key,vals|
391
- vals = Array( vals ).collect {|val| val.to_s }
392
- LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, key.to_s, vals )
354
+ if attributes.empty?
355
+ self.log.info "No attributes specified; deleting entire entry for %s" % [ self.dn ]
356
+ self.directory.delete( self )
357
+ else
358
+ self.log.debug "Deleting attributes: %p" % [ attributes ]
359
+ mods = attributes.flatten.collect do |attribute|
360
+ if attribute.is_a?( Hash )
361
+ attribute.collect do |key,vals|
362
+ vals = Array( vals ).collect {|val| val.to_s }
363
+ LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, key.to_s, vals )
364
+ end
365
+ else
366
+ LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute.to_s, [] )
393
367
  end
394
- else
395
- LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute.to_s, [] )
396
- end
397
- end.flatten
368
+ end.flatten
369
+
370
+ self.directory.modify( self, mods )
371
+ end
398
372
 
399
- self.directory.modify( self, mods )
400
373
  self.clear_caches
401
374
 
402
375
  return true
@@ -478,7 +451,8 @@ class Treequel::Branch
478
451
  ### @param [String] rdn The RDN of the child to fetch.
479
452
  ### @return [Treequel::Branch]
480
453
  def get_child( rdn )
481
- newdn = [ rdn, self.dn ].join( ',' )
454
+ self.log.debug "Getting child %p from base = %p" % [ rdn, self.dn ]
455
+ newdn = [ rdn, self.dn ].reject {|part| part.empty? }.join( ',' )
482
456
  return self.class.new( self.directory, newdn )
483
457
  end
484
458
 
@@ -517,6 +491,21 @@ class Treequel::Branch
517
491
  end
518
492
 
519
493
 
494
+ ### Return the receiver's operational attributes as attributeType schema objects.
495
+ ###
496
+ ### @return [Array<Treequel::Schema::AttributeType>] the operational attributes
497
+ def operational_attribute_types
498
+ return self.directory.schema.operational_attribute_types
499
+ end
500
+
501
+
502
+ ### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the
503
+ ### receiver's operational attributes.
504
+ def operational_attribute_oids
505
+ return self.operational_attribute_types.map( &:oid )
506
+ end
507
+
508
+
520
509
  ### Return Treequel::Schema::AttributeType instances for each of the receiver's
521
510
  ### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
522
511
  ### include the MUST attributeTypes for them as well. This can be used to predict what
@@ -626,22 +615,29 @@ class Treequel::Branch
626
615
  end
627
616
  end
628
617
 
629
- attrhash[ :objectClass ] |= additional_object_classes
618
+ # :FIXME: Does the resulting hash need the additional objectClasses? objectClass is
619
+ # MUST via 'top', so it should already exist in that hash when merged with
620
+ # this one...
621
+ # attrhash[ :objectClass ] |= additional_object_classes
622
+
630
623
  return attrhash
631
624
  end
632
625
 
633
626
 
634
627
  ### Return Treequel::Schema::AttributeType instances for the set of all of the receiver's
635
- ### MUST and MAY attributeTypes.
628
+ ### MUST and MAY attributeTypes plus the operational attributes.
636
629
  ###
637
630
  ### @return [Array<Treequel::Schema::AttributeType>]
638
631
  def valid_attribute_types
639
- return self.must_attribute_types | self.may_attribute_types
632
+ return self.must_attribute_types |
633
+ self.may_attribute_types |
634
+ self.operational_attribute_types
640
635
  end
641
636
 
642
637
 
643
638
  ### Return a uniqified Array of OIDs (numeric OIDs as Strings, named OIDs as Symbols) for
644
- ### the set of all of the receiver's MUST and MAY attributeTypes.
639
+ ### the set of all of the receiver's MUST and MAY attributeTypes plus the operational
640
+ ### attributes.
645
641
  ###
646
642
  ### @return [Array<String, Symbol>]
647
643
  def valid_attribute_oids
@@ -649,8 +645,10 @@ class Treequel::Branch
649
645
  end
650
646
 
651
647
 
652
- ### If the given +attroid+ is a valid attributeType name or numeric OID, return the
648
+ ### If the attribute associated with the given +attroid+ is in the list of valid
649
+ ### attributeTypes for the receiver given its objectClasses, return the
653
650
  ### AttributeType object that corresponds with it. If it isn't valid, return nil.
651
+ ### Includes operational attributes.
654
652
  ###
655
653
  ### @param [String,Symbol] attroid a numeric OID (as a String) or a named OID (as a Symbol)
656
654
  ### @return [Treequel::Schema::AttributeType] the validated attributeType
@@ -660,7 +658,7 @@ class Treequel::Branch
660
658
 
661
659
 
662
660
  ### Return +true+ if the specified +attrname+ is a valid attributeType given the
663
- ### receiver's current objectClasses.
661
+ ### receiver's current objectClasses. Does not include operational attributes.
664
662
  ###
665
663
  ### @param [String, Symbol] the OID (numeric or name) of the attribute in question
666
664
  ### @return [Boolean]
@@ -736,7 +734,6 @@ class Treequel::Branch
736
734
 
737
735
  ### Fetch the entry from the Branch's directory.
738
736
  def lookup_entry
739
- self.log.debug "Looking up entry for %p" % [ self ]
740
737
  if self.include_operational_attrs?
741
738
  self.log.debug " including operational attributes."
742
739
  return self.directory.get_extended_entry( self )
@@ -747,6 +744,42 @@ class Treequel::Branch
747
744
  end
748
745
 
749
746
 
747
+ ### Get the value associated with +attrsym+, convert it to a Ruby object if the Branch's
748
+ ### directory has a conversion rule, and return it.
749
+ def get_converted_object( attrsym )
750
+ return nil unless self.entry
751
+ value = self.entry[ attrsym.to_s ] or return nil
752
+
753
+ if attribute = self.directory.schema.attribute_types[ attrsym ]
754
+ if attribute.single?
755
+ value = self.directory.convert_to_object( attribute.syntax.oid, value.first )
756
+ else
757
+ value = value.collect do |raw|
758
+ self.directory.convert_to_object( attribute.syntax.oid, raw )
759
+ end
760
+ end
761
+ else
762
+ self.log.info "no attributeType for %p" % [ attrsym ]
763
+ end
764
+
765
+ return value
766
+ end
767
+
768
+
769
+ ### Convert the specified +object+ according to the Branch's directory's conversion rules,
770
+ ### and return it.
771
+ def get_converted_attribute( attrsym, object )
772
+ if attribute = self.directory.schema.attribute_types[ attrsym ]
773
+ self.log.debug "converting %p object to a %p attribute" %
774
+ [ attrsym, attribute.syntax.oid ]
775
+ return self.directory.convert_to_attribute( attribute.syntax.oid, object )
776
+ else
777
+ self.log.info "no attributeType for %p" % [ attrsym ]
778
+ return object.to_s
779
+ end
780
+ end
781
+
782
+
750
783
  ### Clear any cached values when the structural state of the object changes.
751
784
  ### @return [void]
752
785
  def clear_caches
@@ -793,6 +826,34 @@ class Treequel::Branch
793
826
  end
794
827
  end
795
828
 
829
+
830
+ ### Make LDIF for the given +attribute+ and its +values+, wrapping at the given
831
+ ### +width+.
832
+ ###
833
+ ### @param [String] attribute the attribute
834
+ ### @param [Array<String>] values the values for the given +attribute+
835
+ ### @param [Fixnum] width the maximum width of the lines to return
836
+ def ldif_for_attr( attribute, value, width )
837
+ unsplit_line = "#{attribute}:"
838
+
839
+ if value.empty? || value =~ /\A#{LDIF_SAFE_STRING}\Z/
840
+ unsplit_line << ' ' << value.to_s
841
+ else
842
+ unsplit_line << ': ' << [ value ].pack( 'm' ).chomp
843
+ end
844
+ unsplit_line.gsub!( /\n/, '' )
845
+
846
+ ldif = ''
847
+ ldif << unsplit_line.slice!( 0, width ) << LDIF_FOLD_SEPARATOR until
848
+ unsplit_line.empty?
849
+
850
+ ldif.rstrip!
851
+ ldif << "\n"
852
+
853
+ return ldif
854
+ end
855
+
856
+
796
857
  end # class Treequel::Branch
797
858
 
798
859
 
@@ -178,19 +178,30 @@ class Treequel::BranchCollection
178
178
  end
179
179
 
180
180
 
181
- ### Return a new Treequel::BranchCollection that includes both the receiver's Branchsets and
182
- ### those in +other_object+ (or +other_object+ itself if it's a Branchset).
181
+ ### Return either a new Treequel::BranchCollection that includes both the receiver's
182
+ ### Branchsets and those in +other_object+ (if it responds_to #branchsets), or the results
183
+ ### from executing the BranchCollection's search with +other_object+ appended if it doesn't.
184
+ ### @param [Object] other_object
183
185
  def +( other_object )
184
186
  if other_object.respond_to?( :branchsets )
185
187
  return self.class.new( self.branchsets + other_object.branchsets )
186
- elsif other_object.respond_to?( :branchset )
187
- return self.class.new( self.branchsets + [other_object.branchset] )
188
- else
188
+ elsif other_object.respond_to?( :collection )
189
189
  return self.class.new( self.branchsets + [other_object] )
190
+ else
191
+ return self.all + Array( other_object )
190
192
  end
191
193
  end
192
194
 
193
195
 
196
+ ### Return the results from each of the receiver's Branchsets without the +other_object+.
197
+ ### @param [#dn] other_object the object to omit from the results. Must respond_to #dn.
198
+ ### @return [Array<Treequel::Branch>]
199
+ def -( other_object )
200
+ other_dn = other_object.dn
201
+ return self.reject {|branch| branch.dn == other_dn }
202
+ end
203
+
204
+
194
205
  ### Return a new Treequel::BranchCollection that contains the union of the branchsets from both
195
206
  ### collections.
196
207
  def &( other_collection )
@@ -176,12 +176,26 @@ class Treequel::Branchset
176
176
  end
177
177
 
178
178
 
179
- ### Create a BranchCollection from the receiver and the +other_branchset+ and return
180
- ### it.
181
- ### @param [Treequel::Branchset] other_branchset the branchset to combine with
182
- ### @return [Treequel::BranchCollection] the resulting collection object
183
- def +( other_branchset )
184
- return Treequel::BranchCollection.new( self, other_branchset )
179
+ ### If given another Branchset or a BranchCollection, return a BranchCollection that includes
180
+ ### them both. If given anything else, execute the search and return
181
+ ### @param [Treequel::Branchset, Treequel::Branch] other the branchset or branch to combine
182
+ ### with
183
+ ### @return [Treequel::BranchCollection, Array<Treequel::Branch>]
184
+ def +( other )
185
+ if other.is_a?( Treequel::BranchCollection ) || other.is_a?( Treequel::Branchset )
186
+ return Treequel::BranchCollection.new( self, other )
187
+ else
188
+ return self.all + Array( other )
189
+ end
190
+ end
191
+
192
+
193
+ ### Return the results of executing the search without the +other_object+.
194
+ ### @param [#dn] other_object the object to omit from the results; must respond_to #dn.
195
+ ### @return [Array<Treequel::Branch>]
196
+ def -( other_object )
197
+ other_dn = other_object.dn
198
+ return self.reject {|branch| branch.dn == other_dn }
185
199
  end
186
200
 
187
201
 
@@ -276,6 +290,23 @@ class Treequel::Branchset
276
290
  end
277
291
 
278
292
 
293
+ ### Add an alternate filter to an existing filter by ORing them.
294
+ ### @param filterspec the filter spec to OR with the existing filter
295
+ ### @raises [Treequel::InvalidOperation] if there is no existing filter
296
+ ### @see Treequel::Filter.new for specifics on what +filterspec+ can be
297
+ def or( *filterspec )
298
+ opts = self.options
299
+ existing_filter = self.filter
300
+ raise Treequel::ExpressionError, "no existing filter" if
301
+ existing_filter.promiscuous?
302
+
303
+ newfilter = Treequel::Filter.new( *filterspec )
304
+
305
+ self.log.debug "cloning %p with alternative filterspec: %p" % [ self, filterspec ]
306
+ return self.clone( :filter => (self.filter | newfilter) )
307
+ end
308
+
309
+
279
310
  ### If called with no argument, returns the current scope of the Branchset. If
280
311
  ### called with an argument (which should be one of the keys of
281
312
  ### Treequel::Constants::SCOPE), returns a clone of the receiving Branchset
@@ -141,6 +141,18 @@ module Treequel::Constants
141
141
  FEATURE_NAMES = FEATURE_OIDS.invert.freeze
142
142
 
143
143
 
144
+ ### The list of RFC4512 operational attributes that "SHOULD" be maintained.
145
+ ### (http://tools.ietf.org/html/rfc4512#section-3.4)
146
+ MINIMAL_OPERATIONAL_ATTRIBUTES = [
147
+ :creatorsName,
148
+ :createTimestamp,
149
+ :modifiersName,
150
+ :modifyTimestamp,
151
+ :structuralObjectClass,
152
+ :governingStructureRule,
153
+ ]
154
+
155
+
144
156
  ### OIDs of RFC values
145
157
  module OIDS
146
158