Skip to content

Commit b78eb32

Browse files
committed
Merge OAI-PMH ingestors and autodetect Bioschemas support
1 parent 664b1ae commit b78eb32

3 files changed

Lines changed: 155 additions & 164 deletions

File tree

lib/ingestors/ingestor_factory.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ def self.ingestors
1010
Ingestors::MaterialCsvIngestor,
1111
Ingestors::TessEventIngestor,
1212
Ingestors::ZenodoIngestor,
13-
Ingestors::OaiPmhIngestor,
14-
Ingestors::OaiPmhBioschemasIngestor
13+
Ingestors::OaiPmhIngestor
1514
] + taxila_ingestors + llm_ingestors
1615
end
1716

lib/ingestors/oai_pmh_bioschemas_ingestor.rb

Lines changed: 0 additions & 157 deletions
This file was deleted.

lib/ingestors/oai_pmh_ingestor.rb

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
require 'open-uri'
22
require 'tess_rdf_extractors'
3-
require 'oai'
4-
require 'nokogiri'
53

64
module Ingestors
75
class OaiPmhIngestor < Ingestor
@@ -18,15 +16,25 @@ def self.config
1816
}
1917
end
2018

19+
def read(source_url)
20+
client = OAI::Client.new source_url, headers: { 'From' => config[:mail] }
21+
found_bioschemas = begin
22+
read_oai_rdf(client)
23+
rescue OAI::ArgumentException
24+
false
25+
end
26+
27+
read_oai_default(client) unless found_bioschemas
28+
end
29+
2130
def ns
2231
{
2332
'dc' => 'http://purl.org/dc/elements/1.1/',
2433
'oai_dc' => 'http://www.openarchives.org/OAI/2.0/oai_dc/'
2534
}
2635
end
2736

28-
def read(source_url)
29-
client = OAI::Client.new source_url, headers: { 'From' => config[:mail] }
37+
def read_oai_default(client)
3038
count = 0
3139
client.list_records.full.each do |record|
3240
read_dublin_core(record.metadata.to_s)
@@ -52,7 +60,9 @@ def read_dublin_core_material(xml_doc)
5260
material.description = convert_description(xml_doc.at_xpath('//dc:description', ns)&.text)
5361
material.authors = xml_doc.xpath('//dc:creator', ns).map(&:text)
5462
material.contributors = xml_doc.xpath('//dc:contributor', ns).map(&:text)
55-
material.licence = xml_doc.at_xpath('//dc:rights', ns)&.text
63+
64+
rights = xml_doc.xpath('//dc:rights', ns).map { |n| n.text&.strip }.reject(&:empty?)
65+
material.licence = rights.find { |r| r.start_with?('http://', 'https://') } || rights.first || 'notspecified'
5666

5767
dates = xml_doc.xpath('//dc:date', ns).map(&:text)
5868
parsed_dates = dates.map do |d|
@@ -100,5 +110,144 @@ def read_dublin_core_event(_xml_doc)
100110

