cocina-models 0.75.0 → 0.76.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +40 -12
- data/.rubocop_todo.yml +71 -2
- data/README.md +10 -3
- data/cocina-models.gemspec +2 -0
- data/description_types.yml +165 -38
- data/docs/description_types.md +469 -216
- data/lib/cocina/generator/generator.rb +7 -12
- data/lib/cocina/generator/schema.rb +1 -3
- data/lib/cocina/generator/schema_base.rb +0 -8
- data/lib/cocina/generator/schema_ref.rb +1 -1
- data/lib/cocina/generator/schema_value.rb +14 -4
- data/lib/cocina/models/access.rb +4 -4
- data/lib/cocina/models/admin_policy.rb +1 -1
- data/lib/cocina/models/admin_policy_access_template.rb +7 -7
- data/lib/cocina/models/admin_policy_administrative.rb +1 -1
- data/lib/cocina/models/admin_policy_with_metadata.rb +3 -3
- data/lib/cocina/models/builders/name_title_group_builder.rb +0 -4
- data/lib/cocina/models/builders/title_builder.rb +0 -2
- data/lib/cocina/models/citation_only_access.rb +2 -2
- data/lib/cocina/models/collection_access.rb +4 -4
- data/lib/cocina/models/collection_identification.rb +1 -1
- data/lib/cocina/models/collection_with_metadata.rb +2 -2
- data/lib/cocina/models/contributor.rb +4 -4
- data/lib/cocina/models/controlled_digital_lending_access.rb +2 -2
- data/lib/cocina/models/dark_access.rb +4 -4
- data/lib/cocina/models/description.rb +3 -3
- data/lib/cocina/models/descriptive_basic_value.rb +13 -13
- data/lib/cocina/models/descriptive_parallel_contributor.rb +5 -5
- data/lib/cocina/models/descriptive_parallel_event.rb +3 -3
- data/lib/cocina/models/descriptive_value.rb +13 -13
- data/lib/cocina/models/descriptive_value_language.rb +6 -6
- data/lib/cocina/models/dro.rb +1 -1
- data/lib/cocina/models/dro_access.rb +8 -8
- data/lib/cocina/models/dro_with_metadata.rb +3 -3
- data/lib/cocina/models/embargo.rb +5 -5
- data/lib/cocina/models/event.rb +3 -3
- data/lib/cocina/models/file.rb +4 -4
- data/lib/cocina/models/file_access.rb +4 -4
- data/lib/cocina/models/identification.rb +2 -2
- data/lib/cocina/models/language.rb +12 -12
- data/lib/cocina/models/location_based_access.rb +1 -1
- data/lib/cocina/models/location_based_download_access.rb +1 -1
- data/lib/cocina/models/mapping/error_notifier.rb +36 -0
- data/lib/cocina/models/mapping/from_mods/access.rb +177 -0
- data/lib/cocina/models/mapping/from_mods/admin_metadata.rb +217 -0
- data/lib/cocina/models/mapping/from_mods/alt_rep_group.rb +26 -0
- data/lib/cocina/models/mapping/from_mods/authority.rb +51 -0
- data/lib/cocina/models/mapping/from_mods/contributor.rb +161 -0
- data/lib/cocina/models/mapping/from_mods/description.rb +99 -0
- data/lib/cocina/models/mapping/from_mods/description_builder.rb +61 -0
- data/lib/cocina/models/mapping/from_mods/event.rb +543 -0
- data/lib/cocina/models/mapping/from_mods/form.rb +381 -0
- data/lib/cocina/models/mapping/from_mods/geographic.rb +219 -0
- data/lib/cocina/models/mapping/from_mods/hydrus_default_title_builder.rb +28 -0
- data/lib/cocina/models/mapping/from_mods/identifier.rb +51 -0
- data/lib/cocina/models/mapping/from_mods/identifier_builder.rb +71 -0
- data/lib/cocina/models/mapping/from_mods/identifier_type.rb +292 -0
- data/lib/cocina/models/mapping/from_mods/language.rb +36 -0
- data/lib/cocina/models/mapping/from_mods/language_script.rb +30 -0
- data/lib/cocina/models/mapping/from_mods/language_term.rb +106 -0
- data/lib/cocina/models/mapping/from_mods/name_builder.rb +307 -0
- data/lib/cocina/models/mapping/from_mods/note.rb +162 -0
- data/lib/cocina/models/mapping/from_mods/part_builder.rb +147 -0
- data/lib/cocina/models/mapping/from_mods/primary.rb +27 -0
- data/lib/cocina/models/mapping/from_mods/purl.rb +53 -0
- data/lib/cocina/models/mapping/from_mods/related_resource.rb +105 -0
- data/lib/cocina/models/mapping/from_mods/subject.rb +413 -0
- data/lib/cocina/models/mapping/from_mods/subject_authority_codes.rb +794 -0
- data/lib/cocina/models/mapping/from_mods/title.rb +160 -0
- data/lib/cocina/models/mapping/from_mods/title_builder.rb +106 -0
- data/lib/cocina/models/mapping/from_mods/title_builder_strategy.rb +19 -0
- data/lib/cocina/models/mapping/from_mods/value_uri.rb +25 -0
- data/lib/cocina/models/mapping/normalizers/base.rb +16 -0
- data/lib/cocina/models/mapping/normalizers/mods/geo_extension_normalizer.rb +69 -0
- data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +191 -0
- data/lib/cocina/models/mapping/normalizers/mods/origin_info_normalizer.rb +157 -0
- data/lib/cocina/models/mapping/normalizers/mods/subject_normalizer.rb +296 -0
- data/lib/cocina/models/mapping/normalizers/mods/title_normalizer.rb +91 -0
- data/lib/cocina/models/mapping/normalizers/mods_normalizer.rb +409 -0
- data/lib/cocina/models/mapping/purl.rb +28 -0
- data/lib/cocina/models/mapping/to_mods/access.rb +155 -0
- data/lib/cocina/models/mapping/to_mods/admin_metadata.rb +129 -0
- data/lib/cocina/models/mapping/to_mods/contributor.rb +49 -0
- data/lib/cocina/models/mapping/to_mods/description.rb +63 -0
- data/lib/cocina/models/mapping/to_mods/event.rb +200 -0
- data/lib/cocina/models/mapping/to_mods/form.rb +292 -0
- data/lib/cocina/models/mapping/to_mods/geographic.rb +151 -0
- data/lib/cocina/models/mapping/to_mods/id_generator.rb +25 -0
- data/lib/cocina/models/mapping/to_mods/identifier.rb +57 -0
- data/lib/cocina/models/mapping/to_mods/language.rb +82 -0
- data/lib/cocina/models/mapping/to_mods/mods_writer.rb +38 -0
- data/lib/cocina/models/mapping/to_mods/name_title_group.rb +29 -0
- data/lib/cocina/models/mapping/to_mods/name_writer.rb +228 -0
- data/lib/cocina/models/mapping/to_mods/note.rb +105 -0
- data/lib/cocina/models/mapping/to_mods/part_writer.rb +115 -0
- data/lib/cocina/models/mapping/to_mods/related_resource.rb +108 -0
- data/lib/cocina/models/mapping/to_mods/role_writer.rb +50 -0
- data/lib/cocina/models/mapping/to_mods/subject.rb +486 -0
- data/lib/cocina/models/mapping/to_mods/title.rb +260 -0
- data/lib/cocina/models/object_metadata.rb +2 -2
- data/lib/cocina/models/presentation.rb +2 -2
- data/lib/cocina/models/related_resource.rb +9 -9
- data/lib/cocina/models/release_tag.rb +4 -4
- data/lib/cocina/models/request_admin_policy.rb +1 -1
- data/lib/cocina/models/request_administrative.rb +1 -1
- data/lib/cocina/models/request_collection.rb +2 -2
- data/lib/cocina/models/request_description.rb +3 -3
- data/lib/cocina/models/request_dro.rb +4 -4
- data/lib/cocina/models/request_file.rb +5 -5
- data/lib/cocina/models/request_identification.rb +1 -1
- data/lib/cocina/models/sequence.rb +1 -1
- data/lib/cocina/models/source.rb +4 -4
- data/lib/cocina/models/standard.rb +5 -5
- data/lib/cocina/models/stanford_access.rb +2 -2
- data/lib/cocina/models/title.rb +13 -13
- data/lib/cocina/models/validators/dark_validator.rb +4 -2
- data/lib/cocina/models/validators/open_api_validator.rb +0 -4
- data/lib/cocina/models/version.rb +1 -1
- data/lib/cocina/models/world_access.rb +2 -2
- data/lib/cocina/models.rb +4 -0
- data/lib/cocina/rspec/factories.rb +157 -0
- data/lib/cocina/rspec.rb +2 -0
- data/openapi.yml +4 -4
- metadata +88 -3
- data/docs/_config.yml +0 -1
@@ -0,0 +1,543 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module FromMods
|
7
|
+
# Maps originInfo to cocina events
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
9
|
+
class Event
|
10
|
+
# key: MODS date element name
|
11
|
+
# value: cocina date type
|
12
|
+
DATE_ELEMENTS_2_TYPE = {
|
13
|
+
'copyrightDate' => 'copyright',
|
14
|
+
'dateCaptured' => 'capture',
|
15
|
+
'dateCreated' => 'creation',
|
16
|
+
'dateIssued' => 'publication',
|
17
|
+
'dateModified' => 'modification',
|
18
|
+
'dateOther' => '', # cocina type is set differently for dateOther
|
19
|
+
'dateValid' => 'validity'
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
# a preferred vocabulary, if you will
|
23
|
+
EVENT_TYPES = [
|
24
|
+
'acquisition',
|
25
|
+
'capture',
|
26
|
+
'collection',
|
27
|
+
'copyright',
|
28
|
+
'creation',
|
29
|
+
'degree conferral',
|
30
|
+
'development',
|
31
|
+
'distribution',
|
32
|
+
'generation',
|
33
|
+
'manufacture',
|
34
|
+
'modification',
|
35
|
+
'performance',
|
36
|
+
'presentation',
|
37
|
+
'production',
|
38
|
+
'publication',
|
39
|
+
'recording',
|
40
|
+
'release',
|
41
|
+
'submission',
|
42
|
+
'validity',
|
43
|
+
'withdrawal'
|
44
|
+
].freeze
|
45
|
+
|
46
|
+
# because eventType is a relatively new addition to the MODS schema, records converted from MARC to MODS prior
|
47
|
+
# to its introduction used displayLabel as a stopgap measure.
|
48
|
+
# These are the displayLabel values that should be converted to eventType instead of displayLabel.
|
49
|
+
# These values were also sometimes used as eventType values themselves, and will be converted to our preferred vocab.
|
50
|
+
LEGACY_EVENT_TYPES_2_TYPE = {
|
51
|
+
'distributor' => 'distribution',
|
52
|
+
'manufacturer' => 'manufacture',
|
53
|
+
'producer' => 'production',
|
54
|
+
'publisher' => 'publication'
|
55
|
+
}.freeze
|
56
|
+
|
57
|
+
# @param [Nokogiri::XML::Element] resource_element mods or relatedItem element
|
58
|
+
# @param [Cocina::Models::Mapping::FromMods::DescriptionBuilder] description_builder
|
59
|
+
# @param [String] purl
|
60
|
+
# @return [Hash] a hash that can be mapped to a cocina model
|
61
|
+
def self.build(resource_element:, description_builder:, purl: nil)
|
62
|
+
new(resource_element: resource_element, description_builder: description_builder).build
|
63
|
+
end
|
64
|
+
|
65
|
+
def initialize(resource_element:, description_builder:)
|
66
|
+
@resource_element = resource_element
|
67
|
+
@notifier = description_builder.notifier
|
68
|
+
end
|
69
|
+
|
70
|
+
def build
|
71
|
+
altrepgroup_origin_info_nodes, other_origin_info_nodes = AltRepGroup.split(nodes: resource_element.xpath(
|
72
|
+
'mods:originInfo', mods: Description::DESC_METADATA_NS
|
73
|
+
))
|
74
|
+
|
75
|
+
results = build_grouped_origin_infos(altrepgroup_origin_info_nodes) + build_ungrouped_origin_infos(other_origin_info_nodes)
|
76
|
+
results if results.present? && results.first.present? # avoid [{}] case
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
attr_reader :resource_element, :notifier
|
82
|
+
|
83
|
+
def build_ungrouped_origin_infos(origin_infos)
|
84
|
+
origin_infos.filter_map do |origin_info|
|
85
|
+
next if origin_info.content.blank? &&
|
86
|
+
origin_info.xpath('.//*[@valueURI]').empty? &&
|
87
|
+
origin_info.xpath('.//*[@xlink:href]', xlink: Description::XLINK_NS).empty?
|
88
|
+
|
89
|
+
build_event_for_origin_info(origin_info)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def build_event_for_origin_info(origin_info_node)
|
94
|
+
return build_copyright_notice_event(origin_info_node) if origin_info_node['eventType'] == 'copyright notice'
|
95
|
+
|
96
|
+
event = {
|
97
|
+
type: event_type(origin_info_node),
|
98
|
+
displayLabel: display_label(origin_info_node),
|
99
|
+
valueLanguage: LanguageScript.build(node: origin_info_node)
|
100
|
+
}
|
101
|
+
add_info_to_event(event, origin_info_node)
|
102
|
+
event.compact
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_grouped_origin_infos(grouped_origin_infos)
|
106
|
+
grouped_origin_infos.map do |origin_info_nodes|
|
107
|
+
common_event_type = event_type_in_common(origin_info_nodes)
|
108
|
+
common_display_label = display_label_in_common(origin_info_nodes)
|
109
|
+
|
110
|
+
parallel_event = {
|
111
|
+
type: common_event_type,
|
112
|
+
displayLabel: common_display_label,
|
113
|
+
parallelEvent: build_parallel_origin_infos(origin_info_nodes, common_event_type,
|
114
|
+
common_display_label)
|
115
|
+
}
|
116
|
+
|
117
|
+
parallel_event.compact
|
118
|
+
end.flatten
|
119
|
+
end
|
120
|
+
|
121
|
+
# For parallelEvent items, the valueLanguage construct is at the same level as the rest
|
122
|
+
# of the event attributes, rather than inside each event attribute
|
123
|
+
def build_parallel_origin_infos(origin_infos, common_event_type, common_display_label)
|
124
|
+
origin_infos.flat_map do |origin_info|
|
125
|
+
event = build_event_for_parallel_origin_info(origin_info)
|
126
|
+
event[:valueLanguage] = LanguageScript.build(node: origin_info)
|
127
|
+
event[:type] = display_label(origin_info) if common_event_type.blank?
|
128
|
+
event[:displayLabel] = display_label(origin_info) if common_display_label.blank?
|
129
|
+
|
130
|
+
event.compact
|
131
|
+
end.compact
|
132
|
+
end
|
133
|
+
|
134
|
+
def build_event_for_parallel_origin_info(origin_info_node)
|
135
|
+
return build_copyright_notice_event(origin_info_node) if origin_info_node['eventType'] == 'copyright notice'
|
136
|
+
|
137
|
+
event = {}
|
138
|
+
add_info_to_event(event, origin_info_node)
|
139
|
+
event.compact
|
140
|
+
end
|
141
|
+
|
142
|
+
# @return String type for the cocina event if it is the same for all the origin_info_nodes, o.w. nil
|
143
|
+
def event_type_in_common(origin_info_nodes)
|
144
|
+
raw_type = origin_info_nodes.first['eventType']
|
145
|
+
return if raw_type.blank?
|
146
|
+
|
147
|
+
first_event_type = event_type(origin_info_nodes.first)
|
148
|
+
return first_event_type if origin_info_nodes.all? { |node| event_type(node) == first_event_type }
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return String displayLabel for the cocina event if it is the same for all the origin_info_nodes, o.w. nil
|
152
|
+
def display_label_in_common(origin_info_nodes)
|
153
|
+
raw_label = origin_info_nodes.first['displayLabel']
|
154
|
+
return if raw_label.blank?
|
155
|
+
|
156
|
+
first_label = display_label(origin_info_nodes.first)
|
157
|
+
return first_label if origin_info_nodes.all? { |node| display_label(node) == first_label }
|
158
|
+
end
|
159
|
+
|
160
|
+
def add_info_to_event(event, origin_info_node)
|
161
|
+
place_nodes = origin_info_node.xpath('mods:place', mods: Description::DESC_METADATA_NS)
|
162
|
+
add_place_info(event, place_nodes) if place_nodes.present?
|
163
|
+
|
164
|
+
publisher = origin_info_node.xpath('mods:publisher', mods: Description::DESC_METADATA_NS)
|
165
|
+
add_publisher_info(event, publisher, origin_info_node) if publisher.present?
|
166
|
+
|
167
|
+
issuance = origin_info_node.xpath('mods:issuance', mods: Description::DESC_METADATA_NS)
|
168
|
+
add_issuance_note(event, issuance) if issuance.present?
|
169
|
+
|
170
|
+
edition = origin_info_node.xpath('mods:edition', mods: Description::DESC_METADATA_NS)
|
171
|
+
add_edition_info(event, edition) if edition.present?
|
172
|
+
|
173
|
+
frequency = origin_info_node.xpath('mods:frequency', mods: Description::DESC_METADATA_NS)
|
174
|
+
add_frequency_info(event, frequency) if frequency.present?
|
175
|
+
|
176
|
+
date_values = build_date_values(origin_info_node)
|
177
|
+
event[:date] = date_values if date_values.present?
|
178
|
+
end
|
179
|
+
|
180
|
+
XPATH_HAS_CONTENT_PREDICATE = '[string-length(normalize-space()) > 0]'
|
181
|
+
|
182
|
+
def build_copyright_notice_event(origin_info_node)
|
183
|
+
date_nodes = origin_info_node.xpath("mods:copyrightDate#{XPATH_HAS_CONTENT_PREDICATE}",
|
184
|
+
mods: Description::DESC_METADATA_NS)
|
185
|
+
return if date_nodes.blank?
|
186
|
+
|
187
|
+
{
|
188
|
+
type: 'copyright notice',
|
189
|
+
note: [
|
190
|
+
{
|
191
|
+
value: date_nodes.first.content,
|
192
|
+
type: 'copyright statement'
|
193
|
+
}
|
194
|
+
]
|
195
|
+
}
|
196
|
+
end
|
197
|
+
|
198
|
+
def build_date_values(origin_info_node)
|
199
|
+
date_values = []
|
200
|
+
DATE_ELEMENTS_2_TYPE.each do |mods_el_name, cocina_type|
|
201
|
+
date_values << build_date_desc_values(mods_el_name, origin_info_node, cocina_type)
|
202
|
+
end
|
203
|
+
date_values.flatten.compact
|
204
|
+
end
|
205
|
+
|
206
|
+
def build_date_desc_values(mods_date_el_name, origin_info_node, default_type)
|
207
|
+
date_nodes = origin_info_node.xpath("mods:#{mods_date_el_name}#{XPATH_HAS_CONTENT_PREDICATE}",
|
208
|
+
mods: Description::DESC_METADATA_NS)
|
209
|
+
if mods_date_el_name == 'dateOther' && date_nodes.present?
|
210
|
+
date_other_type = date_other_type_attr(origin_info_node['eventType'], date_nodes.first)
|
211
|
+
date_values_for_event(date_nodes, date_other_type)
|
212
|
+
else
|
213
|
+
date_values_for_event(date_nodes, default_type)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# encapsulate where warnings are given for dateOther@type
|
218
|
+
# per Arcadia: no date type/no event type warns 'undetermined date type'
|
219
|
+
def date_other_type_attr(event_type, date_other_node)
|
220
|
+
date_type = date_other_node['type']
|
221
|
+
notifier.warn('Undetermined date type') if date_type.blank? && event_type.blank?
|
222
|
+
date_type
|
223
|
+
end
|
224
|
+
|
225
|
+
def date_values_for_event(date_nodes, default_type)
|
226
|
+
dates = date_nodes.reject { |node| node['point'] }.map do |node|
|
227
|
+
addl_attributes = {}
|
228
|
+
# NOTE: only dateOther should have type attribute; not sure if we have dirty data in this respect.
|
229
|
+
# If so, it's invalid MODS, so validating against the MODS schema will catch it
|
230
|
+
addl_attributes[:type] = node['type'] if node['type'].present?
|
231
|
+
build_date(node).merge(addl_attributes)
|
232
|
+
end
|
233
|
+
|
234
|
+
points = date_nodes.select { |node| node['point'] }
|
235
|
+
points_date = build_structured_date(points)
|
236
|
+
dates << points_date if points_date
|
237
|
+
|
238
|
+
dates.compact!
|
239
|
+
dates.each { |date| date[:type] = default_type if date[:type].blank? && default_type.present? }
|
240
|
+
end
|
241
|
+
|
242
|
+
# map legacy event types, encapsulate where warnings are given for originInfo@eventType
|
243
|
+
# per Arcadia: unknown event type/any date type warns 'unrecognized event type'
|
244
|
+
# NOTE: Do any eventType/displayLabel transformations before determining contributor role
|
245
|
+
def event_type(origin_info_node)
|
246
|
+
event_type = origin_info_node['eventType']
|
247
|
+
event_type = origin_info_node['displayLabel'] if event_type.blank? &&
|
248
|
+
LEGACY_EVENT_TYPES_2_TYPE.key?(origin_info_node['displayLabel'])
|
249
|
+
event_type = LEGACY_EVENT_TYPES_2_TYPE[event_type] if LEGACY_EVENT_TYPES_2_TYPE.key?(event_type)
|
250
|
+
|
251
|
+
return if event_type.blank?
|
252
|
+
|
253
|
+
notifier.warn('Unrecognized event type') unless EVENT_TYPES.include?(event_type)
|
254
|
+
event_type
|
255
|
+
end
|
256
|
+
|
257
|
+
def display_label(origin_info_node)
|
258
|
+
origin_info_node[:displayLabel] if origin_info_node[:displayLabel].present? &&
|
259
|
+
!LEGACY_EVENT_TYPES_2_TYPE.key?(origin_info_node[:displayLabel])
|
260
|
+
end
|
261
|
+
|
262
|
+
# placeTerm can have type=code or type=text or neither; placeTerms of type code and text may combine into a single
|
263
|
+
# cocina location (when under the same place element), or they might refer to separate cocina locations (under separate place elements)
|
264
|
+
def add_place_info(event, place_nodes)
|
265
|
+
return unless place_nodes_have_info?(place_nodes)
|
266
|
+
|
267
|
+
# text only and text-and-code placeTerm types in single place node
|
268
|
+
text_places = place_nodes.select do |place|
|
269
|
+
place.xpath("mods:placeTerm[not(@type='code')]", mods: Description::DESC_METADATA_NS).present?
|
270
|
+
end
|
271
|
+
code_only_places = place_nodes.reject { |place| text_places.include?(place) }
|
272
|
+
|
273
|
+
event[:location] =
|
274
|
+
locations_for_place_terms_with_text(text_places) + locations_for_code_only_place_terms(code_only_places)
|
275
|
+
event[:location].compact!
|
276
|
+
end
|
277
|
+
|
278
|
+
def place_nodes_have_info?(place_nodes)
|
279
|
+
return true if place_nodes.any? { |node| node.content.present? }
|
280
|
+
return true if place_nodes.any? do |node|
|
281
|
+
node.xpath('mods:placeTerm[@valueURI]', mods: Description::DESC_METADATA_NS).present?
|
282
|
+
end
|
283
|
+
|
284
|
+
place_nodes.any? do |node|
|
285
|
+
node.xpath('mods:placeTerm[@xlink:href]', { mods: Description::DESC_METADATA_NS, xlink: Description::XLINK_NS }).present?
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# @param [Nokogiri::XML::NodeSet] place elements that have at least one placeTerm child of type text
|
290
|
+
# @return cocina locations
|
291
|
+
def locations_for_place_terms_with_text(place_nodes)
|
292
|
+
place_nodes.map do |place_node|
|
293
|
+
text_place_term_node = place_node.xpath("mods:placeTerm[not(@type='code')]",
|
294
|
+
mods: Description::DESC_METADATA_NS).first
|
295
|
+
next if text_place_term_node.text.blank?
|
296
|
+
|
297
|
+
cocina_location = {}
|
298
|
+
add_authority_info(cocina_location, text_place_term_node)
|
299
|
+
cocina_location[:value] = text_place_term_node.text
|
300
|
+
code_place_term_node = place_node.xpath("mods:placeTerm[@type='code']", mods: Description::DESC_METADATA_NS).first
|
301
|
+
if code_place_term_node
|
302
|
+
cocina_location[:code] = code_place_term_node.text
|
303
|
+
# NOTE: deliberately skipping situation where text node has some authority info and code node
|
304
|
+
# has other authority info as we may never encounter this
|
305
|
+
if cocina_location[:source].blank? && cocina_location[:uri].blank?
|
306
|
+
add_authority_info(cocina_location,
|
307
|
+
code_place_term_node)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
lang_script = LanguageScript.build(node: text_place_term_node)
|
311
|
+
cocina_location[:valueLanguage] = lang_script if lang_script
|
312
|
+
cocina_location[:type] = 'supplied' if place_node[:supplied] == 'yes'
|
313
|
+
cocina_location.compact
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# @param [Nokogiri::XML::NodeSet] place elements that have placeTerm children ONLY of type code
|
318
|
+
# @return cocina locations
|
319
|
+
def locations_for_code_only_place_terms(place_nodes)
|
320
|
+
place_nodes.map do |place_node|
|
321
|
+
code_place_term_node = place_node.xpath("mods:placeTerm[@type='code']", mods: Description::DESC_METADATA_NS).first
|
322
|
+
next if code_place_term_node.content.blank?
|
323
|
+
|
324
|
+
cocina_location = {}
|
325
|
+
add_authority_info(cocina_location, code_place_term_node)
|
326
|
+
if cocina_location.empty?
|
327
|
+
notifier.warn('Place code missing authority',
|
328
|
+
{ code: code_place_term_node.text })
|
329
|
+
end
|
330
|
+
|
331
|
+
cocina_location[:code] = code_place_term_node.text
|
332
|
+
cocina_location[:type] = 'supplied' if place_node[:supplied] == 'yes'
|
333
|
+
cocina_location.compact
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def add_issuance_note(event, issuance_nodes)
|
338
|
+
return if issuance_nodes.empty?
|
339
|
+
|
340
|
+
event[:note] ||= []
|
341
|
+
issuance_nodes.each do |issuance|
|
342
|
+
next if issuance.text.blank?
|
343
|
+
|
344
|
+
event[:note] << {
|
345
|
+
source: { value: 'MODS issuance terms' },
|
346
|
+
type: 'issuance',
|
347
|
+
value: issuance.text
|
348
|
+
}.compact
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def add_frequency_info(event, freq_nodes)
|
353
|
+
return if freq_nodes.empty?
|
354
|
+
|
355
|
+
event[:note] ||= []
|
356
|
+
freq_nodes.each do |frequency|
|
357
|
+
next if frequency.text.blank?
|
358
|
+
|
359
|
+
note = {
|
360
|
+
type: 'frequency',
|
361
|
+
value: frequency.text,
|
362
|
+
valueLanguage: LanguageScript.build(node: frequency)
|
363
|
+
}
|
364
|
+
add_authority_info(note, frequency).compact
|
365
|
+
event[:note] << note.compact
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def add_edition_info(event, edition_nodes)
|
370
|
+
return if edition_nodes.empty?
|
371
|
+
|
372
|
+
event[:note] ||= []
|
373
|
+
edition_nodes.each do |edition|
|
374
|
+
next if edition.text.blank?
|
375
|
+
|
376
|
+
event[:note] << {
|
377
|
+
type: 'edition',
|
378
|
+
value: edition.text,
|
379
|
+
valueLanguage: LanguageScript.build(node: edition)
|
380
|
+
}.compact
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def add_publisher_info(event, publisher_nodes, origin_info_node)
|
385
|
+
return if publisher_nodes.empty?
|
386
|
+
|
387
|
+
event[:contributor] ||= []
|
388
|
+
publisher_nodes.each do |publisher_node|
|
389
|
+
next if publisher_node.text.blank?
|
390
|
+
|
391
|
+
event[:contributor] << {
|
392
|
+
name: [
|
393
|
+
{
|
394
|
+
value: publisher_node.text,
|
395
|
+
valueLanguage: LanguageScript.build(node: publisher_node)
|
396
|
+
}.tap do |attrs|
|
397
|
+
if origin_info_node['transliteration']
|
398
|
+
attrs[:type] = 'transliteration'
|
399
|
+
attrs[:standard] = { value: origin_info_node['transliteration'] }
|
400
|
+
end
|
401
|
+
if publisher_node['transliteration']
|
402
|
+
attrs[:type] = 'transliteration'
|
403
|
+
attrs[:standard] = { value: publisher_node['transliteration'] }
|
404
|
+
end
|
405
|
+
end.compact
|
406
|
+
],
|
407
|
+
role: [role_for(event)],
|
408
|
+
type: 'organization'
|
409
|
+
}.compact
|
410
|
+
end
|
411
|
+
|
412
|
+
event.delete(:contributor) if event[:contributor].empty?
|
413
|
+
end
|
414
|
+
|
415
|
+
def add_authority_info(cocina_desc_val, xml_node)
|
416
|
+
cocina_desc_val[:uri] = ValueURI.sniff(xml_node['valueURI'], notifier) if xml_node['valueURI']
|
417
|
+
source = {
|
418
|
+
code: Authority.normalize_code(xml_node['authority'], notifier),
|
419
|
+
uri: Authority.normalize_uri(xml_node['authorityURI'])
|
420
|
+
}.compact
|
421
|
+
cocina_desc_val[:source] = source if source.present?
|
422
|
+
cocina_desc_val
|
423
|
+
end
|
424
|
+
|
425
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
426
|
+
def build_structured_date(date_nodes)
|
427
|
+
return if date_nodes.blank?
|
428
|
+
|
429
|
+
common_attribs = common_date_attributes(date_nodes)
|
430
|
+
|
431
|
+
remove_dup_key_date_from_end_point(date_nodes)
|
432
|
+
dates = date_nodes.map do |node|
|
433
|
+
next if node.text.blank? && node.attributes.empty?
|
434
|
+
|
435
|
+
new_node = node.deep_dup
|
436
|
+
new_node.remove_attribute('encoding') if common_attribs[:encoding].present? || node[:encoding]&.size&.zero?
|
437
|
+
new_node.remove_attribute('qualifier') if common_attribs[:qualifier].present? || node[:qualifier]&.size&.zero?
|
438
|
+
build_date(new_node)
|
439
|
+
end
|
440
|
+
{ structuredValue: dates }.merge(common_attribs).compact
|
441
|
+
end
|
442
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
443
|
+
|
444
|
+
# Per Arcadia, keyDate should only appear once in an originInfo.
|
445
|
+
# If keyDate is on a date of type point and is on both the start and end points, then
|
446
|
+
# it should be removed from the end point
|
447
|
+
def remove_dup_key_date_from_end_point(date_nodes)
|
448
|
+
key_date_point_nodes = date_nodes.select { |node| node['keyDate'] == 'yes' && node['point'].present? }
|
449
|
+
return unless key_date_point_nodes.size == 2
|
450
|
+
|
451
|
+
end_node = key_date_point_nodes.find { |node| node['point'] == 'end' }
|
452
|
+
end_node.delete('keyDate')
|
453
|
+
end
|
454
|
+
|
455
|
+
def common_date_attributes(date_nodes)
|
456
|
+
first_encoding = date_nodes.first['encoding']
|
457
|
+
first_qualifier = date_nodes.first['qualifier']
|
458
|
+
encoding_is_common = date_nodes.all? { |node| node['encoding'] == first_encoding }
|
459
|
+
qualifier_is_common = date_nodes.all? { |node| node['qualifier'] == first_qualifier }
|
460
|
+
attribs = {}
|
461
|
+
attribs[:qualifier] = first_qualifier if qualifier_is_common && first_qualifier.present?
|
462
|
+
attribs[:encoding] = { code: first_encoding } if encoding_is_common && first_encoding.present?
|
463
|
+
attribs.compact
|
464
|
+
end
|
465
|
+
|
466
|
+
def build_date(date_node)
|
467
|
+
{}.tap do |date|
|
468
|
+
date[:value] = clean_date(date_node.text) if date_node.text.present?
|
469
|
+
date[:encoding] = { code: date_node['encoding'] } if date_node['encoding']
|
470
|
+
date[:status] = 'primary' if date_node['keyDate']
|
471
|
+
date[:note] = build_date_note(date_node)
|
472
|
+
date[:qualifier] = date_node['qualifier'] if date_node['qualifier'].present?
|
473
|
+
date[:type] = date_node['point'] if date_node['point'].present?
|
474
|
+
date[:valueLanguage] = LanguageScript.build(node: date_node)
|
475
|
+
end.compact
|
476
|
+
end
|
477
|
+
|
478
|
+
def build_date_note(date_node)
|
479
|
+
return if date_node['calendar'].blank?
|
480
|
+
|
481
|
+
[
|
482
|
+
{
|
483
|
+
value: date_node['calendar'],
|
484
|
+
type: 'calendar'
|
485
|
+
}
|
486
|
+
]
|
487
|
+
end
|
488
|
+
|
489
|
+
def clean_date(date)
|
490
|
+
date.delete_suffix('.')
|
491
|
+
end
|
492
|
+
|
493
|
+
# NOTE: Do any eventType/displayLabel transformations before determining role (i.e. with LEGACY_EVENT_TYPES_2_TYPE)
|
494
|
+
def role_for(event)
|
495
|
+
case event[:type]
|
496
|
+
when 'distribution'
|
497
|
+
{
|
498
|
+
value: 'distributor',
|
499
|
+
code: 'dst',
|
500
|
+
uri: 'http://id.loc.gov/vocabulary/relators/dst',
|
501
|
+
source: {
|
502
|
+
code: 'marcrelator',
|
503
|
+
uri: 'http://id.loc.gov/vocabulary/relators/'
|
504
|
+
}
|
505
|
+
}
|
506
|
+
when 'manufacture'
|
507
|
+
{
|
508
|
+
value: 'manufacturer',
|
509
|
+
code: 'mfr',
|
510
|
+
uri: 'http://id.loc.gov/vocabulary/relators/mfr',
|
511
|
+
source: {
|
512
|
+
code: 'marcrelator',
|
513
|
+
uri: 'http://id.loc.gov/vocabulary/relators/'
|
514
|
+
}
|
515
|
+
}
|
516
|
+
when 'production'
|
517
|
+
{
|
518
|
+
value: 'creator',
|
519
|
+
code: 'cre',
|
520
|
+
uri: 'http://id.loc.gov/vocabulary/relators/cre',
|
521
|
+
source: {
|
522
|
+
code: 'marcrelator',
|
523
|
+
uri: 'http://id.loc.gov/vocabulary/relators/'
|
524
|
+
}
|
525
|
+
}
|
526
|
+
else
|
527
|
+
{
|
528
|
+
value: 'publisher',
|
529
|
+
code: 'pbl',
|
530
|
+
uri: 'http://id.loc.gov/vocabulary/relators/pbl',
|
531
|
+
source: {
|
532
|
+
code: 'marcrelator',
|
533
|
+
uri: 'http://id.loc.gov/vocabulary/relators/'
|
534
|
+
}
|
535
|
+
}
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
# rubocop:enable Metrics/ClassLength
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|