treequel 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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