#============================================================= -*-Perl-*-
#
# Template::Plugins
#
# DESCRIPTION
#   Plugin provider which handles the loading of plugin modules and
#   instantiation of plugin objects.
#
# AUTHORS
#   Andy Wardley <abw@wardley.org>
#
# COPYRIGHT
#   Copyright (C) 1996-2022 Andy Wardley.  All Rights Reserved.
#   Copyright (C) 1998-2000 Canon Research Centre Europe Ltd.
#
#   This module is free software; you can redistribute it and/or
#   modify it under the same terms as Perl itself.
#
#============================================================================

package Template::Plugins;

use strict;
use warnings;
use base 'Template::Base';
use Template::Constants;

our $VERSION = '3.100';
our $DEBUG   = 0 unless defined $DEBUG;
our $PLUGIN_BASE = 'Template::Plugin';
our $STD_PLUGINS = {
    'assert'     => 'Template::Plugin::Assert',
    'cgi'        => 'Template::Plugin::CGI',
    'datafile'   => 'Template::Plugin::Datafile',
    'date'       => 'Template::Plugin::Date',
    'debug'      => 'Template::Plugin::Debug',
    'directory'  => 'Template::Plugin::Directory',
    'dbi'        => 'Template::Plugin::DBI',
    'dumper'     => 'Template::Plugin::Dumper',
    'file'       => 'Template::Plugin::File',
    'format'     => 'Template::Plugin::Format',
    'html'       => 'Template::Plugin::HTML',
    'image'      => 'Template::Plugin::Image',
    'iterator'   => 'Template::Plugin::Iterator',
    'latex'      => 'Template::Plugin::Latex',
    'pod'        => 'Template::Plugin::Pod',
    'scalar'     => 'Template::Plugin::Scalar',
    'table'      => 'Template::Plugin::Table',
    'url'        => 'Template::Plugin::URL',
    'view'       => 'Template::Plugin::View',
    'wrap'       => 'Template::Plugin::Wrap',
    'xml'        => 'Template::Plugin::XML',
    'xmlstyle'   => 'Template::Plugin::XML::Style',
};


#========================================================================
#                         -- PUBLIC METHODS --
#========================================================================

#------------------------------------------------------------------------
# fetch($name, \@args, $context)
#
# General purpose method for requesting instantiation of a plugin
# object.  The name of the plugin is passed as the first parameter.
# The internal FACTORY lookup table is consulted to retrieve the
# appropriate factory object or class name.  If undefined, the _load()
# method is called to attempt to load the module and return a factory
# class/object which is then cached for subsequent use.  A reference
# to the calling context should be passed as the third parameter.
# This is passed to the _load() class method.  The new() method is
# then called against the factory class name or prototype object to
# instantiate a new plugin object, passing any arguments specified by
# list reference as the second parameter.  e.g. where $factory is the
# class name 'MyClass', the new() method is called as a class method,
# $factory->new(...), equivalent to MyClass->new(...) .  Where
# $factory is a prototype object, the new() method is called as an
# object method, $myobject->new(...).  This latter approach allows
# plugins to act as Singletons, cache shared data, etc.
#
# Returns a reference to a plugin, (undef, STATUS_DECLINE) to decline
# the request or ($error, STATUS_ERROR) on error.
#------------------------------------------------------------------------

sub fetch {
    my ($self, $name, $args, $context) = @_;
    my ($factory, $plugin, $error);

    $self->debug("fetch($name, ",
                 defined $args ? ('[ ', join(', ', @$args), ' ]') : '<no args>', ', ',
                 defined $context ? $context : '<no context>',
                 ')') if $self->{ DEBUG };

    # NOTE:
    # the $context ref gets passed as the first parameter to all regular
    # plugins, but not to those loaded via LOAD_PERL;  to hack around
    # this until we have a better implementation, we pass the $args
    # reference to _load() and let it unshift the first args in the
    # LOAD_PERL case

    $args ||= [ ];
    unshift @$args, $context;

    $factory = $self->{ FACTORY }->{ $name } ||= do {
        ($factory, $error) = $self->_load($name, $context);
        return ($factory, $error) if $error;			## RETURN
        $factory;
    };

    # call the new() method on the factory object or class name
    eval {
        if (ref $factory eq 'CODE') {
            defined( $plugin = &$factory(@$args) )
                || die "$name plugin failed\n";
        }
        else {
            defined( $plugin = $factory->new(@$args) )
                || die "$name plugin failed: ", $factory->error(), "\n";
        }
    };
    if ($error = $@) {
    return $self->{ TOLERANT }
           ? (undef,  Template::Constants::STATUS_DECLINED)
           : ($error, Template::Constants::STATUS_ERROR);
    }

    return $plugin;
}



