ape 1.0.0 → 1.5.0

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.
Files changed (63) hide show
  1. data/LICENSE +1 -1
  2. data/README +22 -8
  3. data/Rakefile +66 -0
  4. data/bin/ape_server +3 -3
  5. data/lib/ape.rb +131 -937
  6. data/lib/ape/atomURI.rb +1 -1
  7. data/lib/ape/authent.rb +11 -17
  8. data/lib/ape/categories.rb +8 -7
  9. data/lib/ape/collection.rb +3 -7
  10. data/lib/ape/crumbs.rb +1 -1
  11. data/lib/ape/entry.rb +1 -1
  12. data/lib/ape/escaper.rb +1 -1
  13. data/lib/ape/feed.rb +26 -14
  14. data/lib/ape/handler.rb +8 -3
  15. data/lib/ape/html.rb +1 -1
  16. data/lib/ape/invoker.rb +1 -1
  17. data/lib/ape/invokers/deleter.rb +1 -1
  18. data/lib/ape/invokers/getter.rb +1 -1
  19. data/lib/ape/invokers/poster.rb +1 -1
  20. data/lib/ape/invokers/putter.rb +1 -1
  21. data/lib/ape/names.rb +1 -1
  22. data/lib/ape/print_writer.rb +4 -6
  23. data/lib/ape/reporter.rb +156 -0
  24. data/lib/ape/reporters/atom_reporter.rb +51 -0
  25. data/lib/ape/reporters/atom_template.eruby +38 -0
  26. data/lib/ape/reporters/html_reporter.rb +53 -0
  27. data/lib/ape/reporters/html_template.eruby +62 -0
  28. data/lib/ape/reporters/text_reporter.rb +37 -0
  29. data/lib/ape/samples.rb +30 -51
  30. data/lib/ape/server.rb +16 -4
  31. data/lib/ape/service.rb +1 -1
  32. data/lib/ape/util.rb +67 -0
  33. data/lib/ape/validator.rb +85 -57
  34. data/lib/ape/validator_dsl.rb +40 -0
  35. data/lib/ape/validators/entry_posts_validator.rb +226 -0
  36. data/lib/ape/validators/media_linkage_validator.rb +78 -0
  37. data/lib/ape/validators/media_posts_validator.rb +104 -0
  38. data/lib/ape/validators/sanitization_validator.rb +57 -0
  39. data/lib/ape/validators/schema_validator.rb +57 -0
  40. data/lib/ape/validators/service_document_validator.rb +64 -0
  41. data/lib/ape/validators/sorting_validator.rb +87 -0
  42. data/lib/ape/version.rb +1 -1
  43. data/{lib/ape/samples → samples}/atom_schema.txt +0 -0
  44. data/{lib/ape/samples → samples}/basic_entry.eruby +4 -4
  45. data/{lib/ape/samples → samples}/categories_schema.txt +0 -0
  46. data/{lib/ape/samples → samples}/mini_entry.eruby +0 -0
  47. data/{lib/ape/samples → samples}/service_schema.txt +0 -0
  48. data/{lib/ape/samples → samples}/unclean_xhtml_entry.eruby +0 -0
  49. data/test/test_helper.rb +33 -1
  50. data/test/unit/ape_test.rb +92 -0
  51. data/test/unit/authent_test.rb +2 -2
  52. data/test/unit/reporter_test.rb +102 -0
  53. data/test/unit/samples_test.rb +2 -2
  54. data/test/unit/validators_test.rb +50 -0
  55. data/{lib/ape/layout → web}/ape.css +5 -1
  56. data/{lib/ape/layout → web}/ape_logo.png +0 -0
  57. data/{lib/ape/layout → web}/index.html +2 -5
  58. data/{lib/ape/layout → web}/info.png +0 -0
  59. metadata +108 -56
  60. data/CHANGELOG +0 -1
  61. data/Manifest +0 -45
  62. data/ape.gemspec +0 -57
  63. data/scripts/go.rb +0 -29
@@ -1,65 +1,93 @@
1
- # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
- # Use is subject to license terms - see file "LICENSE"
3
-
4
- if RUBY_PLATFORM =~ /java/
5
- require 'java'
6
- CompactSchemaReader = com.thaiopensource.validate.rng.CompactSchemaReader
7
- ValidationDriver = com.thaiopensource.validate.ValidationDriver
8
- StringReader = java.io.StringReader
9
- StringWriter = java.io.StringWriter
10
- InputSource = org.xml.sax.InputSource
11
- ErrorHandlerImpl = com.thaiopensource.xml.sax.ErrorHandlerImpl
12
- PropertyMapBuilder = com.thaiopensource.util.PropertyMapBuilder
13
- ValidateProperty = com.thaiopensource.validate.ValidateProperty
14
- end
15
-
16
1
  module Ape
17
- class Validator
18
-
19
- attr_reader :error
20
-
21
- def Validator.validate(schema, text, name, ape)
22
- # Can do this in JRuby, not native Ruby (sigh)
23
- if RUBY_PLATFORM =~ /java/
24
- rnc_validate(schema, text, name, ape)
25
- else
26
- true
27
- end
28
- end
29
-
30
- def Validator.rnc_validate(schema, text, name, ape)
31
- schemaError = StringWriter.new
32
- schemaEH = ErrorHandlerImpl.new(schemaError)
33
- properties = PropertyMapBuilder.new
34
- properties.put(ValidateProperty::ERROR_HANDLER, schemaEH)
35
- error = nil
36
- driver = ValidationDriver.new(properties.toPropertyMap, CompactSchemaReader.getInstance)
37
- if driver.loadSchema(InputSource.new(StringReader.new(schema)))
38
- begin
39
- if !driver.validate(InputSource.new(StringReader.new(text)))
40
- error = schemaError.toString
2
+ require 'rexml/document'
3
+ class ValidationError < StandardError ; end
4
+
5
+ class Validator
6
+ require File.dirname(__FILE__) + '/validator_dsl.rb'
7
+ include Ape::ValidatorDsl
8
+ include Ape::Util
9
+
10
+ attr_accessor :reporter, :authent
11
+
12
+ def self.custom_validators(reporter, authent)
13
+ validators = []
14
+ Dir[Ape.home + '/validators/*.rb'].each do |v|
15
+ require v
16
+ class_name = v.gsub(/(.+\/validators\/)(.+)(.rb)/, '\2').gsub(/(^|_)(.)/) { $2.upcase }
17
+ validator = eval("#{class_name}.new", binding, __FILE__, __LINE__)
18
+ if validator.enabled?
19
+ validator.reporter = reporter
20
+ validator.authent = authent
21
+ validators << validator
41
22
  end
42
- rescue org.xml.sax.SAXParseException
43
- error = $!.to_s.sub(/\n.*$/, '')
44
23
  end
45
- else
46
- error = schemaError.toString
24
+ validators
47
25
  end
48
-
49
- if !error
50
- ape.good "#{name} passed schema validation."
51
- true
52
- else
53
- # this kind of sucks, but I spent a looong time lost in a maze of twisty
54
- # little passages without being able to figure out how to
55
- # tell jing what name I'd like to call the InputSource
56
- ape.error "#{name} failed schema validation:\n" + error.gsub('(unknown file):', 'Line ')
57
- false
26
+
27
+ def self.instance(key, reporter, authent = nil)
28
+ validator = resolve_plugin(key, 'validators', 'validator')
29
+ raise ValidationError, "Unknown validator #{key}" unless validator
30
+ validator.reporter = reporter
31
+ validator.authent = authent
32
+ validator
58
33
  end
34
+
35
+ =begin
36
+ Each validator implements its own bussiness logic. This method is executed by the main script
37
+ in order to assure that some aspect of atomPub implementation is correct
38
+ =end
39
+ def validate(opts = {})
40
+ raise ValidationError, "superclass doesn't implement this method"
41
+ end
42
+
43
+ protected
44
+ # Fetch a feed and look up an entry by ID in it
45
+ def find_entry(feed_uri, name, id, report=false)
46
+ entries = Feed.read(feed_uri, name, reporter, report)
47
+ entries.each do |from_feed|
48
+ return from_feed if id == from_feed.child_content('id')
49
+ end
59
50
 
51
+ return "Couldn't find id #{id} in feed #{feed_uri}"
52
+ end
53
+
54
+ def delete_entry(entry, name = nil)
55
+ link = entry.link('edit', self)
56
+ unless link
57
+ reporter.error(self, "Can't delete entry without edit link")
58
+ return false
59
+ end
60
+ deleter = Deleter.new(link, @authent)
61
+ worked = deleter.delete
62
+
63
+ reporter.save_dialog(name, deleter) if name
64
+ if worked
65
+ reporter.success(self, "Entry deletion reported success.", name)
66
+ else
67
+ reporter.error(self, "Couldn't delete the entry: " + deleter.last_error, name)
68
+ end
69
+ return worked
70
+ end
71
+
72
+ def method_missing(name, *args)
73
+ if (name == :enabled?)
74
+ new_method = self.class.send(:define_method, 'enabled?') do
75
+ return true
76
+ end
77
+ elsif (name == :deterministic?)
78
+ new_method = self.class.send(:define_method, 'deterministic?') do
79
+ return false
80
+ end
81
+ elsif (name == :manifest)
82
+ new_method = self.class.send(:define_method, 'manifest') do
83
+ return []
84
+ end
85
+ else
86
+ super
87
+ end
88
+ new_method.call(args) if new_method
89
+ end
90
+
60
91
  end
