mondrian-rest 0.5.0-java → 0.6.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.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/README.md +1 -1
- data/lib/mondrian_rest/api.rb +36 -3
- data/lib/mondrian_rest/api_formatters.rb +72 -11
- data/lib/mondrian_rest/api_helpers.rb +41 -0
- data/lib/mondrian_rest/mondrian_ext.rb +82 -33
- data/lib/mondrian_rest/nest.rb +121 -0
- data/lib/mondrian_rest/query_helper.rb +45 -13
- data/lib/mondrian_rest.rb +1 -0
- data/mondrian-rest.gemspec +9 -9
- metadata +22 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2710889190b4d58cd7592415468caf4b7f5b7b95
|
4
|
+
data.tar.gz: 79bfcad96399941a47c1ba2e553e466209b053af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
data/lib/mondrian_rest/api.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
90
|
-
|
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
|
-
|
145
|
+
|
146
|
+
y.yield vdim + get_props(cm, pnames, props, dimensions) + msrs
|
113
147
|
else
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
9
|
-
|
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, :
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
42
|
-
get_member(cube, unparse_node(id_node))
|
43
|
-
}
|
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
|
-
|
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
data/mondrian-rest.gemspec
CHANGED
@@ -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.
|
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.
|
18
|
-
s.add_runtime_dependency 'grape', ["~> 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'
|
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'
|
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.
|
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:
|
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.
|
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.
|
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.
|
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.
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
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
|
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:
|
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:
|
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.
|
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
|