berkeley_library-tind 0.4.2 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,152 @@
1
+ require 'nokogiri'
2
+ require 'marc_extensions'
3
+ require 'berkeley_library/tind/marc/xml_builder'
4
+
5
+ module BerkeleyLibrary
6
+ module TIND
7
+ module MARC
8
+ class XMLWriter
9
+ include BerkeleyLibrary::Util::Files
10
+ include BerkeleyLibrary::Logging
11
+
12
+ # ------------------------------------------------------------
13
+ # Constants
14
+
15
+ UTF_8 = Encoding::UTF_8.name
16
+
17
+ EMPTY_COLLECTION_DOC = Nokogiri::XML::Builder.new(encoding: UTF_8) do |xml|
18
+ xml.collection(xmlns: ::MARC::MARC_NS)
19
+ end.doc.freeze
20
+
21
+ COLLECTION_CLOSING_TAG = '</collection>'.freeze
22
+
23
+ DEFAULT_NOKOGIRI_OPTS = { encoding: UTF_8 }.freeze
24
+
25
+ # ------------------------------------------------------------
26
+ # Fields
27
+
28
+ attr_reader :out
29
+ attr_reader :nokogiri_options
30
+
31
+ # ------------------------------------------------------------
32
+ # Initializer
33
+
34
+ # Initializes a new {XMLWriter}.
35
+ #
36
+ # ```ruby
37
+ # File.open('marc.xml', 'wb') do |f|
38
+ # w = XMLWriter.new(f)
39
+ # marc_records.each { |r| w.write(r) }
40
+ # w.close
41
+ # end
42
+ # ```
43
+ #
44
+ # @param out [IO, String] an IO, or the name of a file
45
+ # @param nokogiri_options [Hash] Options passed to
46
+ # {https://nokogiri.org/rdoc/Nokogiri/XML/Node.html#method-i-write_to Nokogiri::XML::Node#write_to}
47
+ # Note that the `encoding` option is ignored, except insofar as
48
+ # passing an encoding other than UTF-8 will raise an `ArgumentError`.
49
+ # @raise ArgumentError if `out` is not an IO or a string, or is a string referencing
50
+ # a file path that cannot be opened for writing; or if an encoding other than UTF-8
51
+ # is specified in `nokogiri-options`
52
+ # @see #open
53
+ def initialize(out, **nokogiri_options)
54
+ @nokogiri_options = valid_nokogiri_options(nokogiri_options)
55
+ @out = ensure_io(out)
56
+ end
57
+
58
+ # ------------------------------------------------------------
59
+ # Class methods
60
+
61
+ class << self
62
+
63
+ # Opens a new {XMLWriter} with the specified output destination and
64
+ # Nokogiri options, writes the XML prolog and opening `<collection>`
65
+ # tag, yields the writer to write one or more MARC records, and closes
66
+ # the writer.
67
+ #
68
+ # ```ruby
69
+ # XMLWriter.open('marc.xml') do |w|
70
+ # marc_records.each { |r| w.write(r) }
71
+ # end
72
+ # ```
73
+ #
74
+ # Note that unlike initializing a writer with {#new} and closing it
75
+ # immediately, this will write an XML document with an empty
76
+ # `<collection></collection>` tag even if no records are written.
77
+ #
78
+ # @yieldparam writer [XMLWriter] the writer
79
+ # @see #new
80
+ # @see #close
81
+ def open(out, **nokogiri_options)
82
+ writer = new(out, **nokogiri_options)
83
+ writer.send(:ensure_open!)
84
+ yield writer if block_given?
85
+ writer.close
86
+ end
87
+ end
88
+
89
+ # ------------------------------------------------------------
90
+ # Instance methods
91
+
92
+ # Writes the specified record to the underlying stream, writing the
93
+ # XML prolog and opening `<collection>` tag if they have not yet
94
+ # been written.
95
+ #
96
+ # @param record [::MARC::Record] the MARC record to write.
97
+ # @raise IOError if the underlying stream has already been closed.
98
+ def write(record)
99
+ ensure_open!
100
+ record_element = XMLBuilder.new(record).build
101
+ record_element.write_to(out, nokogiri_options)
102
+ out.write("\n")
103
+ end
104
+
105
+ # Closes the underlying stream. If the XML prolog and opening `<collection>`
106
+ # tag have already been written, the closing `<collection/>` tag is written
107
+ # first.
108
+ def close
109
+ out.write(COLLECTION_CLOSING_TAG) if @open
110
+ out.close
111
+ end
112
+
113
+ # ------------------------------------------------------------
114
+ # Private
115
+
116
+ private
117
+
118
+ def ensure_open!
119
+ return if @open
120
+
121
+ out.write(prolog_and_opening_tag)
122
+ @open = true
123
+ end
124
+
125
+ def prolog_and_opening_tag
126
+ StringIO.open do |tmp|
127
+ EMPTY_COLLECTION_DOC.write_to(tmp, nokogiri_options)
128
+ result = tmp.string
129
+ result.sub!(%r{/>\s*$}, ">\n")
130
+ result
131
+ end
132
+ end
133
+
134
+ def ensure_io(file)
135
+ return file if writer_like?(file)
136
+ return File.open(file, 'wb') if parent_exists?(file)
137
+
138
+ raise ArgumentError, "Don't know how to write XML to #{file.inspect}: not an IO or file path"
139
+ end
140
+
141
+ def valid_nokogiri_options(opts)
142
+ if (encoding = opts.delete(:encoding)) && encoding != UTF_8
143
+ raise ArgumentError, "#{self.class.name} only supports #{UTF_8}; unable to use specified encoding #{encoding}"
144
+ end
145
+
146
+ DEFAULT_NOKOGIRI_OPTS.merge(opts)
147
+ end
148
+
149
+ end
150
+ end
151
+ end
152
+ end
@@ -7,7 +7,7 @@ module BerkeleyLibrary
7
7
  SUMMARY = 'TIND DA utilities for the UC Berkeley Library'.freeze
