mondrian-rest 0.5.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fca6cfddd8cf42e7d87ec8e35f10b8389fdd08ff
4
+ data.tar.gz: 08b4017a7cdc2eebc4f0b2218b51822396061dd5
5
+ SHA512:
6
+ metadata.gz: 9fba6d67d771da283e085b0d89784baa093b5676b0c737aa89cdc5ef2b4ba1e9f3c9a68574db21b85759733e7f1161ec538e2051820bc641d446a62ac6577fd9
7
+ data.tar.gz: 44ed30d9adf06ddfbb7fe157a8a2a6d5e1765936b82fbdbc2b24bbc12d2dbf88f23c41285d3c1f745034e82d4228d8b936128de6583c1e7e51b5d17d8fcacc59
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .DS_Store
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - jruby-9.1.5.0
4
+ env:
5
+ - JRUBY_OPTS=--debug
6
+
7
+ branches:
8
+ only:
9
+ - public
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'http://rubygems.org'
2
+ gem 'mondrian-olap', :git => 'https://github.com/rsim/mondrian-olap.git', :branch => 'master'
3
+ gem 'coveralls', require: false
4
+
5
+ gemspec
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # mondrian-rest [![Build Status](https://travis-ci.org/jazzido/mondrian-rest.svg?branch=public)](https://travis-ci.org/jazzido/mondrian-rest) [![Coverage Status](https://coveralls.io/repos/github/jazzido/mondrian-rest/badge.svg?branch=public)](https://coveralls.io/github/jazzido/mondrian-rest?branch=public)
2
+
3
+ A REST endpoint for [Mondrian](http://community.pentaho.com/projects/mondrian/)
4
+
5
+ ## Usage
6
+
7
+ See [`mondrian-rest-demo`](https://github.com/jazzido/mondrian-rest-demo) and [`mondrian-rest-foodmart`](https://github.com/jazzido/mondrian-rest-foodmart) for example OLAP cubes implemented with `mondrian-rest`.
8
+
9
+ [`mondrian-rest-ui`](https://github.com/jazzido/mondrian-rest-ui) is an experimental visualization tool for `mondrian-rest`, inspired by [CubesViewer](https://github.com/jjmontesl/cubesviewer) and [Polestar](https://github.com/vega/polestar).
10
+
11
+ ## Credits
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.
14
+
15
+
16
+ ## License
17
+
18
+ The MIT License (MIT)
19
+
20
+ Copyright (c) 2015 Manuel Aristarán
21
+
22
+ Permission is hereby granted, free of charge, to any person obtaining a copy
23
+ of this software and associated documentation files (the "Software"), to deal
24
+ in the Software without restriction, including without limitation the rights
25
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
+ copies of the Software, and to permit persons to whom the Software is
27
+ furnished to do so, subject to the following conditions:
28
+
29
+ The above copyright notice and this permission notice shall be included in all
30
+ copies or substantial portions of the Software.
31
+
32
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
+ SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require "rspec/core/rake_task"
2
+
3
+ require_relative './lib/mondrian_rest.rb'
4
+
5
+ desc "Run specs"
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ RSpec::Core::RakeTask.new(:rcov) do |t|
8
+ t.rcov = true
9
+ t.rcov_opts = ['--exclude', '/Library,spec/']
10
+ end
11
+
12
+ desc "API Routes"
13
+ task :routes do
14
+ Mondrian::REST::Api.routes.each do |api|
15
+ method = api.route_method.ljust(10)
16
+ path = api.route_path
17
+ puts " #{method} #{path}"
18
+ end
19
+ end
20
+
21
+ task :default => [:spec]
@@ -0,0 +1,175 @@
1
+ require 'uri'
2
+
3
+ require_relative './api_helpers.rb'
4
+ require_relative './query_helper.rb'
5
+ require_relative './api_formatters.rb'
6
+
7
+ module Mondrian::REST
8
+
9
+ class Api < Grape::API
10
+ version '1', using: :header, vendor: 'mondrian_rest'
11
+ format :json
12
+
13
+ helpers Mondrian::REST::APIHelpers
14
+ helpers Mondrian::REST::QueryHelper
15
+ helpers do
16
+ def logger
17
+ Api.logger
18
+ end
19
+ end
20
+
21
+ resource :mdx do
22
+ content_type :xls, "application/vnd.ms-excel"
23
+ formatter :xls, Mondrian::REST::Formatters::XLS
24
+ content_type :csv, "text/csv"
25
+ formatter :csv, Mondrian::REST::Formatters::CSV
26
+ content_type :json, "application/json"
27
+ formatter :json, Mondrian::REST::Formatters::AggregationJSON
28
+
29
+ desc "Execute an MDX query against a cube"
30
+ content_type :txt, "text/plain"
31
+ post do
32
+ status 200
33
+
34
+ rbody = env['api.request.body'].force_encoding('utf-8')
35
+ mdx(rbody)
36
+ end
37
+ end
38
+
39
+ resource :cubes do
40
+ desc "Returns the cubes defined in this server's schema"
41
+ get do
42
+ {
43
+ 'cubes' => olap.cube_names.map { |cn| olap.cube(cn).to_h }
44
+ }
45
+ end
46
+
47
+ route_param :cube_name do
48
+ desc "Return a cube"
49
+ params do
50
+ requires :cube_name, type: String, desc: "Cube name"
51
+ end
52
+
53
+ get do
54
+ cube = get_cube_or_404(params[:cube_name])
55
+ cube.to_h
56
+ end
57
+
58
+ resource :members do
59
+ desc "return a member by its full name"
60
+ params do
61
+ requires :full_name,
62
+ type: String,
63
+ regexp: /[a-z0-9\.,\-\s%\\[\\]\(\)]+/i
64
+ end
65
+ get do
66
+ member_full_name = URI.decode(params[:full_name])
67
+
68
+ m = get_member(get_cube_or_404(params[:cube_name]),
69
+ member_full_name)
70
+ if m.nil?
71
+ error!("Member `#{member_full_name}` not found in cube `#{params[:cube_name]}`", 404)
72
+ end
73
+ m.to_h.merge({
74
+ ancestors: m.ancestors.map(&:to_h),
75
+ dimension: m.dimension_info
76
+ })
77
+ end
78
+ end
79
+
80
+
81
+ resource :aggregate do
82
+ content_type :xls, "application/vnd.ms-excel"
83
+ formatter :xls, Mondrian::REST::Formatters::XLS
84
+ content_type :csv, "text/csv"
85
+ formatter :csv, Mondrian::REST::Formatters::CSV
86
+ content_type :json, "application/json"
87
+ formatter :json, Mondrian::REST::Formatters::AggregationJSON
88
+
89
+ desc "aggregate from query parameters"
90
+ params do
91
+ optional :measures, type: Array
92
+ optional :cut, type: Array, desc: "Specification of slicer axis"
93
+ optional :drilldown, type: Array, desc: "Dimension(s) to be drilled down"
94
+ optional :nonempty, type: Boolean, desc: "Only return non empty cells"
95
+ optional :distinct, type: Boolean, desc: "Apply DISTINCT() to every axis"
96
+ optional :parents, type: Boolean, desc: "Include members' ancestors"
97
+ optional :debug, type: Boolean, desc: "Include generated MDX", default: false
98
+ optional :properties, type: Array, desc: "Include member properties"
99
+ end
100
+ get do
101
+ cube = get_cube_or_404(params[:cube_name])
102
+ query = build_query(cube, params)
103
+ mdx_query = query.to_mdx
104
+
105
+ result = mdx(query.to_mdx)
106
+ result.mdx = mdx_query if params[:debug]
107
+ result.properties = params[:properties]
108
+ result.cube = cube
109
+
110
+ result
111
+ end
112
+ end
113
+
114
+ resource :dimensions do
115
+ route_param :dimension_name do
116
+ desc "Return a dimension's members"
117
+ params do
118
+ requires :cube_name, type: String, desc: "Cube name"
119
+ requires :dimension_name, type: String, desc: "Dimension name"
120
+ end
121
+
122
+ get do
123
+ cube = get_cube_or_404(params[:cube_name])
124
+ dimension = cube.dimension(params[:dimension_name])
125
+ dimension.to_h
126
+ end
127
+
128
+ resource :levels do
129
+ route_param :level_name do
130
+ resource :members do
131
+
132
+ params do
133
+ optional :member_properties, type: Array
134
+ end
135
+
136
+ get do
137
+ cube = get_cube_or_404(params[:cube_name])
138
+
139
+ dimension = cube.dimension(params[:dimension_name])
140
+ if dimension.nil?
141
+ error!("dimension #{params[:dimension_name]} not found in cube #{params[:cube_name]}", 404)
142
+ end
143
+
144
+ level = dimension.hierarchies[0].level(params[:level_name])
145
+ if level.nil?
146
+ error!("level #{params[:level_name]} not found in dimension #{params[:dimension_name]}")
147
+ end
148
+
149
+ level.to_h(params[:member_properties])
150
+ end
151
+
152
+ route_param :member_key,
153
+ type: String,
154
+ requirements: { member_key: /[a-z0-9\.\-\s]+/i } do
155
+ get do
156
+ cube = get_cube_or_404(params[:cube_name])
157
+ dimension = cube.dimension(params[:dimension_name])
158
+ level = dimension.hierarchies[0].level(params[:level_name])
159
+
160
+ member = level.members.detect { |m|
161
+ m.property_value('MEMBER_KEY').to_s == params[:member_key]
162
+ }
163
+ error!('member not found', 404) if member.nil?
164
+ member.to_h.merge({ancestors: member.ancestors.map(&:to_h)})
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,126 @@
1
+ require 'csv'
2
+ require 'writeexcel'
3
+
4
+ module Mondrian::REST::Formatters
5
+
6
+ module AggregationJSON
7
+ def self.call(result, env)
8
+ add_parents = env['rack.request.query_hash']['parents'] == 'true'
9
+ debug = env['rack.request.query_hash']['debug'] == 'true'
10
+
11
+ result.to_h(add_parents, debug).to_json
12
+ end
13
+ end
14
+
15
+ module XLS
16
+ def self.call(result, env)
17
+ add_parents = env['rack.request.query_hash']['parents'] == 'true'
18
+ debug = env['rack.request.query_hash']['debug'] == 'true'
19
+
20
+ out = StringIO.new
21
+ book = WriteExcel.new(out)
22
+ sheet = book.add_worksheet
23
+
24
+ Mondrian::REST::Formatters
25
+ .tidy(result,
26
+ add_parents: add_parents,
27
+ debug: debug)
28
+ .each_with_index do |row, i|
29
+ row.each_with_index { |cell, j|
30
+ sheet.write(i, j, cell)
31
+ }
32
+ end
33
+
34
+ book.close
35
+ out.string
36
+ end
37
+ end
38
+
39
+ module CSV
40
+ def self.call(result, env)
41
+ add_parents = env['rack.request.query_hash']['parents'] == 'true'
42
+ debug = env['rack.request.query_hash']['debug'] == 'true'
43
+
44
+ rows = Mondrian::REST::Formatters.tidy(result,
45
+ add_parents: add_parents,
46
+ debug: debug)
47
+
48
+ ::CSV.generate do |csv|
49
+ rows.each { |row| csv << row }
50
+ end
51
+ end
52
+ end
53
+
54
+ ##
55
+ # Generate 'tidy data' (http://vita.had.co.nz/papers/tidy-data.pdf)
56
+ # from a result set.
57
+ def self.tidy(result, options)
58
+ cube = result.cube
59
+
60
+ add_parents = options[:add_parents]
61
+ rs = result.to_h(add_parents, options[:debug])
62
+ measures = rs[:axes].first[:members]
63
+ dimensions = rs[:axis_dimensions][1..-1]
64
+ columns = []
65
+ slices = []
66
+ level_has_all = []
67
+
68
+ Enumerator.new do |y|
69
+ dimensions.each do |dd|
70
+ if add_parents
71
+ hier = cube.dimension(dd[:name])
72
+ .hierarchies
73
+ .first
74
+
75
+ level_has_all << hier.has_all?
76
+ slices << dd[:level_depth]
77
+
78
+ hier
79
+ .levels[(hier.has_all? ? 1 : 0)...dd[:level_depth]]
80
+ .each do |ancestor_level|
81
+
82
+ columns += ["ID #{ancestor_level.caption}", ancestor_level.caption]
83
+ end
84
+ end
85
+
86
+ columns += ["ID #{dd[:level]}", dd[:level]]
87
+ end
88
+
89
+ # append measure columns and yield table header
90
+ y.yield columns + pluck(measures, :name)
91
+
92
+ prod = rs[:axes][1..-1].map { |e|
93
+ e[:members].map.with_index { |e_, i| [e_,i] }
94
+ }
95
+ values = rs[:values]
96
+
97
+ prod.shift.product(*prod).each do |cell|
98
+ cidxs = cell.map { |c,i| i }.reverse
99
+
100
+ cm = cell.map(&:first)
101
+
102
+ msrs = measures.map.with_index { |m, mi|
103
+ (cidxs + [mi]).reduce(values) { |_, idx| _[idx] }
104
+ }
105
+ if add_parents
106
+ vdim = cm.each.with_index.reduce([]) { |cnames, (member, j)|
107
+ member[:ancestors][0...slices[j] - (level_has_all[j] ? 1 : 0)].each { |ancestor|
108
+ cnames += [ancestor[:key], ancestor[:caption]]
109
+ }
110
+ cnames += [member[:key], member[:caption]]
111
+ }
112
+ y.yield vdim + msrs
113
+ else
114
+ y.yield pluck(cm, :key)
115
+ .zip(pluck(cm, :caption))
116
+ .flatten \
117
+ + msrs
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def self.pluck(a, m)
124
+ a.map { |e| e[m] }
125
+ end
126
+ end
@@ -0,0 +1,50 @@
1
+ module Mondrian::REST
2
+ module APIHelpers
3
+
4
+ @@olap = nil
5
+ @@mdx_parser = nil
6
+
7
+ def olap
8
+ if @@olap.nil?
9
+ @@olap = Mondrian::OLAP::Connection.new(env['mondrian-olap.params'])
10
+ @@olap.connect
11
+ end
12
+ @@olap
13
+ end
14
+
15
+ ##
16
+ # Returns an instance of org.olap4j.mdx.parser.MdxParser
17
+ def mdx_parser
18
+ if @@mdx_parser.nil?
19
+ @@mdx_parser = olap.raw_connection.getParserFactory
20
+ .createMdxParser(olap.raw_connection)
21
+ end
22
+ @@mdx_parser
23
+ end
24
+
25
+ def olap_flush
26
+ if olap.connected?
27
+ olap.flush_schema_cache
28
+ olap.close
29
+ end
30
+ olap.connect
31
+ end
32
+
33
+ def get_cube_or_404(name)
34
+ cube = olap.cube(name)
35
+ error!('Not found', 404) if cube.nil?
36
+ cube
37
+ end
38
+
39
+ def mdx(query)
40
+ logger.info("Executing MDX query #{query}")
41
+ begin
42
+ result = olap.execute query
43
+ return result
44
+ rescue Mondrian::OLAP::Error => st
45
+ error!({error: st.backtrace}, 400)
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,201 @@
1
+ module Mondrian
2
+ module OLAP
3
+
4
+ class Cube
5
+ def to_h
6
+ # gather dimensions
7
+ dimensions = self.dimensions
8
+ .find_all { |d| d.dimension_type != :measures }
9
+ .map { |d|
10
+ {
11
+ :name => d.name,
12
+ :caption => d.caption,
13
+ :type => d.dimension_type,
14
+ :annotations => d.annotations,
15
+ :hierarchies => d.hierarchies.map { |h|
16
+ {
17
+ :name => h.name,
18
+ :has_all => h.has_all?,
19
+ :all_member_name => h.all_member_name,
20
+ :levels => h.levels.map { |l|
21
+ {
22
+ :name => l.name,
23
+ :full_name => l.full_name,
24
+ :caption => l.caption
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ return {
33
+ :name => self.name,
34
+ :annotations => self.annotations,
35
+ :dimensions => dimensions,
36
+ :measures => self.dimensions
37
+ .find(&:measures?)
38
+ .hierarchy
39
+ .levels.first
40
+ .members
41
+ .find_all(&:visible?)
42
+ .map do |m|
43
+ {
44
+ :name => m.name,
45
+ :caption => m.caption,
46
+ :annotations => m.annotations,
47
+ :full_name => m.full_name
48
+ }
49
+ end
50
+ }
51
+ end
52
+
53
+ end
54
+
55
+ class Dimension
56
+ def to_h
57
+ {
58
+ hierarchies: self.hierarchies.map { |h|
59
+ {
60
+ name: h.name,
61
+ has_all: h.has_all?,
62
+ levels: h.levels.map(&:to_h) #/levels
63
+ } # /hierarchies
64
+ } #/map
65
+ } #/ dimension
66
+ end
67
+ end
68
+
69
+ class Level
70
+ attr_reader :hierarchy
71
+
72
+ def full_name
73
+ @full_name ||= @raw_level.getUniqueName
74
+ end
75
+
76
+ def to_h(member_properties=[])
77
+ {
78
+ name: self.name,
79
+ caption: self.caption,
80
+ members: self.members
81
+ .uniq { |m| m.property_value('MEMBER_KEY') }
82
+ .map { |m| m.to_h(member_properties) }
83
+ }
84
+ end
85
+ end
86
+
87
+ class Member
88
+
89
+ def raw_level
90
+ @raw_member.getLevel
91
+ end
92
+
93
+ def to_h(properties=[])
94
+ kv = [:name, :full_name, :caption, :all_member?,
95
+ :drillable?, :depth].map { |m|
96
+ [m, self.send(m)]
97
+ }
98
+ kv << [:key, self.property_value('MEMBER_KEY')]
99
+ kv << [:num_children, self.property_value('CHILDREN_CARDINALITY')]
100
+ kv << [:parent_name, self.property_value('PARENT_UNIQUE_NAME')]
101
+
102
+ if properties.size > 0
103
+ kv << [
104
+ :properties,
105
+ properties.reduce({}) { |h, p| h[p] = self.property_value(p); h }
106
+ ]
107
+ end
108
+
109
+ Hash[kv]
110
+ end
111
+
112
+ def dimension_info
113
+ d = @raw_member.getDimension
114
+ l = @raw_member.getLevel
115
+ {
116
+ name: d.getName,
117
+ caption: d.getCaption,
118
+ type: self.dimension_type,
119
+ level: l.getCaption,
120
+ level_depth: l.depth
121
+ }
122
+ end
123
+
124
+ def ancestors
125
+ @raw_member.getAncestorMembers.map { |am|
126
+ self.class.new(am)
127
+ }
128
+ end
129
+ end
130
+
131
+ class Result
132
+
133
+ attr_accessor :cube, :mdx, :properties
134
+
135
+ def to_json
136
+ to_h.to_json
137
+ end
138
+
139
+ def to_h(parents=false, debug=false)
140
+ # XXX TODO
141
+ # return the contents of the filter axis
142
+ # puts self.raw_cell_set.getFilterAxis.inspect
143
+
144
+ dimensions = self.axis_members.map { |am| am.first.dimension_info }
145
+
146
+ pprops = unless self.properties.nil?
147
+ parse_properties(dimensions[1..-1]) # exclude Measures dimension
148
+ else
149
+ {}
150
+ end
151
+
152
+ rv = {
153
+ axes: self.axis_members.each_with_index.map { |a, i|
154
+ {
155
+ members: a.map { |m|
156
+ mh = m.to_h(pprops[m.raw_member.getDimension.name] || [])
157
+ if parents
158
+ mh.merge!({
159
+ ancestors: m.ancestors.map { |ma|
160
+ ma.to_h(pprops[ma.raw_member.getDimension.name] || [])
161
+ }
162
+ })
163
+ end
164
+ mh
165
+ }
166
+ }
167
+ },
168
+ axis_dimensions: dimensions,
169
+ values: self.values
170
+ }
171
+
172
+ rv[:mdx] = self.mdx if debug
173
+
174
+ rv
175
+
176
+ 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
+ end
200
+ end
201
+ end
@@ -0,0 +1,188 @@
1
+ module Mondrian::REST
2
+ module QueryHelper
3
+
4
+ def unparse_node(node)
5
+ sw = java.io.StringWriter.new
6
+ ptw = org.olap4j.mdx.ParseTreeWriter.new(sw)
7
+ node.unparse(ptw)
8
+ sw.toString
9
+ end
10
+
11
+ def get_dimension(cube, dname)
12
+ cube_dimensions = cube.dimensions
13
+ .find_all { |d| d.dimension_type != :measures }
14
+ dim = cube_dimensions.find { |d| d.name == dname }
15
+ error!("Dimension #{dname} does not exist", 400) if dim.nil?
16
+ dim
17
+ end
18
+
19
+ def get_member(cube, member_exp)
20
+ begin
21
+ rm = cube.raw_cube
22
+ .lookupMember(org.olap4j.mdx.IdentifierNode.parseIdentifier(member_exp).getSegmentList)
23
+ rescue Java::JavaLang::IllegalArgumentException
24
+ error!("Illegal member expression: #{member_exp}", 400)
25
+ end
26
+ member = nil
27
+ unless rm.nil?
28
+ member = Mondrian::OLAP::Member.new(rm)
29
+ end
30
+ member
31
+ end
32
+
33
+ def parse_cut(cube, cut_expr)
34
+ p = mdx_parser.parseExpression(cut_expr)
35
+
36
+ case p
37
+ when org.olap4j.mdx.CallNode
38
+ case p.getOperatorName
39
+ when "{}"
40
+ # 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
44
+ unless ls.size == 1
45
+ error!("Illegal cut: " + cut_expr, 400)
46
+ end
47
+ { level: ls.first, cut: unparse_node(p), type: :set }
48
+ when "()"
49
+ # check that the range contains a valid range
50
+
51
+ unless p.getArgList.first.is_a?(org.olap4j.mdx.CallNode) \
52
+ and p.getArgList.first.getOperatorName == ':'
53
+ error!("Illegal cut: " + cut_expr, 400)
54
+ end
55
+
56
+ ls = p.getArgList.first.getArgList.map { |id_node|
57
+ get_member(cube, unparse_node(id_node)).raw_level
58
+ }.uniq
59
+
60
+ unless ls.size == 1
61
+ error!("Illegal cut: " + cut_expr, 400)
62
+ end
63
+
64
+ { level: ls.first, cut: unparse_node(p), type: :range }
65
+ else
66
+ error!("Illegal cut: " + cut_expr, 400)
67
+ end
68
+ when org.olap4j.mdx.IdentifierNode
69
+ # if `cut_expr` looks like a member, check that it's level is
70
+ # equal to `level`
71
+ m = get_member(cube, cut_expr)
72
+ { level: m.raw_level, cut: cut_expr, type: :member }
73
+ else
74
+ error!("Illegal cut: " + cut_expr, 400)
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Parses a drilldown specification
80
+ # XXX TODO write doc
81
+ def parse_drilldown(cube, drilldown)
82
+ begin
83
+ s = org.olap4j.mdx.IdentifierNode.parseIdentifier(drilldown).getSegmentList
84
+ rescue Java::JavaLang::IllegalArgumentException
85
+ error!("Illegal drilldown specification: #{drilldown}", 400)
86
+ end
87
+
88
+ if s.size > 3 || s.map(&:quoting).any? { |q| q.name == 'KEY' }
89
+ error!("Illegal drilldown specification: #{drilldown}", 400)
90
+ return
91
+ end
92
+
93
+ dimension = get_dimension(cube, s.first.name)
94
+ hierarchy = dimension.hierarchies.first
95
+ level = hierarchy.levels[hierarchy.has_all? ? 1 : 0]
96
+
97
+ if s.size > 1
98
+ if s.size == 3 # 3 parts, means that a hierarchy was provided
99
+ hierarchy = dimension.hierarchies.find { |h_| h_.name == "#{dimension.name}.#{s[1].name}" }
100
+ if hierarchy.nil?
101
+ error!("Hierarchy `#{s[1].name}` does not exist in dimension #{dimension.name}", 404)
102
+ end
103
+ end
104
+ level = hierarchy.levels.find { |l_| l_.name == s[s.size - 1].name }
105
+ if level.nil?
106
+ error!("Level `#{s[1].name}` does not exist in #{dimension.name}", 404)
107
+ end
108
+ end
109
+
110
+ level
111
+ end
112
+
113
+ def build_query(cube, options={})
114
+
115
+ measure_members = cube.dimension('Measures').hierarchy.levels.first.members
116
+ options = {
117
+ 'cut' => [],
118
+ 'drilldown' => [],
119
+ 'measures' => [measure_members.first.name],
120
+ 'nonempty' => false,
121
+ 'distinct' => false
122
+ }.merge(options)
123
+
124
+ # validate measures exist
125
+ cm_names = measure_members.map(&:name)
126
+
127
+ options['measures'].each { |m|
128
+ error!("Measure #{m} does not exist in cube #{cube.name}", 404) unless cm_names.include?(m)
129
+ }
130
+
131
+ # measures go in axis(0) of the resultset
132
+ query = olap.from(cube.name)
133
+ .axis(0,
134
+ *options['measures'].map { |m|
135
+ measure_members.find { |cm| cm.name == m }.full_name
136
+ })
137
+ if options['nonempty']
138
+ query = query.nonempty
139
+ end
140
+ axis_idx = 1
141
+
142
+ query_axes = options['drilldown'].map { |dd| parse_drilldown(cube, dd) }
143
+
144
+ slicer_axis = options['cut'].reduce({}) { |h, cut_expr|
145
+ pc = parse_cut(cube, cut_expr)
146
+ h[pc[:level]] = pc
147
+ h
148
+ }
149
+
150
+ dd = query_axes.map do |qa|
151
+ # there's a slice (cut) on this axis
152
+ if slicer_axis[qa.raw_level]
153
+ cut = slicer_axis.delete(qa.raw_level)
154
+ case cut[:type]
155
+ when :member
156
+ "{#{cut[:cut]}}"
157
+ else
158
+ cut[:cut]
159
+ end
160
+ else
161
+ qa.raw_level.unique_name + '.Members'
162
+ end
163
+ end
164
+
165
+ # query axes (drilldown)
166
+ dd.each do |ds|
167
+ query = query.axis(axis_idx,
168
+ ds)
169
+
170
+ if options['distinct']
171
+ query = query.distinct
172
+ end
173
+
174
+ if options['nonempty']
175
+ query = query.nonempty
176
+ end
177
+
178
+ axis_idx += 1
179
+ end
180
+
181
+ # slicer axes (cut)
182
+ if slicer_axis.size >= 1
183
+ query = query.where(slicer_axis.values.map { |v| v[:cut] })
184
+ end
185
+ query
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,3 @@
1
+ module Mondrian::REST
2
+ VERSION = "0.5.0"
3
+ end
@@ -0,0 +1,9 @@
1
+ require 'json'
2
+
3
+ Java::JavaLang::System.setProperty("jdbc.driver.autoload", "true")
4
+
5
+ require 'mondrian-olap'
6
+ require 'grape'
7
+
8
+ require_relative './mondrian_rest/api.rb'
9
+ require_relative './mondrian_rest/mondrian_ext.rb'
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ Gem::Specification.new do |s|
3
+ s.name = "mondrian-rest"
4
+ s.version = '0.5.0'
5
+ s.authors = ["Manuel Aristarán"]
6
+ s.email = ["manuel@jazzido.com"]
7
+ s.homepage = "https://github.com/jazzido/mondrian-rest"
8
+ s.summary = %q{A REST interface for Mondrian ROLAP server}
9
+ s.description = %q{A REST interface for Mondrian ROLAP server}
10
+ s.license = 'MIT'
11
+
12
+ s.platform = 'java'
13
+
14
+ s.files = `git ls-files`.split("\n").reject { |f| f =~ /^spec\// }
15
+ s.require_paths = ["lib"]
16
+
17
+ s.add_runtime_dependency 'mondrian-olap', ["~> 0.7.0"]
18
+ s.add_runtime_dependency 'grape', ["~> 0.14.0"]
19
+ s.add_runtime_dependency 'writeexcel', '~> 1.0', '>= 1.0.5'
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'
27
+ end
metadata ADDED
@@ -0,0 +1,219 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mondrian-rest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: java
6
+ authors:
7
+ - Manuel Aristarán
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.7.0
19
+ name: mondrian-olap
20
+ prerelease: false
21
+ type: :runtime
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.7.0
27
+ - !ruby/object:Gem::Dependency
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.14.0
33
+ name: grape
34
+ prerelease: false
35
+ type: :runtime
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.14.0
41
+ - !ruby/object:Gem::Dependency
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 1.0.5
50
+ name: writeexcel
51
+ prerelease: false
52
+ type: :runtime
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '1.0'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.0.5
61
+ - !ruby/object:Gem::Dependency
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '10.4'
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 10.4.2
70
+ name: rake
71
+ prerelease: false
72
+ type: :development
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '10.4'
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 10.4.2
81
+ - !ruby/object:Gem::Dependency
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '3.4'
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 3.4.0
90
+ name: rspec
91
+ prerelease: false
92
+ type: :development
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '3.4'
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.4.0
101
+ - !ruby/object:Gem::Dependency
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: '10.11'
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 10.11.1.1
110
+ name: jdbc-derby
111
+ prerelease: false
112
+ type: :development
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '10.11'
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 10.11.1.1
121
+ - !ruby/object:Gem::Dependency
122
+ requirement: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '3.8'
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: 3.8.11.2
130
+ name: jdbc-sqlite3
131
+ prerelease: false
132
+ type: :development
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '3.8'
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: 3.8.11.2
141
+ - !ruby/object:Gem::Dependency
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: 0.6.3
147
+ name: rack-test
148
+ prerelease: false
149
+ type: :development
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: 0.6.3
155
+ - !ruby/object:Gem::Dependency
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '1.1'
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: 1.1.7
164
+ name: rubyzip
165
+ prerelease: false
166
+ type: :development
167
+ version_requirements: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - "~>"
170
+ - !ruby/object:Gem::Version
171
+ version: '1.1'
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: 1.1.7
175
+ description: A REST interface for Mondrian ROLAP server
176
+ email:
177
+ - manuel@jazzido.com
178
+ executables: []
179
+ extensions: []
180
+ extra_rdoc_files: []
181
+ files:
182
+ - ".gitignore"
183
+ - ".travis.yml"
184
+ - Gemfile
185
+ - README.md
186
+ - Rakefile
187
+ - lib/mondrian_rest.rb
188
+ - lib/mondrian_rest/api.rb
189
+ - lib/mondrian_rest/api_formatters.rb
190
+ - lib/mondrian_rest/api_helpers.rb
191
+ - lib/mondrian_rest/mondrian_ext.rb
192
+ - lib/mondrian_rest/query_helper.rb
193
+ - lib/mondrian_rest/version.rb
194
+ - mondrian-rest.gemspec
195
+ homepage: https://github.com/jazzido/mondrian-rest
196
+ licenses:
197
+ - MIT
198
+ metadata: {}
199
+ post_install_message:
200
+ rdoc_options: []
201
+ require_paths:
202
+ - lib
203
+ required_ruby_version: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
208
+ required_rubygems_version: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
213
+ requirements: []
214
+ rubyforge_project:
215
+ rubygems_version: 2.4.8
216
+ signing_key:
217
+ specification_version: 4
218
+ summary: A REST interface for Mondrian ROLAP server
219
+ test_files: []