mondrian-olap 0.4.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/Changelog.md +60 -0
- data/Gemfile +21 -0
- data/LICENSE-Mondrian.html +259 -0
- data/LICENSE.txt +22 -0
- data/README.md +302 -0
- data/RUNNING_TESTS.rdoc +66 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/lib/mondrian-olap.rb +1 -0
- data/lib/mondrian/jars/commons-collections-3.1.jar +0 -0
- data/lib/mondrian/jars/commons-dbcp-1.2.1.jar +0 -0
- data/lib/mondrian/jars/commons-logging-1.0.4.jar +0 -0
- data/lib/mondrian/jars/commons-math-1.0.jar +0 -0
- data/lib/mondrian/jars/commons-pool-1.2.jar +0 -0
- data/lib/mondrian/jars/commons-vfs-1.0.jar +0 -0
- data/lib/mondrian/jars/eigenbase-properties.jar +0 -0
- data/lib/mondrian/jars/eigenbase-resgen.jar +0 -0
- data/lib/mondrian/jars/eigenbase-xom.jar +0 -0
- data/lib/mondrian/jars/javacup.jar +0 -0
- data/lib/mondrian/jars/log4j-1.2.8.jar +0 -0
- data/lib/mondrian/jars/log4j.properties +5 -0
- data/lib/mondrian/jars/mondrian.jar +0 -0
- data/lib/mondrian/jars/olap4j.jar +0 -0
- data/lib/mondrian/olap.rb +17 -0
- data/lib/mondrian/olap/connection.rb +201 -0
- data/lib/mondrian/olap/cube.rb +297 -0
- data/lib/mondrian/olap/error.rb +57 -0
- data/lib/mondrian/olap/query.rb +342 -0
- data/lib/mondrian/olap/result.rb +264 -0
- data/lib/mondrian/olap/schema.rb +378 -0
- data/lib/mondrian/olap/schema_element.rb +153 -0
- data/lib/mondrian/olap/schema_udf.rb +282 -0
- data/mondrian-olap.gemspec +128 -0
- data/spec/connection_role_spec.rb +130 -0
- data/spec/connection_spec.rb +72 -0
- data/spec/cube_spec.rb +318 -0
- data/spec/fixtures/MondrianTest.xml +134 -0
- data/spec/fixtures/MondrianTestOracle.xml +134 -0
- data/spec/mondrian_spec.rb +53 -0
- data/spec/query_spec.rb +807 -0
- data/spec/rake_tasks.rb +260 -0
- data/spec/schema_definition_spec.rb +1249 -0
- data/spec/spec_helper.rb +134 -0
- data/spec/support/matchers/be_like.rb +24 -0
- 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
|