mondrian-rest 0.7.9-java → 1.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbd9d6692bb827f1b325d682e9df7872a83be6a5085b0af4ceb41cc4194b59d7
4
- data.tar.gz: 8e1d250c08c9c88adec227560d816ee79afe1f6c236f0ab31fa5574e981d3d81
3
+ metadata.gz: 2cfb2bb9ca02faf64e851332d271430bed71fcda750ae4f0b46e71cacb8d4c72
4
+ data.tar.gz: 82aa47f20dabcfb4a8deff1e8636f9a8570b667956baf90db522351d6c84a0c7
5
5
  SHA512:
6
- metadata.gz: cc64f9b2911c3e3e7b4142ec91a2796a83190e4abb00a0cb340b0a841d0f706ff43aade8dad0c833e54273b9a4672cf7f2a53223281e4febf9e92fd15bbdaadb
7
- data.tar.gz: 55e965893aec267dd80d1acac550d0b9b85443e36485357468c1c27dcc3e7851205dfef6bc0afcc2b0025786b3a2f07f1448da254cb2c3c826f763724e5c32bb
6
+ metadata.gz: b340ebdde3e89747453ecbbdab83e8c112115a240799778f680cb575359c900ad22b43711ab2f93e402f42dacbd14d6883d612887b11fe103f40e9035b13b60d
7
+ data.tar.gz: a2f4252ee653683f7e39df31f270f052c944b264418c69902c4c7dffec662e6bf3e667936793501cbf6b6e65e64f9278f24e2c956fcf915e2d1c99532fb6df54
data/Rakefile CHANGED
@@ -13,8 +13,8 @@ desc "API Routes"
13
13
  task :routes do
14
14
  require_relative './lib/mondrian_rest.rb'
15
15
  Mondrian::REST::Api.routes.each do |api|
16
- method = api.route_method.ljust(10)
17
- path = api.route_path
16
+ method = api.request_method.ljust(10)
17
+ path = api.path
18
18
  puts " #{method} #{path}"
19
19
  end
20
20
  end
data/lib/mondrian_rest.rb CHANGED
@@ -2,10 +2,10 @@ require 'json'
2
2
 
3
3
  Java::JavaLang::System.setProperty("jdbc.driver.autoload", "true")
4
4
 
5
- require_relative './jars/mondrian-rest_jars.rb'
6
-
7
5
  require 'mondrian-olap'
8
6
  require 'grape'
7
+ require 'active_support'
8
+ require 'active_support/core_ext/enumerable'
9
9
 
10
10
  require_relative './mondrian_rest/nest.rb'
11
11
  require_relative './mondrian_rest/api.rb'
@@ -6,7 +6,6 @@ require_relative './query_helper.rb'
6
6
  require_relative './api_formatters.rb'
7
7
 
8
8
  module Mondrian::REST
9
-
10
9
  class Api < Grape::API
11
10
  version '1', using: :header, vendor: 'mondrian_rest'
12
11
  default_format :json
@@ -20,26 +19,26 @@ module Mondrian::REST
20
19
  end
21
20
 
22
21
  resource :mdx do
23
- content_type :xls, "application/vnd.ms-excel"
22
+ content_type :xls, 'application/vnd.ms-excel'
24
23
  formatter :xls, Mondrian::REST::Formatters::XLS
25
24
 
26
- content_type :csv, "text/csv"
25
+ content_type :csv, 'text/csv'
27
26
  formatter :csv, Mondrian::REST::Formatters::CSV
28
27
 
29
- content_type :json, "application/json"
28
+ content_type :json, 'application/json'
30
29
  formatter :json, Mondrian::REST::Formatters::AggregationJSON
31
30
 
32
- content_type :jsonrecords, "application/x-jsonrecords"
31
+ content_type :jsonrecords, 'application/x-jsonrecords'
33
32
  formatter :jsonrecords, Mondrian::REST::Formatters::JSONRecords
34
33
 
35
- desc "Execute an MDX query against a cube"
36
- content_type :txt, "text/plain"
34
+ desc 'Execute an MDX query against a cube'
35
+ content_type :txt, 'text/plain'
37
36
 
38
37
  params do
39
38
  optional :parents, type: Boolean, desc: "Include members' ancestors"
40
- optional :debug, type: Boolean, desc: "Include generated MDX", default: false
41
- optional :properties, type: Array, desc: "Include member properties"
42
- optional :caption, type: Array, desc: "Replace caption with property", default: []
39
+ optional :debug, type: Boolean, desc: 'Include generated MDX', default: false
40
+ optional :properties, type: Array, desc: 'Include member properties'
41
+ optional :caption, type: Array, desc: 'Replace caption with property', default: []
43
42
  end
44
43
 
45
44
  post do
@@ -52,17 +51,17 @@ module Mondrian::REST
52
51
 
53
52
  resource :flush do
54
53
  params do
55
- requires :secret, type: String, desc: "Secret key"
54
+ requires :secret, type: String, desc: 'Secret key'
56
55
  end
57
- content_type :json, "application/json"
58
- desc "Flush the schema cache"
56
+ content_type :json, 'application/json'
57
+ desc 'Flush the schema cache'
59
58
 
60
59
  get do
61
60
  if ENV['MONDRIAN_REST_SECRET'].nil?
62
- error!("Please set MONDRIAN_REST_SECRET to use this endpoint", 403)
61
+ error!('Please set MONDRIAN_REST_SECRET to use this endpoint', 403)
63
62
  end
64
63
  if params[:secret] != ENV['MONDRIAN_REST_SECRET']
65
- error!("Invalid secret key.", 403)
64
+ error!('Invalid secret key.', 403)
66
65
  end
67
66
  {
68
67
  'status' => olap_flush
@@ -71,7 +70,7 @@ module Mondrian::REST
71
70
  end
72
71
 
73
72
  resource :cubes do
74
- content_type :json, "application/json"
73
+ content_type :json, 'application/json'
75
74
  default_format :json
76
75
  desc "Returns the cubes defined in this server's schema"
77
76
  get do
@@ -81,9 +80,9 @@ module Mondrian::REST
81
80
  end
82
81
 
83
82
  route_param :cube_name do
84
- desc "Return a cube"
83
+ desc 'Return a cube'
85
84
  params do
86
- requires :cube_name, type: String, desc: "Cube name"
85
+ requires :cube_name, type: String, desc: 'Cube name'
87
86
  end
88
87
 
89
88
  get do
@@ -92,7 +91,7 @@ module Mondrian::REST
92
91
  end
93
92
 
94
93
  resource :members do
95
- desc "return a member by its full name"
94
+ desc 'return a member by its full name'
96
95
  params do
97
96
  requires :full_name,
98
97
  type: String,
@@ -106,46 +105,47 @@ module Mondrian::REST
106
105
  if m.nil?
107
106
  error!("Member `#{member_full_name}` not found in cube `#{params[:cube_name]}`", 404)
108
107
  end
109
- m.to_h.merge({
110
- ancestors: m.ancestors.map(&:to_h),
111
- dimension: m.dimension_info
112
- })
108
+ m.to_h.merge(
109
+ ancestors: m.ancestors.map(&:to_h),
110
+ dimension: m.dimension_info
111
+ )
113
112
  end
114
113
  end
115
114
 
116
-
117
115
  resource :aggregate do
118
- content_type :xls, "application/vnd.ms-excel"
116
+ content_type :xls, 'application/vnd.ms-excel'
119
117
  formatter :xls, Mondrian::REST::Formatters::XLS
120
118
 
121
- content_type :csv, "text/csv"
119
+ content_type :csv, 'text/csv'
122
120
  formatter :csv, Mondrian::REST::Formatters::CSV
123
121
 
124
- content_type :json, "application/json"
122
+ content_type :json, 'application/json'
125
123
  formatter :json, Mondrian::REST::Formatters::AggregationJSON
126
124
 
127
- content_type :jsonrecords, "application/x-jsonrecords"
125
+ content_type :jsonrecords, 'application/x-jsonrecords'
128
126
  formatter :jsonrecords, Mondrian::REST::Formatters::JSONRecords
129
127
 
130
- content_type :jsonstat, "application/x-jsonstat"
131
- formatter :jsonstat, Mondrian::REST::Formatters::JSONStat
132
-
133
128
  rescue_from PropertyError do |e|
134
- error!({error: e}, 400)
129
+ error!({ error: e }, 400)
135
130
  end
136
131
 
137
- desc "aggregate from query parameters"
132
+ desc 'aggregate from query parameters'
138
133
  params do
139
134
  optional :measures, type: Array
140
- optional :cut, type: Array, desc: "Specification of slicer axis"
141
- optional :drilldown, type: Array, desc: "Dimension(s) to be drilled down"
142
- optional :nonempty, type: Boolean, desc: "Only return non empty cells"
143
- optional :sparse, type: Boolean, desc: "Skip rows where all measures are null (only applies to CSV, XLS and JSONRECORDS)", default: !java.lang.System.getProperty('mondrian-rest.sparseDefault').nil?
144
- optional :distinct, type: Boolean, desc: "Apply DISTINCT() to every axis"
135
+ optional :cut, type: Array, desc: 'Specification of slicer axis'
136
+ optional :drilldown, type: Array, desc: 'Dimension(s) to be drilled down'
137
+ optional :nonempty, type: Boolean, desc: 'Only return non empty cells'
138
+ optional :sparse, type: Boolean, desc: 'Skip rows where all measures are null (only applies to CSV, XLS and JSONRECORDS)', default: !java.lang.System.getProperty('mondrian-rest.sparseDefault').nil?
139
+ optional :distinct, type: Boolean, desc: 'Apply DISTINCT() to every axis'
145
140
  optional :parents, type: Boolean, desc: "Include members' ancestors"
146
- optional :debug, type: Boolean, desc: "Include generated MDX", default: false
147
- optional :properties, type: Array, desc: "Include member properties"
148
- optional :caption, type: Array, desc: "Replace caption with property", default: []
141
+ optional :debug, type: Boolean, desc: 'Include generated MDX', default: false
142
+ optional :properties, type: Array, desc: 'Include member properties'
143
+ optional :caption, type: Array, desc: 'Replace caption with property', default: []
144
+ optional :filter, type: Array, desc: "Filter by measure value. Accepts: #{Mondrian::REST::QueryHelper::VALID_FILTER_OPS.join(', ')}"
145
+ optional :order, type: String, desc: 'Sort by measure or property name'
146
+ optional :order_desc, type: Boolean, desc: 'Sort direction (true is descending)', default: false
147
+ optional :offset, type: Integer, desc: 'Offset this resultset'
148
+ optional :limit, type: Integer, desc: 'Limit number of results'
149
149
  end
150
150
 
151
151
  get do
@@ -161,8 +161,8 @@ module Mondrian::REST
161
161
  route_param :dimension_name do
162
162
  desc "Return a dimension's members"
163
163
  params do
164
- requires :cube_name, type: String, desc: "Cube name"
165
- requires :dimension_name, type: String, desc: "Dimension name"
164
+ requires :cube_name, type: String, desc: 'Cube name'
165
+ requires :dimension_name, type: String, desc: 'Dimension name'
166
166
  end
167
167
 
168
168
  get do
@@ -171,33 +171,37 @@ module Mondrian::REST
171
171
  dimension.to_h(get_members: true)
172
172
  end
173
173
 
174
+ resource :hierarchies do
175
+ route_param :hierarchy_name do
176
+ resource :levels do
177
+ route_param :level_name do
178
+ resource :members do
179
+ params do
180
+ optional :member_properties, type: Array, default: []
181
+ optional :caption, type: String, desc: 'Replace caption with property', default: nil
182
+ optional :children, type: Boolean, default: false
183
+ end
184
+
185
+ get do
186
+ get_members(params)
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+
174
194
  resource :levels do
175
195
  route_param :level_name do
176
196
  resource :members do
177
-
178
197
  params do
179
198
  optional :member_properties, type: Array, default: []
180
- optional :caption, type: String, desc: "Replace caption with property", default: nil
199
+ optional :caption, type: String, desc: 'Replace caption with property', default: nil
181
200
  optional :children, type: Boolean, default: false
182
201
  end
183
202
 
184
203
  get do
185
- cube = get_cube_or_404(params[:cube_name])
186
-
187
- dimension = cube.dimension(params[:dimension_name])
188
- if dimension.nil?
189
- error!("dimension #{params[:dimension_name]} not found in cube #{params[:cube_name]}", 404)
190
- end
191
-
192
- level = dimension.hierarchies[0].level(params[:level_name])
193
- if level.nil?
194
- error!("level #{params[:level_name]} not found in dimension #{params[:dimension_name]}")
195
- end
196
-
197
- level.to_h(member_properties: params[:member_properties],
198
- get_children: params[:children],
199
- member_caption: params[:caption],
200
- get_members: true)
204
+ get_members(params)
201
205
  end
202
206
 
203
207
  route_param :member_key,
@@ -205,7 +209,7 @@ module Mondrian::REST
205
209
  requirements: { member_key: /[A-Za-z0-9\.\-\s%]+/i } do
206
210
 
207
211
  params do
208
- optional :caption, type: String, desc: "Replace caption with property", default: nil
212
+ optional :caption, type: String, desc: 'Replace caption with property', default: nil
209
213
  optional :member_properties, type: Array, default: []
210
214
  optional :children, type: Boolean, default: false
211
215
  end
@@ -215,13 +219,13 @@ module Mondrian::REST
215
219
  dimension = cube.dimension(params[:dimension_name])
216
220
  level = dimension.hierarchies[0].level(params[:level_name])
217
221
 
218
- member = level.members.detect { |m|
222
+ member = level.members.detect do |m|
219
223
  m.property_value('MEMBER_KEY').to_s == params[:member_key]
220
- }
224
+ end
221
225
  error!('member not found', 404) if member.nil?
222
226
  member
223
227
  .to_h(params[:member_properties], params[:caption], params[:children])
224
- .merge({ancestors: member.ancestors.map(&:to_h)})
228
+ .merge(ancestors: member.ancestors.map(&:to_h))
225
229
  end
226
230
  end
227
231
  end
@@ -2,29 +2,27 @@ require_relative './formatters/aggregation_json'
2
2
  require_relative './formatters/csv'
3
3
  require_relative './formatters/excel'
4
4
  require_relative './formatters/jsonrecords'
5
- require_relative './formatters/jsonstat'
6
-
7
5
 
8
6
  module Mondrian::REST::Formatters
9
-
10
7
  ##
11
8
  # Generate 'tidy data' (http://vita.had.co.nz/papers/tidy-data.pdf)
12
9
  # from a result set.
13
10
  def self.tidy(result, options)
14
-
15
11
  cube = result.cube
16
12
 
17
13
  add_parents = options[:add_parents]
18
14
  properties = options[:properties]
19
- sparse = options[:sparse]
20
15
  rs = result.to_h(add_parents, options[:debug])
21
16
 
22
- if rs[:values].empty?
23
- return []
24
- end
17
+ return [] if rs[:values].empty?
25
18
 
26
19
  measures = rs[:axes].first[:members]
27
- dimensions = rs[:axis_dimensions][1..-1]
20
+ dimensions = rs[:axes][1..-1]
21
+
22
+ indexed_members = rs[:axes].map do |ax|
23
+ ax[:members].index_by { |m| m[:key] }
24
+ end
25
+
28
26
  columns = []
29
27
  slices = []
30
28
  level_has_all = []
@@ -33,8 +31,7 @@ module Mondrian::REST::Formatters
33
31
  dimensions.each do |dd|
34
32
  if add_parents
35
33
  hier = cube.dimension(dd[:name])
36
- .hierarchies
37
- .first # TODO: Support other hierarchies
34
+ .hierarchy(dd[:hierarchy])
38
35
 
39
36
  level_has_all << hier.has_all?
40
37
  slices << dd[:level_depth]
@@ -51,71 +48,54 @@ module Mondrian::REST::Formatters
51
48
  end
52
49
 
53
50
  props = Mondrian::REST::APIHelpers.parse_properties(properties, dimensions)
54
- pnames = properties.map { |p|
51
+ pnames = properties.map do |p|
55
52
  org.olap4j.mdx.IdentifierNode.parseIdentifier(p).getSegmentList.last.name
56
- }
53
+ end
57
54
 
58
55
  # append properties and measure columns and yield table header
59
56
  y.yield columns + pnames + pluck(measures, :name)
60
57
 
61
- prod = rs[:axes][1..-1].map { |e|
62
- e[:members].map.with_index { |e_, i| [e_,i] }
63
- }
64
- values = rs[:values]
65
-
66
- prod.shift.product(*prod).each do |cell|
67
- cidxs = cell.map { |c,i| i }.reverse
58
+ rs[:cell_keys].each_with_index do |row, i|
59
+ cm = row.each_with_index.map { |member_key, i| indexed_members[i + 1][member_key] }
60
+ msrs = rs[:values][i]
68
61
 
69
- cm = cell.map(&:first)
70
-
71
- msrs = measures.map.with_index { |m, mi|
72
- (cidxs + [mi]).reduce(values) { |_, idx| _[idx] }
73
- }
74
-
75
- next if sparse && msrs.all?(&:nil?)
76
-
77
- if add_parents
78
- vdim = cm.each.with_index.reduce([]) { |cnames, (member, j)|
79
- member[:ancestors][0...slices[j] - (level_has_all[j] ? 1 : 0)].reverse.each { |ancestor|
62
+ if !add_parents
63
+ y.yield pluck(cm, :key).zip(pluck(cm, :caption)).flatten \
64
+ + get_props(cm, pnames, props, dimensions) \
65
+ + msrs
66
+ else
67
+ vdim = cm.each.with_index.reduce([]) do |cnames, (member, j)|
68
+ member[:ancestors][0...slices[j] - (level_has_all[j] ? 1 : 0)].reverse.each do |ancestor|
80
69
  cnames += [ancestor[:key], ancestor[:caption]]
81
- }
70
+ end
82
71
  cnames += [member[:key], member[:caption]]
83
- }
72
+ end
84
73
 
85
74
  y.yield vdim + get_props(cm, pnames, props, dimensions) + msrs
86
- else
87
- row = pluck(cm, :key)
88
- .zip(pluck(cm, :caption))
89
- .flatten
90
-
91
- y.yield row + get_props(cm, pnames, props, dimensions) + msrs
92
75
  end
93
76
  end
94
77
  end
95
78
  end
96
79
 
97
80
  def self.get_props(cm, pnames, props, dimensions)
98
- pvalues = cm.each.with_index.reduce({}) do |h, (member, ax_i)|
81
+ pvalues = cm.each.with_index.each_with_object({}) do |(member, ax_i), h|
99
82
  dname = dimensions[ax_i][:name]
100
- if props[dname] # are there properties requested for members of this dimension?
101
- mmbr_lvl = dimensions[ax_i][:level]
102
- (props[dname][mmbr_lvl] || []).each { |p|
103
- h[p] = member[:properties][p]
104
- }
105
- if member[:ancestors]
106
- props[dname]
107
- .select { |k, _| k != mmbr_lvl } # levels other than member's own
108
- .each { |l, p|
109
- p.each # get all requested props for this level's ancestor
110
- .with_object(member[:ancestors].find { |anc|
111
- anc[:level_name] == l
112
- }) { |prop, anc|
113
- h[prop] = anc[:properties][prop]
114
- }
115
- }
83
+ next unless props[dname] # are there properties requested for members of this dimension?
84
+ mmbr_lvl = dimensions[ax_i][:level]
85
+ (props[dname][mmbr_lvl] || []).each do |p|
86
+ h[p] = member[:properties][p]
87
+ end
88
+ next unless member[:ancestors]
89
+ props[dname]
90
+ .reject { |k, _| k == mmbr_lvl } # levels other than member's own
91
+ .each do |l, p|
92
+ p.each # get all requested props for this level's ancestor
93
+ .with_object(member[:ancestors].find do |anc|
94
+ anc[:level_name] == l
95
+ end) do |prop, anc|
96
+ h[prop] = anc[:properties][prop]
116
97
  end
117
98
  end
118
- h
119
99
  end # reduce
120
100
  pnames.map { |pn| pvalues[pn] }
121
101
  end
@@ -4,7 +4,6 @@ module Mondrian::REST
4
4
  end
5
5
 
6
6
  module APIHelpers
7
-
8
7
  @@olap = nil
9
8
  @@mdx_parser = nil
10
9
 
@@ -42,44 +41,66 @@ module Mondrian::REST
42
41
 
43
42
  def mdx(query)
44
43
  logger.info("Executing MDX query #{query}")
45
- begin
46
- result = olap.execute query
47
- result.mdx = query if params[:debug]
48
- result.properties = params[:properties]
49
- result.caption_properties = params[:caption]
50
- result.cube = Mondrian::OLAP::Cube.new(olap,
51
- olap.raw_connection.prepareOlapStatement(query).getCube)
52
- return result
53
- rescue Mondrian::OLAP::Error => st
54
- error!({error: st.backtrace}, 400)
55
- end
44
+
45
+ result = olap.execute query
46
+ result.mdx = query if params[:debug]
47
+ result.properties = params[:properties]
48
+ result.caption_properties = params[:caption]
49
+ result.cube = Mondrian::OLAP::Cube.new(olap,
50
+ olap.raw_connection.prepareOlapStatement(query).getCube)
51
+ result
52
+ rescue Mondrian::OLAP::Error => st
53
+ error!({ error: st.backtrace }, 400)
56
54
  end
57
55
 
58
56
  def run_from_params(params)
59
57
  cube = get_cube_or_404(params[:cube_name])
60
58
  query = build_query(cube, params)
61
- mdx_query = query.to_mdx
62
59
 
63
60
  result = mdx(query.to_mdx)
64
61
  result.cube = cube
65
62
  result
66
63
  end
67
64
 
65
+ def get_members(params)
66
+ cube = get_cube_or_404(params[:cube_name])
67
+
68
+ dimension = cube.dimension(params[:dimension_name])
69
+ if dimension.nil?
70
+ error!("dimension #{params[:dimension_name]} not found in cube #{params[:cube_name]}", 404)
71
+ end
72
+
73
+ hier = unless params[:hierarchy_name].nil?
74
+ h = dimension.hierarchy(params[:hierarchy_name])
75
+ error!("Hierarchy #{params[:hierarchy_name]} does not exist in dimension #{params[:dimension_name]}", 404) if h.nil?
76
+ h
77
+ else
78
+ dimension.hierarchies.first
79
+ end
80
+
81
+ level = hier.level(params[:level_name])
82
+ if level.nil?
83
+ error!("level #{params[:level_name]} not found in dimension #{params[:dimension_name]}")
84
+ end
85
+
86
+ level.to_h(member_properties: params[:member_properties],
87
+ get_children: params[:children],
88
+ member_caption: params[:caption],
89
+ get_members: true)
90
+ end
91
+
68
92
  NEST = Mondrian::REST::Nest.new
69
- .key { |d| d[0] }
70
- .key { |d| d[1] }
93
+ .key { |d| d[0] }
94
+ .key { |d| d[1] }.freeze
71
95
 
72
96
  def self.parse_caption_properties(cprops)
73
- if cprops.nil? or cprops.size < 1
74
- return {}
75
- end
97
+ return {} if cprops.nil? || cprops.empty?
76
98
 
77
- NEST.map(cprops.map { |cp|
99
+ NEST.map(cprops.map do |cp|
78
100
  names = org.olap4j.mdx.IdentifierNode.parseIdentifier(cp).getSegmentList.to_a.map(&:name)
79
101
  # IF prop in Dim.Hier.Lvl.Prop format, skip names[1]
80
102
  names.size == 4 ? [names[0], names[2], names[3]] : names
81
- })
82
-
103
+ end)
83
104
  end
84
105
 
85
106
  ##
@@ -9,6 +9,29 @@ module Mondrian
9
9
  raw_cube.getSets
10
10
  end
11
11
 
12
+ def measure(name)
13
+ self.dimension('Measures')
14
+ .hierarchy
15
+ .levels
16
+ .first
17
+ .members
18
+ .detect { |m| m.name == name }
19
+ end
20
+
21
+ def valid_measure?(name)
22
+ !self.measure(name).nil?
23
+ end
24
+
25
+ def level(*parts)
26
+ dim = self.dimension(parts[0])
27
+ hier = if parts.size > 2
28
+ dim.hierarchy(parts[1])
29
+ else
30
+ dim.hierarchies[0]
31
+ end
32
+ hier.level(parts.last)
33
+ end
34
+
12
35
  def to_h
13
36
  # gather named sets
14
37
  named_sets = self.named_sets
@@ -128,6 +151,10 @@ module Mondrian
128
151
  }
