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/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
+