davetron5000-gliffy 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ require 'logger'
2
+
3
+ module Gliffy
4
+
5
+ # Global Configuration for Gliffy. This is a singleton currently and can be accessed
6
+ # via the config class method
7
+ #
8
+ # * You mostly need to set api_key, secret_key and account_name
9
+ # * You may wish to set protocol if you have a premium account and wish to use https
10
+ class Config
11
+
12
+ # A Logger level to control logging
13
+ attr_accessor :log_level
14
+ # The log device (as passed to Logger) for where log messages should go
15
+ attr_accessor :log_device
16
+ # The protocol, either 'http' or 'https' (though feel free to try 'gopher:' :)
17
+ attr_accessor :protocol
18
+ # The gliffy app root, which is pretty much www.gliffy.com/gliffy unless you know a secret
19
+ attr_accessor :gliffy_app_root
20
+ # The url relative to gliffy_app_root of where the API is accessed
21
+ attr_accessor :gliffy_rest_context
22
+ # Your API Key
23
+ attr_accessor :api_key
24
+ # Your Secret Key
25
+ attr_accessor :secret_key
26
+ # The name of your account
27
+ attr_accessor :account_name
28
+
29
+ @@instance=nil
30
+
31
+ def initialize
32
+ @log_level = Logger::DEBUG
33
+ @log_device = STDERR
34
+ @protocol = 'http'
35
+ @gliffy_app_root = 'www.gliffy.com/gliffy';
36
+ @gliffy_rest_context = 'rest'
37
+ @api_key = 'no api key specified'
38
+ @secret_key = 'no secret key specified'
39
+ @account_name = 'no account name specified'
40
+ end
41
+
42
+ # Returns the entire URL to the gliffy api root. This uses protocol, gliffy_app_root
43
+ # and gliffy_rest_context, so you should not really override this
44
+ def gliffy_root
45
+ "#{protocol}://#{gliffy_app_root}/#{gliffy_rest_context}"
46
+ end
47
+
48
+ def self.config=(config); @@instance = config; end
49
+ def self.config
50
+ @@instance = Config.new if !@@instance
51
+ @@instance
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,100 @@
1
+ require 'rexml/document'
2
+ require 'array_has_response'
3
+ require 'gliffy/rest'
4
+
5
+ include REXML
6
+
7
+ module Gliffy
8
+
9
+ # A gliffy diagram (or, rather, the meta data about that diagram)
10
+ class Diagram < Response
11
+
12
+ attr_reader :id
13
+ attr_reader :num_versions
14
+ attr_reader :name
15
+ # The username of the proper owner of this diagram
16
+ attr_reader :owner_username
17
+ # A Time representing the date on which this diagram was created
18
+ attr_reader :create_date
19
+ # A Time representing the date on which this diagram was last modified
20
+ attr_reader :mod_date
21
+ # A Time representing the date on which this diagram was published,
22
+ # or nil if it was not published
23
+ attr_reader :published_date
24
+
25
+ def self.from_xml(element)
26
+ id = element.attributes['id'].to_i
27
+ num_versions = element.attributes['num-versions'].to_i
28
+ is_private = element.attributes['is-private'] == "true"
29
+ is_public = element.attributes['is-public'] == "true"
30
+
31
+ create_date = Time.at(element.elements['create-date'].text.to_i / 1000)
32
+ mod_date = Time.at(element.elements['mod-date'].text.to_i / 1000)
33
+ published_date = element.elements['published-date'] ? Time.at(element.elements['published-date'].text.to_i / 1000) : nil
34
+ name = element.elements['name'].text
35
+ owner_username = element.elements['owner'] ? element.elements['owner'].text : nil
36
+
37
+ Diagram.new(id,name,owner_username,is_public,is_private,num_versions,create_date,mod_date,published_date)
38
+ end
39
+
40
+ # True if this diagram is public
41
+ def is_public?
42
+ @is_public
43
+ end
44
+
45
+ # True if this diagram is private (and available only
46
+ # to the owner and account administrators)
47
+ def is_private?
48
+ @is_private
49
+ end
50
+
51
+ # sorts by diagram name
52
+ def <=>(other_diagram)
53
+ name <=> other_diagram.name
54
+ end
55
+
56
+ protected
57
+ def initialize(id,name,owner_username,is_public,is_private,num_versions,create_date,mod_date,published_date)
58
+ super()
59
+ @id = id
60
+ @name = name
61
+ @owner_username = owner_username
62
+ @is_public = is_public
63
+ @is_private = is_private
64
+ @num_versions = num_versions
65
+ @create_date = create_date
66
+ @mod_date = mod_date
67
+ @published_date = published_date
68
+ end
69
+
70
+ end
71
+
72
+ # A link to edit a specific gliffy diagram
73
+ class LaunchLink < Response
74
+
75
+ # The name of the diagram, which can helpful
76
+ # in creating HTML hyperlinks to url
77
+ attr_reader :diagram_name
78
+ attr_reader :url
79
+
80
+ def self.from_xml(element)
81
+ diagram_name = element.attributes['diagram-name']
82
+ url = element.text
83
+ LaunchLink.new(diagram_name,url)
84
+ end
85
+
86
+ def full_url
87
+ Gliffy::Config.config.protocol + "://" + Gliffy::Config.config.gliffy_app_root + url
88
+ end
89
+
90
+ protected
91
+
92
+ def initialize(name,url)
93
+ super()
94
+ @diagram_name = name
95
+ @url = url
96
+ end
97
+ end
98
+
99
+ class Diagrams < ArrayResponseParser; end
100
+ end
@@ -0,0 +1,63 @@
1
+ require 'rexml/document'
2
+ require 'array_has_response'
3
+ require 'gliffy/rest'
4
+
5
+ include REXML
6
+
7
+ module Gliffy
8
+
9
+ class Folders < ArrayResponseParser; end
10
+
11
+ class Folder < Response
12
+
13
+ # An array of Folder objects that are contained within this folder
14
+ # If this is empty, it means this Folder is a leaf
15
+ attr_reader :child_folders
16
+ attr_reader :id
17
+ attr_reader :name
18
+ # The full path to this folder within the account's
19
+ # folder hierarchy
20
+ attr_reader :path
21
+
22
+ def self.from_xml(element)
23
+ id = element.attributes['id'].to_i
24
+ default = element.attributes['is-default'] == "true"
25
+ name = element.elements['name'].text
26
+ path = element.elements['path'].text
27
+ child_folders = Array.new
28
+ element.each_element do |element|
29
+ child_folders << Folder.from_xml(element) if element.name == "folder"
30
+ end
31
+ Folder.new(id,name,default,path,child_folders)
32
+ end
33
+
34
+ # Returns true if this folder is the default folder
35
+ # used when an operation requiring a folder
36
+ # doesn't specify one (such as when creating a new
37
+ # diagram)
38
+ def default?
39
+ @default
40
+ end
41
+
42
+ # Encodes the elements of a folder path so it can safely go into a URL
43
+ def self.encode_path_elements(folder_path)
44
+ encoded = ''
45
+ folder_path.split(/\//).each do |part|
46
+ encoded += CGI::escape(part)
47
+ encoded += "/"
48
+ end
49
+ encoded.gsub(/\/$/,'')
50
+ end
51
+
52
+ protected
53
+
54
+ def initialize(id,name,default,path,child_folders)
55
+ super()
56
+ @id = id
57
+ @name = name
58
+ @default = default
59
+ @path = path
60
+ @child_folders = child_folders
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,367 @@
1
+ require 'gliffy/rest'
2
+ require 'gliffy/response'
3
+ require 'gliffy/user'
4
+ require 'gliffy/diagram'
5
+ require 'gliffy/folder'
6
+ require 'gliffy/account'
7
+ require 'gliffy/config'
8
+
9
+ module Gliffy
10
+
11
+
12
+ # A "handle" to access Gliffy on a per-user-session basis
13
+ # Since most calls to gliffy require a user-token, this class
14
+ # encapsulates that token and the calls made under it.
15
+ #
16
+ # The methods here are designed to raise exceptions if there are problems from Gliffy.
17
+ # These problems usually indicate a programming error or a server-side problem with Gliffy and
18
+ # are generally unhandleable. However, if you wish to do something better than simply raise an exception
19
+ # you may override handle_error to do something else
20
+ #
21
+ class Handle
22
+
23
+ # Create a new handle to gliffy for the given user. Tokens will be requested as needed
24
+ def initialize(username,token=nil)
25
+ @username = username
26
+ @rest = Rest.new
27
+ @logger = Logger.new(Config.config.log_device)
28
+ @logger.level = Config.config.log_level
29
+ if token
30
+ @rest.current_token = token
31
+ else
32
+ update_token(username)
33
+ end
34
+ end
35
+
36
+ # Run an arbitrary rest call against gliffy, parsing the result. This allows you
37
+ # essentially direct access to the underlying Gliffy::Rest, without having to worry about
38
+ # setting it up.
39
+ #
40
+ # [+method+] a rest method, :get, :put, :post, :delete
41
+ # [+url+] url to request, relative to Gliffy::Config#gliffy_root
42
+ # [+params+] hash of parameters
43
+ # [+headers+] hash of HTTP headers
44
+ #
45
+ # This will return a Gliffy::Response *if* the method you called returned XML.
46
+ # If that is not what you want, you should use Gliffy::Rest.
47
+ def rest_free(method,url,params={},headers={})
48
+ Response.from_xml(do_simple_rest(method,url,"rest_free",params,headers))
49
+ end
50
+
51
+ # Adds a new user explicitly.
52
+ def add_user(username)
53
+ do_user(:put,username)
54
+ end
55
+
56
+ # Deletes the user from this account. May not be the user who owns the token for this
57
+ # session (i.e. was passed to the constructor). <b>This cannot be undone</b>.
58
+ def delete_user(username)
59
+ do_user(:delete,username)
60
+ end
61
+
62
+ # Allows +username+ to access +folder_path+ and all its child folders
63
+ def add_user_to_folder(username,folder_path)
64
+ do_user_folder(:put,username,folder_path)
65
+ end
66
+
67
+ # Revokes +username+ access to +folder_path+
68
+ def remove_user_from_folder(username,folder_path)
69
+ do_user_folder(:delete,username,folder_path)
70
+ end
71
+
72
+ # Creates a new blank diagram (or based on an existing one)
73
+ #
74
+ # [+diagram_name+] the name of the new diagram
75
+ # [+template_diagram_id+] the id of a diagram to use as a template. You must have access to this diagram
76
+ #
77
+ def create_diagram(diagram_name,template_diagram_id=nil)
78
+ params = Hash.new
79
+ params['diagramName'] = diagram_name
80
+ params['templateDiagramId'] = template_diagram_id if template_diagram_id
81
+ diagrams = do_simple_rest(:post,'diagrams',"Creating diagram named #{diagram_name}",params)
82
+ if diagrams.size >= 1
83
+ return diagrams[0]
84
+ else
85
+ raise "Got no diagrams, but creation was successful."
86
+ end
87
+ end
88
+
89
+ # Creates a folder with the given name. The parent path should already exist
90
+ def create_folder(folder_path)
91
+ do_folder(:put,folder_path)
92
+ end
93
+
94
+ # deletes the diagram with the given id. <b>This cannot be undone</b>
95
+ def delete_diagram(diagram_id)
96
+ do_simple_rest(:delete,"diagrams/#{diagram_id}","Deleting diagram #{diagram_id}")
97
+ end
98
+
99
+ # Deletes a folder, moving all diagrams in it to the default folder.
100
+ # The folder must be empty
101
+ # <b>This cannot be undone</b>
102
+ def delete_folder(folder_path)
103
+ folder_path = Folder.encode_path_elements(folder_path)
104
+ do_simple_rest(:delete,"folders/#{folder_path}","Deleting folder #{folder_path}")
105
+ end
106
+
107
+ # Returns an array of User objects representing the admins of the account
108
+ def get_admins
109
+ do_simple_rest(:get,'admins','Getting admins for account')
110
+ end
111
+
112
+ # Gets the diagram as an image, possibly saving it to a file.
113
+ #
114
+ # [+diagram_id+] the id of the diagram to get
115
+ # [+options+] a hash of options controlling the diagram and how it's fetched
116
+ # [<tt>:size</tt>] one of :thumbnail, :small, :medium, or :large (default is :large)
117
+ # [<tt>:file</tt>] if present, the diagram is written to the named file
118
+ # [<tt>:mime_type</tt>] the mime type to retrie. You can also use :jpeg, :png and :svg as shortcuts (default is :jpeg)
119
+ # [<tt>:version</tt>] if present, the version number to retrieve (default is most recent)
120
+ #
121
+ # returns the bytes of the diagram if file was nil, otherwise, returns true
122
+ #
123
+ def get_diagram_as_image(diagram_id,options={:mime_type => :jpeg})
124
+ params,headers = create_diagram_request_info(options)
125
+ update_token
126
+ extension = extension_for(options[:mime_type])
127
+ extension = ''
128
+ bytes = @rest.get(url("diagrams/#{diagram_id}#{extension}"),params,headers)
129
+ response = nil
130
+ begin
131
+ response = Response.from_xml(bytes)
132
+ rescue
133
+ response = nil
134
+ end
135
+
136
+ if response.respond_to?(:success?) && !response.success?
137
+ handle_error(response,"While getting bytes of diagram #{diagram_id}")
138
+ else
139
+ if options[:file]
140
+ fp = File.new(options[:file],'w')
141
+ fp.puts bytes
142
+ fp.close
143
+ true
144
+ else
145
+ bytes
146
+ end
147
+ end
148
+ end
149
+
150
+ # returns the URL that would get the diagram in question. Same parameters as get_diagram_as_image
151
+ def get_diagram_as_url(diagram_id,options={:mime_type => :jpeg})
152
+ params,headers = create_diagram_request_info(options)
153
+ update_token
154
+ extension = extension_for(options[:mime_type])
155
+ @rest.create_url(url("diagrams/#{diagram_id}#{extension}"),params)
156
+ end
157
+
158
+ def extension_for(mime_type)
159
+ case mime_type
160
+ when :jpeg
161
+ return ".jpg"
162
+ when :png
163
+ return ".png"
164
+ when :svg
165
+ return ".svg"
166
+ end
167
+ ""
168
+ end
169
+
170
+ # GliffyDiagram getDiagramMetaData (integer $diagramId)
171
+ def get_diagram_meta_data(diagram_id)
172
+ params = {'diagramId' => diagram_id}
173
+ diagrams = do_simple_rest(:get,'diagrams',"Getting meta data for diagram #{diagram_id}",params)
174
+ diagrams[0]
175
+ end
176
+
177
+ # Gets a list of diagrams, either for the given folder, or the entire account
178
+ def get_diagrams(folder_path=nil)
179
+ folder_path = Folder.encode_path_elements(folder_path) if folder_path
180
+ url = (folder_path ? "folders/#{folder_path}/" : "") + "diagrams"
181
+ do_simple_rest(:get,url,"Get all diagrams in " + (folder_path ? folder_path : "account"))
182
+ end
183
+
184
+ # Gets the link that can be used <b>by this user while his token is valid</b> to edit the diagram.
185
+ #
186
+ # [+diagram_id+] the id of the diagram to edit
187
+ # [+return_url+] if present represents the URL to return the user to after they have completed their editing. You should not urlencode this, it will be done for you
188
+ # [+return_text+] the text that should be used in Gliffy to represent the "return to the application" button.
189
+ #
190
+ # returns a Gliffy::LaunchLink that contains the complete URL to be used to edit the given diagram and behave as described. The GliffyLaunchLink also contains the diagram name, which can be used for linking. Note that the url is relative to the Gliffy website
191
+ def get_edit_diagram_link(diagram_id,return_url=nil,return_text=nil)
192
+ params = Hash.new
193
+ params['returnURL'] = CGI::escape(return_url) if return_url
194
+ params['returnText'] = CGI::escape(return_text) if return_text
195
+
196
+ do_simple_rest(:get,"diagrams/#{diagram_id}/launchLink","Getting launch link for diagram #{diagram_id}",params)
197
+ end
198
+
199
+ # array getFolders ()
200
+ def get_folders()
201
+ do_simple_rest(:get,'folders','Getting folders for account')
202
+ end
203
+
204
+ # Gets the folders that +username+ has access to, in nested form
205
+ def get_user_folders(username)
206
+ do_simple_rest(:get,"users/#{username}/folders","Getting folders for user #{username}")
207
+ end
208
+
209
+ # Gets the users in the given folder, or in the entire account
210
+ #
211
+ # [+folder_path+] if present, returns users with access to this folder
212
+ def get_users(folder_path=nil)
213
+ url = ''
214
+ if (folder_path)
215
+ folder_path = Folder.encode_path_elements(folder_path)
216
+ url += "folders/#{folder_path}/"
217
+ end
218
+ url += 'users'
219
+ do_simple_rest(:get,url,"Getting users for " + (folder_path ? "folder #{folder_path}" : 'account'))
220
+ end
221
+
222
+ # returns the user token if it exists
223
+ def current_token()
224
+ @rest.current_token
225
+ end
226
+
227
+ # move diagram +diagram_id+ to folder path +folder_path+
228
+ def move_diagram(diagram_id,folder_path)
229
+ folder_path = Folder.encode_path_elements(folder_path)
230
+ do_simple_rest(:put,"folders/#{folder_path}/diagrams/#{diagram_id}","Moving #{diagram_id} to folder #{folder_path}")
231
+ end
232
+
233
+ # Updates the user.
234
+ #
235
+ # [+username+] user to update
236
+ # [+attributes+] has of attributes to change.
237
+ # [<tt>:email</tt>] email address
238
+ # [<tt>:password</tt>] password for logging into Gliffy Online
239
+ # [<tt>:admin</tt>] true to make them an admin, false to revoke their admin-ness
240
+ #
241
+ def update_user(username,attributes)
242
+ params = Hash.new
243
+ params['admin'] = attributes[:admin].to_s if attributes.has_key? :admin
244
+ params['email'] = attributes[:email] if attributes[:email]
245
+ params['password'] = attributes[:password] if attributes[:password]
246
+ do_simple_rest(:put,"users/#{username}","Updating #{username}",params)
247
+ end
248
+
249
+ # Updates the user's token, if he needs it
250
+ #
251
+ # [+force+] if true, the token is updated regardless
252
+ def update_token(force=false)
253
+ if force || !@rest.current_token || @rest.current_token.expired?
254
+ @logger.debug('Forcing a new token') if force
255
+ @logger.debug('No current token') if !@rest.current_token
256
+ @rest.current_token = nil
257
+ token = Response.from_xml(@rest.get(url("users/#{@username}/token")))
258
+ if (token.success?)
259
+ @logger.info("User #{@username} assigned token #{token.token} from Gliffy")
260
+ @rest.current_token = token
261
+ else
262
+ handle_error(token)
263
+ end
264
+ else
265
+ @logger.debug('Not getting a new token')
266
+ end
267
+ end
268
+
269
+ # Override this if you want error handling that doesn't rasie an exception
270
+ #
271
+ # [+error_response+] an Error object that holds the error from Gliffy
272
+ # [+action_cause+] a string describing the action being taken when the error ocurred
273
+ def handle_error(error_response,action_cause=nil)
274
+ msg = ""
275
+ msg += "While #{action_cause}: " if action_cause
276
+ msg += error_response.to_s
277
+ raise msg
278
+ end
279
+
280
+ private
281
+
282
+ def do_user(method,username)
283
+ do_simple_rest(method,"users/#{username}","#{rest_to_text(method)} user #{username}")
284
+ end
285
+
286
+ def do_folder(method,folder_path)
287
+ folder_path = Folder.encode_path_elements(folder_path)
288
+ do_simple_rest(method,"folders/#{folder_path}","#{rest_to_text(method)} folder #{folder_path}")
289
+ end
290
+
291
+ def do_user_folder(method,username,folder_path)
292
+ folder_path = Folder.encode_path_elements(folder_path)
293
+ do_simple_rest(method,"folders/#{folder_path}/users/#{username}","#{rest_to_text(method)} #{username} to #{folder_path}")
294
+ end
295
+
296
+ def create_diagram_request_info(options)
297
+ mime_type = options[:mime_type]
298
+ params = Hash.new
299
+ params['size'] = size_to_param(options[:size]) if options[:size]
300
+ params['version'] = options[:version] if options[:version]
301
+ headers = Hash.new
302
+ if mime_type.is_a? Symbol
303
+ headers['Accept'] = mime_type_to_header(mime_type)
304
+ else
305
+ headers['Accept'] = mime_type
306
+ end
307
+ [params,headers]
308
+ end
309
+
310
+ def do_simple_rest_helper(method,url_fragment,description,params=nil,headers={},first=false)
311
+ update_token
312
+ response = Response.from_xml(@rest.send(method,url(url_fragment),params,headers))
313
+ if !response.success?
314
+ if first
315
+ @logger.warn("Possible token problem, try again after updating token")
316
+ update_token true
317
+ response = do_simple_rest_helper(method,url_fragment,description,params,headers,false)
318
+ else
319
+ handle_error(response,description)
320
+ end
321
+ end
322
+ response
323
+ end
324
+
325
+ def do_simple_rest(method,url_fragment,description,params=nil,headers={})
326
+ do_simple_rest_helper(method,url_fragment,description,params,headers,true)
327
+ end
328
+
329
+
330
+ def url(fragment)
331
+ "/accounts/#{Config.config.account_name}/#{fragment}"
332
+ end
333
+
334
+ def mime_type_to_header(mime_type_symbol)
335
+ case mime_type_symbol
336
+ when :jpeg: 'image/jpeg'
337
+ when :jpg: 'image/jpg'
338
+ when :png: 'image/png'
339
+ when :svg: 'image/svg+xml'
340
+ else raise "#{mime_type_symbol} is not a known mime type"
341
+ end
342
+ end
343
+ def size_to_param(size)
344
+ return nil if !size
345
+ case size
346
+ when :thumbnail: 'T'
347
+ when :small: 'S'
348
+ when :medium: 'M'
349
+ when :large: 'L'
350
+ else raise "#{size} is not a supported size"
351
+ end
352
+ end
353
+
354
+ def rest_to_text(method)
355
+ case method
356
+ when :put : 'Creating'
357
+ when :delete : 'Deleting'
358
+ when :post : 'Updating'
359
+ when :get : 'Getting'
360
+ else
361
+ raise "Unknown method #{method.to_s}"
362
+ end
363
+ end
364
+
365
+
366
+ end
367
+ end