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.
- 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
|