ape 1.0.0 → 1.5.0

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