#!/usr/bin/perl -w
# ODBC Bridge to Linux (Client) 1.0
# Copyright (c) 2009 by David Stansfield (david@littlesystems.com.au). March 2009.
#
# You may distribute under the terms of the Artistic License 2.0, as specified in the README.license file.
#
use strict;
# Supported database(s): PostgreSQL
#
# Functionality
# =============
#
# **** (1) Download schema XML & data XML
# **** (2) Parse schema XML to SQL
# **** (3) Parse data XML to SQL
# **** (4) Drop original database
# **** (5) Rename update database to original's name
#
use XML::Parser;
use XML::SimpleObject::LibXML; # The LibXML version of SimpleObject keeps the XML child objects (e.g. field names) in their original order.
use LWP::Simple;
#
##
# START CONFIGURATION
# Primary configuration variables
#
my $bridgeURL = "http://192.168.0.104:8080/xml";
my $bridgeLocalRoot = "/var/lib/postgresql/bridge";
my $timeMinInterval = 300; # minimum time interval (in seconds) between requests to $bridgeURL
my $DBAdminName = "myob"; # needs to be created manually in psql. e.g. CREATE USER myob WITH PASSWORD myob
#
# Secondary configuration variables (these should not need to be modified)
#
my $dropAttempts = 30;
my $delayBetweenDropAttempts = 2; # Seconds
my $createDatabaseName = "myob";
my $timeLogFile = "time.log";
my $schemaFileName = "schema";
my $dataFileName = "data";
my $SQLFile = "create.sql";
my $SQLLogFile = "refresh.sql.log";
my $SQLWarningLogFile = "refresh.sql.warning";
my $tmpFile = "refresh.tmp";
#
# END CONFIGURATION
#
#
my $bypassTimeCheck = 0; # testing feature
my $bypassDiffCheck = 0; # testing feature
my $systemCommand;
my $dropSQL;
my $renameSQL;
my $connectCount;
$createDatabaseName = lc($createDatabaseName);
#
#
# Set the per-filehandle variable to true (flushing output)
$|=1;
# ****************************************************************************************
# ****
# **** (1) Download schema XML & data XML
# ****
# ****************************************************************************************
my $timeStart = time();
my $timeLastStart;
if (!$bypassTimeCheck) {
# Check sufficient time has elapsed since last URL request
print "Check whether sufficient time has elapsed ($timeMinInterval"."s) since the last update request";
if (-e "$bridgeLocalRoot/$timeLogFile") {
open (TIMELOG, "< $bridgeLocalRoot/$timeLogFile") or die "\nCouldn't open $bridgeLocalRoot/$timeLogFile";
$timeLastStart = <TIMELOG>;
close (TIMELOG);
} else {
$timeLastStart = 0;
}
if (($timeStart - $timeLastStart) < $timeMinInterval) {
die ". Fail!\nPlease wait at least ".($timeMinInterval - ($timeStart - $timeLastStart))." more seconds.\n";
} else {
print ". OK\n\n";
open (TIMELOG, "> $bridgeLocalRoot/$timeLogFile") or die "\nCouldn't save $bridgeLocalRoot/$timeLogFile";
print (TIMELOG $timeStart);
close (TIMELOG);
}
}
# Download remote schema XML & data XML from URL then save to file
print "Connect to ".uc($createDatabaseName)." ODBC Bridge Server\n";
foreach my $downloadFile ($schemaFileName, $dataFileName) {
print " Download $bridgeURL/$downloadFile.php\n";
my $downloadXML = get("$bridgeURL/$downloadFile.php"); die " Error: Couldn't download $bridgeURL/$downloadFile.php" unless defined $downloadXML;
open (OUTPUTXML, "> $bridgeLocalRoot/$downloadFile"."Update.xml") or die " Error: Couldn't save $bridgeLocalRoot/$downloadFile"."Update.xml";
print (OUTPUTXML $downloadXML);
close (OUTPUTXML);
}
print "\n";
# Exception - Check original database exists. If not, let user know and bypass diff check.
if (!(&databaseExists($createDatabaseName))) {
print "Database $createDatabaseName does not exist\n";
print " Therefore continue and create database\n\n";
$bypassDiffCheck = 1;
}
# Exception - Check update database does not exist. If so, delete it.
if (&databaseExists($createDatabaseName."update")) {
print "Database $createDatabaseName"."update already exists\n";
print " Therefore drop database $createDatabaseName"."update and create anew\n\n";
$dropSQL = "DROP DATABASE $createDatabaseName"."update;";
&doSQL($dropSQL);
}
if (!$bypassDiffCheck) {
# MINOR CACHE
# Only continue database update if an XML file is modiied
if (-e "$bridgeLocalRoot/$schemaFileName.xml" && -e "$bridgeLocalRoot/$dataFileName.xml") {
print "Check for changes\n ";
if (!(system("diff --brief $bridgeLocalRoot/$schemaFileName"."Update.xml $bridgeLocalRoot/$schemaFileName.xml"))) {
if (!(system("diff --brief $bridgeLocalRoot/$dataFileName"."Update.xml $bridgeLocalRoot/$dataFileName.xml"))) {
print " No changes, therefore end database update\n";
exit;
}
}
print " Therefore continue database update\n\n";
}
}
# Delete original schema & data XML
&clearSchemaFiles();
# Rename updated schema & data XML
system ("mv $bridgeLocalRoot/$schemaFileName"."Update.xml $bridgeLocalRoot/$schemaFileName.xml");
system ("mv $bridgeLocalRoot/$dataFileName"."Update.xml $bridgeLocalRoot/$dataFileName.xml");
# ****************************************************************************************
# ****
# **** (2) Parse schema XML to SQL
# ****
# ****************************************************************************************
#
print "Create database $createDatabaseName"."update\n";
print " Parse schema XML to SQL";
my $createSQL = "";
# Setup Schema XML
my $schemaparser = new XML::LibXML;
my $schemadom = $schemaparser->parse_file("$bridgeLocalRoot/$schemaFileName.xml");
my $schemaxmlobj = new XML::SimpleObject::LibXML ($schemadom);
my $schemafilesobj = $schemaxmlobj->child("schema");
# Create database
$createSQL .= "CREATE DATABASE $createDatabaseName"."update WITH OWNER = $DBAdminName ENCODING='LATIN1';\n\n";
# Connect the database
$createSQL .= "\\c $createDatabaseName"."update\n\n";
foreach my $table ($schemafilesobj->children_names) {
my $schematable = $schemafilesobj->child($table);
my $isfirstfield = 1;
# Create table
$createSQL .= "CREATE TABLE $table (\n";
foreach my $field ($schematable->children_names) {
my $schemafield = $schematable->child($field);
# Load field attributes
my $fieldtype = $schemafield->child("type")->value;
my $fieldlength = $schemafield->child("length")->value;
my $fieldprimarykey = $schemafield->child("primarykey")->value;
my $fieldtypestring = &returnFieldTypeString($fieldtype, $fieldlength);
# Create field
if (!$isfirstfield) { $createSQL .= ",\n"; } $isfirstfield = 0;
$createSQL .= " $field $fieldtypestring";
if ($fieldprimarykey eq "Y") { $createSQL .= " PRIMARY KEY"; }
}
$createSQL .= "\n);\n\n";
# Apply table permissions for each database user
foreach my $databaseUser ($DBAdminName) {
$createSQL .= "GRANT ALL ON TABLE $table TO $databaseUser;\n";
}
$createSQL .= "\n";
print "."; # Progress indicator
}
print "\n";
# ****************************************************************************************
# ****
# **** (3) Parse data XML to SQL
# ****
# ****************************************************************************************
# ****
#
print " Parse data XML to SQL";
my $insertSQL = "";
# Setup Data XML
my $dataparser = new XML::LibXML;
my $datadom =$dataparser->parse_file("$bridgeLocalRoot/$dataFileName.xml");
my $dataxmlobj = new XML::SimpleObject::LibXML ($datadom);
my $datafilesobj = $dataxmlobj->child("data");
foreach my $table ($datafilesobj->children_names) {
my $datatable = $datafilesobj->child($table);
foreach my $row ($datatable->children) {
my $insertIsFirstField = 1;
# SQL insert statement for each row
$insertSQL .= "insert into $table values(";
# SQL for each field value
foreach my $field ($row->children_names) {
my $isNumeric = &isNumeric($table, $field);
my $fieldValue = $row->child($field)->value;
if (!$insertIsFirstField) { $insertSQL .= ","; } $insertIsFirstField = 0;
if (!$isNumeric) { $insertSQL .= "'"; }
if ($fieldValue) { # is not null
$fieldValue =~ s/(['"])/\\$1/g;
$insertSQL .= $fieldValue;
} else {
if ($isNumeric) {
$insertSQL .= "0";
}
}
if (!$isNumeric) { $insertSQL .= "'"; }
}
$insertSQL .= ");\n";
}
$insertSQL .= "\n";
print "."; # Progress indicator
}
print "\n";
# Build SQL file
print " Build SQL file\n";
open (SQLFILE, "> $bridgeLocalRoot/$SQLFile") or die "Couldn't open $bridgeLocalRoot/$SQLFile for writing: $!\n";
print SQLFILE $createSQL.$insertSQL;
close (SQLFILE);
# Execute SQL file
print " Execute SQL file with psql\n";
&doSQLFile($SQLFile);
print "\n";
# ****************************************************************************************
# ****
# **** (4) Drop original database
# ****
# ****************************************************************************************
# ****
#
print "Establish database $createDatabaseName"."update\n";
$dropSQL = "DROP DATABASE $createDatabaseName;";
# Execute drop SQL
if (&databaseExists($createDatabaseName)) { # Continue if original database exists
print " Drop database $createDatabaseName";
if (&databaseExists($createDatabaseName."update")) { # Continue if update database exists
my $warned = 0;
for ($connectCount=0;$connectCount<$dropAttempts;$connectCount++) { # Waiting until original database is not in use
if (&databaseInUse($createDatabaseName)) { # Wait a bit
if (!$warned) {
print "\n Database in use! Waiting";
$warned = 1;
}
print ".";
sleep ($delayBetweenDropAttempts);
} else { # Drop original database
&doSQL($dropSQL);
if (!(&databaseExists($createDatabaseName))) {
# Success
print "\n";
last;
}
}
}
} else { # Exception - Update database does not exist.
print "\n $createDatabaseName"."update does not exist. End update\n";
exit;
}
}
if ($connectCount==$dropAttempts) {
if (&databaseExists($createDatabaseName)) {
print "\n Wait limit reached. I am giving up\n";
&clearSchemaFiles();
exit;
}
}
# ****************************************************************************************
# ****
# **** (5) Rename update database to original's name
# ****
# ****************************************************************************************
# ****
#
$renameSQL .= "ALTER DATABASE $createDatabaseName"."update RENAME TO $createDatabaseName";
# Execute rename SQL
print " Rename database $createDatabaseName"."update to $createDatabaseName";
if (&databaseExists($createDatabaseName."update")) {
&doSQL($renameSQL);
}
print "\n";
# Concluding summary
print "\n";
print "Done. The ".uc($createDatabaseName)." update completed in ";
print time() - $timeStart." seconds.\n";
print "\n";
print "Psql log files created:\n";
system ("ls $bridgeLocalRoot/$SQLLogFile $bridgeLocalRoot/$SQLWarningLogFile -l");
&clearSQLFiles();
# End
############################################ FUNCTIONS ##############################################
# Translate schema field type to database specific field type
sub returnFieldTypeString {
my ($fieldtype, $fieldlength) = @_;
if ($fieldtype eq "Text") {
return "varchar($fieldlength)";
} elsif ($fieldtype eq "Integer") {
return "integer";
} elsif ($fieldtype eq "Double") {
$fieldlength =~ s/\./\,/;
return "numeric($fieldlength)";
}
}
# Determine whether schema field type is numeric
sub isNumeric() {
my ($table, $field) = @_;
my $fieldtype = $schemafilesobj->child($table)->child($field)->child("type")->value;
if ($fieldtype eq "Text") {
return 0;
} elsif ($fieldtype eq "Integer") {
return 1;
} elsif ($fieldtype eq "Double") {
return 1;
}
}
# Check whether database exists
sub databaseExists () {
my ($database) = @_;
my $databaseExists = 0;
my $subCommand = "psql --list -A ";
$subCommand .= "> $bridgeLocalRoot/$tmpFile";
system($subCommand);
open (TMPFILE, "< $bridgeLocalRoot/$tmpFile") or die "\nCouldn't open $bridgeLocalRoot/$tmpFile";
my @tmpFileData = <TMPFILE>;
close (TMPFILE);
my $activeDatabaseResultCount = @tmpFileData;
my $t=1;
foreach my $tmpFileData (@tmpFileData) {
if (($t>2)&&($t!=$activeDatabaseResultCount)) {
my @fields = split('\|',$tmpFileData);
if ($fields[0] eq $database) {
$databaseExists = 1;
}
}
$t++;
}
if (-e "$bridgeLocalRoot/$tmpFile") { system ("rm $bridgeLocalRoot/$tmpFile"); }
return $databaseExists;
}
# Check whether database in use
sub databaseInUse () {
my ($database) = @_;
my $databaseInUse = 1;
my $subCommand = "/usr/bin/psql -c \"SELECT usename FROM pg_stat_activity WHERE datname = '$createDatabaseName';\" -A ";
$subCommand .= "> $bridgeLocalRoot/$tmpFile";
system ($subCommand);
open (TMPFILE, "< $bridgeLocalRoot/$tmpFile") or die "\nCouldn't open $bridgeLocalRoot/$tmpFile";
my @tmpFileData = <TMPFILE>;
close (TMPFILE);
foreach my $tmpFileData (@tmpFileData) {
$_ = $tmpFileData;
if (m/0 rows/) {
$databaseInUse = 0;
}
}
if (-e "$bridgeLocalRoot/$tmpFile") { system ("rm $bridgeLocalRoot/$tmpFile"); }
return $databaseInUse;
}
# Do SQL and log accordingly
sub doSQL () {
my ($SQL) = @_;
$systemCommand = "psql -c '$SQL'"; # Run the SQL command with the command line program psql
$systemCommand .= " >> $bridgeLocalRoot/$SQLLogFile"; # Append the earlier log file
$systemCommand .= " 2>> $bridgeLocalRoot/$SQLWarningLogFile"; # Append the earlier warning file
system ($systemCommand);
}
# Do SQL file and log accordingly
sub doSQLFile () {
my ($SQLFile) = @_;
$systemCommand = "psql -f $bridgeLocalRoot/$SQLFile"; # Run the SQL file with the command line program psql
$systemCommand .= " > $bridgeLocalRoot/$SQLLogFile"; # Create log file
$systemCommand .= " 2> $bridgeLocalRoot/$SQLWarningLogFile"; # Create warning file
system ($systemCommand);
}
# Clear original schema files
sub clearSchemaFiles () {
if (-e "$bridgeLocalRoot/$schemaFileName.xml") { system ("rm $bridgeLocalRoot/$schemaFileName.xml"); }
if (-e "$bridgeLocalRoot/$dataFileName.xml") { system ("rm $bridgeLocalRoot/$dataFileName.xml"); }
}
# Clear SQL files
sub clearSQLFiles () {
if (-e "$bridgeLocalRoot/$SQLFile") { system ("rm $bridgeLocalRoot/$SQLFile"); }
}