cul-fedora-arm 0.5.1
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.
- data/.buildpath +6 -0
- data/.document +5 -0
- data/.gitignore +6 -0
- data/.loadpath +5 -0
- data/.project +17 -0
- data/LICENSE +20 -0
- data/README.rdoc +29 -0
- data/Rakefile +60 -0
- data/VERSION +1 -0
- data/cul-fedora-arm.gemspec +92 -0
- data/lib/cul-fedora-arm.rb +6 -0
- data/lib/cul/fedora/.rb +0 -0
- data/lib/cul/fedora/arm/builder.rb +230 -0
- data/lib/cul/fedora/arm/foxml_builder.rb +300 -0
- data/lib/cul/fedora/arm/tasks.rb +106 -0
- data/lib/cul/fedora/connector.rb +64 -0
- data/lib/cul/fedora/image/image.rb +353 -0
- data/test/cul_fedora_arm_builder_test.rb +372 -0
- data/test/cul_fedora_arm_tasks_test.rb +59 -0
- data/test/cul_fedora_image_test.rb +117 -0
- data/test/factories.rb +1 -0
- data/test/fixtures/case1/builder-template-noheader.txt +6 -0
- data/test/fixtures/case1/builder-template.txt +7 -0
- data/test/fixtures/case2/builder-template.txt +3 -0
- data/test/fixtures/case3/builder-template.txt +2 -0
- data/test/fixtures/case3/test001.bmp +0 -0
- data/test/fixtures/case3/test001.gif +0 -0
- data/test/fixtures/case3/test001.jpg +0 -0
- data/test/fixtures/case3/test001.png +0 -0
- data/test/fixtures/case3/test001.tiff +0 -0
- data/test/fixtures/case4/builder-template.txt +2 -0
- data/test/fixtures/case4/test-mods.xml +6 -0
- data/test/helpers/soap_inputs.rb +38 -0
- data/test/helpers/template_builder.rb +4 -0
- data/test/test_helper.rb +17 -0
- metadata +135 -0
@@ -0,0 +1,353 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'stringio'
|
5
|
+
module Cul
|
6
|
+
module Fedora
|
7
|
+
module Image
|
8
|
+
WIDTH_TEMPLATE = '<si-basic:imageWidth>%s</si-basic:imageWidth>'
|
9
|
+
LENGTH_TEMPLATE = '<si-basic:imageLength>%s</si-basic:imageLength>'
|
10
|
+
XSAMPLING_TEMPLATE = '<si-assess:xSamplingFrequency>%s</si-assess:xSamplingFrequency>'
|
11
|
+
YSAMPLING_TEMPLATE = '<si-assess:ySamplingFrequency>%s</si-assess:ySamplingFrequency>'
|
12
|
+
SAMPLINGUNIT_CM = '<si-assess:samplingFrequencyUnit rdf:resource="http://purl.oclc.org/NET/CUL/RESOURCE/STILLIMAGE/ASSESSMENT/CentimeterSampling" />'
|
13
|
+
SAMPLINGUNIT_IN = '<si-assess:samplingFrequencyUnit rdf:resource="http://purl.oclc.org/NET/CUL/RESOURCE/STILLIMAGE/ASSESSMENT/InchSampling" />'
|
14
|
+
SAMPLINGUNIT_NA = '<si-assess:samplingFrequencyUnit rdf:resource="http://purl.oclc.org/NET/CUL/RESOURCE/STILLIMAGE/ASSESSMENT/NoAbsoluteSampling" />'
|
15
|
+
UNITS = {:inch => SAMPLINGUNIT_IN, :cm => SAMPLINGUNIT_CM}
|
16
|
+
SIZE_TEMPLATE = '<dcmi:extent>%s</dcmi:extent>'
|
17
|
+
# magic bytes
|
18
|
+
# 2 bytes signatures
|
19
|
+
BITMAP = [0x42,0x4d] # "BM"
|
20
|
+
JPEG = [0xff,0xd8]
|
21
|
+
# 4 byte signatures
|
22
|
+
TIFF_BE = [0x49,0x49,0x2A,0] # "II*\x00"
|
23
|
+
TIFF_LE = [0x4d,0x4d,0,0x2A] # "MM\x00*"
|
24
|
+
GIF = [0x47,0x49,0x46,0x38] # "GIF8"
|
25
|
+
# 8 byte signatures
|
26
|
+
PNG = [0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a]
|
27
|
+
|
28
|
+
def analyze_image(file_name, debug=false)
|
29
|
+
result = {}
|
30
|
+
file = nil
|
31
|
+
file_size = 0
|
32
|
+
begin
|
33
|
+
if (file_name.index("http://") == 0)
|
34
|
+
temp_file = Tempfile.new("image-download")
|
35
|
+
#download the url content, write to tempfile
|
36
|
+
file = temp_file
|
37
|
+
else
|
38
|
+
file = File.open(file_name,'rb')
|
39
|
+
end
|
40
|
+
result[:size] = file_size = File.size(file.path)
|
41
|
+
# get properties
|
42
|
+
header = []
|
43
|
+
8.times {
|
44
|
+
header.push(file.getc())
|
45
|
+
}
|
46
|
+
case
|
47
|
+
when header[0..1].eql?(BITMAP):
|
48
|
+
file.rewind()
|
49
|
+
result.merge!(analyze_bitmap(file,debug))
|
50
|
+
when header[0..1].eql?(JPEG):
|
51
|
+
file.rewind()
|
52
|
+
result.merge!(analyze_jpeg(file,debug))
|
53
|
+
when header[0..3].eql?(TIFF_LE), header[0..3].eql?(TIFF_BE):
|
54
|
+
file.rewind()
|
55
|
+
result.merge!(analyze_tiff(file,debug))
|
56
|
+
when header[0..3].eql?(GIF):
|
57
|
+
file.rewind()
|
58
|
+
result.merge!(analyze_gif(file,debug))
|
59
|
+
when header.eql?(PNG):
|
60
|
+
file.rewind()
|
61
|
+
result.merge!(analyze_png(file,debug))
|
62
|
+
else
|
63
|
+
msg = ''
|
64
|
+
header.each {|c|
|
65
|
+
msg += c.to_s(16)
|
66
|
+
msg += ' '
|
67
|
+
}
|
68
|
+
puts "\nUnmatched header bytes: " + msg
|
69
|
+
end
|
70
|
+
ensure
|
71
|
+
file.close() if file
|
72
|
+
end
|
73
|
+
# return hash
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
def map_image_properties(att_hash)
|
78
|
+
result = []
|
79
|
+
if (att_hash.has_key?(:size))
|
80
|
+
result.push(sprintf(SIZE_TEMPLATE,att_hash[:size]))
|
81
|
+
end
|
82
|
+
if (att_hash.has_key?(:width))
|
83
|
+
result.push(sprintf(WIDTH_TEMPLATE,att_hash[:width]))
|
84
|
+
end
|
85
|
+
if (att_hash.has_key?(:length))
|
86
|
+
result.push(sprintf(LENGTH_TEMPLATE,att_hash[:length]))
|
87
|
+
end
|
88
|
+
if (att_hash.has_key?(:x_sampling))
|
89
|
+
result.push(sprintf(YSAMPLING_TEMPLATE,att_hash[:x_sampling]))
|
90
|
+
end
|
91
|
+
if (att_hash.has_key?(:y_sampling))
|
92
|
+
result.push(sprintf(YSAMPLING_TEMPLATE,att_hash[:y_sampling]))
|
93
|
+
end
|
94
|
+
if (att_hash.has_key?(:sampling_unit) and UNITS.has_key?(att_hash[:sampling_unit]))
|
95
|
+
result.push(UNITS[att_hash[:sampling_unit]])
|
96
|
+
end
|
97
|
+
result
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def analyze_gif(file,debug)
|
103
|
+
props = {}
|
104
|
+
header = file.read(13)
|
105
|
+
width = header[6...8].unpack('v')[0]
|
106
|
+
length = header[8...10].unpack('v')[0]
|
107
|
+
par = header[12]
|
108
|
+
props[:width] = width
|
109
|
+
props[:length] = length
|
110
|
+
props[:mime] = 'image/gif'
|
111
|
+
props
|
112
|
+
end
|
113
|
+
|
114
|
+
def analyze_png(file,debug)
|
115
|
+
result = {}
|
116
|
+
size = File.size(file.path)
|
117
|
+
file.read(8) # skip signature
|
118
|
+
while (file.pos < size - 1) do
|
119
|
+
len_bytes = file.read(4)
|
120
|
+
length = len_bytes.unpack('N')[0]
|
121
|
+
ctype = file.read(4)
|
122
|
+
case
|
123
|
+
when 'pHYs'.eql?(ctype):
|
124
|
+
val = file.read(length)
|
125
|
+
xsam = val[0..3].unpack('N')[0]
|
126
|
+
ysam = val[4..7].unpack('N')[0]
|
127
|
+
unit = val[8].unpack('C')
|
128
|
+
if (unit ==1)
|
129
|
+
result[:sampling_unit] = :cm
|
130
|
+
xsam = xsam/100
|
131
|
+
ysam = ysam/100
|
132
|
+
end
|
133
|
+
result[:x_sampling] = xsam
|
134
|
+
result[:y_sampling] = ysam
|
135
|
+
file.seek(4,IO::SEEK_CUR)
|
136
|
+
when 'IHDR'.eql?(ctype):
|
137
|
+
val = file.read(length)
|
138
|
+
result[:width] = val[0..3].unpack('N')[0]
|
139
|
+
result[:length] = val[4..7].unpack('N')[0]
|
140
|
+
file.seek(4,IO::SEEK_CUR)
|
141
|
+
else
|
142
|
+
file.seek(4,IO::SEEK_CUR)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
result
|
146
|
+
end
|
147
|
+
def analyze_bitmap(file,debug)
|
148
|
+
result = {}
|
149
|
+
file.seek(0x12,IO::SEEK_CUR)
|
150
|
+
width = file.read(4).unpack('V')[0]
|
151
|
+
length = file.read(4).unpack('V')[0]
|
152
|
+
file.seek(0x0c,IO::SEEK_CUR)
|
153
|
+
xsam = file.read(4).unpack('V')[0]
|
154
|
+
ysam = file.read(4).unpack('V')[0]
|
155
|
+
xsam /= 100 # ppm -> ppc
|
156
|
+
ysam /= 100 # ppm -> ppc
|
157
|
+
result[:mime] = 'image/bmp'
|
158
|
+
result[:sampling_unit] = :cm
|
159
|
+
result[:x_sampling] = xsam
|
160
|
+
result[:y_sampling] = ysam
|
161
|
+
result[:width] = width
|
162
|
+
result[:length] = length
|
163
|
+
result
|
164
|
+
end
|
165
|
+
=begin
|
166
|
+
TIFF Format notes taken from http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf
|
167
|
+
Header format:
|
168
|
+
Bytes 0-1: BOM. [0x49,0x49] = LittleEndian, [0x4d,0x4d] = BigEndian
|
169
|
+
Bytes 2-3: Format marker (42) in the byte order indicated previously
|
170
|
+
Bytes 4-7: Byte offset of the first IFD, relative to file beginning
|
171
|
+
IFD format:
|
172
|
+
Bytes 0-1: Number of 12-byte IFD Entries
|
173
|
+
[IFD Entries]
|
174
|
+
Bytes -4 - -1: Byte offset of next IFD, or 0 if none remain
|
175
|
+
IFD Entry Format:
|
176
|
+
Bytes 0-1: Tag
|
177
|
+
Bytes 2-3: Type
|
178
|
+
1 = BYTE (unsigned 8-bit integer)
|
179
|
+
2 = ASCII (8 bit byte containing 7-bit char code)
|
180
|
+
3 = SHORT (16-bit unsigned integer)
|
181
|
+
4 = LONG (32-bit unsigned integer)
|
182
|
+
5 = RATIONAL (Two LONGs, first is numerator, second denominator)
|
183
|
+
Bytes 4-7: Num values of indicated Type
|
184
|
+
Bytes 8-11: Value offset
|
185
|
+
=end
|
186
|
+
def analyze_tiff(file,debug)
|
187
|
+
result = {:mime=>'image/tiff'}
|
188
|
+
littleEndian = [0x49,0x49].eql?(file.read(2))
|
189
|
+
file.seek(2,IO::SEEK_CUR)
|
190
|
+
nextIFD = littleEndian ? file.read(4).unpack('V')[0] : file.read(4).unpack('N')[0]
|
191
|
+
result.merge!(analyze_exif(file,nextIFD,littleEndian,debug))
|
192
|
+
result
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
JPEG_NO_PAYLOAD = (0xd0..0xd9)
|
197
|
+
JPEG_APP = (0xe0..0xef)
|
198
|
+
JPEG_VARIABLE_PAYLOAD = [0xc0,0xc2,0xc4,0xda,0xdb,0xfe]
|
199
|
+
def analyze_jpeg(file,debug)
|
200
|
+
result = {:mime => 'image/jpeg'}
|
201
|
+
while ((header = file.read(2)) and not "\xff\xd9".eql?(header))
|
202
|
+
case
|
203
|
+
when 0xdd.eql?(header[1]):
|
204
|
+
payload = nil
|
205
|
+
file.seek(2,IO::SEEK_CUR)
|
206
|
+
when JPEG_APP.member?(header[1]), JPEG_VARIABLE_PAYLOAD.member?(header[1]):
|
207
|
+
len = file.read(2).unpack('n')[0]
|
208
|
+
if ("\xff\xe0".eql?(header)): # APP0 segment - JFIF
|
209
|
+
puts "JFIF file segment detected" if debug
|
210
|
+
payload = file.read(len)
|
211
|
+
id = payload[0..4]
|
212
|
+
version = payload[5..6]
|
213
|
+
unit = payload[7]
|
214
|
+
x_sample = payload[8..9].unpack('n')[0]
|
215
|
+
y_sample = payload[10..11].unpack('n')[0]
|
216
|
+
result[:x_sampling] = x_sample
|
217
|
+
result[:y_sampling] = y_sample
|
218
|
+
if (unit == "\x01"): result[:sampling_unit] = :inch
|
219
|
+
elsif (unit == "\x02"): result[:sampling_unit] = :cm
|
220
|
+
end
|
221
|
+
elsif ("\xff\xe1".eql?(header)): # APP1 segment - EXIF
|
222
|
+
puts "EXIF file segment detected" if debug
|
223
|
+
payload = file.read(len)
|
224
|
+
result.merge!(analyze_exif(StringIO.new(payload),0,false,debug))
|
225
|
+
elsif (header[1] >= 0xc0 and header[1] <= 0xc3)
|
226
|
+
payload = file.read(len)
|
227
|
+
precision = payload[0]
|
228
|
+
length = payload[1..2].unpack('n')[0]
|
229
|
+
width = payload[3..4].unpack('n')[0]
|
230
|
+
result[:width] = width
|
231
|
+
result[:length] = length
|
232
|
+
else
|
233
|
+
file.seek(len,IO::SEEK_CUR)
|
234
|
+
end
|
235
|
+
else
|
236
|
+
payload = nil?
|
237
|
+
end
|
238
|
+
end
|
239
|
+
result
|
240
|
+
end
|
241
|
+
|
242
|
+
def analyze_exif(file,nextIFD,littleEndian,debug=false)
|
243
|
+
result = Hash.new()
|
244
|
+
until (nextIFD == 0) do
|
245
|
+
file.seek(nextIFD,IO::SEEK_SET)
|
246
|
+
bytes = file.read(2)
|
247
|
+
numEntries = littleEndian ? bytes.unpack('v')[0] : bytes.unpack('n')[0]
|
248
|
+
entries = Hash.new()
|
249
|
+
numEntries.times do
|
250
|
+
if (littleEndian)
|
251
|
+
tag = file.read(2).unpack('v')
|
252
|
+
ttype = file.read(2).unpack('v')[0]
|
253
|
+
numValues = file.read(4).unpack('V')[0]
|
254
|
+
valueOffsetBytes = file.read(4)
|
255
|
+
valueOffset = valueOffsetBytes.unpack('V')[0]
|
256
|
+
else
|
257
|
+
tag = file.read(2).unpack('n')[0]
|
258
|
+
ttype = file.read(2).unpack('n')[0]
|
259
|
+
numValues = file.read(4).unpack('N')[0]
|
260
|
+
valueOffsetBytes = file.read(4)
|
261
|
+
valueOffset = valueOffsetBytes.unpack('N')[0]
|
262
|
+
end
|
263
|
+
if (debug)
|
264
|
+
puts "\ntag : #{tag.to_s(16)} ttype: #{ttype.to_s(16)} numValues: #{numValues} valueOffset: #{valueOffset}"
|
265
|
+
end
|
266
|
+
|
267
|
+
nextEntry = file.tell()
|
268
|
+
values = []
|
269
|
+
if (1 <= ttype and ttype <= 5 and numValues > 0)
|
270
|
+
case
|
271
|
+
when ttype == 1: # unsigned bytes
|
272
|
+
if (numValues > 4)
|
273
|
+
file.seek(valueOffset,IO::SEEK_SET)
|
274
|
+
values = file.read(numValues)
|
275
|
+
else
|
276
|
+
values = valueOffsetBytes
|
277
|
+
end
|
278
|
+
values = values.unpack('C*')
|
279
|
+
|
280
|
+
when ttype == 2:
|
281
|
+
if (numValues > 4)
|
282
|
+
file.seek(valueOffset,IO::SEEK_SET)
|
283
|
+
values = file.read(numValues)
|
284
|
+
else
|
285
|
+
values = valueOffsetBytes
|
286
|
+
end
|
287
|
+
values = values.unpack('C*')
|
288
|
+
values.collect! {|c|
|
289
|
+
c.to_chr
|
290
|
+
}
|
291
|
+
when ttype == 3:
|
292
|
+
if (numValues > 2)
|
293
|
+
file.seek(valueOffset,IO::SEEK_SET)
|
294
|
+
values = file.read(numValues * 2)
|
295
|
+
else
|
296
|
+
values = valueOffsetBytes
|
297
|
+
end
|
298
|
+
values = littleEndian ? values.unpack('v*'):values.unpack('n*')
|
299
|
+
when ttype == 4:
|
300
|
+
if (numValues > 1)
|
301
|
+
file.seek(valueOffset,IO::SEEK_SET)
|
302
|
+
values = file.read(numValues * 4)
|
303
|
+
else
|
304
|
+
values = valueOffsetBytes
|
305
|
+
end
|
306
|
+
values = littleEndian ? values.unpack('V*'):values.unpack('N*')
|
307
|
+
when ttype == 5:
|
308
|
+
# RATIONAL: a sequence of pairs of 32-bit integers, numerator and denominator
|
309
|
+
file.seek(valueOffset,IO::SEEK_SET)
|
310
|
+
values = file.read(numValues * 8)
|
311
|
+
if(values.length % 8) != 0:
|
312
|
+
raise "Unexpected end of bytestream when reading EXIF data"
|
313
|
+
end
|
314
|
+
values = littleEndian ? values.unpack('V*'):values.unpack('N*')
|
315
|
+
values = (0...values.length).step(2).collect {|ix|
|
316
|
+
values[ix].quo(values[(ix)+1])
|
317
|
+
}
|
318
|
+
else
|
319
|
+
if debug: puts "Unknown tag type: #{ttype}"
|
320
|
+
end
|
321
|
+
end
|
322
|
+
entries[tag] = values
|
323
|
+
end
|
324
|
+
file.seek(nextEntry,IO::SEEK_SET)
|
325
|
+
end
|
326
|
+
nextIFD = littleEndian ? file.read(4).unpack('V')[0] : file.read(4).unpack('N')[0]
|
327
|
+
end
|
328
|
+
if (entries.has_key?(0x0100))
|
329
|
+
result[:width] = entries[0x0100][0]
|
330
|
+
end
|
331
|
+
if (entries.has_key?(0x0101))
|
332
|
+
result[:length] = entries[0x0101][0]
|
333
|
+
end
|
334
|
+
if (entries.has_key?(0x011a))
|
335
|
+
result[:x_sampling] = entries[0x011a][0]
|
336
|
+
end
|
337
|
+
if (entries.has_key?(0x011b))
|
338
|
+
result[:y_sampling] = entries[0x011b][0]
|
339
|
+
end
|
340
|
+
if (entries.has_key?(0x128))
|
341
|
+
unit_key = entries[0x128][0]
|
342
|
+
if (unit_key == 2)
|
343
|
+
result[:sampling_unit] = :inch
|
344
|
+
elsif (unit_key == 3)
|
345
|
+
result[:sampling_unit] = :cm
|
346
|
+
end
|
347
|
+
end
|
348
|
+
result
|
349
|
+
end
|
350
|
+
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
@@ -0,0 +1,372 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
class CulFedoraArmBuilderTest < Test::Unit::TestCase
|
5
|
+
TEST = 'test'
|
6
|
+
CASE1 = "#{TEST}/fixtures/case1"
|
7
|
+
CASE2 = "#{TEST}/fixtures/case2"
|
8
|
+
CASE3 = "#{TEST}/fixtures/case3"
|
9
|
+
CASE4 = "#{TEST}/fixtures/case4"
|
10
|
+
|
11
|
+
context "Given the 'test_config' fedora instance " do
|
12
|
+
setup do
|
13
|
+
@connector = Cul::Fedora::Connector.parse(YAML::load_file("private/fedora-config.yml"))["test_config"]
|
14
|
+
end
|
15
|
+
|
16
|
+
should "build a connector properly" do
|
17
|
+
assert_kind_of Cul::Fedora::Connector, @connector
|
18
|
+
end
|
19
|
+
|
20
|
+
context "The builder class" do
|
21
|
+
setup do
|
22
|
+
@builder_class = Cul::Fedora::Arm::Builder
|
23
|
+
end
|
24
|
+
|
25
|
+
should "have a blank read_only array of parts" do
|
26
|
+
@builder = @builder_class.new
|
27
|
+
|
28
|
+
assert_instance_of @builder_class, @builder
|
29
|
+
assert_equal [], @builder.parts
|
30
|
+
assert_raise NoMethodError do
|
31
|
+
@builder.parts = [:test]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
should "have a default array of columns for templates without headers" do
|
36
|
+
assert_equal @builder_class::DEFAULT_TEMPLATE_HEADER, [:sequence, :target, :model_type, :source, :template_type, :dc_format, :id, :pid, :action, :license]
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
should "have a list of mandatory, valid, and required columns" do
|
41
|
+
assert_instance_of Array, @builder_class::REQUIRED_COLUMNS
|
42
|
+
|
43
|
+
assert_equal @builder_class::REQUIRED_COLUMNS, [:sequence]
|
44
|
+
assert_equal @builder_class::MANDATORY_COLUMNS, [:sequence, :target, :model_type]
|
45
|
+
assert_equal @builder_class::VALID_COLUMNS, [:sequence, :target, :model_type, :source, :template_type, :dc_format, :id, :pid, :action, :license]
|
46
|
+
end
|
47
|
+
|
48
|
+
should "accept only options :template or :file" do
|
49
|
+
assert_instance_of @builder_class, @builder_class.new(:template => nil)
|
50
|
+
assert_instance_of @builder_class, @builder_class.new(:file => nil)
|
51
|
+
|
52
|
+
assert_raise(ArgumentError) do
|
53
|
+
@builder_class.new(:template => nil, :invalid => true)
|
54
|
+
end
|
55
|
+
|
56
|
+
assert_raise(ArgumentError) do
|
57
|
+
@builder_class.new(:template => "test", :file => "test")
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
should "not accept :header without :template" do
|
63
|
+
assert_raise(ArgumentError) do
|
64
|
+
@builder_class.new(:header => true)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
context "given a blank builder" do
|
70
|
+
setup do
|
71
|
+
@builder = @builder_class.new
|
72
|
+
end
|
73
|
+
|
74
|
+
should "have a blank array of parts" do
|
75
|
+
assert_equal @builder.parts, []
|
76
|
+
end
|
77
|
+
|
78
|
+
should "be able to add parts" do
|
79
|
+
@builder.add_part(:sequence => "0", :target => "collection:1;ac:5", :source => "/test-0001.xml", :model_type => "Metadata")
|
80
|
+
end
|
81
|
+
|
82
|
+
should "not add parts without a sequence" do
|
83
|
+
assert_raise RuntimeError, "Missing required values sequence" do
|
84
|
+
@builder.add_part(:target => "collection:1;ac:5", :source => "/test-0001.xml")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
should "not add parts with the same sequence id" do
|
89
|
+
|
90
|
+
assert_raise(RuntimeError, "Sequence ID already taken") do
|
91
|
+
@builder.add_part(:sequence => "2", :source => "/test-0001.xml", :model_type => "Metadata")
|
92
|
+
@builder.add_part(:sequence => "2", :source => "/test-0002.xml", :model_type => "Metadata")
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "given headers for templates" do
|
99
|
+
setup do
|
100
|
+
@good_header = %w{sequence target model_type source template_type dc_format id}
|
101
|
+
@invalid_column = %w{sequence target model_type random}
|
102
|
+
@sequence_not_first = %w{target model_type sequence}
|
103
|
+
@missing_mandatory = %w{sequence target template_type}
|
104
|
+
end
|
105
|
+
|
106
|
+
should "accept good headers" do
|
107
|
+
assert_instance_of @builder_class, @builder_class.new(:template => template_builder(@good_header))
|
108
|
+
end
|
109
|
+
|
110
|
+
should "accept blank template" do
|
111
|
+
assert_nothing_raised do
|
112
|
+
@builder_class.new(:template => nil)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
should "reject invalid column names" do
|
117
|
+
assert_raise RuntimeError, "Invalid column name: random" do
|
118
|
+
@builder_class.new(:template => template_builder(@invalid_column))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
should "insist on all mandatory columns" do
|
123
|
+
assert_raise RuntimeError, "Missing mandatory column: metadata" do
|
124
|
+
@builder_class.new(:template => template_builder(@missing_mandatory))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "with an example template with header" do
|
130
|
+
setup do
|
131
|
+
@builder = @builder_class.new(:file => "#{CASE1}/builder-template.txt")
|
132
|
+
@builder_via_template_option = @builder_class.new(:template => File.open("#{CASE1}/builder-template.txt", "r"))
|
133
|
+
@builder_no_header = @builder_class.new(:file => "#{CASE1}/builder-template-noheader.txt", :header => false)
|
134
|
+
end
|
135
|
+
|
136
|
+
should "load successfully" do
|
137
|
+
assert_instance_of @builder_class, @builder
|
138
|
+
assert_instance_of @builder_class, @builder_no_header
|
139
|
+
end
|
140
|
+
|
141
|
+
should "have equivalent results for header and no_header instances of the default column set" do
|
142
|
+
@builder.parts.each{|part|
|
143
|
+
assert_equal part, @builder_no_header.part_by_sequence(part[:sequence])
|
144
|
+
}
|
145
|
+
assert_equal @builder.parts.length, @builder_no_header.parts.length
|
146
|
+
end
|
147
|
+
|
148
|
+
should "have equivalent results for opening via template and file" do
|
149
|
+
assert_equal @builder.parts, @builder_via_template_option.parts
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
should "ignore the header row and load template data successfully" do
|
154
|
+
assert_equal @builder.parts.length, 6
|
155
|
+
assert_equal @builder.parts[2][:source], "/test-0001.xml"
|
156
|
+
assert_equal @builder.parts[3][:license], "license:by-nc-nd"
|
157
|
+
end
|
158
|
+
|
159
|
+
should "have parts accessible by sequence id" do
|
160
|
+
assert_kind_of Hash, @builder.part_by_sequence("6")
|
161
|
+
assert_equal @builder.parts[4], @builder.part_by_sequence("6")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context "with an example template with header, and fedora credentials" do
|
166
|
+
setup do
|
167
|
+
args = {:file => "#{CASE1}/builder-template.txt", :connector => @connector}
|
168
|
+
@builder = @builder_class.new(args)
|
169
|
+
@builder_via_template_option = @builder_class.new(:template => File.open("#{CASE1}/builder-template.txt", "r"))
|
170
|
+
@builder_no_header = @builder_class.new(:file => "#{CASE1}/builder-template-noheader.txt", :header => false)
|
171
|
+
end
|
172
|
+
|
173
|
+
should "load successfully" do
|
174
|
+
assert_instance_of @builder_class, @builder
|
175
|
+
assert_instance_of @builder_class, @builder_no_header
|
176
|
+
end
|
177
|
+
|
178
|
+
should "have equivalent results for header and no_header instances of the default column set" do
|
179
|
+
@builder.parts.each{|part|
|
180
|
+
assert_equal part, @builder_no_header.part_by_sequence(part[:sequence])
|
181
|
+
}
|
182
|
+
assert_equal @builder.parts.length, @builder_no_header.parts.length
|
183
|
+
end
|
184
|
+
|
185
|
+
should "have equivalent results for opening via template and file" do
|
186
|
+
assert_equal @builder.parts, @builder_via_template_option.parts
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
should "ignore the header row and load template data successfully" do
|
191
|
+
assert_equal @builder.parts.length, 6
|
192
|
+
assert_equal @builder.parts[2][:source], "/test-0001.xml"
|
193
|
+
assert_equal @builder.parts[3][:license], "license:by-nc-nd"
|
194
|
+
end
|
195
|
+
|
196
|
+
should "have parts accessible by sequence id" do
|
197
|
+
assert_kind_of Hash, @builder.part_by_sequence("6")
|
198
|
+
assert_equal @builder.parts[4], @builder.part_by_sequence("6")
|
199
|
+
end
|
200
|
+
|
201
|
+
should "be able to procure PIDs for parts in sequence" do
|
202
|
+
@builder.reserve_pids()
|
203
|
+
@builder.parts.each { |part|
|
204
|
+
assert part.has_key?(:pid)
|
205
|
+
assert !(part[:pid].empty?)
|
206
|
+
assert_match /^\w+:\d+$/, part[:pid]
|
207
|
+
targets = part[:target].split(';')
|
208
|
+
targets.each {|target|
|
209
|
+
assert_no_match /^\d+$/, target
|
210
|
+
}
|
211
|
+
}
|
212
|
+
end
|
213
|
+
end
|
214
|
+
context "with example input with header for two inserted aggregators, and fedora credentials" do
|
215
|
+
setup do
|
216
|
+
args = {:file => "#{CASE2}/builder-template.txt",:ns=>'test', :connector => @connector}
|
217
|
+
@builder = @builder_class.new(args)
|
218
|
+
@builder_via_template_option = @builder_class.new(:template => File.open("#{CASE1}/builder-template.txt", "r"))
|
219
|
+
end
|
220
|
+
should "be able to ingest aggregators into repository" do
|
221
|
+
# do ingest
|
222
|
+
@assigned = @builder.reserve_pids()
|
223
|
+
if (@assigned)
|
224
|
+
@assigned.each {|pid|
|
225
|
+
assert @builder.part_by_pid(pid), "Could not find part assigned to #{pid}"
|
226
|
+
}
|
227
|
+
end
|
228
|
+
response = @builder.process_parts()
|
229
|
+
|
230
|
+
# get objects, verify properties
|
231
|
+
if (@assigned)
|
232
|
+
host, port = @connector.config_for(:rest,:host),@connector.config_for(:rest, :port)
|
233
|
+
http = Net::HTTP.start(host, port)
|
234
|
+
|
235
|
+
@connector.rest_interface do |http|
|
236
|
+
@assigned.each { |pid|
|
237
|
+
resp = http.head("/fedora/get/#{pid}/DC")
|
238
|
+
assert_equal "200", resp.code, "#{pid} not loaded correctly to repo at #{host}:#{port}... #{resp.code} #{resp.message} "
|
239
|
+
}
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
teardown do
|
244
|
+
# purge objects
|
245
|
+
if (@assigned)
|
246
|
+
@assigned.each {|pid|
|
247
|
+
begin
|
248
|
+
@builder.purge(pid)
|
249
|
+
rescue Exception=>e
|
250
|
+
puts "Error purging #{pid}: #{e}"
|
251
|
+
end
|
252
|
+
}
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
context "with example input with header for an inserted metadata, and fedora credentials" do
|
257
|
+
setup do
|
258
|
+
infile_path = "#{CASE4}/builder-template.txt"
|
259
|
+
b = binding
|
260
|
+
template = File.open(infile_path) {|f|
|
261
|
+
f.readlines().collect{ |line|
|
262
|
+
line.gsub(/\#\{(\w+)\}/) {|match| eval($1, b)}
|
263
|
+
}
|
264
|
+
}
|
265
|
+
args = {:template => template,:ns=>'test', :connector => @connector}
|
266
|
+
@builder = @builder_class.new(args)
|
267
|
+
@builder_via_template_option = @builder_class.new(:template => File.open("#{CASE4}/builder-template.txt", "r"))
|
268
|
+
end
|
269
|
+
should "be able to ingest metadata objects into repository" do
|
270
|
+
# do ingest
|
271
|
+
@assigned = @builder.reserve_pids()
|
272
|
+
if (@assigned)
|
273
|
+
@assigned.each {|pid|
|
274
|
+
assert @builder.part_by_pid(pid), "Could not find part assigned to #{pid}"
|
275
|
+
}
|
276
|
+
end
|
277
|
+
response = @builder.process_parts()
|
278
|
+
|
279
|
+
# get objects, verify properties
|
280
|
+
if (@assigned)
|
281
|
+
@connector.rest_interface do |http|
|
282
|
+
@assigned.each { |pid|
|
283
|
+
resp = http.get("/fedora/get/#{pid}/CONTENT")
|
284
|
+
assert_equal "200", resp.code, "#{pid} not loaded correctly to repo at #{@connector.rest_location}... #{resp.code} #{resp.message} "
|
285
|
+
eTitle = "<title>test record title</title>"
|
286
|
+
assert resp.body.index(eTitle), "did not find expected mods title tag #{eTitle} in #{pid}/CONTENT"
|
287
|
+
resp = http.get("/fedora/get/#{pid}/DC")
|
288
|
+
assert_equal "200", resp.code, "#{pid} not loaded correctly to repo at #{@connector.rest_location}... #{resp.code} #{resp.message} "
|
289
|
+
eTitle = "<dc:title>test record title</dc:title>"
|
290
|
+
assert resp.body.index(eTitle), "did not find expected title tag #{eTitle} in #{pid}/DC"
|
291
|
+
}
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
teardown do
|
296
|
+
# purge objects
|
297
|
+
if (@assigned)
|
298
|
+
@assigned.each {|pid|
|
299
|
+
begin
|
300
|
+
@builder.purge(pid)
|
301
|
+
rescue Exception=>e
|
302
|
+
puts "Error purging #{pid}: #{e}"
|
303
|
+
end
|
304
|
+
}
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
context "with example input with header for an inserted resource, and fedora credentials" do
|
309
|
+
setup do
|
310
|
+
infile_path = "#{CASE3}/builder-template.txt"
|
311
|
+
b = binding
|
312
|
+
template = File.open(infile_path) {|f|
|
313
|
+
f.readlines().collect{ |line|
|
314
|
+
line.gsub(/\#\{(\w+)\}/) {|match| eval($1, b)}
|
315
|
+
}
|
316
|
+
}
|
317
|
+
args = {:template => template,:ns=>'test', :connector => @connector}
|
318
|
+
@builder = @builder_class.new(args)
|
319
|
+
@builder_via_template_option = @builder_class.new(:template => File.open("#{CASE3}/builder-template.txt", "r"))
|
320
|
+
end
|
321
|
+
should "be able to ingest resource objects into repository" do
|
322
|
+
# do ingest
|
323
|
+
@assigned = @builder.reserve_pids()
|
324
|
+
if (@assigned)
|
325
|
+
@assigned.each {|pid|
|
326
|
+
assert @builder.part_by_pid(pid), "Could not find part assigned to #{pid}"
|
327
|
+
}
|
328
|
+
end
|
329
|
+
response = @builder.process_parts()
|
330
|
+
|
331
|
+
# get objects, verify properties
|
332
|
+
if (@assigned)
|
333
|
+
@connector.rest_interface do |http|
|
334
|
+
@assigned.each { |pid|
|
335
|
+
resp = http.head("/fedora/get/#{pid}/DC")
|
336
|
+
assert_equal "200", resp.code, "#{pid}/DC not loaded correctly to repo at #{@connector.rest_location}... #{resp.code} #{resp.message} "
|
337
|
+
resp = http.get("/fedora/get/#{pid}/CONTENT")
|
338
|
+
assert_equal "200", resp.code, "#{pid}/CONTENT not loaded correctly to repo at #{@connector.rest_location}... #{resp.code} #{resp.message} "
|
339
|
+
actual = resp.body
|
340
|
+
begin
|
341
|
+
expected = File.open(@builder.part_by_pid(pid)[:source],'rb')
|
342
|
+
a_digest = Digest::MD5.hexdigest(actual)
|
343
|
+
e_digest = Digest::MD5.hexdigest(expected.read())
|
344
|
+
assert_equal e_digest, a_digest
|
345
|
+
ensure
|
346
|
+
expected.close()
|
347
|
+
end
|
348
|
+
resp = http.get("/fedora/get/#{pid}/RELS-EXT")
|
349
|
+
assert_equal "200", resp.code, "#{pid}/RELS-EXT not loaded correctly to repo at #{@connector.rest_location}... #{resp.code} #{resp.message} "
|
350
|
+
rels = resp.body
|
351
|
+
assert rels.index("<dcmi:extent>15138</dcmi:extent>") > -1
|
352
|
+
http.finish()
|
353
|
+
}
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
teardown do
|
358
|
+
# purge objects
|
359
|
+
if (@assigned)
|
360
|
+
@assigned.each {|pid|
|
361
|
+
begin
|
362
|
+
@builder.purge(pid)
|
363
|
+
rescue Exception=>e
|
364
|
+
puts "Error purging #{pid}: #{e}"
|
365
|
+
end
|
366
|
+
}
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|