8
8
  DESCRIPTION = 'UC Berkeley Library utility gem for working with the TIND DA digital archive.'.freeze
9
9
  LICENSE = 'MIT'.freeze
10
- VERSION = '0.4.2'.freeze
10
+ VERSION = '0.5.1'.freeze
11
11
  HOMEPAGE = 'https://github.com/BerkeleyLibrary/tind'.freeze
12
12
  end
13
13
  end
@@ -0,0 +1,39 @@
1
+ module BerkeleyLibrary
2
+ module Util
3
+ # TODO: Move this to `berkeley_library-util`
4
+ module Files
5
+ class << self
6
+ include Files
7
+ end
8
+
9
+ def file_exists?(path)
10
+ (path.respond_to?(:exist?) && path.exist?) ||
11
+ (path.respond_to?(:to_str) && File.exist?(path))
12
+ end
13
+
14
+ def parent_exists?(path)
15
+ path.respond_to?(:parent) && path.parent.exist? ||
16
+ path.respond_to?(:to_str) && Pathname.new(path).parent.exist?
17
+ end
18
+
19
+ # Returns true if `obj` is close enough to an IO object for Nokogiri
20
+ # to parse as one.
21
+ #
22
+ # @param obj [Object] the object that might be an IO
23
+ # @see https://github.com/sparklemotion/nokogiri/blob/v1.11.1/lib/nokogiri/xml/sax/parser.rb#L81 Nokogiri::XML::SAX::Parser#parse
24
+ def reader_like?(obj)
25
+ obj.respond_to?(:read) && obj.respond_to?(:close)
26
+ end
27
+
28
+ # Returns true if `obj` is close enough to an IO object for Nokogiri
29
+ # to write to.
30
+ #
31
+ # @param obj [Object] the object that might be an IO
32
+ def writer_like?(obj)
33
+ # TODO: is it possible/desirable to loosen this? how strict is libxml2?
34
+ obj.is_a?(IO) || obj.is_a?(StringIO)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -24,6 +24,8 @@ module BerkeleyLibrary
24
24
  tag = '245'
25
25
  ind_bad = '!'
26
26
 
27
+ records = MARC::XMLReader.read('spec/data/records-manual-search.xml', freeze: false).to_a
28
+
27
29
  record = records.first
28
30
  record[tag].indicator1 = ind_bad
29
31
  expect { table << record }.to raise_error(Export::ExportException) do |e|
