mondrian-olap 0.4.0-java

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 (46) hide show
  1. data/.rspec +2 -0
  2. data/Changelog.md +60 -0
  3. data/Gemfile +21 -0
  4. data/LICENSE-Mondrian.html +259 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +302 -0
  7. data/RUNNING_TESTS.rdoc +66 -0
  8. data/Rakefile +48 -0
  9. data/VERSION +1 -0
  10. data/lib/mondrian-olap.rb +1 -0
  11. data/lib/mondrian/jars/commons-collections-3.1.jar +0 -0
  12. data/lib/mondrian/jars/commons-dbcp-1.2.1.jar +0 -0
  13. data/lib/mondrian/jars/commons-logging-1.0.4.jar +0 -0
  14. data/lib/mondrian/jars/commons-math-1.0.jar +0 -0
  15. data/lib/mondrian/jars/commons-pool-1.2.jar +0 -0
  16. data/lib/mondrian/jars/commons-vfs-1.0.jar +0 -0
  17. data/lib/mondrian/jars/eigenbase-properties.jar +0 -0
  18. data/lib/mondrian/jars/eigenbase-resgen.jar +0 -0
  19. data/lib/mondrian/jars/eigenbase-xom.jar +0 -0
  20. data/lib/mondrian/jars/javacup.jar +0 -0
  21. data/lib/mondrian/jars/log4j-1.2.8.jar +0 -0
  22. data/lib/mondrian/jars/log4j.properties +5 -0
  23. data/lib/mondrian/jars/mondrian.jar +0 -0
  24. data/lib/mondrian/jars/olap4j.jar +0 -0
  25. data/lib/mondrian/olap.rb +17 -0
  26. data/lib/mondrian/olap/connection.rb +201 -0
  27. data/lib/mondrian/olap/cube.rb +297 -0
  28. data/lib/mondrian/olap/error.rb +57 -0
  29. data/lib/mondrian/olap/query.rb +342 -0
  30. data/lib/mondrian/olap/result.rb +264 -0
  31. data/lib/mondrian/olap/schema.rb +378 -0
  32. data/lib/mondrian/olap/schema_element.rb +153 -0
  33. data/lib/mondrian/olap/schema_udf.rb +282 -0
  34. data/mondrian-olap.gemspec +128 -0
  35. data/spec/connection_role_spec.rb +130 -0
  36. data/spec/connection_spec.rb +72 -0
  37. data/spec/cube_spec.rb +318 -0
  38. data/spec/fixtures/MondrianTest.xml +134 -0
  39. data/spec/fixtures/MondrianTestOracle.xml +134 -0
  40. data/spec/mondrian_spec.rb +53 -0
  41. data/spec/query_spec.rb +807 -0
  42. data/spec/rake_tasks.rb +260 -0
  43. data/spec/schema_definition_spec.rb +1249 -0
  44. data/spec/spec_helper.rb +134 -0
  45. data/spec/support/matchers/be_like.rb +24 -0
  46. metadata +278 -0
