Index: gs2-extensions/tdb/trunk/perllib/DBDrivers/JDBM.pm
===================================================================
--- gs2-extensions/tdb/trunk/perllib/DBDrivers/JDBM.pm	(revision 30337)
+++ gs2-extensions/tdb/trunk/perllib/DBDrivers/JDBM.pm	(revision 30338)
@@ -32,16 +32,27 @@
 use util;
 use FileUtils;
-use DBDrivers::BaseDBDriver;
+# - OO inheritence
+use parent 'DBDrivers::BaseDBDriver';
 
 sub BEGIN
 {
-    @DBDrivers::JDBM::ISA = ( 'DBDrivers::BaseDBDriver' );
-}
-
+    if (!defined $ENV{'GSDLHOME'} || !defined $ENV{'GSDLOS'}) {
+        die("Error! Environment must be prepared by sourcing setup.bash\n");
+    }
+}
+
+
+## @function constructor
+#
 sub new
 {
     my $class = shift(@_);
-    return bless ($self, $class);
-}
+    my $self = DBDrivers::BaseDBDriver->new();
+    $self->{'default_file_extension'} = 'jdb';
+    bless($self, $class);
+    return $self;
+}
+## constructor() ##
+
 
 # -----------------------------------------------------------------------------
@@ -54,11 +65,21 @@
 # be constructed that changes between much of the code that is used
 
+# Handled by BaseDBDriver
+# sub get_infodb_file_path {}
+
+
+
 sub open_infodb_write_handle
 {
-  my $infodb_file_path = shift(@_);
-  my $opt_append = shift(@_);
-
-  my $jdbmwrap_jar = &util::filename_cat($ENV{'GSDLHOME'},"bin","java", "JDBMWrapper.jar");
-  my $jdbm_jar = &util::filename_cat($ENV{'GSDLHOME'},"lib","java", "jdbm.jar");
+    my $self = shift(@_);
+    my $infodb_file_path = shift(@_);
+    my $opt_append = shift(@_);
+    if (!defined $opt_append) {
+	$opt_append = '';
+    }
+    $self->_debugPrint('("' . $infodb_file_path . '","' . $opt_append . '")');
+
+    my $jdbmwrap_jar = &FileUtils::filenameConcatenate($ENV{'GSDLHOME'},"bin","java", "JDBMWrapper.jar");
+    my $jdbm_jar = &FileUtils::filenameConcatenate($ENV{'GSDLHOME'},"lib","java", "jdbm.jar");
 
   my $classpath = &util::pathname_cat($jdbmwrap_jar,$jdbm_jar);
@@ -76,5 +97,5 @@
   my $txt2jdb_cmd = "java -cp \"$classpath\" Txt2Jdb";
 
-  if ((defined $opt_append) && ($opt_append eq "append")) {
+  if ($opt_append eq "append") {
       $txt2jdb_cmd .= " -append";
       print STDERR "Append operation to $infodb_file_path\n";
@@ -113,15 +134,4 @@
 
   close($infodb_handle);
-}
-
-
-sub get_infodb_file_path
-{
-  my $collection_name = shift(@_);
-  my $infodb_directory_path = shift(@_);
-
-  my $infodb_file_extension = ".jdb";
-  my $infodb_file_name = &util::get_dirsep_tail($collection_name) . $infodb_file_extension;
-  return &util::filename_cat($infodb_directory_path, $infodb_file_name);
 }
 
Index: gs2-extensions/tdb/trunk/perllib/DBDrivers/TDB.pm
===================================================================
--- gs2-extensions/tdb/trunk/perllib/DBDrivers/TDB.pm	(revision 30337)
+++ gs2-extensions/tdb/trunk/perllib/DBDrivers/TDB.pm	(revision 30338)
@@ -32,7 +32,10 @@
 # Libraries
 use Cwd;
+use Devel::Peek;
 use ghtml;
+use Scalar::Util 'refaddr';
 use util;
-use DBDrivers::GDBM;
+# - OO inheritence
+use parent 'DBDrivers::GDBM';
 
 sub BEGIN
@@ -44,5 +47,4 @@
         die("Error! Path to TDB binaries not found. Have you sourced setup.bash?\n");
     }
-    @DBDrivers::TDB::ISA = ( 'DBDrivers::GDBM' );
 }
 