@@ -16,6 +16,25 @@ module BerkeleyLibrary
16
16
  expect(record0['024']['a']).to eq('BANC PIC 1982.078:15--ALB')
17
17
  end
18
18
 
19
+ describe 'freeze: true' do
20
+ it 'freezes the records' do
21
+ reader = XMLReader.new('spec/data/records-api-search.xml', freeze: true)
22
+ records = reader.to_a
23
+ expect(records).not_to be_empty # just to be sure
24
+ records.each { |record| expect(record).to be_frozen }
25
+ end
26
+ end
27
+
28
+ describe :records_yielded do
29
+ it 'counts the records' do
30
+ reader = XMLReader.new('spec/data/records-api-search.xml')
31
+ reader.each_with_index do |_, i|
32
+ expect(reader.records_yielded).to eq(i)
33
+ end
34
+ expect(reader.records_yielded).to eq(5)
35
+ end
36
+ end
37
+
19
38
  describe :new do
20
39
  it 'accepts a string path' do
21
40
  path = 'spec/data/records-api-search.xml'
@@ -59,7 +78,7 @@ module BerkeleyLibrary
59
78
 
60
79
  it 'raises ArgumentError if passed something random' do
61
80
  non_xml = Object.new
62
- # noinspection RubyYardParamTypeMatch
81
+ # noinspection RubyMismatchedArgumentType
63
82
  expect { XMLReader.new(non_xml) }.to raise_error(ArgumentError)
64
83
  end
65
84
  end
@@ -87,6 +106,28 @@ module BerkeleyLibrary
87
106
  end
88
107
  end
89
108
 
109
+ describe 'TIND peculiarities' do
110
+ attr_reader :record
111
+
112
+ before(:each) do
113
+ reader = XMLReader.new('spec/data/new-records.xml')
114
+ records = reader.to_a
115
+ expect(records.size).to eq(1) # just to be sure
116
+ @record = records.first
117
+ end
118
+
119
+ it 'converts backslashes in control fields to spaces' do
120
+ cf_008 = record['008']
121
+ expect(cf_008).to be_a(::MARC::ControlField)
122
+ expect(cf_008.value).to eq('190409s2015 xx eng ')
123
+ end
124
+
125
+ it 'parses CF 000 as the leader' do
126
+ expect(record.leader).to eq('00287cam a2200313 4500')
127
+ expect(record['000']).to be_nil
128
+ end
129
+ end
130
+
90
131
  end
91
132
  end
92
133
  end
