Index: main/trunk/greenstone2/perllib/docprint.pm
===================================================================
--- main/trunk/greenstone2/perllib/docprint.pm	(revision 32535)
+++ main/trunk/greenstone2/perllib/docprint.pm	(revision 32536)
@@ -27,4 +27,7 @@
 # Greenstone XML format.
 # This is used by GreenstoneXMLPlugout and doc.pm
+# It's also used by GreenstoneSQLPlugout to output a doc_obj's meta and/or
+# fulltxt to an SQL db, while subroutine  unescape_text() is used by
+# GreenstoneSQLPlugin for unescaping txt read back in.
 
 package docprint;
@@ -117,3 +120,16 @@
 }
 
+# used by GreenstoneSQLPlugin when reading back from sqldb
+sub unescape_text {
+    my ($text) = @_;
+    # special characters in the xml encoding
+    $text =~ s/& &/&&/g;
+    $text =~ s/&amp;/&/g; # this has to be first...
+    $text =~ s/&lt;/</g;
+    $text =~ s/&gt;/>/g;
+    $text =~ s/&quot;/"/g;
+
+    return $text;
+}
+
 1;
Index: main/trunk/greenstone2/perllib/gssql.pm
===================================================================
--- main/trunk/greenstone2/perllib/gssql.pm	(revision 32535)
+++ main/trunk/greenstone2/perllib/gssql.pm	(revision 32536)
@@ -31,8 +31,5 @@
 no strict 'subs';
 
-use GreenstoneXMLPlugout;
-use docprint;
-
-use DBI; # the central package for this plugout
+use DBI; # the central package for this module used by GreenstoneSQL Plugout and Plugin
 
 # Need params_map keys:
@@ -151,4 +148,7 @@
 }
 
+# will attempt to load the specified db and the <coll>_metadata and <coll>_fulltxt for this
+# collection, or create any of these (db, tables) that don't yet exist. At the end
+# it will have loaded the requested database (in MySQL: "use <db>;")
 sub load_db_and_tables {
     my $self= shift (@_);
@@ -205,7 +205,7 @@
 }
 
-# this will terminate if the db does not exist
-# it will not attempt to create the requested db (nor its tables)
-# The upcoming GreenstoneSQLPlugin can use this.
+# GreenstoneSQLPlugin calls this method to load an existing db.
+# This will terminate if the db does not exist. Unlike load_db_and_tables() above, used by
+# GreenstoneSQLPlugout, this method will not attempt to create the requested db (nor its tables)
 sub use_db {
     my $self= shift (@_);
@@ -370,4 +370,10 @@
 }
 
+# returns database handle, dbh
+sub get_db_handle {
+    my $self= shift (@_);
+    return $self->{'db_handle'};
+}
+
 
 # I can get my version of table_exists to work, but it's not so ideal