@@ -50,6 +52,43 @@
 {
     my $class = shift(@_);
-    return bless ($self, $class);
-}
+
+    my $self = DBDrivers::GDBM->new();
+
+    # Default TDB file extension
+    $self->{'default_file_extension'} = 'tdb';
+    # Should the TDB used a specific affinity?
+    $self->{'forced_affinity'} = -1; # zero upwards indicates the affinity
+    # Keep track of all opened file handles
+    $self->{'handle_pool'} = {};
+    # Ask TDB executables to display debugging information?
+    $self->{'tdb_debug'} = 0; # 1 to enable
+
+    bless($self, $class);
+    return $self;
+}
+
+
+## @function DESTROY
+#
+# Built-in destructor block that, unlike END, gets passed a reference to self.
+# Responsible for properly closing any open database handles.
+#
+sub DESTROY
+{
+    my $self = shift(@_);
+    # Close all remaining filehandles
+    foreach my $infodb_file_path (keys(%{$self->{'handle_pool'}})) {
+	my $infodb_handle = $self->{'handle_pool'}->{$infodb_file_path};
+	# By passing the filepath as the second argument we instruct the driver
+	# that we actually want to close the connection by passing a non-zero
+	# value, but we sneakily optimize things a little as the close method
+	# can now check to see if it's been provided a file_path rather than
+	# having to search the handle pool for it. The file_path is needed to
+	# remove the closed handle from the pool anyway.
+	$self->close_infodb_write_handle($infodb_handle, $infodb_file_path);
+    }
+}
+## DESTROY(void) => void ##
+
 
 # -----------------------------------------------------------------------------
@@ -57,12 +96,4 @@
 # -----------------------------------------------------------------------------
 
-## Ask TDB executables to display debugging information?
-my $debug = 0; # 1 to enable
-
-## Should the TDB used a specific affinity?
-my $forced_affinity = -1; # zero upwards indicates the affinity
-
-## Default TDB file extension
-my $infodb_file_extension = '.tdb';
 
 ## @function _get_tdb_executable(string)
@@ -70,4 +101,5 @@
 sub _get_tdb_executable
 {
+    my $self = shift(@_);
     my $program = shift(@_);
     if (!defined $ENV{GEXTTDBEDIT_INSTALLED} || !-d $ENV{GEXTTDBEDIT_INSTALLED})
@@ -82,9 +114,11 @@
     return $program_exe;
 }
-## get_tdb_executable(string) => string ##
-
+## _get_tdb_executable(string) => string ##
+
+
+# Handled by BaseDBDriver
+# sub get_infodb_file_path(string, string)
 
 # With infodb_handle already set up, these functions work the same as parent version
-# sub close_infodb_write_handle {}
 # sub delete_infodb_entry {}
 # sub write_infodb_entry {}
