og 0.31.0 → 0.40.0
Sign up to get free protection for your applications and to get access to all the features.
- data/doc/{AUTHORS → CONTRIBUTORS} +26 -10
- data/doc/LICENSE +2 -3
- data/doc/RELEASES +56 -7
- data/doc/tutorial.txt +15 -15
- data/lib/glue/cacheable.rb +2 -5
- data/lib/glue/hierarchical.rb +1 -4
- data/lib/glue/optimistic_locking.rb +0 -2
- data/lib/glue/orderable.rb +79 -75
- data/lib/glue/revisable.rb +19 -24
- data/lib/glue/searchable.rb +0 -2
- data/lib/glue/taggable.rb +31 -29
- data/lib/glue/timestamped.rb +4 -2
- data/lib/og.rb +50 -29
- data/lib/og/adapter.rb +19 -0
- data/lib/og/adapter/mysql.rb +212 -0
- data/lib/og/adapter/mysql/override.rb +34 -0
- data/lib/og/adapter/mysql/script.rb +15 -0
- data/lib/og/adapter/mysql/utils.rb +40 -0
- data/lib/og/adapter/postgresql.rb +231 -0
- data/lib/og/adapter/postgresql/override.rb +117 -0
- data/lib/og/adapter/postgresql/script.rb +15 -0
- data/lib/og/adapter/postgresql/utils.rb +35 -0
- data/lib/og/adapter/sqlite.rb +132 -0
- data/lib/og/adapter/sqlite/override.rb +33 -0
- data/lib/og/adapter/sqlite/script.rb +15 -0
- data/lib/og/collection.rb +35 -7
- data/lib/og/{evolution.rb → dump.rb} +4 -5
- data/lib/og/entity.rb +102 -173
- data/lib/og/entity/clone.rb +119 -0
- data/lib/og/errors.rb +0 -2
- data/lib/og/manager.rb +85 -37
- data/lib/og/relation.rb +52 -34
- data/lib/og/relation/belongs_to.rb +0 -2
- data/lib/og/relation/has_many.rb +27 -4
- data/lib/og/relation/joins_many.rb +41 -14
- data/lib/og/relation/many_to_many.rb +10 -0
- data/lib/og/relation/refers_to.rb +22 -5
- data/lib/og/store.rb +80 -86
- data/lib/og/store/sql.rb +710 -713
- data/lib/og/store/sql/evolution.rb +119 -0
- data/lib/og/store/sql/join.rb +155 -0
- data/lib/og/store/sql/utils.rb +149 -0
- data/lib/og/test/assertions.rb +1 -3
- data/lib/og/test/testcase.rb +0 -2
- data/lib/og/types.rb +2 -5
- data/lib/og/validation.rb +6 -9
- data/test/{og/mixin → glue}/tc_hierarchical.rb +3 -13
- data/test/glue/tc_og_paginate.rb +47 -0
- data/test/{og/mixin → glue}/tc_optimistic_locking.rb +2 -12
- data/test/{og/mixin → glue}/tc_orderable.rb +15 -23
- data/test/glue/tc_orderable2.rb +47 -0
- data/test/glue/tc_revisable.rb +3 -3
- data/test/{og/mixin → glue}/tc_taggable.rb +20 -10
- data/test/{og/mixin → glue}/tc_timestamped.rb +2 -12
- data/test/glue/tc_webfile.rb +36 -0
- data/test/og/CONFIG.rb +8 -11
- data/test/og/multi_validations_model.rb +14 -0
- data/test/og/store/tc_filesys.rb +3 -1
- data/test/og/store/tc_kirby.rb +16 -13
- data/test/og/store/tc_sti.rb +11 -11
- data/test/og/store/tc_sti2.rb +79 -0
- data/test/og/tc_build.rb +41 -0
- data/test/og/tc_cacheable.rb +3 -2
- data/test/og/tc_has_many.rb +96 -0
- data/test/og/tc_inheritance.rb +6 -4
- data/test/og/tc_joins_many.rb +93 -0
- data/test/og/tc_multi_validations.rb +5 -7
- data/test/og/tc_multiple.rb +7 -6
- data/test/og/tc_override.rb +13 -7
- data/test/og/tc_primary_key.rb +30 -0
- data/test/og/tc_relation.rb +8 -14
- data/test/og/tc_reldelete.rb +163 -0
- data/test/og/tc_reverse.rb +17 -14
- data/test/og/tc_scoped.rb +3 -11
- data/test/og/tc_setup.rb +13 -11
- data/test/og/tc_store.rb +21 -28
- data/test/og/tc_validation2.rb +2 -2
- data/test/og/tc_validation_loop.rb +17 -15
- metadata +109 -103
- data/INSTALL +0 -91
- data/ProjectInfo +0 -51
- data/README +0 -177
- data/doc/config.txt +0 -28
- data/examples/README +0 -23
- data/examples/mysql_to_psql.rb +0 -71
- data/examples/run.rb +0 -271
- data/lib/glue/tree.rb +0 -218
- data/lib/og/store/alpha/filesys.rb +0 -110
- data/lib/og/store/alpha/memory.rb +0 -295
- data/lib/og/store/alpha/sqlserver.rb +0 -256
- data/lib/og/store/kirby.rb +0 -490
- data/lib/og/store/mysql.rb +0 -415
- data/lib/og/store/psql.rb +0 -875
- data/lib/og/store/sqlite.rb +0 -348
- data/lib/og/store/sqlite2.rb +0 -241
- data/setup.rb +0 -1585
- data/test/og/tc_sti_find.rb +0 -35
@@ -0,0 +1,119 @@
|
|
1
|
+
module Og
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Implement Og's automatic schema evolution features.
|
5
|
+
# Add schema evolution related methods to SqlStore.
|
6
|
+
#++
|
7
|
+
|
8
|
+
module Evolution
|
9
|
+
|
10
|
+
#--
|
11
|
+
# Override if needed in the actual Adapter implementation.
|
12
|
+
#++
|
13
|
+
|
14
|
+
def add_sql_field(klass, a, anno)
|
15
|
+
Logger.info "Adding field '#{a}' to '#{klass.table}'"
|
16
|
+
query "ALTER TABLE #{klass.table} ADD COLUMN #{field_sql_for_attribute a, anno}"
|
17
|
+
end
|
18
|
+
alias add_sql_column add_sql_field
|
19
|
+
|
20
|
+
#--
|
21
|
+
# Override if needed in the actual Adapter implementation.
|
22
|
+
#++
|
23
|
+
|
24
|
+
def remove_sql_field(klass, a)
|
25
|
+
Logger.info "Removing field '#{a}' from '#{klass.table}'"
|
26
|
+
query "ALTER TABLE #{klass.table} DROP COLUMN #{a}"
|
27
|
+
end
|
28
|
+
alias add_sql_column add_sql_field
|
29
|
+
|
30
|
+
#--
|
31
|
+
# Override if needed in the actual Adapter implementation.
|
32
|
+
#++
|
33
|
+
|
34
|
+
def rename_sql_table(_old, _new)
|
35
|
+
Logger.info "Rename table '#{_old}' to '#{_new}'"
|
36
|
+
query "ALTER TABLE #{_old} RENAME #{_new}"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Evolve the schema (table in sql stores) for the given
|
40
|
+
# class. Compares the fields in the database schema with
|
41
|
+
# the serializable attributes of the given class and tries
|
42
|
+
# to fix mismatches by adding are droping columns.
|
43
|
+
#
|
44
|
+
# === Evolution options
|
45
|
+
#
|
46
|
+
# * :evolve_schema => :add (only add, dont remove columns)
|
47
|
+
# * :evolve_schema => :full (add and delete columns)
|
48
|
+
# * :evolve_schema => :warn (only emit warnings, DEFAULT_
|
49
|
+
# * :evolve_schema => false (no evolution)
|
50
|
+
#
|
51
|
+
# === Example
|
52
|
+
#
|
53
|
+
# Og.setup(
|
54
|
+
# ..
|
55
|
+
# :evolve_schema => :full
|
56
|
+
# ..
|
57
|
+
# )
|
58
|
+
|
59
|
+
def evolve_schema(klass)
|
60
|
+
return unless @options[:evolve_schema]
|
61
|
+
|
62
|
+
sql_fields = create_field_map(klass).keys
|
63
|
+
attrs = klass.serializable_attributes
|
64
|
+
|
65
|
+
# Add new fields to the table.
|
66
|
+
|
67
|
+
for field in attrs
|
68
|
+
unless sql_fields.include? field
|
69
|
+
unless @options[:evolve_schema] == :warn
|
70
|
+
add_sql_field klass, field, klass.ann(field)
|
71
|
+
else
|
72
|
+
Logger.warn "Missing field '#{field}' on table '#{klass.table}'!"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Remove obsolete fields from the table.
|
78
|
+
|
79
|
+
for field in sql_fields
|
80
|
+
unless attrs.include? field
|
81
|
+
if @options[:evolve_schema] == :full
|
82
|
+
remove_sql_field klass, field
|
83
|
+
else
|
84
|
+
Logger.warn "Obsolete field '#{field}' found on table '#{klass.table}'!"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Renames the schema (table in sql stores) for the given
|
91
|
+
# class.
|
92
|
+
#
|
93
|
+
# === Input
|
94
|
+
#
|
95
|
+
# * new_schema = the new schema (Class or table name)
|
96
|
+
# * old_schema = the old schema (Class or table name)
|
97
|
+
#
|
98
|
+
# === Example
|
99
|
+
#
|
100
|
+
# store.rename_schema(TicketArticle, Ticket::Article)
|
101
|
+
|
102
|
+
def rename_schema(old_schema, new_schema)
|
103
|
+
if old_schema.is_a? Class
|
104
|
+
old_schema = table(old_schema)
|
105
|
+
end
|
106
|
+
|
107
|
+
if new_schema.is_a? Class
|
108
|
+
new_schema = table(new_schema)
|
109
|
+
end
|
110
|
+
|
111
|
+
rename_sql_table(old_schema, new_schema)
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
SqlStore.send :include, Evolution
|
117
|
+
|
118
|
+
end
|
119
|
+
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Og
|
2
|
+
|
3
|
+
# Add join related utility methods in SqlUtils.
|
4
|
+
|
5
|
+
module SqlUtils
|
6
|
+
|
7
|
+
def join_object_ordering(obj1, obj2)
|
8
|
+
if obj1.class.to_s <= obj2.class.to_s
|
9
|
+
return obj1, obj2
|
10
|
+
else
|
11
|
+
return obj2, obj1, true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def join_class_ordering(class1, class2)
|
16
|
+
if class1.to_s <= class2.to_s
|
17
|
+
return class1, class2
|
18
|
+
else
|
19
|
+
return class2, class1, true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_join_name(class1, class2, postfix = nil)
|
24
|
+
# Don't reorder arguments, as this is used in places that
|
25
|
+
# have already determined the order they want.
|
26
|
+
"#{Og.table_prefix}j_#{tableize(class1)}_#{tableize(class2)}#{postfix}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def join_table(class1, class2, postfix = nil)
|
30
|
+
first, second = join_class_ordering(class1, class2)
|
31
|
+
build_join_name(first, second, postfix)
|
32
|
+
end
|
33
|
+
|
34
|
+
def join_table_index(key)
|
35
|
+
"#{key}_idx"
|
36
|
+
end
|
37
|
+
|
38
|
+
def join_table_key(klass)
|
39
|
+
klass = klass.schema_inheritance_root_class if klass.schema_inheritance_child?
|
40
|
+
"#{klass.to_s.demodulize.underscore.downcase}_oid"
|
41
|
+
end
|
42
|
+
|
43
|
+
def join_table_keys(class1, class2)
|
44
|
+
if class1 == class2
|
45
|
+
# Fix for the self-join case.
|
46
|
+
return join_table_key(class1), "#{join_table_key(class2)}2"
|
47
|
+
else
|
48
|
+
return join_table_key(class1), join_table_key(class2)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def ordered_join_table_keys(class1, class2)
|
53
|
+
first, second = join_class_ordering(class1, class2)
|
54
|
+
return join_table_keys(first, second)
|
55
|
+
end
|
56
|
+
|
57
|
+
def join_table_info(relation, postfix = nil)
|
58
|
+
|
59
|
+
# some fixes for schema inheritance.
|
60
|
+
|
61
|
+
owner_class, target_class = relation.owner_class, relation.target_class
|
62
|
+
|
63
|
+
raise "Undefined owner_class in #{target_class}" unless owner_class
|
64
|
+
raise "Undefined target_class in #{owner_class}" unless target_class
|
65
|
+
|
66
|
+
owner_class = owner_class.schema_inheritance_root_class if owner_class.schema_inheritance_child?
|
67
|
+
target_class = target_class.schema_inheritance_root_class if target_class.schema_inheritance_child?
|
68
|
+
|
69
|
+
owner_key, target_key = join_table_keys(owner_class, target_class)
|
70
|
+
first, second, changed = join_class_ordering(owner_class, target_class)
|
71
|
+
|
72
|
+
if changed
|
73
|
+
first_key, second_key = target_key, owner_key
|
74
|
+
else
|
75
|
+
first_key, second_key = owner_key, target_key
|
76
|
+
end
|
77
|
+
|
78
|
+
table = (relation.table ?
|
79
|
+
relation.table :
|
80
|
+
join_table(owner_class, target_class, postfix)
|
81
|
+
)
|
82
|
+
|
83
|
+
return {
|
84
|
+
:table => table,
|
85
|
+
:owner_key => owner_key,
|
86
|
+
:owner_table => table(owner_class),
|
87
|
+
:target_key => target_key,
|
88
|
+
:target_table => table(target_class),
|
89
|
+
:first_table => table(first),
|
90
|
+
:first_key => first_key,
|
91
|
+
:first_index => join_table_index(first_key),
|
92
|
+
:second_table => table(second),
|
93
|
+
:second_key => second_key,
|
94
|
+
:second_index => join_table_index(second_key)
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Subclasses can override this if they need a different
|
99
|
+
# syntax.
|
100
|
+
|
101
|
+
def create_join_table_sql(join_table_info, suffix = 'NOT NULL', key_type = 'integer')
|
102
|
+
join_table = join_table_info[:table]
|
103
|
+
first_index = join_table_info[:first_index]
|
104
|
+
first_key = join_table_info[:first_key]
|
105
|
+
second_key = join_table_info[:second_key]
|
106
|
+
second_index = join_table_info[:second_index]
|
107
|
+
|
108
|
+
sql = []
|
109
|
+
|
110
|
+
sql << %{
|
111
|
+
CREATE TABLE #{join_table} (
|
112
|
+
#{first_key} integer NOT NULL,
|
113
|
+
#{second_key} integer NOT NULL,
|
114
|
+
PRIMARY KEY(#{first_key}, #{second_key})
|
115
|
+
)
|
116
|
+
}
|
117
|
+
|
118
|
+
# gmosx: not that useful?
|
119
|
+
# sql << "CREATE INDEX #{first_index} ON #{join_table} (#{first_key})"
|
120
|
+
# sql << "CREATE INDEX #{second_index} ON #{join_table} (#{second_key})"
|
121
|
+
|
122
|
+
return sql
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
# Extends SqlStore by adding join related methods.
|
128
|
+
|
129
|
+
class SqlStore < Store
|
130
|
+
|
131
|
+
# Relate two objects through an intermediate join table.
|
132
|
+
# Typically used in joins_many and many_to_many relations.
|
133
|
+
|
134
|
+
def join(obj1, obj2, table, options = nil)
|
135
|
+
first, second = join_object_ordering(obj1, obj2)
|
136
|
+
first_key, second_key = ordered_join_table_keys(obj1.class, obj2.class)
|
137
|
+
if options
|
138
|
+
exec "INSERT INTO #{table} (#{first_key},#{second_key}, #{options.keys.join(',')}) VALUES (#{first.pk},#{second.pk}, #{options.values.map { |v| quote(v) }.join(',')})"
|
139
|
+
else
|
140
|
+
exec "INSERT INTO #{table} (#{first_key},#{second_key}) VALUES (#{first.pk}, #{second.pk})"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Unrelate two objects be removing their relation from the
|
145
|
+
# join table.
|
146
|
+
|
147
|
+
def unjoin(obj1, obj2, table)
|
148
|
+
first, second = join_object_ordering(obj1, obj2)
|
149
|
+
first_key, second_key = ordered_join_table_keys(obj1.class, obj2.class)
|
150
|
+
exec "DELETE FROM #{table} WHERE #{first_key}=#{first.pk} AND #{second_key}=#{second.pk}"
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Og
|
4
|
+
|
5
|
+
# A collection of useful SQL utilities.
|
6
|
+
|
7
|
+
module SqlUtils
|
8
|
+
|
9
|
+
# Escape an SQL string
|
10
|
+
|
11
|
+
def escape(str)
|
12
|
+
return nil unless str
|
13
|
+
return str.gsub(/'/, "''")
|
14
|
+
end
|
15
|
+
|
16
|
+
# Convert a ruby time to an sql timestamp.
|
17
|
+
#--
|
18
|
+
# TODO: Optimize this.
|
19
|
+
#++
|
20
|
+
|
21
|
+
def timestamp(time = Time.now)
|
22
|
+
return nil unless time
|
23
|
+
return time.strftime("%Y-%m-%d %H:%M:%S")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Output YYY-mm-dd
|
27
|
+
#--
|
28
|
+
# TODO: Optimize this.
|
29
|
+
#++
|
30
|
+
|
31
|
+
def date(date)
|
32
|
+
return nil unless date
|
33
|
+
return "#{date.year}-#{date.month}-#{date.mday}"
|
34
|
+
end
|
35
|
+
|
36
|
+
#--
|
37
|
+
# TODO: implement me!
|
38
|
+
#++
|
39
|
+
|
40
|
+
def blob(val)
|
41
|
+
val
|
42
|
+
end
|
43
|
+
|
44
|
+
# Parse an integer.
|
45
|
+
|
46
|
+
def parse_int(int)
|
47
|
+
int = int.to_i if int
|
48
|
+
return int
|
49
|
+
end
|
50
|
+
|
51
|
+
# Parse a float.
|
52
|
+
|
53
|
+
def parse_float(fl)
|
54
|
+
fl = fl.to_f if fl
|
55
|
+
fl
|
56
|
+
end
|
57
|
+
|
58
|
+
# Parse sql datetime
|
59
|
+
#--
|
60
|
+
# TODO: Optimize this.
|
61
|
+
#++
|
62
|
+
|
63
|
+
def parse_timestamp(str)
|
64
|
+
return nil unless str
|
65
|
+
return Time.parse(str)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Input YYYY-mm-dd
|
69
|
+
#--
|
70
|
+
# TODO: Optimize this.
|
71
|
+
#++
|
72
|
+
|
73
|
+
def parse_date(str)
|
74
|
+
return nil unless str
|
75
|
+
return Date.strptime(str)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Parse a boolean
|
79
|
+
# true, 1, t => true
|
80
|
+
# other => false
|
81
|
+
|
82
|
+
def parse_boolean(str)
|
83
|
+
return true if (str=='true' || str=='t' || str=='1')
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
|
87
|
+
#--
|
88
|
+
# TODO: implement me!!
|
89
|
+
#++
|
90
|
+
|
91
|
+
def parse_blob(val)
|
92
|
+
val
|
93
|
+
end
|
94
|
+
|
95
|
+
# Escape the various Ruby types.
|
96
|
+
|
97
|
+
def quote(vals)
|
98
|
+
vals = [vals] unless vals.is_a?(Array)
|
99
|
+
quoted = vals.inject('') do |s,val|
|
100
|
+
s += case val
|
101
|
+
when Fixnum, Integer, Float
|
102
|
+
val ? val.to_s : 'NULL'
|
103
|
+
when String
|
104
|
+
val ? "'#{escape(val)}'" : 'NULL'
|
105
|
+
when Time
|
106
|
+
val ? "'#{timestamp(val)}'" : 'NULL'
|
107
|
+
when Date
|
108
|
+
val ? "'#{date(val)}'" : 'NULL'
|
109
|
+
when TrueClass, FalseClass
|
110
|
+
val ? "'t'" : 'NULL'
|
111
|
+
else
|
112
|
+
# gmosx: keep the '' for nil symbols.
|
113
|
+
val ? escape(val.to_yaml) : ''
|
114
|
+
end + ','
|
115
|
+
end
|
116
|
+
quoted.chop!
|
117
|
+
vals.size > 1 ? "(#{quoted})" : quoted
|
118
|
+
end
|
119
|
+
|
120
|
+
# Escape the Array Ruby type.
|
121
|
+
|
122
|
+
def quote_array(val)
|
123
|
+
case val
|
124
|
+
when Array
|
125
|
+
val.collect{ |v| quotea(v) }.join(',')
|
126
|
+
else
|
127
|
+
quote(val)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
alias_method :quotea, :quote_array
|
131
|
+
|
132
|
+
# Apply table name conventions to a class name.
|
133
|
+
|
134
|
+
def tableize(klass)
|
135
|
+
"#{klass.to_s.gsub(/::/, "_").downcase}"
|
136
|
+
end
|
137
|
+
|
138
|
+
# Return the table name for the given class.
|
139
|
+
|
140
|
+
def table(klass)
|
141
|
+
#klass.ann.self[:sql_table] || klass.ann.self[:table] || "#{Og.table_prefix}#{tableize(klass)}"
|
142
|
+
(klass.ann.self[:sql_table] rescue nil) ||
|
143
|
+
(klass.ann.self[:table] rescue nil) ||
|
144
|
+
"#{Og.table_prefix}#{tableize(klass)}"
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
data/lib/og/test/assertions.rb
CHANGED
@@ -141,7 +141,7 @@ module Test::Unit::Assertions
|
|
141
141
|
assert_block(msg) { cookie.value == value }
|
142
142
|
end
|
143
143
|
|
144
|
-
# :section:
|
144
|
+
# :section: Nitro::Template related assertions.
|
145
145
|
|
146
146
|
# :section: Redirection assertions.
|
147
147
|
|
@@ -171,5 +171,3 @@ module Test::Unit::Assertions
|
|
171
171
|
end
|
172
172
|
|
173
173
|
end
|
174
|
-
|
175
|
-
# * George Moschovitis <gm@navel.gr>
|
data/lib/og/test/testcase.rb
CHANGED
data/lib/og/types.rb
CHANGED
@@ -2,11 +2,11 @@ module Og
|
|
2
2
|
|
3
3
|
# Some useful type macros to help when defining properties.
|
4
4
|
# You can easily code your own type macros. Just return the
|
5
|
-
# array that should be passed to the
|
5
|
+
# array that should be passed to the attr_xxx macros.
|
6
6
|
#
|
7
7
|
# === Example
|
8
8
|
#
|
9
|
-
#
|
9
|
+
# attr_accessor :name, VarChar(30)
|
10
10
|
|
11
11
|
def self.VarChar(size)
|
12
12
|
return String, :sql => "VARCHAR(#{size})"
|
@@ -17,6 +17,3 @@ NotNull = { :sql => 'NOT NULL' }.freeze
|
|
17
17
|
Null = { :sql => 'NULL' }.freeze
|
18
18
|
|
19
19
|
end
|
20
|
-
|
21
|
-
# * Michael Neumann <mneumann@ntecs.de>
|
22
|
-
# * George Moschovitis <gm@navel.gr>
|