treequel 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. data/ChangeLog +354 -0
  2. data/LICENSE +27 -0
  3. data/README +66 -0
  4. data/Rakefile +345 -0
  5. data/Rakefile.local +43 -0
  6. data/bin/treeirb +14 -0
  7. data/bin/treequel +229 -0
  8. data/examples/company-directory.rb +112 -0
  9. data/examples/ldap-monitor.rb +143 -0
  10. data/examples/ldap-monitor/public/css/master.css +328 -0
  11. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  12. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  13. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  14. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  15. data/examples/ldap-monitor/public/images/plug.png +0 -0
  16. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  17. data/examples/ldap-monitor/public/images/tick.png +0 -0
  18. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  19. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  20. data/examples/ldap-monitor/views/backends.erb +41 -0
  21. data/examples/ldap-monitor/views/connections.erb +74 -0
  22. data/examples/ldap-monitor/views/databases.erb +39 -0
  23. data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
  24. data/examples/ldap-monitor/views/index.erb +14 -0
  25. data/examples/ldap-monitor/views/layout.erb +35 -0
  26. data/examples/ldap-monitor/views/listeners.erb +30 -0
  27. data/examples/ldap_state.rb +62 -0
  28. data/lib/treequel.rb +145 -0
  29. data/lib/treequel/branch.rb +589 -0
  30. data/lib/treequel/branchcollection.rb +204 -0
  31. data/lib/treequel/branchset.rb +360 -0
  32. data/lib/treequel/constants.rb +604 -0
  33. data/lib/treequel/directory.rb +541 -0
  34. data/lib/treequel/exceptions.rb +32 -0
  35. data/lib/treequel/filter.rb +704 -0
  36. data/lib/treequel/mixins.rb +325 -0
  37. data/lib/treequel/schema.rb +245 -0
  38. data/lib/treequel/schema/attributetype.rb +252 -0
  39. data/lib/treequel/schema/ldapsyntax.rb +96 -0
  40. data/lib/treequel/schema/matchingrule.rb +124 -0
  41. data/lib/treequel/schema/matchingruleuse.rb +124 -0
  42. data/lib/treequel/schema/objectclass.rb +289 -0
  43. data/lib/treequel/sequel_integration.rb +26 -0
  44. data/lib/treequel/utils.rb +169 -0
  45. data/rake/191_compat.rb +26 -0
  46. data/rake/dependencies.rb +76 -0
  47. data/rake/helpers.rb +434 -0
  48. data/rake/hg.rb +261 -0
  49. data/rake/manual.rb +782 -0
  50. data/rake/packaging.rb +135 -0
  51. data/rake/publishing.rb +318 -0
  52. data/rake/rdoc.rb +30 -0
  53. data/rake/style.rb +62 -0
  54. data/rake/svn.rb +668 -0
  55. data/rake/testing.rb +187 -0
  56. data/rake/verifytask.rb +64 -0
  57. data/rake/win32.rb +190 -0
  58. data/spec/lib/constants.rb +93 -0
  59. data/spec/lib/helpers.rb +100 -0
  60. data/spec/treequel/branch_spec.rb +569 -0
  61. data/spec/treequel/branchcollection_spec.rb +213 -0
  62. data/spec/treequel/branchset_spec.rb +376 -0
  63. data/spec/treequel/directory_spec.rb +487 -0
  64. data/spec/treequel/filter_spec.rb +482 -0
  65. data/spec/treequel/mixins_spec.rb +330 -0
  66. data/spec/treequel/schema/attributetype_spec.rb +237 -0
  67. data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
  68. data/spec/treequel/schema/matchingrule_spec.rb +158 -0
  69. data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
  70. data/spec/treequel/schema/objectclass_spec.rb +262 -0
  71. data/spec/treequel/schema_spec.rb +118 -0
  72. data/spec/treequel/utils_spec.rb +49 -0
  73. data/spec/treequel_spec.rb +179 -0
  74. metadata +169 -0
@@ -0,0 +1,39 @@
1
+ <h2>Databases</h2>
2
+
3
+ <p><%= subsystem[:description ]%></p>
4
+
5
+ <table id="database-list" class="horizontal">
6
+ <thead>
7
+ <tr>
8
+ <th></th>
9
+ <th class="even">Shadow?</th>
10
+ <th class="odd">URI</th>
11
+ <th class="even">See Also</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <% databases.each_with_index do |database, i| %>
16
+ <% rowclass = i.divmod(2).last.zero? ? "even" : "odd" %>
17
+ <tr class="<%= rowclass %>">
18
+ <th><%= database[:monitoredInfo] %></th>
19
+ <td class="even icon">
20
+ <% if database[:monitorIsShadow].first == 'TRUE' %>
21
+ <img src="/images/tick.png" width="16" height="16" alt="Yes" />
22
+ <% else %>
23
+ &nbsp;
24
+ <% end %>
25
+ </td>
26
+ <td class="odd list">
27
+ <% if database[:labeledURI] %>
28
+ <span class="uri"><%= database[:labeledURI].first.gsub(/[^\x20-\x7f]/, '') %></span>
29
+ <% end %>
30
+ </td>
31
+ <td class="even">
32
+ <% if database[:seeAlso] %><span class="dn"><%= database[:seeAlso] %></span><% end %>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ </tbody>
37
+ </table>
38
+
39
+
@@ -0,0 +1,14 @@
1
+ <h2>The <%= h subsystem[:cn] %> Subsystem</h2>
2
+
3
+ <p><%= subsystem[:description].join(' ') %></p>
4
+
5
+ <h3>Subsystem</h3>
6
+ <pre><%= subsystem.to_ldif %></pre>
7
+
8
+ <h3>Contents</h3>
9
+ <dl>
10
+ <% contents.each do |branch| %>
11
+ <dt><%= branch.dn %></dt>
12
+ <dd><pre><%= branch.to_ldif %></pre></dd>
13
+ <% end %>
14
+ </dl>
@@ -0,0 +1,14 @@
1
+ <h2>Monitor Datapoints</h2>
2
+
3
+ <p>This server is running: <code><%= server_info %></code>.</p>
4
+
5
+ <p>Monitor Subsystems</p>
6
+
7
+ <dl>
8
+ <% for subsystem in datapoints.sort_by {|b| b[:cn].first } %>
9
+ <dt><a href="/<%= subsystem[:cn].first.downcase %>"><%= h subsystem[:cn].first %></a></dt>
10
+ <dd><%= h subsystem[:description].join(" ") %></dd>
11
+ <% end %>
12
+ </dl>
13
+
14
+
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+
4
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5
+ <head>
6
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
7
+ <title>LDAP Web Monitor</title>
8
+
9
+ <link rel="icon" type="image/png" href="/images/treequel-favicon.png" />
10
+
11
+ <link rel="stylesheet" href="/css/master.css" type="text/css" media="screen" title="master"
12
+ charset="utf-8">
13
+ </head>
14
+ <body>
15
+
16
+ <div id="content">
17
+ <h1>LDAP Web Monitor</h1>
18
+
19
+ <div id="homelink">
20
+ <a href="/">&#x2191; Top</a>
21
+ </div>
22
+
23
+ <%= yield %>
24
+
25
+ </div>
26
+
27
+ <div id="footer">
28
+ <p>
29
+ <span class="name">Treequel LDAP Web Monitor</span>
30
+ <span class="vcsrev">$rev$</span>
31
+ </p>
32
+ </div>
33
+
34
+ </body>
35
+ </html>
@@ -0,0 +1,30 @@
1
+ <h2>Listeners</h2>
2
+
3
+ <p><%= subsystem[:description ]%></p>
4
+
5
+ <table id="listener-list" class="horizontal">
6
+ <thead>
7
+ <tr>
8
+ <th></th>
9
+ <th class="odd">URI</th>
10
+ <th class="even">Socket</th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <% listeners.each_with_index do |listener, i| %>
15
+ <% rowclass = i.divmod(2).last.zero? ? "even" : "odd" %>
16
+ <tr class="<%= rowclass %>">
17
+ <th><%= listener[:monitoredInfo] %></th>
18
+ <td class="even icon">
19
+ <span class="uri"><%= listener[:labeledURI].first.gsub(/[^\x20-\x7f]/, '') %></span>
20
+ </td>
21
+ <td class="odd list">
22
+ <span class="socket"><%=
23
+ listener[:monitorConnectionLocalAddress].first[/IP=(.*)/, 1] %></span>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+
30
+
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+
4
+ # Use the OpenLDAP monitoring interface (cn=Monitor) to poll a collection of LDAP
5
+ # servers for collection information. See
6
+ #
7
+ # http://www.openldap.org/doc/admin24/monitoringslapd.html
8
+ #
9
+ # for details on how to set your servers up with this interface.
10
+ #
11
+ # Original ruby-ldap version by Mahlon E. Smith.
12
+ # Ported to Treequel by Michael Granger
13
+
14
+ BEGIN {
15
+ require 'pathname'
16
+ basedir = Pathname( __FILE__ ).dirname.parent
17
+ libdir = basedir + 'lib'
18
+
19
+ $LOAD_PATH.unshift( libdir.to_s )
20
+ }
21
+
22
+ require 'rubygems'
23
+ require 'treequel'
24
+
25
+ BIND_DN = 'cn=admin,cn=Monitor'
26
+ BIND_PASS = 'XXX'
27
+
28
+ SERVER_LIST = %w{
29
+ ldap1.acme.com
30
+ ldap2.acme.com
31
+ ldap3.acme.com
32
+ }
33
+
34
+ Treequel::Branch.include_operational_attrs = true
35
+
36
+ total_connections = 0
37
+ total_operations = 0
38
+
39
+ SERVER_LIST.each do |server|
40
+ con = ops = 0
41
+ dir = Treequel.directory( :host => server, :base_dn => 'cn=Monitor' )
42
+
43
+ conns = dir.cn( :connections ).filter( :objectClass => :monitorConnection ).
44
+ select( :monitorConnectionNumber, :monitorConnectionOpsExecuting )
45
+
46
+ dir.bound_as( BIND_DN, BIND_PASS ) do
47
+ con = conns.all.length
48
+ ops = conns.map( :monitorConnectionOpsExecuting ).
49
+ collect {|connops| connops.first.to_i }.
50
+ inject {|sum,connops| sum + connops }
51
+
52
+ puts "LDAP server: %s\n\t%s\n\tServing %d operations across %d clients\n\n" % [
53
+ server, dir[:monitoredInfo], ops, con
54
+ ]
55
+ end
56
+
57
+ total_connections = total_connections + con
58
+ total_operations = total_operations + ops
59
+ end
60
+
61
+ puts "\n%d active operations across %d clients\n" % [ total_operations, total_connections ]
62
+
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'logger'
4
+ require 'uri'
5
+ require 'uri/ldap'
6
+
7
+
8
+ ### Add an LDAPS URI type if none exists (ruby pre 1.8.7)
9
+ unless URI.const_defined?( :LDAPS )
10
+ module URI
11
+ class LDAPS < LDAP
12
+ DEFAULT_PORT = 636
13
+ end
14
+ @@schemes['LDAPS'] = LDAPS
15
+ end
16
+ end
17
+
18
+
19
+ # A library for interacting with LDAP modelled after Sequel.
20
+ #
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.
31
+ #
32
+ module Treequel
33
+
34
+ # Library version
35
+ VERSION = '1.0.0'
36
+
37
+ # VCS revision
38
+ REVISION = %q$rev: e352bc86498a $
39
+
40
+ # Load the logformatters and some other stuff first
41
+ require 'treequel/constants'
42
+ require 'treequel/utils'
43
+
44
+ include Treequel::Constants
45
+
46
+
47
+ ### Logging
48
+ @default_logger = Logger.new( $stderr )
49
+ @default_logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
50
+
51
+ @default_log_formatter = Treequel::LogFormatter.new( @default_logger )
52
+ @default_logger.formatter = @default_log_formatter
53
+
54
+ @logger = @default_logger
55
+
56
+
57
+ class << self
58
+ # The log formatter that will be used when the logging subsystem is reset
59
+ attr_accessor :default_log_formatter
60
+
61
+ # The logger that will be used when the logging subsystem is reset
62
+ attr_accessor :default_logger
63
+
64
+ # The logger that's currently in effect
65
+ attr_accessor :logger
66
+ alias_method :log, :logger
67
+ alias_method :log=, :logger=
68
+ end
69
+
70
+
71
+ ### Reset the global logger object to the default
72
+ def self::reset_logger
73
+ self.logger = self.default_logger
74
+ self.logger.level = Logger::WARN
75
+ self.logger.formatter = self.default_log_formatter
76
+ end
77
+
78
+
79
+ ### Returns +true+ if the global logger has not been set to something other than
80
+ ### the default one.
81
+ def self::using_default_logger?
82
+ return self.logger == self.default_logger
83
+ end
84
+
85
+
86
+ ### Return the library's version string
87
+ def self::version_string( include_buildnum=false )
88
+ vstring = "%s %s" % [ self.name, VERSION ]
89
+ vstring << " (build %s)" % [ REVISION ] if include_buildnum
90
+ return vstring
91
+ end
92
+
93
+ ### Create a Treequel::Directory object, either from a Hash of options or an LDAP URL.
94
+ def self::directory( *args )
95
+ options = {}
96
+
97
+ args.each do |arg|
98
+ case arg
99
+ when String, URI
100
+ options.merge!( self.make_options_from_uri(arg) )
101
+ when Hash
102
+ options.merge!( arg )
103
+ else
104
+ raise ArgumentError, "unknown directory option %p: expected URL or Hash"
105
+ end
106
+ end
107
+
108
+ return Treequel::Directory.new( options )
109
+ end
110
+
111
+
112
+ ### Make an options hash suitable for passing to Treequel::Directory.new from the
113
+ ### given +uri+.
114
+ def self::make_options_from_uri( uri )
115
+ uri = URI( uri ) unless uri.is_a?( URI )
116
+ raise ArgumentError, "not an LDAP URL: %p" % [ uri ] unless
117
+ uri.scheme =~ /ldaps?/
118
+ options = {}
119
+
120
+ if uri.port
121
+ options[:port] = uri.port
122
+ elsif uri.scheme == 'ldaps'
123
+ options[:port] = LDAP::LDAPS_PORT
124
+ end
125
+
126
+ options[:connect_type] = :ssl if uri.scheme == 'ldaps'
127
+
128
+ options[:host] = uri.host if uri.host
129
+ options[:base_dn] = uri.dn unless uri.dn.nil? || uri.dn.empty?
130
+ options[:bind_dn] = uri.user if uri.user
131
+ options[:pass] = uri.password if uri.password
132
+
133
+ return options
134
+ end
135
+
136
+ # Now load the rest of the library
137
+ require 'treequel/exceptions'
138
+ require 'treequel/directory'
139
+ require 'treequel/branch'
140
+ require 'treequel/branchset'
141
+ require 'treequel/filter'
142
+
143
+ end # module Treequel
144
+
145
+
@@ -0,0 +1,589 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'forwardable'
4
+ require 'ldap'
5
+ require 'ldap/ldif'
6
+
7
+ require 'treequel'
8
+ require 'treequel/mixins'
9
+ require 'treequel/constants'
10
+ require 'treequel/branchset'
11
+ require 'treequel/branchcollection'
12
+
13
+
14
+ # The object in Treequel that wraps an entry. It knows how to construct other branches
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
+ class Treequel::Branch
29
+ include Comparable,
30
+ Treequel::Loggable,
31
+ Treequel::Constants
32
+
33
+ extend Treequel::Delegation,
34
+ Treequel::AttributeDeclarations
35
+
36
+
37
+
38
+ #################################################################
39
+ ### C L A S S M E T H O D S
40
+ #################################################################
41
+
42
+ # Whether or not to include operational attributes when fetching the entry for branches.
43
+ class << self
44
+ extend Treequel::AttributeDeclarations
45
+ @include_operational_attrs = false
46
+ predicate_attr :include_operational_attrs
47
+ end
48
+
49
+
50
+ ### Create a new Treequel::Branch from the given +entry+ hash from the specified +directory+.
51
+ def self::new_from_entry( entry, directory )
52
+ return self.new( directory, entry['dn'].first, entry )
53
+ end
54
+
55
+
56
+ #################################################################
57
+ ### I N S T A N C E M E T H O D S
58
+ #################################################################
59
+
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
+ ### time it is needed.
64
+ def initialize( directory, dn, entry=nil )
65
+ raise ArgumentError, "invalid DN" unless dn.match( Patterns::DISTINGUISHED_NAME )
66
+ raise ArgumentError, "can't cast a %s to an LDAP::Entry" % [entry.class.name] unless
67
+ entry.nil? || entry.is_a?( Hash )
68
+
69
+ @directory = directory
70
+ @dn = dn
71
+ @entry = entry
72
+
73
+ @include_operational_attrs = self.class.include_operational_attrs?
74
+
75
+ @values = {}
76
+ end
77
+
78
+
79
+ ######
80
+ public
81
+ ######
82
+
83
+ # Delegate some other methods to a new Branchset via the #branchset method
84
+ def_method_delegators :branchset, :filter, :scope, :select, :limit, :timeout, :order
85
+
86
+
87
+ # The directory the branch's entry lives in
88
+ attr_reader :directory
89
+
90
+ # The DN of the branch
91
+ attr_reader :dn
92
+ alias_method :to_s, :dn
93
+
94
+ # Whether or not to include operational attributes when fetching the Branch's entry
95
+ predicate_attr :include_operational_attrs
96
+
97
+
98
+ ### Change the DN the Branch uses to look up its entry.
99
+
100
+ def dn=( newdn )
101
+ self.clear_caches
102
+ @dn = newdn
103
+ end
104
+
105
+
106
+ ### Enable or disable fetching of operational attributes (RC4512, section 3.4).
107
+ def include_operational_attrs=( new_setting )
108
+ self.clear_caches
109
+ @include_operational_attrs = new_setting ? true : false
110
+ end
111
+
112
+
113
+ ### Return the attribute/s which make up this Branch's RDN.
114
+ def rdn_attributes
115
+ return make_rdn_hash( self.rdn )
116
+ end
117
+
118
+
119
+ ### Return the LDAP::Entry associated with the receiver, fetching it from the
120
+ ### directory if necessary. Returns +nil+ if the entry doesn't exist in the
121
+ ### directory.
122
+ 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
132
+ end
133
+
134
+
135
+ ### Returns <tt>true</tt> if there is an entry currently in the directory with the
136
+ ### branch's DN.
137
+ def exists?
138
+ return self.entry ? true : false
139
+ end
140
+
141
+
142
+ ### Return the RDN of the branch.
143
+ def rdn
144
+ return self.split_dn( 2 ).first
145
+ end
146
+
147
+
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.
151
+ def split_dn( limit=0 )
152
+ return self.dn.split( /\s*,\s*/, limit )
153
+ end
154
+
155
+
156
+ ### Return the LDAP URI for this branch
157
+ def uri
158
+ uri = self.directory.uri
159
+ uri.dn = self.dn
160
+ return uri
161
+ end
162
+
163
+
164
+ ### Return the DN of this entry's parent, or nil if it doesn't have one.
165
+ def parent_dn
166
+ return nil if self.dn == self.directory.base_dn
167
+ return self.split_dn( 2 ).last
168
+ end
169
+
170
+
171
+ ### Return the Branch's immediate parent node.
172
+ def parent
173
+ return self.class.new( self.directory, self.parent_dn )
174
+ end
175
+
176
+
177
+ ### Return the Branch's immediate children as Treeque::Branch objects.
178
+ def children
179
+ return self.directory.search( self, :one, '(objectClass=*)' )
180
+ end
181
+
182
+
183
+ ### Return a Treequel::Branchset that will use the receiver as its base.
184
+ def branchset
185
+ return Treequel::Branchset.new( self )
186
+ end
187
+
188
+
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
+ ### Returns a human-readable representation of the object suitable for
336
+ ### debugging.
337
+ def inspect
338
+ return "#<%s:0x%0x %s @ %s entry=%p>" % [
339
+ self.class.name,
340
+ self.object_id * 2,
341
+ self.dn,
342
+ self.directory,
343
+ @entry,
344
+ ]
345
+ end
346
+
347
+
348
+ ### Return the entry's DN as an RFC1781-style UFN (User-Friendly Name).
349
+ def to_ufn
350
+ return LDAP.dn2ufn( self.dn )
351
+ end
352
+
353
+
354
+ ### Return the Branch as an LDAP::LDIF::Entry.
355
+ def to_ldif
356
+ ldif = "dn: %s\n" % [ self.dn ]
357
+
358
+ entry = self.entry || self.valid_attributes_hash
359
+
360
+ entry.keys.reject {|k| k == 'dn' }.each do |attribute|
361
+ 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
366
+ end
367
+ end
368
+
369
+ return LDAP::LDIF::Entry.new( ldif )
370
+ end
371
+
372
+
373
+ ### Fetch the value/s associated with the given +attrname+ from the underlying entry.
374
+ def []( attrname )
375
+ attrsym = attrname.to_sym
376
+
377
+ unless @values.key?( attrsym )
378
+ directory = self.directory
379
+ entry = self.entry or return nil
380
+ return nil unless (( value = entry[attrsym.to_s] ))
381
+
382
+ self.log.debug " value is not cached; checking its attributeType"
383
+ if attribute = directory.schema.attribute_types[ attrsym ]
384
+ self.log.debug " attribute exists; checking the entry for a value"
385
+
386
+ syntax_oid = attribute.syntax_oid
387
+
388
+ if attribute.single?
389
+ self.log.debug " attributeType is SINGLE; unwrapping the Array"
390
+ @values[ attrsym ] = directory.convert_syntax_value( syntax_oid, value.first )
391
+ else
392
+ self.log.debug " attributeType is not SINGLE; keeping the Array"
393
+ @values[ attrsym ] = value.collect do |raw|
394
+ directory.convert_syntax_value( syntax_oid, raw )
395
+ end
396
+ @values[ attrsym ].freeze if @values[ attrsym ].is_a?( Array )
397
+ end
398
+
399
+ else
400
+ self.log.info "no attributeType for %p" % [ attrsym ]
401
+ @values[ attrsym ] = value
402
+ @values[ attrsym ].freeze
403
+ end
404
+ else
405
+ self.log.debug " value is cached."
406
+ end
407
+
408
+ return @values[ attrsym ]
409
+ end
410
+
411
+
412
+ ### Set attribute +attrname+ to a new +value+.
413
+ def []=( attrname, value )
414
+ value = [ value ] unless value.is_a?( Array )
415
+ self.log.debug "Modifying %s to %p" % [ attrname, value ]
416
+ self.directory.modify( self, attrname.to_s => value )
417
+ @values.delete( attrname.to_sym )
418
+ self.entry[ attrname.to_s ] = value
419
+ end
420
+
421
+
422
+ ### Make the changes to the entry specified by the given +attributes+.
423
+ def merge( attributes )
424
+ self.directory.modify( self, attributes )
425
+ self.clear_caches
426
+
427
+ return true
428
+ end
429
+ alias_method :modify, :merge
430
+
431
+
432
+ ### Delete the entry associated with the branch from the directory.
433
+ def delete
434
+ self.directory.delete( self )
435
+ self.clear_caches
436
+
437
+ return true
438
+ end
439
+
440
+
441
+ ### Create the entry for this Branch with the specified +attributes+. The +attributes+ should,
442
+ ### at a minimum, contain the pair `:objectClass => :someStructuralObjectClass`.
443
+ def create( attributes={} )
444
+ self.directory.create( self, attributes )
445
+ return self
446
+ end
447
+
448
+
449
+ ### Copy the entry for this Branch to a new entry with the given +newdn+ and merge in the
450
+ ### specified +attributes+.
451
+ def copy( newdn, attributes={} )
452
+
453
+ # Fully-qualify RDNs
454
+ newdn = newdn + ',' + self.parent_dn unless newdn.index(',')
455
+
456
+ self.log.debug "Creating a copy of %p at %p" % [ self.dn, newdn ]
457
+ newbranch = self.class.new( self.directory, newdn )
458
+
459
+ attributes = self.entry.merge( attributes )
460
+
461
+ self.log.debug " merged attributes: %p" % [ attributes ]
462
+ self.directory.create( newbranch, attributes )
463
+
464
+ return newbranch
465
+ end
466
+
467
+
468
+ ### Move the entry associated with this branch to a new entry indicated by +rdn+. If
469
+ ### any +attributes+ are given, also replace the corresponding attributes on the new
470
+ ### entry with them.
471
+ def move( rdn, attributes={} )
472
+ self.log.debug "Asking the directory to move me to an entry called %p" % [ rdn ]
473
+ return self.directory.move( self, rdn, attributes )
474
+ end
475
+
476
+
477
+ ### Comparable interface: Returns -1 if other_branch is less than, 0 if other_branch is
478
+ ### equal to, and +1 if other_branch is greater than the receiving Branch.
479
+ def <=>( other_branch )
480
+ # Try the easy cases first
481
+ return nil unless other_branch.respond_to?( :dn ) &&
482
+ other_branch.respond_to?( :split_dn )
483
+ return 0 if other_branch.dn == self.dn
484
+
485
+ # Try comparing reversed attribute pairs
486
+ rval = nil
487
+ pairseq = self.split_dn.reverse.zip( other_branch.split_dn.reverse )
488
+ pairseq.each do |a,b|
489
+ comparison = (a <=> b)
490
+ return comparison if !comparison.nil? && comparison.nonzero?
491
+ end
492
+
493
+ # The branches are related, so directly comparing DN strings will work
494
+ return self.dn <=> other_branch.dn
495
+ end
496
+
497
+
498
+ ### Fetch a new Treequel::Branch object for the child of the receiver with the specified
499
+ ### +rdn+.
500
+ def get_child( rdn )
501
+ newdn = [ rdn, self.dn ].join( ',' )
502
+ return self.class.new( self.directory, newdn )
503
+ end
504
+
505
+
506
+ ### Addition operator: return a Treequel::BranchCollection that contains both the receiver
507
+ ### and +other_branch+.
508
+ def +( other_branch )
509
+ return Treequel::BranchCollection.new( self.branchset, other_branch.branchset )
510
+ end
511
+
512
+
513
+
514
+ #########
515
+ protected
516
+ #########
517
+
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.
521
+ ###
522
+ ### E.g.,
523
+ ### branch = Treequel::Branch.new( directory, 'ou=people,dc=acme,dc=com' )
524
+ ### branch.uid( :chester ).dn
525
+ ### # => 'uid=chester,ou=people,dc=acme,dc=com'
526
+ ### branch.uid( :chester, :employeeType => 'admin' ).dn
527
+ ### # => '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?
530
+ valid_types = self.directory.schema.attribute_types
531
+
532
+ return super(attribute) unless
533
+ valid_types.key?( attribute ) &&
534
+ additional_attributes.keys.all? {|ex_attr| valid_types.key?(ex_attr) }
535
+
536
+ rdn = rdn_from_pair_and_hash( attribute, value, additional_attributes )
537
+
538
+ return self.get_child( rdn )
539
+ end
540
+
541
+
542
+ ### Clear any cached values when the structural state of the object changes.
543
+ def clear_caches
544
+ @entry = nil
545
+ @values.clear
546
+ end
547
+
548
+
549
+ #######
550
+ private
551
+ #######
552
+
553
+ ### Make an RDN string (RFC 4514) from the primary +attribute+ and +value+ pair plus any
554
+ ### +additional_attributes+ (for multivalue RDNs).
555
+ def rdn_from_pair_and_hash( attribute, value, additional_attributes={} )
556
+ additional_attributes.merge!( attribute => value )
557
+ return additional_attributes.sort_by {|k,v| k.to_s }.
558
+ collect {|pair| pair.join('=') }.
559
+ join('+')
560
+ end
561
+
562
+
563
+ ### Split the given +rdn+ into an Array of the iniital RDN attribute and value, and a
564
+ ### Hash containing any additional pairs.
565
+ def pair_and_hash_from_rdn( rdn )
566
+ initial, *trailing = rdn.split( '+' )
567
+ initial_pair = initial.split( /\s*=\s*/ )
568
+ trailing_pairs = trailing.inject({}) do |hash,pair|
569
+ k,v = pair.split( /\s*=\s*/ )
570
+ hash[ k ] = v
571
+ hash
572
+ end
573
+
574
+ return initial_pair + [ trailing_pairs ]
575
+ end
576
+
577
+
578
+ ### Given an +RDN+, return a Hash of the key/value pairs which make it up.
579
+ def make_rdn_hash( rdn )
580
+ return rdn.split( /\s*\+\s*/ ).inject({}) do |attributes, pair|
581
+ attrname, value = pair.split(/\s*=\s*/)
582
+ attributes[ attrname ] = [ value ]
583
+ attributes
584
+ end
585
+ end
586
+
587
+ end # class Treequel::Branch
588
+
589
+