treequel 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +130 -1
- data/Rakefile +8 -3
- data/Rakefile.local +2 -0
- data/bin/treeirb +6 -2
- data/bin/treequel +5 -4
- data/lib/treequel/branch.rb +133 -72
- data/lib/treequel/branchcollection.rb +16 -5
- data/lib/treequel/branchset.rb +37 -6
- data/lib/treequel/constants.rb +12 -0
- data/lib/treequel/directory.rb +96 -41
- data/lib/treequel/filter.rb +42 -1
- data/lib/treequel/model/objectclass.rb +111 -0
- data/lib/treequel/model.rb +363 -0
- data/lib/treequel/monkeypatches.rb +65 -0
- data/lib/treequel/schema/attributetype.rb +108 -18
- data/lib/treequel/schema/ldapsyntax.rb +15 -0
- data/lib/treequel/schema/matchingrule.rb +24 -0
- data/lib/treequel/schema/matchingruleuse.rb +24 -0
- data/lib/treequel/schema/objectclass.rb +70 -5
- data/lib/treequel/schema/table.rb +5 -15
- data/lib/treequel/schema.rb +64 -1
- data/lib/treequel.rb +5 -5
- data/rake/documentation.rb +27 -0
- data/rake/hg.rb +14 -2
- data/rake/manual.rb +1 -1
- data/spec/lib/constants.rb +9 -7
- data/spec/lib/control_behavior.rb +1 -0
- data/spec/lib/matchers.rb +1 -0
- data/spec/treequel/branch_spec.rb +229 -20
- data/spec/treequel/branchcollection_spec.rb +73 -39
- data/spec/treequel/branchset_spec.rb +59 -8
- data/spec/treequel/control_spec.rb +1 -0
- data/spec/treequel/controls/contentsync_spec.rb +1 -0
- data/spec/treequel/controls/pagedresults_spec.rb +1 -0
- data/spec/treequel/controls/sortedresults_spec.rb +1 -0
- data/spec/treequel/directory_spec.rb +46 -10
- data/spec/treequel/filter_spec.rb +28 -5
- data/spec/treequel/mixins_spec.rb +7 -14
- data/spec/treequel/model/objectclass_spec.rb +330 -0
- data/spec/treequel/model_spec.rb +433 -0
- data/spec/treequel/monkeypatches_spec.rb +118 -0
- data/spec/treequel/schema/attributetype_spec.rb +116 -0
- data/spec/treequel/schema/ldapsyntax_spec.rb +8 -0
- data/spec/treequel/schema/matchingrule_spec.rb +6 -1
- data/spec/treequel/schema/matchingruleuse_spec.rb +5 -0
- data/spec/treequel/schema/objectclass_spec.rb +41 -3
- data/spec/treequel/schema/table_spec.rb +31 -18
- data/spec/treequel/schema_spec.rb +13 -16
- data/spec/treequel_spec.rb +22 -0
- data.tar.gz.sig +1 -0
- metadata +40 -7
- metadata.gz.sig +0 -0
- data/spec/treequel/utils_spec.rb +0 -49
data/lib/treequel/directory.rb
CHANGED
@@ -9,6 +9,7 @@ require 'treequel'
|
|
9
9
|
require 'treequel/schema'
|
10
10
|
require 'treequel/mixins'
|
11
11
|
require 'treequel/constants'
|
12
|
+
require 'treequel/branch'
|
12
13
|
|
13
14
|
|
14
15
|
# The object in Treequel that represents a connection to a directory, the
|
@@ -27,17 +28,33 @@ class Treequel::Directory
|
|
27
28
|
:connect_type => :tls,
|
28
29
|
:base_dn => nil,
|
29
30
|
:bind_dn => nil,
|
30
|
-
:pass => nil
|
31
|
+
:pass => nil,
|
32
|
+
:results_class => Treequel::Branch,
|
31
33
|
}
|
32
34
|
|
33
|
-
# Default mapping of SYNTAX OIDs to conversions
|
34
|
-
# information on what a valid conversion is.
|
35
|
-
|
36
|
-
OIDS::BIT_STRING_SYNTAX
|
37
|
-
OIDS::BOOLEAN_SYNTAX
|
38
|
-
OIDS::GENERALIZED_TIME_SYNTAX
|
39
|
-
OIDS::UTC_TIME_SYNTAX
|
40
|
-
OIDS::INTEGER_SYNTAX
|
35
|
+
# Default mapping of SYNTAX OIDs to conversions from an LDAP string.
|
36
|
+
# See #add_attribute_conversions for more information on what a valid conversion is.
|
37
|
+
DEFAULT_ATTRIBUTE_CONVERSIONS = {
|
38
|
+
OIDS::BIT_STRING_SYNTAX => lambda {|bs, _| bs[0..-1].to_i(2) },
|
39
|
+
OIDS::BOOLEAN_SYNTAX => { 'TRUE' => true, 'FALSE' => false },
|
40
|
+
OIDS::GENERALIZED_TIME_SYNTAX => lambda {|string, _| Time.parse(string) },
|
41
|
+
OIDS::UTC_TIME_SYNTAX => lambda {|string, _| Time.parse(string) },
|
42
|
+
OIDS::INTEGER_SYNTAX => lambda {|string, _| Integer(string) },
|
43
|
+
OIDS::DISTINGUISHED_NAME_SYNTAX => lambda {|dn, directory|
|
44
|
+
resclass = directory.results_class
|
45
|
+
resclass.new( directory, dn )
|
46
|
+
},
|
47
|
+
}
|
48
|
+
|
49
|
+
# Default mapping of SYNTAX OIDs to conversions to an LDAP string from a Ruby object.
|
50
|
+
# See #add_object_conversion for more information on what a valid conversion is.
|
51
|
+
DEFAULT_OBJECT_CONVERSIONS = {
|
52
|
+
OIDS::BIT_STRING_SYNTAX => lambda {|bs, _| bs.to_i.to_s(2) },
|
53
|
+
OIDS::BOOLEAN_SYNTAX => lambda {|obj, _| obj ? 'TRUE' : 'FALSE' },
|
54
|
+
OIDS::GENERALIZED_TIME_SYNTAX => lambda {|time, _| time.ldap_generalized },
|
55
|
+
OIDS::UTC_TIME_SYNTAX => lambda {|time, _| time.ldap_utc },
|
56
|
+
OIDS::INTEGER_SYNTAX => lambda {|obj, _| Integer(obj).to_s },
|
57
|
+
OIDS::DISTINGUISHED_NAME_SYNTAX => lambda {|obj, _| obj.dn },
|
41
58
|
}
|
42
59
|
|
43
60
|
# :NOTE: the docs for #search_ext2 lie. The method signature is actually:
|
@@ -102,22 +119,26 @@ class Treequel::Directory
|
|
102
119
|
### The DN of the user to bind as; if unset, binds anonymously.
|
103
120
|
### @option options [String] :pass (nil)
|
104
121
|
### The password to use when binding.
|
122
|
+
### @option options [CLass] :results_class (Treequel::Branch)
|
123
|
+
### The class to instantiate by default for entries fetched from the Directory.
|
105
124
|
def initialize( options={} )
|
106
|
-
options
|
125
|
+
options = DEFAULT_OPTIONS.merge( options )
|
107
126
|
|
108
|
-
@host
|
109
|
-
@port
|
110
|
-
@connect_type
|
127
|
+
@host = options[:host]
|
128
|
+
@port = options[:port]
|
129
|
+
@connect_type = options[:connect_type]
|
130
|
+
@results_class = options[:results_class]
|
111
131
|
|
112
|
-
@conn
|
113
|
-
@bound_user
|
132
|
+
@conn = nil
|
133
|
+
@bound_user = nil
|
114
134
|
|
115
|
-
@base_dn
|
135
|
+
@base_dn = options[:base_dn] || self.get_default_base_dn
|
116
136
|
|
117
|
-
@base
|
137
|
+
@base = nil
|
118
138
|
|
119
|
-
@
|
120
|
-
@
|
139
|
+
@object_conversions = DEFAULT_OBJECT_CONVERSIONS.dup
|
140
|
+
@attribute_conversions = DEFAULT_ATTRIBUTE_CONVERSIONS.dup
|
141
|
+
@registered_controls = []
|
121
142
|
|
122
143
|
# Immediately bind if credentials are passed to the initializer.
|
123
144
|
if ( options[:bind_dn] && options[:pass] )
|
@@ -134,7 +155,7 @@ class Treequel::Directory
|
|
134
155
|
def_method_delegators :base, *DELEGATED_BRANCH_METHODS
|
135
156
|
|
136
157
|
# Delegate some methods to the connection via the #conn method
|
137
|
-
def_method_delegators :conn, :controls, :referrals
|
158
|
+
def_method_delegators :conn, :controls, :referrals, :root_dse
|
138
159
|
|
139
160
|
|
140
161
|
# The host to connect to.
|
@@ -149,6 +170,10 @@ class Treequel::Directory
|
|
149
170
|
# @return [Symbol]
|
150
171
|
attr_accessor :connect_type
|
151
172
|
|
173
|
+
# The Class to instantiate when wrapping results fetched from the Directory.
|
174
|
+
# @return [Class]
|
175
|
+
attr_accessor :results_class
|
176
|
+
|
152
177
|
# The base DN of the directory
|
153
178
|
# @return [String]
|
154
179
|
attr_accessor :base_dn
|
@@ -165,7 +190,7 @@ class Treequel::Directory
|
|
165
190
|
### Fetch the Branch for the base node of the directory.
|
166
191
|
### @return [Treequel::Branch]
|
167
192
|
def base
|
168
|
-
return @base ||=
|
193
|
+
return @base ||= self.results_class.new( self, self.base_dn )
|
169
194
|
end
|
170
195
|
|
171
196
|
|
@@ -330,8 +355,8 @@ class Treequel::Directory
|
|
330
355
|
###
|
331
356
|
### @option options [Class] :results_class (Treequel::Branch)
|
332
357
|
### The Class to use when wrapping results; if not specified, defaults to the class
|
333
|
-
### of +base+ if it responds to #new_from_entry, or
|
334
|
-
### if it does not.
|
358
|
+
### of +base+ if it responds to #new_from_entry, or the directory object's
|
359
|
+
### #results_class if it does not.
|
335
360
|
### @option options [Array<String, Symbol>] :selectattrs (['*'])
|
336
361
|
### The attributes to return from the search; defaults to '*', which means to
|
337
362
|
### return all non-operational attributes. Specifying '+' will cause the search
|
@@ -372,7 +397,9 @@ class Treequel::Directory
|
|
372
397
|
if options.key?( :results_class )
|
373
398
|
collectclass = options.delete( :results_class )
|
374
399
|
else
|
375
|
-
collectclass = base.class.respond_to?( :new_from_entry ) ?
|
400
|
+
collectclass = base.class.respond_to?( :new_from_entry ) ?
|
401
|
+
base.class :
|
402
|
+
self.results_class
|
376
403
|
end
|
377
404
|
|
378
405
|
# Format the arguments in the way #search_ext2 expects them
|
@@ -395,7 +422,6 @@ class Treequel::Directory
|
|
395
422
|
]]
|
396
423
|
|
397
424
|
results = []
|
398
|
-
|
399
425
|
self.conn.search_ext2( base_dn, scope, filter, *parameters ).each do |entry|
|
400
426
|
branch = collectclass.new_from_entry( entry, self )
|
401
427
|
branch.include_operational_attrs = base.include_operational_attrs? if
|
@@ -495,25 +521,41 @@ class Treequel::Directory
|
|
495
521
|
end
|
496
522
|
|
497
523
|
|
524
|
+
### Add +conversion+ mapping for attributes of specified +oid+ to a Ruby object. A
|
525
|
+
### conversion is any object that responds to #[] with a String
|
526
|
+
### argument(e.g., Proc, Method, Hash); the argument is the raw value String returned
|
527
|
+
### from the LDAP entry, and it should return the converted value. Adding a mapping
|
528
|
+
### with a nil +conversion+ effectively clears it.
|
529
|
+
### @see #convert_to_object
|
530
|
+
def add_attribute_conversion( oid, conversion=nil )
|
531
|
+
conversion = Proc.new if block_given?
|
532
|
+
@attribute_conversions[ oid ] = conversion
|
533
|
+
end
|
534
|
+
|
535
|
+
|
498
536
|
### Add +conversion+ mapping for the specified +oid+. A conversion is any object that
|
499
|
-
### responds to #[] with
|
500
|
-
### the
|
501
|
-
###
|
502
|
-
|
537
|
+
### responds to #[] with an object argument(e.g., Proc, Method, Hash); the argument is
|
538
|
+
### the Ruby object that's being set as a value in an LDAP entry, and it should return the
|
539
|
+
### raw LDAP string. Adding a mapping with a nil +conversion+ effectively clears it.
|
540
|
+
### @see #convert_to_attribute
|
541
|
+
def add_object_conversion( oid, conversion=nil )
|
503
542
|
conversion = Proc.new if block_given?
|
504
|
-
@
|
543
|
+
@object_conversions[ oid ] = conversion
|
505
544
|
end
|
506
545
|
|
507
546
|
|
508
547
|
### Register the specified +modules+
|
509
548
|
def register_controls( *modules )
|
510
|
-
|
511
|
-
|
549
|
+
supported_controls = self.supported_control_oids
|
550
|
+
self.log.debug "Got %d supported controls: %p" %
|
551
|
+
[ supported_controls.length, supported_controls ]
|
512
552
|
|
513
553
|
modules.each do |mod|
|
514
554
|
oid = mod.const_get( :OID ) if mod.const_defined?( :OID )
|
515
555
|
raise NotImplementedError, "%s doesn't define an OID" % [ mod.name ] if oid.nil?
|
516
556
|
|
557
|
+
self.log.debug "Checking for directory support for %p (%s)" % [ mod, oid ]
|
558
|
+
|
517
559
|
if supported_controls.include?( oid )
|
518
560
|
@registered_controls << mod
|
519
561
|
else
|
@@ -524,17 +566,30 @@ class Treequel::Directory
|
|
524
566
|
end
|
525
567
|
|
526
568
|
|
527
|
-
### Map the specified +
|
569
|
+
### Map the specified LDAP +attribute+ to its Ruby datatype if one is registered for the given
|
528
570
|
### syntax +oid+. If there is no conversion registered, just return the +value+ as-is.
|
529
|
-
def
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
return
|
571
|
+
def convert_to_object( oid, attribute )
|
572
|
+
return attribute unless conversion = @attribute_conversions[ oid ]
|
573
|
+
|
574
|
+
if conversion.respond_to?( :call )
|
575
|
+
return conversion.call( attribute, self )
|
576
|
+
else
|
577
|
+
return conversion[ attribute ]
|
534
578
|
end
|
579
|
+
end
|
535
580
|
|
536
|
-
|
537
|
-
|
581
|
+
|
582
|
+
### Map the specified Ruby +object+ to its LDAP string equivalent if a conversion is
|
583
|
+
### registered for the given syntax +oid+. If there is no conversion registered, just
|
584
|
+
### returns the +value+ as a String (via #to_s).
|
585
|
+
def convert_to_attribute( oid, object )
|
586
|
+
return object.to_s unless conversion = @object_conversions[ oid ]
|
587
|
+
|
588
|
+
if conversion.respond_to?( :call )
|
589
|
+
return conversion.call( object, self )
|
590
|
+
else
|
591
|
+
return conversion[ object ]
|
592
|
+
end
|
538
593
|
end
|
539
594
|
|
540
595
|
|
@@ -619,7 +674,7 @@ class Treequel::Directory
|
|
619
674
|
|
620
675
|
### Fetch the default base dn for the server from the server's Root DSE.
|
621
676
|
def get_default_base_dn
|
622
|
-
dse = self.
|
677
|
+
dse = self.root_dse
|
623
678
|
return '' if dse.nil? || dse.empty?
|
624
679
|
return dse.first['namingContexts'].first
|
625
680
|
end
|
data/lib/treequel/filter.rb
CHANGED
@@ -62,6 +62,15 @@ class Treequel::Filter
|
|
62
62
|
return self.filters.collect {|f| f.to_s }.join
|
63
63
|
end
|
64
64
|
|
65
|
+
|
66
|
+
### Append operator: add the +other+ filter to the list.
|
67
|
+
### @param [Treequel::Filter] other the new filter to add
|
68
|
+
### @return [Treequel::Filter::FilterList] self (for chaining)
|
69
|
+
def <<( other )
|
70
|
+
@filters << other
|
71
|
+
return self
|
72
|
+
end
|
73
|
+
|
65
74
|
end # class FilterList
|
66
75
|
|
67
76
|
|
@@ -140,6 +149,12 @@ class Treequel::Filter
|
|
140
149
|
return '&' + @filterlist.to_s
|
141
150
|
end
|
142
151
|
|
152
|
+
### Add an additional filter to the list of requirements
|
153
|
+
### @param [Treequel::Filter] filter the new requirement
|
154
|
+
def add_requirement( filter )
|
155
|
+
@filterlist << filter
|
156
|
+
end
|
157
|
+
|
143
158
|
end # AndComponent
|
144
159
|
|
145
160
|
|
@@ -160,6 +175,12 @@ class Treequel::Filter
|
|
160
175
|
return '|' + @filterlist.to_s
|
161
176
|
end
|
162
177
|
|
178
|
+
### Add an additional filter to the list of alternatives
|
179
|
+
### @param [Treequel::Filter] filter the new alternative
|
180
|
+
def add_alternation( filter )
|
181
|
+
@filterlist << filter
|
182
|
+
end
|
183
|
+
|
163
184
|
end # class OrComponent
|
164
185
|
|
165
186
|
|
@@ -215,8 +236,10 @@ class Treequel::Filter
|
|
215
236
|
self.log.debug "creating a new %s %s for %p and %p" %
|
216
237
|
[ filtertype, self.class.name, attribute, value ]
|
217
238
|
|
218
|
-
|
239
|
+
# Handle Sequel :attribute.identifier
|
240
|
+
attribute = attribute.value if attribute.respond_to?( :value )
|
219
241
|
|
242
|
+
filtertype = filtertype.to_s.downcase.to_sym
|
220
243
|
if FILTERTYPE_OP.key?( filtertype )
|
221
244
|
# no-op
|
222
245
|
elsif FILTEROP_NAMES.key?( filtertype.to_s )
|
@@ -688,6 +711,24 @@ class Treequel::Filter
|
|
688
711
|
alias_method :+, :&
|
689
712
|
|
690
713
|
|
714
|
+
### OR two filters together
|
715
|
+
### @param [Treequel::Filter] other_filter
|
716
|
+
def |( other_filter )
|
717
|
+
return other_filter if self.promiscuous?
|
718
|
+
return self.dup if other_filter.promiscuous?
|
719
|
+
|
720
|
+
# Collapse nested ORs into a single one with an additional alternation
|
721
|
+
# if possible.
|
722
|
+
if self.component.respond_to?( :add_alternation )
|
723
|
+
self.log.debug "collapsing nested ORs..."
|
724
|
+
newcomp = self.component.dup
|
725
|
+
newcomp.add_alternation( other_filter )
|
726
|
+
return self.class.new( newcomp )
|
727
|
+
else
|
728
|
+
return self.class.new( :or, [self, other_filter] )
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
691
732
|
|
692
733
|
end # class Treequel::Filter
|
693
734
|
|
@@ -0,0 +1,111 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
require 'treequel'
|
5
|
+
require 'treequel/model'
|
6
|
+
require 'treequel/mixins'
|
7
|
+
require 'treequel/constants'
|
8
|
+
|
9
|
+
|
10
|
+
# Mixin that provides Treequel::Model characteristics to a mixin module.
|
11
|
+
module Treequel::Model::ObjectClass
|
12
|
+
|
13
|
+
### Extension callback -- add data structures to the extending +mod+.
|
14
|
+
### @param [Module] mod the mixin module to be extended
|
15
|
+
def self::extended( mod )
|
16
|
+
# mod.instance_variable_set( :@model_directory, nil )
|
17
|
+
# mod.instance_variable_set( :@model_bases, [] )
|
18
|
+
mod.instance_variable_set( :@model_class, Treequel::Model )
|
19
|
+
mod.instance_variable_set( :@model_objectclasses, [] )
|
20
|
+
mod.instance_variable_set( :@model_bases, [] )
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
### Inclusion callback -- Methods should be applied to the module rather than an instance.
|
26
|
+
### Warn the user if they use include() and extend() instead.
|
27
|
+
def self::included( mod )
|
28
|
+
warn "extending %p rather than appending features to it" % [ mod ]
|
29
|
+
mod.extend( self )
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
### Declare which Treequel::Model subclasses the mixin will register itself with. If this is
|
34
|
+
### used, it should be declared *before* declaring the mixin's bases and/or objectClasses.
|
35
|
+
def model_class( mclass=nil )
|
36
|
+
if mclass
|
37
|
+
|
38
|
+
# If there were already registered objectclasses, remove them from the previous
|
39
|
+
# model class
|
40
|
+
unless @model_objectclasses.empty? && @model_bases.empty?
|
41
|
+
Treequel.log.warn "%p: model_class should come before model_objectclasses" % [ self ]
|
42
|
+
@model_class.unregister_mixin( self )
|
43
|
+
mclass.register_mixin( self )
|
44
|
+
end
|
45
|
+
@model_class = mclass
|
46
|
+
end
|
47
|
+
|
48
|
+
return @model_class
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
### Set or get objectClasses that the mixin requires. Also registers the mixin with
|
53
|
+
### Treequel::Model.
|
54
|
+
###
|
55
|
+
### @param [Array<Symbol>] objectclasses the objectClasses the mixin will apply to, as an
|
56
|
+
### array of Symbols
|
57
|
+
### @return [Array<Symbol>] the objectClasses that the module requires
|
58
|
+
def model_objectclasses( *objectclasses )
|
59
|
+
unless objectclasses.empty?
|
60
|
+
@model_objectclasses = objectclasses
|
61
|
+
@model_class.register_mixin( self )
|
62
|
+
end
|
63
|
+
return @model_objectclasses.dup
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
### Set or get base DNs that the mixin applies to.
|
68
|
+
def model_bases( *base_dns )
|
69
|
+
unless base_dns.empty?
|
70
|
+
@model_bases = base_dns.collect {|dn| dn.gsub(/\s+/, '') }
|
71
|
+
@model_class.register_mixin( self )
|
72
|
+
end
|
73
|
+
|
74
|
+
return @model_bases.dup
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
### Return a Branchset (or BranchCollection if the receiver has more than one
|
79
|
+
### base) that can be used to search the given +directory+ for entries to which
|
80
|
+
### the receiver applies.
|
81
|
+
###
|
82
|
+
### @param [Treequel::Directory] directory the directory to search
|
83
|
+
### @return [Treequel::Branchset, Treequel::BranchCollection] the encapsulated search
|
84
|
+
def search( directory )
|
85
|
+
bases = self.model_bases
|
86
|
+
objectclasses = self.model_objectclasses
|
87
|
+
|
88
|
+
raise Treequel::ModelError, "%p has no search criteria defined" % [ self ] if
|
89
|
+
bases.empty? && objectclasses.empty?
|
90
|
+
|
91
|
+
Treequel.log.debug "Creating search for %s using model class %p" %
|
92
|
+
[ self.name, self.model_class ]
|
93
|
+
|
94
|
+
# Start by making a Branchset or BranchCollection for the mixin's bases. If
|
95
|
+
# the mixin doesn't have any bases, just use the base DN of the directory
|
96
|
+
# to be searched
|
97
|
+
bases = [directory.base_dn] if bases.empty?
|
98
|
+
search = bases.
|
99
|
+
map {|base| self.model_class.new(directory, base).branchset }.
|
100
|
+
inject {|branch1,branch2| branch1 + branch2 }
|
101
|
+
|
102
|
+
Treequel.log.debug "Search branch after applying bases is: %p" % [ search ]
|
103
|
+
|
104
|
+
return self.model_objectclasses.inject( search ) do |branchset, oid|
|
105
|
+
Treequel.log.debug " adding filter for objectClass=%s to %p" % [ oid, branchset ]
|
106
|
+
branchset.filter( :objectClass => oid )
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end # Treequel::Model::ObjectClass
|
111
|
+
|