rptman 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ 0.0.1 (Pending)
2
+ * Initial Release
data/LICENSE ADDED
@@ -0,0 +1,176 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
@@ -0,0 +1,105 @@
1
+ = rptman
2
+
3
+ This is a command line tool and suite of rake tasks for uploading SSRS
4
+ reports to a server. The tool can also generate project files for
5
+ the "SQL Server Business Intelligence Development Studio".
6
+
7
+ == Installation
8
+
9
+ The extension is packaged as a jruby gem named "rptman", consult the ruby
10
+ gems installation steps but typically it is
11
+
12
+ jgem install rptman
13
+
14
+ == Basic Overview
15
+
16
+ Reports are stored in sub-directories of a <report-dir>. The filename
17
+ of the report must end with ".rdl". The directory hierarchy on the local
18
+ file system is mirrored on the SSRS server during an upload. So if a file
19
+ exists with the name
20
+
21
+ <report-dir>/IRIS/Coordination/Tour Of Duty Report.rdl
22
+
23
+ It will be uploaded to the SSRS server with the path
24
+
25
+ /IRIS/Coordination/Tour Of Duty Report
26
+
27
+ Every top level directory on the local filesystem that includes a report
28
+ file is deleted when it during the upload process. So in the above scenario
29
+ the /IRIS directory on SSRS server is delted prior to uploading the
30
+ directory. It is assumed that every report in a particular hierarhcy is
31
+ stored in one location.
32
+
33
+ The tool can also create data source definitions and these are placed in a
34
+ directory named "/DataSources".
35
+
36
+ The gem supports prefixing all paths with when uploading to the SSRS server.
37
+ It is possible to prefix all reports managed by this tool with /Auto so as
38
+ to distinguish them from reports uploaded through other means. If multiple
39
+ people are working of the same SSRS instance it is also possible to decorate
40
+ the prefix with a username or environment. (i.e. /Auto/PD42/DEV)
41
+
42
+ The configuration data for determing which SSRS instance and prefix to use
43
+ is in a yaml file with a format described below. The easiest way to use the
44
+ tool is to create a ruby script such as the following;
45
+
46
+ gem 'rptman'
47
+
48
+ # The configuration file
49
+ SSRS::Config.config_filename = "database.yml"
50
+
51
+ # The directory in which the reports are stored
52
+ SSRS::Config.reports_dir = "reports"
53
+
54
+ # Define a data source named IRIS_CENTRAL that has
55
+ # configration data stored under the key 'central'
56
+ SSRS::Config.define_datasource('IRIS_CENTRAL','central')
57
+
58
+ # actually run the tool
59
+ SSRS::Shell.run
60
+
61
+ The script can then be run with a -h parameter to see the various options.
62
+
63
+ == Configuration Format
64
+
65
+ The configuration is stored in a yaml file. The configuration file format
66
+ allows for multiple "environments" (a.k.a. configuration) in one file. Most
67
+ configuration files will have "development" and "production" envioronments.
68
+ There must be one section for each SQL Server or SSRS instance. Each section
69
+ is named "<key>_<environment>" where the key for SSRS servers is "ssrs". The
70
+ key for SQL Server instances must correspond to the key specified in the
71
+ invocation of "SSRS::Config.define_datasource(<name>,<key>)" above.
72
+
73
+ The SSRS server must specify the report_target and prefix keys. The SQL Server
74
+ instances must specify the database and host keys and may optionally specify
75
+ the username, password and isntance keys. If username and password are not
76
+ specified then integrated security is used.
77
+
78
+ Here is an example configuration file:
79
+
80
+ ssrs_development:
81
+ report_target: http://ssrs-dev.example.com/SSRS01_WS
82
+ prefix: /Auto/PD42/DEV
83
+
84
+ central_development:
85
+ database: PD42_IRIS_CENTRAL_DEV
86
+ username: MyUsername
87
+ password: MyPassword
88
+ host: sqlserver-dev.example.com
89
+ instance: myinstance
90
+
91
+ ssrs_production:
92
+ report_target: http://ssrs.example.com/SSRS01_WS
93
+ prefix: /Auto
94
+
95
+ central_production:
96
+ database: IRIS_CENTRAL
97
+ username: MyUsername
98
+ password: MyPassword
99
+ host: sqlserver.example.com
100
+ instance: myinstance
101
+
102
+ == Credit
103
+
104
+ The gem was initially developed as by StockSoftware for use in the Department
105
+ of Sustainability and Environment, Victoria, Australia.
@@ -0,0 +1,14 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+ require 'optparse'
4
+
5
+ require "#{File.dirname(__FILE__)}/ssrs/ssrs-api.jar"
6
+ require "#{File.dirname(__FILE__)}/ssrs/UUID.rb"
7
+ require "#{File.dirname(__FILE__)}/ssrs/config.rb"
8
+ require "#{File.dirname(__FILE__)}/ssrs/ssrs.rb"
9
+ require "#{File.dirname(__FILE__)}/ssrs/datasource.rb"
10
+ require "#{File.dirname(__FILE__)}/ssrs/report_project.rb"
11
+ require "#{File.dirname(__FILE__)}/ssrs/report.rb"
12
+ require "#{File.dirname(__FILE__)}/ssrs/uploader.rb"
13
+ require "#{File.dirname(__FILE__)}/ssrs/bids.rb"
14
+ require "#{File.dirname(__FILE__)}/ssrs/shell.rb"
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright(c) 2005 URABE, Shyouhei.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this code, to deal in the code without restriction, including without
6
+ # limitation the rights to use, copy, modify, merge, publish, distribute,
7
+ # sublicense, and/or sell copies of the code, and to permit persons to whom the
8
+ # code is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be
11
+ # included in all copies or substantial portions of the code.
12
+ #
13
+ # THE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE CODE OR THE USE OR OTHER DEALINGS IN THE
19
+ # CODE.
20
+
21
+ %w[
22
+ digest/md5
23
+ digest/sha1
24
+ socket
25
+ tmpdir
26
+ ].each do |f|
27
+ require f
28
+ end
29
+
30
+ module SSRS
31
+ # Pure ruby UUID generator, which is compatible with RFC4122
32
+ UUID = Struct.new "UUID", :raw_bytes
33
+ class UUID
34
+ private_class_method :new
35
+
36
+ class << self
37
+ def mask str # :nodoc
38
+ v = str[7]
39
+ v = v & 0b00001111
40
+ v = v | 0b01010000
41
+ str[7] = v
42
+ r = str[8]
43
+ r = r & 0b00111111
44
+ r = r | 0b10000000
45
+ str[8] = r
46
+ str
47
+ end
48
+ private :mask
49
+
50
+ # UUID generation using SHA1. Recommended over create_md5.
51
+ # Namespace object is another UUID, some of them are pre-defined below.
52
+ def create_sha1 str, namespace
53
+ sha1 = Digest::SHA1.new
54
+ sha1.update namespace.raw_bytes
55
+ sha1.update str
56
+ sum = sha1.digest
57
+ raw = mask sum[0..15]
58
+ ret = new raw
59
+ ret.raw_bytes.freeze
60
+ ret.freeze
61
+ ret
62
+ end
63
+
64
+ # UUID generation using MD5 (for backward compat.)
65
+ def create_md5 str, namespace
66
+ md5 = Digest::MD5.new
67
+ md5.update namespace.raw_bytes
68
+ md5.update str
69
+ sum = md5.digest
70
+ raw = mask sum[0..16]
71
+ ret = new raw
72
+ ret.raw_bytes.freeze
73
+ ret.freeze
74
+ ret
75
+ end
76
+
77
+ # UUID generation using random-number generator. From it's random
78
+ # nature, there's no warranty that the created ID is really universaly
79
+ # unique.
80
+ def create_random
81
+ rnd = [
82
+ rand(0x100000000),
83
+ rand(0x100000000),
84
+ rand(0x100000000),
85
+ rand(0x100000000),
86
+ ].pack "N4"
87
+ raw = mask rnd
88
+ ret = new raw
89
+ ret.raw_bytes.freeze
90
+ ret.freeze
91
+ ret
92
+ end
93
+
94
+ def read_state fp # :nodoc:
95
+ fp.rewind
96
+ Marshal.load fp.read
97
+ end
98
+
99
+ def write_state fp, c, m # :nodoc:
100
+ fp.rewind
101
+ str = Marshal.dump [c, m]
102
+ fp.write str
103
+ end
104
+
105
+ private :read_state, :write_state
106
+ STATE_FILE = 'ruby-uuid'
107
+
108
+ # create the "version 1" UUID with current system clock, current UTC
109
+ # timestamp, and the IEEE 802 address (so-called MAC address).
110
+ #
111
+ # Speed notice: it's slow. It writes some data into hard drive on every
112
+ # invokation. If you want to speed this up, try remounting tmpdir with a
113
+ # memory based filesystem (such as tmpfs). STILL slow? then no way but
114
+ # rewrite it with c :)
115
+ def create( clock=nil, time=nil, mac_addr=nil )
116
+ c = t = m = nil
117
+ Dir.chdir Dir.tmpdir do
118
+ unless FileTest.exist? STATE_FILE then
119
+ # Generate a pseudo MAC address because we have no pure-ruby way
120
+ # to know the MAC address of the NIC this system uses. Note
121
+ # that cheating with pseudo arresses here is completely legal:
122
+ # see Section 4.5 of RFC4122 for details.
123
+ sha1 = Digest::SHA1.new
124
+ 256.times do
125
+ r = [rand(0x100000000)].pack "N"
126
+ sha1.update r
127
+ end
128
+ str = sha1.digest
129
+ r = rand 34 # 40-6
130
+ node = str[r, 6] || str
131
+ node[0] |= 0x01 # multicast bit
132
+ k = rand 0x40000
133
+ open STATE_FILE, 'wb' do |fp|
134
+ fp.flock IO::LOCK_EX
135
+ write_state fp, k, node
136
+ fp.chmod 0o777 # must be world writable
137
+ end
138
+ end
139
+ open STATE_FILE, 'r+b' do |fp|
140
+ fp.flock IO::LOCK_EX
141
+ c, m = read_state fp
142
+ c = clock % 0x4000 if clock
143
+ m = mac_addr if mac_addr
144
+ t = time
145
+ if t.nil? then
146
+ # UUID epoch is 1582/Oct/15
147
+ tt = Time.now
148
+ t = tt.to_i*10000000 + tt.tv_usec*10 + 0x01B21DD213814000
149
+ end
150
+ c = c.succ # important; increment here
151
+ write_state fp, c, m
152
+ end
153
+ end
154
+
155
+ tl = t & 0xFFFF_FFFF
156
+ tm = t >> 32
157
+ tm = tm & 0xFFFF
158
+ th = t >> 48
159
+ th = th & 0x0FFF
160
+ th = th | 0x1000
161
+ cl = c & 0xFF
162
+ ch = c & 0x3F00
163
+ ch = ch >> 8
164
+ ch = ch | 0x80
165
+ pack tl, tm, th, cl, ch, m
166
+ end
167
+
168
+ # A simple GUID parser: just ignores unknown characters and convert
169
+ # hexadecimal dump into 16-octet object.
170
+ def parse obj
171
+ str = obj.to_s.sub %r/\Aurn:uuid:/, ''
172
+ str.gsub! %r/[^0-9A-Fa-f]/, ''
173
+ raw = str[0..31].to_a.pack 'H*'
174
+ ret = new raw
175
+ ret.raw_bytes.freeze
176
+ ret.freeze
177
+ ret
178
+ end
179
+
180
+ # The 'primitive constructor' of this class
181
+ # Note UUID.pack(uuid.unpack) == uuid
182
+ def pack tl, tm, th, ch, cl, n
183
+ raw = [tl, tm, th, ch, cl, n].pack "NnnCCa6"
184
+ ret = new raw
185
+ ret.raw_bytes.freeze
186
+ ret.freeze
187
+ ret
188
+ end
189
+ end
190
+
191
+ # The 'primitive deconstructor', or the dual to pack.
192
+ # Note UUID.pack(uuid.unpack) == uuid
193
+ def unpack
194
+ raw_bytes.unpack "NnnCCa6"
195
+ end
196
+
197
+ # Generate the string representation (a.k.a GUID) of this UUID
198
+ def to_s
199
+ a = unpack
200
+ tmp = a[-1].unpack 'C*'
201
+ a[-1] = sprintf '%02x%02x%02x%02x%02x%02x', *tmp
202
+ "%08x-%04x-%04x-%02x%02x-%s" % a
203
+ end
204
+ alias guid to_s
205
+
206
+ # Convert into a RFC4122-comforming URN representation
207
+ def to_uri
208
+ "urn:uuid:" + self.to_s
209
+ end
210
+ alias urn to_uri
211
+
212
+ # Convert into 128-bit unsigned integer
213
+ # Typically a Bignum instance, but can be a Fixnum.
214
+ def to_int
215
+ tmp = self.raw_bytes.unpack "C*"
216
+ tmp.inject do |r, i|
217
+ r * 256 | i
218
+ end
219
+ end
220
+ alias to_i to_int
221
+
222
+ # Two UUIDs are said to be equal if and only if their (byte-order
223
+ # canonicalized) integer representations are equivallent. Refer RFC4122 for
224
+ # details.
225
+ def == other
226
+ to_i == other.to_i
227
+ end
228
+
229
+ include Comparable
230
+ # UUIDs are comparable (don't know what benefits are there, though).
231
+ def <=> other
232
+ to_s <=> other.to_s
233
+ end
234
+
235
+ # Pre-defined UUID Namespaces described in RFC4122 Appendix C.
236
+ NameSpace_DNS = parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
237
+ NameSpace_URL = parse "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
238
+ NameSpace_OID = parse "6ba7b812-9dad-11d1-80b4-00c04fd430c8"
239
+ NameSpace_X500 = parse "6ba7b814-9dad-11d1-80b4-00c04fd430c8"
240
+
241
+ # The Nil UUID in RFC4122 Section 4.1.7
242
+ Nil = parse "00000000-0000-0000-0000-000000000000"
243
+ end
244
+
245
+ if __FILE__ == $0 then
246
+ require 'test/unit'
247
+
248
+ class TC_UUID < Test::Unit::TestCase
249
+ def test_v1
250
+ u1 = UUID.create
251
+ u2 = UUID.create
252
+ assert_not_equal u1, u2
253
+ end
254
+
255
+ def test_v1_repeatability
256
+ u1 = UUID.create 1, 2, "345678"
257
+ u2 = UUID.create 1, 2, "345678"
258
+ assert_equal u1, u2
259
+ end
260
+
261
+ def test_v3
262
+ u1 = UUID.create_md5 "foo", UUID::NameSpace_DNS
263
+ u2 = UUID.create_md5 "foo", UUID::NameSpace_DNS
264
+ u3 = UUID.create_md5 "foo", UUID::NameSpace_URL
265
+ assert_equal u1, u2
266
+ assert_not_equal u1, u3
267
+ end
268
+
269
+ def test_v5
270
+ u1 = UUID.create_sha1 "foo", UUID::NameSpace_DNS
271
+ u2 = UUID.create_sha1 "foo", UUID::NameSpace_DNS
272
+ u3 = UUID.create_sha1 "foo", UUID::NameSpace_URL
273
+ assert_equal u1, u2
274
+ assert_not_equal u1, u3
275
+ end
276
+
277
+ def test_v4
278
+ # This test is not perfect, because the random nature of version 4
279
+ # UUID it is not always true that the three objects below really
280
+ # differ. But in real life it's enough to say we're OK when this
281
+ # passes.
282
+ u1 = UUID.create_random
283
+ u2 = UUID.create_random
284
+ u3 = UUID.create_random
285
+ assert_not_equal u1.raw_bytes, u2.raw_bytes
286
+ assert_not_equal u1.raw_bytes, u3.raw_bytes
287
+ assert_not_equal u2.raw_bytes, u3.raw_bytes
288
+ end
289
+
290
+ def test_pack
291
+ u1 = UUID.pack 0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4,
292
+ "\000\300O\3240\310"
293
+ assert_equal UUID::NameSpace_DNS, u1
294
+ end
295
+
296
+ def test_unpack
297
+ tl, tm, th, cl, ch, m = UUID::NameSpace_DNS.unpack
298
+ assert_equal 0x6ba7b810, tl
299
+ assert_equal 0x9dad, tm
300
+ assert_equal 0x11d1, th
301
+ assert_equal 0x80, cl
302
+ assert_equal 0xb4, ch
303
+ assert_equal "\000\300O\3240\310", m
304
+ end
305
+
306
+ def test_parse
307
+ u1 = UUID.pack 0x6ba7b810, 0x9dad, 0x11d1, 0x80, 0xb4,
308
+ "\000\300O\3240\310"
309
+ u2 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
310
+ u3 = UUID.parse "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
311
+ assert_equal u1, u2
312
+ assert_equal u1, u3
313
+ end
314
+
315
+ def test_to_s
316
+ u1 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
317
+ assert_equal "6ba7b810-9dad-11d1-80b4-00c04fd430c8", u1.to_s
318
+ end
319
+
320
+ def test_to_i
321
+ u1 = UUID.parse "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
322
+ assert_equal 0x6ba7b8109dad11d180b400c04fd430c8, u1.to_i
323
+ end
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,35 @@
1
+ module SSRS
2
+ # Generator for "SQL Server Business Intelligence Development Studio" projects
3
+ class BIDS
4
+ def self.generate
5
+ self.generate_data_sources
6
+ self.generate_project_files
7
+ end
8
+
9
+ private
10
+
11
+ def self.generate_project_files
12
+ SSRS::Config.upload_dirs.each do |upload_dir|
13
+ SSRS.info("Generating Project for #{upload_dir}")
14
+ actual_dir = File.expand_path("#{SSRS::Config.reports_dir}/#{upload_dir}")
15
+ filename = File.expand_path("#{SSRS::Config.projects_dir}/#{upload_dir[1,upload_dir.size].gsub('/', '_')}.rptproj")
16
+ project = SSRS::ReportProject.new(upload_dir, filename, actual_dir)
17
+ File.open(filename, 'w') do |f|
18
+ project.write(f)
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.generate_data_sources
24
+ FileUtils.rm_rf SSRS::Config.projects_dir
25
+ FileUtils.mkdir_p SSRS::Config.projects_dir
26
+ SSRS::Config.datasources.each do |ds|
27
+ filename = "#{SSRS::Config.projects_dir}/#{ds.name}.rds"
28
+ SSRS.info("Generating DataSource #{ds.name}")
29
+ File.open(filename, 'w') do |f|
30
+ ds.write(f)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,139 @@
1
+ module SSRS
2
+ class Config
3
+ class Server
4
+ attr_reader :report_target
5
+ attr_reader :upload_prefix
6
+
7
+ def initialize(report_target, upload_prefix)
8
+ @report_target = report_target
9
+ @upload_prefix = upload_prefix
10
+ end
11
+ end
12
+
13
+ class << self
14
+ attr_writer :environment
15
+
16
+ def environment
17
+ return 'development' unless @environment
18
+ @environment
19
+ end
20
+
21
+ # config_file is where the yaml config file is located
22
+ attr_writer :config_filename
23
+
24
+ def config_filename
25
+ raise "config_filename not specified" unless @config_filename
26
+ @config_filename
27
+ end
28
+
29
+ # reports_dir is where the report hierarchy is located
30
+ attr_writer :reports_dir
31
+
32
+ def reports_dir
33
+ raise "reports_dir not specified" unless @reports_dir
34
+ @reports_dir
35
+ end
36
+
37
+ # projects_dir is where the VS projects are generated
38
+ attr_writer :projects_dir
39
+
40
+ def projects_dir
41
+ return "#{self.reports_dir}/projects" unless @projects_dir
42
+ return @projects_dir
43
+ end
44
+
45
+ def datasources
46
+ datasources_map.values
47
+ end
48
+
49
+ def define_datasource(name, database_key)
50
+ data_source = SSRS::DataSource.new(name)
51
+ configure_datasource(data_source, database_key)
52
+ datasources_map[name] = data_source
53
+ end
54
+
55
+ def reports
56
+ unless @reports
57
+ @reports = Dir.glob("#{self.reports_dir}/**/*.rdl").collect do |filename|
58
+ SSRS::Report.new(upload_path(filename), filename)
59
+ end
60
+ end
61
+ return @reports
62
+ end
63
+
64
+ # Return list of dirs uploaded
65
+ def upload_dirs
66
+ self.reports.collect {|report| File.dirname(report.name)}.sort.uniq
67
+ end
68
+
69
+ def upload_prefix
70
+ current_ssrs_config.upload_prefix
71
+ end
72
+
73
+ def wsdl_path
74
+ "#{report_target}/ReportService2005.asmx"
75
+ end
76
+
77
+ def report_target
78
+ current_ssrs_config.report_target
79
+ end
80
+
81
+ def server_config(env_key)
82
+ load_ssrs_config(env_key)
83
+ end
84
+
85
+ private
86
+
87
+ def upload_path(filename)
88
+ symbolic_path = filename.gsub(Regexp.escape(reports_dir), '')
89
+ return "#{File.dirname(symbolic_path)}/#{File.basename(symbolic_path,'.rdl')}"
90
+ end
91
+
92
+ def current_ssrs_config
93
+ @server ||= load_ssrs_config(environment)
94
+ end
95
+
96
+ def load_ssrs_config(env_key)
97
+ config_key = "ssrs_#{env_key}"
98
+ config = config_for_key(config_key)
99
+ report_target = expect_config_element(config_key, config, 'report_target').to_s
100
+ upload_prefix = expect_config_element(config_key, config, 'prefix').to_s
101
+ SSRS::Config::Server.new(report_target, upload_prefix)
102
+ end
103
+
104
+ def configure_datasource(data_source, database_key)
105
+ config_key = "#{database_key}_#{environment}"
106
+ config = config_for_key(config_key)
107
+
108
+ data_source.host = expect_config_element(config_key, config, 'host')
109
+ data_source.database = expect_config_element(config_key, config, 'database')
110
+ data_source.instance = config['instance']
111
+ data_source.username = config['username']
112
+ data_source.password = config['password']
113
+ end
114
+
115
+ def expect_config_element(config_key, config, element_key)
116
+ raise "Missing #{element_key} for #{config_key} database config" unless config[element_key]
117
+ config[element_key]
118
+ end
119
+
120
+ def config_for_key(config_key)
121
+ c = config_data[config_key]
122
+ raise "Missing configuration #{config_key} in #{self.config_filename}" unless c
123
+ c
124
+ end
125
+
126
+ def config_data
127
+ unless @config_data
128
+ raise "Unable to locate config file #{self.config_filename}" unless File.exist?(self.config_filename)
129
+ @config_data = ::YAML::load(ERB.new(IO.read(self.config_filename)).result)
130
+ end
131
+ @config_data
132
+ end
133
+
134
+ def datasources_map
135
+ @datasources ||= {}
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,41 @@
1
+ module SSRS
2
+ class DataSource
3
+ BASE_PATH = 'DataSources'
4
+ attr_accessor :name, :host, :instance, :database, :datasource_id, :username, :password
5
+
6
+ def initialize(name)
7
+ self.name = name
8
+ self.datasource_id = SSRS::UUID.create.to_s
9
+ end
10
+
11
+ def host_spec
12
+ "#{self.host}#{self.instance.nil? ? '' : '\\'}#{self.instance}"
13
+ end
14
+
15
+ def symbolic_name
16
+ "#{DataSource::BASE_PATH}/#{self.name}"
17
+ end
18
+
19
+ def connection_string
20
+ auth_details = unless ( self.username || self.password )
21
+ 'Integrated Security=SSPI'
22
+ else
23
+ "User Id=#{self.username};Password=#{self.password}"
24
+ end
25
+ "Data Source=#{self.host_spec};Initial Catalog=#{self.database};#{auth_details};"
26
+ end
27
+
28
+ def write(file)
29
+ file.write <<XML
30
+ <RptDataSource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
31
+ <Name>#{self.name}</Name>
32
+ <ConnectionProperties>
33
+ <Extension>SQL</Extension>
34
+ <ConnectString>#{connection_string}</ConnectString>
35
+ </ConnectionProperties>
36
+ <DataSourceID>#{self.datasource_id}</DataSourceID>
37
+ </RptDataSource>
38
+ XML
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ module SSRS
2
+ class Report
3
+ attr_reader :name
4
+ attr_reader :filename
5
+
6
+ def initialize(name, filename)
7
+ @name, @filename = name, filename
8
+ end
9
+
10
+ def generate_upload_version
11
+ require 'tempfile'
12
+ file = Tempfile.new("ssrs_report")
13
+ xformed_document.write file
14
+ xformed_filename = file.path
15
+ file.close
16
+ xformed_filename
17
+ end
18
+
19
+ protected
20
+
21
+ def xformed_document
22
+ document = self.document
23
+ REXML::XPath.each(document.root, "//Report/DataSources/DataSource/DataSourceReference") do |element|
24
+ text_node = element.get_text
25
+ text_node.value = "#{SSRS::Config.upload_prefix}/#{DataSource::BASE_PATH}/#{text_node.value}"
26
+ end
27
+ document
28
+ end
29
+
30
+ def document
31
+ require 'rexml/document'
32
+ REXML::Document.new(File.read(self.filename))
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,83 @@
1
+ module SSRS
2
+ class ReportProject
3
+ attr_reader :name
4
+ attr_accessor :project_filename
5
+ attr_accessor :dir
6
+
7
+ def initialize(name, project_filename, dir)
8
+ @name, @project_filename, @dir = name, project_filename, dir
9
+ end
10
+
11
+ def write(file)
12
+ file.write <<XML
13
+ <?xml version="1.0" encoding="utf-8"?>
14
+ <Project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
15
+ <State>$base64$PFNvdXJjZUNvbnRyb2xJbmZvIHhtbG5zOnhzZD0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOmRkbDI9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vYW5hbHlzaXNzZXJ2aWNlcy8yMDAzL2VuZ2luZS8yIiB4bWxuczpkZGwyXzI9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vYW5hbHlzaXNzZXJ2aWNlcy8yMDAzL2VuZ2luZS8yLzIiIHhtbG5zOmRkbDEwMF8xMDA9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vYW5hbHlzaXNzZXJ2aWNlcy8yMDA4L2VuZ2luZS8xMDAvMTAwIiB4bWxuczpkd2Q9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vRGF0YVdhcmVob3VzZS9EZXNpZ25lci8xLjAiPg0KICA8RW5hYmxlZD5mYWxzZTwvRW5hYmxlZD4NCiAgPFByb2plY3ROYW1lPjwvUHJvamVjdE5hbWU+DQogIDxBdXhQYXRoPjwvQXV4UGF0aD4NCiAgPExvY2FsUGF0aD48L0xvY2FsUGF0aD4NCiAgPFByb3ZpZGVyPjwvUHJvdmlkZXI+DQo8L1NvdXJjZUNvbnRyb2xJbmZvPg==</State>
16
+ <DataSources>
17
+ XML
18
+ SSRS::Config.datasources.each do |ds|
19
+ file.write <<XML
20
+ <ProjectItem>
21
+ <Name>#{ds.name}.rds</Name>
22
+ <FullPath>#{ds.name}.rds</FullPath>
23
+ </ProjectItem>
24
+ XML
25
+ end
26
+ file.write <<XML
27
+ </DataSources>
28
+ <Reports>
29
+ XML
30
+
31
+ Dir["#{self.dir}/*.rdl"].each do |f|
32
+ file.write <<XML
33
+ <ProjectItem>
34
+ <Name>#{File.basename(f)}</Name>
35
+ <FullPath>#{relativepath(File.expand_path(f), project_filename)}</FullPath>
36
+ </ProjectItem>
37
+ XML
38
+ end
39
+
40
+ file.write <<XML
41
+ </Reports>
42
+ <Configurations>
43
+ XML
44
+ development = SSRS::Config.server_config("development")
45
+ file.write gen_configuration("Debug", development)
46
+ file.write gen_configuration("DebugLocal", development)
47
+ production = SSRS::Config.server_config("production") rescue nil
48
+ file.write gen_configuration("Release", production) if production
49
+ file.write <<XML
50
+ </Configurations>
51
+ </Project>
52
+ XML
53
+ end
54
+
55
+ private
56
+
57
+ def gen_configuration(name, server_config)
58
+ <<XML
59
+ <Configuration>
60
+ <Name>#{name}</Name>
61
+ <Platform>Win32</Platform>
62
+ <Options>
63
+ <TargetServerURL>#{server_config.report_target}</TargetServerURL>
64
+ <TargetFolder>#{server_config.upload_prefix}#{self.name}</TargetFolder>
65
+ <TargetDataSourceFolder>#{server_config.upload_prefix}/#{DataSource::BASE_PATH}</TargetDataSourceFolder>
66
+ </Options>
67
+ </Configuration>
68
+ XML
69
+ end
70
+
71
+ # Convert the given absolute path into a path
72
+ # relative to the second given absolute path.
73
+ def relativepath(abspath, relativeto)
74
+ path = abspath.split(File::SEPARATOR)
75
+ rel = relativeto.split(File::SEPARATOR)
76
+ while (path.length > 0) && (path.first == rel.first)
77
+ path.shift
78
+ rel.shift
79
+ end
80
+ ('..' + File::SEPARATOR) * (rel.length - 1) + path.join(File::SEPARATOR)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,77 @@
1
+ module SSRS
2
+ # Generator for "SQL Server Business Intelligence Development Studio" projects
3
+ class Shell
4
+ class << self
5
+ attr_writer :generate_projects
6
+
7
+ def generate_projects?
8
+ @generate_projects ||= false
9
+ end
10
+
11
+ attr_writer :upload_reports
12
+
13
+ def upload_reports?
14
+ @upload_reports ||= false
15
+ end
16
+ end
17
+
18
+ def self.run
19
+ parse_args
20
+ banner
21
+ SSRS::BIDS.generate if generate_projects?
22
+ SSRS::Uploader.upload if upload_reports?
23
+ end
24
+
25
+ private
26
+
27
+ def self.banner
28
+ SSRS.info("Rptman:")
29
+ SSRS.info("\tEnvironment: #{SSRS::Config.environment}")
30
+ SSRS.info("\tDataSource Count: #{SSRS::Config.datasources.size}")
31
+ SSRS.info("\tReport Count: #{SSRS::Config.reports.size}")
32
+ SSRS.info("\tReport Dir Count: #{SSRS::Config.upload_dirs.size}")
33
+ SSRS.info("\tUpload Prefix: #{SSRS::Config.upload_prefix}")
34
+ SSRS.info("\tReport Target: #{SSRS::Config.report_target}")
35
+ SSRS.info("")
36
+ if !generate_projects? && !upload_reports?
37
+ SSRS.info("Run with -h for help")
38
+ end
39
+ end
40
+
41
+ def self.parse_args
42
+ SSRS::Config.environment = "development"
43
+
44
+ optparse = OptionParser.new do |opts|
45
+ opts.on('-v', '--verbose', 'Output more information') do
46
+ Java::IrisSSRS::SSRS.setupLogger(true)
47
+ end
48
+
49
+ opts.on('-e', '--environment environment', 'Database environment to use') do |environment|
50
+ SSRS::Config.environment = environment
51
+ end
52
+
53
+ opts.on('-u', '--upload', 'Upload the reports') do
54
+ SSRS::Shell.upload_reports = true
55
+ end
56
+
57
+ opts.on('-p', '--generate-projects', 'Generate the Buisness Intelligence Studio projects') do
58
+ SSRS::Shell.generate_projects = true
59
+ end
60
+
61
+ # This displays the help screen, all programs are
62
+ # assumed to have this option.
63
+ opts.on('-h', '--help', 'Display this screen') do
64
+ puts opts
65
+ exit
66
+ end
67
+ end
68
+
69
+ # Parse the command-line. Remember there are two forms
70
+ # of the parse method. The 'parse' method simply parses
71
+ # ARGV, while the 'parse!' method parses ARGV and removes
72
+ # any options found there, as well as any parameters for
73
+ # the options. What's left is the list of files to resize.
74
+ optparse.parse!
75
+ end
76
+ end
77
+ end
Binary file
@@ -0,0 +1,6 @@
1
+ module SSRS
2
+ def self.info(message)
3
+ Java::IrisSSRS::SSRS.info(message)
4
+ end
5
+ end
6
+ Java::IrisSSRS::SSRS.setupLogger(false)
@@ -0,0 +1,32 @@
1
+ module SSRS
2
+ class Uploader
3
+ def self.upload
4
+ ssrs_soap_port = Java::IrisSSRS::SSRS.new(Java::JavaNet.URL.new(SSRS::Config.wsdl_path),
5
+ SSRS::Config.upload_prefix)
6
+ self.upload_datasources(ssrs_soap_port)
7
+ self.upload_reports(ssrs_soap_port)
8
+ end
9
+
10
+ private
11
+
12
+ def self.upload_datasources(ssrs_soap_port)
13
+ ssrs_soap_port.mkdir(SSRS::DataSource::BASE_PATH)
14
+ SSRS::Config.datasources.each do |ds|
15
+ ssrs_soap_port.delete(ds.symbolic_name)
16
+ ssrs_soap_port.createSQLDataSource(ds.symbolic_name, ds.connection_string)
17
+ end
18
+ end
19
+
20
+ def self.upload_reports(ssrs_soap_port)
21
+ top_level_upload_dirs =
22
+ SSRS::Config.upload_dirs.collect { |d| d.split('/').delete_if { |p| p == "" }.first }.sort.uniq
23
+ top_level_upload_dirs.each do |upload_dir|
24
+ ssrs_soap_port.delete(upload_dir)
25
+ end
26
+ SSRS::Config.reports.each do |report|
27
+ ssrs_soap_port.mkdir(File.dirname(report.name))
28
+ ssrs_soap_port.createReport(report.name, Java::JavaIo.File.new(report.generate_upload_version))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ namespace :rptman do
2
+ namespace :vs_projects do
3
+ desc "Generate MS VS projects for each report dir"
4
+ task :generate do
5
+ SSRS::BIDS.generate
6
+ end
7
+
8
+ desc "Clean generated projects"
9
+ task :clean do
10
+ rm_rf SSRS::Config.projects_dir
11
+ end
12
+ end
13
+
14
+ namespace :ssrs do
15
+ desc "Upload reports to SSRS server"
16
+ task :upload do
17
+ SSRS::Uploader.upload
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'rptman'
3
+ spec.version = `git describe`.strip.split('-').first
4
+ spec.authors = ['Peter Donald']
5
+ spec.email = ["peter@realityforge.org"]
6
+
7
+ spec.homepage = "http://github.com/stocksoftware/rptman"
8
+ spec.summary = "Tool for managing SSRS reports"
9
+ spec.description = <<-TEXT
10
+ This is a command line tool and suite of rake tasks for uploading SSRS
11
+ reports to a server. The tool can also generate project files for
12
+ the "SQL Server Business Intelligence Development Studio".
13
+ TEXT
14
+ spec.files = Dir['{lib}/**/*', '*.gemspec'] +
15
+ ['lib/ssrs/ssrs-api.jar','LICENSE', 'README.rdoc', 'CHANGELOG']
16
+ spec.require_paths = ['lib']
17
+ spec.platform = RUBY_PLATFORM[/java/]
18
+
19
+ spec.has_rdoc = false
20
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rptman
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Peter Donald
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-06-26 00:00:00 +10:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: |
17
+ This is a command line tool and suite of rake tasks for uploading SSRS
18
+ reports to a server. The tool can also generate project files for
19
+ the "SQL Server Business Intelligence Development Studio".
20
+
21
+ email:
22
+ - peter@realityforge.org
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib/ssrs/shell.rb
31
+ - lib/ssrs/UUID.rb
32
+ - lib/ssrs/uploader.rb
33
+ - lib/ssrs/report.rb
34
+ - lib/ssrs/config.rb
35
+ - lib/ssrs/bids.rb
36
+ - lib/ssrs/ssrs.rb
37
+ - lib/ssrs/datasource.rb
38
+ - lib/ssrs/report_project.rb
39
+ - lib/tasks/rptman.rake
40
+ - lib/rptman.rb
41
+ - rptman.gemspec
42
+ - lib/ssrs/ssrs-api.jar
43
+ - LICENSE
44
+ - README.rdoc
45
+ - CHANGELOG
46
+ has_rdoc: true
47
+ homepage: http://github.com/stocksoftware/rptman
48
+ licenses: []
49
+
50
+ post_install_message:
51
+ rdoc_options: []
52
+
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project:
70
+ rubygems_version: 1.3.5
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: Tool for managing SSRS reports
74
+ test_files: []
75
+