treequel 1.0.1 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/ChangeLog +176 -14
  2. data/LICENSE +1 -1
  3. data/Rakefile +61 -45
  4. data/Rakefile.local +20 -0
  5. data/bin/treequel +502 -269
  6. data/examples/ldap-rack-auth.rb +2 -0
  7. data/lib/treequel.rb +221 -18
  8. data/lib/treequel/branch.rb +410 -201
  9. data/lib/treequel/branchcollection.rb +25 -13
  10. data/lib/treequel/branchset.rb +42 -40
  11. data/lib/treequel/constants.rb +233 -3
  12. data/lib/treequel/control.rb +95 -0
  13. data/lib/treequel/controls/contentsync.rb +138 -0
  14. data/lib/treequel/controls/pagedresults.rb +162 -0
  15. data/lib/treequel/controls/sortedresults.rb +216 -0
  16. data/lib/treequel/directory.rb +212 -65
  17. data/lib/treequel/exceptions.rb +11 -12
  18. data/lib/treequel/filter.rb +1 -12
  19. data/lib/treequel/mixins.rb +83 -47
  20. data/lib/treequel/monkeypatches.rb +29 -0
  21. data/lib/treequel/schema.rb +23 -19
  22. data/lib/treequel/schema/attributetype.rb +33 -3
  23. data/lib/treequel/schema/ldapsyntax.rb +0 -11
  24. data/lib/treequel/schema/matchingrule.rb +0 -11
  25. data/lib/treequel/schema/matchingruleuse.rb +0 -11
  26. data/lib/treequel/schema/objectclass.rb +36 -10
  27. data/lib/treequel/schema/table.rb +159 -0
  28. data/lib/treequel/sequel_integration.rb +7 -7
  29. data/lib/treequel/utils.rb +4 -66
  30. data/rake/documentation.rb +89 -0
  31. data/rake/helpers.rb +375 -307
  32. data/rake/hg.rb +16 -2
  33. data/rake/manual.rb +11 -6
  34. data/rake/packaging.rb +20 -35
  35. data/rake/publishing.rb +22 -62
  36. data/spec/lib/constants.rb +20 -0
  37. data/spec/lib/control_behavior.rb +44 -0
  38. data/spec/lib/matchers.rb +51 -0
  39. data/spec/treequel/branch_spec.rb +88 -29
  40. data/spec/treequel/branchcollection_spec.rb +24 -1
  41. data/spec/treequel/branchset_spec.rb +123 -51
  42. data/spec/treequel/control_spec.rb +48 -0
  43. data/spec/treequel/controls/contentsync_spec.rb +38 -0
  44. data/spec/treequel/controls/pagedresults_spec.rb +138 -0
  45. data/spec/treequel/controls/sortedresults_spec.rb +171 -0
  46. data/spec/treequel/directory_spec.rb +186 -16
  47. data/spec/treequel/mixins_spec.rb +42 -3
  48. data/spec/treequel/schema/attributetype_spec.rb +22 -20
  49. data/spec/treequel/schema/objectclass_spec.rb +67 -46
  50. data/spec/treequel/schema/table_spec.rb +134 -0
  51. data/spec/treequel_spec.rb +277 -15
  52. metadata +89 -108
  53. data/bin/treequel.orig +0 -963
  54. data/examples/ldap-monitor.rb +0 -143
  55. data/examples/ldap-monitor/public/css/master.css +0 -328
  56. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  57. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  58. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  59. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  60. data/examples/ldap-monitor/public/images/plug.png +0 -0
  61. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  62. data/examples/ldap-monitor/public/images/tick.png +0 -0
  63. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  64. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  65. data/examples/ldap-monitor/views/backends.erb +0 -41
  66. data/examples/ldap-monitor/views/connections.erb +0 -74
  67. data/examples/ldap-monitor/views/databases.erb +0 -39
  68. data/examples/ldap-monitor/views/dump_subsystem.erb +0 -14
  69. data/examples/ldap-monitor/views/index.erb +0 -14
  70. data/examples/ldap-monitor/views/layout.erb +0 -35
  71. data/examples/ldap-monitor/views/listeners.erb +0 -30
  72. data/rake/rdoc.rb +0 -30
  73. data/rake/win32.rb +0 -190
@@ -99,3 +99,5 @@ module LdapAuthentification
99
99
  end
100
100
 
101
101
  end # class BindAuth
102
+ end
103
+
data/lib/treequel.rb CHANGED
@@ -1,12 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'ldap'
4
+ require 'ldap/schema'
5
+ require 'ldap/control'
6
+
3
7
  require 'logger'
8
+ require 'pathname'
9
+
4
10
  require 'uri'
5
11
  require 'uri/ldap'
6
12
 
7
13
 
8
14
  ### Add an LDAPS URI type if none exists (ruby pre 1.8.7)
9
15
  unless URI.const_defined?( :LDAPS )
16
+ # @private
10
17
  module URI
11
18
  class LDAPS < LDAP
12
19
  DEFAULT_PORT = 636
@@ -18,24 +25,52 @@ end
18
25
 
19
26
  # A library for interacting with LDAP modelled after Sequel.
20
27
  #
21
- # == Authors
22
- #
23
- # * Michael Granger <ged@FaerieMUD.org>
24
- # * Mahlon E. Smith <mahlon@martini.nu>
25
- #
26
- # :include: LICENSE
27
- #
28
- #--
29
- #
30
- # Please see the file LICENSE in the base directory for licensing details.
28
+ # @version 1.0.4
29
+ #
30
+ # @example
31
+ # # Connect to the directory at the specified URL
32
+ # dir = Treequel.directory( 'ldap://ldap.company.com/dc=company,dc=com' )
33
+ #
34
+ # # Get a list of email addresses of every person in the directory (as
35
+ # # long as people are under ou=people)
36
+ # dir.ou( :people ).filter( :mail ).map( :mail ).flatten
37
+ #
38
+ # # Get a list of all IP addresses for all hosts in any ou=hosts group
39
+ # # in the whole directory:
40
+ # dir.filter( :ou => :hosts ).collection.filter( :ipHostNumber ).
41
+ # map( :ipHostNumber ).flatten
42
+ #
43
+ # # Get all people in the directory in the form of a hash of names
44
+ # # keyed by email addresses
45
+ # dir.ou( :people ).filter( :mail ).to_hash( :mail, :cn )
46
+ #
47
+ # @author Michael Granger <ged@FaerieMUD.org>
48
+ # @author Mahlon E. Smith <mahlon@martini.nu>
31
49
  #
50
+ # @see LICENSE Please see the LICENSE file in the base directory for licensing
51
+ # details.
52
+ #
32
53
  module Treequel
33
54
 
34
55
  # Library version
35
- VERSION = '1.0.1'
56
+ VERSION = '1.0.4'
36
57
 
37
58
  # VCS revision
