berkeley_library-tind 0.4.2 → 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.
@@ -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>