oaipmh 0.0.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.
- 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
|
+
|