@@ -0,0 +1,57 @@
1
+ module Mondrian
2
+ module OLAP
3
+
4
+ NATIVE_ERROR_REGEXP = /^(org\.olap4j\.|mondrian\.|java\.lang\.reflect\.UndeclaredThrowableException\: Mondrian Error\:)/
5
+
6
+ class Error < StandardError
7
+ # root_cause will be nil if there is no cause for wrapped native error
8
+ # root_cause_message will have either root_cause message or wrapped native error message
9
+ attr_reader :native_error, :root_cause_message, :root_cause
10
+
11
+ def initialize(native_error)
12
+ @native_error = native_error
13
+ get_root_cause
14
+ super(native_error.message)
15
+ add_root_cause_to_backtrace
16
+ end
17
+
18
+ def self.wrap_native_exception
19
+ yield
20
+ rescue NativeException => e
21
+ if e.message =~ NATIVE_ERROR_REGEXP
22
+ raise Mondrian::OLAP::Error.new(e)
23
+ else
24
+ raise
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def get_root_cause
31
+ @root_cause = nil
32
+ e = @native_error
33
+ while e.respond_to?(:cause) && (cause = e.cause)
34
+ @root_cause = e = cause
35
+ end
36
+ message = e.message
37
+ if message =~ /\AMondrian Error:(.*)\Z/m
38
+ message = $1
39
+ end
40
+ @root_cause_message = message
41
+ end
42
+
43
+ def add_root_cause_to_backtrace
44
+ bt = @native_error.backtrace
45
+ if @root_cause
46
+ root_cause_bt = Array(@root_cause.backtrace)
47
+ root_cause_bt[0,5].reverse.each do |bt_line|
48
+ bt.unshift "root cause: #{bt_line}"
49
+ end
50
+ bt.unshift "root cause: #{@root_cause.java_class.name}: #{@root_cause.message.chomp}"
51
+ end
52
+ set_backtrace bt
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,342 @@
1
+ module Mondrian
2
+ module OLAP
3
+ class Query
4
+ def self.from(connection, cube_name)
5
+ query = self.new(connection)
6
+ query.cube_name = cube_name
7
+ query
8
+ end
9
+
10
+ attr_accessor :cube_name
11
+
12
+ def initialize(connection)
13
+ @connection = connection
14
+ @cube = nil
15
+ @axes = []
16
+ @where = []
17
+ @with = []
18
+ end
19
+
20
+ # Add new axis(i) to query
21
+ # or return array of axis(i) members if no arguments specified
22
+ def axis(i, *axis_members)
23
+ if axis_members.empty?
24
+ @axes[i]
25
+ else
26
+ @axes[i] ||= []
27
+ @current_set = @axes[i]
28
+ if axis_members.length == 1 && axis_members[0].is_a?(Array)
29
+ @current_set.concat(axis_members[0])
30
+ else
31
+ @current_set.concat(axis_members)
32
+ end
33
+ self
34
+ end
35
+ end
36
+
37
+ AXIS_ALIASES = %w(columns rows pages sections chapters)
38
+ AXIS_ALIASES.each_with_index do |axis, i|
39
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
40
+ def #{axis}(*axis_members)
41
+ axis(#{i}, *axis_members)
42
+ end
43
+ RUBY
44
+ end
45
+
46
+ %w(crossjoin nonempty_crossjoin).each do |method|
47
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
48
+ def #{method}(*axis_members)
49
+ raise ArgumentError, "cannot use #{method} method before axis or with_set method" unless @current_set
50
+ raise ArgumentError, "specify list of members for #{method} method" if axis_members.empty?
51
+ members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
52
+ @current_set.replace [:#{method}, @current_set.clone, members]
53
+ self
54
+ end
55
+ RUBY
56
+ end
57
+
58
+ def except(*axis_members)
59
+ raise ArgumentError, "cannot use except method before axis or with_set method" unless @current_set
60
+ raise ArgumentError, "specify list of members for except method" if axis_members.empty?
61
+ members = axis_members.length == 1 && axis_members[0].is_a?(Array) ? axis_members[0] : axis_members
62
+ if [:crossjoin, :nonempty_crossjoin].include? @current_set[0]
63
+ @current_set[2] = [:except, @current_set[2], members]
64
+ else
65
+ @current_set.replace [:except, @current_set.clone, members]
66
+ end
67
+ self
68
+ end
69
+
70
+ def nonempty
71
+ raise ArgumentError, "cannot use nonempty method before axis method" unless @current_set
72
+ @current_set.replace [:nonempty, @current_set.clone]
73
+ self
74
+ end
75
+
76
+ def filter(condition, options={})
77
+ raise ArgumentError, "cannot use filter method before axis or with_set method" unless @current_set
78
+ @current_set.replace [:filter, @current_set.clone, condition]
79
+ @current_set << options[:as] if options[:as]
80
+ self
81
+ end
82
+
83
+ def filter_nonempty
84
+ raise ArgumentError, "cannot use filter_nonempty method before axis or with_set method" unless @current_set
85
+ condition = "NOT ISEMPTY(S.CURRENT)"
86
+ @current_set.replace [:filter, @current_set.clone, condition, 'S']
87
+ self
88
+ end
89
+
90
+ VALID_ORDERS = ['ASC', 'BASC', 'DESC', 'BDESC']
91
+
92
+ def order(expression, direction)
93
+ raise ArgumentError, "cannot use order method before axis or with_set method" unless @current_set
94
+ direction = direction.to_s.upcase
95
+ raise ArgumentError, "invalid order direction #{direction.inspect}," <<
96
+ " should be one of #{VALID_ORDERS.inspect[1..-2]}" unless VALID_ORDERS.include?(direction)
97
+ @current_set.replace [:order, @current_set.clone, expression, direction]
98
+ self
99
+ end
100
+
101
+ %w(top bottom).each do |extreme|
102
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
103
+ def #{extreme}_count(count, expression=nil)
104
+ raise ArgumentError, "cannot use #{extreme}_count method before axis or with_set method" unless @current_set
105
+ @current_set.replace [:#{extreme}_count, @current_set.clone, count]
106
+ @current_set << expression if expression
107
+ self
108
+ end
109
+ RUBY
110
+
111
+ %w(percent sum).each do |extreme_name|
112
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
113
+ def #{extreme}_#{extreme_name}(value, expression)
114
+ raise ArgumentError, "cannot use #{extreme}_#{extreme_name} method before axis or with_set method" unless @current_set
115
+ @current_set.replace [:#{extreme}_#{extreme_name}, @current_set.clone, value, expression]
116
+ self
117
+ end
118
+ RUBY
119
+ end
120
+ end
121
+
122
+ def hierarchize(order=nil, all=nil)
123
+ raise ArgumentError, "cannot use hierarchize method before axis or with_set method" unless @current_set
124
+ order = order && order.to_s.upcase
125
+ raise ArgumentError, "invalid hierarchize order #{order.inspect}" unless order.nil? || order == 'POST'
126
+ if all.nil? && [:crossjoin, :nonempty_crossjoin].include?(@current_set[0])
127
+ @current_set[2] = [:hierarchize, @current_set[2]]
128
+ @current_set[2] << order if order
129
+ else
130
+ @current_set.replace [:hierarchize, @current_set.clone]
131
+ @current_set << order if order
132
+ end
133
+ self
134
+ end
135
+
136
+ def hierarchize_all(order=nil)
137
+ hierarchize(order, :all)
138
+ end
139
+
140
+ # Add new WHERE condition to query
141
+ # or return array of existing conditions if no arguments specified
142
+ def where(*members)
143
+ if members.empty?
144
+ @where
145
+ else
146
+ @current_set = @where
147
+ if members.length == 1 && members[0].is_a?(Array)
148
+ @where.concat(members[0])
149
+ else
150
+ @where.concat(members)
151
+ end
152
+ self
153
+ end
154
+ end
155
+
156
+ # Add definition of calculated member
157
+ def with_member(member_name)
158
+ @with << [:member, member_name]
159
+ @current_set = nil
160
+ self
161
+ end
162
+
163
+ # Add definition of named_set
164
+ def with_set(set_name)
165
+ @current_set = []
166
+ @with << [:set, set_name, @current_set]
167
+ self
168
+ end
169
+
170
+ # return array of member and set definitions
171
+ def with
172
+ @with
173
+ end
174
+
175
+ # Add definition to calculated member or to named set
176
+ def as(*params)
177
+ # definition of named set
178
+ if @current_set
179
+ if params.empty?
180
+ raise ArgumentError, "named set cannot be empty"
181
+ else
182
+ raise ArgumentError, "cannot use 'as' method before with_set method" unless @current_set.empty?
183
+ if params.length == 1 && params[0].is_a?(Array)
184
+ @current_set.concat(params[0])
185
+ else
186
+ @current_set.concat(params)
187
+ end
188
+ end
189
+ # definition of calculated member
190
+ else
191
+ member_definition = @with.last
192
+ options = params.last.is_a?(Hash) ? params.pop : nil
193
+ raise ArgumentError, "cannot use 'as' method before with_member method" unless member_definition &&
194
+ member_definition[0] == :member && member_definition.length == 2
195
+ raise ArgumentError, "calculated member definition should be single expression" unless params.length == 1
196
+ member_definition << params[0]
197
+ member_definition << options if options
198
+ end
199
+ self
200
+ end
201
+
202
+ def to_mdx
203
+ mdx = ""
204
+ mdx << "WITH #{with_to_mdx}\n" unless @with.empty?
205
+ mdx << "SELECT #{axis_to_mdx}\n"
206
+ mdx << "FROM #{from_to_mdx}"
207
+ mdx << "\nWHERE #{where_to_mdx}" unless @where.empty?
208
+ mdx
209
+ end
210
+
211
+ def execute
212
+ Error.wrap_native_exception do
213
+ @connection.execute to_mdx
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ # FIXME: keep original order of WITH MEMBER and WITH SET defitions
220
+ def with_to_mdx
221
+ @with.map do |definition|
222
+ case definition[0]
223
+ when :member
224
+ member_name = definition[1]
225
+ expression = definition[2]
226
+ options = definition[3]
227
+ options_string = ''
228
+ options && options.each do |option, value|
229
+ options_string << ", #{option.to_s.upcase} = #{quote_value(value)}"
230
+ end
231
+ "MEMBER #{member_name} AS #{quote_value(expression)}#{options_string}"
232
+ when :set
233
+ set_name = definition[1]
234
+ set_members = definition[2]
235
+ "SET #{set_name} AS #{quote_value(members_to_mdx(set_members))}"
236
+ end
237
+ end.join("\n")
238
+ end
239
+
240
+ def axis_to_mdx
241
+ mdx = ""
242
+ @axes.each_with_index do |axis_members, i|
243
+ axis_name = AXIS_ALIASES[i] ? AXIS_ALIASES[i].upcase : "AXIS(#{i})"
244
+ mdx << ",\n" if i > 0
245
+ mdx << members_to_mdx(axis_members) << " ON " << axis_name
246
+ end
247
+ mdx
248
+ end
249
+
250
+ MDX_FUNCTIONS = {
251
+ :top_count => 'TOPCOUNT',
252
+ :top_percent => 'TOPPERCENT',
253
+ :top_sum => 'TOPSUM',
254
+ :bottom_count => 'BOTTOMCOUNT',
255
+ :bottom_percent => 'BOTTOMPERCENT',
256
+ :bottom_sum => 'BOTTOMSUM'
257
+ }
258
+
259
+ def members_to_mdx(members)
260
+ # if only one member which does not end with ]
261
+ # then assume it is expression which returns set
262
+ # TODO: maybe always include also single expressions in {...} to avoid some edge cases?
263
+ if members.length == 1 && members[0][-1,1] != ']'
264
+ members[0]
265
+ elsif members[0].is_a?(Symbol)
266
+ case members[0]
267
+ when :crossjoin
268
+ "CROSSJOIN(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
269
+ when :nonempty_crossjoin
270
+ "NONEMPTYCROSSJOIN(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
271
+ when :except
272
+ "EXCEPT(#{members_to_mdx(members[1])}, #{members_to_mdx(members[2])})"
273
+ when :nonempty
274
+ "NON EMPTY #{members_to_mdx(members[1])}"
275
+ when :filter
276
+ as_alias = members[3] ? " AS #{members[3]}" : nil
277
+ "FILTER(#{members_to_mdx(members[1])}#{as_alias}, #{members[2]})"
278
+ when :order
279
+ "ORDER(#{members_to_mdx(members[1])}, #{expression_to_mdx(members[2])}, #{members[3]})"
280
+ when :top_count, :bottom_count
281
+ mdx = "#{MDX_FUNCTIONS[members[0]]}(#{members_to_mdx(members[1])}, #{members[2]}"
282
+ mdx << (members[3] ? ", #{expression_to_mdx(members[3])})" : ")")
283
+ when :top_percent, :top_sum, :bottom_percent, :bottom_sum
284
+ "#{MDX_FUNCTIONS[members[0]]}(#{members_to_mdx(members[1])}, #{members[2]}, #{expression_to_mdx(members[3])})"
285
+ when :hierarchize
286
+ "HIERARCHIZE(#{members_to_mdx(members[1])}#{members[2] && ", #{members[2]}"})"
287
+ else
288
+ raise ArgumentError, "Cannot generate MDX for invalid set operation #{members[0].inspect}"
289
+ end
290
+ else
291
+ "{#{members.join(', ')}}"
292
+ end
293
+ end
294
+
295
+ def expression_to_mdx(expression)
296
+ expression.is_a?(Array) ? "(#{expression.join(', ')})" : expression
297
+ end
298
+
299
+ def from_to_mdx
300
+ "[#{@cube_name}]"
301
+ end
302
+
303
+ def where_to_mdx
304
+ # generate set MDX expression
305
+ if @where[0].is_a?(Symbol) ||
306
+ @where.length > 1 && @where.map{|full_name| extract_dimension_name(full_name)}.uniq.length == 1
307
+ members_to_mdx(@where)
308
+ # generate tuple MDX expression
309
+ else
310
+ where_to_mdx_tuple
311
+ end
312
+ end
313
+
314
+ def where_to_mdx_tuple
315
+ mdx = '('
316
+ mdx << @where.map do |condition|
317
+ condition
318
+ end.join(', ')
319
+ mdx << ')'
320
+ end
321
+
322
+ def quote_value(value)
323
+ case value
324
+ when String
325
+ "'#{value.gsub("'", "''")}'"
326
+ when TrueClass, FalseClass
327
+ value ? 'TRUE' : 'FALSE'
328
+ when NilClass
329
+ 'NULL'
330
+ else
331
+ "#{value}"
332
+ end
333
+ end
334
+
335
+ def extract_dimension_name(full_name)
336
+ if full_name =~ /\A[^\[]*\[([^\]]+)\]/
337
+ $1
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,264 @@
1
+ require 'nokogiri'
2
+ require 'bigdecimal'
3
+
4
+ module Mondrian
5
+ module OLAP
6
+ class Result
7
+ def initialize(connection, raw_cell_set)
8
+ @connection = connection
9
+ @raw_cell_set = raw_cell_set
10
+ end
11
+
12
+ def axes_count
13
+ axes.length
14
+ end
15
+
16
+ def axis_names
17
+ @axis_names ||= axis_positions(:getName)
18
+ end
19
+
20
+ def axis_full_names
21
+ @axis_full_names ||= axis_positions(:getUniqueName)
22
+ end
23
+
24
+ def axis_members
25
+ @axis_members ||= axis_positions(:to_member)
26
+ end
27
+
28
+ AXIS_SYMBOLS = [:column, :row, :page, :section, :chapter]
29
+ AXIS_SYMBOLS.each_with_index do |axis, i|
30
+ define_method :"#{axis}_names" do
31
+ axis_names[i]
32
+ end
33
+
34
+ define_method :"#{axis}_full_names" do
35
+ axis_full_names[i]
36
+ end
37
+
38
+ define_method :"#{axis}_members" do
39
+ axis_members[i]
40
+ end
41
+ end
42
+
43
+ def values(*axes_sequence)
44
+ values_using(:getValue, axes_sequence)
45
+ end
46
+
47
+ def formatted_values(*axes_sequence)
48
+ values_using(:getFormattedValue, axes_sequence)
49
+ end
50
+
51
+ def values_using(values_method, axes_sequence = [])
52
+ if axes_sequence.empty?
53
+ axes_sequence = (0...axes_count).to_a.reverse
54
+ elsif axes_sequence.size != axes_count
55
+ raise ArgumentError, "axes sequence size is not equal to result axes count"
56
+ end
57
+ recursive_values(values_method, axes_sequence, 0)
58
+ end
59
+
60
+ # format results in simple HTML table
61
+ def to_html(options = {})
62
+ case axes_count
63
+ when 1
64
+ builder = Nokogiri::XML::Builder.new do |doc|
65
+ doc.table do
66
+ doc.tr do
67
+ column_full_names.each do |column_full_name|
68
+ column_full_name = column_full_name.join(',') if column_full_name.is_a?(Array)
69
+ doc.th column_full_name, :align => 'right'
70
+ end
71
+ end
72
+ doc.tr do
73
+ (options[:formatted] ? formatted_values : values).each do |value|
74
+ doc.td value, :align => 'right'
75
+ end
76
+ end
77
+ end
78
+ end
79
+ builder.doc.to_html
80
+ when 2
81
+ builder = Nokogiri::XML::Builder.new do |doc|
82
+ doc.table do
83
+ doc.tr do
84
+ doc.th
85
+ column_full_names.each do |column_full_name|
86
+ column_full_name = column_full_name.join(',') if column_full_name.is_a?(Array)
87
+ doc.th column_full_name, :align => 'right'
88
+ end
89
+ end
90
+ (options[:formatted] ? formatted_values : values).each_with_index do |row, i|
91
+ doc.tr do
92
+ row_full_name = row_full_names[i].is_a?(Array) ? row_full_names[i].join(',') : row_full_names[i]
93
+ doc.th row_full_name, :align => 'left'
94
+ row.each do |cell|
95
+ doc.td cell, :align => 'right'
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ builder.doc.to_html
102
+ else
103
+ raise ArgumentError, "just columns and rows axes are supported"
104
+ end
105
+ end
106
+
107
+ # specify drill through cell position, for example, as
108
+ # :row => 0, :cell => 1
109
+ # specify max returned rows with :max_rows parameter
110
+ def drill_through(position_params = {})
111
+ Error.wrap_native_exception do
112
+ cell_params = []
113
+ axes_count.times do |i|
114
+ axis_symbol = AXIS_SYMBOLS[i]
115
+ raise ArgumentError, "missing position #{axis_symbol.inspect}" unless axis_position = position_params[axis_symbol]
116
+ cell_params << Java::JavaLang::Integer.new(axis_position)
117
+ end
118
+ raw_cell = @raw_cell_set.getCell(cell_params)
119
+ DrillThrough.from_raw_cell(raw_cell, position_params)
120
+ end
121
+ end
122
+
123
+ class DrillThrough
124
+ def self.from_raw_cell(raw_cell, params = {})
125
+ max_rows = params[:max_rows] || -1
126
+ # workaround to avoid calling raw_cell.drillThroughInternal private method
127
+ # which fails when running inside TorqueBox
128
+ cell_field = raw_cell.java_class.declared_field('cell')
129
+ cell_field.accessible = true
130
+ rolap_cell = cell_field.value(raw_cell)
131
+ if rolap_cell.canDrillThrough
132
+ sql_statement = rolap_cell.drillThroughInternal(max_rows, -1, nil, true, nil)
133
+ raw_result_set = sql_statement.getWrappedResultSet
134
+ new(raw_result_set)
135
+ end
136
+ end
137
+
138
+ def initialize(raw_result_set)
139
+ @raw_result_set = raw_result_set
140
+ end
141
+
142
+ def column_types
143
+ @column_types ||= (1..metadata.getColumnCount).map{|i| metadata.getColumnTypeName(i).to_sym}
144
+ end
145
+
146
+ def column_names
147
+ @column_names ||= begin
148
+ # if PostgreSQL then use getBaseColumnName as getColumnName returns empty string
149
+ if metadata.respond_to?(:getBaseColumnName)
150
+ (1..metadata.getColumnCount).map{|i| metadata.getBaseColumnName(i)}
151
+ else
152
+ (1..metadata.getColumnCount).map{|i| metadata.getColumnName(i)}
153
+ end
154
+ end
155
+ end
156
+
157
+ def table_names
158
+ @table_names ||= begin
159
+ # if PostgreSQL then use getBaseTableName as getTableName returns empty string
160
+ if metadata.respond_to?(:getBaseTableName)
161
+ (1..metadata.getColumnCount).map{|i| metadata.getBaseTableName(i)}
162
+ else
163
+ (1..metadata.getColumnCount).map{|i| metadata.getTableName(i)}
164
+ end
165
+ end
166
+ end
167
+
168
+ def column_labels
169
+ @column_labels ||= (1..metadata.getColumnCount).map{|i| metadata.getColumnLabel(i)}
170
+ end
171
+
172
+ def fetch
173
+ if @raw_result_set.next
174
+ row_values = []
175
+ column_types.each_with_index do |column_type, i|
176
+ row_values << Result.java_to_ruby_value(@raw_result_set.getObject(i+1), column_type)
177
+ end
178
+ row_values
179
+ else
180
+ @raw_result_set.close
181
+ nil
182
+ end
183
+ end
184
+
185
+ def rows
186
+ @rows ||= begin
187
+ rows_values = []
188
+ while row_values = fetch
189
+ rows_values << row_values
190
+ end
191
+ rows_values
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ def metadata
198
+ @metadata ||= @raw_result_set.getMetaData
199
+ end
200
+
201
+ end
202
+
203
+ def self.java_to_ruby_value(value, column_type = nil)
204
+ case value
205
+ when Numeric, String
206
+ value
207
+ when Java::JavaMath::BigDecimal
208
+ BigDecimal(value.to_s)
209
+ else
210
+ value
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ def axes
217
+ @axes ||= @raw_cell_set.getAxes
218
+ end
219
+
220
+ def axis_positions(map_method, join_with=false)
221
+ axes.map do |axis|
222
+ axis.getPositions.map do |position|
223
+ names = position.getMembers.map do |member|
224
+ if map_method == :to_member
225
+ Member.new(member)
226
+ else
227
+ member.send(map_method)
228
+ end
229
+ end
230
+ if names.size == 1
231
+ names[0]
232
+ elsif join_with
233
+ names.join(join_with)
234
+ else
235
+ names
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ AXIS_SYMBOL_TO_NUMBER = {
242
+ :columns => 0,
243
+ :rows => 1,
244
+ :pages => 2,
245
+ :sections => 3,
246
+ :chapters => 4
247
+ }.freeze
248
+
249
+ def recursive_values(value_method, axes_sequence, current_index, cell_params=[])
250
+ if axis_number = axes_sequence[current_index]
251
+ axis_number = AXIS_SYMBOL_TO_NUMBER[axis_number] if axis_number.is_a?(Symbol)
252
+ positions_size = axes[axis_number].getPositions.size
253
+ (0...positions_size).map do |i|
254
+ cell_params[axis_number] = Java::JavaLang::Integer.new(i)
255
+ recursive_values(value_method, axes_sequence, current_index + 1, cell_params)
256
+ end
257
+ else
258
+ self.class.java_to_ruby_value(@raw_cell_set.getCell(cell_params).send(value_method))
259
+ end
260
+ end
261
+
262
+ end
263
+ end
264
+ end