atomutil 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +4 -0
- data/License.txt +20 -0
- data/Manifest.txt +35 -0
- data/README.txt +632 -0
- data/Rakefile +4 -0
- data/config/hoe.rb +71 -0
- data/config/requirements.rb +17 -0
- data/lib/atomutil.rb +1717 -0
- data/log/debug.log +0 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +74 -0
- data/setup.rb +1585 -0
- data/spec/categories_spec.rb +44 -0
- data/spec/category_spec.rb +27 -0
- data/spec/content_spec.rb +56 -0
- data/spec/customfeed_spec.rb +68 -0
- data/spec/feed_spec.rb +227 -0
- data/spec/link_spec.rb +58 -0
- data/spec/namespace_spec.rb +40 -0
- data/spec/opensearch_spec.rb +22 -0
- data/spec/person_spec.rb +61 -0
- data/spec/samples/feed.atom +60 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/threading_spec.rb +47 -0
- data/tasks/deployment.rake +34 -0
- data/tasks/environment.rake +7 -0
- data/tasks/rspec.rake +21 -0
- data/tasks/website.rake +17 -0
- data/website/index.html +11 -0
- data/website/index.txt +39 -0
- data/website/javascripts/rounded_corners_lite.inc.js +285 -0
- data/website/stylesheets/screen.css +138 -0
- data/website/template.rhtml +48 -0
- metadata +85 -0
data/lib/atomutil.rb
ADDED
@@ -0,0 +1,1717 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C) 2007 Lyo Kato, <lyo.kato _at_ gmail.com>.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#
|
23
|
+
# This package allows you to handle AtomPub and Atom Syndication Format easily.
|
24
|
+
# This is just a porting for Perl's great libraries, XML::Atom, XML::Atom::Service,
|
25
|
+
# XML::Atom::Ext::Threading, and Atompub
|
26
|
+
#
|
27
|
+
# http://search.cpan.org/perldoc?XML%3A%3AAtom
|
28
|
+
# http://search.cpan.org/perldoc?XML%3A%3AAtom%3A%3AService
|
29
|
+
# http://search.cpan.org/perldoc?XML%3A%3AAtom%3A%3AExt%3A%3AThreading
|
30
|
+
# http://search.cpan.org/perldoc?Atompub
|
31
|
+
#
|
32
|
+
# This package supports however only version 1.0 of Atom(original Perl libraries support also version 0.3),
|
33
|
+
# and is not stable yet.
|
34
|
+
# We need more document, more tutorials, and more tests by RSpec.
|
35
|
+
#++
|
36
|
+
require 'sha1'
|
37
|
+
require 'uri'
|
38
|
+
require 'open-uri'
|
39
|
+
require 'pathname'
|
40
|
+
require 'time'
|
41
|
+
require 'net/http'
|
42
|
+
require 'rexml/document'
|
43
|
+
|
44
|
+
# = Utilities for AtomPub / Atom Syndication Format
|
45
|
+
#
|
46
|
+
# This class containts two important modules
|
47
|
+
#
|
48
|
+
# [Atom] Includes classes for parsing or building atom element.
|
49
|
+
# For example, Atom::Entry, Atom::Feed, and etc.
|
50
|
+
#
|
51
|
+
# [Atompub] Includes client class works according to AtomPub protocol.
|
52
|
+
# And other useful helper classes.
|
53
|
+
#
|
54
|
+
module AtomUtil
|
55
|
+
module VERSION#:nodoc:
|
56
|
+
MAJOR = 0
|
57
|
+
MINOR = 0
|
58
|
+
TINY = 1
|
59
|
+
STRING = [MAJOR, MINOR, TINY].join('.')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
# = Utility to build or parse Atom Syndication Format
|
63
|
+
#
|
64
|
+
# Spec: http://atompub.org/rfc4287.html
|
65
|
+
#
|
66
|
+
# This allows you to handle elements used on Atom Syndication Format easily.
|
67
|
+
# See each element classes' document in detail.
|
68
|
+
#
|
69
|
+
# == Service Document
|
70
|
+
#
|
71
|
+
# === Element Classes used in service documents
|
72
|
+
#
|
73
|
+
# * Atom::Service
|
74
|
+
# * Atom::Workspace
|
75
|
+
# * Atom::Collection
|
76
|
+
# * Atom::Categories
|
77
|
+
# * Atom::Category
|
78
|
+
#
|
79
|
+
# == Categories Document
|
80
|
+
#
|
81
|
+
# === Element Classes used in categories documents
|
82
|
+
#
|
83
|
+
# * Atom::Categories
|
84
|
+
# * Atom::Category
|
85
|
+
#
|
86
|
+
# == Feed
|
87
|
+
#
|
88
|
+
# === Element classes used in feeds.
|
89
|
+
#
|
90
|
+
# * Atom::Feed
|
91
|
+
# * Atom::Entry
|
92
|
+
#
|
93
|
+
# == Entry
|
94
|
+
#
|
95
|
+
# == Element classes used in entries.
|
96
|
+
#
|
97
|
+
# * Atom::Entry
|
98
|
+
# * Atom::Link
|
99
|
+
# * Atom::Author
|
100
|
+
# * Atom::Contributor
|
101
|
+
# * Atom::Content
|
102
|
+
# * Atom::Control
|
103
|
+
# * Atom::Category
|
104
|
+
#
|
105
|
+
module Atom
|
106
|
+
# Namespace Object Class
|
107
|
+
#
|
108
|
+
# Example:
|
109
|
+
#
|
110
|
+
# namespace = Atom::Namespace.new(:prefix => 'dc', :uri => 'http://purl.org/dc/elements/1.1/')
|
111
|
+
# # you can omit prefix
|
112
|
+
# # namespace = Atom::Namespace.new(:uri => 'http://purl.org/dc/elements/1.1/')
|
113
|
+
# puts namespace.prefix # dc
|
114
|
+
# puts namespace.uri # http://purl.org/dc/elements/1.1/
|
115
|
+
#
|
116
|
+
# Mager namespaces are already set as constants. You can use them directly
|
117
|
+
# without making new Atom::Namespace instance
|
118
|
+
#
|
119
|
+
class Namespace
|
120
|
+
attr_reader :prefix, :uri
|
121
|
+
def initialize(params) #:nodoc:
|
122
|
+
@prefix, @uri = params[:prefix], params[:uri]
|
123
|
+
raise ArgumentError.new(%Q<:uri is not found.>) if @uri.nil?
|
124
|
+
end
|
125
|
+
def to_s #:nodoc:
|
126
|
+
@uri
|
127
|
+
end
|
128
|
+
# Atom namespace
|
129
|
+
ATOM = self.new :uri => 'http://www.w3.org/2005/Atom'
|
130
|
+
# Atom namespace using prefix
|
131
|
+
ATOM_WITH_PREFIX = self.new :prefix => 'atom', :uri => 'http://www.w3.org/2005/Atom'
|
132
|
+
# Atom namespace for version 0.3
|
133
|
+
OBSOLETE_ATOM = self.new :uri => 'http://purl.org/atom/ns#'
|
134
|
+
# Atom namespace for version 0.3 using prefix
|
135
|
+
OBSOLETE_ATOM_WITH_PREFIX = self.new :prefix => 'atom', :uri => 'http://purl.org/atom/ns#'
|
136
|
+
# Atom app namespace
|
137
|
+
APP = self.new :uri => 'http://www.w3.org/2007/app'
|
138
|
+
# Atom app namespace with prefix
|
139
|
+
APP_WITH_PREFIX = self.new :prefix => 'app', :uri => 'http://www.w3.org/2007/app'
|
140
|
+
# Atom app namespace for version 0.3
|
141
|
+
OBSOLETE_APP = self.new :uri => 'http://purl.org/atom/app#'
|
142
|
+
# Atom app namespace for version 0.3 with prefix
|
143
|
+
OBSOLETE_APP_WITH_PREFIX = self.new :prefix => 'app', :uri => 'http://purl.org/atom/app#'
|
144
|
+
# Dubline Core namespace
|
145
|
+
DC = self.new :prefix => 'dc', :uri => 'http://purl.org/dc/elements/1.1/'
|
146
|
+
# Open Search namespace that is often used for pagination
|
147
|
+
OPEN_SEARCH = self.new :prefix => 'openSearch', :uri => 'http://a9.com/-/spec/opensearchrss/1.1/'
|
148
|
+
RDF = self.new :prefix => 'rdf', :uri => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
|
149
|
+
FOAF = self.new :prefix => 'foaf', :uri => 'http://xmlns.com/foaf/0.1'
|
150
|
+
THR = self.new :prefix => 'thr', :uri => 'http://purl.org/syndication/thread/1.0'
|
151
|
+
end
|
152
|
+
# = MediaType
|
153
|
+
#
|
154
|
+
# Class represents MediaType
|
155
|
+
#
|
156
|
+
# == Accessors
|
157
|
+
#
|
158
|
+
# feed = Atom::MediaType.new 'application/atom+xml;type=feed'
|
159
|
+
# puts feed.type # application
|
160
|
+
# puts feed.subtype # atom+xml
|
161
|
+
# puts feed.subtype_major # xml
|
162
|
+
# puts feed.without_parameters # application/atom+xml
|
163
|
+
# puts feed.parameters # type=feed
|
164
|
+
# puts feed.to_s # application/atom+xml;type=feed
|
165
|
+
#
|
166
|
+
# == Equivalence
|
167
|
+
#
|
168
|
+
# feed2 = Atom::MediaType.new 'application/atom+xml;type=feed'
|
169
|
+
# entry = Atom::MediaType.new 'application/atom+xml;type=entry'
|
170
|
+
# feed == feed2 # -> true
|
171
|
+
# feed == entry # -> false
|
172
|
+
# feed == 'application/atom+xml;type=feed' # -> true
|
173
|
+
#
|
174
|
+
# == Constants
|
175
|
+
#
|
176
|
+
# Major media types for atom syndication format are already prepared.
|
177
|
+
# Use following constants for them.
|
178
|
+
#
|
179
|
+
# [Atom::MediaType::SERVICE] application/atomsvc+xml
|
180
|
+
# [Atom::MediaType::CATEGORIES] application/atomcat+xml
|
181
|
+
# [Atom::MediaType::FEED] application/atom+xml;type=feed
|
182
|
+
# [Atom::MediaType::ENTRY] application/atom+xml;type=entry
|
183
|
+
#
|
184
|
+
class MediaType
|
185
|
+
attr_reader :type, :subtype, :parameters
|
186
|
+
def initialize(type) #:nodoc:
|
187
|
+
result = type.split(%r<[/;]>)
|
188
|
+
@type = result[0]
|
189
|
+
@subtype = result[1]
|
190
|
+
@parameters = result[2]
|
191
|
+
end
|
192
|
+
|
193
|
+
def subtype_major
|
194
|
+
@subtype =~ /\+(.+)/ ? $1 : @subtype
|
195
|
+
end
|
196
|
+
|
197
|
+
def without_parameters
|
198
|
+
"#{@type}/#{@subtype}"
|
199
|
+
end
|
200
|
+
|
201
|
+
def to_s
|
202
|
+
[without_parameters, @parameters].select{ |p| !p.nil? }.join(";")
|
203
|
+
end
|
204
|
+
|
205
|
+
def ==(value)
|
206
|
+
if value.is_a?(MediaType)
|
207
|
+
to_s == value.to_s
|
208
|
+
else
|
209
|
+
to_s == value
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def is_a?(value)
|
214
|
+
value = self.class.new value unless value.instance_of?(self.class)
|
215
|
+
return true if value.type == '*'
|
216
|
+
return false unless value.type == @type
|
217
|
+
return true if value.subtype == '*'
|
218
|
+
return false unless value.subtype == @subtype
|
219
|
+
return true if value.parameters.nil? || @parameters.nil?
|
220
|
+
return value.parameters == @parameters
|
221
|
+
end
|
222
|
+
SERVICE = self.new 'application/atomsvc+xml'
|
223
|
+
CATEGORIES = self.new 'application/atomcat+xml'
|
224
|
+
FEED = self.new 'application/atom+xml;type=feed'
|
225
|
+
ENTRY = self.new 'application/atom+xml;type=entry'
|
226
|
+
end
|
227
|
+
# = Atom::Element
|
228
|
+
#
|
229
|
+
# Base Element Object Class
|
230
|
+
#
|
231
|
+
# You don't use this class directly.
|
232
|
+
# This is a base class of each element classes used in Atom Syndication Format.
|
233
|
+
#
|
234
|
+
class Element
|
235
|
+
def self.new(params={})
|
236
|
+
obj = super(params)
|
237
|
+
yield(obj) if block_given?
|
238
|
+
obj
|
239
|
+
end
|
240
|
+
@@ns = Namespace::ATOM
|
241
|
+
def self.ns(ns=nil)
|
242
|
+
unless ns.nil?
|
243
|
+
@@ns = ns.is_a?(Namespace) ? ns : Namespace.new(:uri => ns)
|
244
|
+
end
|
245
|
+
@@ns
|
246
|
+
end
|
247
|
+
@element_name = nil
|
248
|
+
def self.element_name(name=nil)
|
249
|
+
unless name.nil?
|
250
|
+
@element_name = name.to_s
|
251
|
+
end
|
252
|
+
@element_name
|
253
|
+
end
|
254
|
+
@element_ns = nil
|
255
|
+
def self.element_ns(ns=nil)
|
256
|
+
unless ns.nil?
|
257
|
+
@element_ns = ns.is_a?(Namespace) ? ns : Namespace.new(:uri => ns)
|
258
|
+
end
|
259
|
+
@element_ns
|
260
|
+
end
|
261
|
+
# Generate element accessor for indicated name
|
262
|
+
# The generated accessors can deal with elements which has only simple text-node,
|
263
|
+
# such as title, summary, rights, and etc.
|
264
|
+
# Of course, these elements should handle more complex data.
|
265
|
+
# In such a case, you can control them directly with 'set' and 'get' method.
|
266
|
+
#
|
267
|
+
# Example:
|
268
|
+
#
|
269
|
+
# class Entry < Element
|
270
|
+
# element_text_accessor 'title'
|
271
|
+
# element_text_accessor 'summary'
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# elem = MyElement.new
|
275
|
+
# elem.title = "foo"
|
276
|
+
# elem.summary = "bar"
|
277
|
+
# puts elem.title #foo
|
278
|
+
# puts elem.summary #bar
|
279
|
+
#
|
280
|
+
# div = REXML::Element.new("<div><p>hoge</p></div>")
|
281
|
+
# elem.set('http://www.w3.org/2005/Atom', 'title', div, { :type => 'xhtml' })
|
282
|
+
#
|
283
|
+
def self.element_text_accessor(name)
|
284
|
+
name = name.to_s
|
285
|
+
name.tr!('-', '_')
|
286
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
287
|
+
def #{name}
|
288
|
+
value = get(@ns, '#{name}')
|
289
|
+
value.nil? ? nil : value.text
|
290
|
+
end
|
291
|
+
def #{name}=(value, attributes=nil)
|
292
|
+
set(@ns, '#{name}', value, attributes)
|
293
|
+
end
|
294
|
+
EOS
|
295
|
+
end
|
296
|
+
# You can set text_accessor at once with this method
|
297
|
+
#
|
298
|
+
# Example:
|
299
|
+
# class Entry < BaseEntry
|
300
|
+
# element_text_accessors :title, :summary
|
301
|
+
# end
|
302
|
+
# entry = Entry.new
|
303
|
+
# entry.title = "hoge"
|
304
|
+
# puts entry.title #hoge
|
305
|
+
#
|
306
|
+
def self.element_text_accessors(*names)
|
307
|
+
names.each{ |n| element_text_accessor(n) }
|
308
|
+
end
|
309
|
+
# Generate datetime element accessor for indicated name.
|
310
|
+
#
|
311
|
+
# Example:
|
312
|
+
# class Entry < BaseEntry
|
313
|
+
# element_datetime_accessor :updated
|
314
|
+
# element_datetime_accessor :published
|
315
|
+
# end
|
316
|
+
# entry = Entry.new
|
317
|
+
# entry.updated = Time.now
|
318
|
+
# puts entry.updated.year
|
319
|
+
# puts entry.updated.month
|
320
|
+
#
|
321
|
+
def self.element_datetime_accessor(name)
|
322
|
+
name = name.to_s
|
323
|
+
name.tr!('-', '_')
|
324
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
325
|
+
def #{name}
|
326
|
+
dt = get(@ns, '#{name}')
|
327
|
+
dt.nil? ? nil : Time.iso8601(dt.text)
|
328
|
+
end
|
329
|
+
def #{name}=(value, attributes=nil)
|
330
|
+
case value
|
331
|
+
when Time
|
332
|
+
date = value.iso8601
|
333
|
+
else
|
334
|
+
date = value
|
335
|
+
end
|
336
|
+
set(@ns, '#{name}', date, attributes)
|
337
|
+
end
|
338
|
+
EOS
|
339
|
+
end
|
340
|
+
# You can set datetime accessor at once with this method
|
341
|
+
#
|
342
|
+
# Example:
|
343
|
+
# class Entry < BaseEntry
|
344
|
+
# element_datetime_accessor :updated, :published
|
345
|
+
# end
|
346
|
+
# entry = Entry.new
|
347
|
+
# entry.updated = Time.now
|
348
|
+
# puts entry.updated.year
|
349
|
+
# puts entry.updated.month
|
350
|
+
#
|
351
|
+
def self.element_datetime_accessors(*names)
|
352
|
+
names.each{ |n| element_datetime_accessor(n) }
|
353
|
+
end
|
354
|
+
# Generates text accessor for multiple value.
|
355
|
+
#
|
356
|
+
# Example:
|
357
|
+
def self.element_text_list_accessor(name, moniker=nil)
|
358
|
+
name = name.to_s
|
359
|
+
name.tr!('-', '_')
|
360
|
+
unless moniker.nil?
|
361
|
+
moniker = moniker.to_s
|
362
|
+
moniker.tr!('-', '_')
|
363
|
+
end
|
364
|
+
elem_ns = element_ns || ns
|
365
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
366
|
+
def #{name}
|
367
|
+
value = getlist('#{elem_ns}', '#{name}')
|
368
|
+
value.empty?? nil : value.first
|
369
|
+
end
|
370
|
+
def #{name}=(stuff)
|
371
|
+
set('#{elem_ns}', '#{name}', stuff)
|
372
|
+
end
|
373
|
+
def add_#{name}(stuff)
|
374
|
+
add('#{elem_ns}', '#{name}', stuff)
|
375
|
+
end
|
376
|
+
EOS
|
377
|
+
class_eval(<<-EOS, __FILE__, __LINE__) unless moniker.nil?
|
378
|
+
def #{moniker}
|
379
|
+
getlist('#{elem_ns}', '#{name}')
|
380
|
+
end
|
381
|
+
def #{moniker}=(stuff)
|
382
|
+
#{name} = stuff
|
383
|
+
end
|
384
|
+
EOS
|
385
|
+
end
|
386
|
+
# Generate useful accessor for the multiple element
|
387
|
+
#
|
388
|
+
# Example:
|
389
|
+
# class Entry < Element
|
390
|
+
# element_object_list_accessors :author, Author, :authors
|
391
|
+
# element_object_list_accessors :contributor, Contributor, :contributors
|
392
|
+
# end
|
393
|
+
#
|
394
|
+
def self.element_object_list_accessor(name, ext_class, moniker=nil)
|
395
|
+
name = name.to_s
|
396
|
+
name.tr!('-', '_')
|
397
|
+
unless moniker.nil?
|
398
|
+
moniker = moniker.to_s
|
399
|
+
moniker.tr!('-', '_')
|
400
|
+
end
|
401
|
+
elem_ns = ext_class.element_ns || ns
|
402
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
403
|
+
def #{name}
|
404
|
+
get_object('#{elem_ns}', '#{name}', #{ext_class})
|
405
|
+
end
|
406
|
+
def #{name}=(stuff)
|
407
|
+
set('#{elem_ns}', '#{name}', stuff)
|
408
|
+
end
|
409
|
+
def add_#{name}(stuff)
|
410
|
+
add('#{elem_ns}', '#{name}', stuff)
|
411
|
+
end
|
412
|
+
EOS
|
413
|
+
class_eval(<<-EOS, __FILE__, __LINE__) unless moniker.nil?
|
414
|
+
def #{moniker}
|
415
|
+
get_objects('#{elem_ns}', '#{name}', #{ext_class})
|
416
|
+
end
|
417
|
+
def #{moniker}=(stuff)
|
418
|
+
#{name} = stuff
|
419
|
+
end
|
420
|
+
EOS
|
421
|
+
end
|
422
|
+
# Attribute accessor generator
|
423
|
+
def self.element_attr_accessor(name)
|
424
|
+
name = name.to_s
|
425
|
+
name.tr!('-', '_')
|
426
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
427
|
+
def #{name}
|
428
|
+
get_attr('#{name}')
|
429
|
+
end
|
430
|
+
def #{name}=(value)
|
431
|
+
set_attr('#{name}', value)
|
432
|
+
end
|
433
|
+
EOS
|
434
|
+
end
|
435
|
+
# You can generate attribute accessor at once.
|
436
|
+
def self.element_attr_accessors(*names)
|
437
|
+
names.each{ |n| element_attr_accessor(n) }
|
438
|
+
end
|
439
|
+
|
440
|
+
# Setup element.
|
441
|
+
def initialize(params={})
|
442
|
+
@ns = params.has_key?(:namespace) ? params[:namespace] \
|
443
|
+
: self.class.element_ns ? self.class.element_ns \
|
444
|
+
: self.class.ns
|
445
|
+
@elem = params.has_key?(:elem) ? params[:elem] : REXML::Element.new(self.class.element_name)
|
446
|
+
if @ns.is_a?(Namespace)
|
447
|
+
unless @ns.prefix.nil?
|
448
|
+
@elem.add_namespace @ns.prefix, @ns.uri
|
449
|
+
else
|
450
|
+
@elem.add_namespace @ns.uri
|
451
|
+
end
|
452
|
+
else
|
453
|
+
@elem.add_namespace @ns
|
454
|
+
end
|
455
|
+
params.keys.each do |key|
|
456
|
+
setter = "#{key}=";
|
457
|
+
send(setter.to_sym, params[key]) if respond_to?(setter.to_sym)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
# accessor for xml-element(REXML::Element) object.
|
461
|
+
attr_reader :elem
|
462
|
+
# This method allows you to handle extra-element such as you can't represent
|
463
|
+
# with elements defined in Atom namespace.
|
464
|
+
#
|
465
|
+
# entry = Atom::Entry.new
|
466
|
+
# entry.set('http://example/2007/mynamespace', 'foo', 'bar')
|
467
|
+
#
|
468
|
+
# Now your entry includes new element.
|
469
|
+
# <foo xmlns="http://example/2007/mynamespace">bar</foo>
|
470
|
+
#
|
471
|
+
# You also can add attributes
|
472
|
+
#
|
473
|
+
# entry.set('http://example/2007/mynamespace', 'foo', 'bar', { :myattr => 'attr1', :myattr2 => 'attr2' })
|
474
|
+
#
|
475
|
+
# And you can get following element from entry
|
476
|
+
#
|
477
|
+
# <foo xmlns="http://example/2007/mynamespace" myattr="attr1" myattr2="attr2">bar</foo>
|
478
|
+
#
|
479
|
+
# Or using prefix,
|
480
|
+
#
|
481
|
+
# entry = Atom::Entry.new
|
482
|
+
# ns = Atom::Namespace.new(:prefix => 'dc', :uri => 'http://purl.org/dc/elements/1.1/')
|
483
|
+
# entry.set(ns, 'subject', 'buz')
|
484
|
+
#
|
485
|
+
# Then your element contains
|
486
|
+
#
|
487
|
+
# <dc:subject xmlns:dc="http://purl.org/dc/elements/1.1/">buz</dc:subject>
|
488
|
+
#
|
489
|
+
# And in case you need to handle more complex element, pass the REXML::Element object
|
490
|
+
# which you customized as third argument instead of text-value.
|
491
|
+
#
|
492
|
+
# custom_element = REXML::Element.new
|
493
|
+
# custom_child = REXML::Element.new('mychild')
|
494
|
+
# custom_child.add_text = 'child!'
|
495
|
+
# custom_element.add_element custom_child
|
496
|
+
# entry.set(ns, 'mynamespace', costom_element)
|
497
|
+
#
|
498
|
+
def set(ns, element_name, value="", attributes=nil)
|
499
|
+
xpath = child_xpath(ns, element_name)
|
500
|
+
@elem.elements.delete_all(xpath)
|
501
|
+
add(ns, element_name, value, attributes)
|
502
|
+
end
|
503
|
+
# Same as 'set', but when a element-name confliction occurs,
|
504
|
+
# append new element without overriding.
|
505
|
+
def add(ns, element_name, value, attributes={})
|
506
|
+
element = REXML::Element.new(element_name)
|
507
|
+
if ns.is_a?(Namespace)
|
508
|
+
unless ns.prefix.nil? || ns.prefix.empty?
|
509
|
+
element.name = "#{ns.prefix}:#{element_name}"
|
510
|
+
element.add_namespace ns.prefix, ns.uri unless @ns == ns || @ns == ns.uri
|
511
|
+
else
|
512
|
+
element.add_namespace ns.uri unless @ns == ns || @ns == ns.uri
|
513
|
+
end
|
514
|
+
else
|
515
|
+
element.add_namespace ns unless @ns == ns || @ns.to_s == ns
|
516
|
+
end
|
517
|
+
if value.is_a?(Element)
|
518
|
+
value.elem.each_element do |e|
|
519
|
+
element.add e.deep_clone
|
520
|
+
end
|
521
|
+
value.elem.attributes.each_attribute do |a|
|
522
|
+
unless a.name =~ /^xmlns(?:\:)?/
|
523
|
+
element.add_attribute a
|
524
|
+
end
|
525
|
+
end
|
526
|
+
element.text = value.elem.text unless value.elem.text.nil?
|
527
|
+
else
|
528
|
+
if value.is_a?(REXML::Element)
|
529
|
+
element.add_element value.deep_clone
|
530
|
+
else
|
531
|
+
element.add_text value.to_s
|
532
|
+
end
|
533
|
+
end
|
534
|
+
element.add_attributes attributes unless attributes.nil?
|
535
|
+
@elem.add_element element
|
536
|
+
end
|
537
|
+
# Get indicated element.
|
538
|
+
# If it matches multiple, returns first one.
|
539
|
+
#
|
540
|
+
# elem = entry.get('http://example/2007/mynamespace', 'foo')
|
541
|
+
#
|
542
|
+
# ns = Atom::Namespace.new(:prefix => 'dc', :uri => 'http://purl.org/dc/elements/1.1/')
|
543
|
+
# elem = entry.get(ns, 'subject')
|
544
|
+
#
|
545
|
+
def get(ns, element_name)
|
546
|
+
getlist(ns, element_name).first
|
547
|
+
end
|
548
|
+
# Get indicated elements as array
|
549
|
+
#
|
550
|
+
# ns = Atom::Namespace.new(:prefix => 'dc', :uri => 'http://purl.org/dc/elements/1.1/')
|
551
|
+
# elems = entry.getlist(ns, 'subject')
|
552
|
+
#
|
553
|
+
def getlist(ns, element_name)
|
554
|
+
@elem.get_elements(child_xpath(ns, element_name))
|
555
|
+
end
|
556
|
+
# Get indicated elements as an object of the class you passed as thrid argument.
|
557
|
+
#
|
558
|
+
# ns = Atom::Namespace.new(:uri => 'http://example.com/ns#')
|
559
|
+
# obj = entry.get_object(ns, 'mytag', MyClass)
|
560
|
+
# puts obj.class #MyClass
|
561
|
+
#
|
562
|
+
# MyClass should inherit Atom::Element
|
563
|
+
#
|
564
|
+
def get_object(ns, element_name, ext_class)
|
565
|
+
elements = getlist(ns, element_name)
|
566
|
+
return nil if elements.empty?
|
567
|
+
ext_class.new(:namespace => ns, :elem => elements.first)
|
568
|
+
end
|
569
|
+
# Get all indicated elements as an object of the class you passed as thrid argument.
|
570
|
+
#
|
571
|
+
# entry.get_objects(ns, 'mytag', MyClass).each{ |obj|
|
572
|
+
# p obj.class #MyClass
|
573
|
+
# }
|
574
|
+
#
|
575
|
+
def get_objects(ns, element_name, ext_class)
|
576
|
+
elements = getlist(ns, element_name)
|
577
|
+
return [] if elements.empty?
|
578
|
+
elements.collect do |e|
|
579
|
+
ext_class.new(:namespace => ns, :elem => e)
|
580
|
+
end
|
581
|
+
end
|
582
|
+
# Get attribute value for indicated key
|
583
|
+
def get_attr(name)
|
584
|
+
@elem.attributes[name.to_s]
|
585
|
+
end
|
586
|
+
# Set attribute value for indicated key
|
587
|
+
def set_attr(name, value)
|
588
|
+
@elem.attributes[name.to_s] = value
|
589
|
+
end
|
590
|
+
# Convert to XML-Document and return it as string
|
591
|
+
def to_s(indent=true)
|
592
|
+
doc = REXML::Document.new
|
593
|
+
decl = REXML::XMLDecl.new("1.0", "utf-8")
|
594
|
+
doc.add decl
|
595
|
+
doc.add_element @elem.deep_clone
|
596
|
+
if indent
|
597
|
+
doc.to_s(0)
|
598
|
+
else
|
599
|
+
doc.to_s
|
600
|
+
end
|
601
|
+
end
|
602
|
+
private
|
603
|
+
# Get a xpath string to traverse child elements with namespace and name.
|
604
|
+
def child_xpath(ns, element_name, attributes=nil)
|
605
|
+
ns_uri = ns.is_a?(Namespace) ? ns.uri : ns
|
606
|
+
unless !attributes.nil? && attributes.is_a?(Hash)
|
607
|
+
"descendant-or-self::*[local-name()='#{element_name}' and namespace-uri()='#{ns_uri}']"
|
608
|
+
else
|
609
|
+
attr_str = attributes.collect{|key, val| "@#{key.to_s}='#{val}'"}.join(' and ')
|
610
|
+
"descendant-or-self::*[local-name()='#{element_name}' and namespace-uri()='#{ns_uri}' and #{attr_str}]"
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
# = Atom::Person
|
615
|
+
#
|
616
|
+
# This class represents person construct
|
617
|
+
# Use this for 'author' or 'contributor' elements.
|
618
|
+
# You also can use Atom::Author or Atom::Contributor directly for each element,
|
619
|
+
# But this class can be converted to each class's object easily
|
620
|
+
# with 'to_author' or 'to_contributor' method.
|
621
|
+
#
|
622
|
+
# Example:
|
623
|
+
#
|
624
|
+
# person = Atom::Person.new
|
625
|
+
# person.name = "John"
|
626
|
+
# person.email = "example@example.com"
|
627
|
+
# person.url = "http://example.com/"
|
628
|
+
# entry = Atom::Entry.new
|
629
|
+
# entry.add_authors person.to_author
|
630
|
+
# entry.add_contributor person.to_contributor
|
631
|
+
#
|
632
|
+
class Person < Element
|
633
|
+
element_name :author
|
634
|
+
element_text_accessors :name, :email, :uri
|
635
|
+
# Convert to an Atom::Author object
|
636
|
+
def to_author
|
637
|
+
author = Author.new
|
638
|
+
author.name = self.name
|
639
|
+
author.email = self.email unless self.email.nil?
|
640
|
+
author.uri = self.uri unless self.uri.nil?
|
641
|
+
author
|
642
|
+
end
|
643
|
+
# Convert to an Atom::Contributor object
|
644
|
+
def to_contributor
|
645
|
+
contributor = Contributor.new
|
646
|
+
contributor.name = self.name
|
647
|
+
contributor.email = self.email unless self.email.nil?
|
648
|
+
contributor.uri = self.uri unless self.uri.nil?
|
649
|
+
contributor
|
650
|
+
end
|
651
|
+
end
|
652
|
+
# = Atom::Author
|
653
|
+
#
|
654
|
+
# This class represents Author
|
655
|
+
class Author < Person
|
656
|
+
element_name :author
|
657
|
+
end
|
658
|
+
# = Atom::Contributor
|
659
|
+
#
|
660
|
+
# This class represents Contributor
|
661
|
+
class Contributor < Person
|
662
|
+
element_name :contributor
|
663
|
+
end
|
664
|
+
|
665
|
+
class Generator < Element
|
666
|
+
element_name :generator
|
667
|
+
element_attr_accessors :uri, :version
|
668
|
+
def name
|
669
|
+
@elem.text
|
670
|
+
end
|
671
|
+
def name=(name)
|
672
|
+
@elem.text = name
|
673
|
+
end
|
674
|
+
end
|
675
|
+
# = Atom::Link
|
676
|
+
#
|
677
|
+
# This class represents link element
|
678
|
+
#
|
679
|
+
# You can use these accessors
|
680
|
+
# * href
|
681
|
+
# * rel
|
682
|
+
# * type
|
683
|
+
# * hreflang
|
684
|
+
# * title
|
685
|
+
# * length
|
686
|
+
#
|
687
|
+
class Link < Element
|
688
|
+
element_name :link
|
689
|
+
element_attr_accessors :href, :rel, :type, :hreflang, :title, :length
|
690
|
+
def to_replies_link
|
691
|
+
RepliesLink.new(:elem => @elem)
|
692
|
+
end
|
693
|
+
end
|
694
|
+
|
695
|
+
class RepliesLink < Link
|
696
|
+
|
697
|
+
def initialize(params={})
|
698
|
+
super(params)
|
699
|
+
@elem.add_namespace(Namespace::THR.prefix, Namespace::THR.uri)
|
700
|
+
set_attr('rel', 'replies')
|
701
|
+
end
|
702
|
+
|
703
|
+
def rel=(name)
|
704
|
+
end
|
705
|
+
|
706
|
+
def count
|
707
|
+
num = get_attr('thr:count')
|
708
|
+
num.nil?? nil : num.to_i
|
709
|
+
end
|
710
|
+
|
711
|
+
def count=(num)
|
712
|
+
set_attr('thr:count', num.to_s)
|
713
|
+
end
|
714
|
+
|
715
|
+
def updated
|
716
|
+
value = get_attr('thr:updated')
|
717
|
+
value.nil?? nil : Time.iso8601(value)
|
718
|
+
end
|
719
|
+
|
720
|
+
def updated=(time)
|
721
|
+
time = time.iso8601 if time.instance_of?(Time)
|
722
|
+
set_attr('thr:updated', time)
|
723
|
+
end
|
724
|
+
|
725
|
+
end
|
726
|
+
|
727
|
+
class ReplyTarget < Element
|
728
|
+
element_ns Namespace::THR
|
729
|
+
element_name 'in-reply-to'
|
730
|
+
element_attr_accessors :href, :ref, :type, :source
|
731
|
+
|
732
|
+
def id
|
733
|
+
self.ref
|
734
|
+
end
|
735
|
+
|
736
|
+
def id=(ref)
|
737
|
+
self.ref = ref
|
738
|
+
end
|
739
|
+
|
740
|
+
end
|
741
|
+
|
742
|
+
# Category class
|
743
|
+
class Category < Element
|
744
|
+
element_name :category
|
745
|
+
element_attr_accessors :term, :scheme, :label
|
746
|
+
end
|
747
|
+
# Content class
|
748
|
+
class Content < Element
|
749
|
+
|
750
|
+
element_name :content
|
751
|
+
element_attr_accessors :type, :src
|
752
|
+
|
753
|
+
def initialize(params={})
|
754
|
+
super(params)
|
755
|
+
#self.body = params[:body] if params.has_key?(:body)
|
756
|
+
self.type = params[:type] if params.has_key?(:type)
|
757
|
+
end
|
758
|
+
|
759
|
+
def body=(value)
|
760
|
+
if value =~ /^[[:print:]]*$/
|
761
|
+
copy = "<div xmlns=\"http://www.w3.org/1999/xhtml\">#{value}</div>"
|
762
|
+
is_valid = true
|
763
|
+
begin
|
764
|
+
node = REXML::Document.new(copy).elements[1][0]
|
765
|
+
rescue
|
766
|
+
is_valid = false
|
767
|
+
end
|
768
|
+
if is_valid && node.instance_of?(REXML::Element)
|
769
|
+
@elem.add_element node
|
770
|
+
self.type = 'xhtml'
|
771
|
+
else
|
772
|
+
@elem.add_text value
|
773
|
+
self.type = (value =~ /^\s*</) ? 'html' : 'text'
|
774
|
+
end
|
775
|
+
else
|
776
|
+
@elem.add_text([value].pack('m').chomp)
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
def body
|
781
|
+
if @body.nil?
|
782
|
+
mode = self.type == 'xhtml' ? 'xml'\
|
783
|
+
: self.type =~ %r{[\/+]xml$} ? 'xml'\
|
784
|
+
: self.type == 'html' ? 'escaped'\
|
785
|
+
: self.type == 'text' ? 'escaped'\
|
786
|
+
: self.type =~ %r{^text} ? 'escaped'\
|
787
|
+
: 'base64'
|
788
|
+
case(mode)
|
789
|
+
when 'xml'
|
790
|
+
unless @elem.elements.empty?
|
791
|
+
if @elem.elements.size == 1 && @elem.elements[1].name == 'div'
|
792
|
+
@body = @elem.elements[1].collect{ |c| c.to_s }.join('')
|
793
|
+
else
|
794
|
+
@body = @elem.collect{ |c| c.to_s }.join('')
|
795
|
+
end
|
796
|
+
else
|
797
|
+
@body = @elem.text
|
798
|
+
end
|
799
|
+
when 'escaped'
|
800
|
+
@body = @elem.text
|
801
|
+
when 'base64'
|
802
|
+
text = @elem.text
|
803
|
+
@body = text.nil?? nil : text.unpack('m').first
|
804
|
+
else
|
805
|
+
@body = nil
|
806
|
+
end
|
807
|
+
end
|
808
|
+
@body
|
809
|
+
end
|
810
|
+
end
|
811
|
+
|
812
|
+
class RootElement < Element
|
813
|
+
def initialize(params={})
|
814
|
+
super(params)
|
815
|
+
if params.has_key?(:stream)
|
816
|
+
stream = params[:stream]
|
817
|
+
@elem = REXML::Document.new(stream).root
|
818
|
+
elsif params.has_key?(:doc)
|
819
|
+
@elem = params[:doc].elements[1]
|
820
|
+
end
|
821
|
+
@ns = Namespace.new(:uri => @elem.namespace)
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
class CoreElement < RootElement
|
826
|
+
|
827
|
+
element_text_accessors :id, :title, :rights
|
828
|
+
element_datetime_accessor :updated
|
829
|
+
element_object_list_accessor :link, Link, :links
|
830
|
+
element_object_list_accessor :category, Category, :categories
|
831
|
+
element_object_list_accessor :author, Author, :authors
|
832
|
+
element_object_list_accessor :contributor, Contributor, :contributors
|
833
|
+
|
834
|
+
def self.element_link_accessor(type)
|
835
|
+
type = type.to_s
|
836
|
+
meth_name = [type.tr('-','_'), 'link'].join('_')
|
837
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
838
|
+
|
839
|
+
def #{meth_name}
|
840
|
+
selected = links.select{ |l| l.rel == '#{type}' }
|
841
|
+
selected.empty? ? nil : selected.first.href
|
842
|
+
end
|
843
|
+
|
844
|
+
def #{meth_name}s
|
845
|
+
links.select{ |l| l.rel == '#{type}' }.collect{ |l| l.href }
|
846
|
+
end
|
847
|
+
|
848
|
+
def add_#{meth_name}(href)
|
849
|
+
l = Link.new
|
850
|
+
l.href = href
|
851
|
+
l.rel = '#{type}'
|
852
|
+
add_link l
|
853
|
+
end
|
854
|
+
|
855
|
+
def #{meth_name}=(href)
|
856
|
+
xpath = child_xpath(Namespace::ATOM, 'link', { :rel => '#{type}' })
|
857
|
+
@elem.elements.delete_all(xpath)
|
858
|
+
add_#{meth_name}(href)
|
859
|
+
end
|
860
|
+
EOS
|
861
|
+
end
|
862
|
+
|
863
|
+
def self.element_link_accessors(*types)
|
864
|
+
types.flatten.each{ |type| element_link_accessor(type) }
|
865
|
+
end
|
866
|
+
|
867
|
+
element_link_accessors %w(self edit edit-media related enclosure via first previous next last)
|
868
|
+
|
869
|
+
def alternate_links
|
870
|
+
links.select{ |l| l.rel.nil? || l.rel == 'alternate' }.collect{ |l| l.href }
|
871
|
+
end
|
872
|
+
|
873
|
+
def alternate_link
|
874
|
+
alternates = links.select{ |l| l.rel.nil? || l.rel == 'alternate' }
|
875
|
+
alternates.empty? ? nil : alternates.first.href
|
876
|
+
end
|
877
|
+
|
878
|
+
def add_alternate_link(href)
|
879
|
+
l = Link.new
|
880
|
+
l.href = href
|
881
|
+
l.rel = 'alternate'
|
882
|
+
add_link l
|
883
|
+
end
|
884
|
+
|
885
|
+
def alternate_link=(href)
|
886
|
+
xpath = child_xpath(Namespace::ATOM, 'link', { :rel => 'alternate' })
|
887
|
+
@elem.elements.delete_all(xpath)
|
888
|
+
add_alternate_link(href)
|
889
|
+
end
|
890
|
+
|
891
|
+
def initialize(params={})
|
892
|
+
if params.has_key?(:uri) || params.has_key?(:file)
|
893
|
+
target = params.has_key?(:uri) ? URI.parse(params.delete(:uri)) \
|
894
|
+
: params[:file].is_a?(Pathname) ? params.delete(:file) \
|
895
|
+
: Pathname.new(params.delete(:file))
|
896
|
+
params[:stream] = target.open { |f| f.read }
|
897
|
+
end
|
898
|
+
super(params)
|
899
|
+
end
|
900
|
+
|
901
|
+
end
|
902
|
+
|
903
|
+
class Control < Element
|
904
|
+
element_ns Namespace::APP_WITH_PREFIX
|
905
|
+
element_name :control
|
906
|
+
element_text_accessor :draft
|
907
|
+
end
|
908
|
+
|
909
|
+
class Categories < Element
|
910
|
+
element_ns Namespace::APP
|
911
|
+
element_name :categories
|
912
|
+
element_attr_accessors :href, :scheme, :fixed
|
913
|
+
|
914
|
+
def category
|
915
|
+
get_object(Namespace::ATOM_WITH_PREFIX, 'category', Category)
|
916
|
+
end
|
917
|
+
|
918
|
+
def category=(value)
|
919
|
+
set(Namespace::ATOM_WITH_PREFIX, 'category', value)
|
920
|
+
end
|
921
|
+
|
922
|
+
def add_category(value)
|
923
|
+
add(Namespace::ATOM_WITH_PREFIX, 'category', value)
|
924
|
+
end
|
925
|
+
|
926
|
+
def categories
|
927
|
+
get_objects(Namespace::ATOM_WITH_PREFIX, 'category', Category)
|
928
|
+
end
|
929
|
+
|
930
|
+
def categories=(value)
|
931
|
+
category = value
|
932
|
+
end
|
933
|
+
end
|
934
|
+
|
935
|
+
class Collection < Element
|
936
|
+
element_ns Namespace::APP
|
937
|
+
element_name :collection
|
938
|
+
element_attr_accessor :href
|
939
|
+
element_text_list_accessor :accept, :accepts
|
940
|
+
element_object_list_accessor :categories, Categories, :categories_list
|
941
|
+
def title
|
942
|
+
title = get(Namespace::ATOM_WITH_PREFIX, 'title')
|
943
|
+
title.nil?? nil : title.text
|
944
|
+
end
|
945
|
+
def title=(value)
|
946
|
+
set(Namespace::ATOM_WITH_PREFIX, 'title', value)
|
947
|
+
end
|
948
|
+
|
949
|
+
end
|
950
|
+
|
951
|
+
class Workspace < Element
|
952
|
+
element_ns Namespace::APP
|
953
|
+
element_name :workspace
|
954
|
+
element_object_list_accessor :collection, Collection, :collections
|
955
|
+
def title
|
956
|
+
title = get(Namespace::ATOM_WITH_PREFIX, 'title')
|
957
|
+
title.nil?? nil : title.text
|
958
|
+
end
|
959
|
+
def title=(value)
|
960
|
+
set(Namespace::ATOM_WITH_PREFIX, 'title', value)
|
961
|
+
end
|
962
|
+
end
|
963
|
+
# = Atom::Service
|
964
|
+
#
|
965
|
+
# This class represents service document
|
966
|
+
#
|
967
|
+
class Service < RootElement
|
968
|
+
element_ns Namespace::APP
|
969
|
+
element_name :service
|
970
|
+
element_object_list_accessor :workspace, Workspace, :workspaces
|
971
|
+
end
|
972
|
+
|
973
|
+
class Entry < CoreElement
|
974
|
+
element_name :entry
|
975
|
+
element_text_accessors :source, :summary
|
976
|
+
element_datetime_accessor :published
|
977
|
+
element_link_accessor :replies
|
978
|
+
|
979
|
+
def links
|
980
|
+
ls = super
|
981
|
+
ls.collect do |l|
|
982
|
+
l.rel == 'replies' ? l.to_replies_link : l
|
983
|
+
end
|
984
|
+
end
|
985
|
+
|
986
|
+
def link
|
987
|
+
l = super
|
988
|
+
l.rel == 'replies' ? l.to_replies_link : l
|
989
|
+
end
|
990
|
+
|
991
|
+
def control
|
992
|
+
get_object(Namespace::APP_WITH_PREFIX, 'control', Control)
|
993
|
+
end
|
994
|
+
|
995
|
+
def control=(control)
|
996
|
+
set(Namespace::APP_WITH_PREFIX, 'control', control)
|
997
|
+
end
|
998
|
+
|
999
|
+
def add_control(control)
|
1000
|
+
add(Namespace::APP_WITH_PREFIX, 'control', control)
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
def controls
|
1004
|
+
get_objects(Namespace::APP_WITH_PREFIX, 'control', Control)
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
def controls=(control)
|
1008
|
+
control = control
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
def edited
|
1012
|
+
get(Namespace::APP_WITH_PREFIX, 'edited')
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def edited=(value)
|
1016
|
+
set(Namespace::APP_WITH_PREFIX, 'edited', value)
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
def total
|
1020
|
+
value = get(Namespace::THR, 'total')
|
1021
|
+
value.nil?? nil : value.to_i
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
def total=(value)
|
1025
|
+
set(Namespace::THR, 'total', value.to_s)
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
def content
|
1029
|
+
get_object(@ns, 'content', Content)
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
def content=(value)
|
1033
|
+
unless value.is_a?(Content)
|
1034
|
+
value = Content.new(:body => value)
|
1035
|
+
end
|
1036
|
+
set(@ns, 'content', value)
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
def in_reply_to(value=nil)
|
1040
|
+
if value.nil?
|
1041
|
+
get_object(Namespace::THR, 'in-reply-to', ReplyTarget)
|
1042
|
+
else
|
1043
|
+
value = ReplyTarget.new(value) if value.is_a?(Hash)
|
1044
|
+
set(Namespace::THR, 'in-reply-to', value)
|
1045
|
+
end
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
end
|
1049
|
+
# Feed Class
|
1050
|
+
#
|
1051
|
+
class Feed < CoreElement
|
1052
|
+
element_name :feed
|
1053
|
+
element_text_accessors :icon, :logo, :subtitle
|
1054
|
+
element_object_list_accessor :entry, Entry, :entries
|
1055
|
+
|
1056
|
+
def total_results
|
1057
|
+
value = get(Namespace::OPEN_SEARCH, 'totalResults')
|
1058
|
+
value.nil?? nil : value.text.to_i
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
def total_results=(num)
|
1062
|
+
set(Namespace::OPEN_SEARCH, 'totalResults', num.to_s)
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
def start_index
|
1066
|
+
value = get(Namespace::OPEN_SEARCH, 'startIndex')
|
1067
|
+
value.nil?? nil : value.text.to_i
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
def start_index=(num)
|
1071
|
+
set(Namespace::OPEN_SEARCH, 'startIndex', num.to_s)
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
def items_per_page
|
1075
|
+
value = get(Namespace::OPEN_SEARCH, 'itemsPerPage')
|
1076
|
+
value.nil?? nil : value.text.to_i
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
def items_per_page=(num)
|
1080
|
+
set(Namespace::OPEN_SEARCH, 'itemsPerPage', num.to_s)
|
1081
|
+
end
|
1082
|
+
|
1083
|
+
def generator
|
1084
|
+
get_object(Namespace::ATOM, 'generator', Generator)
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
def generator=(gen)
|
1088
|
+
gen = gen.is_a?(Generator) ? gen : Generator.new(:name => gen)
|
1089
|
+
set(Namespace::ATOM, 'generator', gen)
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
def language
|
1093
|
+
@elem.attributes['xml:lang']
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
def language=(lang)
|
1097
|
+
#@elem.add_attribute 'lang', 'http://www.w3.org/XML/1998/Namespace'
|
1098
|
+
@elem.add_attribute 'xml:lang', lang
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
def version
|
1102
|
+
@elem.attributes['version']
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
def version=(ver)
|
1106
|
+
@elem.add_attribute 'version', ver
|
1107
|
+
end
|
1108
|
+
end
|
1109
|
+
end
|
1110
|
+
# = Atompub
|
1111
|
+
#
|
1112
|
+
module Atompub
|
1113
|
+
|
1114
|
+
class RequestError < StandardError; end #:nodoc:
|
1115
|
+
class AuthError < RequestError ; end #:nodoc:
|
1116
|
+
class CacheNotFoundError < RequestError ; end #:nodoc:
|
1117
|
+
class ResponseError < RequestError ; end #:nodoc:
|
1118
|
+
class MediaTypeError < RequestError ; end #:nodoc:
|
1119
|
+
# = Atompub::CacheResource
|
1120
|
+
#
|
1121
|
+
# Cache resource that is stored by AbstractCache or it's subclass.
|
1122
|
+
# This class just has only three accessors.
|
1123
|
+
#
|
1124
|
+
# * etag
|
1125
|
+
# * last_modofied
|
1126
|
+
# * resource
|
1127
|
+
#
|
1128
|
+
class CacheResource
|
1129
|
+
attr_accessor :etag, :last_modified, :resource
|
1130
|
+
def initialize(params)
|
1131
|
+
@etag = params[:etag]
|
1132
|
+
@last_modified = parmas[:last_modified]
|
1133
|
+
@resource = params[:rc]
|
1134
|
+
end
|
1135
|
+
end
|
1136
|
+
# = Atompub::AbstractCache
|
1137
|
+
#
|
1138
|
+
# Cache storage for atompub networking.
|
1139
|
+
# In case the server that provieds AtomPub-API handles caching with
|
1140
|
+
# http headers, ETag or If-Modified-Since, you can handle them with this class.
|
1141
|
+
# But this class does nothing, use subclass that inherits this.
|
1142
|
+
#
|
1143
|
+
class AbstractCache
|
1144
|
+
# singleton closure
|
1145
|
+
@@singleton = nil
|
1146
|
+
# Get singleton instance.
|
1147
|
+
def self.instance
|
1148
|
+
@@singleton = self.new if @@singleton.nil?
|
1149
|
+
@@singleton
|
1150
|
+
end
|
1151
|
+
# initializer
|
1152
|
+
def initialize
|
1153
|
+
end
|
1154
|
+
# Get cache resource for indicated uri
|
1155
|
+
def get(uri)
|
1156
|
+
nil
|
1157
|
+
end
|
1158
|
+
# Store cache resource
|
1159
|
+
def put(uri, params)
|
1160
|
+
end
|
1161
|
+
end
|
1162
|
+
# = Atompub::SimpleCache
|
1163
|
+
#
|
1164
|
+
# Basic cache storage class.
|
1165
|
+
# Use Hash object to store data.
|
1166
|
+
class SimpleCache < AbstractCache
|
1167
|
+
# singleton closure
|
1168
|
+
@@singleton = nil
|
1169
|
+
# Get singleton instance
|
1170
|
+
def self.instance
|
1171
|
+
@@singleton = self.new if @@singleton.nil?
|
1172
|
+
@@singleton
|
1173
|
+
end
|
1174
|
+
# initializer
|
1175
|
+
def initialize
|
1176
|
+
@cache = Hash.new
|
1177
|
+
end
|
1178
|
+
# Pick cache resource from hash for indicated uri.
|
1179
|
+
def get(uri)
|
1180
|
+
@cache.has_key?(url) ? @cache[uri] : nil
|
1181
|
+
end
|
1182
|
+
# Set cache resource into hash.
|
1183
|
+
def put(uri, params)
|
1184
|
+
@cache[uri] = CacheResource.new(params)
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
class ServiceInfo
|
1190
|
+
|
1191
|
+
def initialize(params)
|
1192
|
+
@collection = params[:collection]
|
1193
|
+
@allowed_categories = nil
|
1194
|
+
@accepts = nil
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
def allows_category?(test)
|
1198
|
+
return true if @collection.nil?
|
1199
|
+
categories_list = @collection.categories_list
|
1200
|
+
return true if categories_list.empty?
|
1201
|
+
return true if categories_list.all? { |cats| cats.fixed.nil? || cats.fixed != 'yes' }
|
1202
|
+
if @allowed_categories.nil?
|
1203
|
+
@allowed_categories = categories_list.collect do |cats|
|
1204
|
+
cats.categories.collect do |cat|
|
1205
|
+
scheme = cat.scheme || cats.scheme || nil
|
1206
|
+
new_cat = Atom::Category.new :term => cat.term
|
1207
|
+
new_cat.scheme = scheme unless scheme.nil?
|
1208
|
+
new_cat
|
1209
|
+
end
|
1210
|
+
end.flatten
|
1211
|
+
end
|
1212
|
+
return false if @allowed_categories.empty?
|
1213
|
+
@allowed_categories.any?{ |c| c.term == test.term && (c.scheme.nil? || (!c.scheme.nil? && c.scheme == test.scheme )) }
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
def accepts_media_type?(content_type)
|
1217
|
+
return true if @collection.nil?
|
1218
|
+
if @accepts.nil?
|
1219
|
+
@accepts = @collection.accepts.collect do |accept|
|
1220
|
+
accept.split(/[\s,]+/)
|
1221
|
+
end.flatten
|
1222
|
+
@accepts << Atom::MediaType::ENTRY if @accepts.empty?
|
1223
|
+
end
|
1224
|
+
type = Atom::MediaType.new(content_type)
|
1225
|
+
@accepts.any?{ |a| type.is_a?(a) }
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
class ServiceInfoStorage
|
1231
|
+
|
1232
|
+
@@singleton = nil
|
1233
|
+
|
1234
|
+
def self.instance
|
1235
|
+
@@singleton = self.new if @@singleton.nil?
|
1236
|
+
@@singleton
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
def initialize
|
1240
|
+
@info = Hash.new
|
1241
|
+
end
|
1242
|
+
|
1243
|
+
def get(uri)
|
1244
|
+
@info.has_key?(uri) ? @info[uri] : nil
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
def put(uri, collection, client=nil)
|
1248
|
+
collection = clone_collection(collection, client)
|
1249
|
+
@info[uri] = ServiceInfo.new(:collection => collection)
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
private
|
1253
|
+
def clone_collection(collection, client=nil)
|
1254
|
+
coll = Atom::Collection.new
|
1255
|
+
coll.title = collection.title
|
1256
|
+
coll.href = collection.href
|
1257
|
+
collection.accepts.each { |a| coll.add_accept a }
|
1258
|
+
collection.categories_list.each do |cats|
|
1259
|
+
unless cats.nil?
|
1260
|
+
new_cats = cats.href.nil?? clone_categories(cats) : get_categories(cats.href, client)
|
1261
|
+
coll.categories = new_cats unless new_cats.nil?
|
1262
|
+
end
|
1263
|
+
end
|
1264
|
+
end
|
1265
|
+
|
1266
|
+
def get_categories(uri, client=nil)
|
1267
|
+
client.nil?? nil : client.get_categories(uri)
|
1268
|
+
end
|
1269
|
+
|
1270
|
+
def clone_categories(categories)
|
1271
|
+
cats = Atom::Categories.new
|
1272
|
+
cats.fixed = categories.fixed
|
1273
|
+
cats.scheme = categories.scheme
|
1274
|
+
categories.categories.each do |c|
|
1275
|
+
new_c = Atom::Category.new
|
1276
|
+
new_c.term = c.term
|
1277
|
+
new_c.scheme = c.scheme
|
1278
|
+
new_c.label = c.label
|
1279
|
+
cats.add_category new_c
|
1280
|
+
end
|
1281
|
+
cats
|
1282
|
+
end
|
1283
|
+
end
|
1284
|
+
# = Atompub::Client
|
1285
|
+
#
|
1286
|
+
class Client
|
1287
|
+
# user agent
|
1288
|
+
attr_accessor :agent
|
1289
|
+
# request object for current networking context
|
1290
|
+
attr_reader :req
|
1291
|
+
alias_method :request, :req
|
1292
|
+
# response object for current networking context
|
1293
|
+
attr_reader :res
|
1294
|
+
alias_method :response, :res
|
1295
|
+
# resource object for current networking context
|
1296
|
+
attr_reader :rc
|
1297
|
+
alias_method :resource, :rc
|
1298
|
+
# Initializer
|
1299
|
+
#
|
1300
|
+
# * auth
|
1301
|
+
# * cache
|
1302
|
+
#
|
1303
|
+
def initialize(params={})
|
1304
|
+
unless params.has_key?(:auth)
|
1305
|
+
throw ArgumentError.new("Atompub::Client needs :auth as argument for constructor.")
|
1306
|
+
end
|
1307
|
+
@auth = params[:auth]
|
1308
|
+
@cache = params.has_key?(:cache) && params[:info].kind_of?(AbstractCache) ? params[:cache] : AbstractCache.instance
|
1309
|
+
@service_info = params.has_key?(:info) && params[:info].kind_of?(ServiceInfoStorage) ? params[:info] : ServiceInfoStorage.instance
|
1310
|
+
@http_class = Net::HTTP
|
1311
|
+
@agent = "Atompub::Client/#{AtomUtil::VERSION}"
|
1312
|
+
end
|
1313
|
+
# Set proxy if you need.
|
1314
|
+
#
|
1315
|
+
# Example:
|
1316
|
+
#
|
1317
|
+
# client.use_proxy('http://myproxy/', 8080)
|
1318
|
+
# client.use_proxy('http://myproxy/', 8080, 'myusername', 'mypassword')
|
1319
|
+
#
|
1320
|
+
def use_proxy(uri, port, user=nil, pass=nil)
|
1321
|
+
@http_class = Net::HTTP::Proxy(uri, port, user, pass)
|
1322
|
+
end
|
1323
|
+
# Get service document
|
1324
|
+
# This returns Atom::Service object.
|
1325
|
+
# see the document of Atom::Service in detail.
|
1326
|
+
#
|
1327
|
+
# Example:
|
1328
|
+
#
|
1329
|
+
# service = client.get_service(service_uri)
|
1330
|
+
# service.workspaces.each do |w|
|
1331
|
+
# w.collections.each do |c|
|
1332
|
+
# puts c.href
|
1333
|
+
# end
|
1334
|
+
# end
|
1335
|
+
#
|
1336
|
+
def get_service(service_uri)
|
1337
|
+
get_contents_except_resources(service_uri) do |res|
|
1338
|
+
warn "Bad Content Type" unless Atom::MediaType::SERVICE.is_a?(@res['Content-Type'])
|
1339
|
+
@rc = Atom::Service.new :stream => @res.body
|
1340
|
+
@rc.workspaces.each do |workspace|
|
1341
|
+
workspace.collections.each do |collection|
|
1342
|
+
#@service_info.put(collection.href, collection, self)
|
1343
|
+
@service_info.put(collection.href, collection)
|
1344
|
+
end
|
1345
|
+
end
|
1346
|
+
end
|
1347
|
+
@rc
|
1348
|
+
end
|
1349
|
+
# Get categories
|
1350
|
+
# This returns Atom::Categories object.
|
1351
|
+
# see the document of Atom::Categories in detail.
|
1352
|
+
#
|
1353
|
+
# Example:
|
1354
|
+
#
|
1355
|
+
#
|
1356
|
+
def get_categories(categories_uri)
|
1357
|
+
get_contents_except_resources(categories_uri) do |res|
|
1358
|
+
warn "Bad Content Type" unless Atom::MediaType::CATEGORIES.is_a?(@res['Content-Type'])
|
1359
|
+
@rc = Atom::Categories.new :stream => @res.body
|
1360
|
+
end
|
1361
|
+
@rc
|
1362
|
+
end
|
1363
|
+
# Get feed
|
1364
|
+
# This returns Atom::Feed object.
|
1365
|
+
# see the document of Atom::Feed in detail.
|
1366
|
+
#
|
1367
|
+
# Example:
|
1368
|
+
#
|
1369
|
+
def get_feed(feed_uri)
|
1370
|
+
get_contents_except_resources(feed_uri) do |res|
|
1371
|
+
warn "Bad Content Type" unless Atom::MediaType::FEED.is_a?(@res['Content-Type'])
|
1372
|
+
@rc = Atom::Feed.new :stream => res.body
|
1373
|
+
end
|
1374
|
+
@rc
|
1375
|
+
end
|
1376
|
+
# Get entry
|
1377
|
+
#
|
1378
|
+
# Example:
|
1379
|
+
#
|
1380
|
+
# entry = client.get_entry(entry_uri)
|
1381
|
+
# puts entry.id
|
1382
|
+
# puts entry.title
|
1383
|
+
#
|
1384
|
+
def get_entry(entry_uri)
|
1385
|
+
get_resource(entry_uri)
|
1386
|
+
unless @rc.instance_of?(Atom::Entry)
|
1387
|
+
raise ResponseError, "Response is not Atom Entry"
|
1388
|
+
end
|
1389
|
+
@rc
|
1390
|
+
end
|
1391
|
+
# Get media resource
|
1392
|
+
#
|
1393
|
+
# Example:
|
1394
|
+
#
|
1395
|
+
# resource, content_type = client.get_media(media_uri)
|
1396
|
+
#
|
1397
|
+
def get_media(media_uri)
|
1398
|
+
get_resource(media_uri)
|
1399
|
+
if @rc.instance_of?(Atom::Entry)
|
1400
|
+
raise ResponseError, "Response is not Media Resource"
|
1401
|
+
end
|
1402
|
+
return @rc, @res.content_type
|
1403
|
+
end
|
1404
|
+
# Create new entry
|
1405
|
+
#
|
1406
|
+
# Example:
|
1407
|
+
#
|
1408
|
+
# entry = Atom::Entry.new
|
1409
|
+
# entry.title = 'foo'
|
1410
|
+
# author = Atom::Author.new
|
1411
|
+
# author.name = 'Lyo Kato'
|
1412
|
+
# author.email = 'lyo.kato@gmail.com'
|
1413
|
+
# entry.author = author
|
1414
|
+
# entry_uri = client.create_entry(post_uri, entry)
|
1415
|
+
#
|
1416
|
+
def create_entry(post_uri, entry, slug=nil)
|
1417
|
+
unless entry.kind_of?(Atom::Entry)
|
1418
|
+
entry = Atom::Entry.new :stream => entry
|
1419
|
+
end
|
1420
|
+
service = @service_info.get(post_uri)
|
1421
|
+
unless entry.categories.all?{ |c| service.allows_category?(c) }
|
1422
|
+
raise RequestError, "Forbidden Category"
|
1423
|
+
end
|
1424
|
+
create_resource(post_uri, entry.to_s, Atom::MediaType::ENTRY.to_s, slug)
|
1425
|
+
@res['Location']
|
1426
|
+
end
|
1427
|
+
# Create new media resource
|
1428
|
+
#
|
1429
|
+
# Example:
|
1430
|
+
#
|
1431
|
+
# media_uri = client.create_media(post_media_uri, 'myimage.jpg', 'image/jpeg')
|
1432
|
+
#
|
1433
|
+
def create_media(media_uri, file_path, content_type, slug=nil)
|
1434
|
+
file_path = Pathname.new(file_path) unless file_path.is_a?(Pathname)
|
1435
|
+
stream = file_path.open { |f| f.binmode; f.read }
|
1436
|
+
service = @service_info.get(media_uri)
|
1437
|
+
unless service.accept_media_type?(content_type)
|
1438
|
+
raise RequestError, "Unsupported Media Type"
|
1439
|
+
end
|
1440
|
+
create_resource(media_uri, stream, content_type, slug)
|
1441
|
+
@res['Location']
|
1442
|
+
end
|
1443
|
+
# Update entry
|
1444
|
+
#
|
1445
|
+
# Example:
|
1446
|
+
#
|
1447
|
+
# entry = client.get_entry(resource_uri)
|
1448
|
+
# entry.summary = "Changed Summary!"
|
1449
|
+
# client.update_entry(entry)
|
1450
|
+
#
|
1451
|
+
def update_entry(edit_uri, entry)
|
1452
|
+
unless entry.kind_of?(Atom::Entry)
|
1453
|
+
entry = Atom::Entry.new :stream => entry
|
1454
|
+
end
|
1455
|
+
update_resource(edit_uri, entry.to_s, Atom::MediaType::ENTRY.to_s)
|
1456
|
+
end
|
1457
|
+
# Update media resource
|
1458
|
+
#
|
1459
|
+
# Example:
|
1460
|
+
#
|
1461
|
+
# entry = client.get_entry(media_link_uri)
|
1462
|
+
# client.update_media(entry.edit_media_link, 'newimage.jpg', 'image/jpeg')
|
1463
|
+
#
|
1464
|
+
def update_media(media_uri, file_path, content_type)
|
1465
|
+
file_path = Pathname.new(file_path) unless file_path.is_a?(Pathname)
|
1466
|
+
stream = file_path.open { |f| f.binmode; f.read }
|
1467
|
+
update_resource(media_uri, stream, content_type)
|
1468
|
+
end
|
1469
|
+
# Delete entry
|
1470
|
+
#
|
1471
|
+
# Example:
|
1472
|
+
#
|
1473
|
+
# entry = client.get_entry(resource_uri)
|
1474
|
+
# client.delete_entry(entry.edit_link)
|
1475
|
+
#
|
1476
|
+
def delete_entry(edit_uri)
|
1477
|
+
delete_resource(edit_uri)
|
1478
|
+
end
|
1479
|
+
# Delete media
|
1480
|
+
#
|
1481
|
+
# Example:
|
1482
|
+
#
|
1483
|
+
# entry = client.get_entry(resource_uri)
|
1484
|
+
# client.delete_media(entry.edit_media_link)
|
1485
|
+
#
|
1486
|
+
def delete_media(media_uri)
|
1487
|
+
delete_resource(media_uri)
|
1488
|
+
end
|
1489
|
+
private
|
1490
|
+
# Set request headers those are required on each request accessing resources.
|
1491
|
+
def set_common_info(req)
|
1492
|
+
req['User-Agent'] = @agent
|
1493
|
+
@auth.authorize(req)
|
1494
|
+
end
|
1495
|
+
# Get contents, for example, service-document, categories, and feed.
|
1496
|
+
def get_contents_except_resources(uri, &block)
|
1497
|
+
clear
|
1498
|
+
uri = URI.parse(uri)
|
1499
|
+
@req = Net::HTTP::Get.new uri.path
|
1500
|
+
set_common_info(@req)
|
1501
|
+
@http_class.start(uri.host, uri.port) do |http|
|
1502
|
+
@res = http.request(@req)
|
1503
|
+
case @res
|
1504
|
+
when Net::HTTPOK
|
1505
|
+
block.call(@res) if block_given?
|
1506
|
+
else
|
1507
|
+
raise RequestError, "Failed to get contents. #{@res.code}"
|
1508
|
+
end
|
1509
|
+
end
|
1510
|
+
end
|
1511
|
+
# Get resouces(entry or media)
|
1512
|
+
def get_resource(uri)
|
1513
|
+
clear
|
1514
|
+
uri = URI.parse(uri)
|
1515
|
+
@req = Net::HTTP::Get.new uri.path
|
1516
|
+
set_common_info(@req)
|
1517
|
+
cache = @cache.get(uri.to_s)
|
1518
|
+
unless cache.nil?
|
1519
|
+
@req['If-Modified-Since'] = cache.last_modified unless cache.last_modified.nil?
|
1520
|
+
@req['If-None-Match'] = cache.etag unless cache.etag.nil?
|
1521
|
+
end
|
1522
|
+
@http_class.start(uri.host, uri.port) do |http|
|
1523
|
+
@res = http.request(@req)
|
1524
|
+
case @res
|
1525
|
+
when Net::HTTPOK
|
1526
|
+
if Atom::MediaType::ENTRY.is_a?(@res['Content-Type'])
|
1527
|
+
@rc = Atom::Entry.new :stream => @res.body
|
1528
|
+
else
|
1529
|
+
@rc = @res.body
|
1530
|
+
end
|
1531
|
+
@cache.put uri.to_s, {
|
1532
|
+
:rc => @rc,
|
1533
|
+
:last_modified => @res['Last-Modified'],
|
1534
|
+
:etag => @res['ETag'] }
|
1535
|
+
when Net::HTTPNotModified
|
1536
|
+
unless cache.nil?
|
1537
|
+
@rc = cache.rc
|
1538
|
+
else
|
1539
|
+
raise CacheNotFoundError, "Got Not-Modified response, but has no cache."
|
1540
|
+
end
|
1541
|
+
else
|
1542
|
+
raise RequestError, "Failed to get content. #{@res.code}"
|
1543
|
+
end
|
1544
|
+
end
|
1545
|
+
end
|
1546
|
+
# Create new resources(entry or media)
|
1547
|
+
def create_resource(uri, r, content_type, slug=nil)
|
1548
|
+
clear
|
1549
|
+
uri = URI.parse(uri)
|
1550
|
+
service = @service_info.get(uri.to_s)
|
1551
|
+
#unless service.accepts_media_type(content_type)
|
1552
|
+
# raise UnsupportedMediaTypeError, "Unsupported media type: #{content_type}."
|
1553
|
+
#end
|
1554
|
+
@req = Net::HTTP::Post.new uri.path
|
1555
|
+
@req['Content-Type'] = content_type
|
1556
|
+
@req['Slug'] = URI.encode(URI.decode(slug)) unless slug.nil?
|
1557
|
+
set_common_info(@req)
|
1558
|
+
@req.body = r
|
1559
|
+
@http_class.start(uri.host, uri.port) do |http|
|
1560
|
+
@res = http.request(@req)
|
1561
|
+
case @res
|
1562
|
+
when Net::HTTPSuccess
|
1563
|
+
warn "Bad Status Code" unless @res.class == Net::HTTPCreated
|
1564
|
+
warn "Bad Content Type" unless Atom::MediaType::ENTRY.is_a?(@res['Content-Type'])
|
1565
|
+
if @res['Location'].nil?
|
1566
|
+
raise ResponseError, "No Location"
|
1567
|
+
end
|
1568
|
+
unless @res.body.nil?
|
1569
|
+
@rc = Atom::Entry.new :stream => @res.body
|
1570
|
+
@cache.put uri.to_s, {
|
1571
|
+
:rc => @rc,
|
1572
|
+
:last_modified => @res['Last-Modified'],
|
1573
|
+
:etag => @res['ETag']
|
1574
|
+
}
|
1575
|
+
end
|
1576
|
+
else
|
1577
|
+
raise RequestError, "Failed to create resource."
|
1578
|
+
end
|
1579
|
+
end
|
1580
|
+
end
|
1581
|
+
# updated resources(entry or media)
|
1582
|
+
def update_resource(uri, r, content_type)
|
1583
|
+
clear
|
1584
|
+
uri = URI.parse(uri)
|
1585
|
+
@req = Net::HTTP::Put.new uri.path
|
1586
|
+
@req['Content-Type'] = content_type
|
1587
|
+
cache = @cache.get(uri.to_s)
|
1588
|
+
unless cache.nil?
|
1589
|
+
@req['If-Not-Modified-Since'] = cache.last_modofied unless cache.last_modified.nil?
|
1590
|
+
@req['If-Match'] = cache.etag unless cache.etag.nil?
|
1591
|
+
end
|
1592
|
+
set_common_info(@req)
|
1593
|
+
@req.body = r
|
1594
|
+
@http_class.start(uri.host, uri.port) do |http|
|
1595
|
+
@res = http.request(@req)
|
1596
|
+
case @res
|
1597
|
+
when Net::HTTPSuccess
|
1598
|
+
warn "Bad Status Code" unless @res.class == Net::HTTPOK
|
1599
|
+
unless @res.body.nil?
|
1600
|
+
@rc = Atom::MediaType::ENTRY.is_a?(@res['Content-Type']) ? Atom::Entry.new(:stream => @res.body) : @res.body
|
1601
|
+
@cache.put uri.to_s, {
|
1602
|
+
:rc => @rc,
|
1603
|
+
:etag => @res['ETag'],
|
1604
|
+
:last_modified => @res['Last-Modified'] }
|
1605
|
+
end
|
1606
|
+
else
|
1607
|
+
raise RequestError, "Failed to update resource. #{@res.code}"
|
1608
|
+
end
|
1609
|
+
end
|
1610
|
+
end
|
1611
|
+
# Delete resources(entry or media)
|
1612
|
+
def delete_resource(uri)
|
1613
|
+
clear
|
1614
|
+
uri = URI.parse(uri)
|
1615
|
+
@req = Net::HTTP::Delete.new uri.path
|
1616
|
+
@http_class.start(uri.host, uri.port) do |http|
|
1617
|
+
@res = http.request(@req)
|
1618
|
+
case @res
|
1619
|
+
when Net::HTTPSuccess
|
1620
|
+
warn "Bad Status Code" unless @res.class == Net::HTTPOK
|
1621
|
+
else
|
1622
|
+
raise RequestError, "Failed to delete resource. #{@res.code}"
|
1623
|
+
end
|
1624
|
+
end
|
1625
|
+
end
|
1626
|
+
# clear objects which depend on each networking context.
|
1627
|
+
def clear
|
1628
|
+
@req = nil
|
1629
|
+
@res = nil
|
1630
|
+
@rc = nil
|
1631
|
+
end
|
1632
|
+
end
|
1633
|
+
# Authentication classes
|
1634
|
+
module Auth
|
1635
|
+
# = Atompub::Auth::Wsse
|
1636
|
+
#
|
1637
|
+
# Class handles WSSE authentication
|
1638
|
+
# All you have to do is create this class's object with username and password,
|
1639
|
+
# and pass to Atompub::Client#new
|
1640
|
+
#
|
1641
|
+
# Usage:
|
1642
|
+
#
|
1643
|
+
# auth = Atompub::Auth::Wsse.new :username => username, :password => password
|
1644
|
+
# client = Atompub::Client.new :auth => auth
|
1645
|
+
#
|
1646
|
+
class Wsse
|
1647
|
+
# initializer
|
1648
|
+
#
|
1649
|
+
# Set two parameters as hash
|
1650
|
+
# * username
|
1651
|
+
# * password
|
1652
|
+
#
|
1653
|
+
# Usage:
|
1654
|
+
#
|
1655
|
+
# auth = Atompub::Auth::Wsse.new :username => name, :password => pass
|
1656
|
+
#
|
1657
|
+
def initialize(params)
|
1658
|
+
@username, @password = params[:username], params[:password]
|
1659
|
+
end
|
1660
|
+
# Add credential info to Net::HTTP::Request object
|
1661
|
+
#
|
1662
|
+
# Usaage:
|
1663
|
+
#
|
1664
|
+
# req = Net::HTTP::Get.new uri.path
|
1665
|
+
# auth.authorize(req)
|
1666
|
+
#
|
1667
|
+
def authorize(req)
|
1668
|
+
req['Authorization'] = 'WSSE profile="UsernameToken"'
|
1669
|
+
req['X-Wsse'] = gen_token
|
1670
|
+
end
|
1671
|
+
private
|
1672
|
+
# Generate username token for WSSE authentication
|
1673
|
+
def gen_token
|
1674
|
+
nonce = Array.new(10){rand(0x100000000)}.pack('I*')
|
1675
|
+
nonce_base64 = [nonce].pack('m').chomp
|
1676
|
+
now = Time.now.utc.iso8601
|
1677
|
+
digest = [Digest::SHA1.digest(nonce + now + @password)].pack('m').chomp
|
1678
|
+
sprintf(%Q<UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s">,
|
1679
|
+
@username, digest, nonce_base64, now)
|
1680
|
+
end
|
1681
|
+
end
|
1682
|
+
# = Atompub::Auth::Basic
|
1683
|
+
#
|
1684
|
+
# Usage:
|
1685
|
+
#
|
1686
|
+
# auth = Atompub::Auth::Basic.new :username => username, :password => password
|
1687
|
+
# client = Atompub::Client.new :auth => auth
|
1688
|
+
#
|
1689
|
+
class Basic
|
1690
|
+
# initializer
|
1691
|
+
#
|
1692
|
+
# Set two parameters as hash
|
1693
|
+
# * username
|
1694
|
+
# * password
|
1695
|
+
#
|
1696
|
+
# Usage:
|
1697
|
+
#
|
1698
|
+
# auth = Atompub::Auth::Basic.new :username => name, :password => pass
|
1699
|
+
#
|
1700
|
+
def initialize(params)
|
1701
|
+
@username, @password = params[:username], params[:password]
|
1702
|
+
end
|
1703
|
+
# Add credential info to Net::HTTP::Request object
|
1704
|
+
#
|
1705
|
+
# Usage:
|
1706
|
+
#
|
1707
|
+
# req = Net::HTTP::Get.new uri.path
|
1708
|
+
# auth.authorize(req)
|
1709
|
+
#
|
1710
|
+
def authorize(req)
|
1711
|
+
req.basic_auth @username, @password
|
1712
|
+
end
|
1713
|
+
end
|
1714
|
+
end
|
1715
|
+
|
1716
|
+
end
|
1717
|
+
|