Skip to content

Commit

Permalink
Identifier metadata is now loaded lazily from EZID
Browse files Browse the repository at this point in the history
The local metadata is cleared after persisting (create/mint or modify)
and reloaded only on subsequent access.

The `#metadata` reader method provides a boolean `refresh` parameter
(default: `true`) which can be used to bypass the reload. This may
be useful in testing scenarios.

Internal implementation details of Ezid::Metadata were also changed
without public API impact (the class itself is documented as
private).

Closes #23
  • Loading branch information
dchandekstark committed Feb 27, 2015
1 parent 6cd2857 commit 3012f66
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 99 deletions.
97 changes: 55 additions & 42 deletions lib/ezid/identifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ module Ezid
#
# Represents an EZID identifier as a resource.
#
# Ezid::Identifier delegates access to registered metadata elements through #method_missing.
#
# @api public
#
class Identifier

attr_reader :id, :client
attr_accessor :shoulder, :metadata
attr_reader :client
attr_accessor :id, :shoulder, :metadata, :state

private :state, :state=, :id=

# Attributes to display on inspect
INSPECT_ATTRS = %w( id status target created )
Expand Down Expand Up @@ -46,15 +46,16 @@ def initialize(args={})
@client = args.delete(:client) || Client.new
@id = args.delete(:id)
@shoulder = args.delete(:shoulder)
@deleted = false
init_metadata(args)
@state = :new
self.metadata = Metadata.new args.delete(:metadata)
update_metadata self.class.defaults.merge(args) # deprecate?
end

def inspect
attrs = if deleted?
"id=\"#{id}\" DELETED"
else
INSPECT_ATTRS.map { |attr| "#{attr}=\"#{send(attr)}\"" }.join(" ")
INSPECT_ATTRS.map { |attr| "#{attr}=#{send(attr).inspect}" }.join(", ")
end
"#<#{self.class.name} #{attrs}>"
end
Expand All @@ -63,6 +64,14 @@ def to_s
id
end

# Returns the identifier metadata
# @param refresh [Boolean] - flag to refresh the metadata from EZID if stale (default: `true`)
# @return [Ezid::Metadata] the metadata
def metadata(refresh = true)
refresh_metadata if refresh && stale?
@metadata
end

# Persist the identifer and/or metadata to EZID.
# If the identifier is already persisted, this is an update operation;
# Otherwise, create (if it has an id) or mint (if it has a shoulder)
Expand All @@ -72,8 +81,8 @@ def to_s
# with an error status.
def save
raise Error, "Cannot save a deleted identifier." if deleted?
persisted? ? modify : create_or_mint
reload
persist
reset
end

# Updates the metadata
Expand All @@ -87,14 +96,13 @@ def update_metadata(attrs={})
# Is the identifier persisted?
# @return [Boolean]
def persisted?
return false if deleted?
!!(id && created)
state == :persisted
end

# Has the identifier been deleted?
# @return [Boolean]
def deleted?
@deleted
state == :deleted
end

# Updates the metadata and saves the identifier
Expand Down Expand Up @@ -128,7 +136,7 @@ def reset
def delete
raise Error, "Only persisted, reserved identifiers may be deleted: #{inspect}." unless deletable?
client.delete_identifier(id)
@deleted = true
self.state = :deleted
reset
end

Expand Down Expand Up @@ -174,44 +182,49 @@ def public!

protected

def method_missing(method, *args)
metadata.send(method, *args)
rescue NoMethodError
super
end
def method_missing(method, *args)
metadata.send(method, *args)
rescue NoMethodError
super
end

private

def refresh_metadata
response = client.get_identifier_metadata(id)
@metadata = Metadata.new(response.metadata)
end
def stale?
persisted? && metadata(false).empty?
end

def clear_metadata
@metadata.clear
end
def refresh_metadata
response = client.get_identifier_metadata(id)
self.metadata = Metadata.new response.metadata
self.state = :persisted
end

def modify
client.modify_identifier(id, metadata)
end
def clear_metadata
metadata(false).clear
end

def create_or_mint
id ? create : mint
end
def modify
client.modify_identifier(id, metadata)
end

def mint
response = client.mint_identifier(shoulder, metadata)
@id = response.id
end
def create_or_mint
id ? create : mint
end

def create
client.create_identifier(id, metadata)
end
def mint
response = client.mint_identifier(shoulder, metadata)
self.id = response.id
end

def init_metadata(args)
@metadata = Metadata.new(args.delete(:metadata))
update_metadata(self.class.defaults.merge(args))
end
def create
client.create_identifier(id, metadata)
end

def persist
persisted? ? modify : create_or_mint
self.state = :persisted
end

