atomutil 0.0.1

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