#========================================================================
#                        -- PRIVATE METHODS --
#========================================================================

#------------------------------------------------------------------------
# _init(\%config)
#
# Private initialisation method.
#------------------------------------------------------------------------

sub _init {
    my ($self, $params) = @_;
    my ($pbase, $plugins, $factory) =
        @$params{ qw( PLUGIN_BASE PLUGINS PLUGIN_FACTORY ) };

    $plugins ||= { };

    # update PLUGIN_BASE to an array ref if necessary
    $pbase = [ ] unless defined $pbase;
    $pbase = [ $pbase ] unless ref($pbase) eq 'ARRAY';

    # add default plugin base (Template::Plugin) if set
    push(@$pbase, $PLUGIN_BASE) if $PLUGIN_BASE;

    $self->{ PLUGIN_BASE } = $pbase;
    $self->{ PLUGINS     } = { %$STD_PLUGINS, %$plugins };
    $self->{ TOLERANT    } = $params->{ TOLERANT }  || 0;
    $self->{ LOAD_PERL   } = $params->{ LOAD_PERL } || 0;
    $self->{ FACTORY     } = $factory || { };
    $self->{ DEBUG       } = ( $params->{ DEBUG } || 0 )
                             & Template::Constants::DEBUG_PLUGINS;

    return $self;
}



#------------------------------------------------------------------------
# _load($name, $context)
#
# Private method which attempts to load a plugin module and determine the
# correct factory name or object by calling the load() class method in
# the loaded module.
#------------------------------------------------------------------------

