initial incarnation of newdblayer as really separated opensource sdbl4j library
authorGuillaume Cottenceau <gcottenc@gmail.com>
Fri, 18 Feb 2011 13:59:47 +0000 (14:59 +0100)
committerGuillaume Cottenceau <gcottenc@gmail.com>
Fri, 18 Feb 2011 14:36:21 +0000 (15:36 +0100)
20 files changed:
.gitignore [new file with mode: 0644]
APACHE-LICENSE-2.0 [new file with mode: 0644]
LICENSING [new file with mode: 0644]
README [new file with mode: 0644]
bin/gen-db-access-class.pl [new file with mode: 0755]
bin/gen-db-access-classes.pl [new file with mode: 0755]
build.xml [new file with mode: 0644]
lib/apache-tomcat-servlet-api-6.0.29.jar [new file with mode: 0644]
lib/log4j-1.2.15.jar [new file with mode: 0644]
src/java/org/gc/sdbl4j/BatchHelper.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/BatchedUpdateHandle.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/ConnectionParameters.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/DBConnectionPool.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/DBSelectHelpers.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/DBSelectMultiService.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/DBSelectService.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/DBUpdateService.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/DBUtils.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/DatabaseConnectionHandlerFilter.java [new file with mode: 0644]
src/java/org/gc/sdbl4j/SupplementaryThreadHelper.java [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..9d0b71a
--- /dev/null
@@ -0,0 +1,2 @@
+build
+dist
diff --git a/APACHE-LICENSE-2.0 b/APACHE-LICENSE-2.0
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/LICENSING b/LICENSING
new file mode 100644 (file)
index 0000000..03ac246
--- /dev/null
+++ b/LICENSING
@@ -0,0 +1,13 @@
+Copyright 2010 Guillaume Cottenceau and MNC S.A.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you
+may not use this site or the software running this site except in
+compliance with the License. You may obtain a copy of the License at
+
+           http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, this is
+provided on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the specific
+language governing permissions and limitations under the
+License.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..ff80f17
--- /dev/null
+++ b/README
@@ -0,0 +1,3 @@
+sdbl4j - Simple DB Layer for Java
+
+See website for documentation.
diff --git a/bin/gen-db-access-class.pl b/bin/gen-db-access-class.pl
new file mode 100755 (executable)
index 0000000..6c39b28
--- /dev/null
@@ -0,0 +1,519 @@
+#!/usr/bin/perl
+#
+# Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+#
+# This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+#
+
+use Data::Dumper;
+use XML::LibXML;
+
+my $DEBUG = 0;
+
+for my $input (@ARGV) {
+    $input =~ s|^\./||;
+
+    #- parse input file
+    my $parser = XML::LibXML->new();
+    my $doc = $parser->parse_file($input);
+    
+    sub get_data {
+        my ($doc, $name) = @_;
+        my @retval = ();
+        foreach my $element ($doc->getElementsByTagName($name)) {
+            my $attributes = {};
+            my $retval = { attributes => $attributes };
+            if ($element->getFirstChild) {
+                $retval->{cdata} = $element->getFirstChild->getData;
+            }
+            foreach my $attr ($element->attributes) {
+                $attributes->{$attr->name} = $attr->getValue;
+            }
+            push @retval, $retval;
+        }
+        return @retval;
+    }
+    
+    my @sql = get_data($doc, 'sql');
+    my @variation = get_data($doc, 'variation');
+    my @db = get_data($doc, 'db');
+    my @in = get_data($doc, 'in');
+    my @out = get_data($doc, 'out');
+    my @multi = get_data($doc, 'multiRows');
+    my @addableSql = get_data($doc, 'addableSql');
+    my @implements = get_data($doc, 'implements');
+    my @outsugar = get_data($doc, 'outsugar');
+    my @postOrdering = get_data($doc, 'postOrdering');
+    @postOrdering && !@multi and die "$input: <postOrdering> without <multiRows> makes no sense.\n";
+
+    my $logerror = get_data($doc, 'noErrorLog') ? "log.debug" : "log.error";
+    
+    foreach my $out (@out) {
+        my $colname = $out->{attributes}{name};
+        $colname =~ s/([a-z])([A-Z])/${1}_$2/g;
+        $out->{colname} = lc($colname);
+    }
+    
+    if ($DEBUG) {
+        print STDERR "sql: $sql\ndb: $db\n"
+              . Data::Dumper->Dump([\@in], ['in'])
+              . Data::Dumper->Dump([\@out], ['out']);
+    }
+    
+    
+    #- sanity check input
+    if (@sql != 1) {
+        die "$input: there should be one <sql> element\n";
+    }
+    my $sql = $sql[0]{cdata};
+    if ($sql =~ /##VARIATIONS##/ && @variation < 2) {
+        die "$input: at least two <variation> elements make sense when there is ##VARIATION## in SQL\n";
+    }
+    if (@db != 1) {
+        die "$input: there should be one <db> element\n";
+    }
+    my $db = $db[0]{cdata};
+    my $implements = @implements ? $implements[0]{cdata} : '';
+    
+    my $parameters_amount = 0;
+    $parameters_amount++ while $sql =~ /\?/g;
+    
+    foreach my $param (@in, @out) {
+        if (!member($param->{attributes}{type}, qw(Boolean Integer BigDecimal Double Long String Timestamp Date byte[] Array Object[] String[]))) {
+            die "$input: $param->{attributes}{name}: '$param->{attributes}{type}' is not a supported type\n";
+        }
+        if ($param->{attributes}{name} !~ /^[a-zA-Z_0-9]+$/) {
+            die "$input: $param->{attributes}{name}: should be all alphanumerical or underscore\n";
+        }
+        while ($param->{attributes}{parameter} =~ /(\d+)/g) {
+            if ($1 < 1 || $1 > $parameters_amount) {
+                die "$input: $param->{attributes}{name}: '$1' is not in the parameters amount range (1..$parameters_amount)\n";
+            }
+        }
+        #- make sure we don't accidentally hit a reserved word
+        if (member($param->{attributes}{name}, qw(public protected private static transient volatile class this void))) {
+            $param->{attributes}{varname} = '_' . $param->{attributes}{name};
+        } else {
+            $param->{attributes}{varname} = $param->{attributes}{name};
+        }
+    }
+    
+    #- check there are no unset in parameters
+    if (@in) {
+        foreach my $paramnumber (1 .. $parameters_amount) {
+            (grep { $_->{attributes}{parameter} =~ /\b$paramnumber\b/ } @in) > 0
+              or die "$input: parameter number '$paramnumber' set by no 'in' parameter\n";
+        }
+    }
+    
+    #- output Java class
+    my $kind = @out == 0 ? 'update' : @multi == 0 ? 'uniqueSelect' : 'multiSelect';
+    
+    my $package = $input;
+    $package =~ s|.*\borg\b|org|;
+    $package =~ s|/[^/]+$||;
+    $package =~ s|/|.|g;
+    
+    my $output;
+    
+    $output .= "package $package;
+    
+import org.apache.log4j.Logger;
+    
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.ResultSet;
+";
+    if ((grep { $_->{attributes}{type} eq 'Timestamp' } @in, @out) > 0) {
+        $output .= "import java.sql.Timestamp;\n";
+    }
+    if ((grep { $_->{attributes}{type} eq 'BigDecimal' } @in, @out) > 0) {
+        $output .= "import java.math.BigDecimal;\n";
+    }
+    if ((grep { $_->{attributes}{type} eq 'Array' } @in, @out) > 0) {
+        $output .= "import java.sql.Array;\n";
+    }
+    if ((grep { $_->{attributes}{type} eq 'Date' } @in, @out) > 0) {
+        $output .= "import java.util.Date;\n";
+    }
+    if (@postOrdering) {
+        $output .= "import java.util.Collections;\n";
+    }
+    $output .= "\nimport org.gc.sdbl4j.DBUtils;\n";
+    if ($kind eq 'update') {
+        $output .= "import org.gc.sdbl4j.BatchHelper;\n";
+        $output .= "import org.gc.sdbl4j.BatchedUpdateHandle;\n";
+    } else {
+        $output .= "import org.gc.sdbl4j.DBSelectHelpers;\n";
+    }
+    my $motherclass = $kind eq 'update' ? 'DBUpdateService'
+              : $kind eq 'uniqueSelect' ? 'DBSelectService'
+                                        : 'DBSelectMultiService';
+    $output .= "import org.gc.sdbl4j.DBConnectionPool;
+import org.gc.sdbl4j.$motherclass;
+
+";
+    
+    (my $classname = basename($input)) =~ s/\..*//;
+
+    $output .= "/*************************************************************************
+* !!! IMPORTANT !!! DO NOT MODIFY THIS FILE, IT HAS BEEN AUTOGENERATED! *
+*************************************************************************/
+public class $classname extends $motherclass";
+    if ($kind eq 'multiSelect') {
+        $output .= "<$classname.RowContainer>";
+    }
+    
+    if ($implements) {
+        $output .= " implements " . trim($implements);
+    }
+    
+    $sql =~ s/\n/" +\n        "/g;
+    
+    $output .= " {
+
+    private static Logger log = Logger.getLogger( $classname.class );
+
+    private static final String sql =
+        \"$sql\";
+
+";
+
+    if (@variation) {
+        $output .= "    public enum Variation { " . join(", ", map { $_->{attributes}{name} } @variation) . " };
+
+    private static String getSql( Variation variation ) {\n";
+        foreach my $variation (@variation) {
+            $output .= "        if ( variation == Variation." . $variation->{attributes}{name} . " ) {
+            return sql.replaceFirst( \"##VARIATIONS##\", \"$variation->{cdata}\" );
+        }\n";
+        }
+        $output .= "        log.error( \"unknown variation \" + variation );
+        return null;
+    }\n\n";
+    }
+    
+    if ($kind eq 'update') {
+        $output .= "    /**\n     * Perform the database request, return the number of rows modified or -1 or error.\n";
+    } elsif ($kind eq 'uniqueSelect') {
+        $output .= "    /**\n     * Perform the database request, return a container for the received data, or <code>null</code> if the query
+     * didn't return any rows.\n";
+    } else {
+        $output .= "    /**\n     * Perform the database request, return a container holding all the received rows. You must call 
+     * {\@link #next()} to iterate over the rows.\n";
+    }
+    
+    $output .= "     */\n";
+    
+    if ($kind eq 'update') {
+        $output .= "    public static int perform(";
+    } else {
+        $output .= "    public static $classname perform(";
+    }
+    if (@addableSql) {
+        $output .= " String sqlAddition";
+    }
+    if (@variation) {
+        if (@addableSql) {
+            $output .= ', ';
+        }
+        $output .= " Variation variation";
+    }
+    
+    my $perform_params = join(', ', map { $_->{attributes}{type} . ' ' . $_->{attributes}{varname} } @in);
+    if ($perform_params) {
+        if (@addableSql || @variation) {
+            $output .= ',';
+        }
+        $output .=  " $perform_params ";
+    } else {
+        if (@addableSql || @variation) {
+            $output .= ' ';
+        }
+    }
+    
+    my $get_connection_indent = ' ' x length("            ps = DBConnectionPool.getConnection( \"$db\" ).prepareStatement(");
+    
+    $output .= ') {
+        /*************************************************************************
+         * !!! IMPORTANT !!! DO NOT MODIFY THIS FILE, IT HAS BEEN AUTOGENERATED! *
+         *************************************************************************/
+        PreparedStatement ps = null;
+        try {';
+    my $ps_creator = '';
+    my $sql = @variation ? "getSql( variation )" : "sql";
+    if (@addableSql) {
+        $ps_creator .= "
+            ps = DBConnectionPool.getConnection( \"$db\" ).prepareStatement( $sql + sqlAddition,";
+    } else {
+        $ps_creator .= "
+            ps = DBConnectionPool.getConnection( \"$db\" ).prepareStatement( $sql,";
+    }
+    $ps_creator .= "
+$get_connection_indent ResultSet.TYPE_SCROLL_INSENSITIVE,
+$get_connection_indent ResultSet.CONCUR_READ_ONLY );
+
+";
+    $output .= $ps_creator;
+    
+    my %simpletypes = (Integer => { sqltype => 'INTEGER', pssetter => 'Int' },
+                       Long => { sqltype => 'BIGINT', pssetter => 'Long' },
+                       Double => { sqltype => 'DOUBLE', pssetter => 'Double' },
+                       Boolean => { sqltype => 'BOOLEAN', pssetter => 'Boolean' });
+
+    my $params = '';
+
+    my %type2getter = ('Boolean' => 'getBoolean', 'Integer' => 'getInteger', 'BigDecimal' => 'getBigDecimal', 'Double' => 'getDouble',
+                       'Long' => 'getLong', 'String' => 'getString', 'Timestamp' => 'getTimestamp',
+                       'Date' => 'getDate', 'byte[]' => 'getBytes', 'Array' => 'getArray', 'Object[]' => 'getArrayAsObjectArray',
+                       'String[]' => 'getArrayAsStringArray' );
+    
+    foreach my $param (@in) {
+        while ($param->{attributes}{parameter} =~ /(\d+)/g) {
+            my $index = $1;
+            if (member($param->{attributes}{type}, keys %simpletypes)) {
+                $output =~ /java.sql.Types/ or $output =~ s/(import java.sql.ResultSet;)/$1\nimport java.sql.Types;/;
+                $params .= "            if ( $param->{attributes}{varname} == null ) {\n"
+                         . "                ps.setNull( $index, Types.$simpletypes{$param->{attributes}{type}}{sqltype} );\n"
+                         . "            } else {\n"
+                         . "                ps.set$simpletypes{$param->{attributes}{type}}{pssetter}( $index, $param->{attributes}{varname} );\n"
+                         . "            }\n";
+            } elsif ($param->{attributes}{type} eq 'byte[]') {
+                $params .= "            ps.setBytes( $index, $param->{attributes}{varname} );\n";
+            } elsif ($param->{attributes}{type} eq 'Date') {
+                $params .= "            ps.setDate( $index, new java.sql.Date( $param->{attributes}{varname}.getTime() ) );\n";
+            } else {
+                $params .= "            ps.set$param->{attributes}{type}( $index, $param->{attributes}{varname} );\n";
+            }
+        }
+    }
+
+    $output .= $params;
+    
+    if ($kind eq 'update') {
+        $output .= "            int result = executeUpdate( ps );
+            ps.close();
+            return result;
+        } catch ( SQLException se ) {
+            String thissql = ps == null ? \"\" : ps.toString().replaceAll( \"\\\\s+\", \" \" );
+            $logerror( \"failed to perform SQL request\" + thissql + \"\\n\" + DBUtils.prettyPrint( se ) );
+            return -1;
+        }
+    }
+
+    /**
+     * Get a BatchedUpdateHandle object suitable for then sending updates in batch.
+     */
+    public static BatchedUpdateHandle<$classname> getBatchedUpdateHandle(";
+    if (@addableSql) {
+        $output .= " String sqlAddition ";
+    }
+    $output .= ") {
+        PreparedStatement ps = null;
+        try {";
+    $output .= $ps_creator;
+    $output .= "            return new BatchedUpdateHandle<$classname>( ps );
+        } catch ( SQLException se ) {
+            log.error( \"failed to created batched update handle\\n\" + DBUtils.prettyPrint( se ) );
+            return null;
+        }
+    }
+
+    /**
+     * Add an update for a given BatchedUpdateHandle object.
+     * WARNING: do not let too many updates accumulate before performing the 
+     * batched update, because of memory exhaustion risk.
+     */
+    public static void addBatchedUpdate( BatchedUpdateHandle<$classname> handle";
+    if ($perform_params) {
+        $output .=  ", $perform_params ";
+    } else {
+        $output .= ' ';
+    }
+    $output .= ") {
+        PreparedStatement ps = handle.getPreparedStatement();
+        try {
+";
+    $output .= $params;
+    $output .= "            ps.addBatch();
+        } catch ( SQLException se ) {
+            log.error( \"failed to add batched update\\n\" + DBUtils.prettyPrint( se ) );
+        }
+    }
+
+    /**
+     * Perform the updates added to the given BatchedUpdateHandle object.
+     * WARNING: do not let too many updates accumulate before performing the 
+     * batched update, because of memory exhaustion risk.
+     */
+    public static int[] performBatchedUpdate( BatchedUpdateHandle<$classname> handle ) {
+        try {
+            return BatchHelper.performBatchedUpdates( handle.getPreparedStatement() );
+        } catch ( SQLException se ) {
+            log.error( \"failed to perform batched updates\\n\" + DBUtils.prettyPrint( se ) );
+            return null;
+        }
+    }
+
+    private $classname() {}
+
+}\n";
+    
+    } elsif ($kind eq 'uniqueSelect') {
+        my $indent = ' ' x length($classname);
+        $output .= "            ResultSet rs = executeQuery( ps );
+            if ( rs.next() ) {
+                $classname ret
+                    = new $classname( " . join(",\n                          $indent  ",
+                                        map { "DBSelectHelpers.$type2getter{$_->{attributes}{type}}( \"$_->{colname}\", rs )" } @out)
+                                        . " );
+                ps.close();
+                return ret;
+            } else {
+                return null;
+            }
+        } catch ( SQLException se ) {
+            String thissql = ps == null ? \"\" : ps.toString().replaceAll( \"\\\\s+\", \" \" );
+            $logerror( \"failed to perform SQL request\" + thissql + \"\\n\" + DBUtils.prettyPrint( se ) );
+            return null;
+        }
+    }\n\n";
+    
+        foreach my $param (@out) {
+            $output .= "    private $param->{attributes}{type} $param->{attributes}{varname};\n";
+        }
+    
+        $output .= "
+    private $classname( " . join(', ', map { $_->{attributes}{type} . ' ' . $_->{attributes}{varname} } @out) . " ) {\n";
+        foreach my $param (@out) {
+            $output .= "        this.$param->{attributes}{varname} = $param->{attributes}{varname};\n";
+        }
+        $output .= "    }\n\n";
+        foreach my $param (@out) {
+            my $methname = $param->{attributes}{name};
+            $methname =~ s/(.)/uc($1)/e;
+            $methname =~ s/_(.)/uc($1)/ge;
+            $output .= "    public $param->{attributes}{type} get$methname() {
+        return $param->{attributes}{varname};
+    }\n";
+        }
+        $output .= "\n";
+        foreach my $param (@outsugar) {
+            my $methname = $param->{attributes}{name};
+            $methname =~ s/(.)/uc($1)/e;
+            $methname =~ s/_(.)/uc($1)/ge;
+            $output .= "    public $param->{attributes}{type} get$methname" . ($methname =~ /\(/ ? '' : '()') . " {\n";
+            if ($param->{attributes}{code} =~ /return/) {
+                $output .= $param->{attributes}{code};
+            } else {
+                $output .= "        return $param->{attributes}{code};\n";
+            }
+            $output .= "    }\n";
+        }
+        $output .= "}\n";
+    
+    } else {
+        $output .= "            ResultSet rs = executeQuery( ps );
+            $classname ret = new $classname();
+            while ( rs.next() ) {
+                ret.add( " . join(",\n                         ",
+                                  map { "DBSelectHelpers.$type2getter{$_->{attributes}{type}}( \"$_->{colname}\", rs )" } @out)
+                           . " );
+            }
+            ps.close();";
+        if (@postOrdering) {
+            $output .= "
+            Collections.sort( ret.rows );";
+        } 
+            $output .= "
+            return ret;
+        } catch ( SQLException se ) {
+            String thissql = ps == null ? \"\" : ps.toString().replaceAll( \"\\\\s+\", \" \" );
+            $logerror( \"failed to perform SQL request\" + thissql + \"\\n\" + DBUtils.prettyPrint( se ) );
+            return null;
+        }
+    }\n";
+        if (@postOrdering) {
+            $output .= "
+    protected class RowContainer implements Comparable<RowContainer> {\n";
+        } else {
+            $output .= "
+    protected class RowContainer {\n";
+        }
+        foreach my $param (@out) {
+            $output .= "        public $param->{attributes}{type} $param->{attributes}{varname};\n";
+        }
+        $output .= "        public RowContainer( " . join(', ', map { "$_->{attributes}{type} $_->{attributes}{varname}" } @out) . " ) {\n";
+        foreach my $param (@out) {
+            $output .= "            this.$param->{attributes}{varname} = $param->{attributes}{varname};\n";
+        }
+        $output .= "        }\n";
+        if (@postOrdering) {
+            $output .= "
+        public int compareTo( RowContainer o ) {\n";
+            if ($postOrdering[0]{attributes}{code} =~ /\breturn\b/) {
+                $output .= "            $postOrdering[0]{attributes}{code}\n";
+            } else {
+                $output .= "            return " . $postOrdering[0]{attributes}{code} . ";\n";
+            }
+            $output .= "
+        }";
+        }
+        $output .= "
+    }
+
+    private $classname() {}\n\n";
+    
+        $output .= "    private void add( " . join(', ', map { $_->{attributes}{type} . ' ' . $_->{attributes}{varname} } @out) . " ) {
+        rows.add( new RowContainer( " . join(', ', map { $_->{attributes}{varname} } @out) . " ) );
+    }\n\n";
+    
+        foreach my $param (@out) {
+            my $methname = $param->{attributes}{name};
+            $methname =~ s/(.)/uc($1)/e;
+            $methname =~ s/_(.)/uc($1)/ge;
+            $output .= "    public $param->{attributes}{type} get$methname() {
+        if ( currentRow == null ) {
+            log.error( \"No current row! #next wasn't called? row number = \" + getRow() + \"; rows = \" + getRows() + \"\\n\"
+                       + DBUtils.backtrace() );
+            return null;
+        }
+        return currentRow.$param->{attributes}{varname};
+    }\n";
+        }
+    
+        foreach my $param (@outsugar) {
+            my $methname = $param->{attributes}{name};
+            $methname =~ s/(.)/uc($1)/e;
+            $methname =~ s/_(.)/uc($1)/ge;
+            $output .= "    public $param->{attributes}{type} get$methname " . ($methname =~ /\(/ ? '' : '()') . " {
+        if ( currentRow == null ) {
+            log.error( \"No current row! #next wasn't called? row number = \" + getRow() + \"; rows = \" + getRows() + \"\\n\"
+                       + DBUtils.backtrace() );
+            return null;
+        }\n";
+            if ($param->{attributes}{code} =~ /return/) {
+                $output .= $param->{attributes}{code};
+            } else {
+                $output .= "        return $param->{attributes}{code};\n";
+            }
+            $output .= "\n}\n";
+        }
+        $output .= "}\n";
+    }
+    
+    my $file = $input;
+    $file =~ s/\.db\.xml$/\.java/;
+    output($file, $output);
+}
+
+
+sub member { my $e = shift; foreach (@_) { $e eq $_ and return 1 } 0 }
+sub basename { local $_ = shift; s|/*\s*$||; s|.*/||; $_ }
+sub output { my $f = shift; open(my $F, ">$f") or die "output in file $f failed: $!\n"; print $F $_ foreach @_; 1 }
+sub trim { $_[0] =~ s/^\s+//; $_[0] =~ s/\s+$//; return $_[0]; }
+sub timing {
+    my $dur = gettimeofday() - $time;
+#    print STDERR $_[0] . " in " . $dur . "\n";
+    $time = gettimeofday();
+}
diff --git a/bin/gen-db-access-classes.pl b/bin/gen-db-access-classes.pl
new file mode 100755 (executable)
index 0000000..b01ba65
--- /dev/null
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+#
+# Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+#
+# This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+#
+
+@ARGV > 0 or die "Usage: gen-db-access-classes.pl <dirname>+\n";
+-d $_ or die "Parameter $_ not a directory\n" foreach @ARGV;
+
+my @to_generate = ();
+
+foreach (@ARGV) {
+    foreach my $dbxml_src (`find $_ -name "*.db.xml"`) {
+        chomp $dbxml_src;
+        my $dbxml_mtime = (stat($dbxml_src))[9];
+        (my $java_src = $dbxml_src) =~ s/\.db\.xml$/.java/;
+        my $java_mtime = (stat($java_src))[9];
+        my $generator_mtime = (stat("bin/db/gen-db-access-class.pl"))[9];
+        if (!defined($java_mtime) || $dbxml_mtime > $java_mtime || $generator_mtime > $java_mtime) {
+            push @to_generate, $dbxml_src;
+        }
+    }
+}
+
+#- guess base dir
+my $basedir = `readlink -f "$0"`;
+chomp $basedir;
+$basedir =~ s|/[^/]+$||;
+
+if (@to_generate) {
+    print "Generating " . int(@to_generate) . " database access source files\n";
+    my $cmd = "$basedir/gen-db-access-class.pl @to_generate";
+    system($cmd) == 0 or die "Failed invoking $cmd: $!\n";
+}
diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..ea22052
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,227 @@
+<!--\r
+     General purpose build script for web applications and web services,\r
+     including enhanced support for deploying directly to a Tomcat 5\r
+     based server.\r
+\r
+     This build script assumes that the source code of your web application\r
+     is organized into the following subdirectories underneath the source\r
+     code directory from which you execute the build script:\r
+\r
+        docs                 Static documentation files to be copied to\r
+                             the "docs" subdirectory of your distribution.\r
+\r
+        src                  Java source code (and associated resource files)\r
+                             to be compiled to the "WEB-INF/classes"\r
+                             subdirectory of your web applicaiton.\r
+\r
+        web                  Static HTML, JSP, and other content (such as\r
+                             image files), including the WEB-INF subdirectory\r
+                             and its configuration file contents.\r
+\r
+-->\r
+\r
+\r
+<!-- A "project" describes a set of targets that may be requested\r
+     when Ant is executed.  The "default" attribute defines the\r
+     target which is executed if no specific target is requested,\r
+     and the "basedir" attribute defines the current working directory\r
+     from which Ant executes the requested task.  This is normally\r
+     set to the current working directory.\r
+-->\r
+\r
+<project name="sdbl4j" default="dist" basedir=".">\r
+\r
+\r
+\r
+<!-- ===================== Property Definitions =========================== -->\r
+\r
+\r
+<!--\r
+\r
+  Each of the following properties are used in the build script.\r
+  Values for these properties are set by the first place they are\r
+  defined, from the following list:\r
+\r
+  * Definitions on the "ant" command line (ant -Dfoo=bar compile).\r
+\r
+  * Definitions from a "build.properties" file in the top level\r
+    source directory of this application.\r
+\r
+  * Definitions from a "build.properties" file in the developer's\r
+    home directory.\r
+\r
+  * Default definitions in this build.xml file.\r
+\r
+  You will note below that property values can be composed based on the\r
+  contents of previously defined properties.  This is a powerful technique\r
+  that helps you minimize the number of changes required when your development\r
+  environment is modified.  Note that property composition is allowed within\r
+  "build.properties" files as well as in the "build.xml" script.\r
+\r
+-->\r
+\r
+  <property file="build.properties"/>\r
+  <property file="${user.home}/build.properties"/>\r
+\r
+\r
+<!-- ==================== File and Directory Names ======================== -->\r
+\r
+\r
+<!--\r
+\r
+  These properties generally define file and directory names (or paths) that\r
+  affect where the build process stores its outputs.\r
+\r
+  app.name             Base name of this application, used to\r
+                       construct filenames and directories.\r
+                       Defaults to "myapp".\r
+\r
+  app.path             Context path to which this application should be\r
+                       deployed (defaults to "/" plus the value of the\r
+                       "app.name" property).\r
+\r
+  app.version          Version number of this iteration of the application.\r
+\r
+  build.home           The directory into which the "prepare" and\r
+                       "compile" targets will generate their output.\r
+                       Defaults to "build".\r
+\r
+  dist.home            The name of the base directory in which\r
+                       distribution files are created.\r
+                       Defaults to "dist".\r
+\r
+  manager.password     The login password of a user that is assigned the\r
+                       "manager" role (so that he or she can execute\r
+                       commands via the "/manager" web application)\r
+\r
+  manager.url          The URL of the "/manager" web application on the\r
+                       Tomcat installation to which we will deploy web\r
+                       applications and web services.\r
+\r
+  manager.username     The login username of a user that is assigned the\r
+                       "manager" role (so that he or she can execute\r
+                       commands via the "/manager" web application)\r
+\r
+-->\r
+\r
+  <property name="app.name"      value="sdbl4j"/>\r
+  <property name="app.path"      value="/${app.name}"/>\r
+  <property name="app.version"   value="1.0"/>\r
+  <property name="build.home"    value="${basedir}/build"/>\r
+  <property name="dist.home"     value="${basedir}/dist"/>\r
+  <property name="src.home"      value="${basedir}/src"/>\r
+\r
+\r
+<!--  ==================== Compilation Control Options ==================== -->\r
+\r
+<!--\r
+\r
+  These properties control option settings on the Javac compiler when it\r
+  is invoked using the <javac> task.\r
+\r
+  compile.debug        Should compilation include the debug option?\r
+\r
+  compile.deprecation  Should compilation include the deprecation option?\r
+\r
+  compile.optimize     Should compilation include the optimize option?\r
+\r
+-->\r
+\r
+  <property name="compile.debug"       value="true"/>\r
+  <property name="compile.deprecation" value="false"/>\r
+  <property name="compile.optimize"    value="true"/>\r
+\r
+\r
+\r
+<!-- ==================== Compilation Classpath =========================== -->\r
+\r
+<!--\r
+\r
+  Rather than relying on the CLASSPATH environment variable, Ant includes\r
+  features that makes it easy to dynamically construct the classpath you\r
+  need for each compilation.  The example below constructs the compile\r
+  classpath to include the servlet.jar file, as well as the other components\r
+  that Tomcat makes available to web applications automatically, plus anything\r
+  that you explicitly added.\r
+\r
+-->\r
+\r
+  <path id="compile.classpath">\r
+\r
+    <fileset dir="${basedir}/lib">\r
+      <include name="*.jar"/>\r
+    </fileset>\r
+\r
+  </path>\r
+\r
+\r
+\r
+<!-- ==================== Clean Target ==================================== -->\r
+\r
+<!--\r
+\r
+  The "clean" target deletes any previous "build" and "dist" directory,\r
+  so that you can be ensured the application can be built from scratch.\r
+\r
+-->\r
+\r
+  <target name="clean">\r
+    <delete dir="${build.home}"/>\r
+    <delete dir="${dist.home}"/>\r
+  </target>\r
+\r
+\r
+\r
+<!-- ==================== Compile Target ================================== -->\r
+\r
+<!--\r
+\r
+  The "compile" target transforms source files (from your "src" directory)\r
+  into object files in the appropriate location in the build directory.\r
+  This example assumes that you will be including your classes in an\r
+  unpacked directory hierarchy under "/WEB-INF/classes".\r
+\r
+-->\r
+\r
+  <target name="compile" description="Compile Java sources">\r
+\r
+    <!-- Compile Java classes as necessary -->\r
+    <mkdir    dir="${build.home}/classes"/>\r
+    <javac srcdir="${src.home}/java"\r
+          destdir="${build.home}/classes"\r
+            debug="${compile.debug}"\r
+      deprecation="${compile.deprecation}"\r
+         optimize="${compile.optimize}">\r
+        <classpath refid="compile.classpath"/>\r
+        <compilerarg value="-Xlint"/>\r
+    </javac>\r
+\r
+  </target>\r
+\r
+\r
+\r
+<!-- ==================== Dist Target ===================================== -->\r
+\r
+\r
+<!--\r
+\r
+  The "dist" target creates a binary distribution of your application\r
+  in a directory structure ready to be archived in a tar.gz or zip file.\r
+  Note that this target depends on two others:\r
+\r
+  * "compile" so that the entire web application (including external\r
+    dependencies) will have been assembled\r
+\r
+  * "javadoc" so that the application Javadocs will have been created\r
+\r
+-->\r
+\r
+  <target name="dist" depends="compile" description="Create binary distribution">\r
+\r
+    <mkdir dir="${dist.home}"/>\r
+    <jar jarfile="${dist.home}/${app.name}-${app.version}.jar" basedir="${build.home}/classes"/>\r
+\r
+  </target>\r
+\r
+\r
+</project>\r
diff --git a/lib/apache-tomcat-servlet-api-6.0.29.jar b/lib/apache-tomcat-servlet-api-6.0.29.jar
new file mode 100644 (file)
index 0000000..32aa9d6
Binary files /dev/null and b/lib/apache-tomcat-servlet-api-6.0.29.jar differ
diff --git a/lib/log4j-1.2.15.jar b/lib/log4j-1.2.15.jar
new file mode 100644 (file)
index 0000000..c930a6a
Binary files /dev/null and b/lib/log4j-1.2.15.jar differ
diff --git a/src/java/org/gc/sdbl4j/BatchHelper.java b/src/java/org/gc/sdbl4j/BatchHelper.java
new file mode 100644 (file)
index 0000000..b91679f
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import org.apache.log4j.Logger;
+
+import java.sql.BatchUpdateException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+public class BatchHelper {
+
+    private static Logger log = Logger.getLogger( BatchHelper.class );
+
+    public static int[] performBatchedUpdates( PreparedStatement ps ) throws SQLException {
+        Connection connection = ps.getConnection();
+        // avoid rollbacking other queries/updates running concurrently on this connection, in case
+        // this batched update fails
+        synchronized( connection ) {
+            int[] ret = null;
+            // save current auto commit mode
+            boolean acMode = connection.getAutoCommit();
+            connection.setAutoCommit( false );
+            try {
+                if ( log.isDebugEnabled() ) {
+                    long begin = System.currentTimeMillis();
+                    ret = ps.executeBatch();
+                    log.debug( ( System.currentTimeMillis() - begin ) + " ms for: "
+                               + ps.toString().replaceAll( "\\s+", " " )
+                               + "[" + DBUtils.getStackTrace()[2].getClassName() + "/BATCHED]" );
+                } else {
+                    ret = ps.executeBatch();
+                }
+                if ( acMode ) {
+                    connection.commit();
+                }
+            } catch ( SQLException ex ) {
+                if ( acMode ) {
+                    connection.rollback();
+                }
+                log.error( ex );
+                if ( ex instanceof BatchUpdateException ) {
+                    log.error( "next exception: " + ex.getNextException() );
+                }
+                log.error( "\n" + DBUtils.backtrace() );
+            }
+            // restore old auto commit mode
+            connection.setAutoCommit( acMode );
+            return ret;
+        }
+    }
+
+}
diff --git a/src/java/org/gc/sdbl4j/BatchedUpdateHandle.java b/src/java/org/gc/sdbl4j/BatchedUpdateHandle.java
new file mode 100644 (file)
index 0000000..3ce8a1f
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import java.sql.PreparedStatement;
+
+public class BatchedUpdateHandle<T extends DBUpdateService> {
+    private PreparedStatement ps;
+    /**
+     * This attribute is not used internally, but is meant to be used as a counter when performing a lot of
+     * updates, to flush the SQL request regularly, to optimize performance and memory footprint.
+     */
+    public int flushLimitCounter = 0;
+    public BatchedUpdateHandle( PreparedStatement ps ) {
+        this.ps = ps;
+    }
+    public PreparedStatement getPreparedStatement() {
+        return ps;
+    }
+}
diff --git a/src/java/org/gc/sdbl4j/ConnectionParameters.java b/src/java/org/gc/sdbl4j/ConnectionParameters.java
new file mode 100644 (file)
index 0000000..5de4946
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import java.sql.Connection;
+
+public class ConnectionParameters {
+
+    private int preOpenedConnections;
+    private int alertLevel;
+    private int maxConnections;
+    private ConnectionCreator connectionCreator;
+    
+    public interface ConnectionCreator {
+        public Connection createConnection();
+    }
+    
+    public ConnectionParameters( int preOpenedConnections, int alertLevel, int maxConnections,
+                                 ConnectionCreator connectionCreator ) {
+        this.preOpenedConnections = preOpenedConnections;
+        this.alertLevel = alertLevel;
+        this.maxConnections = maxConnections;
+        this.connectionCreator = connectionCreator;
+    }
+
+    public int getPreOpenedConnections() {
+        return preOpenedConnections;
+    }
+    
+    public int getAlertLevel() {
+        return alertLevel;
+    }
+    
+    public int getMaxConnections() {
+        return maxConnections;
+    }
+    
+    public Connection createConnection() {
+        return connectionCreator.createConnection();
+    }
+    
+    public String toString() {
+        return "connectionParameters{preOpenedConnections=" + preOpenedConnections + ",alertLevel=" + alertLevel
+               + ",maxConnections=" + maxConnections + ",connectionCreator=" + connectionCreator + "}";
+    }
+}
diff --git a/src/java/org/gc/sdbl4j/DBConnectionPool.java b/src/java/org/gc/sdbl4j/DBConnectionPool.java
new file mode 100644 (file)
index 0000000..9b4cf72
--- /dev/null
@@ -0,0 +1,456 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import org.apache.log4j.Logger;
+
+/**
+ * There are subtle interleaved synchronizations happening here.
+ * Use a traffic load generator with low preOpenedConnections for all modifications in here!
+ */
+public class DBConnectionPool {
+
+    private static Logger log = Logger.getLogger( DBConnectionPool.class ); 
+
+    private static class BusyConnection {
+        public Connection conn;
+        public String backtrace;
+        public long tsTotal;
+        public long tsLatest;
+        public BusyConnection( Connection conn ) {
+            this.conn = conn;
+            backtrace = DBUtils.backtrace( 3 );
+            tsTotal = tsLatest = System.currentTimeMillis();
+        }
+        public void updateRequester() {
+            backtrace = DBUtils.backtrace( 2 );
+            tsLatest = System.currentTimeMillis();
+        }
+        public String toString() {
+            List<String> us = new ArrayList<String>();
+            boolean users_list_ok = false;
+            int attempts = 0;
+            while ( ! users_list_ok ) {
+                try {
+                    Set<Thread> users = supplementary_thread_busy_connection_users.get( conn );
+                    if ( users != null ) {
+                        for ( Thread u : users ) {
+                            us.add( u.getName() );
+                        }
+                    }
+                    users_list_ok = true;
+                } catch ( ConcurrentModificationException cme ) {
+                    // There are subtle interleaved synchronizations happening here.
+                    // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+                    attempts++;
+                    if ( attempts > 3 ) {
+                        log.error( "3 attempts, abandon" );
+                        break;
+                    }
+                    us.clear();
+                }
+            }
+            return "{" + conn + ",oldnessTotal=" + ( System.currentTimeMillis() - tsTotal )
+                   + ",oldnessLatest=" + ( System.currentTimeMillis() - tsLatest )
+                   + ",supplementaryThreadUsers=" + DBUtils.join( ";", us ) + ",requester=\n" + backtrace + "}";
+        }
+    }
+    
+    private static Map<String, ConnectionParameters> dbParameters = null;
+    private static Map<String, LinkedList<Connection>> available_connections;
+    private static Map<String, ConcurrentMap<Object, BusyConnection>> busy_connections;
+    private static Map<Connection, Set<Thread>> supplementary_thread_busy_connection_users;
+    
+    /** 
+     * List of threads that are created in external libraries in that require a db connection. 
+     * All connections from those threads MUST be manually released after use.
+     * */
+    private static Set<String> supported_external_threads;
+
+    public static void init( Map<String, ConnectionParameters> parameters ) {
+        dbParameters = parameters;
+        available_connections = new HashMap<String, LinkedList<Connection>>();
+        busy_connections = new HashMap<String, ConcurrentMap<Object, BusyConnection>>();
+        for ( Map.Entry<String, ConnectionParameters> db : parameters.entrySet() ) {
+            log.info( "Initializing: " + db.getValue() );
+            if ( db.getValue().getPreOpenedConnections() > db.getValue().getMaxConnections() ) {
+                log.error( db.getKey() + ": pre opened connections > max connections, this is stupid!" );
+            }
+            if ( db.getValue().getAlertLevel() < db.getValue().getPreOpenedConnections() ) {
+                log.error( db.getKey() + ": alert level < pre opened connections, this is stupid!" );
+            }
+            LinkedList<Connection> connections = new LinkedList<Connection>();
+            available_connections.put( db.getKey(), connections );
+            for ( int i = 0; i < db.getValue().getPreOpenedConnections(); i++ ) {
+                connections.add( db.getValue().createConnection() );
+            }
+            busy_connections.put( db.getKey(), new ConcurrentHashMap<Object, BusyConnection>() );
+            if ( db.getValue().getMaxConnections() < 1 ) {
+                log.error( db.getKey() + ": max connections cannot be < 1" );
+            }
+        }
+        supplementary_thread_busy_connection_users = new HashMap<Connection, Set<Thread>>();
+        supported_external_threads = new HashSet<String>();
+    }
+
+    /**
+     * Add an external thread name that requires a DB connection.<br/>
+     * All connections from this thread MUST be manually released after use.
+     * @param threadName
+     */
+    public static void addSupportedExternalThread( String threadName ) {
+        if ( threadName.equals( "" ) ) {
+            return;
+        }
+        supported_external_threads.add( threadName );
+    }
+    
+    /** Check if the given thread name is a supported thread creating from an external library. */
+    public static boolean isSupportedExternalThread( String threadName ) {
+        for ( String name : supported_external_threads ) {
+            if ( threadName.startsWith( name ) ) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    public static Map<String, ConnectionParameters> getParameters() {
+        return dbParameters;
+    }
+    
+    public static List<String> listBusyConnections() {
+        List<String> retval = new ArrayList<String>();
+        List<String> kinds = new ArrayList<String>( dbParameters.keySet() );
+        Collections.sort( kinds );
+        for ( String dbkind : kinds ) {
+            int count = 0;
+            List<String> kind = new ArrayList<String>();
+            for ( Map.Entry<Object, BusyConnection> busy : busy_connections.get( dbkind ).entrySet() ) {
+                if ( busy.getKey() instanceof Thread ) {
+                    // e.g. used by a request processing thread
+                    kind.add( ( (Thread) busy.getKey() ).getName() + busy.getValue() );
+                } else {
+                    // e.g. used by supplementary thread(s)
+                    kind.add( ( (ThreadGroup) busy.getKey() ).getName() + busy.getValue() );
+                }
+                count++;
+            }
+            if ( count == 0 ) {
+                retval.add( dbkind + ": 0" );
+            } else {
+                retval.add( dbkind + ": " + count + " (" + DBUtils.join( ", ", kind ) + ")" );
+            }
+        }
+        return retval;
+    }
+    
+    /**
+     * Get a database connection to use. Normally never called directly, only by auto-generated database access
+     * code.
+     * 
+     * - in case the calling Thread is a request processing Thread:
+     * 
+     *   This connection will be associated to the calling Thread and reserved for it (one connection dedicated to
+     *   one processing thread model).
+     * 
+     * - in case the calling Thread is a supplementary Thread(1)
+     * 
+     *   This connection can be reserved or shared with other supplementary Threads depending on configuration
+     *   (<code>connectionGroup</code> for asynchronous events, dedicated instance properties for other threads).
+     *   
+     * (1) a supplementary Thread, e.g. not a request processing Thread, is thus:
+     *     - any Thread invoking asynchronous events (such as Spooler, ElectionsStartStop, etc)
+     *     - the Spooler sub threads used for MT retries
+     *     - any {@link ThreadedElementsConsumer} uses, e.g. DB loggers, DLR processors, Notification Sender
+     *     - the MessageSender threads
+     */ 
+    public static Connection getConnection( String dbkind ) {
+        // for request processing threads, we have one connection per thread; for supplementary threads, we
+        // have one connection per thread group; to optimize request processing, we first try to look if we
+        // have a busy thread associated with this thread, even if it's possibly not a request processing thread
+        ConcurrentMap<Object, BusyConnection> connections = busy_connections.get( dbkind );
+        if ( connections == null ) {
+            log.error( "DB kind '" + dbkind + "' doesn't exist" );
+            return null;
+        }
+        BusyConnection conn = connections.get( Thread.currentThread() );  // ConcurrentMap
+        if ( conn != null ) {
+            if ( log.isTraceEnabled() ) {
+                log.trace( dbkind + ": use busy connection - " + conn );
+            }
+            conn.updateRequester();
+            return conn.conn;
+        }
+        
+        // supplementary threads are recognized by using a specific parent's thread group
+        if ( Thread.currentThread().getThreadGroup().getParent() == SupplementaryThreadHelper.parentThreadGroup ) {
+            return getConnectionForSupplementaryThread( dbkind );
+            
+        } else {
+            // sanity
+            if ( ! Thread.currentThread().getName().startsWith( "http" )
+                && ! Thread.currentThread().getName().equals( "main" ) ) {
+                if ( isSupportedExternalThread( Thread.currentThread().getName() ) ) {
+                    log.debug( "Thread identified as being externally created (external library): the DB " +
+                               "connections allocated to non-request processing threads MUST be " +
+                               "manually released after use" );
+                } else {
+                    log.error( "Thread identified as request processing thread (parent of thread group is not "
+                               + "SupplementaryThreadHelper.parentThreadGroup), but name doesn't start with http!"
+                               + " (thread name: " + Thread.currentThread().getName() + ")" );
+                }
+            }
+            return findOrCreateAvailableConnection( dbkind, Thread.currentThread() );
+        }
+
+    }
+
+    public static void supplementaryThreadReleaseConnections() {
+        if ( dbParameters == null ) {
+            log.trace( "Do nothing (hook for combining old style and new style DB accesses in acme)" );
+            return;
+        }
+        log.trace( "Enter for group " + Thread.currentThread().getThreadGroup().getName() + ", thread "
+                   + Thread.currentThread().getName() );
+        for ( String dbkind : dbParameters.keySet() ) {
+            // There are subtle interleaved synchronizations happening here.
+            // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+            synchronized( Thread.currentThread().getThreadGroup() ) {
+                BusyConnection conn = busy_connections.get( dbkind ).get( Thread.currentThread().getThreadGroup() );
+                if ( conn != null ) {
+                    // at least one of the threads in this thread group use this connection, either that thread, 
+                    // others, or that thread and others; first, check if that thread is really using it:
+                    // There are subtle interleaved synchronizations happening here.
+                    // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+                    synchronized( supplementary_thread_busy_connection_users ) {
+                        Set<Thread> users = supplementary_thread_busy_connection_users.get( conn.conn );
+                        if ( log.isTraceEnabled() ) {
+                            List<String> us = new ArrayList<String>();
+                            for ( Thread u : users ) {
+                                us.add( u.getName() );
+                            }
+                        }
+                        if ( users.remove( Thread.currentThread() ) ) {
+                            // this thread was using this connection. now let's see if no other threads are.
+                            if ( users.size() == 0 ) {
+                                if ( log.isTraceEnabled() ) {
+                                    log.trace( "Group " + Thread.currentThread().getThreadGroup().getName()
+                                               + ", thread " + Thread.currentThread().getName()
+                                               + ", release connection " + conn + " on no more users" );
+                                }
+                                releaseConnection( dbkind, Thread.currentThread().getThreadGroup() );
+                                supplementary_thread_busy_connection_users.remove( conn.conn );
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    private static void releaseConnection( String dbkind, Object key ) {
+        LinkedList<Connection> available = available_connections.get( dbkind );
+        Map<Object, BusyConnection> busy = busy_connections.get( dbkind );
+        ConnectionParameters dbParams = dbParameters.get( dbkind );
+
+        BusyConnection conn = busy.get( key );  // ConcurrentMap
+        if ( conn == null ) {
+            return;
+        }
+        
+        // There are subtle interleaved synchronizations happening here.
+        // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+        synchronized( available ) {
+            BusyConnection conn2 = busy.remove( key );
+            // sanity
+            if ( conn2 == null || ! conn.conn.equals( conn2.conn ) ) {
+                log.error( "Internal error, removed connection " + conn2 + " not equals to getted connection "
+                           + conn );
+                return;
+            }
+
+            if ( busy.size() + available.size() >= dbParams.getPreOpenedConnections() ) {
+                if ( log.isTraceEnabled() ) {
+                    log.trace( "closing connection for " + dbkind + " - " + conn + " - "
+                               + " busy:" + busy.size() + " available:" + available.size() + " busies:\n"
+                               + DBUtils.join( "\n", listBusyConnections() ) );
+                } else if ( log.isDebugEnabled() ) {
+                    log.debug( "closing connection for " + dbkind + " - " + conn + " - "
+                               + " busy:" + busy.size() + " available:" + available.size() );
+                }
+                try {
+                    conn.conn.close();
+                } catch ( SQLException se ) {
+                    log.warn( "error closing connection for dbkind=" + dbkind + ": " + se );
+                }
+
+            } else {
+                available.addFirst( conn.conn );
+            }
+
+            available.notify();
+        }
+    }
+            
+    /** Close all of the database connections. */
+    public static void closeConnections() {
+        log.info( "START - closing connections" );
+        if ( available_connections.size() == 0 ) {
+            log.error( "No available connections - already closed?" );
+            return;
+        }
+        for ( String dbkind : dbParameters.keySet() ) {
+            // There are subtle interleaved synchronizations happening here.
+            // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+            synchronized( available_connections.get( dbkind ) ) {
+                for ( Connection conn : available_connections.get( dbkind ) ) {
+                    try {
+                        conn.close();
+                    } catch ( SQLException se ) {
+                        log.warn( "error closing connection for dbkind=" + dbkind + ": " + se );
+                    }
+                }
+                for ( Map.Entry<Object, BusyConnection> busy : busy_connections.get( dbkind ).entrySet() ) {
+                    log.warn( "Still busy connection (leak?): "
+                              + ( ( busy.getKey() instanceof Thread ) ? ( (Thread) busy.getKey() ).getName()
+                                                                      : ( (ThreadGroup) busy.getKey() ).getName() )
+                              + " - " + busy.getValue() );
+                }
+            }
+        }
+        available_connections.clear();
+        log.info( "END - closing connections" );
+    }
+    
+    private static Connection getConnectionForSupplementaryThread( String dbkind ) {
+        
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        
+        // synchronize on group, because more than one thread may be requesting a connection at the same time,
+        // and we want no race condition to potentially populate busy_connections in that situation
+        // There are subtle interleaved synchronizations happening here.
+        // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+        synchronized( group ) {
+            // re-run the check for existing busy connections, on the proper key now
+            BusyConnection conn = busy_connections.get( dbkind ).get( group );
+            if ( conn != null ) {
+                if ( log.isTraceEnabled() ) {
+                    log.trace( dbkind + ": use busy connection for " + group.getName() + " - " + conn );
+                }
+                // remember this thread is also using this connection
+                // There are subtle interleaved synchronizations happening here.
+                // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+                synchronized( supplementary_thread_busy_connection_users ) {
+                    supplementary_thread_busy_connection_users.get( conn.conn ).add( Thread.currentThread() );
+                }
+                conn.updateRequester();
+                return conn.conn;
+            }
+
+            Connection c = findOrCreateAvailableConnection( dbkind, group );
+            // that thread is the first user of this connection
+            Set<Thread> users = new HashSet<Thread>();
+            users.add( Thread.currentThread() );
+            // There are subtle interleaved synchronizations happening here.
+            // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+            synchronized( supplementary_thread_busy_connection_users ) {
+                Set<Thread> previous_users = supplementary_thread_busy_connection_users.put( c, users );
+                // sanity
+                if ( previous_users != null ) {
+                    List<String> us = new ArrayList<String>();
+                    for ( Thread u : previous_users ) {
+                        us.add( u.getName() );
+                    }
+                    log.error( "Internal error (previous users should be empty): previous users: "
+                               + previous_users );
+                }
+            }
+            return c;
+        }
+    }
+
+    /**
+     * Find a currently available connection, or create a new one if none is available and we have
+     * not yet reached the maximum.
+     */
+    private static Connection findOrCreateAvailableConnection( String dbkind, Object busyKey ) {
+        
+        // find an available connection
+        LinkedList<Connection> available = available_connections.get( dbkind );
+        Map<Object, BusyConnection> busy = busy_connections.get( dbkind );
+        ConnectionParameters dbParams = dbParameters.get( dbkind );
+        // There are subtle interleaved synchronizations happening here.
+        // Use a traffic load generator with low preOpenedConnections for all modifications in here!
+        synchronized( available ) {
+            Connection conn = available.poll();
+            if ( conn == null ) {
+                if ( busy.size() < dbParams.getMaxConnections() ) {
+                    conn = dbParams.createConnection();
+                    if ( conn == null ) {
+                        throw new RuntimeException( "Connection creation not available" );
+                    }
+                    log.debug( "created connection " + conn + ", since busy size is " + busy.size() );
+                }
+            } else {
+                // TODO: check if the connection is still alive?
+                if ( log.isTraceEnabled() ) {
+                    if ( busyKey instanceof ThreadGroup ) {
+                        log.trace( dbkind + ": use available connection for "
+                                   + ( (ThreadGroup) busyKey ).getName() + " - " + conn );
+                    } else {
+                        log.trace( dbkind + ": use available connection - " + conn );
+                    }
+                }
+            }
+            if ( conn != null ) {
+                busy.put( busyKey, new BusyConnection( conn ) );
+                if ( busy.size() + available.size() >= dbParams.getAlertLevel() ) {
+                    log.warn( "Many connections for dbkind=" + dbkind + ": busy=" + busy.size()
+                              + " max=" + dbParams.getMaxConnections() + ": "
+                              + DBUtils.join( ", ", listBusyConnections() ) );
+                }
+                return conn;
+
+            } else {
+                // TODO: switch to log.warn when we are reasonably confident with
+                log.error( "No available connections for dbkind=" + dbkind + ": busy=" + busy.size()
+                          + " max=" + dbParams.getMaxConnections() + " busies=["
+                          + DBUtils.join( ", ", listBusyConnections() ) + "], waiting...\n"
+                          + DBUtils.backtrace() );
+                try {
+                    available.wait(); 
+                } catch ( InterruptedException ex ) {}
+                log.error( "Wait over" );
+                return findOrCreateAvailableConnection( dbkind, busyKey );
+            }
+        }
+    }
+    
+    public static void releaseConnectionsForProcessingThread() {
+        for ( String dbkind : dbParameters.keySet() ) {
+            releaseConnection( dbkind, Thread.currentThread() );
+        }
+    }
+    
+}
diff --git a/src/java/org/gc/sdbl4j/DBSelectHelpers.java b/src/java/org/gc/sdbl4j/DBSelectHelpers.java
new file mode 100644 (file)
index 0000000..db26ead
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import java.math.BigDecimal;
+import java.sql.ResultSet;
+import java.sql.Date;
+import java.sql.Timestamp;
+import java.sql.Array;
+
+import org.apache.log4j.Logger;
+
+public class DBSelectHelpers {
+
+    private static Logger log = Logger.getLogger( DBSelectHelpers.class ); 
+
+    /**
+     * Get a string value for the given column from the ResultSet.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static String getString( String column, ResultSet rs ) {
+       try {
+           return rs.getString( column );
+       } catch ( Exception e ) {
+           log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+           return null;
+       }
+    }
+
+    /**
+     * Get an integer value for the given column from the ResultSet, properly set as null if needed.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static Integer getInteger( String column, ResultSet rs ) {
+        try {
+            int ret = rs.getInt( column );
+            if ( rs.wasNull() ) {
+                return null;
+            } else {
+                return ret;
+            }
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+        }
+    }
+    
+    /**
+     * Get a long value for the given column from the ResultSet, properly set as null if needed.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static Long getLong( String column, ResultSet rs ) {
+        try {
+            long ret = rs.getLong( column );
+            if ( rs.wasNull() ) {
+                return null;
+            } else {
+                return ret;
+            }
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+        }
+    }
+
+    /**
+     * Get a double value for the given column from the ResultSet, properly set as null if needed.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static Double getDouble( String column, ResultSet rs ) {
+        try {
+            double ret = rs.getDouble( column );
+            if ( rs.wasNull() ) {
+                return null;
+            } else {
+                return ret;
+            }
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+        }
+    }
+
+    /**
+     * Get a boolean value for the given column from the ResultSet, properly set as null if needed.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static Boolean getBoolean( String column, ResultSet rs ) {
+       try {
+           boolean ret = rs.getBoolean( column );
+            if ( rs.wasNull() ) {
+                return null;
+            } else {
+                return ret;
+            }
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return false;
+       }
+    }
+    
+    /**
+     * Get a timestamp value for the given column from the ResultSet.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static Timestamp getTimestamp( String column, ResultSet rs ) {
+       try {
+           return rs.getTimestamp( column );
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+       }
+    }
+
+    /**
+     * Get a date value for the given column from the ResultSet.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static Date getDate( String column, ResultSet rs ) {
+       try {
+           return rs.getDate( column );
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+       }
+    }
+
+    /**
+     * Get a bytea (byte[]) value for the given column from the ResultSet.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static byte[] getBytes( String column, ResultSet rs ) {
+       try {
+           return rs.getBytes( column );
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+        }
+    }
+    
+    /**
+     * Get an Array value for the given column from the ResultSet.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static Array getArray( String column, ResultSet rs ) {
+        try {
+            return rs.getArray( column );
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+        }
+    }
+
+    /**
+     * Get a java object array value for the given column from the ResultSet.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static Object[] getArrayAsObjectArray( String column, ResultSet rs ) {
+        try {
+            Array result = getArray( column, rs );
+            if ( result == null ) {
+                return new Object[] {};
+            } else {
+                return (Object[]) result.getArray();
+            }
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+        }
+    }
+    
+    /**
+     * Get a java object array value for the given column from the ResultSet.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static String[] getArrayAsStringArray( String column, ResultSet rs ) {
+        try {
+            Array result = getArray( column, rs );
+            if ( result == null ) {
+                return new String[] {};
+            } else {
+                return (String[]) result.getArray();
+            }
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+        }
+    }
+
+    /**
+     * Get a big decimal value for the given column from the ResultSet.
+     * <p>
+     * This method assumes that next() has already been called on the given 
+     * ResultSet and does <strong>not</strong> call next() after retrieving
+     * the requested value.
+     */
+    public static BigDecimal getBigDecimal( String column, ResultSet rs ) {
+        try {
+            return rs.getBigDecimal( column );
+        } catch ( Exception e ) {
+            log.warn( "Error getting column: " + column + "\n" + DBUtils.prettyPrint( e ) );
+            return null;
+        }
+    }
+    
+}
diff --git a/src/java/org/gc/sdbl4j/DBSelectMultiService.java b/src/java/org/gc/sdbl4j/DBSelectMultiService.java
new file mode 100644 (file)
index 0000000..7f6e301
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class DBSelectMultiService<T extends Object> extends DBSelectService {
+
+    protected List<T> rows = new ArrayList<T>(); 
+    protected T currentRow = null;
+    private int index = -1;
+    
+    /**
+     * Retrieves the current row number. The first row is number 1, the second number 2, and so on. 
+     * @return the current row number; 0 if there is no current row.
+     */
+    public int getRow() {
+        if ( index == -1 || index == rows.size() ) {
+            return 0;
+        } else {
+            return index + 1;
+        }
+    }
+    
+    private void setCurrentRow() {
+        if ( index >= 0 && index < rows.size() ) {
+            currentRow = rows.get( index );
+        } else {
+            currentRow = null;
+        }
+    }
+    
+    /** Retrieves the total number of rows. */
+    public int getRows() {
+        return rows.size();
+    }
+    
+    /**
+     * Go to the next row.
+     * @return true if there is a new row to read from, false otherwise.
+     */
+    public boolean next() {
+        if ( index < rows.size() ) {
+            index++;
+        }
+        setCurrentRow();
+        return index < rows.size();
+    }
+    
+    /**
+     * Go to the previous row.
+     * @return true if we are not off the result set
+     */
+    public boolean previous() {
+        if ( index >= 0 ) {
+            index--;
+        }
+        setCurrentRow();
+        return index >= 0;
+    }
+    
+    /** Returns true if there are no rows (the word "empty" is reserved in JSP2.0 EL). */
+    public boolean isVoid() {
+        return rows.size() == 0;
+    }
+
+    /**
+     * Move to the first row.
+     * @return <code>true</code> if the cursor is on a valid row;
+     * <code>false</code> if there are no rows in the ResultSet.
+     */
+    public boolean first() {
+        if ( rows.size() == 0 ) {
+            return false;
+        } else {
+            index = 0;
+            setCurrentRow();
+            return true;
+        }
+    }
+
+    /**
+     * Move to the last row.
+     * @return <code>true</code> if the cursor is on a valid row;
+     * <code>false</code> if there are no rows in the ResultSet.
+     */
+    public boolean last() {
+       if ( rows.size() == 0 ) {
+           return false;
+       } else {
+           index = rows.size() - 1;
+            setCurrentRow();
+           return true;
+       }
+    }
+
+    /** Move to before the first row. */
+    public void beforeFirst() {
+        index = -1;
+        currentRow = null;
+    }
+
+    /** Move to after the last row. */
+    public void afterLast() {
+        index = rows.size();
+        currentRow = null;
+    }
+
+}
diff --git a/src/java/org/gc/sdbl4j/DBSelectService.java b/src/java/org/gc/sdbl4j/DBSelectService.java
new file mode 100644 (file)
index 0000000..a6a18e2
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.apache.log4j.Logger;
+
+public abstract class DBSelectService {
+
+    private static Logger log = Logger.getLogger( DBSelectService.class ); 
+
+    protected static ResultSet executeQuery( PreparedStatement ps )
+            throws SQLException {
+        // synchronize for autocommit and rollback stuff of batched update running on the same connection
+        synchronized ( ps.getConnection() ) {
+            if ( log.isDebugEnabled() ) {
+                long begin = System.currentTimeMillis();
+                ResultSet rs = ps.executeQuery();
+                log.debug( ( System.currentTimeMillis() - begin ) + " ms for: " + ps.toString().replaceAll( "\\s+", " " )
+                           + "[" + DBUtils.getStackTrace()[2].getClassName() + "]" ); 
+
+                return rs;
+            } else {
+                return ps.executeQuery();
+            }
+        }
+    }
+    
+}
diff --git a/src/java/org/gc/sdbl4j/DBUpdateService.java b/src/java/org/gc/sdbl4j/DBUpdateService.java
new file mode 100644 (file)
index 0000000..618cc80
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import org.apache.log4j.Logger;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+public abstract class DBUpdateService {
+
+    private static Logger log = Logger.getLogger( DBUpdateService.class ); 
+
+    protected static int executeUpdate( PreparedStatement ps )
+            throws SQLException {
+        // synchronize for autocommit and rollback stuff of batched update running on the same connection
+        synchronized ( ps.getConnection() ) {
+            if ( log.isDebugEnabled() ) {
+                long begin = System.currentTimeMillis();
+                int result = ps.executeUpdate();
+                log.debug( ( System.currentTimeMillis() - begin ) + " ms for: " + ps.toString().replaceAll( "\\s+", " " )
+                           + "[" + DBUtils.getStackTrace()[2].getClassName() + "]" ); 
+                return result;
+            } else {
+                return ps.executeUpdate();
+            }
+        }
+    }
+        
+    
+}
diff --git a/src/java/org/gc/sdbl4j/DBUtils.java b/src/java/org/gc/sdbl4j/DBUtils.java
new file mode 100644 (file)
index 0000000..2c43288
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import javax.servlet.ServletException;
+
+/**
+ * Generic utils.
+ * 
+ * These generic utils are not tied to DB stuff. They should probably go into a separate JAR of utils but it is
+ * so small that it probably doesn't deserve it.
+ */
+public class DBUtils {
+
+    /** Go up the chain of exception causes, and print a nice trace of that all. */ 
+    public static String prettyPrint( Exception e ) { 
+        StringBuilder out = new StringBuilder();
+        String causePrefix = "";
+        Throwable t = e;
+        while ( t != null ) {
+            out.append( causePrefix )
+               .append( "exception: " )
+               .append( t )
+               .append( " at: " );
+            if ( t.getCause() == null ) {
+                // no more cause, print full backtrace now because that's the most interesting exception
+                out.append( "\n" )
+                   .append( backtrace( t.getStackTrace() ) )
+                   .append( "\n" );
+            } else {
+                // there's a cause, print only one line of trace because that is not the most interesting exception
+                out.append( t.getStackTrace()[ 0 ] )
+                   .append( "\n" );                    
+            }
+            t = t.getCause();
+            // grow the cause prefix 
+            causePrefix += "...cause: ";
+        }
+        if ( e instanceof ServletException ) {
+            // in case of servlet exception, normally the root cause is an interesting exception
+            Throwable rc = ( (ServletException) e ).getRootCause();
+            if ( rc != null ) {
+                out.append( "rootCause: " )
+                   .append( rc )
+                   .append( " at:\n" )
+                   .append( backtrace( rc.getStackTrace() ) )
+                   .append( "\n" );
+            }
+        }
+        return out.toString();
+    }
+    
+    public static String backtrace( StackTraceElement[] trace ) {
+        StringBuilder out = new StringBuilder();
+        for ( StackTraceElement ste : trace ) {
+            out.append( "\t" ).append( ste ).append( "\n" );
+        }
+        return out.toString();
+    }
+
+    /**
+     * Returns the backtrace corresponding to the StackTraceElement's passed.
+     * This is useful to get the backtrace from a catch block.
+     */
+    public static String backtrace( StackTraceElement[] trace, int from ) {
+        StringBuilder sb = new StringBuilder();
+        for ( int i = 2; i < trace.length && trace[i].toString().startsWith( "org" ); i++ ) {
+            sb.append( "\t" ).append( trace[ i ].toString() ).append( "\n" );
+        }
+        return sb.toString();
+    }
+    
+    /**
+     * Returns the backtrace of the caller method, skipping n levels.
+     */
+    public static String backtrace( int skip ) {
+        return backtrace( getStackTrace(), 2 + skip );
+    }
+
+    public static StackTraceElement[] getStackTrace() {
+        try {
+            throw new Exception();
+        } catch ( Exception e ) { 
+            return e.getStackTrace();
+        }
+    }
+
+    public static String backtrace() {
+        return backtrace( getStackTrace() );
+    }
+
+    public static String join( String separator, Iterable<?> elements ) {
+
+        StringBuilder sb = null;
+        for ( Object elem : elements ) {
+            if ( sb == null ) {
+                sb = new StringBuilder();
+                sb.append( elem.toString() );
+            } else {
+                sb.append( separator ).append( elem.toString() );
+            }
+        }
+        return sb == null ? "" : sb.toString();
+    }
+    
+}
diff --git a/src/java/org/gc/sdbl4j/DatabaseConnectionHandlerFilter.java b/src/java/org/gc/sdbl4j/DatabaseConnectionHandlerFilter.java
new file mode 100644 (file)
index 0000000..0630d95
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import java.io.IOException;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.ServletException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Commit or rollback the statements from the thread-specific connection after it's finished.
+ */
+public class DatabaseConnectionHandlerFilter implements Filter {
+
+    private static Logger log; 
+    
+    public void init( FilterConfig config ) {}
+    public static void finishInit() {
+        log = Logger.getLogger( DatabaseConnectionHandlerFilter.class );
+        log.info( "ok" );
+    }
+    public void destroy() {}
+    
+    public void doFilter( ServletRequest req, ServletResponse res, FilterChain chain ) 
+           throws IOException, ServletException {
+        try {
+            chain.doFilter( req, res );
+        } catch ( Exception e ) {
+            log.error( "Uncatched exception\n" + DBUtils.prettyPrint( e ) );
+            throw new ServletException( e );
+        } finally {
+            DBConnectionPool.releaseConnectionsForProcessingThread();
+        }
+    }
+}
diff --git a/src/java/org/gc/sdbl4j/SupplementaryThreadHelper.java b/src/java/org/gc/sdbl4j/SupplementaryThreadHelper.java
new file mode 100644 (file)
index 0000000..55048a8
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ *
+ * Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
+ *
+ * This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
+ *
+ */
+
+package org.gc.sdbl4j;
+
+import java.sql.Connection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SupplementaryThreadHelper {
+
+    /**
+     * The ThreadGroup for all supplementary (non request processing) threads. Used by {@link DBConnectionPool}
+     * to differentiate with request processing threads when giving a {@link Connection} object.
+     */
+    public static ThreadGroup parentThreadGroup = new ThreadGroup( "supplementary threads parent" );
+
+    public static Map<String, ThreadGroup> supplementaryThreadGroups = new HashMap<String, ThreadGroup>();
+     
+    /** Return the {@link ThreadGroup} tied to the given name. */ 
+    public static ThreadGroup getThreadGroup( String name ) {
+        synchronized( supplementaryThreadGroups ) {
+            ThreadGroup tg = supplementaryThreadGroups.get( name );
+            if ( tg != null ) {
+                return tg;
+            }
+            supplementaryThreadGroups.put( name, tg = new ThreadGroup( parentThreadGroup, name ) );
+            return tg;
+        }
+    }
+    
+}