ro-bundle 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +11 -0
- data/.ruby-env +1 -0
- data/.ruby-gemset +2 -0
- data/.ruby-version +2 -0
- data/.travis.yml +11 -0
- data/Changes.rdoc +129 -0
- data/Gemfile +11 -0
- data/Licence.rdoc +29 -0
- data/Rakefile +29 -0
- data/ReadMe.rdoc +57 -0
- data/bin/dir2ro +48 -0
- data/bin/ro-bundle-info +45 -0
- data/bin/verify-ro-bundle +27 -0
- data/bin/zip2ro +57 -0
- data/lib/ro-bundle.rb +45 -0
- data/lib/ro-bundle/exceptions.rb +30 -0
- data/lib/ro-bundle/file.rb +323 -0
- data/lib/ro-bundle/ro/agent.rb +73 -0
- data/lib/ro-bundle/ro/aggregate.rb +107 -0
- data/lib/ro-bundle/ro/annotation.rb +89 -0
- data/lib/ro-bundle/ro/directory.rb +120 -0
- data/lib/ro-bundle/ro/manifest.rb +338 -0
- data/lib/ro-bundle/ro/provenance.rb +153 -0
- data/lib/ro-bundle/util.rb +57 -0
- data/lib/ro-bundle/version.rb +13 -0
- data/ro-bundle.gemspec +43 -0
- data/test/data/HelloAnyone.robundle +0 -0
- data/test/data/empty-manifest.json +1 -0
- data/test/data/example3-manifest.json +40 -0
- data/test/data/invalid-manifest.json +5 -0
- data/test/data/invalid-manifest.robundle +0 -0
- data/test/helpers/fake_manifest.rb +23 -0
- data/test/helpers/fake_provenance.rb +32 -0
- data/test/helpers/list_tests.rb +22 -0
- data/test/tc_add_annotation.rb +571 -0
- data/test/tc_agent.rb +63 -0
- data/test/tc_aggregate.rb +116 -0
- data/test/tc_annotation.rb +84 -0
- data/test/tc_create.rb +170 -0
- data/test/tc_manifest.rb +221 -0
- data/test/tc_provenance.rb +121 -0
- data/test/tc_read.rb +66 -0
- data/test/tc_remove.rb +140 -0
- data/test/tc_util.rb +64 -0
- data/test/ts_ro_bundle.rb +28 -0
- metadata +217 -0
@@ -0,0 +1,107 @@
|
|
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 aggregated resource in a Research Object. It holds
|
13
|
+
# standard meta-data for either file or URI resources. An aggregate can only
|
14
|
+
# represent a file OR a URI resource, not both at once.
|
15
|
+
class Aggregate
|
16
|
+
include Provenance
|
17
|
+
|
18
|
+
# :call-seq:
|
19
|
+
# new(filename, mediatype = nil)
|
20
|
+
# new(URI)
|
21
|
+
#
|
22
|
+
# Create a new file or URI aggregate.
|
23
|
+
def initialize(object, second = nil)
|
24
|
+
@structure = {}
|
25
|
+
|
26
|
+
if object.instance_of?(Hash)
|
27
|
+
init_json(object)
|
28
|
+
else
|
29
|
+
init_file_or_uri(object)
|
30
|
+
|
31
|
+
if @structure[:file]
|
32
|
+
@structure[:mediatype] = second
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# :call-seq:
|
38
|
+
# file
|
39
|
+
#
|
40
|
+
# The path of this aggregate. It should start with '/'.
|
41
|
+
def file
|
42
|
+
@structure[:file]
|
43
|
+
end
|
44
|
+
|
45
|
+
# :call-seq:
|
46
|
+
# file_entry
|
47
|
+
#
|
48
|
+
# The path of this aggregate in "rubyzip" format, i.e. no leading '/'.
|
49
|
+
def file_entry
|
50
|
+
Util.strip_leading_slash(file)
|
51
|
+
end
|
52
|
+
|
53
|
+
# :call-seq:
|
54
|
+
# uri
|
55
|
+
#
|
56
|
+
# The URI of this aggregate. It should be an absolute URI.
|
57
|
+
def uri
|
58
|
+
@structure[:uri]
|
59
|
+
end
|
60
|
+
|
61
|
+
# :call-seq:
|
62
|
+
# mediatype
|
63
|
+
#
|
64
|
+
# For a file aggregate, its
|
65
|
+
# {IANA media type}[http://www.iana.org/assignments/media-types].
|
66
|
+
def mediatype
|
67
|
+
@structure[:mediatype]
|
68
|
+
end
|
69
|
+
|
70
|
+
# :call-seq:
|
71
|
+
# to_json(options = nil) -> String
|
72
|
+
#
|
73
|
+
# Write this Aggregate out as a json string. Takes the same options as
|
74
|
+
# JSON#generate.
|
75
|
+
def to_json(*a)
|
76
|
+
Util.clean_json(@structure).to_json(*a)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def structure
|
82
|
+
@structure
|
83
|
+
end
|
84
|
+
|
85
|
+
def init_json(object)
|
86
|
+
init_file_or_uri(object[:file] || object[:uri])
|
87
|
+
@structure = init_provenance_defaults(object)
|
88
|
+
|
89
|
+
if @structure[:file]
|
90
|
+
@structure[:mediatype] = object[:mediatype]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def init_file_or_uri(object)
|
95
|
+
if object.is_a?(String) && !Util.is_absolute_uri?(object)
|
96
|
+
name = object.start_with?("/") ? object : "/#{object}"
|
97
|
+
@structure[:file] = name
|
98
|
+
elsif Util.is_absolute_uri?(object)
|
99
|
+
@structure[:uri] = object.to_s
|
100
|
+
else
|
101
|
+
raise InvalidAggregateError.new(object)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,89 @@
|
|
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 Annotation in a Research Object.
|
13
|
+
class Annotation
|
14
|
+
include Provenance
|
15
|
+
|
16
|
+
# :call-seq:
|
17
|
+
# new(target, content = nil)
|
18
|
+
#
|
19
|
+
# Create a new Annotation with the specified "about" identifier. A new
|
20
|
+
# annotation ID is generated and set for the new annotation. The +content+
|
21
|
+
# parameter can be optionally used to set the file or URI that holds the
|
22
|
+
# body of the annotation.
|
23
|
+
#
|
24
|
+
# An annotation id is a UUID prefixed with "urn:uuid" as per
|
25
|
+
# {RFC4122}[http://www.ietf.org/rfc/rfc4122.txt].
|
26
|
+
def initialize(object, content = nil)
|
27
|
+
if object.instance_of?(Hash)
|
28
|
+
@structure = object
|
29
|
+
init_provenance_defaults(@structure)
|
30
|
+
else
|
31
|
+
@structure = {}
|
32
|
+
@structure[:about] = object
|
33
|
+
@structure[:annotation] = UUID.generate(:urn)
|
34
|
+
@structure[:content] = content
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# :call-seq:
|
39
|
+
# target
|
40
|
+
#
|
41
|
+
# The identifier for the annotated resource. This is considered the target
|
42
|
+
# of the annotation, that is the resource the annotation content is
|
43
|
+
# "somewhat about".
|
44
|
+
def target
|
45
|
+
@structure[:about]
|
46
|
+
end
|
47
|
+
|
48
|
+
# :call-seq:
|
49
|
+
# content
|
50
|
+
#
|
51
|
+
# The identifier for a resource that contains the body of the annotation.
|
52
|
+
def content
|
53
|
+
@structure[:content]
|
54
|
+
end
|
55
|
+
|
56
|
+
# :call-seq:
|
57
|
+
# content = new_content
|
58
|
+
#
|
59
|
+
# Set the content of this annotation.
|
60
|
+
def content=(new_content)
|
61
|
+
@structure[:content] = new_content
|
62
|
+
end
|
63
|
+
|
64
|
+
# :call-seq:
|
65
|
+
# annotation_id -> String
|
66
|
+
#
|
67
|
+
# Return the annotation id of this Annotation.
|
68
|
+
def annotation_id
|
69
|
+
@structure[:annotation]
|
70
|
+
end
|
71
|
+
|
72
|
+
# :call-seq:
|
73
|
+
# to_json(options = nil) -> String
|
74
|
+
#
|
75
|
+
# Write this Annotation out as a json string. Takes the same options as
|
76
|
+
# JSON#generate.
|
77
|
+
def to_json(*a)
|
78
|
+
Util.clean_json(@structure).to_json(*a)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def structure
|
84
|
+
@structure
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,120 @@
|
|
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 managed .ro directory entry of a Research Object.
|
13
|
+
#
|
14
|
+
# For internal use only.
|
15
|
+
class RODir < ZipContainer::ManagedDirectory
|
16
|
+
|
17
|
+
DIR_NAME = ".ro" # :nodoc:
|
18
|
+
|
19
|
+
# :call-seq:
|
20
|
+
# new(manifest)
|
21
|
+
#
|
22
|
+
# Create a new .ro managed directory entry with the specified manifest
|
23
|
+
# file object.
|
24
|
+
def initialize(manifest)
|
25
|
+
@manifest = manifest
|
26
|
+
@annotations_directory = AnnotationsDir.new
|
27
|
+
|
28
|
+
super(DIR_NAME, :required => true,
|
29
|
+
:entries => [@manifest, @annotations_directory])
|
30
|
+
end
|
31
|
+
|
32
|
+
# :stopdoc:
|
33
|
+
def cleanup_annotation_data
|
34
|
+
container.glob("#{@annotations_directory.full_name}/*",
|
35
|
+
:include_hidden => true) do |file|
|
36
|
+
|
37
|
+
found = false
|
38
|
+
@manifest.annotations.each do |ann|
|
39
|
+
content_name = normalize_content_name(ann.content)
|
40
|
+
|
41
|
+
if content_name == file.name
|
42
|
+
found = true
|
43
|
+
break
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
container.remove(file.name, true) unless found
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def write_annotation_data(source, options)
|
52
|
+
uuid = UUID.generate
|
53
|
+
|
54
|
+
if options[:aggregate]
|
55
|
+
entry = uuid
|
56
|
+
content = "/#{uuid}"
|
57
|
+
else
|
58
|
+
mk_annotations_dir
|
59
|
+
content = "#{@annotations_directory.name}/#{uuid}"
|
60
|
+
entry = "#{full_name}/#{content}"
|
61
|
+
end
|
62
|
+
|
63
|
+
if ::File.exist?(source)
|
64
|
+
container.add(entry, source, options)
|
65
|
+
else
|
66
|
+
container.file.open(entry, "w") do |annotation|
|
67
|
+
annotation.write source.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
if options[:aggregate]
|
71
|
+
@manifest.add_aggregate(entry)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
content
|
76
|
+
end
|
77
|
+
# :startdoc:
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def mk_annotations_dir
|
82
|
+
dir_name = "#{full_name}/#{@annotations_directory.name}"
|
83
|
+
if container.find_entry(dir_name, :include_hidden => true).nil?
|
84
|
+
container.mkdir dir_name
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Convert an annotation content field into something compatible with the
|
89
|
+
# rubyzip file naming convention (i.e. full paths not prefixed with /).
|
90
|
+
def normalize_content_name(name)
|
91
|
+
return if name.nil?
|
92
|
+
|
93
|
+
if name.start_with?(@annotations_directory.name)
|
94
|
+
"#{full_name}/#{name}"
|
95
|
+
elsif name.start_with?("/#{@annotations_directory.full_name}")
|
96
|
+
name.slice(1, name.length)
|
97
|
+
else
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# The managed annotations directory within the .ro directory.
|
103
|
+
#
|
104
|
+
# For internal use only.
|
105
|
+
class AnnotationsDir < ZipContainer::ManagedDirectory
|
106
|
+
|
107
|
+
DIR_NAME = "annotations" # :nodoc:
|
108
|
+
|
109
|
+
# :call-seq:
|
110
|
+
# new
|
111
|
+
#
|
112
|
+
# Create a new annotations managed directory. The directory is hidden
|
113
|
+
# under normal circumstances.
|
114
|
+
def initialize
|
115
|
+
super(DIR_NAME, :hidden => true)
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,338 @@
|
|
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 manifest.json managed file entry for a Research Object.
|
13
|
+
class Manifest < ZipContainer::ManagedFile
|
14
|
+
include Provenance
|
15
|
+
|
16
|
+
FILE_NAME = "manifest.json" # :nodoc:
|
17
|
+
DEFAULT_CONTEXT = "https://w3id.org/bundle/context" # :nodoc:
|
18
|
+
DEFAULT_ID = "/" # :nodoc:
|
19
|
+
|
20
|
+
# :call-seq:
|
21
|
+
# new
|
22
|
+
#
|
23
|
+
# Create a new managed file entry to represent the manifest.json file.
|
24
|
+
def initialize
|
25
|
+
super(FILE_NAME, :required => true)
|
26
|
+
|
27
|
+
@edited = false
|
28
|
+
end
|
29
|
+
|
30
|
+
# :call-seq:
|
31
|
+
# context -> List of context URIs
|
32
|
+
#
|
33
|
+
# Return the list of @context URIs for this Research Object manifest.
|
34
|
+
def context
|
35
|
+
structure[:@context].dup
|
36
|
+
end
|
37
|
+
|
38
|
+
# :call-seq:
|
39
|
+
# add_context
|
40
|
+
#
|
41
|
+
# Add a URI to the front of the @context list.
|
42
|
+
def add_context(uri)
|
43
|
+
@edited = true
|
44
|
+
structure[:@context].insert(0, uri.to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
# :call-seq:
|
48
|
+
# id -> String
|
49
|
+
#
|
50
|
+
# An RO identifier (usually '/') indicating the relative top-level folder
|
51
|
+
# as the identifier.
|
52
|
+
def id
|
53
|
+
structure[:id]
|
54
|
+
end
|
55
|
+
|
56
|
+
# :call-seq:
|
57
|
+
# id = new_id
|
58
|
+
#
|
59
|
+
# Set the id of this Manifest.
|
60
|
+
def id=(new_id)
|
61
|
+
@edited = true
|
62
|
+
structure[:id] = new_id
|
63
|
+
end
|
64
|
+
|
65
|
+
# :call-seq:
|
66
|
+
# history -> List of history entry names
|
67
|
+
#
|
68
|
+
# Return a list of filenames that hold provenance information for this
|
69
|
+
# Research Object.
|
70
|
+
def history
|
71
|
+
structure[:history].dup
|
72
|
+
end
|
73
|
+
|
74
|
+
# :call-seq:
|
75
|
+
# add_history(entry)
|
76
|
+
#
|
77
|
+
# Add the given entry to the history list in this manifest.
|
78
|
+
# <tt>Errno:ENOENT</tt> is raised if the entry does not exist.
|
79
|
+
def add_history(entry)
|
80
|
+
raise Errno::ENOENT if container.find_entry(entry).nil?
|
81
|
+
|
82
|
+
# Mangle the filename according to the RO Bundle specification.
|
83
|
+
name = entry_name(entry)
|
84
|
+
dir = "#{@parent.full_name}/"
|
85
|
+
name = name.start_with?(dir) ? name.sub(dir, "") : "/#{name}"
|
86
|
+
|
87
|
+
@edited = true
|
88
|
+
structure[:history] << name
|
89
|
+
end
|
90
|
+
|
91
|
+
# :call-seq:
|
92
|
+
# aggregates -> List of aggregated resources.
|
93
|
+
#
|
94
|
+
# Return a list of all the aggregated resources in this Research Object.
|
95
|
+
def aggregates
|
96
|
+
structure[:aggregates].dup
|
97
|
+
end
|
98
|
+
|
99
|
+
# :call-seq:
|
100
|
+
# add_aggregate(entry) -> Aggregate
|
101
|
+
# add_aggregate(uri) -> Aggregate
|
102
|
+
#
|
103
|
+
# Add the given entry or URI to the list of aggregates in this manifest.
|
104
|
+
# <tt>Errno:ENOENT</tt> is raised if the entry does not exist.
|
105
|
+
#
|
106
|
+
# The Aggregate object added to the Research Object is returned.
|
107
|
+
def add_aggregate(entry)
|
108
|
+
unless entry.instance_of?(Aggregate)
|
109
|
+
unless Util.is_absolute_uri?(entry)
|
110
|
+
raise Errno::ENOENT if container.find_entry(entry).nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
entry = Aggregate.new(entry)
|
114
|
+
end
|
115
|
+
|
116
|
+
@edited = true
|
117
|
+
structure[:aggregates] << entry
|
118
|
+
entry
|
119
|
+
end
|
120
|
+
|
121
|
+
# :call-seq:
|
122
|
+
# remove_aggregate(filename)
|
123
|
+
# remove_aggregate(uri)
|
124
|
+
# remove_aggregate(Aggregate)
|
125
|
+
#
|
126
|
+
# Remove (unregister) an aggregate from this Research Object. If a
|
127
|
+
# filename is supplied then the file is no longer aggregated, but it is
|
128
|
+
# not deleted from the bundle by this method.
|
129
|
+
#
|
130
|
+
# Any annotations with the removed aggregate as their target are also
|
131
|
+
# removed from the RO.
|
132
|
+
def remove_aggregate(object)
|
133
|
+
removed = nil
|
134
|
+
|
135
|
+
if object.is_a?(Aggregate)
|
136
|
+
removed = structure[:aggregates].delete(object)
|
137
|
+
|
138
|
+
unless removed.nil?
|
139
|
+
removed = removed.file.nil? ? removed.uri : removed.file
|
140
|
+
end
|
141
|
+
else
|
142
|
+
removed = remove_aggregate_by_file_or_uri(object)
|
143
|
+
end
|
144
|
+
|
145
|
+
unless removed.nil?
|
146
|
+
remove_annotation(removed)
|
147
|
+
@edited = true
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# :call-seq:
|
152
|
+
# add_annotation(annotation) -> Annotation
|
153
|
+
# add_annotation(target, content = nil) -> Annotation
|
154
|
+
#
|
155
|
+
# Add an annotation to this Research Object. An annotation can either be
|
156
|
+
# an already created annotation object, or a pair of values to build a new
|
157
|
+
# annotation object explicitly.
|
158
|
+
#
|
159
|
+
# <tt>Errno:ENOENT</tt> is raised if the target of the annotation is not
|
160
|
+
# an annotatable resource in this RO.
|
161
|
+
#
|
162
|
+
# The Annotation object added to the Research Object is returned.
|
163
|
+
def add_annotation(object, content = nil)
|
164
|
+
if object.instance_of?(Annotation)
|
165
|
+
# If the supplied Annotation object is already registered then it is
|
166
|
+
# the annotation itself we are annotating!
|
167
|
+
if container.annotation?(object)
|
168
|
+
object = Annotation.new(object.annotation_id, content)
|
169
|
+
end
|
170
|
+
else
|
171
|
+
object = Annotation.new(object, content)
|
172
|
+
end
|
173
|
+
|
174
|
+
target = object.target
|
175
|
+
unless container.annotatable?(target)
|
176
|
+
raise Errno::ENOENT,
|
177
|
+
"'#{target}' is not a member of this Research Object or a URI."
|
178
|
+
end
|
179
|
+
|
180
|
+
@edited = true
|
181
|
+
structure[:annotations] << object
|
182
|
+
object
|
183
|
+
end
|
184
|
+
|
185
|
+
# :call-seq:
|
186
|
+
# remove_annotation(Annotation)
|
187
|
+
# remove_annotation(target)
|
188
|
+
# remove_annotation(id)
|
189
|
+
#
|
190
|
+
# Remove (unregister) annotations from this Research Object and return
|
191
|
+
# them. Return +nil+ if the annotation does not exist.
|
192
|
+
#
|
193
|
+
# Any annotation content that is stored in the .ro/annotations directory
|
194
|
+
# is automatically cleaned up when the RO is closed.
|
195
|
+
def remove_annotation(object)
|
196
|
+
if object.is_a?(Annotation)
|
197
|
+
removed = [structure[:annotations].delete(object)].compact
|
198
|
+
else
|
199
|
+
removed = remove_annotation_by_field(object)
|
200
|
+
end
|
201
|
+
|
202
|
+
removed.each do |ann|
|
203
|
+
id = ann.annotation_id
|
204
|
+
remove_annotation(id) unless id.nil?
|
205
|
+
end
|
206
|
+
|
207
|
+
@edited = true unless removed.empty?
|
208
|
+
end
|
209
|
+
|
210
|
+
# :call-seq:
|
211
|
+
# annotations
|
212
|
+
#
|
213
|
+
# Return a list of all the annotations in this Research Object.
|
214
|
+
def annotations
|
215
|
+
structure[:annotations].dup
|
216
|
+
end
|
217
|
+
|
218
|
+
# :call-seq:
|
219
|
+
# edited? -> true or false
|
220
|
+
#
|
221
|
+
# Has this manifest been altered in any way?
|
222
|
+
def edited?
|
223
|
+
@edited
|
224
|
+
end
|
225
|
+
|
226
|
+
# :call-seq:
|
227
|
+
# to_json(options = nil) -> String
|
228
|
+
#
|
229
|
+
# Write this Manifest out as a json string. Takes the same options as
|
230
|
+
# JSON#generate.
|
231
|
+
def to_json(*a)
|
232
|
+
Util.clean_json(structure).to_json(*a)
|
233
|
+
end
|
234
|
+
|
235
|
+
protected
|
236
|
+
|
237
|
+
# :call-seq:
|
238
|
+
# validate -> true or false
|
239
|
+
#
|
240
|
+
# Validate the correctness of the manifest file contents.
|
241
|
+
def validate
|
242
|
+
begin
|
243
|
+
structure
|
244
|
+
rescue JSON::ParserError, ROError
|
245
|
+
return false
|
246
|
+
end
|
247
|
+
|
248
|
+
true
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
|
253
|
+
# The internal structure of this class cannot be setup at construction
|
254
|
+
# time in the initializer as there is no route to its data on disk at that
|
255
|
+
# point. Once loaded, parts of the structure are converted to local
|
256
|
+
# objects where appropriate.
|
257
|
+
def structure
|
258
|
+
return @structure if @structure
|
259
|
+
|
260
|
+
begin
|
261
|
+
struct ||= JSON.parse(contents, :symbolize_names => true)
|
262
|
+
rescue Errno::ENOENT
|
263
|
+
struct = {}
|
264
|
+
end
|
265
|
+
|
266
|
+
@structure = init_defaults(struct)
|
267
|
+
end
|
268
|
+
|
269
|
+
def init_defaults(struct)
|
270
|
+
init_default_context(struct)
|
271
|
+
init_default_id(struct)
|
272
|
+
init_provenance_defaults(struct)
|
273
|
+
struct[:history] = [*struct.fetch(:history, [])]
|
274
|
+
struct[:aggregates] = [*struct.fetch(:aggregates, [])].map do |agg|
|
275
|
+
Aggregate.new(agg)
|
276
|
+
end
|
277
|
+
struct[:annotations] = [*struct.fetch(:annotations, [])].map do |ann|
|
278
|
+
Annotation.new(ann)
|
279
|
+
end
|
280
|
+
|
281
|
+
struct
|
282
|
+
end
|
283
|
+
|
284
|
+
def init_default_context(struct)
|
285
|
+
context = struct[:@context]
|
286
|
+
if context.nil?
|
287
|
+
@edited = true
|
288
|
+
struct[:@context] = [ DEFAULT_CONTEXT ]
|
289
|
+
else
|
290
|
+
struct[:@context] = [*context]
|
291
|
+
end
|
292
|
+
|
293
|
+
struct
|
294
|
+
end
|
295
|
+
|
296
|
+
def init_default_id(struct)
|
297
|
+
id = struct[:id]
|
298
|
+
if id.nil?
|
299
|
+
@edited = true
|
300
|
+
struct[:id] = DEFAULT_ID
|
301
|
+
end
|
302
|
+
|
303
|
+
struct
|
304
|
+
end
|
305
|
+
|
306
|
+
def remove_aggregate_by_file_or_uri(object)
|
307
|
+
aggregates.each do |agg|
|
308
|
+
if Util.is_absolute_uri?(object)
|
309
|
+
return structure[:aggregates].delete(agg).uri if object == agg.uri
|
310
|
+
else
|
311
|
+
if object == agg.file || object == agg.file_entry
|
312
|
+
return structure[:aggregates].delete(agg).file
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Return nil if nothing removed.
|
318
|
+
nil
|
319
|
+
end
|
320
|
+
|
321
|
+
def remove_annotation_by_field(object)
|
322
|
+
removed = []
|
323
|
+
|
324
|
+
annotations.each do |ann|
|
325
|
+
if ann.annotation_id == object ||
|
326
|
+
ann.target == object ||
|
327
|
+
ann.content == object
|
328
|
+
|
329
|
+
removed << structure[:annotations].delete(ann)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
removed
|
334
|
+
end
|
335
|
+
|
336
|
+
end
|
337
|
+
|
338
|
+
end
|