package Net::DAV::LockManager::DB;

use strict;

use DBI;
use File::Temp qw(tmpnam);
use Net::DAV::Lock;

our $VERSION = '1.301';
$VERSION = eval $VERSION;

#
# This provides a listing of all database schema required to initialize
# a database from an empty state.  Note that this is an array, as each
# schema definition must be executed separately due to limitations in
# the SQLite database driver.
#
my @schema = (
    qq/
        create table lock (
            uuid CHAR(36) PRIMARY KEY,
            expiry INTEGER,
            creator CHAR(128),
            owner CHAR(128),
            depth CHAR(32),
            scope CHAR(32),
            path CHAR(512)
        )
    /
);

#
# Create a new lock manager database context.  Optionally accepts a
# parameter representing a DBI-formatted Data Source Name (DSN).  If no
# DSN is provided, then a temporary SQLite database is used by default.
#
sub new {
    my $class = shift;
    my $dsn = $_[0]? $_[0]: undef;
    my $tmp = undef;

    unless ($dsn) {
        $tmp = File::Temp::tmpnam('/tmp', '.webdav-locks');
        $dsn = 'dbi:SQLite:dbname='. $tmp;
    }

    my $self = bless {
        'db' => DBI->connect($dsn, '', '')
    }, $class;

    #
    # In the event no data source name was passed, take note of this fact in
    # the new object instance so that a proper cleanup can happen at destruction
    # time.
    #
    if ($tmp) {
        $self->{'tmp'} = $tmp;
    }

    #
    # Perform any database initializations that may be required prior to
    # returning the newly-constructed object.
    #
    $self->_initialize();

    return $self;
}

#
# Called from the constructor to initialize state (including database
# file and schema) prior to returning a newly-instantiated object.
#
sub _initialize {
    my ($self) = @_;

    #
    # Enable transactions for the duration of this method.  Enable
    # error reporting.
    #
    $self->{'db'}->{'AutoCommit'} = 0;
    $self->{'db'}->{'RaiseError'} = 1;

    #
    # Only perform initialization if the table definition is missing.
    # We can use the internal SQLite table SQLITE_MASTER to verify
    # the presence of our lock table.
    #
    # If the schema has already been applied to the current database,
    # then we can safely return.
    #
    if ($self->{'db'}->selectrow_hashref(q/select name from sqlite_master where name = 'lock'/)) {
        #
        # Disable transactions and raised errors to revert to default
        # state.
        #
        $self->{'db'}->{'AutoCommit'} = 1;
        $self->{'db'}->{'RaiseError'} = 0;
        return;
    }

    #
    # The schema has not been applied.  Instantiate it.
    #
    eval {
        foreach my $definition (@schema) {
            $self->{'db'}->do($definition);
        }

        $self->{'db'}->commit();
    };

    #
    # Gracefully recover from any errors in instantiating the schema,
    # in this case by throwing another error describing the situation.
    #
    if ($@) {
        warn("Unable to initialize database schema: $@");

        eval {
            $self->{'db'}->rollback();
        };
    }

    #
    # Disable transactions and raised errors to revert to default
    # state.
    #
    $self->{'db'}->{'AutoCommit'} = 1;
    $self->{'db'}->{'RaiseError'} = 0;
}

#
# Intended to be dispatched by the caller whenever the database is no
# longer required.  This method will remove any temporary, one-time
# use databases which may have been created at object instantiation
# time.
#
sub close {
    my ($self) = @_;

    $self->{'db'}->disconnect();

    #
    # If the name of a temporary database was stored in this object,
    # be sure to unlink() said file.
    #
    if ($self->{'tmp'}) {
        unlink($self->{'tmp'});
    }
}

#
# Garbage collection hook to perform tidy cleanup prior to deallocation.
#
sub DESTROY {
    my ($self) = @_;

    $self->close();
}

#
# Given a normalized string representation of a resource path, return
# the first lock found.  Return undef if no object was found in the
# database.
#
sub get {
    my ($self, $path) = @_;

    my $row = $self->{'db'}->selectrow_hashref(q/select * from lock where path = ?/, {}, $path);

    return $row? Net::DAV::Lock->reanimate($row): undef;
}

#
# Given a path string, return any lock objects whose paths are descendants
# of the specified path, excluding the current path.
#
sub list_descendants {
    my ($self, $path) = @_;

    if ($path eq '/') {
        return map {
            Net::DAV::Lock->reanimate($_)
        } @{$self->{'db'}->selectall_arrayref(q(select * from lock where path != '/'), { 'Slice' => {} })};
    }

    my $sql = q/select * from lock where path like ?/;

    return map {
        Net::DAV::Lock->reanimate($_)
    } @{$self->{'db'}->selectall_arrayref($sql, { 'Slice' => {} }, "$path/%")};
}

#
# Given an instance of Net::DAV::Lock, update any entries in the
# database whose path corresponds to the value provided in the
# object.
#
sub update {
    my ($self, $lock) = @_;

    $self->{'db'}->do(q/update lock set expiry = ? where path = ?/, {},
        $lock->expiry,
        $lock->path
    );

    return $lock;
}

#
# Insert the data passed in an instance of Net::DAV::Lock into the
# database, and return that reference.
#
sub add {
    my ($self, $lock) = @_;

    my $sql = qq{
        insert into lock (
            uuid, expiry, creator, owner, depth, scope, path
        ) values (
            ?, ?, ?, ?, ?, ?, ?
        )
    };

    $self->{'db'}->do($sql, {},
        $lock->uuid,
        $lock->expiry,
        $lock->creator,
        $lock->owner,
        $lock->depth,
        $lock->scope,
        $lock->path
    );

    return $lock;
}

#
# Given a Net::DAV::Lock object, the database record which contains the
# corresponding path.
#
sub remove {
    my ($self, $lock) = @_;

    $self->{'db'}->do(q/delete from lock where path = ?/, {}, $lock->path);
}

1;

__END__
Copyright (c) 2010, cPanel, Inc. All rights reserved.
This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.
