oaipmh 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +0 -0
- data/README +3 -0
- data/Rakefile +85 -0
- data/lib/oaipmh.rb +9 -0
- data/lib/oaipmh/constants.rb +31 -0
- data/lib/oaipmh/exceptions.rb +72 -0
- data/lib/oaipmh/extensions/camping.rb +22 -0
- data/lib/oaipmh/helpers.rb +60 -0
- data/lib/oaipmh/metadata.rb +14 -0
- data/lib/oaipmh/metadata/oai_dc.rb +84 -0
- data/lib/oaipmh/model.rb +34 -0
- data/lib/oaipmh/provider.rb +421 -0
- data/lib/oaipmh/version.rb +9 -0
- data/test/oaipmh_test.rb +11 -0
- data/test/test_helper.rb +2 -0
- metadata +74 -0
data/CHANGELOG
ADDED
File without changes
|
data/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
require 'fileutils'
|
10
|
+
include FileUtils
|
11
|
+
require File.join(File.dirname(__FILE__), 'lib', 'oaipmh', 'version')
|
12
|
+
|
13
|
+
AUTHOR = "will"
|
14
|
+
EMAIL = "your contact email for bug fixes and info"
|
15
|
+
DESCRIPTION = "description of gem"
|
16
|
+
RUBYFORGE_PROJECT = "oaipmh"
|
17
|
+
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
|
18
|
+
BIN_FILES = %w( )
|
19
|
+
|
20
|
+
|
21
|
+
NAME = "oaipmh"
|
22
|
+
REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
|
23
|
+
VERS = ENV['VERSION'] || (Oaipmh::VERSION::STRING + (REV ? ".#{REV}" : ""))
|
24
|
+
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
25
|
+
RDOC_OPTS = ['--quiet', '--title', "oaipmh documentation",
|
26
|
+
"--opname", "index.html",
|
27
|
+
"--line-numbers",
|
28
|
+
"--main", "README",
|
29
|
+
"--inline-source"]
|
30
|
+
|
31
|
+
desc "Packages up oaipmh gem."
|
32
|
+
task :default => [:test]
|
33
|
+
task :package => [:clean]
|
34
|
+
|
35
|
+
Rake::TestTask.new("test") { |t|
|
36
|
+
t.libs << "test"
|
37
|
+
t.pattern = "test/**/*_test.rb"
|
38
|
+
t.verbose = true
|
39
|
+
}
|
40
|
+
|
41
|
+
spec =
|
42
|
+
Gem::Specification.new do |s|
|
43
|
+
s.name = NAME
|
44
|
+
s.version = VERS
|
45
|
+
s.platform = Gem::Platform::RUBY
|
46
|
+
s.has_rdoc = true
|
47
|
+
s.extra_rdoc_files = ["README", "CHANGELOG"]
|
48
|
+
s.rdoc_options += RDOC_OPTS + ['--exclude', '^(examples|extras)/']
|
49
|
+
s.summary = DESCRIPTION
|
50
|
+
s.description = DESCRIPTION
|
51
|
+
s.author = AUTHOR
|
52
|
+
s.email = EMAIL
|
53
|
+
s.homepage = HOMEPATH
|
54
|
+
s.executables = BIN_FILES
|
55
|
+
s.rubyforge_project = RUBYFORGE_PROJECT
|
56
|
+
s.bindir = "bin"
|
57
|
+
s.require_path = "lib"
|
58
|
+
s.autorequire = "oaipmh"
|
59
|
+
|
60
|
+
#s.add_dependency('activesupport', '>=1.3.1')
|
61
|
+
#s.required_ruby_version = '>= 1.8.2'
|
62
|
+
|
63
|
+
s.files = %w(README CHANGELOG Rakefile) +
|
64
|
+
Dir.glob("{bin,doc,test,lib,templates,generator,extras,website,script}/**/*") +
|
65
|
+
Dir.glob("ext/**/*.{h,c,rb}") +
|
66
|
+
Dir.glob("examples/**/*.rb") +
|
67
|
+
Dir.glob("tools/*.rb")
|
68
|
+
|
69
|
+
# s.extensions = FileList["ext/**/extconf.rb"].to_a
|
70
|
+
end
|
71
|
+
|
72
|
+
Rake::GemPackageTask.new(spec) do |p|
|
73
|
+
p.need_tar = true
|
74
|
+
p.gem_spec = spec
|
75
|
+
end
|
76
|
+
|
77
|
+
task :install do
|
78
|
+
name = "#{NAME}-#{VERS}.gem"
|
79
|
+
sh %{rake package}
|
80
|
+
sh %{sudo gem install pkg/#{name}}
|
81
|
+
end
|
82
|
+
|
83
|
+
task :uninstall => [:clean] do
|
84
|
+
sh %{sudo gem uninstall #{NAME}}
|
85
|
+
end
|
data/lib/oaipmh.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module OaiPmh
|
2
|
+
|
3
|
+
module Const
|
4
|
+
|
5
|
+
# OAI defines six verbs with various allowable options.
|
6
|
+
VERBS = {
|
7
|
+
'Identify' => [],
|
8
|
+
'ListMetadataFormats' => [],
|
9
|
+
'ListSets' => [:token],
|
10
|
+
'GetRecord' => [:id, :from, :until, :set, :prefix, :token],
|
11
|
+
'ListIdentifiers' => [:from, :until, :set, :prefix, :token],
|
12
|
+
'ListRecords' => [:from, :until, :set, :prefix, :token]
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
# Common to many data source, and sadly also a method on object.
|
16
|
+
RESERVED_WORDS = %{type}.freeze
|
17
|
+
|
18
|
+
# Default configuration of a repository
|
19
|
+
DEFAULTS = {
|
20
|
+
:name => 'Open Archives Initiative Data Provider',
|
21
|
+
:url => 'unknown',
|
22
|
+
:prefix => 'oai:localhost',
|
23
|
+
:email => 'nobody@localhost',
|
24
|
+
:deletes => 'no',
|
25
|
+
:granularity => 'YYYY-MM-DDThh:mm:ssZ',
|
26
|
+
:formats => OaiPmh::METADATA
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module OaiPmh
|
2
|
+
|
3
|
+
# Standard error responses for problems serving OAI content. These
|
4
|
+
# messages will be wrapped in an XML response to the client.
|
5
|
+
|
6
|
+
class OAIException < RuntimeError
|
7
|
+
attr_reader :code
|
8
|
+
|
9
|
+
def initialize(code, message)
|
10
|
+
super(message)
|
11
|
+
@code = code
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ArgumentException < OAIException
|
16
|
+
def initialize()
|
17
|
+
super('badArgument', 'The request includes ' \
|
18
|
+
'illegal arguments, is missing required arguments, includes a ' \
|
19
|
+
'repeated argument, or values for arguments have an illegal syntax.')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class VerbException < OAIException
|
24
|
+
def initialize()
|
25
|
+
super('badVerb', 'Value of the verb argument is not a legal OAI-PMH '\
|
26
|
+
'verb, the verb argument is missing, or the verb argument is repeated.')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class FormatException < OAIException
|
31
|
+
def initialize()
|
32
|
+
super('cannotDisseminateFormat', 'The metadata format identified by '\
|
33
|
+
'the value given for the metadataPrefix argument is not supported '\
|
34
|
+
'by the item or by the repository.')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class IdException < OAIException
|
39
|
+
def initialize()
|
40
|
+
super('idDoesNotExist', 'The value of the identifier argument is '\
|
41
|
+
'unknown or illegal in this repository.')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class NoMatchException < OAIException
|
46
|
+
def initialize()
|
47
|
+
super('noRecordsMatch', 'The combination of the values of the from, '\
|
48
|
+
'until, set and metadataPrefix arguments results in an empty list.')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class MetadataFormatException
|
53
|
+
def initialize()
|
54
|
+
super('noMetadataFormats', 'There are no metadata formats available '\
|
55
|
+
'for the specified item.')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class SetException < OAIException
|
60
|
+
def initialize()
|
61
|
+
super('noSetHierarchy', 'This repository does not support sets.')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class ResumptionTokenException < OAIException
|
66
|
+
def initialize()
|
67
|
+
super('badResumptionToken', 'The value of the resumptionToken argument '\
|
68
|
+
'is invalid or expired.')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'oaipmh'
|
2
|
+
|
3
|
+
module OaiPmh
|
4
|
+
module Extensions
|
5
|
+
module Camped
|
6
|
+
|
7
|
+
def self.included(mod)
|
8
|
+
instance_eval(%{module #{mod}::Controllers
|
9
|
+
class Oai
|
10
|
+
def get
|
11
|
+
@headers['Content-Type'] = 'text/xml'
|
12
|
+
provider = OaiPmh::Provider.new
|
13
|
+
provider.process_verb(@input.delete('verb'), @input.merge(:url => "http:"+URL(Oai).to_s))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
})
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module OaiPmh
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
# Output the OAI-PMH header
|
5
|
+
def header
|
6
|
+
@xml = Builder::XmlMarkup.new
|
7
|
+
@xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
8
|
+
@xml.tag!('OAI-PMH',
|
9
|
+
'xmlns' => "http://www.openarchives.org/OAI/2.0/",
|
10
|
+
'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
|
11
|
+
'xsi:schemaLocation' => %{http://www.openarchives.org/OAI/2.0/
|
12
|
+
http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd}) do
|
13
|
+
@xml.responseDate Time.now.utc.xmlschema
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Echo the request parameters back to the client. See spec.
|
19
|
+
def echo_params(verb, opts)
|
20
|
+
@xml.request(@url, {:verb => verb}.merge(opts))
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_scope_hash
|
24
|
+
params = {}
|
25
|
+
params[:from] = parse_date(@opts[:from]) if @opts[:from]
|
26
|
+
params[:until] = parse_date(@opts[:until]) if @opts[:until]
|
27
|
+
params[:set] = @opts[:set] if @opts[:set]
|
28
|
+
params
|
29
|
+
end
|
30
|
+
|
31
|
+
# Use of Chronic here is mostly for human interactions. It's
|
32
|
+
# nice to be able to say '?verb=ListRecords&from=October&until=November'
|
33
|
+
def parse_date(dt_string)
|
34
|
+
# Oddly Chronic doesn't parse an UTC encoded datetime.
|
35
|
+
# Luckily Time does
|
36
|
+
dt = Chronic.parse(dt_string) || Time.parse(dt_string)
|
37
|
+
dt.strftime("%Y-%m-%d %H:%M:%S")
|
38
|
+
end
|
39
|
+
|
40
|
+
# Massage the options until they are bit more palatable.
|
41
|
+
def ensure_valid(verb, opts)
|
42
|
+
return {} unless opts
|
43
|
+
popts = {}
|
44
|
+
opts.keys.each do |k|
|
45
|
+
# Ensure they are all lowercase symbols
|
46
|
+
nk = k.to_s.downcase.intern
|
47
|
+
popts[nk] = opts[k]
|
48
|
+
end
|
49
|
+
# shorten the big ugly long ones
|
50
|
+
popts[:id] = popts.delete(:identifier) if popts[:identifier]
|
51
|
+
popts[:prefix] = popts.delete(:metadataprefix) if popts[:metadataprefix]
|
52
|
+
popts[:token] = popts.delete(:resumptiontoken) if popts[:resumptiontoken]
|
53
|
+
|
54
|
+
raise ArgumentException.new unless popts.empty? ||
|
55
|
+
(popts.keys - OaiPmh::Const::VERBS[verb]).empty?
|
56
|
+
popts
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# = OaiPmh::Metadata::OaiDc
|
2
|
+
#
|
3
|
+
# Copyright (C) 2006 William Groppe
|
4
|
+
#
|
5
|
+
# Will Groppe mailto:wfg@artstor.org
|
6
|
+
#
|
7
|
+
# Only one form of metadata is supported out of the box. Dublin Core is the
|
8
|
+
# most basic form of metadata, and the one recommended for support in all
|
9
|
+
# OAI-PMH repositories.
|
10
|
+
#
|
11
|
+
# To add additional metadata types it's easiest just to subclass
|
12
|
+
# OaiPmh::Metadata::OaiDc. Subclasses should override header(xml) to ouput a
|
13
|
+
# valid metadata header. They should also set defaults for prefix, schema,
|
14
|
+
# namespace, element_ns, and fields.
|
15
|
+
#
|
16
|
+
# === Example
|
17
|
+
# class CdwaLite < OaiPmh::Metadata::OaiDc
|
18
|
+
# prefix = 'cdwalite'
|
19
|
+
# schema = 'http://www.getty.edu/CDWA/CDWALite/CDWALite-xsd-draft-009c2.xsd'
|
20
|
+
# namespace = 'http://www.getty.edu/CDWA/CDWALite'
|
21
|
+
# element_ns = 'cdwalite'
|
22
|
+
# fields = [] # using to_cdwalite in model
|
23
|
+
#
|
24
|
+
# def self.header(xml)
|
25
|
+
# xml.tag!('cdwalite:cdwalite',
|
26
|
+
# 'xmlns:cdwalite' => "http://www.getty.edu/CDWA/CDWALite",
|
27
|
+
# 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
|
28
|
+
# 'xsi:schemaLocation' =>
|
29
|
+
# %{http://www.getty.edu/CDWA/CDWALite
|
30
|
+
# http://www.getty.edu/CDWA/CDWALite/CDWALite-xsd-draft-009c2.xsd}) do
|
31
|
+
# yield xml
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# # Now register the new metadata class
|
37
|
+
# OaiPmh.register_metadata_class(CdwaLite)
|
38
|
+
#
|
39
|
+
module OaiPmh::Metadata
|
40
|
+
|
41
|
+
class OaiDc
|
42
|
+
# Defaults
|
43
|
+
DEFAULTS = {:prefix => 'oai_dc',
|
44
|
+
:schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
|
45
|
+
:namespace => 'http://www.language-archives.org/OLAC/0.2/',
|
46
|
+
:element_ns => 'dc',
|
47
|
+
:fields => %w(title creator subject description publisher
|
48
|
+
contributor date type format identifier
|
49
|
+
source language relation coverage rights)
|
50
|
+
}
|
51
|
+
|
52
|
+
# Create accessors.
|
53
|
+
DEFAULTS.each_key do |proc|
|
54
|
+
class_eval %{ def self.#{proc}; DEFAULTS[:#{proc}]; end }
|
55
|
+
class_eval %{ def self.#{proc}=(value); DEFAULTS[:#{proc}]=value; end }
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class << self
|
60
|
+
def header(xml)
|
61
|
+
xml.tag!('oai_dc:dc',
|
62
|
+
'xmlns:oai_dc' => "http://www.openarchives.org/OAI/2.0/oai_dc/",
|
63
|
+
'xmlns:dc' => "http://purl.org/dc/elements/1.1/",
|
64
|
+
'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
|
65
|
+
'xsi:schemaLocation' =>
|
66
|
+
%{http://www.openarchives.org/OAI/2.0/oai_dc/
|
67
|
+
http://www.openarchives.org/OAI/2.0/oai_dc.xsd}) do
|
68
|
+
yield xml
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
DEFAULTS[:prefix]
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate(document)
|
77
|
+
raise RuntimeError, "Validation not yet implemented."
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
end
|
data/lib/oaipmh/model.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# = model.rb
|
2
|
+
#
|
3
|
+
# Copyright (C) 2006 William Groppe
|
4
|
+
#
|
5
|
+
# Will Groppe mailto: wfg@artstor.org
|
6
|
+
#
|
7
|
+
#
|
8
|
+
# Implementing a model from scratch requires overridding three methods from
|
9
|
+
# OaiPmh::Model
|
10
|
+
#
|
11
|
+
# * oai_earliest - should provide the earliest possible timestamp
|
12
|
+
# * oai_sets - if you want to support sets
|
13
|
+
# * oai_find(selector, opts) - selector can be either a record id, or :all for
|
14
|
+
# finding all matches. opts is a hash of query parameters. Valid parameters
|
15
|
+
# include :from, :until, :set, :token, and :prefix. Any errors in the
|
16
|
+
# parameters should raise a OaiPmh::ArgumentException.
|
17
|
+
#
|
18
|
+
module OaiPmh
|
19
|
+
module Model
|
20
|
+
|
21
|
+
def self.oai_earliest
|
22
|
+
Time.now.utc
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.oai_sets
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.oai_find(selector, opts={})
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,421 @@
|
|
1
|
+
# = provider.rb
|
2
|
+
#
|
3
|
+
# Copyright (C) 2006 William Groppe
|
4
|
+
#
|
5
|
+
# Will Groppe mailto:wfg@artstor.org
|
6
|
+
#
|
7
|
+
# Open Archives Initiative - Protocol for Metadata Harvesting see
|
8
|
+
# http://www.openarchives.org/
|
9
|
+
#
|
10
|
+
# === Features
|
11
|
+
# * Easily setup a simple repository
|
12
|
+
# * Simple integration with ActiveRecord
|
13
|
+
# * Dublin Core metadata format included
|
14
|
+
# * Easily add addition metadata formats
|
15
|
+
# * Adaptable to any data source
|
16
|
+
#
|
17
|
+
#
|
18
|
+
# === Current shortcomings
|
19
|
+
# * No resumption tokens
|
20
|
+
# * Doesn't validate metadata
|
21
|
+
# * No deletion support
|
22
|
+
# * Many others I can't think of right now. :-)
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# === ActiveRecord integration
|
26
|
+
#
|
27
|
+
# To successfully use ActiveRecord as a OAI PMH datasource the database table
|
28
|
+
# should include an updated_at column so that updates to the table are
|
29
|
+
# tracked by ActiveRecord. This provides much of the base functionality for
|
30
|
+
# selecting update periods.
|
31
|
+
#
|
32
|
+
# To understand how the data is extracted from the AR model it's best to just
|
33
|
+
# go thru the logic:
|
34
|
+
#
|
35
|
+
# Does the model respond to 'to_{prefix}'? Where prefix is the
|
36
|
+
# metadata prefix. If it does then just include the response from
|
37
|
+
# the model. So if you want to provide custom or complex metadata you can
|
38
|
+
# simply define a 'to_{prefix}' method on your model.
|
39
|
+
#
|
40
|
+
# Example:
|
41
|
+
#
|
42
|
+
# class Record < ActiveRecord::Base
|
43
|
+
#
|
44
|
+
# def to_oai_dc
|
45
|
+
# xml = Builder::XmlMarkup.new
|
46
|
+
# xml.tag!('oai_dc:dc',
|
47
|
+
# 'xmlns:oai_dc' => "http://www.openarchives.org/OAI/2.0/oai_dc/",
|
48
|
+
# 'xmlns:dc' => "http://purl.org/dc/elements/1.1/",
|
49
|
+
# 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
|
50
|
+
# 'xsi:schemaLocation' =>
|
51
|
+
# %{http://www.openarchives.org/OAI/2.0/oai_dc/
|
52
|
+
# http://www.openarchives.org/OAI/2.0/oai_dc.xsd}) do
|
53
|
+
#
|
54
|
+
# xml.oai_dc :title, title
|
55
|
+
# xml.oai_dc :subject, subject
|
56
|
+
# end
|
57
|
+
# xml.to_s
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# If the model doesn't define a 'to_{prefix}' then start iterating thru
|
63
|
+
# the defined metadata fields.
|
64
|
+
#
|
65
|
+
# Grab a mapping if one exists by trying to call 'map_{prefix}'.
|
66
|
+
#
|
67
|
+
# Now do the iteration and try calling methods on the model that match
|
68
|
+
# the field names, or the mapped field names.
|
69
|
+
#
|
70
|
+
# So with Dublin Core we end up with the following:
|
71
|
+
#
|
72
|
+
# 1. Check for 'title' mapped to a different method.
|
73
|
+
# 2. Call model.titles - try plural
|
74
|
+
# 3. Call model.title - try singular last
|
75
|
+
#
|
76
|
+
# Extremely contrived Blog example:
|
77
|
+
#
|
78
|
+
# class Post < ActiveRecord::Base
|
79
|
+
# def map_oai_dc
|
80
|
+
# {:subject => :tags,
|
81
|
+
# :description => :text,
|
82
|
+
# :creator => :user,
|
83
|
+
# :contibutor => :comments}
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# === Supporting custom metadata
|
88
|
+
#
|
89
|
+
# See OaiPmh::Metadata for details.
|
90
|
+
#
|
91
|
+
# == Examples
|
92
|
+
#
|
93
|
+
# === Sub classing a provider
|
94
|
+
#
|
95
|
+
# class MyProvider < OaiPmh::Provider
|
96
|
+
# name 'My little OAI provider'
|
97
|
+
# url 'http://localhost/provider'
|
98
|
+
# prefix 'oai:localhost'
|
99
|
+
# email 'root@localhost' # String or Array
|
100
|
+
# deletes 'no' # future versions will support deletes
|
101
|
+
# granularity 'YYYY-MM-DDThh:mm:ssZ' # update resolution
|
102
|
+
# model MyModel # Class to get data from
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# # Now use it
|
106
|
+
#
|
107
|
+
# provider = MyProvider.new
|
108
|
+
# provider.identify
|
109
|
+
# provider.list_sets
|
110
|
+
# provider.list_metadata_formats
|
111
|
+
# # these verbs require a working model
|
112
|
+
# provider.list_identifiers
|
113
|
+
# provider.list_records
|
114
|
+
# provider.get_record('oai:localhost/1')
|
115
|
+
#
|
116
|
+
#
|
117
|
+
# === Configuring the default provider
|
118
|
+
#
|
119
|
+
# class OaiPmh::Provider
|
120
|
+
# name 'My little OAI Provider'
|
121
|
+
# url 'http://localhost/provider'
|
122
|
+
# prefix 'oai:localhost'
|
123
|
+
# email 'root@localhost' # String or Array
|
124
|
+
# deletes 'no' # future versions will support deletes
|
125
|
+
# granularity 'YYYY-MM-DDThh:mm:ssZ' # update resolution
|
126
|
+
# model MyModel # Class to get data from
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
#
|
130
|
+
module OaiPmh
|
131
|
+
|
132
|
+
class Provider
|
133
|
+
include Helpers
|
134
|
+
|
135
|
+
@@options = {}
|
136
|
+
|
137
|
+
AVAILABLE_FORMATS = {}
|
138
|
+
|
139
|
+
class << self
|
140
|
+
|
141
|
+
OaiPmh::Const::DEFAULTS.keys.each do |field|
|
142
|
+
class_eval %{
|
143
|
+
def #{field}(value)
|
144
|
+
@@options[:#{field}] = value
|
145
|
+
end
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def model(value)
|
150
|
+
@@options[:model] = value
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
def initialize
|
156
|
+
@config = OaiPmh::Const::DEFAULTS.merge(@@options)
|
157
|
+
end
|
158
|
+
|
159
|
+
def identify
|
160
|
+
process_verb 'Identify'
|
161
|
+
end
|
162
|
+
|
163
|
+
def list_metadata_formats
|
164
|
+
process_verb 'ListMetadataFormats'
|
165
|
+
end
|
166
|
+
|
167
|
+
def list_sets(opts = {})
|
168
|
+
process_verb 'ListSets', opts
|
169
|
+
end
|
170
|
+
|
171
|
+
def get_record(id, opts = {})
|
172
|
+
process_verb 'GetRecord', opts.merge(:id => id)
|
173
|
+
end
|
174
|
+
|
175
|
+
def list_identifiers(opts = {})
|
176
|
+
process_verb 'ListIdentifiers', opts
|
177
|
+
end
|
178
|
+
|
179
|
+
def list_records(opts = {})
|
180
|
+
process_verb 'ListRecords', opts
|
181
|
+
end
|
182
|
+
|
183
|
+
# xml_response = process_verb('ListRecords', :from => 'October', :until => 'November') # thanks Chronic!
|
184
|
+
#
|
185
|
+
# If you are implementing a web interface using process_verb is the
|
186
|
+
# preferred way. See extensions/camping.rb
|
187
|
+
def process_verb(verb = nil, opts = {})
|
188
|
+
header do
|
189
|
+
begin
|
190
|
+
raise VerbException.new unless verb &&
|
191
|
+
OaiPmh::Const::VERBS.keys.include?(verb)
|
192
|
+
|
193
|
+
# Allow the request to pass in a
|
194
|
+
@url = opts['url'] ? opts.delete('url') : @config[:url]
|
195
|
+
|
196
|
+
echo_params(verb, opts)
|
197
|
+
|
198
|
+
@opts = ensure_valid(verb, opts)
|
199
|
+
|
200
|
+
@model = @config[:model]
|
201
|
+
|
202
|
+
# Pull out the requested metadata format. Important for GetRecord,
|
203
|
+
# ListRecords, ListIdentifiers? Default to 'oai_dc'
|
204
|
+
@format = @opts[:prefix] ? @opts[:prefix] : "oai_dc"
|
205
|
+
|
206
|
+
# Rubify the verb for calling method
|
207
|
+
call = verb.gsub(/[A-Z]/) {|m| "_#{m.downcase}"}.sub(/^\_/,'')
|
208
|
+
send("#{call}_response")
|
209
|
+
|
210
|
+
rescue
|
211
|
+
if $!.respond_to?(:code)
|
212
|
+
@xml.error $!.to_s, :code => $!.code
|
213
|
+
else
|
214
|
+
puts $!.message
|
215
|
+
puts $!.backtrace.join("\n")
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
def identify_response
|
224
|
+
@xml.Identify do
|
225
|
+
@xml.repositoryName @config[:name]
|
226
|
+
@xml.baseURL @url
|
227
|
+
@xml.protocolVersion 2.0
|
228
|
+
@config[:email].to_a.each do |email|
|
229
|
+
@xml.adminEmail email
|
230
|
+
end
|
231
|
+
@xml.earliestDatestamp earliest
|
232
|
+
@xml.deleteRecord @config[:delete]
|
233
|
+
@xml.granularity @config[:granularity]
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def list_sets_response
|
238
|
+
raise SetException.new unless sets
|
239
|
+
@xml.ListSets do |ls|
|
240
|
+
oai_sets.each do |ms|
|
241
|
+
@xml.set do |set|
|
242
|
+
@xml.setSpec ms.spec
|
243
|
+
@xml.setName ms.name
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def list_metadata_formats_response
|
250
|
+
@xml.ListMetadataFormats do
|
251
|
+
@config[:formats].each_pair do |key, format|
|
252
|
+
@xml.metadataFormat do
|
253
|
+
@xml.metadataPrefix instance_eval("#{format}.prefix")
|
254
|
+
@xml.schema instance_eval("#{format}.schema")
|
255
|
+
@xml.metadataNamespace instance_eval("#{format}.namespace")
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def list_identifiers_response
|
262
|
+
raise FORMAT_ERROR unless @config[:formats].include? @format
|
263
|
+
records = find :all
|
264
|
+
|
265
|
+
raise RECORDS_ERROR if records.nil? || records.empty?
|
266
|
+
|
267
|
+
@xml.ListIdentifiers do
|
268
|
+
records.each do |record|
|
269
|
+
metadata_header record
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def get_record_response
|
275
|
+
raise FormatException.new unless @config[:formats].include? @format
|
276
|
+
|
277
|
+
rec = @opts[:id].gsub("#{@config[:prefix]}/", "")
|
278
|
+
|
279
|
+
record = find rec
|
280
|
+
|
281
|
+
raise IdException.new unless record
|
282
|
+
|
283
|
+
@xml.GetRecord do
|
284
|
+
@xml.record do
|
285
|
+
metadata_header record
|
286
|
+
metadata record
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def list_records_response
|
292
|
+
raise FormatException.new unless @config[:formats].include? @format
|
293
|
+
|
294
|
+
records = find :all
|
295
|
+
|
296
|
+
raise NoMatchException.new if records.nil? || records.empty?
|
297
|
+
|
298
|
+
@xml.ListRecords do
|
299
|
+
records.each do |record|
|
300
|
+
@xml.record do
|
301
|
+
metadata_header record
|
302
|
+
metadata record
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def find(selector)
|
309
|
+
return nil unless @model
|
310
|
+
|
311
|
+
# Try oai finder methods first
|
312
|
+
begin
|
313
|
+
return @model.oai_find(selector, @opts)
|
314
|
+
rescue NoMethodError
|
315
|
+
begin
|
316
|
+
# Try an ActiveRecord finder call
|
317
|
+
return @model.find(selector, build_scope_hash)
|
318
|
+
rescue
|
319
|
+
end
|
320
|
+
end
|
321
|
+
nil
|
322
|
+
end
|
323
|
+
|
324
|
+
def earliest
|
325
|
+
return DateTime.new unless @model
|
326
|
+
|
327
|
+
# Try oai finder methods first
|
328
|
+
begin
|
329
|
+
return @model.oai_earliest
|
330
|
+
rescue NoMethodError
|
331
|
+
begin
|
332
|
+
# Try an ActiveRecord finder call
|
333
|
+
return @model.find(:first, :order => "updated_at asc").updated_at
|
334
|
+
rescue
|
335
|
+
end
|
336
|
+
end
|
337
|
+
nil
|
338
|
+
end
|
339
|
+
|
340
|
+
def sets
|
341
|
+
return nil unless @model
|
342
|
+
|
343
|
+
# Try oai finder methods first
|
344
|
+
begin
|
345
|
+
return @model.oai_sets
|
346
|
+
rescue NoMethodError
|
347
|
+
end
|
348
|
+
nil
|
349
|
+
end
|
350
|
+
|
351
|
+
# emit record header
|
352
|
+
def metadata_header(record)
|
353
|
+
@xml.header do |h|
|
354
|
+
h.identifier "#{@config[:prefix]}/#{record.id}"
|
355
|
+
h.datestamp record.updated_at.utc.xmlschema
|
356
|
+
h.set @opts[:set] if @opts[:set]
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
|
361
|
+
# metadata - core routine for delivering metadata records
|
362
|
+
#
|
363
|
+
def metadata(record)
|
364
|
+
if record.respond_to?("to_#{@format}")
|
365
|
+
@xml.metadata do
|
366
|
+
str = record.send("to_#{@format}")
|
367
|
+
# Strip off the xml header if we got one.
|
368
|
+
str.sub!(/<\?xml.*?\?>/, '')
|
369
|
+
@xml << str
|
370
|
+
end
|
371
|
+
else
|
372
|
+
map = record.respond_to?("map_#{@format}") ?
|
373
|
+
instance_eval("record.map_#{@format}") : {}
|
374
|
+
|
375
|
+
mdformat = @config[:formats][@format]
|
376
|
+
@xml.metadata do
|
377
|
+
mdformat.header(@xml) do
|
378
|
+
mdformat.fields.each do |field|
|
379
|
+
set = value_for(field, record, map)
|
380
|
+
set.each do |mdv|
|
381
|
+
instance_eval("@xml.#{mdformat.element_ns} :#{field}, %{#{mdv}}")
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# We try a bunch of different methods to get the data from the model.
|
390
|
+
#
|
391
|
+
# 1) See if the model will hand us the entire record in the requested
|
392
|
+
# format. Example: if the model defines 'to_oai_dc' we call that
|
393
|
+
# method and append the result to the xml stream.
|
394
|
+
# 2) Check if the model defines a field mapping for the field of
|
395
|
+
# interest.
|
396
|
+
# 3) Try calling the pluralized name method on the model.
|
397
|
+
# 4) Try calling the singular name method on the model, if it's not a
|
398
|
+
# reserved word.
|
399
|
+
def value_for(field, record, map)
|
400
|
+
if map.keys.include?(field)
|
401
|
+
return map[field].nil? ? [] :
|
402
|
+
record.send("#{map[field]}").to_a
|
403
|
+
end
|
404
|
+
|
405
|
+
begin
|
406
|
+
return record.send("#{field.pluralize}").to_a
|
407
|
+
rescue
|
408
|
+
unless OaiPmh::Const::RESERVED_WORDS.include?(field)
|
409
|
+
begin
|
410
|
+
return record.send("#{field}").to_a
|
411
|
+
rescue
|
412
|
+
return []
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
[]
|
417
|
+
end
|
418
|
+
|
419
|
+
end
|
420
|
+
|
421
|
+
end
|
data/test/oaipmh_test.rb
ADDED
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: oaipmh
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2006-11-02 00:00:00 -05:00
|
8
|
+
summary: description of gem
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: your contact email for bug fixes and info
|
12
|
+
homepage: http://oaipmh.rubyforge.org
|
13
|
+
rubyforge_project: oaipmh
|
14
|
+
description: description of gem
|
15
|
+
autorequire: oaipmh
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- will
|
31
|
+
files:
|
32
|
+
- README
|
33
|
+
- CHANGELOG
|
34
|
+
- Rakefile
|
35
|
+
- test/oaipmh_test.rb
|
36
|
+
- test/test_helper.rb
|
37
|
+
- lib/oaipmh
|
38
|
+
- lib/oaipmh.rb
|
39
|
+
- lib/oaipmh/constants.rb
|
40
|
+
- lib/oaipmh/exceptions.rb
|
41
|
+
- lib/oaipmh/extensions
|
42
|
+
- lib/oaipmh/helpers.rb
|
43
|
+
- lib/oaipmh/metadata
|
44
|
+
- lib/oaipmh/metadata.rb
|
45
|
+
- lib/oaipmh/model.rb
|
46
|
+
- lib/oaipmh/provider.rb
|
47
|
+
- lib/oaipmh/version.rb
|
48
|
+
- lib/oaipmh/extensions/camping.rb
|
49
|
+
- lib/oaipmh/metadata/oai_dc.rb
|
50
|
+
test_files: []
|
51
|
+
|
52
|
+
rdoc_options:
|
53
|
+
- --quiet
|
54
|
+
- --title
|
55
|
+
- oaipmh documentation
|
56
|
+
- --opname
|
57
|
+
- index.html
|
58
|
+
- --line-numbers
|
59
|
+
- --main
|
60
|
+
- README
|
61
|
+
- --inline-source
|
62
|
+
- --exclude
|
63
|
+
- ^(examples|extras)/
|
64
|
+
extra_rdoc_files:
|
65
|
+
- README
|
66
|
+
- CHANGELOG
|
67
|
+
executables: []
|
68
|
+
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
dependencies: []
|
74
|
+
|