129
152
  end
130
153
 
154
+ def property(name)
155
+ self.raw_level.getProperties.asMap[name]
156
+ end
157
+
131
158
  end
132
159
 
133
160
  class Member
@@ -171,13 +198,15 @@ module Mondrian
171
198
  def dimension_info
172
199
  d = @raw_member.getDimension()
173
200
  l = @raw_member.getLevel()
201
+ h = l.getHierarchy()
174
202
 
175
203
  x = {
176
- :name => d.getName,
177
- :caption => d.getCaption,
178
- :type => self.dimension_type,
179
- :level => l.getCaption,
180
- :level_depth => l.depth
204
+ name: d.getName,
205
+ caption: d.getCaption,
206
+ type: self.dimension_type,
207
+ level: l.getCaption,
208
+ level_depth: l.depth,
209
+ hierarchy: h.getName
181
210
  }
182
211
  end
183
212
 
@@ -201,49 +230,60 @@ module Mondrian
201
230
  # return the contents of the filter axis
202
231
  # puts self.raw_cell_set.getFilterAxis.inspect
203
232
 
204
- dimensions = self.axis_members.map { |am| am.first ? am.first.dimension_info : nil }
233
+ drilldowns_num = if self.raw_cell_set.getMetaData.getAxesMetaData.size == 1
234
+ 0
235
+ else
236
+ self.raw_cell_set.getMetaData.getAxesMetaData[1].getHierarchies.size
237
+ end
238
+ dimensions = self.axis_members
239
+ .flatten
240
+ .map(&:dimension_info)
241
+ .uniq
242
+
243
+ pprops = {}
244
+ pprops = Mondrian::REST::APIHelpers.parse_properties(self.properties, dimensions[1..-1]) unless self.properties.nil?
245
+ cprops = Mondrian::REST::APIHelpers.parse_caption_properties(self.caption_properties)
246
+
247
+ measure_axis = { members: self.axis_members.first.flatten.map(&:to_h) }
248
+ measure_axis.merge!(dimensions[0]) if dimensions.size > 0
249
+
250
+ member_axes = if drilldowns_num > 1
251
+ self.axis_members[1].transpose
252
+ elsif drilldowns_num == 1
253
+ [self.axis_members[1]]
254
+ else
255
+ []
256
+ end
205
257
 
206
- pprops = unless self.properties.nil?
207
- Mondrian::REST::APIHelpers.parse_properties(self.properties,
208
- dimensions[1..-1]) # exclude Measures dimension
209
- else
210
- {}
211
- end
212
-
213
- cprops = Mondrian::REST::APIHelpers.parse_caption_properties(
214
- self.caption_properties
215
- )
216
-
217
- rv = {
218
- axes: self.axis_members.each_with_index.map { |a, i|
258
+ {
259
+ axes: [measure_axis] + member_axes.each_with_index.map { |a, axis_index|
219
260
  {
220
- members: a.map { |m|
261
+ members: a.uniq(&:full_name).map { |m|
221
262
  mh = m.to_h(
222
263
  pprops.dig(m.raw_member.getDimension.name, m.raw_level.name) || [],
223
264
  (cprops.dig(m.raw_member.getDimension.name, m.raw_level.name) || [[]])[0][-1]
224
265
  )
225
266
  if parents
226
- mh.merge!({
227
- ancestors: m.ancestors.map { |ma|
228
- ma.to_h(
229
- pprops.dig(ma.raw_member.getDimension.name, ma.raw_level.name) || [],
230
- (cprops.dig(ma.raw_member.getDimension.name, ma.raw_level.name) || [[]])[0][-1]
231
- )
232
- }
233
- })
267
+ mh[:ancestors] = m.ancestors.map { |ma|
268
+ ma.to_h(
269
+ pprops.dig(ma.raw_member.getDimension.name, ma.raw_level.name) || [],
270
+ (cprops.dig(ma.raw_member.getDimension.name, ma.raw_level.name) || [[]])[0][-1])
271
+ }
234
272
  end
235
273
  mh
236
274
  }
237
- }
275
+ }.merge(dimensions.size > 1 ? dimensions[axis_index+1] : {})
238
276
  },
239
- axis_dimensions: dimensions,
240
- values: self.values
277
+ cell_keys: if drilldowns_num > 1
278
+ self.axis_members[1].map { |t| t.map { |m| m.property_value('MEMBER_KEY') } }
279
+ elsif drilldowns_num == 1
280
+ self.axis_members[1].map { |t| [ t.property_value('MEMBER_KEY') ] }
281
+ else
282
+ []
283
+ end,
284
+ values: self.values,
285
+ mdx: debug ? self.mdx : nil
241
286
  }
242
-
243
- rv[:mdx] = self.mdx if debug
244
-
245
- rv
246
-
247
287
  end
248
288
  end
249
289
  end
@@ -1,6 +1,11 @@
1
1
  # coding: utf-8
2
2
  module Mondrian::REST
3
3
  module QueryHelper
4
+ VALID_FILTER_OPS = [
5
+ '>', '<', '>=', '<=', '=', '<>'
6
+ ].freeze
7
+ VALID_FILTER_RE = /(?<measure>[a-zA-Z0-9\s]+)\s*(?<operand>#{VALID_FILTER_OPS.join("|")})\s*(?<value>-?\d+\.?\d*)/
8
+ MEMBER_METHODS = %w[Caption Key Name UniqueName].freeze
4
9
 
5
10
  def unparse_node(node)
6
11
  sw = java.io.StringWriter.new
@@ -11,18 +16,16 @@ module Mondrian::REST
11
16
 
12
17
  def get_dimension(cube, dname)
13
18
  cube_dimensions = cube.dimensions
14
- .find_all { |d| d.dimension_type != :measures }
19
+ .find_all { |d| d.dimension_type != :measures }
15
20
  dim = cube_dimensions.find { |d| d.name == dname }
16
21
  error!("Dimension #{dname} does not exist", 400) if dim.nil?
17
22
  dim
18
23
  end
19
24
 
20
25
  def get_member(cube, member_exp)
21
- begin
22
- return cube.member(member_exp)
23
- rescue Java::JavaLang::IllegalArgumentException
24
- error!("Illegal expression: #{member_exp}", 400)
25
- end
26
+ cube.member(member_exp)
27
+ rescue Java::JavaLang::IllegalArgumentException
28
+ error!("Illegal expression: #{member_exp}", 400)
26
29
  end
27
30
 
28
31
  def get_named_set(cube, named_set_exp)
@@ -39,48 +42,42 @@ module Mondrian::REST
39
42
  case p
40
43
  when org.olap4j.mdx.CallNode
41
44
  case p.getOperatorName
42
- when "{}"
45
+ when '{}'
43
46
  # check that the set contains only Members of a single dimension level
44
- set_members = p.getArgList.map { |id_node|
47
+ set_members = p.getArgList.map do |id_node|
45
48
  get_member(cube, unparse_node(id_node))
46
- }
49
+ end
47
50
 
48
- if set_members.any? { |m| m.nil? }
49
- error!("Illegal cut. Unknown member in cut set", 400)
51
+ if set_members.any?(&:nil?)
52
+ error!('Illegal cut. Unknown member in cut set', 400)
50
53
  end
51
54
 
52
55
  ls = set_members.map(&:raw_level).uniq
53
- unless ls.size == 1
54
- error!("Illegal cut: " + cut_expr, 400)
55
- end
56
+ error!('Illegal cut: ' + cut_expr, 400) unless ls.size == 1
56
57
  { level: ls.first, cut: unparse_node(p), type: :set, set_members: set_members }
57
- when "()"
58
+ when '()'
58
59
  # check that the range contains a valid range
59
60
 
60
61
  unless p.getArgList.first.is_a?(org.olap4j.mdx.CallNode) \
61
- and p.getArgList.first.getOperatorName == ':'
62
- error!("Illegal cut: " + cut_expr, 400)
62
+ && (p.getArgList.first.getOperatorName == ':')
63
+ error!('Illegal cut: ' + cut_expr, 400)
63
64
  end
64
65
 
65
- ls = p.getArgList.first.getArgList.map { |id_node|
66
+ ls = p.getArgList.first.getArgList.map do |id_node|
66
67
  get_member(cube, unparse_node(id_node)).raw_level
67
- }.uniq
68
+ end.uniq
68
69
 
69
- unless ls.size == 1
70
- error!("Illegal cut: " + cut_expr, 400)
71
- end
70
+ error!('Illegal cut: ' + cut_expr, 400) unless ls.size == 1
72
71
 
73
72
  { level: ls.first, cut: unparse_node(p), type: :range }
74
73
  else
75
- error!("Illegal cut: " + cut_expr, 400)
74
+ error!('Illegal cut: ' + cut_expr, 400)
76
75
  end
77
76
  when org.olap4j.mdx.IdentifierNode
78
77
 
79
78
  # does cut_expr look like a NamedSet?
80
79
  s = get_named_set(cube, cut_expr)
81
- if !s.nil?
82
- return { level: nil, cut: cut_expr, type: :named_set }
83
- end
80
+ return { level: nil, cut: cut_expr, type: :named_set } unless s.nil?
84
81
 
85
82
  # if `cut_expr` looks like a member, check that it's level is
86
83
  # equal to `level`
@@ -92,7 +89,7 @@ module Mondrian::REST
92
89
 
93
90
  { level: m.raw_level, cut: cut_expr, type: :member }
94
91
  else
95
- error!("Illegal cut: " + cut_expr, 400)
92
+ error!('Illegal cut: ' + cut_expr, 400)
96
93
  end
97
94
  end
98
95
 
@@ -100,10 +97,9 @@ module Mondrian::REST
100
97
  # Parses a drilldown specification
101
98
  # XXX TODO write doc
102
99
  def parse_drilldown(cube, drilldown)
103
-
104
100
  # check if the drilldown is a named set
105
101
  named_sets = cube.named_sets
106
- if ns = named_sets.find { |ns| ns.name == drilldown }
102
+ if (ns = named_sets.find { |ns| ns.name == drilldown })
107
103
  return ns
108
104
  end
109
105
 
@@ -138,46 +134,101 @@ module Mondrian::REST
138
134
  level
139
135
  end
140
136
 
141
- def build_query(cube, options={})
137
+ def parse_measure_filter(cube, filter)
138
+ m = VALID_FILTER_RE.match(filter)
139
+ error!("Filter clause #{filter} is invalid", 400) if m.nil?
140
+
141
+ unless cube.valid_measure?(m['measure'].strip)
142
+ error!("Invalid filter: measure #{m['measure'].strip} does not exist", 400)
143
+ end
144
+
145
+ {
146
+ measure: m['measure'].strip,
147
+ operand: m['operand'].strip,
148
+ value: m['value'].strip
149
+ }
150
+ end
151
+
152
+ def parse_order(cube, order, order_desc)
153
+ begin
154
+ s = org.olap4j.mdx.IdentifierNode.parseIdentifier(order).getSegmentList.map(&:getName)
155
+ rescue Java::JavaLang::IllegalArgumentException
156
+ error!("Invalid order specification: #{order}", 400)
157
+ end
158
+
159
+ if s[0] == 'Measures'
160
+ error!("Invalid measure in order: #{s[1]}", 400) unless cube.valid_measure?(s[1])
161
+
162
+ return {
163
+ order: cube.measure(s[1]).full_name,
164
+ desc: order_desc
165
+ }
166
+ else # ordering by a property
167
+ # we need at least dim.level.property
168
+ error!('Invalid order: specify at least [Dimension].[Level].[Property]', 400) if s.size < 3
169
+
170
+ lvl = cube.level(*s[0..-2])
171
+ error!("Invalid order: level #{s[0..-2].join('.')} not found", 400) if lvl.nil?
172
+
173
+ last = if MEMBER_METHODS.include?(s.last)
174
+ s.last
175
+ else
176
+ prop = lvl.property(s[-1])
177
+ error!("Invalid order: property #{order} not found", 400) if prop.nil?
178
+ "Properties('#{s.last}')"
179
+ end
180
+
181
+ return {
182
+ order: s[0..-2].map do |n|
183
+ Java::MondrianOlap::Util.quoteMdxIdentifier(n)
184
+ end.join('.') + '.CurrentMember.' + last,
185
+ desc: order_desc
186
+ }
187
+ end
188
+ end
142
189
 
190
+ def build_query(cube, options = {})
143
191
  measure_members = cube.dimension('Measures').hierarchy.levels.first.members
144
192
  options = {
145
193
  'cut' => [],
146
194
  'drilldown' => [],
147
195
  'measures' => [measure_members.first.name],
148
196
  'nonempty' => false,
149
- 'distinct' => false
197
+ 'distinct' => false,
198
+ 'filter' => [],
199
+ 'order' => nil,
200
+ 'order_desc' => false,
201
+ 'offset' => nil,
202
+ 'limit' => nil
150
203
  }.merge(options)
151
204
 
152
205
  # validate measures exist
153
206
  cm_names = measure_members.map(&:name)
154
207
 
155
- options['measures'].each { |m|
208
+ options['measures'].each do |m|
156
209
  error!("Measure #{m} does not exist in cube #{cube.name}", 400) unless cm_names.include?(m)
157
- }
210
+ end
211
+
212
+ filters = options['filter'].map { |f| parse_measure_filter(cube, f) }
158
213
 
159
214
  # measures go in axis(0) of the resultset
160
215
  query = olap.from(cube.name)
161
- .axis(0,
162
- *options['measures'].map { |m|
163
- measure_members.find { |cm| cm.name == m }.full_name
164
- })
165
- if options['nonempty']
166
- query = query.nonempty
167
- end
168
- axis_idx = 1
216
+ .axis(0,
217
+ *options['measures'].map do |m|
218
+ measure_members.find { |cm| cm.name == m }.full_name
219
+ end)
220
+ query = query.nonempty if options['nonempty']
169
221
 
170
222
  query_axes = options['drilldown'].map { |dd| parse_drilldown(cube, dd) }
171
223
 
172
- slicer_axis = options['cut'].reduce({}) { |h, cut_expr|
224
+ slicer_axis = options['cut'].each_with_object({}) do |cut_expr, h|
173
225
  pc = parse_cut(cube, cut_expr)
174
226
  h[pc[:level]] = pc
175
- h
176
- }
227
+ end
177
228
 
178
229
  dd = query_axes.map do |qa|
179
230
  # if drilling down on a named set
180
- if qa.kind_of?(Java::MondrianOlap4j::MondrianOlap4jNamedSet)
231
+ if qa.is_a?(Java::MondrianOlap4j::MondrianOlap4jNamedSet)
181
232
  "[#{qa.name}]"
182
233
  # there's a slice (cut) on this axis
183
234
  elsif slicer_axis[qa.raw_level]
@@ -188,10 +239,10 @@ module Mondrian::REST
188
239
  else
189
240
  cut[:cut]
190
241
  end
191
- elsif cut = slicer_axis.find { |lvl, cut|
242
+ elsif cut = slicer_axis.find do |lvl, cut|
192
243
  next if cut[:type] == :named_set
193
244
  qa.raw_level.hierarchy == lvl.hierarchy && lvl.depth < qa.depth
194
- }
245
+ end
195
246
  slicer_axis.delete(cut[0])
196
247
  cut = cut[1]
197
248
 
@@ -200,44 +251,58 @@ module Mondrian::REST
200
251
  "DESCENDANTS(#{cut[:cut]}, #{qa.unique_name})"
201
252
  when :set
202
253
  # TODO
203
- "{" + cut[:set_members].map { |m|
254
+ '{' + cut[:set_members].map do |m|
204
255
  "DESCENDANTS(#{m.full_name}, #{qa.unique_name})"
205
- }.join(",") + "}"
256
+ end.join(',') + '}'
206
257
  when :range
207
258
  # TODO
208
- raise "Unsupported operation"
259
+ raise 'Unsupported operation'
209
260
  end
210
261
  else
211
262
  qa.unique_name + '.Members'
212
263
  end
213
264
  end
214
265
 
215
- # query axes (drilldown)
216
- dd.each do |ds|
217
- query = query.axis(axis_idx,
218
- ds)
266
+ unless dd.empty?
267
+ # Cross join all the drilldowns
268
+ axis_exp = dd.join(' * ')
219
269
 
220
- if options['distinct']
221
- query = query.distinct
270
+ # Apply filters
271
+ unless filters.empty?
272
+ filter_exp = filters.map { |f| "[Measures].[#{org.olap4j.mdx.MdxUtil.mdxEncodeString(f[:measure])}] #{f[:operand]} #{f[:value]}" }.join(' AND ')
273
+ axis_exp = "FILTER(#{axis_exp}, #{filter_exp})"
222
274
  end
223
275
 
224
- if options['nonempty']
225
- query = query.nonempty
276
+ unless options['order'].nil?
277
+ order = parse_order(cube, options['order'], options['order_desc'])
278
+ axis_exp = "ORDER(#{axis_exp}, #{order[:order]}, #{order[:desc] ? 'BDESC' : 'BASC'})"
226
279
  end
227
280
 
228
- axis_idx += 1
281
+ # TODO: Apply pagination
282
+ unless options['offset'].nil?
283
+ axis_exp = if options['limit'].nil?
284
+ "SUBSET(#{axis_exp}, #{options['offset']})"
285
+ else
286
+ "SUBSET(#{axis_exp}, #{options['offset']}, #{options['limit']})"
287
+ end
288
+ end
289
+
290
+ query = query.axis(1, axis_exp)
229
291
  end
230
292
 
293
+ query = query.distinct if options['distinct']
294
+
295
+ query = query.nonempty if options['nonempty']
296
+
231
297
  # slicer axes (cut)
232
298
  if slicer_axis.size >= 1
233
- puts slicer_axis.inspect
234
- query = query.where(slicer_axis.values.map { |v|
299
+ query = query.where(slicer_axis.values.map do |v|
235
300
  if v[:type] == :named_set
236
301
  "[#{v[:cut]}]"
237
302
  else
238
303
  v[:cut]
239
304
  end
240
- }.join(' * '))
305
+ end.join(' * '))
241
306
  end
242
307
  query
243
308
  end
@@ -1,5 +1,5 @@
1
1
  module Mondrian
2
2
  module REST
3
- VERSION = "0.7.9"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
@@ -16,11 +16,11 @@ Gem::Specification.new do |s|
16
16
  s.files = `git ls-files`.split("\n").reject { |f| f =~ /^spec\// }
17
17
  s.require_paths = ["lib", "lib/jars"]
18
18
 
19
- s.requirements << 'jar no.ssb.jsonstat:json-stat-java, 0.2.2'
20
19
 
21
20
  s.add_runtime_dependency 'mondrian-olap', ["~> 0.8.0"]
22
21
  s.add_runtime_dependency 'grape', '~> 1.0', '>= 1.0.0'
23
22
  s.add_runtime_dependency 'writeexcel', '~> 1.0', '>= 1.0.5'
23
+ s.add_runtime_dependency 'activesupport', '~> 5.1', '>= 5.1.5'
24
24
 
25
25
  s.add_development_dependency "jar-dependencies", "~> 0.3.2"
26
26
  s.add_development_dependency 'rake', '~> 12.1', '>= 12.1.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mondrian-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.9
4
+ version: 1.0.0
5
5
  platform: java
6
6
  authors:
7
7
  - Manuel Aristarán
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-28 00:00:00.000000000 Z
11
+ date: 2018-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -64,6 +64,26 @@ dependencies:
64
64
  - - ">="
65
65
  - !ruby/object:Gem::Version
66
66
  version: 1.0.5
67
+ - !ruby/object:Gem::Dependency
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '5.1'
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 5.1.5
76
+ name: activesupport
77
+ prerelease: false
78
+ type: :runtime
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '5.1'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 5.1.5
67
87
  - !ruby/object:Gem::Dependency
68
88
  requirement: !ruby/object:Gem::Requirement
69
89
  requirements:
@@ -205,17 +225,6 @@ files:
205
225
  - LICENSE
206
226
  - README.md
207
227
  - Rakefile
208
- - lib/jars/com/codepoetics/protonpack/1.9/protonpack-1.9.jar
209
- - lib/jars/com/fasterxml/jackson/core/jackson-annotations/2.8.0/jackson-annotations-2.8.0.jar
210
- - lib/jars/com/fasterxml/jackson/core/jackson-core/2.8.5/jackson-core-2.8.5.jar
211
- - lib/jars/com/fasterxml/jackson/core/jackson-databind/2.8.5/jackson-databind-2.8.5.jar
212
- - lib/jars/com/fasterxml/jackson/datatype/jackson-datatype-guava/2.8.5/jackson-datatype-guava-2.8.5.jar
213
- - lib/jars/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.8.5/jackson-datatype-jdk8-2.8.5.jar
214
- - lib/jars/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.8.5/jackson-datatype-jsr310-2.8.5.jar
215
- - lib/jars/com/google/guava/guava/19.0/guava-19.0.jar
216
- - lib/jars/me/yanaga/guava-stream/1.0/guava-stream-1.0.jar
217
- - lib/jars/mondrian-rest_jars.rb
218
- - lib/jars/no/ssb/jsonstat/json-stat-java/0.2.2/json-stat-java-0.2.2.jar
219
228
  - lib/mondrian_rest.rb
220
229
  - lib/mondrian_rest/api.rb
221
230
  - lib/mondrian_rest/api_formatters.rb
@@ -224,7 +233,6 @@ files:
224
233
  - lib/mondrian_rest/formatters/csv.rb
225
234
  - lib/mondrian_rest/formatters/excel.rb
226
235
  - lib/mondrian_rest/formatters/jsonrecords.rb
227
- - lib/mondrian_rest/formatters/jsonstat.rb
228
236
  - lib/mondrian_rest/mondrian_ext.rb
229
237
  - lib/mondrian_rest/nest.rb
230
238
  - lib/mondrian_rest/query_helper.rb
@@ -249,8 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
249
257
  - - ">="
250
258
  - !ruby/object:Gem::Version
251
259
  version: '0'
252
- requirements:
253
- - jar no.ssb.jsonstat:json-stat-java, 0.2.2
260
+ requirements: []
254
261
  rubyforge_project:
255
262
  rubygems_version: 2.6.13
256
263
  signing_key:
@@ -1,28 +0,0 @@
1
- # this is a generated file, to avoid over-writing it just delete this comment
2
- begin
3
- require 'jar_dependencies'
4
- rescue LoadError
5
- require 'com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.8.5/jackson-datatype-jdk8-2.8.5.jar'
6
- require 'com/fasterxml/jackson/core/jackson-annotations/2.8.0/jackson-annotations-2.8.0.jar'
7
- require 'me/yanaga/guava-stream/1.0/guava-stream-1.0.jar'
8
- require 'com/fasterxml/jackson/core/jackson-databind/2.8.5/jackson-databind-2.8.5.jar'
9
- require 'no/ssb/jsonstat/json-stat-java/0.2.2/json-stat-java-0.2.2.jar'
10
- require 'com/fasterxml/jackson/core/jackson-core/2.8.5/jackson-core-2.8.5.jar'
11
- require 'com/fasterxml/jackson/datatype/jackson-datatype-guava/2.8.5/jackson-datatype-guava-2.8.5.jar'
12
- require 'com/google/guava/guava/19.0/guava-19.0.jar'
13
- require 'com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.8.5/jackson-datatype-jsr310-2.8.5.jar'
14
- require 'com/codepoetics/protonpack/1.9/protonpack-1.9.jar'
15
- end
16
-
17
- if defined? Jars
18
- require_jar( 'com.fasterxml.jackson.datatype', 'jackson-datatype-jdk8', '2.8.5' )
19
- require_jar( 'com.fasterxml.jackson.core', 'jackson-annotations', '2.8.0' )
20
- require_jar( 'me.yanaga', 'guava-stream', '1.0' )
21
- require_jar( 'com.fasterxml.jackson.core', 'jackson-databind', '2.8.5' )
22
- require_jar( 'no.ssb.jsonstat', 'json-stat-java', '0.2.2' )
23
- require_jar( 'com.fasterxml.jackson.core', 'jackson-core', '2.8.5' )
24
- require_jar( 'com.fasterxml.jackson.datatype', 'jackson-datatype-guava', '2.8.5' )
25
- require_jar( 'com.google.guava', 'guava', '19.0' )
26
- require_jar( 'com.fasterxml.jackson.datatype', 'jackson-datatype-jsr310', '2.8.5' )
27
- require_jar( 'com.codepoetics', 'protonpack', '1.9' )
28
- end
@@ -1,50 +0,0 @@
1
- require 'java'
2
-
3
- module Mondrian::REST::Formatters
4
- module JSONStat
5
-
6
- def self.call(result, env)
7
-
8
- mapper = Java::ComFasterxmlJacksonDatabind::ObjectMapper.new
9
- mapper.registerModule(Java::NoSsbJsonstat::JsonStatModule.new)
10
- mapper.registerModule(Java::ComFasterxmlJacksonDatatypeJdk8::Jdk8Module.new.configureAbsentsAsNulls(true))
11
- mapper.setSerializationInclusion(Java::ComFasterxmlJacksonAnnotation::JsonInclude::Include::NON_NULL)
12
- mapper.registerModule(Java::ComFasterxmlJacksonDatatypeGuava::GuavaModule.new.configureAbsentsAsNulls(false))
13
-
14
- rs = result.to_h
15
- rdims = rs[:axis_dimensions].reverse
16
-
17
- builder = Java::NoSsbJsonstatV2::Dataset
18
- .create
19
- .withLabel('Aggregation: ')
20
- .withSource(env['REQUEST_URI'] || '')
21
-
22
- dimensions = rdims.map.with_index do |d, i|
23
- dim = Java::NoSsbJsonstatV2::Dimension
24
- .create(d[:name])
25
-
26
- if d[:type] == :measures
27
- dim = dim.withMetricRole
28
- elsif d[:type] == :time
29
- dim = dim.withTimeRole
30
- end
31
-
32
- dim.withLabel(d[:caption])
33
- .withIndexedLabels(
34
- Java::ComGoogleCommonCollect::ImmutableMap.copyOf(
35
- Hash[*(rs[:axes][-1 - i][:members].map { |m| [ m[:key].to_s, m[:caption] ] }.flatten)].to_java
36
- )
37
- )
38
- end
39
-
40
- dataset = builder
41
- .withDimensions(dimensions)
42
- .withValues(rs[:values].flatten)
43
- .build
44
-
45
- mapper.writeValueAsString(dataset)
46
-
47
- end
48
-
49
- end
50
- end