61
-
92
+ Dir[File.dirname(__FILE__) + '/validators/*.rb'].each { |l| require l }
62
93
  end
63
- end
64
-
65
-
@@ -0,0 +1,40 @@
1
+ module Ape
2
+ module ValidatorDsl
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def enabled
9
+ define_method('enabled?') do
10
+ return true
11
+ end
12
+ end
13
+
14
+ def disabled
15
+ define_method('enabled?') do
16
+ return false
17
+ end
18
+ end
19
+
20
+ def deterministic
21
+ define_method('deterministic?') do
22
+ return true
23
+ end
24
+ end
25
+
26
+ def nondeterministic
27
+ define_method('deterministic?') do
28
+ return false
29
+ end
30
+ end
31
+
32
+ def requires_presence_of(*args)
33
+ define_method('manifest') do
34
+ return args
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,226 @@
1
+ module Ape
2
+ class EntryPostsValidator < Validator
3
+ disabled
4
+ requires_presence_of :entry_collection
5
+
6
+ def validate(opts = {})
7
+ entry_collection = opts[:entry_collection]
8
+ reporter.info(self, "Will use collection '#{entry_collection.title}' for entry creation.")
9
+
10
+ collection_uri = entry_collection.href
11
+ entries = Feed.read(collection_uri, 'Entry collection', reporter)
12
+
13
+ # * List the current entries, remember which IDs we've seen
14
+ reporter.info(self, "TESTING: Entry-posting basics.")
15
+ ids = []
16
+ unless entries.empty?
17
+ reporter.start_list(self, "Now in the Entries feed")
18
+ entries.each do |entry|
19
+ reporter.list_item(entry.summarize)
20
+ ids << entry.child_content('id')
21
+ end
22
+ end
23
+
24
+ # Setting up to post a new entry
25
+ poster = Poster.new(collection_uri, @authent)
26
+ if poster.last_error
27
+ reporter.error(self, "Unacceptable URI for '#{entry_collection.title}' collection: " +
28
+ poster.last_error)
29
+ return
30
+ end
31
+
32
+ my_entry = Entry.new(Samples.basic_entry)
33
+
34
+ # ask it to use this in the URI
35
+ slug_num = rand(100000)
36
+ slug = "ape-#{slug_num}"
37
+ slug_re = %r{ape.?#{slug_num}}
38
+ poster.set_header('Slug', slug)
39
+
40
+ # add some categories to the entry, and remember which
41
+ @cats = Categories.add_cats(my_entry, entry_collection, @authent, reporter)
42
+
43
+ # * OK, post it
44
+ worked = poster.post(Names::AtomEntryMediaType, my_entry.to_s)
45
+ name = 'Posting new entry'
46
+ reporter.save_dialog(name, poster)
47
+ if !worked
48
+ reporter.error(self, "Can't POST new entry: #{poster.last_error}", name)
49
+ return
50
+ end
51
+
52
+ location = poster.header('Location')
53
+ unless location
54
+ reporter.error(self, "No Location header upon POST creation", name)
55
+ return
56
+ end
57
+ reporter.success(self, "Posting of new entry to the Entries collection " +
58
+ "reported success, Location: #{location}", name)
59
+
60
+ reporter.info(self, "Examining the new entry as returned in the POST response")
61
+ check_new_entry(my_entry, poster.entry, "Returned entry") if poster.entry
62
+
63
+ # * See if the Location uri can be retrieved, and check its consistency
64
+ name = "Retrieval of newly created entry"
65
+ new_entry = check_resource(location, name, Names::AtomMediaType)
66
+ return unless new_entry
67
+
68
+ # Grab its etag
69
+ etag = new_entry.header 'etag'
70
+
71
+ reporter.info(self, "Examining the new entry as retrieved using Location header in POST response:")
72
+
73
+ begin
74
+ new_entry = Entry.new(new_entry.body, location)
75
+ rescue REXML::ParseException
76
+ prob = $!.to_s.gsub(/\n/, '<br/>')
77
+ reporter.error(self, "New entry is not well-formed: #{prob}")
78
+ return
79
+ end
80
+
81
+ # * See if the slug was used
82
+ slug_used = false
83
+ new_entry.alt_links.each do |a|
84
+ href = a.attributes['href']
85
+ if href && href.index(slug_re)
86
+ slug_used = true
87
+ end
88
+ end
89
+ if slug_used
90
+ reporter.success(self, "Client-provided slug '#{slug}' was used in server-generated URI.")
91
+ else
92
+ reporter.warning(self, "Client-provided slug '#{slug}' not used in server-generated URI.")
93
+ end
94
+
95
+ check_new_entry(my_entry, new_entry, "Retrieved entry")
96
+
97
+ entry_id = new_entry.child_content('id')
98
+
99
+ # * fetch the feed again and check that version
100
+ from_feed = find_entry(collection_uri, "entry collection", entry_id)
101
+ if from_feed.class == String
102
+ reporter.success(self, "About to check #{collection_uri}")
103
+ Feed.read(collection_uri, "Can't find entry in collection", reporter)
104
+ reporter.error(self, "New entry didn't show up in the collections feed.")
105
+ return
106
+ end
107
+
108
+ reporter.info(self, "Examining the new entry as it appears in the collection feed:")
109
+
110
+ # * Check the entry from the feed
111
+ check_new_entry(my_entry, from_feed, "Entry from collection feed")
112
+
113
+ edit_uri = new_entry.link('edit', self)
114
+ if !edit_uri
115
+ reporter.error(self, "Entry from Location header has no edit link.")
116
+ return
117
+ end
118
+
119
+ # * Update the entry, see if the update took
120
+ name = 'In-place update with put'
121
+ putter = Putter.new(edit_uri, @authent)
122
+
123
+ # Conditional PUT if an etag
124
+ putter.set_header('If-Match', etag) if etag
125
+
126
+ new_title = "Let’s all do the Ape!"
127
+ new_text = Samples.retitled_entry(new_title, entry_id)
128
+ response = putter.put(Names::AtomEntryMediaType, new_text)
129
+ reporter.save_dialog(name, putter)
130
+
131
+ if response
132
+ reporter.success(self, "Update of new entry reported success.", name)
133
+ from_feed = find_entry(collection_uri, "entry collection", entry_id)
134
+ if from_feed.class == String
135
+ check_resource(collection_uri, "Check collection after lost update")
136
+ reporter.error(self, "Updated entry ID #{entry_id} not found in entries collection.")
137
+ return
138
+ end
139
+ if from_feed.child_content('title') == new_title
140
+ reporter.success(self, "Title of new entry successfully updated.")
141
+ else
142
+ reporter.warning(self, "After PUT update of title, Expected " +
143
+ "'#{new_title}', but saw '#{from_feed.child_content('title')}'")
144
+ end
145
+ else
146
+ reporter.warning(self,"Can't update new entry with PUT: #{putter.last_error}", name)
147
+ end
148
+
149
+ # the edit-uri might have changed
150
+ return unless delete_entry(from_feed, 'New Entry deletion')
151
+
152
+ # See if it's gone from the feed
153
+ still_there = find_entry(collection_uri, "entry collection", entry_id)
154
+ if still_there.class != String
155
+ reporter.error(self, "Entry is still in collection post-deletion.")
156
+ else
157
+ reporter.success(self, "Entry not found in feed after deletion.")
158
+ end
159
+
160
+ end
161
+
162
+ def check_new_entry(as_posted, new_entry, desc)
163
+
164
+ if compare_entries(as_posted, new_entry, "entry as posted", desc)
165
+ reporter.success(self, "#{desc} is consistent with posted entry.")
166
+ end
167
+
168
+ # * See if the categories we sent made it in
169
+ cat_probs = false
170
+ @cats.each do |cat|
171
+ if !new_entry.has_cat(cat)
172
+ cat_probs = true
173
+ reporter.warning(self, "Provided category not in #{desc}: #{cat}")
174
+ end
175
+ end
176
+ reporter.success(self, "Provided categories included in #{desc}.") unless cat_probs
177
+
178
+ # * See if the dc:subject survived
179
+ dc_subject = new_entry.child_content(Samples.foreign_child, Samples.foreign_namespace)
180
+ if dc_subject
181
+ if dc_subject == Samples.foreign_child_content
182
+ reporter.success(self, "Server preserved foreign markup in #{desc}.")
183
+ else
184
+ reporter.warning(self, "Server altered content of foreign markup in #{desc}.")
185
+ end
186
+ else
187
+ reporter.warning(self, "Server discarded foreign markup in #{desc}.")
188
+ end
189
+ end
190
+
191
+ def compare_entries(e1, e2, e1Name, e2Name)
192
+ problems = 0
193
+ [ 'title', 'summary', 'content' ].each do |field|
194
+ problems += 1 if compare1(e1, e2, e1Name, e2Name, field)
195
+ end
196
+ return problems == 0
197
+ end
198
+
199
+ def compare1(e1, e2, e1Name, e2Name, field)
200
+ c1 = e1.child_content(field)
201
+ c2 = e2.child_content(field)
202
+ if c1 != c2
203
+ problem = true
204
+ if c1 == nil
205
+ reporter.warning(self, "'#{field}' absent in #{e1Name}.")
206
+ elsif c2 == nil
207
+ reporter.warning(self, "'#{field}' absent in #{e2Name}.")
208
+ else
209
+ t1 = e1.child_type(field)
210
+ t2 = e2.child_type(field)
211
+ if t1 != t2
212
+ reporter.warning(self, "'#{field}' has type='#{t1}' " +
213
+ "in #{e1Name}, type='#{t2}' in #{e2Name}.")
214
+ else
215
+ c1 = Escaper.escape(c1)
216
+ c2 = Escaper.escape(c2)
217
+ reporter.warning(self, "'#{field}' in #{e1Name} [#{c1}] " +
218
+ "differs from that in #{e2Name} [#{c2}].")
219
+ end
220
+ end
221
+ end
222
+ return problem
223
+ end
224
+
225
+ end
226
+ end
@@ -0,0 +1,78 @@
1
+ module Ape
2
+ class MediaLinkageValidator < Validator
3
+ disabled
4
+ requires_presence_of :media_collection
5
+
6
+ def validate(opts = {})
7
+ reporter.info(self, "TESTING: Media collection re-ordering after PUT.")
8
+ coll = opts[:media_collection]
9
+
10
+ # We'll post three mini entries to the collection
11
+ data = Samples.picture
12
+ poster = Poster.new(coll.href, @authent)
13
+ ['One', 'Two', 'Three'].each do |num|
14
+ slug = "Picture #{num}"
15
+ poster.set_header('Slug', slug)
16
+ name = "Posting pic #{num}"
17
+ worked = poster.post('image/jpeg', data)
18
+ reporter.save_dialog(name, poster)
19
+ if !worked
20
+ reporter.error(self, "Can't POST Picture #{num}: #{poster.last_error}", name)
21
+ return
22
+ end
23
+ sleep 2
24
+ end
25
+
26
+ # grab the collection to gather the MLE ids
27
+ entries = Feed.read(coll.href, 'Pictures from multi-post', reporter)
28
+ if entries.size < 3
29
+ reporter.error(self, "Pictures apparently not in collection")
30
+ return
31
+ end
32
+
33
+ ids = entries.map { |e| e.child_content('id)') }
34
+
35
+ # let's update one of them; have to fetch it first to get the ETag
36
+ two_media = entries[1].link('edit-media')
37
+ if !two_media
38
+ reporter.error(self, "Second entry from feed doesn't have an 'edit-media' link.")
39
+ return
40
+ end
41
+ two_resp = check_resource(two_media, 'Fetch image to get ETag', 'image/jpeg')
42
+ unless two_resp
43
+ reporter.error(self, "Can't fetch image to get ETag")
44
+ return
45
+ end
46
+ etag = two_resp.header 'etag'
47
+
48
+ putter = Putter.new(two_media, @authent)
49
+ putter.set_header('If-Match', etag)
50
+
51
+ name = 'Updating one of three pix with PUT'
52
+ if putter.put('image/jpeg', data)
53
+ reporter.success(self, "Update one of newly posted pictures went OK.")
54
+ else
55
+ reporter.save_dialog(name, putter)
56
+ reporter.error(self, "Can't update picture at #{two_media}", name)
57
+ return
58
+ end
59
+
60
+ # now the order should have changed
61
+ wanted = [ ids[2], ids[0], ids[1] ]
62
+ entries = Feed.read(coll.href, 'MLEs post-update', reporter)
63
+ entries.each do |from_feed|
64
+ want = wanted.pop
65
+ unless from_feed.child_content('id').eql?(want)
66
+ reporter.error(self, "Updating bits failed to re-order link entries in media collection.")
67
+ return
68
+ end
69
+
70
+ # next to godliness
71
+ delete_entry(from_feed)
72
+
73
+ break if wanted.empty?
74
+ end
75
+ reporter.success(self, "Entries correctly ordered after update of multi-post.")
76
+ end
77
+ end
78
+ end