@@ -92,14 +126,73 @@
 
 
+## @function close_infodb_write_handle(filehandle)
+#
+#  Some slight-of-hand here due to the way Perl passes variables to methods.
+#  Most of the time (i.e. under all the existing calls in the Greenstone code)
+#  this does nothing, as TDB handles can be left open and reused by multiple
+#  writers/readers (the exception being complete file reads, but they are
+#  handled in their own function anyway).
+#
+#  However TDB's version of this function will look for an extra variable and,
+#  if true (non-zero), will actually close the handle. Several methods below
+#  call close but also pass the infodb_file_path as the second argument, which
+#  is enough to have the connections properly closed.
+#
+#  Note that when this class passes from scope all open handles will be
+#  properly closed by the DESTROY block.
+#
+sub close_infodb_write_handle {
+    my $self = shift(@_);
+    my $infodb_handle = shift(@_);
+    my $actually_close = shift(@_); # Undefined most of the time
+    if (defined($actually_close)) {
+	$self->_debugPrint('(<infodb_handle>,"' . $actually_close . '")');
+	# We'll need the file path so we can locate and remove the entry in the
+	# handle pool
+	my $infodb_file_path = undef;
+	# Sometimes we can cheat, as the actually_close variable will have the
+	# file_path in it thanks to the DESTROY block above. Doing a regex on
+	# actually_close will treat it like a string no matter what it was, and
+	# we can search for the appropriate file extension that should be there
+	# for valid paths.
+	my $pattern = '\.' . $self->{'default_file_extension'} . '$';
+	if ($actually_close =~ /$pattern/) {
+	    $infodb_file_path = $actually_close;
+	}
+	# If we can't cheat then we are stuck finding which connection in the
+	# handle_pool we are about to close. Need to compare objects using
+	# refaddr()
+	else {
+	    foreach my $possible_file_path (values(%{$self->{'handle_pool'}})) {
+		my $possible_handle = $self->{'handle_pool'}->{$possible_file_path};
+		if (ref($infodb_handle) && ref($possible_handle) && refaddr($infodb_handle) == refaddr($possible_handle)) {
+		    $infodb_file_path = $possible_file_path;
+		    last;
+		}
+	    }
+	}
+	if (defined($infodb_file_path)) {
+	    delete($self->{'handle_pool'}->{$infodb_file_path});
+	}
+	else {
+	    print STDERR "Warning! About to close TDB database handle, but couldn't locate in open handle pool.\n";
+	}
+	# Call GDBM's close to do the heavy-lifting
+	$self->SUPER::close_infodb_write_handle($infodb_handle);
+    }
+}
+## close_infodb_write_handle(filehandle) => void ##
+
+
 ## @function open_infodb_write_handle(string, string)
 #
 sub open_infodb_write_handle
 {
+    my $self = shift(@_);
     my $infodb_file_path = shift(@_);
     my $opt_append = shift(@_);
 
-    my $txt2tdb_exe = &_get_tdb_executable('txt2tdb');
-
-    my $pool_key = $infodb_file_path;
+    my $txt2tdb_exe = $self->_get_tdb_executable('txt2tdb');
+
     my $cmd = '"' . $txt2tdb_exe . '"';
     if ((defined $opt_append) && ($opt_append eq "append")) {
@@ -108,8 +201,8 @@
     $cmd .= ' "' . $infodb_file_path . '"';
     # Optional flags
-    if ($forced_affinity >= 0) {
+    if ($self->{'forced_affinity'} >= 0) {
         $cmd = 'taskset -c 5 ' . $cmd;
     }
-    if ($debug) {
+    if ($self->{'debug'}) {
         $cmd .= ' -debug';
     }
@@ -118,11 +211,20 @@
     # within the buildproc, so we create a piped handle here
     my $infodb_file_handle = undef;
-    print STDERR "tdb::open_infodb_write_handle(" . $infodb_file_path . ")\n";
-    if(!open($infodb_file_handle, "| $cmd")) {
-        print STDERR "Error: Failed to open pipe to $cmd\n";
-        print STDERR "       $!\n";
-        return undef;
-    }
-    binmode($infodb_file_handle,":utf8");
+    # if the connection is already open, simply return it.
+    if (defined $self->{'handle_pool'}->{$infodb_file_path}) {
+	$infodb_file_handle = $self->{'handle_pool'}->{$infodb_file_path};
+    }
+    else {
+	$self->_debugPrint('(' . $infodb_file_path . ')');
+	if(!open($infodb_file_handle, "| $cmd")) {
+	    print STDERR "Error: Failed to open pipe to $cmd\n";
+	    print STDERR "       $!\n";
+	    return undef;
+	}
+	binmode($infodb_file_handle,":utf8");
+	# Remember to store the newly created connection in the pool so we can
+	# re-use for subsequent calls.
+	$self->{'handle_pool'}->{$infodb_file_path} = $infodb_file_handle;
+    }
     return $infodb_file_handle;
 }
@@ -130,24 +232,15 @@
 
 
-## @function get_infodb_file_path(string, string)
-#
-sub get_infodb_file_path
-{
-    my $collection_name = shift(@_);
-    my $infodb_directory_path = shift(@_);
-    my $infodb_file_name = &util::get_dirsep_tail($collection_name) . $infodb_file_extension;
-    return &util::filename_cat($infodb_directory_path, $infodb_file_name);
-}
-## get_infodb_file_path(string, string) => string ##
-
-
 ## @function read_infodb_file
 #
 sub read_infodb_file
 {
+    my $self = shift(@_);
     my $infodb_file_path = shift(@_);
     my $infodb_map = shift(@_);
 
-    my $tdb2txt_exe = &_get_tdb_executable('tdb2txt');
+    $self->_debugPrint('(' . $infodb_file_path . ', <hashmap>)');
+
+    my $tdb2txt_exe = $self->_get_tdb_executable('tdb2txt');
 
     if (!open (PIPEIN, '"' . $tdb2txt_exe . '" "' . $infodb_file_path . '" |')) {
@@ -184,10 +277,13 @@
 sub read_infodb_keys
 {
+    my $self = shift(@_);
     my $infodb_file_path = shift(@_);
     my $infodb_map = shift(@_);
 
-    my $tdbkeys_exe = &_get_tdb_executable('tdbkeys');
-
-    if (!open (PIPEIN, '"' . tdbkeys_exe . '" "' . $infodb_file_path . '" |')) {
+    $self->_debugPrint('(' . $infodb_file_path . ', <hashmap>)');
+
+    my $tdbkeys_exe = $self->_get_tdb_executable('tdbkeys');
+
+    if (!open (PIPEIN, '"' . $tdbkeys_exe . '" "' . $infodb_file_path . '" |')) {
         die("Error! Couldn't open pipe from read_infodb_keys: $infodb_file_path\n$!\n");
     }
@@ -213,7 +309,10 @@
 sub set_infodb_entry
 {
+    my $self = shift(@_);
     my $infodb_file_path = shift(@_);
     my $infodb_key = shift(@_);
     my $infodb_map = shift(@_);
+
+    $self->_debugPrint('(' . $infodb_file_path . ', ' . $infodb_key . ', <hashmap>)');
 
     # Protect metadata values that go inside quotes for tdbset
@@ -236,8 +335,8 @@
 
     # Generate the record string
-    my $serialized_infodb_map = &_convert_infodb_hash_to_string($infodb_map);
+    my $serialized_infodb_map = $self->_convert_infodb_hash_to_string($infodb_map);
 
     # Store it into GDBM
-    my $tdbset_exe = &_get_tdb_executable('tdbset');
+    my $tdbset_exe = $self->_get_tdb_executable('tdbset');
     my $cmd = '"' . $tdbset_exe . '" "' . $infodb_file_path . '" "' . $infodb_key . '" "' . $serialized_infodb_map . '"';
     my $status = system($cmd);