101111
add_event event
102112
end
113+
114+
def read_oai_rdf(client)
115+
provider_events = []
116+
provider_materials = []
117+
totals = Hash.new(0)
118+
119+
client.list_records(metadata_prefix: 'rdf').full.each do |record|
120+
metadata_tag = Nokogiri::XML(record.metadata.to_s)
121+
bioschemas_xml = metadata_tag.at_xpath('metadata/rdf:RDF', 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')&.to_s
122+
output = parse_bioschemas(bioschemas_xml)
123+
next unless output
124+
125+
provider_events += output[:resources][:events]
126+
provider_materials += output[:resources][:materials]
127+
output[:totals].each do |key, value|
128+
totals[key] += value
129+
end
130+
end
131+
132+
if totals.keys.any?
133+
bioschemas_summary = "Bioschemas summary:\n"
134+
totals.each do |type, count|
135+
bioschemas_summary << "\n - #{type}: #{count}"
136+
end
137+
@messages << bioschemas_summary
138+
end
139+
140+
deduplicate(provider_events).each do |event_params|
141+
add_event(event_params)
142+
end
143+
144+
deduplicate(provider_materials).each do |material_params|
145+
add_material(material_params)
146+
end
147+
148+
provider_events.any? || provider_materials.any?
149+
end
150+
151+
def parse_bioschemas(content)
152+
output = {
153+
resources: {
154+
events: [],
155+
materials: []
156+
},
157+
totals: Hash.new(0)
158+
}
159+
160+
return output unless content
161+
162+
begin
163+
events = Tess::Rdf::EventExtractor.new(content, :rdfxml).extract do |p|
164+
convert_params(p)
165+
end
166+
courses = Tess::Rdf::CourseExtractor.new(content, :rdfxml).extract do |p|
167+
convert_params(p)
168+
end
169+
course_instances = Tess::Rdf::CourseInstanceExtractor.new(content, :rdfxml).extract do |p|
170+
convert_params(p)
171+
end
172+
learning_resources = Tess::Rdf::LearningResourceExtractor.new(content, :rdfxml).extract do |p|
173+
convert_params(p)
174+
end
175+
output[:totals]['Events'] += events.count
176+
output[:totals]['Courses'] += courses.count
177+
output[:totals]['CourseInstances'] += course_instances.count
178+
output[:totals]['LearningResources'] += learning_resources.count
179+
180+
deduplicate(events + courses + course_instances).each do |event|
181+
output[:resources][:events] << event
182+
end
183+
184+
deduplicate(learning_resources).each do |material|
185+
output[:resources][:materials] << material
186+
end
187+
rescue StandardError => e
188+
Rails.logger.error("#{e.class}: #{e.message}")
189+
Rails.logger.error(e.backtrace.join("\n")) if e.backtrace&.any?
190+
error = 'An error'
191+
comment = nil
192+
if e.is_a?(RDF::ReaderError)
193+
error = 'A parsing error'
194+
comment = 'Please check your page contains valid RDF/XML.'
195+
end
196+
message = "#{error} occurred while reading the source."
197+
message << " #{comment}" if comment
198+
@messages << message
199+
end
200+
201+
output
202+
end
203+
204+
# ---- This is copied unchanged from bioschemas_ingestor.rb and needs to be refactored. ----
205+
206+
# If duplicate resources have been extracted, prefer ones with the most metadata.
207+
def deduplicate(resources)
208+
return [] unless resources.any?
209+
210+
puts "De-duplicating #{resources.count} resources" if verbose
211+
hash = {}
212+
scores = {}
213+
resources.each do |resource|
214+
resource_url = resource[:url]
215+
puts " Considering: #{resource_url}" if verbose
216+
if hash[resource_url]
217+
score = metadata_score(resource)
218+
# Replace the resource if this resource has a higher metadata score
219+
puts " Duplicate! Comparing #{score} vs. #{scores[resource_url]}" if verbose
220+
if score > scores[resource_url]
221+
puts ' Replacing resource' if verbose
222+
hash[resource_url] = resource
223+
scores[resource_url] = score
224+
end
225+
else
226+
puts ' Not present, adding' if verbose
227+
hash[resource_url] = resource
228+
scores[resource_url] = metadata_score(resource)
229+
end
230+
end
231+
232+
puts "#{hash.values.count} resources after de-duplication" if verbose
233+
234+
hash.values
235+
end
236+
237+
# Score based on number of metadata fields available
238+
def metadata_score(resource)
239+
score = 0
240+
resource.each_value do |value|
241+
score += 1 unless value.nil? || value == {} || value == [] || (value.is_a?(String) && value.strip == '')
242+
end
243+
244+
score
245+
end
246+
247+
def convert_params(params)
248+
params[:description] = convert_description(params[:description]) if params.key?(:description)
249+
250+
params
251+
end
103252
end
104253
end

0 commit comments

Comments
 (0)