Index: main/trunk/greenstone2/perllib/plugins/GreenstoneSQLPlugin.pm
===================================================================
--- main/trunk/greenstone2/perllib/plugins/GreenstoneSQLPlugin.pm	(revision 32536)
+++ main/trunk/greenstone2/perllib/plugins/GreenstoneSQLPlugin.pm	(revision 32536)
@@ -0,0 +1,338 @@
+###########################################################################
+#
+# GreenstoneSQLPlugin.pm -- reads into doc_obj from SQL db and docsql-<OID>.xml
+# Metadata and/or fulltext are stored in SQL db, the rest may be stored in
+# the docsql .xml files.
+# A component of the Greenstone digital library software
+# from the New Zealand Digital Library Project at the 
+# University of Waikato, New Zealand.
+#
+# Copyright (C) 2001 New Zealand Digital Library Project
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+###########################################################################
+
+package GreenstoneSQLPlugin;
+
+
+use strict;
+no strict 'refs'; # allow filehandles to be variables and viceversa
+
+use DBI;
+use docprint; # for new unescape_text() subroutine
+use GreenstoneXMLPlugin;
+use gssql;
+
+
+# TODO:
+# - Run TODOs here and in plugout by Dr Bainbridge. Ask about docsql naming convention adopted
+# to identify OID. Better way?
+# collection names -> table names: hyphens not allowed? Changed to underscores.
+# - Startup parameters
+# - incremental building: where do we need to add code to delete rows from our sql table after
+# incrementally importing a coll with fewer docs (for instance)? What about deleted/modified meta?
+# - Courier documents in lucene-sql collection: character (degree symbol) not preserved. Is this because we encode in utf8 when putting into db and reading back in?
+# - Have not yet tested writing out just meta or just fulltxt to sql db and reading just that 
+# back in from the sql db while the remainder is to be read back in from the docsql .xml files.
+
+
+# GreenstoneSQLPlugin inherits from GreenstoneXMLPlugin so that it if meta or fulltext
+# is still written out to doc.xml (docsql .xml), that will be processed as usual,
+# whereas GreenstoneSQLPlugin will process all the rest (full text and/or meta, whichever
+# is written out by GreenstoneSQLPlugout into the SQL db).
+
+
+sub BEGIN {
+    @GreenstoneSQLPlugin::ISA = ('GreenstoneXMLPlugin');
+}
+
+# This plugin must be in the document plugins pipeline IN PLACE OF GreenstoneXMLPlugin
+# So we won't have a process exp conflict here.
+sub get_default_process_exp {
+    my $self = shift (@_);
+
+    #return q^(?i)docsql(-\d+)?\.xml$^;
+    return q^(?i)docsql(-.+)?\.xml$^;
+}
+
+my $process_mode_list =
+    [ { 'name' => "meta_only",
+        'desc' => "{GreenstoneSQLPlugout.process_mode.meta_only}" },      
+      { 'name' => "text_only",
+        'desc' => "{GreenstoneSQLPlugout.process_mode.text_only}" },
+      { 'name' => "all",
+        'desc' => "{GreenstoneSQLPlugout.process_mode.all}" } ];
+
+my $arguments =
+    [ { 'name' => "process_exp",
+	'desc' => "{BaseImporter.process_exp}",
+	'type' => "regexp",
+	'deft' => &get_default_process_exp(),
+	'reqd' => "no" },
+       { 'name' => "process_mode", 
+	 'desc' => "{GreenstoneSQLPlugout.process_mode}",
+	 'type' => "enum",
+	 'list' => $process_mode_list,
+	 'deft' => "all",
+	 'reqd' => "no"}
+    ];
+
+my $options = { 'name'     => "GreenstoneSQLPlugin",
+		'desc'     => "{GreenstoneSQLPlugin.desc}",
+		'abstract' => "no",
+		'inherits' => "yes",
+	        'args'     => $arguments };
+
+
+# TODO: For on cancel, add a SIGTERM handler or so to call end()
+# or to explicitly call gs_sql->close_connection if $gs_sql def
+
+sub new {
+    my ($class) = shift (@_);
+    my ($pluginlist,$inputargs,$hashArgOptLists) = @_;
+    push(@$pluginlist, $class);
+
+    push(@{$hashArgOptLists->{"ArgList"}},@{$arguments});
+    push(@{$hashArgOptLists->{"OptList"}},$options);
+
+    my $self = new GreenstoneXMLPlugin($pluginlist, $inputargs, $hashArgOptLists);
+
+    
+    #return bless $self, $class;
+    $self = bless $self, $class;
+    if ($self->{'info_only'}) {
+	# If running pluginfo, we don't need to go further.
+	return $self;
+    }
+
+    # do anything else that needs to be done here when not pluginfo
+    
+    return $self;
+}
+
+# need to store current section 
+#sub xml_start_tag {
+#    my $self = shift(@_);
+#    $self->SUPER::xml_start_tag(@_);
+#    $self->{'stored_section'} = $self->{'section'};
+#    $self->{'stored_section_level'} = $self->{'section_level'}
+#}
+
+# TODO Q: Why are there 3 passes when we're only indexing at doc and section level (2 passes)?
+
+# At the end of superclass GreenstoneXMLPlugin.pm's close_document() method,
+# the doc_obj in memory is processed (indexed) and then made undef.
+# So we have to work with doc_obj before superclass close_document() is finished.
+sub close_document {
+    my $self = shift(@_);
+    
+    my $outhandle = $self->{'outhandle'};
+    my $doc_obj = $self->{'doc_obj'};    
+    my $gs_sql = $self->{'gs_sql'};
+
+    my $oid = $self->{'doc_oid'}; # we stored current doc's OID during sub read() 
+    print $outhandle "@@@@@@ OID of document (meta|text) to be read in from DB: $oid\n"
+	if $self->{'verbosity'} > 1;
+
+#    print STDERR "@@@@ section: |" . $self->{'stored_section'} . "|\n";
+#    print STDERR "@@@@ section_level: " . $self->{'stored_section_level'} . "\n";
+    
+    # for now, we have access to doc_obj (until just before super::close_document() terminates)
+    #$self->{'doc_obj'}->set_OID($oid);
+    
+    # TODO: This function is called on a per doc.xml file basis
+    # but we can process all docs of a collection in one go when dealing with the SQL tables for
+    # the collection. How and where should we read in the collection tables then?
+    # TODO: Perhaps MySQLPlugout could write out a token file (.gssql) into archives during import.pl
+    # and if that file is detected, then MySQLPlugin::read() is passed in that file during
+    # buildcol.pl. And that file will trigger reading the 2 tables for the collection???
+    my $proc_mode = $self->{'process_mode'};
+    if($proc_mode eq "all" || $proc_mode eq "meta_only") {
+	# read in meta for the collection (i.e. select * from <col>_metadata table
+
+	my $meta_table = $gs_sql->get_metadata_table_name();
+	
+	# https://www.effectiveperlprogramming.com/2010/07/set-custom-dbi-error-handlers/
+	my $sth = $gs_sql->get_db_handle()->prepare(qq{SELECT * FROM $meta_table WHERE did = ?});
+	print $outhandle "### stmt: ".$sth->{'Statement'}."\n"
+	    if $self->{'verbosity'} > 1;
+	$sth->execute( $oid ); # will print msg on fail
+
+	print $outhandle "----------SQL DB contains meta-----------\n" if $self->{'verbosity'} > 1;
+	while( my @row = $sth->fetchrow_array() ) {	    
+	    #print $outhandle "row: @row\n";
+	    my ($primary_key, $did, $sid, $metaname, $metaval) = @row;
+
+	    # get rid of the artificial "root" introduced in section id when saving to sql db
+	    $sid =~ s@^root@@;
+	    $sid = $doc_obj->get_top_section() unless $sid;
+	    print $outhandle "### did: $did, sid: |$sid|, meta: $metaname, val: $metaval\n"
+		if $self->{'verbosity'} > 1;
+	    
+	    # TODO:  we accessed the db in utf8 mode, so, we can call doc_obj->add_utf8_meta directly:
+	    $doc_obj->add_utf8_metadata($sid, $metaname, &docprint::unescape_text($metaval));
+	}
+	print $outhandle "----------FIN READING DOC's META FROM SQL DB------------\n"
+	    if $self->{'verbosity'} > 1;
+    }
+    
+    if($proc_mode eq "all" || $proc_mode eq "text_only") {
+	# read in fulltxt for the collection (i.e. select * from <col>_fulltxt table
+
+	my $fulltxt_table = $gs_sql->get_fulltext_table_name();
+	
+	
+	my $sth = $gs_sql->get_db_handle()->prepare(qq{SELECT * FROM $fulltxt_table WHERE did = ?});
+	print $outhandle "### stmt: ".$sth->{'Statement'}."\n"
+	    if $self->{'verbosity'} > 1;
+	$sth->execute( $oid ); # will print msg on fail
+
+	print $outhandle "----------\nSQL DB contains txt entries for-----------\n"
+	    if $self->{'verbosity'} > 1;
+	while( my ($primary_key, $did, $sid, $text) = $sth->fetchrow_array() ) {	   
+
+	    # get rid of the artificial "root" introduced in section id when saving to sql db
+	    #$sid =~ s@^root@@;
+	    $sid = $doc_obj->get_top_section() if ($sid eq "root");
+	    print $outhandle "### did: $did, sid: |$sid|, fulltext: <TXT NOT PRINTED>\n"
+		if $self->{'verbosity'} > 1;
+
+	    # TODO - pass by ref?
+	    # TODO: we accessed the db in utf8 mode, so, we can call doc_obj->add_utf8_text directly:
+	    $doc_obj->add_utf8_text($sid, &docprint::unescape_text($text));
+	}	
+	print $outhandle "----------FIN READING DOC's TXT FROM SQL DB------------\n"
+	    if $self->{'verbosity'} > 1;
+    }
+
+    
+    # don't forget to clean up on close() in superclass
+    # It will get the doc_obj indexed then make it undef
+    $self->SUPER::close_document(@_);
+}
+
+
+# TODO: only want to work with sql db if buildcol.pl
+# call init() not begin() because there can be multiple plugin passes
+# and init() should be called before all passes:
+# one for doc level and another for section level indexing
+sub init {
+    my ($self) = shift (@_);
+#    print STDERR "@@@@@@@@@@ INIT CALLED\n";
+    
+    $self->SUPER::init(@_); # super (GreenstoneXMLPlugin) will not yet be trying to read from doc.xml (docsql .xml) files in init().
+
+        # TODO: how do we know what site we're dealing with unless this is passed in, by buildcol?
+    ###########
+#    print "@@@ SITE NAME: ". $self->{'site_name'} . "\n" if defined $self->{'site_name'};
+#    print "@@@ COLL NAME: ". $ENV{'GSDLCOLLECTION'} . "\n";
+    
+    $self->{'db_driver'} = "mysql";
+    $self->{'site_name'} = "localsite";    
+    $self->{'db_client_user'} = "root";
+    $self->{'db_client_pwd'} = "6reenstone3";
+    $self->{'build_mode'} = "removeold";
+    $self->{'db_host'} = "127.0.0.1";
+    $self->{'db_encoding'} = "utf8";
+    ###########
+    
+    my $gs_sql = new gssql({
+	'collection_name' => $ENV{'GSDLCOLLECTION'},	
+	'db_encoding' => $self->{'db_encoding'}
+	#'db_name' => $self->{'site_name'},
+	#'build_mode' => $self->{'build_mode'},
+			   }
+	);
+
+    # try connecting to the mysql db, if that fails it will die
+    if(!$gs_sql->connect_to_db({
+	'db_driver' => $self->{'db_driver'},
+	'db_client_user' => $self->{'db_client_user'},
+	'db_client_pwd' => $self->{'db_client_pwd'},
+	'db_host' => $self->{'db_host'}
+			       })
+	)
+    {
+	# This is fatal for the plugout, let's terminate here
+	# PrintError would already have displayed the warning message on connection fail	
+	die("Could not connect to db. Can't proceed.\n");
+    }
+    
+    my $db_name = $self->{'site_name'} || "localsite"; # one database per GS3 site
+    #my $build_mode = $self->{'build_mode'} || "removeold";
+
+    # the db and its tables should exist. Attempt to use the db:
+    if(!$gs_sql->use_db($db_name)) {
+	
+	# This is fatal for the plugout, let's terminate here after disconnecting again
+	# PrintError would already have displayed the warning message on load fail
+	$gs_sql->disconnect_from_db()
+	    || warn("Unable to disconnect from database " . $self->{'site_name'} . "\n");
+	die("Could not use db $db_name. Can't proceed.\n");
+    }
+    
+    # store db handle now that we're connected
+    $self->{'gs_sql'} = $gs_sql;
+    
+}
+
+
+# Call deinit() not end() because there can be multiple plugin passes:
+# one for doc level and another for section level indexing
+# and deinit() should be called before all passes
+sub deinit {
+    my ($self) = shift (@_);
+    if($self->{'gs_sql'}) { # can cover TODO: only want to work with sql db if buildcol.pl
+	$self->{'gs_sql'}->disconnect_from_db()
+	    || warn("Unable to disconnect from database " . $self->{'site_name'} . "\n");
+    }
+#    print STDERR "@@@@@@@@@@ DEINIT CALLED\n";
+    $self->SUPER::deinit(@_);
+}
+
+sub read {
+    my $self = shift (@_);  
+  
+    my ($pluginfo, $base_dir, $file, $block_hash, $metadata, $processor, $maxdocs, $total_count, $gli) = @_;
+
+    # when running buildcol.pl, the filename should match "docsql-<OID>.xml"
+    # when running import.pl it will be the original document's filename
+    # we only want to read in from db when running buildcol.pl
+
+    # doc_obj doesn't exist yet and only exists during super::read(): a new doc (doc_obj)
+    # is created in super::open_document() and is made undef again on super::close_document().
+    # Further, can't read it in from doc.xml to work out which OID to query in sql db:
+    # even if we got access to doc_obj, if no meta stored in docsql.xml, then when
+    # doc_obj is read in from docsql.xml there will be no OID. So OID is docsql.xml filename
+    # contains OID in filename. Having extracted OID from the filename, store OID in plugin-self
+    if($file =~ m/docsql-(.+?)\.xml$/) {
+
+	# work out docoid from filename of form "docsql-<OID>.xml". $file can have a containing
+	# subfolder besides filename, e.g. "dir/docsql-<OID>.xml"
+	
+	# https://stackoverflow.com/questions/22836/how-do-i-perform-a-perl-substitution-on-a-string-while-keeping-the-original
+	(my $oid = $file) =~ s@^(.*?)docsql-(.+?)\.xml$@$2@;
+
+	$self->{'doc_oid'} = $oid;
+    }    
+
+    
+    # always read docsql.xml, as we then know doc structure, and assoc files are dealt with
+    # Plus we need to read docsql.xml if either meta or fulltxt went into there instead of to sql db
+    return $self->SUPER::read(@_); # will open_doc, close_doc then process doc_obj for indexing, then undef doc_obj
+
+    
+}
Index: main/trunk/greenstone2/perllib/plugouts/GreenstoneSQLPlugout.pm
===================================================================
--- main/trunk/greenstone2/perllib/plugouts/GreenstoneSQLPlugout.pm	(revision 32535)
+++ main/trunk/greenstone2/perllib/plugouts/GreenstoneSQLPlugout.pm	(revision 32536)
@@ -40,5 +40,5 @@
 
 # TODO: SIGTERM rollback and disconnect?
-
+# TODO Q: what about verbosity for debugging
 
 # this plugout does not output xml to a file, but outputs rows into a mysql table
@@ -146,7 +146,9 @@
     if(!$gs_sql->load_db_and_tables($db_name, $build_mode)) {
 	
-	# This is fatal for the plugout, let's terminate here
-	# PrintError would already have displayed the warning message on connection fail	
-	die("Could not use db or prepare its tables. Can't proceed.\n");
+	# This is fatal for the plugout, let's terminate here after disconnecting again
+	# PrintError would already have displayed the warning message on load fail
+	$gs_sql->disconnect_from_db()
+	    || warn("Unable to disconnect from database " . $self->{'site_name'} . "\n");
+	die("Could not use db $db_name and/or prepare its tables. Can't proceed.\n");
     }
 
@@ -184,17 +186,13 @@
     $self->{'gs_sql'}->disconnect_from_db() || warn("Unable to disconnect from database " . $self->{'site_name'} . "\n"); # disconnect_from_db() will also issue a warning, but this may be clearer
 }
- 
-# produce files called docsql.xml instead of doc.xml
-sub get_short_doc_file {
+
+# produce files called docsql-<OID>.xml instead of doc.xml
+sub get_doc_xml_filename {
     my $self = shift (@_);
-    my ($doc_dir) = @_;
-    return &FileUtils::filenameConcatenate($doc_dir, "docsql.xml");
-}
-
-# produce files called docsql.xml instead of doc.xml
-sub get_output_file {
-    my $self = shift (@_);
-    my ($doc_dir) = @_;
-    return &FileUtils::filenameConcatenate($self->{'output_dir'}, $doc_dir, "docsql.xml");
+    my ($doc_obj) = @_;
+    
+    my $doc_id = $doc_obj->get_OID();
+    my $filename = $doc_id ? "docsql-".$doc_id.".xml" : "docsql.xml";
+    return $filename;
 }
    
@@ -204,5 +202,5 @@
     my ($doc_obj, $doc_dir) = @_;
 
-    print STDERR "\n\n@@@ In saveas\n\n";
+#    print STDERR "\n\n@@@ In saveas\n\n";
     
     my $proc_mode = $self->{'process_mode'};
@@ -291,4 +289,6 @@
 	foreach my $data (@{$section_ptr->{'metadata'}}) {
 	    my $meta_name = $data->[0];
+	    # TODO: does it need to be stored escaped, as it requires unescaping when read back in
+	    # from db (unlike for reading back in from doc.xml)
 	    my $escaped_meta_value = &docprint::escape_text($data->[1]);
 
Index: main/trunk/greenstone2/perllib/plugouts/GreenstoneXMLPlugout.pm
===================================================================
--- main/trunk/greenstone2/perllib/plugouts/GreenstoneXMLPlugout.pm	(revision 32535)
+++ main/trunk/greenstone2/perllib/plugouts/GreenstoneXMLPlugout.pm	(revision 32536)
@@ -94,5 +94,6 @@
 	    $outhandler = $self->{'group_outhandler'};
 	} else {
-	    $output_file = $self->get_output_file($doc_dir); #&FileUtils::filenameConcatenate($self->{'output_dir'}, $doc_dir, "doc.xml");
+	    $output_file = &FileUtils::filenameConcatenate(
+		$self->{'output_dir'}, $doc_dir, $self->get_doc_xml_filename($doc_obj));
 	    # open the new handle
 	    $self->open_xslt_pipe($output_file, $self->{'xslt_file'});
@@ -134,5 +135,6 @@
 	}
     }
-    $self->{'short_doc_file'} = $self->get_short_doc_file($doc_dir); #&FileUtils::filenameConcatenate($doc_dir, "doc.xml");  
+    $self->{'short_doc_file'} = &FileUtils::filenameConcatenate(
+	$doc_dir, $self->get_doc_xml_filename($doc_obj));  
     
     $self->store_output_info_reference($doc_obj);
@@ -142,16 +144,8 @@
 # can be overridden in subclasses, for instance by GreenstoneSQLPlugout, to produce a different filename
 # like docsql.xml
-sub get_short_doc_file {
-    my $self = shift (@_);
-    my ($doc_dir) = @_;
-    return &FileUtils::filenameConcatenate($doc_dir, "doc.xml");
-}
-
-# can be overridden in subclasses, for instance by GreenstoneSQLPlugout, to produce a different filename
-# like docsql.xml
-sub get_output_file {
-    my $self = shift (@_);
-    my ($doc_dir) = @_;
-    return &FileUtils::filenameConcatenate($self->{'output_dir'}, $doc_dir, "doc.xml");
+sub get_doc_xml_filename {
+    my $self = shift (@_);
+    my ($doc_obj) = @_;
+    return "doc.xml";
 }
 
@@ -175,5 +169,6 @@
 	    $outhandler = $self->{'group_outhandler'};
 	} else {
-	    $output_file = $self->get_output_file($doc_dir); #&FileUtils::filenameConcatenate($self->{'output_dir'}, $doc_dir, "doc.xml");
+	    $output_file = $output_file = &FileUtils::filenameConcatenate(
+		$self->{'output_dir'}, $doc_dir, $self->get_doc_xml_filename($doc_obj));
 	    # open the new handle
 	    $self->open_xslt_pipe($output_file, $self->{'xslt_file'});
@@ -235,5 +230,6 @@
 	}
     }
-    $self->{'short_doc_file'} = $self->get_short_doc_file($doc_dir); #&FileUtils::filenameConcatenate($doc_dir, "doc.xml");  
+    $self->{'short_doc_file'} = &FileUtils::filenameConcatenate(
+	$doc_dir, $self->get_doc_xml_filename($doc_obj));
     
     $self->store_output_info_reference($doc_obj);    
Index: main/trunk/greenstone2/perllib/strings.properties
===================================================================
--- main/trunk/greenstone2/perllib/strings.properties	(revision 32535)
+++ main/trunk/greenstone2/perllib/strings.properties	(revision 32536)
@@ -931,4 +931,6 @@
 GreenstoneXMLPlugin.desc:Processes Greenstone Archive XML documents. Note that this plugin does no syntax checking (though the XML::Parser module tests for well-formedness). It's assumed that the Greenstone Archive files conform to their DTD.
 
+GreenstoneSQLPlugin.desc:Processes the contents of a Greenstone SQL database for metadata and/or full text of documents, and processes Greenstone Archive XML documents for the part of that that's not in the database and for document structure. Note that this plugin does no syntax checking (though the XML::Parser module tests for well-formedness). It's assumed that the Greenstone Archive files conform to their DTD.
+
 GISExtractor.desc:Helper extractor plugin for extracting placenames from text. Requires GIS extension to Greenstone.
 
