/lib/OurNet/BBS/Base.pm
Perl | 581 lines | 383 code | 135 blank | 63 comment | 53 complexity | c3224da3c56dade1c42856801bc1f0b5 MD5 | raw file
- # $File: //depot/libOurNet/BBS/lib/OurNet/BBS/Base.pm $ $Author: autrijus $
- # $Revision: #8 $ $Change: 3850 $ $DateTime: 2003/01/25 20:03:29 $
- package OurNet::BBS::Base;
- use 5.006;
- use strict;
- no warnings 'deprecated';
- use constant EGO => 0; use constant FLAG => 1;
- use constant HASH => 1; use constant ARRAY => 2;
- use constant CODE => 3; use constant GLOB => 4;
- use constant TYPES => [qw/_ego _hash _array _code _glob/];
- use constant SIGILS => [qw/$ % @ & */];
- require PerlIO if $] >= 5.008;
- # These magical hashes below holds all cached initvar constants:
- # = subrountines as $RegSub{$glob}
- # = module imports as $RegMod{$glob}
- # = variables as $RegVar{$class}{$sym}
- my (%RegVar, %RegSub, %RegMod);
- my %Packlists; # $packlist cache for contains()
- ## Class Methods ######################################################
- # These methods expects a package name as their first argument.
- # constructor method; turn into an pseudo hash if _phash exists
- use constant CONSTRUCTOR => << '.';
- sub __PKG__::new {
- my __PACKAGE__ $self = bless([\%{__PKG__::FIELDS}], '__PACKAGE__');
- # eval {
- if (ref($_[1])) {
- # Passed in a single hashref -- assign it!
- %{$self} = %{$_[1]};
- }
- else {
- # Automagically fill in the fields.
- $self->{$_} = $_[$self->[0]{$_}] foreach ((__KEYS__)[0 .. $#_-1]);
- }
- # };
- # require Carp and Carp::confess($@) if $@;
-
- __TIE__
- return $self->{_ego} = bless (\[$self, __OBJ__], '__PKG__');
- }
- 1;
- .
- # import does following things:
- # 1. set up @ISA.
- # 2. export type constants.
- # 3. set overload bits.
- # 4. install accessor methods.
- # 5. handle variable propagation.
- # 6. install the new() handler.
- require overload; # no import, please
- sub import {
- my $class = shift;
- my $pkg = caller(0);
- no strict 'refs';
- no warnings 'once';
- # in non-direct usage, only ournet client gets symbols and sigils.
- my $is_client = ($pkg eq 'OurNet::BBS::Client' or $pkg eq 'OurNet::BBS::OurNet::BBS');
- return unless $class eq __PACKAGE__ or $is_client;
- *{"$pkg\::$_"} = \&{$_} foreach qw/EGO FLAG HASH ARRAY CODE GLOB/;
- return *{"$pkg\::SIGILS"} = \&{SIGILS} if $is_client;
- *{"$pkg\::ego"} = sub { ${$_[0]}->[0] };
- push @{"$pkg\::ISA"}, $class;
- my (@overload, $tie_eval, $obj_eval);
- my $fields = \%{"$pkg\::FIELDS"};
- foreach my $type (HASH .. GLOB) {
- if (exists($fields->{TYPES->[$type]})) { # checks for _hash .. _glob
- my $sigil = SIGILS->[$type];
- push @overload, "$sigil\{}" => sub {
- # use Carp; eval { ${$_[0]}->[$type] } || Carp::confess($@)
- ${$_[0]}->[$type]
- };
- if ($type == HASH or $type == ARRAY) {
- $tie_eval = "tie my ${sigil}obj => '$pkg', ".
- "[\$self, $type];\n" . $tie_eval;
- $obj_eval .= ", \\${sigil}obj";
- }
- elsif ($type == CODE) {
- $tie_eval .= 'my $code = sub { $self->refresh(undef, CODE);'.
- '$self->{_code}(@_) };';
- $obj_eval .= ', $code';
- }
- elsif ($type == GLOB) {
- $tie_eval = 'my $glob = \$self->{_glob};' . $tie_eval;
- $obj_eval .= ', $glob';
- }
- }
- else {
- $obj_eval .= ', undef';
-
- }
- }
- $obj_eval =~ s/(?:, undef)+$//;
- my $sub_new = CONSTRUCTOR;
- my $keys = join(' ', sort {
- $fields->{$a} <=> $fields->{$b}
- } grep {
- /^[^_]/
- } keys(%{$fields}));
- $sub_new =~ s/__TIE__/$tie_eval/g;
- $sub_new =~ s/__OBJ__/$obj_eval/g;
- $sub_new =~ s/__PKG__/$pkg/g;
- $sub_new =~ s/__KEYS__/qw{$keys}/g;
- $sub_new =~ s/__PACKAGE__/OurNet::BBS::Base/g;
- unless (eval $sub_new) {
- require Carp;
- Carp::confess "$sub_new\n\n$@";
- }
- $pkg->overload::OVERLOAD(
- @overload,
- '""' => sub { overload::AddrRef($_[0]) },
- '0+' => sub { 0 },
- 'bool' => sub { 1 },
- 'cmp' => sub { "$_[0]" cmp "$_[1]" },
- '<=>' => sub { "$_[0]" cmp "$_[1]" }, # for completeness' sake
- );
- # install accessor methods
- unless (UNIVERSAL::can($pkg, '__accessor')) {
- foreach my $property (keys(%{"$pkg\::FIELDS"}), '__accessor') {
- *{"$pkg\::$property"} = sub {
- my $self = ${$_[0]}->[EGO];
- $self->refresh_meta;
- $self->{$property} = $_[1] if $#_;
- return $self->{$property};
- };
- }
- }
- # my $backend = $1 if $pkg =~ m|^OurNet::BBS::([^:]+)|;
- my $backend = substr($pkg, 13, index($pkg, ':', 14) - 13); # fast
- my @defer; # delayed aliasing until variables are processed
- foreach my $parent (@{"$pkg\::ISA"}) {
- next if $parent eq __PACKAGE__; # Base won't use mutable variables
- while (my ($sym, $ref) = each(%{"$parent\::"})) {
- push @defer, ($pkg, $sym, $ref);
- }
- unshift @_, @{$RegMod{$parent}} if ($RegMod{$parent});
- }
- while (my ($mod, $symref) = splice(@_, 0, 2)) {
- if ($mod =~ m/^\w/) { # getvar from other modules
- push @{$RegMod{$pkg}}, $mod, $symref;
- require "OurNet/BBS/$backend/$mod.pm";
- $mod = "OurNet::BBS::$backend\::$mod";
- foreach my $symref (@{$symref}) {
- my ($ch, $sym) = CORE::unpack('a1a*', $symref);
- die "can't import: $mod\::$sym" unless *{"$mod\::$sym"};
- ++$RegVar{$pkg}{$sym};
- *{"$pkg\::$sym"} = (
- $ch eq '$' ? \${"$mod\::$sym"} :
- $ch eq '@' ? \@{"$mod\::$sym"} :
- $ch eq '%' ? \%{"$mod\::$sym"} :
- $ch eq '*' ? \*{"$mod\::$sym"} :
- $ch eq '&' ? \&{"$mod\::$sym"} : undef
- );
- }
- }
- else { # this module's own setvar
- my ($ch, $sym) = CORE::unpack('a1a*', $mod);
- *{"$pkg\::$sym"} = ($ch eq '$') ? \$symref : $symref;
- ++$RegVar{$pkg}{$sym};
- }
- }
- my @defer_sub; # further deferred subroutines that needs localizing
- while (my ($pkg, $sym, $ref) = splice(@defer, 0, 3)) {
- next if exists $RegVar{$pkg}{$sym} # already imported
- or *{"$pkg\::$sym"}{CODE}; # defined by use subs
- if (defined(&{$ref})) {
- push @defer_sub, ($pkg, $sym, $ref);
- next;
- }
- next unless ($ref =~ /^\*(.+)::(.+)/)
- and exists $RegVar{$1}{$2};
- *{"$pkg\::$sym"} = $ref;
- ++$RegVar{$pkg}{$sym};
- }
- # install per-package wrapper handlers for mutable variables
- while (my ($pkg, $sym, $ref) = splice(@defer_sub, 0, 3)) {
- my $ref = ($RegSub{$ref} || $ref);
- next unless ($ref =~ /^\*(.+)::([^:]+)$/);
- next if defined(&{"$pkg\::$sym"});
- if (%{$RegVar{$pkg}}) {
- eval qq(
- sub $pkg\::$sym {
- ) . join('',
- map { qq(
- local *$1\::$_ = *$pkg\::$_;
- )} (keys(%{$RegVar{$pkg}}))
- ) . qq(
- &{$ref}(\@_);
- };
- );
- }
- else {
- *{"$pkg\::$sym"} = $ref;
- };
- $RegSub{"*$pkg\::$sym"} = $ref;
- }
- return unless $OurNet::BBS::Encoding;
- *{"$pkg\::unpack"} = \&_unpack;
- *{"$pkg\::pack"} = \&_pack;
- }
- sub _unpack {
- require Encode;
- return map Encode::decode($OurNet::BBS::Encoding => $_), CORE::unpack($_[0], $_[1]);
- }
- sub _pack {
- require Encode;
- return CORE::pack($_[0], map Encode::encode($OurNet::BBS::Encoding => $_), @_[1..$#_]);
- }
- ## Instance Methods ###################################################
- # These methods expects a tied object as their first argument.
- # unties through an object to get back the true $self
- sub ego { $_[0] }
- # the all-important cache refresh instance method
- sub refresh {
- my $self = shift;
- my $ego;
- ($self, $ego) = (ref($self) eq __PACKAGE__)
- ? ($self->{_ego}, $self)
- : ($self, ${$self}->[EGO]);
- no strict 'refs';
- my $prefix = ref($self)."::refresh_";
- my $method = $_[0] && defined(&{"$prefix$_[0]"})
- ? "$prefix$_[0]" : $prefix.'meta';
- return $method->($ego, @_);
- }
- # opens access to connections via OurNet protocol
- sub daemonize {
- require OurNet::BBS::Server;
- OurNet::BBS::Server->daemonize(@_);
- }
- =begin comment
- # The following code doesn't work, because they always override.
- # permission checking; fall-back for undefined packages
- sub writeok {
- my ($self, $user, $op, $argref) = @_;
- print "warning: permission model for ".ref($self)." unimplemented.\n".
- " access forbidden for user ".$user->id().".\n"
- if $OurNet::BBS::DEBUG;
- return;
- }
- # ditto
- sub readok {
- my ($self, $user, $op, $argref) = @_;
- print "warning: permission model for ".ref($self)." unimplemented.\n".
- " access forbidden for user ".$user->id().".\n"
- if $OurNet::BBS::DEBUG;
- return;
- }
- =end comment
- =cut
- # clears internal memory; uses CLEAR instead
- sub purge {
- $_[0]->ego->{_ego}->CLEAR;
- }
- # returns the BBS backend for the object
- sub backend {
- my $backend = ref($_[0]);
- $backend = ref($_[0]{_ego}) if $backend eq __PACKAGE__;
- $backend = substr($backend, 13, index($backend, ':', 14) - 13); # fast
- # $backend = $1 if $backend =~ m|^OurNet::BBS::(\w+)|;
- return $backend;
- }
- # developer-friendly way to check files' timestamp for mtime fields
- sub filestamp {
- my ($self, $file, $field, $check_only) = @_;
- my $time = (stat($file))[9];
- no warnings 'uninitialized';
- return 1 if $self->{$field ||= 'mtime'} == $time;
- $self->{$field} = $time unless $check_only;
- return 0; # something changed
- }
- # developer-friendly way to check timestamp for mtime fields
- sub timestamp {
- my ($self, $time, $field, $check_only) = @_;
- no warnings 'uninitialized';
- return 1 if $self->{$field ||= 'mtime'} == $time;
- $self->{$field} = $time unless $check_only;
- return 0; # something changed
- }
- # check if something's in packlist; packages don't contain undef
- sub contains {
- my ($self, $key) = @_;
- $self = $self->{_ego} if ref($self) eq __PACKAGE__;
- no strict 'refs';
- no warnings 'uninitialized';
- # print "checking $key against $self: @{ref($self).'::packlist'}\n";
- return (length($key) and index(
- $Packlists{ref($self)} ||= " @{ref($self).'::packlist'} ",
- " $key ",
- ) > -1);
- }
- # loads a module: ($self, $backend, $module).
- sub fillmod {
- my $self = $_[0];
- $self =~ s|::|/|g;
-
- require "$self/$_[1]/$_[2].pm";
- return "$_[0]::$_[1]::$_[2]";
- }
- # create a new module and fills in arguments in the expected order
- sub fillin {
- my ($self, $key, $class) = splice(@_, 0, 3);
- return if defined($self->{_hash}{$key});
- $self->{_hash}{$key} = OurNet::BBS->fillmod(
- $self->{backend}, $class
- )->new(@_);
- return 1;
- }
- # returns the module in the same backend, or $val's package if supplied
- sub module {
- my ($self, $mod, $val) = @_;
- if ($val and UNIVERSAL::isa($val, 'UNIVERSAL')) {
- my $pkg = ref($val);
- if (UNIVERSAL::isa($val, 'HASH')) {
- # special case: somebody blessed a hash to put into STORE.
- bless $val, 'main'; # you want black magic?
- $_[2] = \%{$val}; # curse (unbless) it!
- }
- return $pkg;
- }
- my $backend = $self->backend;
- require "OurNet/BBS/$backend/$mod.pm";
- return "OurNet::BBS::$backend\::$mod";
- }
- # object serialization for OurNet::Server calls; does nothing otherwise
- sub SPAWN { return $_[0] }
- sub REF { return ref($_[0]) }
- sub KEYS { return keys(%{$_[0]}) }
- # XXX: Object injection
- sub INJECT {
- my ($self, $code, @param) = @_;
- if (UNIVERSAL::isa($code, 'CODE')) {
- require B::Deparse;
- my $deparse = B::Deparse->new(qw/-p -sT/);
- $code = $deparse->coderef2text($code);
- $code =~ s/^\s+use (?:strict|warnings)[^;\n]*;\n//m;
- }
- require Safe;
- my $safe = Safe->new;
- $safe->permit_only(qw{
- :base_core padsv padav padhv padany rv2gv refgen srefgen ref gvsv gv gelem
- });
- my $result = $safe->reval("sub $code");
- warn $@ if $@;
- return sub { $result->($self, @_) };
- }
- ## Tiescalar Accessors ################################################
- # XXX: Experimental: Globs only.
- sub TIESCALAR {
- return bless(\$_[1], $_[0]);
- }
- ## Tiearray Accessors #################################################
- # These methods expects a raw (untied) object as their first argument.
- # merged hasharray!
- sub TIEARRAY {
- return bless(\$_[1], $_[0]);
- }
- sub FETCHSIZE {
- my ($self, $key) = @_;
- my ($ego, $flag) = @{${$self}};
- $self->refresh(undef, ARRAY);
- return scalar @{$ego->{_array} ||= []};
- }
- sub PUSH {
- my $self = shift;
- my $size = $self->FETCHSIZE;
- foreach my $item (@_) {
- $self->STORE($size++, $item);
- }
- }
- ## Tiehash Accessors ##################################################
- # These methods expects a raw (untied) object as their first argument.
- # the Tied Hash constructor method
- sub TIEHASH {
- return bless(\$_[1], $_[0]);
- }
- # fetch accessesor
- sub FETCH {
- my ($self, $key) = @_;
- my ($ego, $flag) = @{${$self}};
- $self->refresh($key, $flag);
- return ($flag == HASH) ? $ego->{_hash}{$key} : $ego->{_array}[$key];
- }
- # fallback implementation to STORE
- sub STORE {
- die "@_: STORE unimplemented";
- }
- # delete an element; calls its remove() subroutine to handle actual removal
- sub DELETE {
- my ($self, $key) = @_;
- my ($ego, $flag) = @{${$self}};
- $self->refresh($key, $flag);
- if ($flag == HASH) {
- return unless exists $ego->{_hash}{$key};
- $ego->{_hash}{$key}->ego->remove
- if UNIVERSAL::can($ego->{_hash}{$key}, 'ego');
- return delete($ego->{_hash}{$key});
- }
- else {
- return unless exists $ego->{_array}[$key];
- $ego->{_array}[$key]->ego->remove
- if UNIVERSAL::can($ego->{_array}[$key], 'ego');
- return delete($ego->{_array}[$key]);
- }
- }
- # check for existence of a key.
- sub EXISTS {
- my ($self, $key) = @_;
- my ($ego, $flag) = @{${$self}};
- $self->refresh($key, $flag);
- return ($flag == HASH) ? exists $ego->{_hash}{$key}
- : exists $ego->{_array}[$key];
- }
- # iterator; this one merely uses 'scalar keys()'
- sub FIRSTKEY {
- my $self = $_[0];
- my $ego = ${$self}->[EGO];
- $ego->refresh_meta(undef, HASH);
- scalar keys (%{$ego->{_hash}});
- return $self->NEXTKEY;
- }
- # ditto
- sub NEXTKEY {
- my $self = $_[0];
- return each %{${$self}->[EGO]->{_hash}};
- }
- # empties the cache, do not DELETE the objects themselves
- sub CLEAR {
- my $self = ${$_[0]}->[EGO];
- %{$self->{_hash}} = () if (exists $self->{_hash});
- @{$self->{_array}} = () if (exists $self->{_array});
- }
- # could care less
- sub DESTROY () {};
- sub UNTIE () {};
- our $AUTOLOAD;
- sub AUTOLOAD {
- my $action = substr($AUTOLOAD, (
- (rindex($AUTOLOAD, ':') - 1) || return
- ));
- no strict 'refs';
- *{$AUTOLOAD} = sub {
- use Carp; confess ref($_[0]->{_ego}).$action
- unless defined &{ref($_[0]->{_ego}).$action};
- goto &{ref($_[0]->{_ego}).$action}
- };
- goto &{$AUTOLOAD};
- }
- 1;