ro-bundle 0.1.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 (47) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +11 -0
  3. data/.ruby-env +1 -0
  4. data/.ruby-gemset +2 -0
  5. data/.ruby-version +2 -0
  6. data/.travis.yml +11 -0
  7. data/Changes.rdoc +129 -0
  8. data/Gemfile +11 -0
  9. data/Licence.rdoc +29 -0
  10. data/Rakefile +29 -0
  11. data/ReadMe.rdoc +57 -0
  12. data/bin/dir2ro +48 -0
  13. data/bin/ro-bundle-info +45 -0
  14. data/bin/verify-ro-bundle +27 -0
  15. data/bin/zip2ro +57 -0
  16. data/lib/ro-bundle.rb +45 -0
  17. data/lib/ro-bundle/exceptions.rb +30 -0
  18. data/lib/ro-bundle/file.rb +323 -0
  19. data/lib/ro-bundle/ro/agent.rb +73 -0
  20. data/lib/ro-bundle/ro/aggregate.rb +107 -0
  21. data/lib/ro-bundle/ro/annotation.rb +89 -0
  22. data/lib/ro-bundle/ro/directory.rb +120 -0
  23. data/lib/ro-bundle/ro/manifest.rb +338 -0
  24. data/lib/ro-bundle/ro/provenance.rb +153 -0
  25. data/lib/ro-bundle/util.rb +57 -0
  26. data/lib/ro-bundle/version.rb +13 -0
  27. data/ro-bundle.gemspec +43 -0
  28. data/test/data/HelloAnyone.robundle +0 -0
  29. data/test/data/empty-manifest.json +1 -0
  30. data/test/data/example3-manifest.json +40 -0
  31. data/test/data/invalid-manifest.json +5 -0
  32. data/test/data/invalid-manifest.robundle +0 -0
  33. data/test/helpers/fake_manifest.rb +23 -0
  34. data/test/helpers/fake_provenance.rb +32 -0
  35. data/test/helpers/list_tests.rb +22 -0
  36. data/test/tc_add_annotation.rb +571 -0
  37. data/test/tc_agent.rb +63 -0
  38. data/test/tc_aggregate.rb +116 -0
  39. data/test/tc_annotation.rb +84 -0
  40. data/test/tc_create.rb +170 -0
  41. data/test/tc_manifest.rb +221 -0
  42. data/test/tc_provenance.rb +121 -0
  43. data/test/tc_read.rb +66 -0
  44. data/test/tc_remove.rb +140 -0
  45. data/test/tc_util.rb +64 -0
  46. data/test/ts_ro_bundle.rb +28 -0
  47. metadata +217 -0
data/lib/ro-bundle.rb ADDED
@@ -0,0 +1,45 @@
1
+ #------------------------------------------------------------------------------
2
+ # Copyright (c) 2014 The University of Manchester, UK.
3
+ #
4
+ # BSD Licenced. See LICENCE.rdoc for details.
5
+ #
6
+ # Author: Robert Haines
7
+ #------------------------------------------------------------------------------
8
+
9
+ require "bundler/setup"
10
+ require "forwardable"
11
+ require "json"
12
+ require "time"
13
+ require "ucf"
14
+ require "uri"
15
+ require "uuid"
16
+
17
+ require "ro-bundle/version"
18
+ require "ro-bundle/util"
19
+ require "ro-bundle/exceptions"
20
+ require "ro-bundle/ro/agent"
21
+ require "ro-bundle/ro/provenance"
22
+ require "ro-bundle/ro/annotation"
23
+ require "ro-bundle/ro/aggregate"
24
+ require "ro-bundle/ro/manifest"
25
+ require "ro-bundle/ro/directory"
26
+ require "ro-bundle/file"
27
+
28
+ # This is a ruby library to read and write Research Object Bundle files in PK
29
+ # Zip format. See the ROBundle::File class for more information.
30
+ #
31
+ # See
32
+ # {the RO Bundle specification}[http://wf4ever.github.io/ro/bundle/]
33
+ # for more details.
34
+ #
35
+ # Most of this library's API is provided by two underlying gems. Please
36
+ # consult their documentation in addition to this:
37
+ #
38
+ # * {zip-container gem}[https://rubygems.org/gems/zip-container]
39
+ # {documentation}[http://mygrid.github.io/ruby-zip-container/]
40
+ # * {ucf gem}[https://rubygems.org/gems/ucf]
41
+ # {documentation}[http://mygrid.github.io/ruby-ucf]
42
+ #
43
+ # There are code examples available with the source code of this library.
44
+ module ROBundle
45
+ end
@@ -0,0 +1,30 @@
1
+ #------------------------------------------------------------------------------
2
+ # Copyright (c) 2014 The University of Manchester, UK.
3
+ #
4
+ # BSD Licenced. See LICENCE.rdoc for details.
5
+ #
6
+ # Author: Robert Haines
7
+ #------------------------------------------------------------------------------
8
+
9
+ #
10
+ module ROBundle
11
+
12
+ # The base of all exceptions raised by this library.
13
+ class ROError < RuntimeError
14
+ end
15
+
16
+ # This exception is raised when an invalid aggregate is detected.
17
+ class InvalidAggregateError < ROError
18
+
19
+ # :call-seq:
20
+ # new(name)
21
+ #
22
+ # Create a new InvalidAggregateError with the invalid object (file or
23
+ # URI) supplied.
24
+ def initialize(object)
25
+ super("'#{object}' is not an absolute filename or a URI.")
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,323 @@
1
+ #------------------------------------------------------------------------------
2
+ # Copyright (c) 2014 The University of Manchester, UK.
3
+ #
4
+ # BSD Licenced. See LICENCE.rdoc for details.
5
+ #
6
+ # Author: Robert Haines
7
+ #------------------------------------------------------------------------------
8
+
9
+ #
10
+ module ROBundle
11
+
12
+ # This class represents a Research Object Bundle file. See the
13
+ # {RO Bundle specification}[http://wf4ever.github.io/ro/bundle/]
14
+ # for more details.
15
+ #
16
+ # Many of the methods that this class provides are actually implemented in
17
+ # the Manifest class, so please see its documentation for details.
18
+ class File < UCF::File
19
+
20
+ extend Forwardable
21
+ def_delegators :@manifest, :add_author, :aggregates, :annotations,
22
+ :authored_by, :authored_on, :authored_on=, :created_by, :created_by=,
23
+ :created_on, :created_on=, :history, :id, :id=, :remove_annotation,
24
+ :remove_author
25
+
26
+ private_class_method :new
27
+
28
+ # :stopdoc:
29
+ MIMETYPE = "application/vnd.wf4ever.robundle+zip"
30
+
31
+ def initialize(filename)
32
+ super(filename)
33
+
34
+ # Initialize the managed entries and register the .ro directory.
35
+ @manifest = Manifest.new
36
+ @ro_dir = RODir.new(@manifest)
37
+ initialize_managed_entries(@ro_dir)
38
+
39
+ # Create the .ro directory if it does not already exist.
40
+ if find_entry(@ro_dir.full_name).nil?
41
+ mkdir(@ro_dir.full_name)
42
+ commit
43
+ end
44
+ end
45
+ # :startdoc:
46
+
47
+ # :call-seq:
48
+ # create(filename) -> File
49
+ # create(filename, mimetype) -> File
50
+ # create(filename) {|file| ...}
51
+ # create(filename, mimetype) {|file| ...}
52
+ #
53
+ # Create a new RO Bundle file on disk and open it for editing. A custom
54
+ # mimetype for the bundle may be specified but is unnecessary if the
55
+ # default, "application/vnd.wf4ever.robundle+zip", will be used.
56
+ #
57
+ # Please see the
58
+ # {UCF documentation}[http://mygrid.github.io/ruby-ucf/]
59
+ # for much more information and a list of all the other methods available
60
+ # in this class. RDoc does not list inherited methods, unfortunately.
61
+ def self.create(filename, mimetype = MIMETYPE, &block)
62
+ super(filename, mimetype, &block)
63
+ end
64
+
65
+ # :call-seq:
66
+ # add(entry, src_path, options = {}, &continue_on_exists_proc) -> Aggregate or nil
67
+ #
68
+ # Convenience method for adding the contents of a file to the bundle
69
+ # file. If asked to add a file with a reserved name, such as the special
70
+ # mimetype header file or .ro/manifest.json, this method will raise a
71
+ # ReservedNameClashError.
72
+ #
73
+ # This method automatically adds new entries to the list of bundle
74
+ # aggregates unless the <tt>:aggregate</tt> option is set to false.
75
+ #
76
+ # If the added entry is aggregated then the Aggregate object is returned,
77
+ # otherwise +nil+ is returned.
78
+ #
79
+ # See the rubyzip documentation for details of the
80
+ # +continue_on_exists_proc+ parameter.
81
+ def add(entry, src_path, options = {}, &continue_on_exists_proc)
82
+ super(entry, src_path, &continue_on_exists_proc)
83
+
84
+ options = { :aggregate => true }.merge(options)
85
+
86
+ if options[:aggregate]
87
+ @manifest.add_aggregate(entry)
88
+ end
89
+ end
90
+
91
+ # :call-seq:
92
+ # add_aggregate(uri) -> Aggregate
93
+ # add_aggregate(entry) -> Aggregate
94
+ # add_aggregate(entry, src_path, &continue_on_exists_proc) -> Aggregate
95
+ #
96
+ # The first form of this method adds a URI as an aggregate of the bundle.
97
+ #
98
+ # The second form adds an already existing entry in the bundle to the list
99
+ # of aggregates. <tt>Errno:ENOENT</tt> is raised if the entry does not
100
+ # exist.
101
+ #
102
+ # The third form is equivalent to File#add called without any options.
103
+ #
104
+ # In all cases the Aggregate object added to the Research Object is
105
+ # returned.
106
+ def add_aggregate(entry, src_path = nil, &continue_on_exists_proc)
107
+ if src_path.nil?
108
+ @manifest.add_aggregate(entry)
109
+ else
110
+ add(entry, src_path, &continue_on_exists_proc)
111
+ end
112
+ end
113
+
114
+ # :call-seq:
115
+ # add_annotation(annotation_object) -> Annotation
116
+ # add_annotation(aggregate, content, options = {}) -> Annotation
117
+ # add_annotation(aggregate, file, options = {}) -> Annotation
118
+ # add_annotation(aggregate, uri, options = {}) -> Annotation
119
+ # add_annotation(uri, content, options = {}) -> Annotation
120
+ # add_annotation(uri, file, options = {}) -> Annotation
121
+ # add_annotation(uri, uri, options = {}) -> Annotation
122
+ # add_annotation(annotation, content, options = {}) -> Annotation
123
+ # add_annotation(annotation, file, options = {}) -> Annotation
124
+ # add_annotation(annotation, uri, options = {}) -> Annotation
125
+ #
126
+ # This method has two forms.
127
+ #
128
+ # The first form registers an already initialized Annotation object in
129
+ # this Research Object.
130
+ #
131
+ # The second form creates a new Annotation object for the specified target
132
+ # with the specified (or empty content) and registers it in this Research
133
+ # Object.
134
+ #
135
+ # In both cases <tt>Errno:ENOENT</tt> is raised if the target of the
136
+ # annotation is not an annotatable resource.
137
+ #
138
+ # The Annotation object added to the Research Object is returned.
139
+ def add_annotation(target, body = nil, options = {})
140
+ options = { :aggregate => false }.merge(options)
141
+
142
+ if target.is_a?(Annotation) || annotatable?(target)
143
+ if body.nil? || aggregate?(body)
144
+ content = body
145
+ elsif Util.is_absolute_uri?(body)
146
+ content = body
147
+ @manifest.add_aggregate(body) if options[:aggregate]
148
+ else
149
+ content = @ro_dir.write_annotation_data(body, options)
150
+ end
151
+
152
+ @manifest.add_annotation(target, content)
153
+ else
154
+ raise Errno::ENOENT,
155
+ "'#{target}' is not a member of this Research Object or a URI."
156
+ end
157
+ end
158
+
159
+ # :call-seq:
160
+ # add_history(entry)
161
+ # add_history(entry, src_path, &continue_on_exists_proc)
162
+ #
163
+ # The first form of this method adds an already existing entry in the
164
+ # bundle to the history list in the manifest. <tt>Errno:ENOENT</tt> is
165
+ # raised if the entry does not exist.
166
+ #
167
+ # The second form adds the entry before adding it to the history list. The
168
+ # entry is not aggregated.
169
+ def add_history(entry, src_path = nil, &continue_on_exists_proc)
170
+ unless src_path.nil?
171
+ add(entry, src_path, :aggregate => false, &continue_on_exists_proc)
172
+ end
173
+
174
+ @manifest.add_history(entry)
175
+ end
176
+
177
+ # :call-seq:
178
+ # aggregate?(uri) -> true or false
179
+ # aggregate?(entry) -> true or false
180
+ #
181
+ # Is the supplied URI or entry aggregated in this Research Object?
182
+ def aggregate?(entry)
183
+ return true if entry == @manifest.id
184
+
185
+ if Util.is_absolute_uri?(entry)
186
+ entry = entry.to_s
187
+ else
188
+ entry = entry_name(entry)
189
+ end
190
+
191
+ aggregates.each do |agg|
192
+ return true if agg.uri == entry ||
193
+ agg.file == entry ||
194
+ agg.file_entry == entry
195
+ end
196
+
197
+ false
198
+ end
199
+
200
+ # :call-seq:
201
+ # annotatable?(target) -> true or false
202
+ #
203
+ # Is the supplied target an annotatable resource? An annotatable resource
204
+ # is either an absolute URI (which may or may not be aggregated in the
205
+ # RO), an aggregated resource or another registered annotation.
206
+ def annotatable?(target)
207
+ Util.is_absolute_uri?(target) || annotation?(target) || aggregate?(target)
208
+ end
209
+
210
+ # :call-seq:
211
+ # annotation?(id) -> true or false
212
+ # annotation?(annotation) -> true or false
213
+ #
214
+ # Is the supplied id or annotation registered in this Research Object?
215
+ def annotation?(id)
216
+ id = id.annotation_id if id.instance_of?(Annotation)
217
+
218
+ annotations.each do |ann|
219
+ return true if ann.annotation_id == id
220
+ end
221
+
222
+ false
223
+ end
224
+
225
+ # :call-seq:
226
+ # commit -> true or false
227
+ # close -> true or false
228
+ #
229
+ # Commits changes that have been made since the previous commit to the
230
+ # RO Bundle file. Returns +true+ if anything was actually done, +false+
231
+ # otherwise.
232
+ def commit
233
+ if @manifest.edited?
234
+ name = @manifest.full_name
235
+ remove(name, true) unless find_entry(name).nil?
236
+
237
+ file.open(name, "w") do |m|
238
+ m.puts JSON.pretty_generate(@manifest)
239
+ end
240
+
241
+ @ro_dir.cleanup_annotation_data
242
+ end
243
+
244
+ super
245
+ end
246
+
247
+ alias :close :commit
248
+
249
+ # :call-seq:
250
+ # commit_required? -> true or false
251
+ #
252
+ # Returns +true+ if any changes have been made to this RO Bundle file
253
+ # since the last commit, +false+ otherwise.
254
+ def commit_required?
255
+ super || @manifest.edited?
256
+ end
257
+
258
+ # :call-seq:
259
+ # find_entry(entry_name, options = {}) -> Zip::Entry or nil
260
+ #
261
+ # Searches for the entry with the specified name. Returns +nil+ if no
262
+ # entry is found or if the specified entry is hidden for normal use. You
263
+ # can specify <tt>:include_hidden => true</tt> to include hidden entries
264
+ # in the search.
265
+ def find_entry(entry_name, options = {})
266
+ return if Util.is_absolute_uri?(entry_name)
267
+
268
+ super(entry_name, options)
269
+ end
270
+
271
+ # :call-seq:
272
+ # remove(entry)
273
+ #
274
+ # Removes the specified entry from the Research Object bundle. If asked to
275
+ # remove any reserved files such as the special mimetype header file this
276
+ # method will do nothing.
277
+ #
278
+ # If the entry being removed is aggregated in this RO then the aggregation
279
+ # is removed. All annotations that refer to the removed entry are also
280
+ # removed.
281
+ def remove(entry, preserve_manifest = false)
282
+ super(entry)
283
+
284
+ # The preserve manifest flag is STRICTLY for internal use only.
285
+ unless preserve_manifest
286
+ name = entry_name(entry)
287
+ @manifest.remove_aggregate("/#{name}")
288
+ remove_annotation("/#{name}")
289
+ end
290
+ end
291
+
292
+ # :call-seq:
293
+ # remove_aggregate(entry)
294
+ # remove_aggregate(uri)
295
+ # remove_aggregate(Aggregate)
296
+ #
297
+ # Remove (unregister) an aggregate from this Research Object. If it is a
298
+ # file then the file is no longer aggregated, and it is deleted from the
299
+ # bundle by this method unless the option <tt>:keep_file => true</tt> is
300
+ # supplied.
301
+ #
302
+ # Any annotations with the removed aggregate as their target are also
303
+ # removed from the RO.
304
+ def remove_aggregate(object, options = {})
305
+ options = { :keep_file => false }.merge(options)
306
+ file = nil
307
+
308
+ if object.is_a?(Aggregate)
309
+ file = object.file_entry
310
+ elsif !Util.is_absolute_uri?(object)
311
+ object = entry_name(object)
312
+ file = Util.strip_leading_slash(object)
313
+ end
314
+
315
+ if !file.nil? && !options[:keep_file]
316
+ remove(file, true)
317
+ end
318
+
319
+ @manifest.remove_aggregate(object)
320
+ end
321
+
322
+ end
323
+ end
@@ -0,0 +1,73 @@
1
+ #------------------------------------------------------------------------------
2
+ # Copyright (c) 2014 The University of Manchester, UK.
3
+ #
4
+ # BSD Licenced. See LICENCE.rdoc for details.
5
+ #
6
+ # Author: Robert Haines
7
+ #------------------------------------------------------------------------------
8
+
9
+ #
10
+ module ROBundle
11
+
12
+ # A class to represent an agent in a Research Object. An agent can be, for
13
+ # example, a person or software.
14
+ class Agent
15
+
16
+ # :call-seq:
17
+ # new(name, uri = nil, orcid = nil)
18
+ #
19
+ # Create a new Agent with the supplied details. If +uri+ or +orcid+ are
20
+ # not absolute URIs then they are set to +nil+.
21
+ def initialize(first, uri = nil, orcid = nil)
22
+ if first.instance_of?(Hash)
23
+ name = first[:name]
24
+ uri = first[:uri]
25
+ orcid = first[:orcid]
26
+ else
27
+ name = first
28
+ end
29
+
30
+ @structure = {
31
+ :name => name,
32
+ :uri => Util.is_absolute_uri?(uri) ? uri.to_s : nil,
33
+ :orcid => Util.is_absolute_uri?(orcid) ? orcid.to_s : nil
34
+ }
35
+ end
36
+
37
+ # :call-seq:
38
+ # name -> string
39
+ #
40
+ # The name of this agent.
41
+ def name
42
+ @structure[:name]
43
+ end
44
+
45
+ # :call-seq:
46
+ # uri -> URI
47
+ #
48
+ # A URI identifying the agent. This should, if it exists, be a
49
+ # {WebID}[http://www.w3.org/wiki/WebID].
50
+ def uri
51
+ @structure[:uri]
52
+ end
53
+
54
+ # :call-seq:
55
+ # orcid -> URI
56
+ #
57
+ # An ORCID identifier URI for this agent.
58
+ def orcid
59
+ @structure[:orcid]
60
+ end
61
+
62
+ # :call-seq:
63
+ # to_json(options = nil) -> String
64
+ #
65
+ # Write this Agent out as a json string. Takes the same options as
66
+ # JSON#generate.
67
+ def to_json(*a)
68
+ Util.clean_json(@structure).to_json(*a)
69
+ end
70
+
71
+ end
72
+
73
+ end