38
- REVISION = %q$rev: ca660bd12f7f $
59
+ REVISION = %q$Revision: c8534439a5bc $
60
+
61
+ # Common paths for ldap.conf
62
+ COMMON_LDAP_CONF_PATHS = %w[
63
+ ./ldaprc
64
+ ~/.ldaprc
65
+ ~/ldaprc
66
+ /etc/ldap/ldap.conf
67
+ /etc/openldap/ldap.conf
68
+ /etc/ldap.conf
69
+ /usr/local/etc/openldap/ldap.conf
70
+ /usr/local/etc/ldap.conf
71
+ /opt/local/etc/openldap/ldap.conf
72
+ /opt/local/etc/ldap.conf
73
+ ]
39
74
 
40
75
  # Load the logformatters and some other stuff first
41
76
  require 'treequel/constants'
@@ -55,13 +90,14 @@ module Treequel
55
90
 
56
91
 
57
92
  class << self
58
- # The log formatter that will be used when the logging subsystem is reset
93
+ # @return [Logger::Formatter] the log formatter that will be used when the logging
94
+ # subsystem is reset
59
95
  attr_accessor :default_log_formatter
60
96
 
61
- # The logger that will be used when the logging subsystem is reset
97
+ # @return [Logger] the logger that will be used when the logging subsystem is reset
62
98
  attr_accessor :default_logger
63
99
 
64
- # The logger that's currently in effect
100
+ # @return [Logger] the logger that's currently in effect
65
101
  attr_accessor :logger
66
102
  alias_method :log, :logger
67
103
  alias_method :log=, :logger=
@@ -69,6 +105,7 @@ module Treequel
69
105
 
70
106
 
71
107
  ### Reset the global logger object to the default
108
+ ### @return [void]
72
109
  def self::reset_logger
73
110
  self.logger = self.default_logger
74
111
  self.logger.level = Logger::WARN
@@ -83,14 +120,30 @@ module Treequel
83
120
  end
84
121
 
85
122
 
86
- ### Return the library's version string
123
+ ### Get the Treequel version.
124
+ ### @return [String] the library's version
87
125
  def self::version_string( include_buildnum=false )
88
126
  vstring = "%s %s" % [ self.name, VERSION ]