@@ -0,0 +1,194 @@
1
+ require 'spec_helper'
2
+ require 'equivalent-xml'
3
+
4
+ module BerkeleyLibrary
5
+ module TIND
6
+ module MARC
7
+ describe XMLWriter do
8
+ let(:input_path) { 'spec/data/new-records.xml' }
9
+ attr_reader :record
10
+
11
+ before(:each) do
12
+ reader = XMLReader.new(input_path)
13
+ @record = reader.first
14
+ end
15
+
16
+ describe :open do
17
+
18
+ it 'writes a MARC record to a file as XML' do
19
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
20
+ output_path = File.join(dir, 'marc.xml')
21
+ XMLWriter.open(output_path) { |w| w.write(record) }
22
+
23
+ expected = File.open(input_path) { |f| Nokogiri::XML(f) }
24
+ actual = File.open(output_path) { |f| Nokogiri::XML(f) }
25
+
26
+ aggregate_failures do
27
+ EquivalentXml.equivalent?(expected, actual) do |n1, n2, result|
28
+ expect(n2.to_s).to eq(n1.to_s) unless result
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ it 'writes a MARC record to a StringIO' do
35
+ out = StringIO.new
36
+ XMLWriter.open(out) { |w| w.write(record) }
37
+ expected = File.open(input_path) { |f| Nokogiri::XML(f) }
38
+ actual = Nokogiri::XML(out.string)
39
+ aggregate_failures do
40
+ EquivalentXml.equivalent?(expected, actual) do |n1, n2, result|
41
+ expect(n2.to_s).to eq(n1.to_s) unless result
42
+ end
43
+ end
44
+ end
45
+
46
+ it 'accepts Nokogiri options' do
47
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
48
+ expected_path = File.join(dir, 'expected.xml')
49
+ XMLWriter.open(expected_path) { |w| w.write(record) }
50
+
51
+ actual_path = File.join(dir, 'actual.xml')
52
+ XMLWriter.open(actual_path, indent_text: "\t") { |w| w.write(record) }
53
+
54
+ expected = File.read(expected_path).gsub(%r{ (?= *<)(?!/)}, "\t")
55
+ actual = File.read(actual_path)
56
+ expect(actual).to eq(expected)
57
+ end
58
+ end
59
+
60
+ it 'accepts an explicit UTF-8 argument' do
61
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
62
+ output_path = File.join(dir, 'marc.xml')
63
+ XMLWriter.open(output_path, encoding: 'UTF-8') { |w| w.write(record) }
64
+
65
+ expected = File.open(input_path) { |f| Nokogiri::XML(f) }
66
+ actual = File.open(output_path) { |f| Nokogiri::XML(f) }
67
+
68
+ aggregate_failures do
69
+ EquivalentXml.equivalent?(expected, actual) do |n1, n2, result|
70
+ expect(n2.to_s).to eq(n1.to_s) unless result
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ it 'only writes UTF-8' do
77
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
78
+ output_path = File.join(dir, 'marc.xml')
79
+ expect { XMLWriter.open(output_path, encoding: 'UTF-16') }.to raise_error(ArgumentError)
80
+ expect(File.exist?(output_path)).to eq(false)
81
+ end
82
+ end
83
+
84
+ it 'rejects an invalid file path' do
85
+ bad_directory = Dir.mktmpdir(File.basename(__FILE__, '.rb')) { |dir| dir }
86
+ expect(File.directory?(bad_directory)).to eq(false)
87
+ output_path = File.join(bad_directory, 'marc.xml')
88
+ expect { XMLWriter.open(output_path) }.to raise_error(ArgumentError)
89
+ end
90
+
91
+ it 'rejects a non-IO, non-String argument' do
92
+ invalid_target = Object.new
93
+ expect { XMLWriter.open(invalid_target) }.to raise_error(ArgumentError)
94
+ end
95
+ end
96
+
97
+ describe :close do
98
+ it 'closes without writing the closing tag if nothing has been written' do
99
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
100
+ output_path = File.join(dir, 'marc.xml')
101
+ w = XMLWriter.new(output_path)
102
+ w.close
103
+
104
+ stat = File.stat(output_path)
105
+ expect(stat.size).to eq(0)
106
+ end
107
+ end
108
+
109
+ it 'writes the closing tag if the opening tag has been written' do
110
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
111
+ output_path = File.join(dir, 'marc.xml')
112
+ XMLWriter.open(output_path)
113
+ expect(File.exist?(output_path)).to eq(true)
114
+
115
+ doc = File.open(output_path) { |f| Nokogiri::XML(f) }
116
+ expect(doc.root.name).to eq('collection')
117
+ end
118
+ end
119
+ end
120
+
121
+ describe :write do
122
+ it 'raises an IOError if the writer has already been closed' do
123
+ Dir.mktmpdir(File.basename(__FILE__, '.rb')) do |dir|
124
+ output_path = File.join(dir, 'marc.xml')
125
+ w = XMLWriter.new(output_path)
126
+ w.close
127
+
128
+ expect { w.write(record) }.to raise_error(IOError)
129
+
130
+ stat = File.stat(output_path)
131
+ expect(stat.size).to eq(0)
132
+ end
133
+ end
134
+
135
+ it 'does not write a nil leader' do
136
+ record.leader = nil
137
+ marc_xml = StringIO.open do |out|
138
+ XMLWriter.open(out) { |w| w.write(record) }
139
+ out.string
140
+ end
141
+ expect(marc_xml).not_to include('leader')
142
+ end
143
+
144
+ it 'does not write a blank leader' do
145
+ record.leader = ''
146
+ marc_xml = StringIO.open do |out|
147
+ XMLWriter.open(out) { |w| w.write(record) }
148
+ out.string
149
+ end
150
+ expect(marc_xml).not_to include('leader')
151
+ end
152
+
153
+ describe 'issue #4' do
154
+ let(:record_expected) { ::MARC::XMLReader.new('spec/data/issue-4.xml').first }
155
+ let(:record_actual) do
156
+ marc_xml = StringIO.open do |out|
157
+ XMLWriter.open(out) { |w| w.write(record_expected) }
158
+ out.string
159
+ end
160
+
161
+ ::MARC::XMLReader.new(StringIO.new(marc_xml)).first
162
+ end
163
+
164
+ it 'does not reorder fields' do
165
+ expected_tags = record_expected.fields.map(&:tag)
166
+ actual_tags = record_actual.fields.map(&:tag).reject { |t| t == '000' }
167
+
168
+ expect(actual_tags).to eq(expected_tags)
169
+ end
170
+
171
+ it 'supports FFT fields' do
172
+ df_expected = record_expected['FFT']
173
+ expect(df_expected).to be_a(::MARC::DataField) # just to be sure
174
+
175
+ df_actual = record_actual['FFT']
176
+ expect(df_actual).to be_a(::MARC::DataField)
177
+ %i[tag indicator1 indicator2].each do |attr|
178
+ v_actual = df_actual.send(attr)
179
+ v_expected = df_expected.send(attr)
180
+ expect(v_actual).to eq(v_expected)
181
+ end
182
+
183
+ df_expected.subfields.each_with_index do |sf_expected, i|
184
+ sf_actual = df_actual.subfields[i]
185
+ expect(sf_actual.code).to eq(sf_expected.code)
186
+ expect(sf_actual.value).to eq(sf_expected.value)
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,157 @@
1
+ <?xml version="1.0"?>
2
+ <collection xmlns="http://www.loc.gov/MARC21/slim" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd">
3
+ <record>
4
+ <leader> Z 22 4500</leader>
5
+ <datafield tag="245" ind1=" " ind2=" ">
6
+ <subfield code="a">Caricatures of Paul Bunyan</subfield>
7
+ </datafield>
8
+ <datafield tag="260" ind1=" " ind2=" ">
9
+ <subfield code="c">19--.</subfield>
10
+ </datafield>
11
+ <datafield tag="300" ind1=" " ind2=" ">
12
+ <subfield code="b">color</subfield>
13
+ <subfield code="c">10 1/2 x 12 1/2 inches.</subfield>
14
+ <subfield code="a">16 caricatures</subfield>
15
+ </datafield>
16
+ <datafield tag="500" ind1=" " ind2=" ">
17
+ <subfield code="a">Title supplied by cataloger.</subfield>
18
+ </datafield>
19
+ <datafield tag="500" ind1=" " ind2=" ">
20
+ <subfield code="a">Published or produced by the Mead Sales Company. Identified artists include Hutton, S.W. Wilcox, and Henry C. Pitz.</subfield>
21
+ </datafield>
22
+ <datafield tag="650" ind1=" " ind2=" ">
23
+ <subfield code="a">Bunyan, Paul (Legendary character) Pictorial works. </subfield>
24
+ </datafield>
25
+ <datafield tag="655" ind1=" " ind2=" ">
26
+ <subfield code="2">gmgpc</subfield>
27
+ <subfield code="a">Caricatures Color. </subfield>
28
+ </datafield>
29
+ <datafield tag="700" ind1="1" ind2=" ">
30
+ <subfield code="a">Pitz, Henry C. 1895-1976. (Henry Clarence),</subfield>
31
+ </datafield>
32
+ <datafield tag="710" ind1="2" ind2=" ">
33
+ <subfield code="a">Mead Sales Company.</subfield>
34
+ </datafield>
35
+ <datafield tag="903" ind1=" " ind2=" ">
36
+ <subfield code="b">c</subfield>
37
+ </datafield>
38
+ <datafield tag="041" ind1=" " ind2=" ">
39
+ <subfield code="a">eng</subfield>
40
+ </datafield>
41
+ <datafield tag="902" ind1=" " ind2=" ">
42
+ <subfield code="d">2022-03-23</subfield>
43
+ </datafield>
44
+ <datafield tag="336" ind1=" " ind2=" ">
45
+ <subfield code="a">Image</subfield>
46
+ </datafield>
47
+ <datafield tag="852" ind1=" " ind2=" ">
48
+ <subfield code="c">Bioscience, Natural Resources &amp; Public Health Library</subfield>
49
+ </datafield>
50
+ <datafield tag="980" ind1=" " ind2=" ">
51
+ <subfield code="a">Forestry</subfield>
52
+ </datafield>
53
+ <datafield tag="982" ind1=" " ind2=" ">
54
+ <subfield code="a">Forestry</subfield>
55
+ <subfield code="b">Forestry</subfield>
56
+ </datafield>
57
+ <datafield tag="901" ind1=" " ind2=" ">
58
+ <subfield code="m">991065640639706532</subfield>
59
+ </datafield>
60
+ <datafield tag="856" ind1="4" ind2="1">
61
+ <subfield code="u">https://search.library.berkeley.edu/discovery/fulldisplay?context=L&amp;vid=01UCS_BER:UCB&amp;docid=alma991065640639706532</subfield>
62
+ <subfield code="y">View library catalog record.</subfield>
63
+ </datafield>
64
+ <datafield tag="998" ind1=" " ind2=" ">
65
+ <subfield code="a">fake-value</subfield>
66
+ </datafield>
67
+ <datafield tag="035" ind1=" " ind2=" ">
68
+ <subfield code="a">b142086125</subfield>
69
+ </datafield>
70
+ <datafield tag="FFT" ind1=" " ind2=" ">
71
+ <subfield code="d">001</subfield>
72
+ <subfield code="a">https://digitalassets.lib.berkeley.edu/forestry/ucb/images/b142086125_i180839998/b142086125_i180839998_001.jpg</subfield>
73
+ </datafield>
74
+ </record>
75
+ <record>
76
+ <leader> Z 22 4500</leader>
77
+ <datafield tag="245" ind1=" " ind2=" ">
78
+ <subfield code="a">Photographs from the University of California Forestry Club</subfield>
79
+ </datafield>
80
+ <datafield tag="260" ind1=" " ind2=" ">
81
+ <subfield code="c">1930-1939.</subfield>
82
+ </datafield>
83
+ <datafield tag="300" ind1=" " ind2=" ">
84
+ <subfield code="a">1 album (photographic prints)</subfield>
85
+ </datafield>
86
+ <datafield tag="500" ind1=" " ind2=" ">
87
+ <subfield code="a">Title supplied by cataloger.</subfield>
88
+ </datafield>
89
+ <datafield tag="500" ind1=" " ind2=" ">
90
+ <subfield code="a">Photographed and or compiled by the Forestry Club.</subfield>
91
+ </datafield>
92
+ <datafield tag="520" ind1=" " ind2=" ">
93
+ <subfield code="a">Album contains views of the University of California campus, Camp Califorest, and of the camp newspaper (Bull of the Woods).</subfield>
94
+ </datafield>
95
+ <datafield tag="610" ind1=" " ind2=" ">
96
+ <subfield code="a">University of California (1868-1952) Forestry Club Pictorial works. </subfield>
97
+ </datafield>
98
+ <datafield tag="610" ind1=" " ind2=" ">
99
+ <subfield code="a">University of California (1868-1952) Camp Califorest Pictorial works. </subfield>
100
+ </datafield>
101
+ <datafield tag="610" ind1=" " ind2=" ">
102
+ <subfield code="a">University of California (1868-1952) Pictorial works. </subfield>
103
+ </datafield>
104
+ <datafield tag="655" ind1=" " ind2=" ">
105
+ <subfield code="2">gmgpc</subfield>
106
+ <subfield code="a">Photographs.</subfield>
107
+ </datafield>
108
+ <datafield tag="710" ind1="2" ind2=" ">
109
+ <subfield code="a">University of California, Berkeley. Forestry students.</subfield>
110
+ </datafield>
111
+ <datafield tag="710" ind1="2" ind2=" ">
112
+ <subfield code="a">University of California (1868-1952) Forestry Club.</subfield>
113
+ </datafield>
114
+ <datafield tag="903" ind1=" " ind2=" ">
115
+ <subfield code="b">c</subfield>
116
+ </datafield>
117
+ <datafield tag="041" ind1=" " ind2=" ">
118
+ <subfield code="a">eng</subfield>
119
+ </datafield>
120
+ <datafield tag="269" ind1=" " ind2=" ">
121
+ <subfield code="a">1930</subfield>
122
+ </datafield>
123
+ <datafield tag="902" ind1=" " ind2=" ">
124
+ <subfield code="d">2022-03-23</subfield>
125
+ </datafield>
126
+ <datafield tag="336" ind1=" " ind2=" ">
127
+ <subfield code="a">Image</subfield>
128
+ </datafield>
129
+ <datafield tag="852" ind1=" " ind2=" ">
130
+ <subfield code="c">Bioscience, Natural Resources &amp; Public Health Library</subfield>
131
+ </datafield>
132
+ <datafield tag="980" ind1=" " ind2=" ">
133
+ <subfield code="a">Forestry</subfield>
134
+ </datafield>
135
+ <datafield tag="982" ind1=" " ind2=" ">
136
+ <subfield code="a">Forestry</subfield>
137
+ <subfield code="b">Forestry</subfield>
138
+ </datafield>
139
+ <datafield tag="901" ind1=" " ind2=" ">
140
+ <subfield code="m">991065707389706532</subfield>
141
+ </datafield>
142
+ <datafield tag="856" ind1="4" ind2="1">
143
+ <subfield code="u">https://search.library.berkeley.edu/discovery/fulldisplay?context=L&amp;vid=01UCS_BER:UCB&amp;docid=alma991065707389706532</subfield>
144
+ <subfield code="y">View library catalog record.</subfield>
145
+ </datafield>
146
+ <datafield tag="998" ind1=" " ind2=" ">
147
+ <subfield code="a">fake-value</subfield>
148
+ </datafield>
149
+ <datafield tag="035" ind1=" " ind2=" ">
150
+ <subfield code="a">b142107827</subfield>
151
+ </datafield>
152
+ <datafield tag="FFT" ind1=" " ind2=" ">
153
+ <subfield code="d">002</subfield>
154
+ <subfield code="a">https://digitalassets.lib.berkeley.edu/forestry/ucb/images/b142086125_i180839998/b142086125_i180839998_002.jpg</subfield>
155
+ </datafield>
156
+ </record>
157
+ </collection>
@@ -0,0 +1,46 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Source: "Batch Uploader: Caveats, common errors and example metadata files", docs.tind.io
4
+ -->
5
+ <collection xmlns="http://www.loc.gov/MARC21/slim">
6
+ <record>
7
+
8
+ <!-- The Leader is encoded in the `000` control field.
9
+ If you want to edit the leader in software such as
10
+ MarcEdit, you will need to change these fields to a
11
+ leader tag. Then, before import into the repository
12
+ you will need to change the fields back to controlfields
13
+ with tag `000`.
14
+ -->
15
+ <controlfield tag="000">00287cam\a2200313\\\4500</controlfield>
16
+
17
+ <!-- All whitespace in control fields need to be replaced with
18
+ backspaces.
19
+ -->
20
+ <controlfield tag="008">190409s2015\\\\xx\\\\\\\\\\\\\\\\\\eng\\</controlfield>
21
+
22
+ <!-- Regular fields are encoded in datafield elements. -->
23
+ <datafield tag="100" ind1="0" ind2=" ">
24
+ <subfield code="a">Aristotle</subfield>
25
+ <subfield code="0">580897</subfield>
26
+ </datafield>
27
+
28
+ <datafield tag="245" ind1="0" ind2="0">
29
+ <subfield code="a">Metaphysics</subfield>
30
+ <subfield code="c">Aristotle</subfield>
31
+ </datafield>
32
+
33
+ <datafield tag="260" ind1=" " ind2=" ">
34
+ <subfield code="a">Narnia</subfield>
35
+ <subfield code="b">Fictive Books</subfield>
36
+ <subfield code="c">2015</subfield>
37
+ </datafield>
38
+
39
+ <!-- Make sure to include a collection when uploading new
40
+ records, so that the record will be searchable.
41
+ -->
42
+ <datafield tag="980" ind1=" " ind2=" ">
43
+ <subfield code="a">BIB</subfield>
44
+ </datafield>
45
+ </record>
46
+ </collection>