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