sub _load {
    my ($self, $name, $context) = @_;
    my ($factory, $module, $base, $pkg, $file, $ok, $error);

    if ($module = $self->{ PLUGINS }->{ $name } || $self->{ PLUGINS }->{ lc $name }) {
        # plugin module name is explicitly stated in PLUGIN_NAME
        $pkg = $module;
        ($file = $module) =~ s|::|/|g;
        $self->debug("loading $module.pm (PLUGIN_NAME)")
            if $self->{ DEBUG };
        $ok = eval { $module->isa($PLUGIN_BASE) or require "$file.pm" };
        $error = $@;
    }
    else {
        # try each of the PLUGIN_BASE values to build module name
        ($module = $name) =~ s/\./::/g;

        foreach $base (@{ $self->{ PLUGIN_BASE } }) {
            $pkg = $base . '::' . $module;
            ($file = $pkg) =~ s|::|/|g;

            $self->debug("loading $file.pm (PLUGIN_BASE)")
                if $self->{ DEBUG };

            $ok = eval { $pkg->isa($PLUGIN_BASE) or require "$file.pm" };
            last unless $@;

            $error .= "$@\n"
                unless ($@ =~ /^Can't locate \Q$file\E\.pm/);
        }
    }

    if ($ok) {
        $self->debug("calling $pkg->load()") if $self->{ DEBUG };

	$factory = eval { $pkg->load($context) };
        $error   = '';
        if ($@ || ! $factory) {
            $error = $@ || 'load() returned a false value';
        }
    }
    elsif ($self->{ LOAD_PERL }) {
        # fallback - is it a regular Perl module?
        ($file = $module) =~ s|::|/|g;
        eval { require "$file.pm" };
        if ($@) {
            $error = $@;
        }
        else {
            # this is a regular Perl module so the new() constructor
            # isn't expecting a $context reference as the first argument;
            # so we construct a closure which removes it before calling
            # $module->new(@_);
            $factory = sub {
                shift;
                $module->new(@_);
            };
            $error   = '';
        }
    }

    if ($factory) {
        $self->debug("$name => $factory") if $self->{ DEBUG };
        return $factory;
    }
    elsif ($error) {
        return $self->{ TOLERANT }
            ? (undef,  Template::Constants::STATUS_DECLINED)
            : ($error, Template::Constants::STATUS_ERROR);
    }
    else {
        return (undef, Template::Constants::STATUS_DECLINED);
    }
}

1;

__END__

=head1 NAME

Template::Plugins - Plugin provider module

=head1 SYNOPSIS

    use Template::Plugins;

    $plugin_provider = Template::Plugins->new(\%options);

    ($plugin, $error) = $plugin_provider->fetch($name, @args);

=head1 DESCRIPTION

The C<Template::Plugins> module defines a provider class which can be used
to load and instantiate Template Toolkit plugin modules.

=head1 METHODS

=head2 new(\%params)

Constructor method which instantiates and returns a reference to a
C<Template::Plugins> object.  A reference to a hash array of configuration
items may be passed as a parameter.  These are described below.

Note that the L<Template> front-end module creates a C<Template::Plugins>
provider, passing all configuration items.  Thus, the examples shown
below in the form:

    $plugprov = Template::Plugins->new({
        PLUGIN_BASE => 'MyTemplate::Plugin',
        LOAD_PERL   => 1,
        ...
    });

can also be used via the L<Template> module as:

    $ttengine = Template->new({
        PLUGIN_BASE => 'MyTemplate::Plugin',
        LOAD_PERL   => 1,
        ...
    });

as well as the more explicit form of:

    $plugprov = Template::Plugins->new({
        PLUGIN_BASE => 'MyTemplate::Plugin',
        LOAD_PERL   => 1,
        ...
    });

    $ttengine = Template->new({
        LOAD_PLUGINS => [ $plugprov ],
    });

=head2 fetch($name, @args)

Called to request that a plugin of a given name be provided. The relevant
module is first loaded (if necessary) and the
L<load()|Template::Plugin#load()> class method called to return the factory
class name (usually the same package name) or a factory object (a prototype).
The L<new()|Template::Plugin#new()> method is then called as a class or object
method against the factory, passing all remaining parameters.

Returns a reference to a new plugin object or C<($error, STATUS_ERROR)>
on error.  May also return C<(undef, STATUS_DECLINED)> to decline to
serve the request.  If C<TOLERANT> is set then all errors will be
returned as declines.

=head1 CONFIGURATION OPTIONS

The following list summarises the configuration options that can be provided
to the C<Template::Plugins> L<new()> constructor.  Please consult
L<Template::Manual::Config> for further details and examples of each
configuration option in use.

=head2 PLUGINS

The L<PLUGINS|Template::Manual::Config#PLUGINS> option can be used to provide
a reference to a hash array that maps plugin names to Perl module names.

    my $plugins = Template::Plugins->new({
        PLUGINS => {
            cgi => 'MyOrg::Template::Plugin::CGI',
            foo => 'MyOrg::Template::Plugin::Foo',
            bar => 'MyOrg::Template::Plugin::Bar',
        },
    });

=head2 PLUGIN_BASE

If a plugin is not defined in the L<PLUGINS|Template::Manual::Config#PLUGINS>
hash then the L<PLUGIN_BASE|Template::Manual::Config#PLUGIN_BASE> is used to
attempt to construct a correct Perl module name which can be successfully
loaded.

    # single value PLUGIN_BASE
    my $plugins = Template::Plugins->new({
        PLUGIN_BASE => 'MyOrg::Template::Plugin',
    });

    # multiple value PLUGIN_BASE
    my $plugins = Template::Plugins->new({
        PLUGIN_BASE => [   'MyOrg::Template::Plugin',
                           'YourOrg::Template::Plugin'  ],
    });

=head2 LOAD_PERL

The L<LOAD_PERL|Template::Manual::Config#LOAD_PERL> option can be set to allow
you to load regular Perl modules (i.e. those that don't reside in the
C<Template::Plugin> or another user-defined namespace) as plugins.

If a plugin cannot be loaded using the
L<PLUGINS|Template::Manual::Config#PLUGINS> or
L<PLUGIN_BASE|Template::Manual::Config#PLUGIN_BASE> approaches then,
if the L<LOAD_PERL|Template::Manual::Config#LOAD_PERL> is set, the
provider will make a final attempt to load the module without prepending any
prefix to the module path.

Unlike regular plugins, modules loaded using L<LOAD_PERL|Template::Manual::Config#LOAD_PERL>
do not receive a L<Template::Context> reference as the first argument to the
C<new()> constructor method.

=head2 TOLERANT

The L<TOLERANT|Template::Manual::Config#TOLERANT> flag can be set to indicate
that the C<Template::Plugins> module should ignore any errors encountered while
loading a plugin and instead return C<STATUS_DECLINED>.

=head2 DEBUG

The L<DEBUG|Template::Manual::Config#DEBUG> option can be used to enable
debugging messages for the C<Template::Plugins> module by setting it to
include the C<DEBUG_PLUGINS> value.

    use Template::Constants qw( :debug );

    my $template = Template->new({
        DEBUG => DEBUG_FILTERS | DEBUG_PLUGINS,
    });

=head1 TEMPLATE TOOLKIT PLUGINS

Please see L<Template::Manual::Plugins> For a complete list of all the plugin
modules distributed with the Template Toolkit.

=head1 AUTHOR

Andy Wardley E<lt>abw@wardley.orgE<gt> L<http://wardley.org/>

=head1 COPYRIGHT

Copyright (C) 1996-2022 Andy Wardley.  All Rights Reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 SEE ALSO

L<Template::Manual::Plugins>, L<Template::Plugin>, L<Template::Context>, L<Template>.

=cut

# Local Variables:
# mode: perl
# perl-indent-level: 4
# indent-tabs-mode: nil
# End:
#
# vim: expandtab shiftwidth=4:
