mondrian-rest 0.5.0-java → 0.6.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
  SHA1:
3
- metadata.gz: fca6cfddd8cf42e7d87ec8e35f10b8389fdd08ff
4
- data.tar.gz: 08b4017a7cdc2eebc4f0b2218b51822396061dd5
3
+ metadata.gz: 2710889190b4d58cd7592415468caf4b7f5b7b95
4
+ data.tar.gz: 79bfcad96399941a47c1ba2e553e466209b053af
5
5
  SHA512:
6
- metadata.gz: 9fba6d67d771da283e085b0d89784baa093b5676b0c737aa89cdc5ef2b4ba1e9f3c9a68574db21b85759733e7f1161ec538e2051820bc641d446a62ac6577fd9
7
- data.tar.gz: 44ed30d9adf06ddfbb7fe157a8a2a6d5e1765936b82fbdbc2b24bbc12d2dbf88f23c41285d3c1f745034e82d4228d8b936128de6583c1e7e51b5d17d8fcacc59
6
+ metadata.gz: 81b615aa3796ac61ddd850dec37bfbcecd75a5565b3de5fad18acea59f212dae2445b2c4434d6d1910e9a5b433045cd46ef6d163b9182b314841b8d285006e1d
7
+ data.tar.gz: 6bbed693f2ba708a44688b9d85b0980afe343c52d7d9da2f26259e10d554b94b53e4abe88c89d77b2c39be453c6f532cb4dae2bf330e2ba0290f541d9c4beecf
data/Gemfile CHANGED
@@ -1,5 +1,6 @@
1
1
  source 'http://rubygems.org'
2
- gem 'mondrian-olap', :git => 'https://github.com/rsim/mondrian-olap.git', :branch => 'master'
3
2
  gem 'coveralls', require: false
3
+ gem 'ruby-debug'
4
+ gem 'mondrian-olap', :path => '/Users/manuel/Work/code/mondrian-olap', :require => 'mondrian-olap'
4
5
 
5
6
  gemspec
data/README.md CHANGED
@@ -10,7 +10,7 @@ See [`mondrian-rest-demo`](https://github.com/jazzido/mondrian-rest-demo) and [`
10
10
 
11
11
  ## Credits
12
12
 
13
- Initial inspiration for this project came from the [Stefan Urbanek](https://github.com/Stiivi)'s [`cubes`](http://cubes.databrewery.org/) OLAP server. `mondrian-rest` uses [Raimonds Simanovskis](https://github.com/rsim)'s fantastic [`mondrian-olap`](https://github.com/rsim/mondrian-olap) library.
13
+ Initial inspiration for this project came from [Stefan Urbanek](https://github.com/Stiivi)'s [`cubes`](http://cubes.databrewery.org/) OLAP server. `mondrian-rest` uses [Raimonds Simanovskis](https://github.com/rsim)'s fantastic [`mondrian-olap`](https://github.com/rsim/mondrian-olap) library.
14
14
 
15
15
 
16
16
  ## License
@@ -8,7 +8,7 @@ module Mondrian::REST
8
8
 
9
9
  class Api < Grape::API
10
10
  version '1', using: :header, vendor: 'mondrian_rest'
11
- format :json
11
+ default_format :json
12
12
 
13
13
  helpers Mondrian::REST::APIHelpers
14
14
  helpers Mondrian::REST::QueryHelper
@@ -36,7 +36,29 @@ module Mondrian::REST
36
36
  end
37
37
  end
38
38
 
39
+ resource :flush do
40
+ params do
41
+ requires :secret, type: String, desc: "Secret key"
42
+ end
43
+ content_type :json, "application/json"
44
+ desc "Flush the schema cache"
45
+
46
+ get do
47
+ if ENV['MONDRIAN_REST_SECRET'].nil?
48
+ error!("Please set MONDRIAN_REST_SECRET to use this endpoint", 403)
49
+ end
50
+ if params[:secret] != ENV['MONDRIAN_REST_SECRET']
51
+ error!("Invalid secret key.", 403)
52
+ end
53
+ {
54
+ 'status' => olap_flush
55
+ }
56
+ end
57
+ end
58
+
39
59
  resource :cubes do
60
+ content_type :json, "application/json"
61
+ default_format :json
40
62
  desc "Returns the cubes defined in this server's schema"
41
63
  get do
42
64
  {
@@ -60,7 +82,7 @@ module Mondrian::REST
60
82
  params do
61
83
  requires :full_name,
62
84
  type: String,
63
- regexp: /[a-z0-9\.,\-\s%\\[\\]\(\)]+/i
85
+ regexp: /[a-z0-9\.,\-\s%\[\]\(\)]+/i
64
86
  end
65
87
  get do
66
88
  member_full_name = URI.decode(params[:full_name])
@@ -81,11 +103,20 @@ module Mondrian::REST
81
103
  resource :aggregate do
82
104
  content_type :xls, "application/vnd.ms-excel"
83
105
  formatter :xls, Mondrian::REST::Formatters::XLS
106
+
84
107
  content_type :csv, "text/csv"
85
108
  formatter :csv, Mondrian::REST::Formatters::CSV
109
+
86
110
  content_type :json, "application/json"
87
111
  formatter :json, Mondrian::REST::Formatters::AggregationJSON
88
112
 
113
+ content_type :jsonrecords, "application/x-jsonrecords"
114
+ formatter :jsonrecords, Mondrian::REST::Formatters::JSONRecords
115
+
116
+ rescue_from PropertyError do |e|
117
+ error!({error: e}, 400)
118
+ end
119
+
89
120
  desc "aggregate from query parameters"
90
121
  params do
91
122
  optional :measures, type: Array
@@ -96,6 +127,7 @@ module Mondrian::REST
96
127
  optional :parents, type: Boolean, desc: "Include members' ancestors"
97
128
  optional :debug, type: Boolean, desc: "Include generated MDX", default: false
98
129
  optional :properties, type: Array, desc: "Include member properties"
130
+ optional :caption, type: Array, desc: "Replace caption with property", default: []
99
131
  end
100
132
  get do
101
133
  cube = get_cube_or_404(params[:cube_name])
@@ -105,6 +137,7 @@ module Mondrian::REST
105
137
  result = mdx(query.to_mdx)
106
138
  result.mdx = mdx_query if params[:debug]
107
139
  result.properties = params[:properties]
140
+ result.caption_properties = params[:caption]
108
141
  result.cube = cube
109
142
 
110
143
  result
@@ -130,7 +163,7 @@ module Mondrian::REST
130
163
  resource :members do
131
164
 
132
165
  params do
133
- optional :member_properties, type: Array
166
+ optional :member_properties, type: Array, default: []
134
167
  end
135
168
 
136
169
  get do
@@ -16,6 +16,7 @@ module Mondrian::REST::Formatters
16
16
  def self.call(result, env)
17
17
  add_parents = env['rack.request.query_hash']['parents'] == 'true'
18
18
  debug = env['rack.request.query_hash']['debug'] == 'true'
19
+ properties = env['rack.request.query_hash']['properties'] || []
19
20
 
20
21
  out = StringIO.new
21
22
  book = WriteExcel.new(out)
@@ -24,7 +25,8 @@ module Mondrian::REST::Formatters
24
25
  Mondrian::REST::Formatters
25
26
  .tidy(result,
26
27
  add_parents: add_parents,
27
- debug: debug)
28
+ debug: debug,
29
+ properties: properties)
28
30
  .each_with_index do |row, i|
29
31
  row.each_with_index { |cell, j|
30
32
  sheet.write(i, j, cell)
@@ -40,10 +42,12 @@ module Mondrian::REST::Formatters
40
42
  def self.call(result, env)
41
43
  add_parents = env['rack.request.query_hash']['parents'] == 'true'
42
44
  debug = env['rack.request.query_hash']['debug'] == 'true'
45
+ properties = env['rack.request.query_hash']['properties'] || []
43
46
 
44
47
  rows = Mondrian::REST::Formatters.tidy(result,
45
48
  add_parents: add_parents,
46
- debug: debug)
49
+ debug: debug,
50
+ properties: properties)
47
51
 
48
52
  ::CSV.generate do |csv|
49
53
  rows.each { |row| csv << row }
@@ -51,6 +55,28 @@ module Mondrian::REST::Formatters
51
55
  end
52
56
  end
53
57
 
58
+ module JSONRecords
59
+ def self.call(result, env)
60
+ add_parents = env['rack.request.query_hash']['parents'] == 'true'
61
+ debug = env['rack.request.query_hash']['debug'] == 'true'
62
+ properties = env['rack.request.query_hash']['properties'] || []
63
+
64
+ rows = Mondrian::REST::Formatters.tidy(result,
65
+ add_parents: add_parents,
66
+ debug: debug,
67
+ properties: properties).lazy
68
+ keys = rows.next
69
+
70
+ {
71
+ data: rows.with_index.with_object([]) { |(row, i), data|
72
+ next if i == 0
73
+ data << Hash[keys.zip(row)]
74
+ }
75
+ }.to_json
76
+
77
+ end
78
+ end
79
+
54
80
  ##
55
81
  # Generate 'tidy data' (http://vita.had.co.nz/papers/tidy-data.pdf)
56
82
  # from a result set.
@@ -58,6 +84,7 @@ module Mondrian::REST::Formatters
58
84
  cube = result.cube
59
85
 
60
86
  add_parents = options[:add_parents]
87
+ properties = options[:properties]
61
88
  rs = result.to_h(add_parents, options[:debug])
62
89
  measures = rs[:axes].first[:members]
63
90
  dimensions = rs[:axis_dimensions][1..-1]
@@ -70,7 +97,7 @@ module Mondrian::REST::Formatters
70
97
  if add_parents
71
98
  hier = cube.dimension(dd[:name])
72
99
  .hierarchies
73
- .first
100
+ .first # TODO: Support other hierarchies
74
101
 
75
102
  level_has_all << hier.has_all?
76
103
  slices << dd[:level_depth]
@@ -86,8 +113,13 @@ module Mondrian::REST::Formatters
86
113
  columns += ["ID #{dd[:level]}", dd[:level]]
87
114
  end
88
115
 
89
- # append measure columns and yield table header
90
- y.yield columns + pluck(measures, :name)
116
+ props = Mondrian::REST::APIHelpers.parse_properties(properties, dimensions)
117
+ pnames = properties.map { |p|
118
+ org.olap4j.mdx.IdentifierNode.parseIdentifier(p).getSegmentList.last.name
119
+ }
120
+
121
+ # append properties and measure columns and yield table header
122
+ y.yield columns + pnames + pluck(measures, :name)
91
123
 
92
124
  prod = rs[:axes][1..-1].map { |e|
93
125
  e[:members].map.with_index { |e_, i| [e_,i] }
@@ -102,24 +134,53 @@ module Mondrian::REST::Formatters
102
134
  msrs = measures.map.with_index { |m, mi|
103
135
  (cidxs + [mi]).reduce(values) { |_, idx| _[idx] }
104
136
  }
137
+
105
138
  if add_parents
106
139
  vdim = cm.each.with_index.reduce([]) { |cnames, (member, j)|
107
- member[:ancestors][0...slices[j] - (level_has_all[j] ? 1 : 0)].each { |ancestor|
140
+ member[:ancestors][0...slices[j] - (level_has_all[j] ? 1 : 0)].reverse.each { |ancestor|
108
141
  cnames += [ancestor[:key], ancestor[:caption]]
109
142
  }
110
143
  cnames += [member[:key], member[:caption]]
111
144
  }
112
- y.yield vdim + msrs
145
+
146
+ y.yield vdim + get_props(cm, pnames, props, dimensions) + msrs
113
147
  else
114
- y.yield pluck(cm, :key)
115
- .zip(pluck(cm, :caption))
116
- .flatten \
117
- + msrs
148
+ row = pluck(cm, :key)
149
+ .zip(pluck(cm, :caption))
150
+ .flatten
151
+
152
+ y.yield row + get_props(cm, pnames, props, dimensions) + msrs
118
153
  end
119
154
  end
120
155
  end
121
156
  end
122
157
 
158
+ def self.get_props(cm, pnames, props, dimensions)
159
+ pvalues = cm.each.with_index.reduce({}) do |h, (member, ax_i)|
160
+ dname = dimensions[ax_i][:name]
161
+ if props[dname] # are there properties requested for members of this dimension?
162
+ mmbr_lvl = dimensions[ax_i][:level]
163
+ (props[dname][mmbr_lvl] || []).each { |p|
164
+ h[p] = member[:properties][p]
165
+ }
166
+ if member[:ancestors]
167
+ props[dname]
168
+ .select { |k, _| k != mmbr_lvl } # levels other than member's own
169
+ .each { |l, p|
170
+ p.each # get all requested props for this level's ancestor
171
+ .with_object(member[:ancestors].find { |anc|
172
+ anc[:level_name] == l
173
+ }) { |prop, anc|
174
+ h[prop] = anc[:properties][prop]
175
+ }
176
+ }
177
+ end
178
+ end
179
+ h
180
+ end # reduce
181
+ pnames.map { |pn| pvalues[pn] }
182
+ end
183
+
123
184
  def self.pluck(a, m)
124
185
  a.map { |e| e[m] }
125
186
  end
@@ -1,4 +1,8 @@
1
1
  module Mondrian::REST
2
+
3
+ class PropertyError < StandardError
4
+ end
5
+
2
6
  module APIHelpers
3
7
 
4
8
  @@olap = nil
@@ -46,5 +50,42 @@ module Mondrian::REST
46
50
  end
47
51
  end
48
52
 
53
+ NEST = Mondrian::REST::Nest.new
54
+ .key { |d| d[0] }
55
+ .key { |d| d[1] }
56
+
57
+ def self.parse_caption_properties(cprops)
58
+ if cprops.nil? or cprops.size < 1
59
+ return {}
60
+ end
61
+
62
+ NEST.map(cprops.map { |cp|
63
+ org.olap4j.mdx.IdentifierNode.parseIdentifier(cp).getSegmentList.to_a.map(&:name)
64
+ })
65
+ end
66
+
67
+ ##
68
+ # parse an array of property specifications like so:
69
+ # input: ['ISICrev4.Level 2.Level 2 ES', 'ISICrev4.Level 1.Level 1 ES']
70
+ # output: {"ISICrev4"=>{"Level 2"=>["Level 2 ES"], "Level 1"=>["Level 1 ES"]}}
71
+ def self.parse_properties(properties, dimensions)
72
+ properties.map { |p|
73
+ sl = org.olap4j.mdx.IdentifierNode.parseIdentifier(p).getSegmentList.to_a
74
+ if sl.size != 3
75
+ raise PropertyError, "Properties must be in the form `Dimension.Level.Property Name`"
76
+ end
77
+
78
+ # check that the dimension is in the drilldown list
79
+ if dimensions.find { |ad| sl[0].name == ad[:name] }.nil?
80
+ raise PropertyError, "Dimension `#{sl[0].name}` not in drilldown list"
81
+ end
82
+
83
+ sl.map(&:name)
84
+ }.group_by(&:first)
85
+ .reduce({}) { |h, (k,v)|
86
+ h[k] = Hash[v.group_by { |x| x[1] }.map { |k1, v1| [k1, v1.map(&:last)] }]
87
+ h
88
+ }
89
+ end
49
90
  end
50
91
  end
@@ -1,12 +1,19 @@
1
+ require 'set'
2
+
1
3
  module Mondrian
2
4
  module OLAP
3
5
 
4
6
  class Cube
7
+
8
+ def named_sets
9
+ raw_cube.getSets
10
+ end
11
+
5
12
  def to_h
6
13
  # gather dimensions
7
14
  dimensions = self.dimensions
8
- .find_all { |d| d.dimension_type != :measures }
9
- .map { |d|
15
+ .find_all { |d| d.dimension_type != :measures }
16
+ .map do |d|
10
17
  {
11
18
  :name => d.name,
12
19
  :caption => d.caption,
@@ -21,18 +28,47 @@ module Mondrian
21
28
  {
22
29
  :name => l.name,
23
30
  :full_name => l.full_name,
24
- :caption => l.caption
31
+ :caption => l.caption,
32
+ :depth => l.depth,
33
+ :annotations => l.annotations,
34
+ :properties => l.own_props.map { |p|
35
+ p.getName
36
+ }
25
37
  }
26
38
  }
27
39
  }
28
40
  }
29
41
  }
30
- }
42
+ end
43
+
44
+ # gather named sets
45
+ named_sets = self.named_sets
46
+ .map do |ns|
47
+
48
+ t = ns.getExpression.getType
49
+ {
50
+ :name => ns.name,
51
+ :dimension => t.getDimension.getName,
52
+ :hierarchy => t.getHierarchy.getName,
53
+ :level => t.getLevel.getName,
54
+ :annotations => begin
55
+ annotated = ns.unwrap(Java::MondrianOlap::Annotated.java_class)
56
+ annotations_hash = annotated.getAnnotationMap.to_hash
57
+ annotations_hash.each do |key, annotation|
58
+ annotations_hash[key] = annotation.getValue
59
+ end
60
+ annotations_hash
61
+ rescue
62
+ {}
63
+ end
64
+ }
65
+ end
31
66
 
32
67
  return {
33
68
  :name => self.name,
34
69
  :annotations => self.annotations,
35
70
  :dimensions => dimensions,
71
+ :named_sets => named_sets,
36
72
  :measures => self.dimensions
37
73
  .find(&:measures?)
38
74
  .hierarchy
@@ -66,6 +102,8 @@ module Mondrian
66
102
  end
67
103
  end
68
104
 
105
+ INTERNAL_PROPS = Set.new(['CATALOG_NAME', 'SCHEMA_NAME', 'CUBE_NAME', 'DIMENSION_UNIQUE_NAME', 'HIERARCHY_UNIQUE_NAME', 'LEVEL_UNIQUE_NAME', 'LEVEL_NUMBER', 'MEMBER_ORDINAL', 'MEMBER_NAME', 'MEMBER_UNIQUE_NAME', 'MEMBER_TYPE', 'MEMBER_GUID', 'MEMBER_CAPTION', 'CHILDREN_CARDINALITY', 'PARENT_LEVEL', 'PARENT_UNIQUE_NAME', 'PARENT_COUNT', 'DESCRIPTION', '$visible', 'MEMBER_KEY', 'IS_PLACEHOLDERMEMBER', 'IS_DATAMEMBER', 'DEPTH', 'DISPLAY_INFO', 'VALUE', '$scenario', 'CELL_FORMATTER', 'CELL_FORMATTER_SCRIPT', 'CELL_FORMATTER_SCRIPT_LANGUAGE', 'DISPLAY_FOLDER', 'FORMAT_EXP', 'KEY', '$name']).freeze
106
+
69
107
  class Level
70
108
  attr_reader :hierarchy
71
109
 
@@ -79,25 +117,39 @@ module Mondrian
79
117
  caption: self.caption,
80
118
  members: self.members
81
119
  .uniq { |m| m.property_value('MEMBER_KEY') }
82
- .map { |m| m.to_h(member_properties) }
120
+ .map { |m| m.to_h(member_properties) },
121
+ :properties => self.own_props.map { |p|
122
+ p.getName
123
+ }
124
+ }
125
+ end
126
+
127
+ def own_props
128
+ @raw_level.properties.select { |p|
129
+ !INTERNAL_PROPS.include?(p.name)
83
130
  }
84
131
  end
132
+
85
133
  end
86
134
 
87
135
  class Member
88
136
 
137
+ alias_method :_caption, :caption
138
+
89
139
  def raw_level
90
140
  @raw_member.getLevel
91
141
  end
92
142
 
93
- def to_h(properties=[])
94
- kv = [:name, :full_name, :caption, :all_member?,
143
+ def to_h(properties=[], caption_property=nil)
144
+ kv = [:name, :full_name, :all_member?,
95
145
  :drillable?, :depth].map { |m|
96
146
  [m, self.send(m)]
97
147
  }
148
+ kv << [:caption, self.pcaption(caption_property)]
98
149
  kv << [:key, self.property_value('MEMBER_KEY')]
99
150
  kv << [:num_children, self.property_value('CHILDREN_CARDINALITY')]
100
151
  kv << [:parent_name, self.property_value('PARENT_UNIQUE_NAME')]
152
+ kv << [:level_name, self.raw_level.name]
101
153
 
102
154
  if properties.size > 0
103
155
  kv << [
@@ -109,6 +161,14 @@ module Mondrian
109
161
  Hash[kv]
110
162
  end
111
163
 
164
+ def pcaption(caption_property=nil)
165
+ if caption_property
166
+ self.property_value(caption_property)
167
+ else
168
+ self._caption
169
+ end
170
+ end
171
+
112
172
  def dimension_info
113
173
  d = @raw_member.getDimension
114
174
  l = @raw_member.getLevel
@@ -130,7 +190,7 @@ module Mondrian
130
190
 
131
191
  class Result
132
192
 
133
- attr_accessor :cube, :mdx, :properties
193
+ attr_accessor :cube, :mdx, :properties, :caption_properties
134
194
 
135
195
  def to_json
136
196
  to_h.to_json
@@ -144,20 +204,31 @@ module Mondrian
144
204
  dimensions = self.axis_members.map { |am| am.first.dimension_info }
145
205
 
146
206
  pprops = unless self.properties.nil?
147
- parse_properties(dimensions[1..-1]) # exclude Measures dimension
207
+ Mondrian::REST::APIHelpers.parse_properties(self.properties,
208
+ dimensions[1..-1]) # exclude Measures dimension
148
209
  else
149
210
  {}
150
211
  end
151
212
 
213
+ cprops = Mondrian::REST::APIHelpers.parse_caption_properties(
214
+ self.caption_properties
215
+ )
216
+
152
217
  rv = {
153
218
  axes: self.axis_members.each_with_index.map { |a, i|
154
219
  {
155
220
  members: a.map { |m|
156
- mh = m.to_h(pprops[m.raw_member.getDimension.name] || [])
221
+ mh = m.to_h(
222
+ pprops.dig(m.raw_member.getDimension.name, m.raw_level.name) || [],
223
+ (cprops.dig(m.raw_member.getDimension.name, m.raw_level.name) || [[]])[0][-1]
224
+ )
157
225
  if parents
158
226
  mh.merge!({
159
227
  ancestors: m.ancestors.map { |ma|
160
- ma.to_h(pprops[ma.raw_member.getDimension.name] || [])
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
+ )
161
232
  }
162
233
  })
163
234
  end
@@ -174,28 +245,6 @@ module Mondrian
174
245
  rv
175
246
 
176
247
  end
177
-
178
- private
179
-
180
- ##
181
- #
182
- def parse_properties(dimensions)
183
- self.properties.reduce({}) { |h, p|
184
- sl = org.olap4j.mdx.IdentifierNode.parseIdentifier(p).getSegmentList.to_a
185
- if sl.size != 2
186
- raise "Properties must be in the form `Dimension.Property Name`"
187
- end
188
-
189
- # check that the dimension is in the drilldown list
190
- if dimensions.find { |ad| sl[0].name == ad[:name] }.nil?
191
- raise "Dimension `#{sl[0].name}` not in drilldown list"
192
- end
193
-
194
- h[sl[0].name] ||= []
195
- h[sl[0].name] << sl[1].name
196
- h
197
- }
198
- end
199
248
  end
200
249
  end
201
250
  end
@@ -0,0 +1,121 @@
1
+ # D3 nest operator
2
+ # from: https://gist.github.com/herrstucki/3974701
3
+ module Mondrian::REST
4
+ class Nest
5
+ def initialize
6
+ @nest = {}
7
+ @keys = []
8
+ @sort_keys = []
9
+ end
10
+
11
+ def map(array)
12
+ _map(array, 0)
13
+ end
14
+
15
+ def entries(array)
16
+ _entries(_map(array, 0), 0)
17
+ end
18
+
19
+ def key(&f)
20
+ @keys << f
21
+ self
22
+ end
23
+
24
+ def sort_keys(&order)
25
+ @sort_keys[@keys.size - 1] = order
26
+ self
27
+ end
28
+
29
+ def sort_values(&order)
30
+ @sort_values = order
31
+ self
32
+ end
33
+
34
+ def rollup(&f)
35
+ @rollup = f
36
+ self
37
+ end
38
+
39
+ private
40
+
41
+ def _map(array, depth)
42
+ if depth >= @keys.size
43
+ return @rollup.call(array) if @rollup
44
+ return array.sort { |a, b| @sort_values.call(a, b) } if @sort_values
45
+ return array
46
+ end
47
+
48
+ key = @keys[depth]
49
+ depth += 1
50
+ values_by_key = {}
51
+
52
+ array.each_with_index do |object, i|
53
+ key_value = key.call(object)
54
+ values_by_key[key_value] ||= []
55
+ values_by_key[key_value] << object
56
+ end
57
+
58
+ o = {}
59
+ values_by_key.each do |key_value, values|
60
+ o[key_value] = _map(values, depth)
61
+ end
62
+ o
63
+ end
64
+
65
+ def _entries(map, depth)
66
+ return map if depth >= @keys.size
67
+
68
+ a = []
69
+ sort_key = @sort_keys[depth]
70
+ depth += 1
71
+
72
+ map.each do |key, values|
73
+ a << {
74
+ key: key,
75
+ values: _entries(values, depth)
76
+ }
77
+ end
78
+
79
+ if sort_key
80
+ a.sort { |a, b| sort_key.call(a[:key], b[:key]) }
81
+ else
82
+ a
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+
90
+ # TEST
91
+
92
+ # require 'pp'
93
+
94
+ # data = [
95
+ # {year: "2013", category: "Cars", value: 1000},
96
+ # {year: "2011", category: "Cars", value: 1200},
97
+ # {year: "2012", category: "Cars", value: 1300},
98
+ # {year: "2011", category: "Planes", value: 1000},
99
+ # {year: "2011", category: "Planes", value: 1100},
100
+ # {year: "2012", category: "Planes", value: 1200},
101
+ # {year: "2011", category: "Planes", value: 1300},
102
+ # {year: "2012", category: "Bikes", value: 1000},
103
+ # {year: "2011", category: "Bikes", value: 1100},
104
+ # {year: "2011", category: "Bikes", value: 1200},
105
+ # ]
106
+
107
+ # nest = Nest.new
108
+ # nest.key { |d| d[:year] }
109
+ # nest.sort_keys { |a,b| a <=> b }
110
+ # nest.key { |d| d[:category] }
111
+ # # nest.sort_values { |a,b| b[:value] <=> a[:value] }
112
+ # nest.rollup do |values|
113
+ # {
114
+ # count: values.size,
115
+ # total_value: values.map { |d| d[:value] }.reduce(:+),
116
+ # raw: values
117
+ # }
118
+ # end
119
+
120
+ # # pp nest.entries(data)
121
+ # pp nest.map(data)
@@ -1,3 +1,4 @@
1
+ # coding: utf-8
1
2
  module Mondrian::REST
2
3
  module QueryHelper
3
4
 
@@ -18,18 +19,15 @@ module Mondrian::REST
18
19
 
19
20
  def get_member(cube, member_exp)
20
21
  begin
21
- rm = cube.raw_cube
22
- .lookupMember(org.olap4j.mdx.IdentifierNode.parseIdentifier(member_exp).getSegmentList)
22
+ return cube.member(member_exp)
23
23
  rescue Java::JavaLang::IllegalArgumentException
24
24
  error!("Illegal member expression: #{member_exp}", 400)
25
25
  end
26
- member = nil
27
- unless rm.nil?
28
- member = Mondrian::OLAP::Member.new(rm)
29
- end
30
- member
31
26
  end
32
27
 
28
+ ##
29
+ # Parses a string containing a 'cut' expression
30
+ # It can be a set (`{Dim.Mem, Dim2.Mem2}`), a range (`([Time].[Year].[1997]:[Time].[Year].[1998])`) or a member identifier (`[Time].[Year].[1998]`).
33
31
  def parse_cut(cube, cut_expr)
34
32
  p = mdx_parser.parseExpression(cut_expr)
35
33
 
@@ -38,13 +36,14 @@ module Mondrian::REST
38
36
  case p.getOperatorName
39
37
  when "{}"
40
38
  # check that the set contains only Members of a single dimension level
41
- ls = p.getArgList.map { |id_node|
42
- get_member(cube, unparse_node(id_node)).raw_level
43
- }.uniq
39
+ set_members = p.getArgList.map { |id_node|
40
+ get_member(cube, unparse_node(id_node))
41
+ }
42
+ ls = set_members.map(&:raw_level).uniq
44
43
  unless ls.size == 1
45
44
  error!("Illegal cut: " + cut_expr, 400)
46
45
  end
47
- { level: ls.first, cut: unparse_node(p), type: :set }
46
+ { level: ls.first, cut: unparse_node(p), type: :set, set_members: set_members }
48
47
  when "()"
49
48
  # check that the range contains a valid range
50
49
 
@@ -69,6 +68,11 @@ module Mondrian::REST
69
68
  # if `cut_expr` looks like a member, check that it's level is
70
69
  # equal to `level`
71
70
  m = get_member(cube, cut_expr)
71
+
72
+ if m.nil?
73
+ error!("Illegal cut: #{cut_expr} — Member does not exist", 400)
74
+ end
75
+
72
76
  { level: m.raw_level, cut: cut_expr, type: :member }
73
77
  else
74
78
  error!("Illegal cut: " + cut_expr, 400)
@@ -79,6 +83,13 @@ module Mondrian::REST
79
83
  # Parses a drilldown specification
80
84
  # XXX TODO write doc
81
85
  def parse_drilldown(cube, drilldown)
86
+
87
+ # check if the drilldown is a named set
88
+ named_sets = cube.named_sets
89
+ if ns = named_sets.find { |ns| ns.name == drilldown }
90
+ return ns
91
+ end
92
+
82
93
  begin
83
94
  s = org.olap4j.mdx.IdentifierNode.parseIdentifier(drilldown).getSegmentList
84
95
  rescue Java::JavaLang::IllegalArgumentException
@@ -148,8 +159,11 @@ module Mondrian::REST
148
159
  }
149
160
 
150
161
  dd = query_axes.map do |qa|
162
+ # if drilling down on a named set
163
+ if qa.kind_of?(Java::MondrianOlap4j::MondrianOlap4jNamedSet)
164
+ "[#{qa.name}]"
151
165
  # there's a slice (cut) on this axis
152
- if slicer_axis[qa.raw_level]
166
+ elsif slicer_axis[qa.raw_level]
153
167
  cut = slicer_axis.delete(qa.raw_level)
154
168
  case cut[:type]
155
169
  when :member
@@ -157,6 +171,24 @@ module Mondrian::REST
157
171
  else
158
172
  cut[:cut]
159
173
  end
174
+ elsif cut = slicer_axis.find { |lvl, cut|
175
+ qa.raw_level.hierarchy == lvl.hierarchy && lvl.depth < qa.depth
176
+ }
177
+ slicer_axis.delete(cut[0])
178
+ cut = cut[1]
179
+
180
+ case cut[:type]
181
+ when :member
182
+ "DESCENDANTS(#{cut[:cut]}, #{qa.full_name})"
183
+ when :set
184
+ # TODO
185
+ "{" + cut[:set_members].map { |m|
186
+ "DESCENDANTS(#{m.full_name}, #{qa.full_name})"
187
+ }.join(",") + "}"
188
+ when :range
189
+ # TODO
190
+ raise "Unsupported operation"
191
+ end
160
192
  else
161
193
  qa.raw_level.unique_name + '.Members'
162
194
  end
@@ -180,7 +212,7 @@ module Mondrian::REST
180
212
 
181
213
  # slicer axes (cut)
182
214
  if slicer_axis.size >= 1
183
- query = query.where(slicer_axis.values.map { |v| v[:cut] })
215
+ query = query.where(slicer_axis.values.map { |v| v[:cut] }.join(' * '))
184
216
  end
185
217
  query
186
218
  end
data/lib/mondrian_rest.rb CHANGED
@@ -5,5 +5,6 @@ Java::JavaLang::System.setProperty("jdbc.driver.autoload", "true")
5
5
  require 'mondrian-olap'
6
6
  require 'grape'
7
7
 
8
+ require_relative './mondrian_rest/nest.rb'
8
9
  require_relative './mondrian_rest/api.rb'
9
10
  require_relative './mondrian_rest/mondrian_ext.rb'
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
2
  Gem::Specification.new do |s|
3
3
  s.name = "mondrian-rest"
4
- s.version = '0.5.0'
4
+ s.version = '0.6.0'
5
5
  s.authors = ["Manuel Aristarán"]
6
6
  s.email = ["manuel@jazzido.com"]
7
7
  s.homepage = "https://github.com/jazzido/mondrian-rest"
@@ -14,14 +14,14 @@ Gem::Specification.new do |s|
14
14
  s.files = `git ls-files`.split("\n").reject { |f| f =~ /^spec\// }
15
15
  s.require_paths = ["lib"]
16
16
 
17
- s.add_runtime_dependency 'mondrian-olap', ["~> 0.7.0"]
18
- s.add_runtime_dependency 'grape', ["~> 0.14.0"]
17
+ s.add_runtime_dependency 'mondrian-olap', ["~> 0.8.0"]
18
+ s.add_runtime_dependency 'grape', ["~> 0.19.0"]
19
19
  s.add_runtime_dependency 'writeexcel', '~> 1.0', '>= 1.0.5'
20
20
 
21
- s.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
22
- s.add_development_dependency 'rspec', '~> 3.4', '>= 3.4.0'
23
- s.add_development_dependency 'jdbc-derby', '~> 10.11', '>= 10.11.1.1'
24
- s.add_development_dependency 'jdbc-sqlite3', '~> 3.8', '>= 3.8.11.2'
25
- s.add_development_dependency 'rack-test', '~> 0.6.3'
26
- s.add_development_dependency 'rubyzip', '~> 1.1', '>= 1.1.7'
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'rspec'
23
+ s.add_development_dependency 'jdbc-derby'
24
+ s.add_development_dependency 'jdbc-sqlite3'
25
+ s.add_development_dependency 'rack-test'
26
+ s.add_development_dependency 'rubyzip'
27
27
  end
metadata CHANGED
@@ -1,21 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mondrian-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.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: 2016-10-25 00:00:00.000000000 Z
11
+ date: 2017-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.7.0
18
+ version: 0.8.0
19
19
  name: mondrian-olap
20
20
  prerelease: false
21
21
  type: :runtime
@@ -23,13 +23,13 @@ dependencies:
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.7.0
26
+ version: 0.8.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.14.0
32
+ version: 0.19.0
33
33
  name: grape
34
34
  prerelease: false
35
35
  type: :runtime
@@ -37,7 +37,7 @@ dependencies:
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.14.0
40
+ version: 0.19.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
@@ -61,117 +61,87 @@ dependencies:
61
61
  - !ruby/object:Gem::Dependency
62
62
  requirement: !ruby/object:Gem::Requirement
63
63
  requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '10.4'
67
64
  - - ">="
68
65
  - !ruby/object:Gem::Version
69
- version: 10.4.2
66
+ version: '0'
70
67
  name: rake
71
68
  prerelease: false
72
69
  type: :development
73
70
  version_requirements: !ruby/object:Gem::Requirement
74
71
  requirements:
75
- - - "~>"
76
- - !ruby/object:Gem::Version
77
- version: '10.4'
78
72
  - - ">="
79
73
  - !ruby/object:Gem::Version
80
- version: 10.4.2
74
+ version: '0'
81
75
  - !ruby/object:Gem::Dependency
82
76
  requirement: !ruby/object:Gem::Requirement
83
77
  requirements:
84
- - - "~>"
85
- - !ruby/object:Gem::Version
86
- version: '3.4'
87
78
  - - ">="
88
79
  - !ruby/object:Gem::Version
89
- version: 3.4.0
80
+ version: '0'
90
81
  name: rspec
91
82
  prerelease: false
92
83
  type: :development
93
84
  version_requirements: !ruby/object:Gem::Requirement
94
85
  requirements:
95
- - - "~>"
96
- - !ruby/object:Gem::Version
97
- version: '3.4'
98
86
  - - ">="
99
87
  - !ruby/object:Gem::Version
100
- version: 3.4.0
88
+ version: '0'
101
89
  - !ruby/object:Gem::Dependency
102
90
  requirement: !ruby/object:Gem::Requirement
103
91
  requirements:
104
- - - "~>"
105
- - !ruby/object:Gem::Version
106
- version: '10.11'
107
92
  - - ">="
108
93
  - !ruby/object:Gem::Version
109
- version: 10.11.1.1
94
+ version: '0'
110
95
  name: jdbc-derby
111
96
  prerelease: false
112
97
  type: :development
113
98
  version_requirements: !ruby/object:Gem::Requirement
114
99
  requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: '10.11'
118
100
  - - ">="
119
101
  - !ruby/object:Gem::Version
120
- version: 10.11.1.1
102
+ version: '0'
121
103
  - !ruby/object:Gem::Dependency
122
104
  requirement: !ruby/object:Gem::Requirement
123
105
  requirements:
124
- - - "~>"
125
- - !ruby/object:Gem::Version
126
- version: '3.8'
127
106
  - - ">="
128
107
  - !ruby/object:Gem::Version
129
- version: 3.8.11.2
108
+ version: '0'
130
109
  name: jdbc-sqlite3
131
110
  prerelease: false
132
111
  type: :development
133
112
  version_requirements: !ruby/object:Gem::Requirement
134
113
  requirements:
135
- - - "~>"
136
- - !ruby/object:Gem::Version
137
- version: '3.8'
138
114
  - - ">="
139
115
  - !ruby/object:Gem::Version
140
- version: 3.8.11.2
116
+ version: '0'
141
117
  - !ruby/object:Gem::Dependency
142
118
  requirement: !ruby/object:Gem::Requirement
143
119
  requirements:
144
- - - "~>"
120
+ - - ">="
145
121
  - !ruby/object:Gem::Version
146
- version: 0.6.3
122
+ version: '0'
147
123
  name: rack-test
148
124
  prerelease: false
149
125
  type: :development
150
126
  version_requirements: !ruby/object:Gem::Requirement
151
127
  requirements:
152
- - - "~>"
128
+ - - ">="
153
129
  - !ruby/object:Gem::Version
154
- version: 0.6.3
130
+ version: '0'
155
131
  - !ruby/object:Gem::Dependency
156
132
  requirement: !ruby/object:Gem::Requirement
157
133
  requirements:
158
- - - "~>"
159
- - !ruby/object:Gem::Version
160
- version: '1.1'
161
134
  - - ">="
162
135
  - !ruby/object:Gem::Version
163
- version: 1.1.7
136
+ version: '0'
164
137
  name: rubyzip
165
138
  prerelease: false
166
139
  type: :development
167
140
  version_requirements: !ruby/object:Gem::Requirement
168
141
  requirements:
169
- - - "~>"
170
- - !ruby/object:Gem::Version
171
- version: '1.1'
172
142
  - - ">="
173
143
  - !ruby/object:Gem::Version
174
- version: 1.1.7
144
+ version: '0'
175
145
  description: A REST interface for Mondrian ROLAP server
176
146
  email:
177
147
  - manuel@jazzido.com
@@ -189,6 +159,7 @@ files:
189
159
  - lib/mondrian_rest/api_formatters.rb
190
160
  - lib/mondrian_rest/api_helpers.rb
191
161
  - lib/mondrian_rest/mondrian_ext.rb
162
+ - lib/mondrian_rest/nest.rb
192
163
  - lib/mondrian_rest/query_helper.rb
193
164
  - lib/mondrian_rest/version.rb
194
165
  - mondrian-rest.gemspec
@@ -212,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
183
  version: '0'
213
184
  requirements: []
214
185
  rubyforge_project:
215
- rubygems_version: 2.4.8
186
+ rubygems_version: 2.6.8
216
187
  signing_key:
217
188
  specification_version: 4
218
189
  summary: A REST interface for Mondrian ROLAP server