# Hey emacs, this is -*-perl-*- ! # # $Id: Base.pm,v 1.10 2001/01/09 12:04:12 cmdjb Exp $ # # Metadata::Base - base class for metadata # # Copyright (C) 1997-2001 Dave Beckett - http://purl.org/net/dajobe/ # All rights reserved. # # This module is free software; you can redistribute it and/or modify # it under the same terms as Perl itself. # package Metadata::Base; require 5.004; use strict; use vars qw($VERSION $Debug); use Carp; $VERSION = sprintf("%d.%02d", ('$Revision: 1.10 $ ' =~ /\$Revision:\s+(\d+)\.(\d+)/)); # Class debugging $Debug = 0; sub debug { my $self=shift; # Object debug - have an object reference if (ref ($self)) { my $old=$self->{DEBUG}; $self->{DEBUG}=@_ ? shift : 1; return $old; } # Class debug (self is debug level) return $Debug if !defined $self; # Careful, could be debug(0) my $old=$Debug; $Debug=$self; $old; } sub whowasi { (caller(1))[3] } # Constructor sub new ($%) { my ($type,$self)=@_; $self = {} unless defined $self; my $class = ref($type) || $type; bless $self, $class; $self->{DEBUG}=$Debug unless defined $self->{DEBUG}; $self->{DEFAULT_OPTIONS}={ %$self }; # Create empty order if needed $self->{ORDER}=[] if $self->{ORDERED}; $self->{ELEMENTS}={}; $self->{ELEMENTS_COUNT}=0; warn "@{[&whowasi]}\n" if $self->{DEBUG}; $self; } # Clone sub clone ($) { my $self=shift; my $copy=new ref($self); my(@order)=$self->{ORDERED} ? @{$self->{ORDER}} : keys %{$self->{ELEMENTS}}; for my $element (@order) { my(@values)=$self->get($element); $copy->set($element, [ @values ]); } $copy->{DEBUG}=$self->{DEBUG}; $copy->{DEFAULT_OPTIONS}={ %{$self->{DEFAULT_OPTIONS}} }; $copy; } sub set ($$$;$) { my $self=shift; return $self->_set('set',@_); } sub add ($$$;$) { my $self=shift; return $self->_set('add',@_); } sub _set ($$$$;$) { my $self=shift; my $operation=shift; my($element,$value,$index)=$self->validate(@_); return if !defined $element; if (!defined $self->{ELEMENTS}->{$element}) { # Update order push(@{$self->{ORDER}}, $element) if $self->{ORDERED}; $self->{ELEMENTS_COUNT}++; warn "@{[&whowasi]} Adding new element $element\n" if $self->{DEBUG}; } if (ref($value)) { # Assuming eq 'ARRAY' $self->{ELEMENTS}->{$element}=[ @$value ]; warn "@{[&whowasi]} Set $element to values @$value\n" if $self->{DEBUG}; } else { if (defined $index) { # Set new value at a particular index $self->{ELEMENTS}->{$element}->[$index]=$value; } else { if ($operation eq 'add') { # Append value to end of list push(@{$self->{ELEMENTS}->{$element}}, $value); $index=@{$self->{ELEMENTS}->{$element}} - 1; } else { $index='(all)'; $self->{ELEMENTS}->{$element}=[ $value ]; } } warn "@{[&whowasi]} Set $element subvalue $index to value $value\n" if $self->{DEBUG}; } } sub get ($$;$) { my $self=shift; my($element,$index)=@_; warn "@{[&whowasi]} Get $element subvalue ", (defined $index) ? $index : "(undefined)","\n" if $self->{DEBUG}; ($element,$index)=$self->validate_elements($element,$index); return if !defined $element; warn "@{[&whowasi]} After validate, element $element subvalue ", (defined $index) ? $index : "(undefined)", "\n" if $self->{DEBUG}; my $value=$self->{ELEMENTS}->{$element}; return if !defined $value; if (defined $index) { return $value->[$index]; } else { return wantarray ? @$value : join(' ', grep (defined $_, @$value)); } } sub delete ($$;$) { my $self=shift; my($element,$index)=@_; warn "@{[&whowasi]} element $element subvalue ", (defined $index) ? $index : "(undefined)","\n" if $self->{DEBUG}; ($element,$index)=$self->validate_elements($element,$index); return if !defined $element; warn "@{[&whowasi]} After validate, element $element subvalue ", (defined $index) ? $index : "(undefined)", "\n" if $self->{DEBUG}; my $value=$self->{ELEMENTS}->{$element}; return if !defined $value; my(@old)=@{$value}; if (defined $index) { undef $value->[$index]; # Are all element subvalues missing / undefined? If so, then # allow code below to delete entire element. $index=undef if !grep (defined $_, @{$self->{ELEMENTS}->{$element}}); } if (!defined $index) { undef @{$self->{ELEMENTS}->{$element}}; delete $self->{ELEMENTS}->{$element}; $self->{ELEMENTS_COUNT}--; if ($self->{ORDERED}) { @{$self->{ORDER}} = grep ($_ ne $element, @{$self->{ORDER}}); } } return(@old); } sub exists ($$;$) { my $self=shift; my($element,$index)=$self->validate_elements(@_); return if !exists $self->{ELEMENTS}->{$element}; return 1 if !defined $index; # Trying to find sub-element return $self->{ELEMENTS}->{$element}->[$index]; } sub size ($;$) { my $self=shift; my $element=shift; return $self->{ELEMENTS_COUNT} if !defined $element; return if !exists $self->{ELEMENTS}->{$element}; my $value=$self->{ELEMENTS}->{$element}; return scalar(@$value); } sub elements ($) { my $self=shift; return @{$self->{ORDER}} if $self->{ORDERED}; return keys %{$self->{ELEMENTS}}; } # Old name sub fields ($) { sub fields_warn { warn Carp::longmess @_; } fields_warn "Depreciated method called\n"; return shift->elements; } sub order ($;@) { my $self=shift; return unless $self->{ORDERED}; return @{$self->{ORDER}} if !@_; my(@old_order)=@{$self->{ORDER}} if defined wantarray; $self->{ORDER}=[@_]; return @old_order if defined wantarray; } # Set the given element, value and index? sub validate ($$$;$) { my $self=shift; # Not used here #my($self, $element, $value, $index)=@_; return @_; } # Check the legality of the given element and index sub validate_elements ($$;$) { my $self=shift; # Not used here #my($self, $element, $value, $index)=@_; return @_; } # Return a native-formatted version of this metadata sub format ($) { my $self=shift; my $string=$self->{ELEMENTS_COUNT}." elements\n"; my(@order)=$self->{ORDERED} ? @{$self->{ORDER}} : keys %{$self->{ELEMENTS}}; $string.="Order: @order\n" if $self->{ORDERED}; for my $element (@order) { for my $value ($self->get($element)) { $string.="$element: $value\n"; } } $string; } # Probably possible to do this using symbol table references sub as_string ($) { shift->format; } # Pack the metadata as small as possible - binary OK and preferred sub pack ($) { my $self=shift; my(@order)=$self->{ORDERED} ? @{$self->{ORDER}} : keys %{$self->{ELEMENTS}}; my $string=''; for my $element (@order) { for my $value ($self->get($element)) { $value='' if !defined $value; $string.="$element\0$value\0"; } } $string; } # Read the packed format and restore the same metadata state sub unpack ($$) { my $self=shift; my $value=shift; return if !defined $value; $self->clear; my(@vals)=(split(/\0/,$value)); while(@vals) { my($element,$value)=splice(@vals,0,2); $self->add($element,$value); } 1; } sub read ($) { confess "Not implemented in base class\n"; } sub write ($$) { my $self=shift; my $fd=shift; print $fd $self->format; } sub reset ($) { my $self=shift; my $default_options=$self->{DEFAULT_OPTIONS}; while(my($attr,$value)=each %$default_options) { $self->{$attr}=$value; } $self->clear; } sub clear ($) { my $self=shift; $self->{ELEMENTS}={}; $self->{ELEMENTS_COUNT}=0; # Empty order if needed $self->{ORDER}=[] if $self->{ORDERED}; } sub get_date_as_seconds ($$) { my $self=shift; iso8601_to_seconds($self->get(shift)); } sub set_date_as_seconds ($$$) { my $self=shift; my($element,$value)=shift; $self->set($element, seconds_to_iso8601($value)); } sub get_date_as_iso8601 ($$) { my $self=shift; $self->get(shift); } sub set_date_as_iso8601 ($$$) { my $self=shift; $self->set(@_); } sub seconds_to_iso8601 ($) { my($ss,$mm,$hh,$day,$month,$year)=gmtime(shift); sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ", $year+1900, $month+1,$day,$hh,$mm,$ss); } sub iso8601_to_seconds ($) { my $value=shift; my($year,$month,$day,$hh,$mm,$ss,$tz)= ($value =~ m{ ^ (\d\d\d\d) (?: # year YYYY required - (\d\d) (?: # month -MM optional - (\d\d) (?: # day -DD optional T (\d\d) : (\d\d) (?: # time 'T'HH:MM optional (?: : (\d\d (?: \.\d+)?) )? # :SS :SS.frac opt. followed by (Z | (?: [+-]\d+:\d+)) # 'Z' | +/-HH:MM timezone )? # optional TZ/SS/SS+TZ )? # optional THH:MM .. )? # optional -DD... )? # optional -MM... $ }x); return if !defined $year; # Round to start of year, month, etc. since it is too difficult to round # to the end (leap years). # Really it should return two values for the start & end of period # - maybe in V2.0 $month ||=1; $day ||=1; $hh ||=0; $mm ||=0; $ss ||=0; $tz ||='Z'; $tz='' if $tz eq 'Z'; require 'Time/Local.pm'; $value=Time::Local::timegm(int($ss),$mm,$hh,$day,$month-1,$year-1900); if ($tz =~ /^(.)(\d+):(\d+)$/) { my $s=(($2*60)+$3)*60; $value= ($1 eq '+') ? $value+$s : $value-$s; } if ($ss=~ /(\.\d+)$/) { $value.= $1; # Note string concatenation } $value; } 1; __END__ =head1 NAME Metadata::Base - base class for metadata =head1 SYNOPSIS package Metadata::FOO use vars(@ISA); ... @ISA=qw(Metadata::Base); ... =head1 DESCRIPTION Metadata:Base class - the core functionality for handling metadata. =head1 CONSTRUCTOR =over 4 =item new [OPTIONS] Create a new Metadata object with an optional hash of options to describe the metadata characteristics. Currently only the following can be set: =over 4 =item DEBUG Set if debugging should be enabled from creation. This can also be set and read by the B method below. If this is not defined, it is set to the current class debugging state which can be read from the class method L described below. =item ORDERED Set if the metadata elements are ordered =back =head1 COPY CONSTRUCTOR =over 4 =item clone Create a new identical Metadata object from this one. =back =head1 CLASS METHODS =over 4 =item debug [VALUE] If I is given, sets the debugging state of this class and returns the old state. Otherwise returns the current debugging state. =item seconds_to_iso8601 SECONDS Convert the I value to (subset of) ISO-8601 format YYYY-MM-DDThh:mm:SSZ representing the GMT/UTC value. =item iso8601_to_seconds VALUE Convert 6 ISO-8601 subset formats to a seconds value. The 6 formats are those proposed for the Dublin Core date use: YYYY YYYY-MM YYYY-MM-DD YYYY-MM-DDThh:mm YYYY-MM-DDThh:mm:ssTZ YYYY-MM-DDThh:mm:ss.ssTZ where TZ can be 'Z', +hh:mm or -hh:mm B: This method rounds towards the start of the period (it should really return two values for start and end). =back =head1 METHODS =over 4 =item debug [VALUE] If I is given, sets the debugging state of this object and returns the old state. Otherwise returns the current debugging state. The default debugging state is determined by the class debug state. =item set ELEMENT, VALUE, [INDEX] Set element I to I. If I is an array reference, the existing array is used to as all the existing sub-values. Otherwise if I is given, sets the particular sub-value of I, otherwise appends to the end of the existing list of sub-values for I. =item get ELEMENT, [INDEX] Return the contents of the given I. In an array context returns the sub-values as an array, in a scalar context they are all returned separated by spaces. If I is given, returns the value of the given sub-value. =item delete ELEMENT, [INDEX} Delete the given I. If an I is given, remove just that sub-value. =item exists ELEMENT, [INDEX] Returns a defined value if the given I and/or sub-value I exists. =item size [ELEMENT] Returns number of elements with no argument or the number of subvalues for the given I or undef if I does not exist. =item elements Return a list of the elements (in the correct order if there is one). =item order [ORDER] If I is given, sets that as the order of the elements and returns the old order list. Otherwise, returns the current order of the elements. If the elements are not ordered, returns undef. =item validate ELEMENT, VALUE, [INDEX] This method is intended to be overriden by subclasses. It is called when a element value is being set. The method should return either a list of I, I and I values to use or an undefined value in which case no element will be set. =item validate_elements ELEMENT, [INDEX] This method is intended to be overriden by subclasses. It is called when a element and/or index is being read. The method should return a list of I and I values to use. =item as_string =item format Returns a string representing the metadata, suitable for storing (in a text file). This is different from the B method because this value is meant to be the native encoding format of the metadata, usually human readable, wheras B uses the minimum space. =item pack Return a packed string representing the metadata format. This can be used with B to restore the values. =item unpack VALUE Initialise the metadata from the packed I which must be the value made by the B method. =item read HANDLE Reads from the given file handle and initialises the metadata elements. Returns undef if end of file is seen. =item write HANDLE Write to the given file handle a formatted version of this metadata format. Likely to use B in most implementations. =item reset Reset the metadata object to the default ones (including any passed with the constructor) and then do a I. =item clear Remove any stored elements in this metadata object. This can be used in conjuction with I to prevent the overhead of many I operations when reading metadata objects from files. =item get_date_as_seconds ELEMENT Assuming I is stored in a date format, returns the number of seconds since 1st January 1970. =item set_date_as_seconds ELEMENT, VALUE Set I encoded as a date corresponding to I which is the number of seconds since 1st January 1970. =back =head1 AUTHOR By Dave Beckett - http://purl.org/net/dajobe/ =head1 COPYRIGHT Copyright (C) 1997-2001 Dave Beckett - http://purl.org/net/dajobe/ All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut