mondrian-rest 0.5.0-java

Sign up to get free protection for your applications and to get access to all the features.
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: []