89
- vstring << " (build %s)" % [ REVISION ] if include_buildnum
127
+ vstring << " (build %s)" % [ REVISION[/: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
90
128
  return vstring
91
129
  end
92
130
 
131
+
93
132
  ### Create a Treequel::Directory object, either from a Hash of options or an LDAP URL.
133
+ ### @overload directory( uri )
134
+ ### Create a Treequel::Directory object from an LDAP URI.
135
+ ### @param [URI, String] uri The URI of the directory to connect to, its base, the bind DN,
136
+ ### etc.
137
+ ### @overload directory( options )
138
+ ### @param [Hash] options the connection options
139
+ ### @option options [String] :host ('localhost') The LDAP host to connect to
140
+ ### @option options [Fixnum] :port (LDAP::LDAP_PORT) The port number to connect to
141
+ ### @option options [Symbol] :connect_type (:tls) The type of connection to establish; :tls,
142
+ ### :ssl, or :plain.
143
+ ### @option options [String] :base_dn The base DN of the directory.
144
+ ### @option options [String] :bind_dn The DN of the user to bind as.
145
+ ### @option options [String] :pass The password to use when binding.
146
+ ### @return [Treequel::Directory] the configured Directory object
94
147
  def self::directory( *args )
95
148
  options = {}
96
149
 
@@ -101,7 +154,7 @@ module Treequel
101
154
  when Hash
102
155
  options.merge!( arg )
103
156
  else
104
- raise ArgumentError, "unknown directory option %p: expected URL or Hash"
157
+ raise ArgumentError, "unknown directory option %p: expected URL or Hash" % [ arg ]
105
158
  end
106
159
  end
107
160
 
@@ -109,20 +162,45 @@ module Treequel
109
162
  end
110
163
 
111
164
 
165
+ ### Read the configuration from the specified +configfile+ and/or values in ENV and return
166
+ ### a {Treequel::Directory} for the resulting configuration. Supports OpenLDAP and nss-style
167
+ ### configuration-file directives, and honors the various OpenLDAP environment variables;
168
+ ### see ldap.conf(5) for details.
169
+ ### @param [String] configfile the path to the configuration file to use
170
+ ### @return [Treequel::Directory] the configured Directory object
171
+ def self::directory_from_config( configfile=nil )
172
+ configfile ||= self.find_configfile or
173
+ raise ArgumentError, "No configfile specified, and no defaults present."
174
+
175
+ # Read options from ENV and the config file
176
+ fileopts = self.read_opts_from_config( configfile )
177
+ envopts = self.read_opts_from_environment
178
+
179
+ # Now merge all the options together with env > file > default
180
+ options = Treequel::Directory::DEFAULT_OPTIONS.merge( fileopts.merge(envopts) )
181
+
182
+ return Treequel::Directory.new( options )
183
+ end
184
+
185
+
112
186
  ### Make an options hash suitable for passing to Treequel::Directory.new from the
113
187
  ### given +uri+.
188
+ ### @param [URI, String] uri the URI to parse for options
189
+ ### @return [Hash] the parsed options
114
190
  def self::make_options_from_uri( uri )
115
191
  uri = URI( uri ) unless uri.is_a?( URI )
116
192
  raise ArgumentError, "not an LDAP URL: %p" % [ uri ] unless
117
193
  uri.scheme =~ /ldaps?/
118
194
  options = {}
119
195
 
196
+ # Use either the scheme or the port from the URI to set the port
120
197
  if uri.port
121
198
  options[:port] = uri.port
122
199
  elsif uri.scheme == 'ldaps'
123
200
  options[:port] = LDAP::LDAPS_PORT
124
201
  end
125
202
 
203
+ # Set the connection type if the scheme dictates it
126
204
  options[:connect_type] = :ssl if uri.scheme == 'ldaps'
127
205
 
128
206
  options[:host] = uri.host if uri.host
@@ -133,12 +211,137 @@ module Treequel
133
211
  return options
134
212
  end
135
213
 
214
+
215
+ ### Find a valid ldap.conf config file by first looking in the LDAPCONF and LDAPRC environment
216
+ ### variables, then searching the list of default paths in Treequel::COMMON_LDAP_CONF_PATHS.
217
+ ### @return [String] the first valid path, or nil if no valid configfile could be found
218
+ ### @raise [RuntimeError] if LDAPCONF or LDAPRC contains an invalid or unreadable path
219
+ def self::find_configfile
220
+ # LDAPCONF may be set to the path of a configuration file. This path can
221
+ # be absolute or relative to the current working directory.
222
+ if configfile = ENV['LDAPCONF']
223
+ Treequel.log.info "Using LDAPCONF environment variable for path to ldap.conf"
224
+ configpath = Pathname( configfile ).expand_path
225
+ raise "Config file #{configfile}, specified in the LDAPCONF environment variable, " +
226
+ "does not exist or isn't readable." unless configpath.readable?
227
+ return configpath
228
+
229
+ # The LDAPRC, if defined, should be the basename of a file in the current working
230
+ # directory or in the user's home directory.
231
+ elsif rcname = ENV['LDAPRC']
232
+ Treequel.log.info "Using LDAPRC environment variable for path to ldap.conf"
233
+ rcpath = Pathname( rcname ).expand_path
234
+ return rcpath if rcpath.readable?
235
+ rcpath = Pathname( "~" ).expand_path + rcname
236
+ return rcpath if rcpath.readable?
237
+
238
+ raise "Config file '#{rcname}', specified in the LDAPRC environment variable, does not " +
239
+ "exist or isn't readable."
240
+ else
241
+ Treequel.log.info "Searching common paths for ldap.conf"
242
+ return COMMON_LDAP_CONF_PATHS.collect {|path| Pathname(path) }.
243
+ find {|path| path.readable? }
244
+ end
245
+ end
246
+
247
+
248
+ ### Read the ldap.conf-style configuration from +configfile+ and return it as a Hash.
249
+ ### @param [String] configfile The path to the config file to read.
250
+ ### @return [Hash] The hash of configuration values read from the file, in a form suitable for
251
+ ### passing to {Treequel::Directory#initialize}.
252
+ def self::read_opts_from_config( configfile )
253
+ Treequel.log.debug "Reading config options from %s..." % [ configfile ]
254
+ opts = {}
255
+
256
+ linecount = 0
257
+ IO.foreach( configfile ) do |line|
258
+ Treequel.log.debug " line: %p" % [ line ]
259
+ linecount += 1
260
+ case line
261
+
262
+ # URI <ldap[si]://[name[:port]] ...>
263
+ # :TODO: Support multiple URIs somehow?
264
+ when /^\s*URI\s+(\S+)/i
265
+ Treequel.log.debug " setting options from a URI: %p" % [ line ]
266
+ uriopts = self.make_options_from_uri( $1 )
267
+ opts.merge!( uriopts )
268
+
269
+ # BASE <base>
270
+ when /^\s*BASE\s+(\S+)/i
271
+ Treequel.log.debug " setting default base DN: %p" % [ line ]
272
+ opts[:base_dn] = $1
273
+
274
+ # BINDDN <dn>
275
+ when /^\s*BINDDN\s+(\S+)/i
276
+ Treequel.log.debug " setting bind DN: %p" % [ line ]
277
+ opts[:bind_dn] = $1
278
+
279
+ # bindpw <bindpw> (ldap_nss only)
280
+ when /^\s*bindpw\s+(\S+)/i
281
+ Treequel.log.debug " setting bind password from line %s" % [ linecount ]
282
+ opts[:pass] = $1
283
+
284
+ # HOST <name[:port] ...>
285
+ when /^\s*HOST\s+(\S+)/i
286
+ Treequel.log.debug " setting host: %p" % [ line ]
287
+ opts[:host] = $1
288
+
289
+ # PORT <port>
290
+ when /^\s*PORT\s+(\S+)/i
291
+ Treequel.log.debug " setting port: %p" % [ line ]
292
+ opts[:port] = $1.to_i
293
+
294
+ # SSL <on|off|start_tls>
295
+ when /^\s*SSL\s+(on|off|start_tls)/i
296
+ mode = $1.downcase
297
+ case mode
298
+ when 'on'
299
+ Treequel.log.debug " enabling plain SSL: %p" % [ line ]
300
+ opts[:port] = 636
301
+ opts[:connect_type] = :ssl
302
+ when 'off'
303
+ Treequel.log.debug " disabling SSL: %p" % [ line ]
304
+ opts[:port] = 389
305
+ opts[:connect_type] = :plain
306
+ when 'start_tls'
307
+ Treequel.log.debug " enabling TLS: %p" % [ line ]
308
+ opts[:port] = 389
309
+ opts[:connect_type] = :tls
310
+ else
311
+ Treequel.log.error "Unknown 'ssl' setting %p in %s line %d" %
312
+ [ mode, configfile, linecount ]
313
+ end
314
+
315
+ end
316
+ end
317
+
318
+ return opts
319
+ end
320
+
321
+
322
+ ### Read OpenLDAP-style connection options from ENV and return them as a Hash.
323
+ ### @return [Hash] The hash of configuration values read from the environment, in a form
324
+ ### suitable for passing to {Treequel::Directory#initialize}.
325
+ def self::read_opts_from_environment
326
+ opts = {}
327
+
328
+ opts.merge!( self.make_options_from_uri(ENV['LDAPURI']) ) if ENV['LDAPURI']
329
+ opts[:host] = ENV['LDAPHOST'] if ENV['LDAPHOST']
330
+ opts[:port] = ENV['LDAPPORT'].to_i if ENV['LDAPPORT']
331
+ opts[:bind_dn] = ENV['LDAPBINDDN'] if ENV['LDAPBINDDN']
332
+ opts[:base_dn] = ENV['LDAPBASE'] if ENV['LDAPBASE']
333
+
334
+ return opts
335
+ end
336
+
337
+
136
338
  # Now load the rest of the library
137
339
  require 'treequel/exceptions'
138
340
  require 'treequel/directory'
139
341
  require 'treequel/branch'
140
342
  require 'treequel/branchset'
141
343
  require 'treequel/filter'
344
+ require 'treequel/monkeypatches'
142
345
 
143
346
  end # module Treequel
144
347
 
@@ -13,41 +13,44 @@ require 'treequel/branchcollection'
13
13
 
14
14
  # The object in Treequel that wraps an entry. It knows how to construct other branches
15
15
  # for the entries below itself, and how to search for those entries.
16
- #
17
- # == Authors
18
- #
19
- # * Michael Granger <ged@FaerieMUD.org>
20
- # * Mahlon E. Smith <mahlon@martini.nu>
21
- #
22
- # :include: LICENSE
23
- #
24
- #--
25
- #
26
- # Please see the file LICENSE in the base directory for licensing details.
27
- #
28
16
  class Treequel::Branch
29
17
  include Comparable,
30
18
  Treequel::Loggable,
31
- Treequel::Constants
19
+ Treequel::Constants,
20
+ Treequel::Constants::Patterns
32
21
 
33
22
  extend Treequel::Delegation,
34
23
  Treequel::AttributeDeclarations
35
24
 
36
25
 
26
+ # The default width of LDIF output
27
+ DEFAULT_LDIF_WIDTH = 70
28
+
29
+ # The characters to use to fold an LDIF line (newline + a space)
30
+ LDIF_FOLD_SEPARATOR = "\n "
31
+
37
32
 
38
33
  #################################################################
39
34
  ### C L A S S M E T H O D S
40
35
  #################################################################
41
36
 
42
- # Whether or not to include operational attributes when fetching the entry for branches.
37
+ # [Boolean] Whether or not to include operational attributes by default.
38
+ @include_operational_attrs = false
39
+
40
+ # Whether or not to include operational attributes when fetching the
41
+ # entry for branches.
43
42
  class << self
44
43
  extend Treequel::AttributeDeclarations
45
- @include_operational_attrs = false
46
44
  predicate_attr :include_operational_attrs
47
45
  end
48
46
 
49
47
 
50
48
  ### Create a new Treequel::Branch from the given +entry+ hash from the specified +directory+.
49
+ ###
50
+ ### @param [LDAP::Entry] entry The raw entry object the Branch is wrapping.
51
+ ### @param [Treequel::Directory] directory The directory object the Branch is from.
52
+ ###
53
+ ### @return [Treequel::Branch] The new branch object.
51
54
  def self::new_from_entry( entry, directory )
52
55
  return self.new( directory, entry['dn'].first, entry )
53
56
  end
@@ -57,10 +60,14 @@ class Treequel::Branch
57
60
  ### I N S T A N C E M E T H O D S
58
61
  #################################################################
59
62
 
60
- ### Create a new Treequel::Branch with the given +directory+, +rdn_attribute+, +rdn_value+, and
61
- ### +base_dn+. If the optional +entry+ object is given, it will be used to fetch values from
62
- ### the directory; if it isn't provided, it will be fetched from the +directory+ the first
63
+ ### Create a new Treequel::Branch with the given +directory+, +dn+, and an optional +entry+.
64
+ ### If the optional +entry+ object is given, it will be used to fetch values from the
65
+ ### directory; if it isn't provided, it will be fetched from the +directory+ the first
63
66
  ### time it is needed.
67
+ ###
68
+ ### @param [Treequel::Directory] directory The directory the Branch belongs to.
69
+ ### @param [String] dn The DN of the entry the Branch is wrapping.
70
+ ### @param [LDAP::Entry, Hash] entry The entry object if it's already been fetched.
64
71
  def initialize( directory, dn, entry=nil )
65
72
  raise ArgumentError, "invalid DN" unless dn.match( Patterns::DISTINGUISHED_NAME )
66
73
  raise ArgumentError, "can't cast a %s to an LDAP::Entry" % [entry.class.name] unless
@@ -69,10 +76,9 @@ class Treequel::Branch
69
76
  @directory = directory
70
77
  @dn = dn
71
78
  @entry = entry
79
+ @values = {}
72
80
 
73
81
  @include_operational_attrs = self.class.include_operational_attrs?
74
-
75
- @values = {}
76
82
  end
77
83
 
78
84
 
@@ -83,11 +89,16 @@ class Treequel::Branch
83
89
  # Delegate some other methods to a new Branchset via the #branchset method
84
90
  def_method_delegators :branchset, :filter, :scope, :select, :limit, :timeout, :order
85
91
 
92
+ # Delegate some methods to the Branch's directory via its accessor
93
+ def_method_delegators :directory, :controls, :referrals
94
+
86
95
 
87
96
  # The directory the branch's entry lives in
97
+ # @return [Treequel::Directory]
88
98
  attr_reader :directory
89
99
 
90
- # The DN of the branch
100
+ # The DN of the branch.
101
+ # @return [String]
91
102
  attr_reader :dn
92
103
  alias_method :to_s, :dn
93
104
 
@@ -96,7 +107,9 @@ class Treequel::Branch
96
107
 
97
108
 
98
109
  ### Change the DN the Branch uses to look up its entry.
99
-
110
+ ###
111
+ ### @param [String] newdn The new DN.
112
+ ### @return [void]
100
113
  def dn=( newdn )
101
114
  self.clear_caches
102
115
  @dn = newdn
@@ -104,6 +117,9 @@ class Treequel::Branch
104
117
 
105
118
 
106
119
  ### Enable or disable fetching of operational attributes (RC4512, section 3.4).
120
+ ###
121
+ ### @param [Boolean] new_setting
122
+ ### @return [void]
107
123
  def include_operational_attrs=( new_setting )
108
124
  self.clear_caches
109
125
  @include_operational_attrs = new_setting ? true : false
@@ -111,6 +127,7 @@ class Treequel::Branch
111
127
 
112
128
 
113
129
  ### Return the attribute/s which make up this Branch's RDN.
130
+ ### @return [Hash<Symbol => String>] The Branch's RDN attributes as a Hash.
114
131
  def rdn_attributes
115
132
  return make_rdn_hash( self.rdn )
116
133
  end
@@ -119,41 +136,40 @@ class Treequel::Branch
119
136
  ### Return the LDAP::Entry associated with the receiver, fetching it from the
120
137
  ### directory if necessary. Returns +nil+ if the entry doesn't exist in the
121
138
  ### directory.
139
+ ###
140
+ ### @return [LDAP::Entry] The entry wrapped by the Branch.
122
141
  def entry
123
- unless @entry
124
- if self.include_operational_attrs?
125
- @entry = self.directory.get_extended_entry( self )
126
- else
127
- @entry = self.directory.get_entry( self )
128
- end
129
- end
130
-
131
- return @entry
142
+ @entry ||= self.lookup_entry
132
143
  end
133
144
 
134
145
 
135
146
  ### Returns <tt>true</tt> if there is an entry currently in the directory with the
136
147
  ### branch's DN.
148
+ ### @return [Boolean]
137
149
  def exists?
138
150
  return self.entry ? true : false
139
151
  end
140
152
 
141
153
 
142
154
  ### Return the RDN of the branch.
155
+ ### @return [String]
143
156
  def rdn
144
157
  return self.split_dn( 2 ).first
145
158
  end
146
159
 
147
160
 
148
- ### Return the receiver's DN as an Array of attribute=value pairs. If +limit+ is non-zero,
149
- ### only the <code>limit-1</code> first pairs are split from the DN, and the remainder
150
- ### will be returned as the last element.
161
+ ### Return the receiver's DN as an Array of attribute=value pairs.
162
+ ###
163
+ ### @param [Fixnum] limit If non-zero, only the <code>limit-1</code> first pairs
164
+ ### are split from the DN, and the remainder will be returned as the last
165
+ ### element.
151
166
  def split_dn( limit=0 )
152
167
  return self.dn.split( /\s*,\s*/, limit )
153
168
  end
154
169
 
155
170
 
156
171
  ### Return the LDAP URI for this branch
172
+ ### @return [URI]
157
173
  def uri
158
174
  uri = self.directory.uri
159
175
  uri.dn = self.dn
@@ -162,6 +178,7 @@ class Treequel::Branch
162
178
 
163
179
 
164
180
  ### Return the DN of this entry's parent, or nil if it doesn't have one.
181
+ ### @return [String]
165
182
  def parent_dn
166
183
  return nil if self.dn == self.directory.base_dn
167
184
  return self.split_dn( 2 ).last
@@ -169,171 +186,44 @@ class Treequel::Branch
169
186
 
170
187
 
171
188
  ### Return the Branch's immediate parent node.
189
+ ### @return [Treequel::Branch]
172
190
  def parent
173
191
  return self.class.new( self.directory, self.parent_dn )
174
192
  end
175
193
 
176
194
 
195
+ ### Perform a search with the specified +scope+, +filter+, and +parameters+
196
+ ### using the receiver as the base.
197
+ ###
198
+ ### @param scope (see Trequel::Directory#search)
199
+ ### @param filter (see Trequel::Directory#search)
200
+ ### @param parameters (see Trequel::Directory#search)
201
+ ### @param block (see Trequel::Directory#search)
202
+ ###
203
+ ### @return [Array<Treequel::Branch>] the search results
204
+ def search( scope=:subtree, filter='(objectClass=*)', parameters={}, &block )
205
+ return self.directory.search( self, scope, filter, parameters, &block )
206
+ end
207
+
208
+
177
209
  ### Return the Branch's immediate children as Treeque::Branch objects.
210
+ ### @return [Array<Treequel::Branch>]
178
211
  def children
179
- return self.directory.search( self, :one, '(objectClass=*)' )
212
+ return self.search( :one, '(objectClass=*)' )
180
213
  end
181
214
 
182
215
 
183
216
  ### Return a Treequel::Branchset that will use the receiver as its base.
217
+ ### @return [Treequel::Branchset]
184
218
  def branchset
185
219
  return Treequel::Branchset.new( self )
186
220
  end
187
221
 
188
222
 
189
- ### Return Treequel::Schema::ObjectClass instances for each of the receiver's
190
- ### objectClass attributes. If any +additional_classes+ are given,
191
- ### merge them with the current list of the current objectClasses for the lookup.
192
- def object_classes( *additional_classes )
193
- schema = self.directory.schema
194
-
195
- object_classes = self[:objectClass] || []
196
- object_classes |= additional_classes.collect {|str| str.to_sym }
197
- object_classes << :top if object_classes.empty?
198
-
199
- return object_classes.
200
- collect {|oid| schema.object_classes[oid.to_sym] }.
201
- uniq
202
- end
203
-
204
-
205
- ### Return Treequel::Schema::AttributeType instances for each of the receiver's
206
- ### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
207
- ### include the MUST attributeTypes for them as well. This can be used to predict what
208
- ### attributes would need to be present for the entry to be saved if it added the
209
- ### +additional_object_classes+ to its own.
210
- def must_attribute_types( *additional_object_classes )
211
- types = []
212
- oclasses = self.object_classes( *additional_object_classes )
213
- self.log.debug "Gathering MUST attribute types for %d objectClasses" % [ oclasses.length ]
214
- oclasses.each do |oc|
215
- self.log.debug " adding %p from %p" % [ oc.must, oc ]
216
- types |= oc.must
217
- end
218
-
219
- return types
220
- end
221
-
222
-
223
- ### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the receiver's
224
- ### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
225
- ### include the OIDs of the MUST attributes for them as well. This can be used to predict
226
- ### what attributes would need to be present for the entry to be saved if it added the
227
- ### +additional_object_classes+ to its own.
228
- def must_oids( *additional_object_classes )
229
- return self.object_classes( *additional_object_classes ).
230
- collect {|oc| oc.must_oids }.flatten.uniq.reject {|val| val == '' }
231
- end
232
-
233
-
234
- ### Return a Hash of the attributes required by the Branch's objectClasses. If
235
- ### any +additional_object_classes+ are given, include the attributes that would be
236
- ### necessary for the entry to be saved with them.
237
- def must_attributes_hash( *additional_object_classes )
238
- attrhash = {}
239
-
240
- self.must_attribute_types( *additional_object_classes ).each do |attrtype|
241
- self.log.debug " adding attrtype %p to the MUST attributes hash" % [ attrtype ]
242
-
243
- if attrtype.name == :objectClass
244
- attrhash[ :objectClass ] = [:top] | additional_object_classes
245
- elsif attrtype.single?
246
- attrhash[ attrtype.name ] = ''
247
- else
248
- attrhash[ attrtype.name ] = ['']
249
- end
250
- end
251
-
252
- return attrhash
253
- end
254
-
255
-
256
- ### Return Treequel::Schema::AttributeType instances for each of the receiver's
257
- ### objectClass's MAY attributeTypes. If any +additional_object_classes+ are given,
258
- ### include the MAY attributeTypes for them as well. This can be used to predict what
259
- ### optional attributes could be added to the entry if the +additional_object_classes+
260
- ### were added to it.
261
- def may_attribute_types( *additional_object_classes )
262
- return self.object_classes( *additional_object_classes ).
263
- collect {|oc| oc.may }.flatten.uniq
264
- end
265
-
266
-
267
- ### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the receiver's
268
- ### objectClass's MAY attributeTypes. If any +additional_object_classes+ are given,
269
- ### include the OIDs of the MAY attributes for them as well. This can be used to predict
270
- ### what optional attributes could be added to the entry if the +additional_object_classes+
271
- ### were added to it.
272
- def may_oids( *additional_object_classes )
273
- return self.object_classes( *additional_object_classes ).
274
- collect {|oc| oc.may_oids }.flatten.uniq
275
- end
276
-
277
-
278
- ### Return a Hash of the optional attributes allowed by the Branch's objectClasses. If
279
- ### any +additional_object_classes+ are given, include the attributes that would be
280
- ### available for the entry if it had them.
281
- def may_attributes_hash( *additional_object_classes )
282
- entry = self.entry
283
- attrhash = {}
284
-
285
- self.may_attribute_types( *additional_object_classes ).each do |attrtype|
286
- self.log.debug " adding attrtype %p to the MAY attributes hash" % [ attrtype ]
287
-
288
- if attrtype.single?
289
- attrhash[ attrtype.name ] = nil
290
- else
291
- attrhash[ attrtype.name ] = []
292
- end
293
- end
294
-
295
- attrhash[ :objectClass ] |= additional_object_classes
296
- return attrhash
297
- end
298
-
299
-
300
- ### Return Treequel::Schema::AttributeType instances for the set of all of the receiver's
301
- ### MUST and MAY attributeTypes.
302
- def valid_attribute_types
303
- return self.must_attribute_types | self.may_attribute_types
304
- end
305
-
306
-
307
- ### Return a uniqified Array of OIDs (numeric OIDs as Strings, named OIDs as Symbols) for
308
- ### the set of all of the receiver's MUST and MAY attributeTypes.
309
- def valid_attribute_oids
310
- return self.must_oids | self.may_oids
311
- end
312
-
313
-
314
- ### Return a Hash of all the attributes allowed by the Branch's objectClasses. If
315
- ### any +additional_object_classes+ are given, include the attributes that would be
316
- ### available for the entry if it had them.
317
- def valid_attributes_hash( *additional_object_classes )
318
- must = self.must_attributes_hash( *additional_object_classes )
319
- may = self.may_attributes_hash( *additional_object_classes )
320
-
321
- return may.merge( must )
322
- end
323
-
324
-
325
- ### Return +true+ if the specified +attrname+ is a valid attributeType given the
326
- ### receiver's current objectClasses.
327
- def valid_attribute?( attroid )
328
- attroid = attroid.to_sym if attroid.is_a?( String ) &&
329
- attroid !~ NUMERICOID
330
-
331
- return self.valid_attribute_types.any? { |a| a.names.include?( attroid ) }
332
- end
333
-
334
-
335
223
  ### Returns a human-readable representation of the object suitable for
336
224
  ### debugging.
225
+ ###
226
+ ### @return [String]
337
227
  def inspect
338
228
  return "#<%s:0x%0x %s @ %s entry=%p>" % [
339
229
  self.class.name,
@@ -346,23 +236,23 @@ class Treequel::Branch
346
236
 
347
237
 
348
238
  ### Return the entry's DN as an RFC1781-style UFN (User-Friendly Name).
239
+ ### @return [String]
349
240
  def to_ufn
350
241
  return LDAP.dn2ufn( self.dn )
351
242
  end
352
243
 
353
244
 
354
245
  ### Return the Branch as an LDAP::LDIF::Entry.
355
- def to_ldif
246
+ ### @return [String]
247
+ def to_ldif( width=DEFAULT_LDIF_WIDTH )
356
248
  ldif = "dn: %s\n" % [ self.dn ]
357
249
 
358
250
  entry = self.entry || self.valid_attributes_hash
251
+ self.log.debug " making LDIF from an entry: %p" % [ entry ]
359
252
 
360
253
  entry.keys.reject {|k| k == 'dn' }.each do |attribute|
361
254
  entry[ attribute ].each do |val|
362
- # self.log.debug " creating LDIF fragment for %p=%p" % [ attribute, val ]
363
- frag = LDAP::LDIF.to_ldif( attribute, [val.dup] )
364
- # self.log.debug " LDIF fragment is: %p" % [ frag ]
365
- ldif << frag
255
+ ldif << ldif_for_attr( attribute, val, width )
366
256
  end
367
257
  end
368
258
 
@@ -370,7 +260,40 @@ class Treequel::Branch
370
260
  end
371
261
 
372
262
 
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}:"
274
+
275
+ if val =~ /^#{LDIF_SAFE_STRING}$/
276
+ line << ' ' << val.to_s
277
+ else
278
+ line << ': ' << [ val ].pack( 'm' ).chomp
279
+ 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"
289
+ end
290
+
291
+ return ldif
292
+ end
293
+
294
+
373
295
  ### Fetch the value/s associated with the given +attrname+ from the underlying entry.
296
+ ### @return [Array, String]
374
297
  def []( attrname )
375
298
  attrsym = attrname.to_sym
376
299
 
@@ -409,7 +332,21 @@ class Treequel::Branch
409
332
  end
410
333
 
411
334
 
335
+ ### Fetch one or more values from the entry.
336
+ ###
337
+ ### @param [Array<Symbol, String>] attributes The attributes to fetch values for.
338
+ ### @return [Array<String>] The values which correspond to +attributes+.
339
+ def values_at( *attributes )
340
+ return attributes.collect do |attribute|
341
+ self[ attribute ]
342
+ end
343
+ end
344
+
345
+
412
346
  ### Set attribute +attrname+ to a new +value+.
347
+ ###
348
+ ### @param [Symbol, String] attrname attribute name
349
+ ### @param [Object] value the attribute value
413
350
  def []=( attrname, value )
414
351
  value = [ value ] unless value.is_a?( Array )
415
352
  self.log.debug "Modifying %s to %p" % [ attrname, value ]
@@ -420,6 +357,9 @@ class Treequel::Branch
420
357
 
421
358
 
422
359
  ### Make the changes to the entry specified by the given +attributes+.
360
+ ###
361
+ ### @param attributes (see Treequel::Directory#modify)
362
+ ### @return [TrueClass] if the merge succeeded
423
363
  def merge( attributes )
424
364
  self.directory.modify( self, attributes )
425
365
  self.clear_caches
@@ -429,9 +369,34 @@ class Treequel::Branch
429
369
  alias_method :modify, :merge
430
370
 
431
371
 
432
- ### Delete the entry associated with the branch from the directory.
433
- def delete
434
- self.directory.delete( self )
372
+ ### Delete the specified attributes.
373
+ ###
374
+ ### @param [Array<Hash, #to_s>] attributes The attributes to delete, either as
375
+ ### attribute names (in which case all values of the attribute are deleted) or
376
+ ### Hashes of attributes and the Array of value/s which should be deleted.
377
+ ###
378
+ ### @example Delete all 'description' attributes
379
+ ### branch.delete( :description )
380
+ ### @example Delete the 'inetOrgPerson' and 'posixAccount' objectClasses from the entry
381
+ ### branch.delete( :objectClass => [:inetOrgPerson, :posixAccount] )
382
+ ### @example Delete any blank 'description' or 'cn' attributes:
383
+ ### branch.delete( :description => '', :cn => '' )
384
+ ###
385
+ ### @return [TrueClass] if the delete succeeded
386
+ 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 )
393
+ end
394
+ else
395
+ LDAP::Mod.new( LDAP::LDAP_MOD_DELETE, attribute.to_s, [] )
396
+ end
397
+ end.flatten
398
+
399
+ self.directory.modify( self, mods )
435
400
  self.clear_caches
436
401
 
437
402
  return true
@@ -440,6 +405,8 @@ class Treequel::Branch
440
405
 
441
406
  ### Create the entry for this Branch with the specified +attributes+. The +attributes+ should,
442
407
  ### at a minimum, contain the pair `:objectClass => :someStructuralObjectClass`.
408
+ ###
409
+ ### @param [Hash<Symbol,String => Object>] attributes
443
410
  def create( attributes={} )
444
411
  self.directory.create( self, attributes )
445
412
  return self
@@ -448,6 +415,10 @@ class Treequel::Branch
448
415
 
449
416
  ### Copy the entry for this Branch to a new entry with the given +newdn+ and merge in the
450
417
  ### specified +attributes+.
418
+ ###
419
+ ### @param [String] newdn the dn of the new entry
420
+ ### @param [Hash<String, Symbol => Object>] attributes merge attributes
421
+ ### @return [Treequel::Branch] a Branch for the new entry
451
422
  def copy( newdn, attributes={} )
452
423
 
453
424
  # Fully-qualify RDNs
@@ -468,14 +439,20 @@ class Treequel::Branch
468
439
  ### Move the entry associated with this branch to a new entry indicated by +rdn+. If
469
440
  ### any +attributes+ are given, also replace the corresponding attributes on the new
470
441
  ### entry with them.
471
- def move( rdn, attributes={} )
442
+ ###
443
+ ### @param [String] rdn
444
+ ### @param [Hash<String, Symbol => Object>] attributes
445
+ def move( rdn )
472
446
  self.log.debug "Asking the directory to move me to an entry called %p" % [ rdn ]
473
- return self.directory.move( self, rdn, attributes )
447
+ return self.directory.move( self, rdn )
474
448
  end
475
449
 
476
450
 
477
451
  ### Comparable interface: Returns -1 if other_branch is less than, 0 if other_branch is
478
452
  ### equal to, and +1 if other_branch is greater than the receiving Branch.
453
+ ###
454
+ ### @param [Treequel::Branch] other_branch
455
+ ### @return [Fixnum]
479
456
  def <=>( other_branch )
480
457
  # Try the easy cases first
481
458
  return nil unless other_branch.respond_to?( :dn ) &&
@@ -497,6 +474,9 @@ class Treequel::Branch
497
474
 
498
475
  ### Fetch a new Treequel::Branch object for the child of the receiver with the specified
499
476
  ### +rdn+.
477
+ ###
478
+ ### @param [String] rdn The RDN of the child to fetch.
479
+ ### @return [Treequel::Branch]
500
480
  def get_child( rdn )
501
481
  newdn = [ rdn, self.dn ].join( ',' )
502
482
  return self.class.new( self.directory, newdn )
@@ -505,41 +485,270 @@ class Treequel::Branch
505
485
 
506
486
  ### Addition operator: return a Treequel::BranchCollection that contains both the receiver
507
487
  ### and +other_branch+.
488
+ ###
489
+ ### @param [Treequel::Branch] other_branch
490
+ ### @return [Treequel::BranchCollection]
508
491
  def +( other_branch )
509
492
  return Treequel::BranchCollection.new( self.branchset, other_branch.branchset )
510
493
  end
511
494
 
512
495
 
496
+ ### Return Treequel::Schema::ObjectClass instances for each of the receiver's
497
+ ### objectClass attributes. If any +additional_classes+ are given,
498
+ ### merge them with the current list of the current objectClasses for the lookup.
499
+ ###
500
+ ### @param [Array<String, Symbol>] additional_classes
501
+ ### @return [Array<Treequel::Schema::ObjectClass>]
502
+ def object_classes( *additional_classes )
503
+ schema = self.directory.schema
504
+
505
+ oc_oids = self[:objectClass] || []
506
+ oc_oids |= additional_classes.collect {|str| str.to_sym }
507
+ oc_oids << :top if oc_oids.empty?
508
+
509
+ oclasses = []
510
+ oc_oids.each do |oid|
511
+ oc = schema.object_classes[ oid.to_sym ] or
512
+ raise Treequel::Error, "schema doesn't have a %p objectClass" % [ oid ]
513
+ oclasses << oc
514
+ end
515
+
516
+ return oclasses.uniq
517
+ end
518
+
519
+
520
+ ### Return Treequel::Schema::AttributeType instances for each of the receiver's
521
+ ### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
522
+ ### include the MUST attributeTypes for them as well. This can be used to predict what
523
+ ### attributes would need to be present for the entry to be saved if it added the
524
+ ### +additional_object_classes+ to its own.
525
+ ###
526
+ ### @param [Array<String, Symbol>] additional_object_classes
527
+ ### @return [Array<Treequel::Schema::AttributeType>]
528
+ def must_attribute_types( *additional_object_classes )
529
+ types = []
530
+ oclasses = self.object_classes( *additional_object_classes )
531
+ self.log.debug "Gathering MUST attribute types for objectClasses: %p" % [ oclasses ]
532
+
533
+ oclasses.each do |oc|
534
+ self.log.debug " adding %p from %p" % [ oc.must, oc ]
535
+ types |= oc.must
536
+ end
537
+
538
+ return types
539
+ end
540
+
541
+
542
+ ### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the receiver's
543
+ ### objectClass's MUST attributeTypes. If any +additional_object_classes+ are given,
544
+ ### include the OIDs of the MUST attributes for them as well. This can be used to predict
545
+ ### what attributes would need to be present for the entry to be saved if it added the
546
+ ### +additional_object_classes+ to its own.
547
+ ###
548
+ ### @param [Array<String, Symbol>] additional_object_classes
549
+ ### @return [Array<String, Symbol>] oid strings and symbols
550
+ def must_oids( *additional_object_classes )
551
+ return self.object_classes( *additional_object_classes ).
552
+ collect {|oc| oc.must_oids }.flatten.uniq.reject {|val| val == '' }
553
+ end
554
+
555
+
556
+ ### Return a Hash of the attributes required by the Branch's objectClasses. If
557
+ ### any +additional_object_classes+ are given, include the attributes that would be
558
+ ### necessary for the entry to be saved with them.
559
+ ###
560
+ ### @param [Array<String, Symbol>] additional_object_classes
561
+ ### @return [Hash<String => String>]
562
+ def must_attributes_hash( *additional_object_classes )
563
+ attrhash = {}
564
+
565
+ self.must_attribute_types( *additional_object_classes ).each do |attrtype|
566
+ self.log.debug " adding attrtype %p to the MUST attributes hash" % [ attrtype ]
567
+
568
+ if attrtype.name == :objectClass
569
+ attrhash[ :objectClass ] = [:top] | additional_object_classes
570
+ elsif attrtype.single?
571
+ attrhash[ attrtype.name ] = ''
572
+ else
573
+ attrhash[ attrtype.name ] = ['']
574
+ end
575
+ end
576
+
577
+ return attrhash
578
+ end
579
+
580
+
581
+ ### Return Treequel::Schema::AttributeType instances for each of the receiver's
582
+ ### objectClass's MAY attributeTypes. If any +additional_object_classes+ are given,
583
+ ### include the MAY attributeTypes for them as well. This can be used to predict what
584
+ ### optional attributes could be added to the entry if the +additional_object_classes+
585
+ ### were added to it.
586
+ ###
587
+ ### @param [Array<String, Symbol>] additional_object_classes
588
+ ### @return [Array<Treequel::Schema::AttributeType>]
589
+ def may_attribute_types( *additional_object_classes )
590
+ return self.object_classes( *additional_object_classes ).
591
+ collect {|oc| oc.may }.flatten.uniq
592
+ end
593
+
594
+
595
+ ### Return OIDs (numeric OIDs as Strings, named OIDs as Symbols) for each of the receiver's
596
+ ### objectClass's MAY attributeTypes. If any +additional_object_classes+ are given,
597
+ ### include the OIDs of the MAY attributes for them as well. This can be used to predict
598
+ ### what optional attributes could be added to the entry if the +additional_object_classes+
599
+ ### were added to it.
600
+ ###
601
+ ### @param [Array<String, Symbol>] additional_object_classes
602
+ ### @return [Array<String, Symbol>] oid strings and symbols
603
+ def may_oids( *additional_object_classes )
604
+ return self.object_classes( *additional_object_classes ).
605
+ collect {|oc| oc.may_oids }.flatten.uniq
606
+ end
607
+
608
+
609
+ ### Return a Hash of the optional attributes allowed by the Branch's objectClasses. If
610
+ ### any +additional_object_classes+ are given, include the attributes that would be
611
+ ### available for the entry if it had them.
612
+ ###
613
+ ### @param [Array<String, Symbol>] additional_object_classes
614
+ ### @return [Hash<String => String>]
615
+ def may_attributes_hash( *additional_object_classes )
616
+ entry = self.entry
617
+ attrhash = {}
618
+
619
+ self.may_attribute_types( *additional_object_classes ).each do |attrtype|
620
+ self.log.debug " adding attrtype %p to the MAY attributes hash" % [ attrtype ]
621
+
622
+ if attrtype.single?
623
+ attrhash[ attrtype.name ] = nil
624
+ else
625
+ attrhash[ attrtype.name ] = []
626
+ end
627
+ end
628
+
629
+ attrhash[ :objectClass ] |= additional_object_classes
630
+ return attrhash
631
+ end
632
+
633
+
634
+ ### Return Treequel::Schema::AttributeType instances for the set of all of the receiver's
635
+ ### MUST and MAY attributeTypes.
636
+ ###
637
+ ### @return [Array<Treequel::Schema::AttributeType>]
638
+ def valid_attribute_types
639
+ return self.must_attribute_types | self.may_attribute_types
640
+ end
641
+
642
+
643
+ ### 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.
645
+ ###
646
+ ### @return [Array<String, Symbol>]
647
+ def valid_attribute_oids
648
+ return self.must_oids | self.may_oids
649
+ end
650
+
651
+
652
+ ### If the given +attroid+ is a valid attributeType name or numeric OID, return the
653
+ ### AttributeType object that corresponds with it. If it isn't valid, return nil.
654
+ ###
655
+ ### @param [String,Symbol] attroid a numeric OID (as a String) or a named OID (as a Symbol)
656
+ ### @return [Treequel::Schema::AttributeType] the validated attributeType
657
+ def valid_attribute_type( attroid )
658
+ return self.valid_attribute_types.find {|attr_type| attr_type.valid_name?(attroid) }
659
+ end
660
+
661
+
662
+ ### Return +true+ if the specified +attrname+ is a valid attributeType given the
663
+ ### receiver's current objectClasses.
664
+ ###
665
+ ### @param [String, Symbol] the OID (numeric or name) of the attribute in question
666
+ ### @return [Boolean]
667
+ def valid_attribute?( attroid )
668
+ return !self.valid_attribute_type( attroid ).nil?
669
+ end
670
+
671
+
672
+ ### Return a Hash of all the attributes allowed by the Branch's objectClasses. If
673
+ ### any +additional_object_classes+ are given, include the attributes that would be
674
+ ### available for the entry if it had them.
675
+ ###
676
+ ### @param [Array<String, Symbol>] additional_object_classes
677
+ ### @return [Hash<String => String>]
678
+ def valid_attributes_hash( *additional_object_classes )
679
+ self.log.debug "Gathering a hash of all valid attributes:"
680
+ must = self.must_attributes_hash( *additional_object_classes )
681
+ self.log.debug " MUST attributes: %p" % [ must ]
682
+ may = self.may_attributes_hash( *additional_object_classes )
683
+ self.log.debug " MAY attributes: %p" % [ may ]
684
+
685
+ return may.merge( must )
686
+ end
687
+
513
688
 
514
689
  #########
515
690
  protected
516
691
  #########
517
692
 
518
- ### Proxy method: if the first argument matches a valid attribute in the directory's
519
- ### schema, return a new Branch for the RDN made by using the first two arguments as
520
- ### attribute and value, and the remaining hash as additional attributes.
693
+ ### Proxy method: call #traverse_branch if +attribute+ is a valid attribute
694
+ ### and +value+ isn't +nil+.
695
+ ### @see Treequel::Branch#traverse_branch
696
+ def method_missing( attribute, value=nil, additional_attributes={} )
697
+ return super( attribute ) if value.nil?
698
+ return self.traverse_branch( attribute, value, additional_attributes )
699
+ end
700
+
701
+
702
+ ### If +attribute+ matches a valid attribute type in the directory's
703
+ ### schema, return a new Branch for the RDN of +attribute+ and +value+, and
704
+ ### +additional_attributes+ if it's a multi-value RDN.
705
+ ###
706
+ ### @param [Symbol] attribute the RDN attribute of the child
707
+ ### @param [String] value the RDN valye of the child
708
+ ### @param [Hash] additional_attributes any additional RDN attributes
521
709
  ###
522
- ### E.g.,
710
+ ### @example
523
711
  ### branch = Treequel::Branch.new( directory, 'ou=people,dc=acme,dc=com' )
524
712
  ### branch.uid( :chester ).dn
525
713
  ### # => 'uid=chester,ou=people,dc=acme,dc=com'
526
714
  ### branch.uid( :chester, :employeeType => 'admin' ).dn
527
715
  ### # => 'uid=chester+employeeType=admin,ou=people,dc=acme,dc=com'
528
- def method_missing( attribute, value=nil, additional_attributes={} )
529
- return super( attribute ) if value.nil?
716
+ ###
717
+ ### @return [Treequel::Branch] the Branch for the specified child
718
+ ### @raise [NoMethodError] if the +attribute+ or any +additional_attributes+ are
719
+ ### not valid attributeTypes.
720
+ def traverse_branch( attribute, value, additional_attributes={} )
530
721
  valid_types = self.directory.schema.attribute_types
531
722
 
532
- return super(attribute) unless
533
- valid_types.key?( attribute ) &&
534
- additional_attributes.keys.all? {|ex_attr| valid_types.key?(ex_attr) }
723
+ # Raise if either the primary attribute or any secondary attributes are invalid
724
+ if !valid_types.key?( attribute )
725
+ raise NoMethodError, "undefined method `%s' for %p" % [ attribute, self ]
726
+ elsif invalid = additional_attributes.keys.find {|ex_attr| !valid_types.key?(ex_attr) }
727
+ raise NoMethodError, "invalid secondary attribute `%s' for %p" %
728
+ [ invalid, self ]
729
+ end
535
730
 
731
+ # Make a normalized RDN from the arguments and return the Branch for it
536
732
  rdn = rdn_from_pair_and_hash( attribute, value, additional_attributes )
537
-
538
733
  return self.get_child( rdn )
539
734
  end
540
735
 
541
736
 
737
+ ### Fetch the entry from the Branch's directory.
738
+ def lookup_entry
739
+ self.log.debug "Looking up entry for %p" % [ self ]
740
+ if self.include_operational_attrs?
741
+ self.log.debug " including operational attributes."
742
+ return self.directory.get_extended_entry( self )
743
+ else
744
+ self.log.debug " not including operational attributes."
745
+ return self.directory.get_entry( self )
746
+ end
747
+ end
748
+
749
+
542
750
  ### Clear any cached values when the structural state of the object changes.
751
+ ### @return [void]
543
752
  def clear_caches
544
753
  @entry = nil
545
754
  @values.clear