end
end
104 changes: 75 additions & 29 deletions lib/ezid/metadata.rb
Original file line number Diff line number Diff line change
@@ -1,42 +1,54 @@
require "delegate"
require "forwardable"

module Ezid
#
# EZID metadata collection for an identifier.
#
# @api private
#
class Metadata < SimpleDelegator
class Metadata
extend Forwardable

attr_reader :elements

def_delegators :elements, :[], :[]=, :each, :clear, :to_h, :empty?

class << self
def metadata_reader(method, alias_as=nil)
define_method method do
self[method.to_s]
def metadata_reader(element, alias_as=nil)
define_method element do
get(element)
end
if alias_as
alias_method alias_as, method
alias_method alias_as, element
end
end

def metadata_writer(method, alias_as=nil)
define_method "#{method}=" do |value|
self[method.to_s] = value
def metadata_writer(element, alias_as=nil)
define_method "#{element}=" do |value|
set(element, value)
end
if alias_as
alias_method "#{alias_as}=".to_sym, "#{method}=".to_sym
alias_method "#{alias_as}=".to_sym, "#{element}=".to_sym
end
end

def metadata_accessor(method, alias_as=nil)
metadata_reader method, alias_as
metadata_writer method, alias_as
def metadata_accessor(element, alias_as=nil)
metadata_reader element, alias_as
metadata_writer element, alias_as
end

def metadata_profile(profile, *methods)
methods.each do |method|
element = [profile, method].join(".")
alias_as = [profile, method].join("_")
metadata_accessor element, alias_as
def metadata_profile(profile, *elements)
elements.each do |element|
profile_element = [profile, element].join(".")
method = [profile, element].join("_")

define_method method do
get(profile_element)
end

define_method "#{method}=" do |value|
set(profile_element, value)
end
end
end
end
Expand Down Expand Up @@ -71,17 +83,28 @@ def metadata_profile(profile, *methods)
# @see http://ezid.cdlib.org/doc/apidoc.html#internal-metadata
READONLY = %w( _owner _ownergroup _shadows _shadowedby _datacenter _created _updated )

# EZID metadata profiles - a hash of (profile => elements)
# @see http://ezid.cdlib.org/doc/apidoc.html#metadata-profiles
# @note crossref is not included because it is a simple element
PROFILES = {
dc: [:creator, :title, :publisher, :date, :type],
datacite: [:creator, :title, :publisher, :publicationyear, :resourcetype],
erc: [:who, :what, :when]
}

PROFILES.each do |profile, elements|
metadata_profile profile, *elements
end

# Accessors for EZID internal metadata elements
metadata_accessor :_coowners, :coowners
metadata_accessor :_crossref
metadata_accessor :_export, :export
metadata_accessor :_profile, :profile
metadata_accessor :_status, :status
metadata_accessor :_target, :target

metadata_accessor :crossref
metadata_accessor :datacite
metadata_accessor :erc

# Readers for EZID read-only internal metadata elements
metadata_reader :_created
metadata_reader :_datacenter, :datacenter
metadata_reader :_owner, :owner
Expand All @@ -90,12 +113,13 @@ def metadata_profile(profile, *methods)
metadata_reader :_shadows, :shadows
metadata_reader :_updated

metadata_profile :dc, :creator, :title, :publisher, :date, :type
metadata_profile :datacite, :creator, :title, :publisher, :publicationyear, :resourcetype
metadata_profile :erc, :who, :what, :when
# Accessors for
metadata_accessor :crossref
metadata_accessor :datacite
metadata_accessor :erc

def initialize(data={})
super coerce(data)
@elements = coerce(data)
end

def created
Expand All @@ -110,19 +134,41 @@ def updated
# @see http://ezid.cdlib.org/doc/apidoc.html#request-response-bodies
# @return [String] the ANVL output
def to_anvl(include_readonly = true)
hsh = __getobj__.dup
hsh = elements.dup
hsh.reject! { |k, v| READONLY.include?(k) } unless include_readonly
elements = hsh.map do |name, value|
lines = hsh.map do |name, value|
element = [escape(ESCAPE_NAMES_RE, name), escape(ESCAPE_VALUES_RE, value)]
element.join(ANVL_SEPARATOR)
end
elements.join("\n").force_encoding(Encoding::UTF_8)
lines.join("\n").force_encoding(Encoding::UTF_8)
end

def inspect
"#<#{self.class.name} elements=#{elements.inspect}>"
end

def to_s
to_anvl
end

def get(element)
self[element.to_s]
end

def set(element, value)
self[element.to_s] = value
end

protected

def method_missing(method, *args)
return get(method) if args.size == 0
if element = method.to_s[/^([^=]+)=$/, 1]
return set(element, *args)
end
super
end

private

def to_time(value)
Expand Down
Loading

0 comments on commit 3012f66

Please sign in to comment.