some comment stripping
[sdbl4j] / bin / gen-db-access-class.pl
1 #!/usr/bin/perl
2 #
3 # Copyright (C) 2010 Guillaume Cottenceau and MNC S.A.
4 #
5 # This file is part of sdbl4j, and is licensed under the Apache 2.0 license.
6 #
7
8 use Data::Dumper;
9 use XML::LibXML;
10
11 my $DEBUG = 0;
12
13 for my $input (@ARGV) {
14     $input =~ s|^\./||;
15
16     #- parse input file
17     my $parser = XML::LibXML->new();
18     my $doc = $parser->parse_file($input);
19     
20     sub get_data {
21         my ($doc, $name) = @_;
22         my @retval = ();
23         foreach my $element ($doc->getElementsByTagName($name)) {
24             my $attributes = {};
25             my $retval = { attributes => $attributes };
26             if ($element->getFirstChild) {
27                 $retval->{cdata} = $element->getFirstChild->getData;
28             }
29             foreach my $attr ($element->attributes) {
30                 $attributes->{$attr->name} = $attr->getValue;
31             }
32             push @retval, $retval;
33         }
34         return @retval;
35     }
36     
37     my @sql = get_data($doc, 'sql');
38     my @variation = get_data($doc, 'variation');
39     my @db = get_data($doc, 'db');
40     my @in = get_data($doc, 'in');
41     my @out = get_data($doc, 'out');
42     my @multi = get_data($doc, 'multiRows');
43     my @addableSql = get_data($doc, 'addableSql');
44     my @implements = get_data($doc, 'implements');
45     my @outsugar = get_data($doc, 'outsugar');
46     my @postOrdering = get_data($doc, 'postOrdering');
47     @postOrdering && !@multi and die "$input: <postOrdering> without <multiRows> makes no sense.\n";
48
49     my $logerror = get_data($doc, 'noErrorLog') ? "log.debug" : "log.error";
50     
51     foreach my $out (@out) {
52         my $colname = $out->{attributes}{name};
53         $colname =~ s/([a-z])([A-Z])/${1}_$2/g;
54         $out->{colname} = lc($colname);
55     }
56     
57     if ($DEBUG) {
58         print STDERR "sql: $sql\ndb: $db\n"
59               . Data::Dumper->Dump([\@in], ['in'])
60               . Data::Dumper->Dump([\@out], ['out']);
61     }
62     
63     
64     #- sanity check input
65     if (@sql != 1) {
66         die "$input: there should be one <sql> element\n";
67     }
68     my $sql = $sql[0]{cdata};
69     if ($sql =~ /##VARIATIONS##/ && @variation < 2) {
70         die "$input: at least two <variation> elements make sense when there is ##VARIATION## in SQL\n";
71     }
72     if (@db != 1) {
73         die "$input: there should be one <db> element\n";
74     }
75     my $db = $db[0]{cdata};
76     my $implements = @implements ? $implements[0]{cdata} : '';
77     
78     my $parameters_amount = 0;
79     $parameters_amount++ while $sql =~ /\?/g;
80     
81     foreach my $param (@in, @out) {
82         if (!member($param->{attributes}{type}, qw(Boolean Integer BigDecimal Double Long String Timestamp Date byte[] Array Object[] String[]))) {
83             die "$input: $param->{attributes}{name}: '$param->{attributes}{type}' is not a supported type\n";
84         }
85         if ($param->{attributes}{name} !~ /^[a-zA-Z_0-9]+$/) {
86             die "$input: $param->{attributes}{name}: should be all alphanumerical or underscore\n";
87         }
88         while ($param->{attributes}{parameter} =~ /(\d+)/g) {
89             if ($1 < 1 || $1 > $parameters_amount) {
90                 die "$input: $param->{attributes}{name}: '$1' is not in the parameters amount range (1..$parameters_amount)\n";
91             }
92         }
93         #- make sure we don't accidentally hit a reserved word
94         if (member($param->{attributes}{name}, qw(public protected private static transient volatile class this void))) {
95             $param->{attributes}{varname} = '_' . $param->{attributes}{name};
96         } else {
97             $param->{attributes}{varname} = $param->{attributes}{name};
98         }
99     }
100     
101     #- check there are no unset in parameters
102     if (@in) {
103         foreach my $paramnumber (1 .. $parameters_amount) {
104             (grep { $_->{attributes}{parameter} =~ /\b$paramnumber\b/ } @in) > 0
105               or die "$input: parameter number '$paramnumber' set by no 'in' parameter\n";
106         }
107     }
108     
109     #- output Java class
110     my $kind = @out == 0 ? 'update' : @multi == 0 ? 'uniqueSelect' : 'multiSelect';
111     
112     my $package = $input;
113     $package =~ s|.*\borg\b|org|;
114     $package =~ s|/[^/]+$||;
115     $package =~ s|/|.|g;
116     
117     my $output;
118     
119     $output .= "package $package;
120     
121 import org.apache.log4j.Logger;
122     
123 import java.sql.PreparedStatement;
124 import java.sql.SQLException;
125 import java.sql.ResultSet;
126 ";
127     if ((grep { $_->{attributes}{type} eq 'Timestamp' } @in, @out) > 0) {
128         $output .= "import java.sql.Timestamp;\n";
129     }
130     if ((grep { $_->{attributes}{type} eq 'BigDecimal' } @in, @out) > 0) {
131         $output .= "import java.math.BigDecimal;\n";
132     }
133     if ((grep { $_->{attributes}{type} eq 'Array' } @in, @out) > 0) {
134         $output .= "import java.sql.Array;\n";
135     }
136     if ((grep { $_->{attributes}{type} eq 'Date' } @in, @out) > 0) {
137         $output .= "import java.util.Date;\n";
138     }
139     if (@postOrdering) {
140         $output .= "import java.util.Collections;\n";
141     }
142     $output .= "\nimport org.gc.sdbl4j.DBUtils;\n";
143     if ($kind eq 'update') {
144         $output .= "import org.gc.sdbl4j.BatchHelper;\n";
145         $output .= "import org.gc.sdbl4j.BatchedUpdateHandle;\n";
146     } else {
147         $output .= "import org.gc.sdbl4j.DBSelectHelpers;\n";
148     }
149     my $motherclass = $kind eq 'update' ? 'DBUpdateService'
150               : $kind eq 'uniqueSelect' ? 'DBSelectService'
151                                         : 'DBSelectMultiService';
152     $output .= "import org.gc.sdbl4j.DBConnectionPool;
153 import org.gc.sdbl4j.$motherclass;
154
155 ";
156     
157     (my $classname = basename($input)) =~ s/\..*//;
158
159     $output .= "/*************************************************************************
160 * !!! IMPORTANT !!! DO NOT MODIFY THIS FILE, IT HAS BEEN AUTOGENERATED! *
161 *************************************************************************/
162 public class $classname extends $motherclass";
163     if ($kind eq 'multiSelect') {
164         $output .= "<$classname.RowContainer>";
165     }
166     
167     if ($implements) {
168         $output .= " implements " . trim($implements);
169     }
170     
171     $sql =~ s/\n/" +\n        "/g;
172     
173     $output .= " {
174
175     private static Logger log = Logger.getLogger( $classname.class );
176
177     private static final String sql =
178         \"$sql\";
179
180 ";
181
182     if (@variation) {
183         $output .= "    public enum Variation { " . join(", ", map { $_->{attributes}{name} } @variation) . " };
184
185     private static String getSql( Variation variation ) {\n";
186         foreach my $variation (@variation) {
187             $output .= "        if ( variation == Variation." . $variation->{attributes}{name} . " ) {
188             return sql.replaceFirst( \"##VARIATIONS##\", \"$variation->{cdata}\" );
189         }\n";
190         }
191         $output .= "        log.error( \"unknown variation \" + variation );
192         return null;
193     }\n\n";
194     }
195     
196     if ($kind eq 'update') {
197         $output .= "    /**\n     * Perform the database request, return the number of rows modified or -1 or error.\n";
198     } elsif ($kind eq 'uniqueSelect') {
199         $output .= "    /**\n     * Perform the database request, return a container for the received data, or <code>null</code> if the query
200      * didn't return any rows.\n";
201     } else {
202         $output .= "    /**\n     * Perform the database request, return a container holding all the received rows. You must call 
203      * {\@link #next()} to iterate over the rows.\n";
204     }
205     
206     $output .= "     */\n";
207     
208     if ($kind eq 'update') {
209         $output .= "    public static int perform(";
210     } else {
211         $output .= "    public static $classname perform(";
212     }
213     if (@addableSql) {
214         $output .= " String sqlAddition";
215     }
216     if (@variation) {
217         if (@addableSql) {
218             $output .= ', ';
219         }
220         $output .= " Variation variation";
221     }
222     
223     my $perform_params = join(', ', map { $_->{attributes}{type} . ' ' . $_->{attributes}{varname} } @in);
224     if ($perform_params) {
225         if (@addableSql || @variation) {
226             $output .= ',';
227         }
228         $output .=  " $perform_params ";
229     } else {
230         if (@addableSql || @variation) {
231             $output .= ' ';
232         }
233     }
234     
235     my $get_connection_indent = ' ' x length("            ps = DBConnectionPool.getConnection( \"$db\" ).prepareStatement(");
236     
237     $output .= ') {
238         /*************************************************************************
239          * !!! IMPORTANT !!! DO NOT MODIFY THIS FILE, IT HAS BEEN AUTOGENERATED! *
240          *************************************************************************/
241         PreparedStatement ps = null;
242         try {';
243     my $ps_creator = '';
244     my $sql = @variation ? "getSql( variation )" : "sql";
245     if (@addableSql) {
246         $ps_creator .= "
247             ps = DBConnectionPool.getConnection( \"$db\" ).prepareStatement( $sql + sqlAddition,";
248     } else {
249         $ps_creator .= "
250             ps = DBConnectionPool.getConnection( \"$db\" ).prepareStatement( $sql,";
251     }
252     $ps_creator .= "
253 $get_connection_indent ResultSet.TYPE_SCROLL_INSENSITIVE,
254 $get_connection_indent ResultSet.CONCUR_READ_ONLY );
255
256 ";
257     $output .= $ps_creator;
258     
259     my %simpletypes = (Integer => { sqltype => 'INTEGER', pssetter => 'Int' },
260                        Long => { sqltype => 'BIGINT', pssetter => 'Long' },
261                        Double => { sqltype => 'DOUBLE', pssetter => 'Double' },
262                        Boolean => { sqltype => 'BOOLEAN', pssetter => 'Boolean' });
263
264     my $params = '';
265
266     my %type2getter = ('Boolean' => 'getBoolean', 'Integer' => 'getInteger', 'BigDecimal' => 'getBigDecimal', 'Double' => 'getDouble',
267                        'Long' => 'getLong', 'String' => 'getString', 'Timestamp' => 'getTimestamp',
268                        'Date' => 'getDate', 'byte[]' => 'getBytes', 'Array' => 'getArray', 'Object[]' => 'getArrayAsObjectArray',
269                        'String[]' => 'getArrayAsStringArray' );
270     
271     foreach my $param (@in) {
272         while ($param->{attributes}{parameter} =~ /(\d+)/g) {
273             my $index = $1;
274             if (member($param->{attributes}{type}, keys %simpletypes)) {
275                 $output =~ /java.sql.Types/ or $output =~ s/(import java.sql.ResultSet;)/$1\nimport java.sql.Types;/;
276                 $params .= "            if ( $param->{attributes}{varname} == null ) {\n"
277                          . "                ps.setNull( $index, Types.$simpletypes{$param->{attributes}{type}}{sqltype} );\n"
278                          . "            } else {\n"
279                          . "                ps.set$simpletypes{$param->{attributes}{type}}{pssetter}( $index, $param->{attributes}{varname} );\n"
280                          . "            }\n";
281             } elsif ($param->{attributes}{type} eq 'byte[]') {
282                 $params .= "            ps.setBytes( $index, $param->{attributes}{varname} );\n";
283             } elsif ($param->{attributes}{type} eq 'Date') {
284                 $params .= "            ps.setDate( $index, new java.sql.Date( $param->{attributes}{varname}.getTime() ) );\n";
285             } else {
286                 $params .= "            ps.set$param->{attributes}{type}( $index, $param->{attributes}{varname} );\n";
287             }
288         }
289     }
290
291     $output .= $params;
292     
293     if ($kind eq 'update') {
294         $output .= "            int result = executeUpdate( ps );
295             ps.close();
296             return result;
297         } catch ( SQLException se ) {
298             String thissql = ps == null ? \"\" : ps.toString().replaceAll( \"\\\\s+\", \" \" );
299             $logerror( \"failed to perform SQL request\" + thissql + \"\\n\" + DBUtils.prettyPrint( se ) );
300             return -1;
301         }
302     }
303
304     /**
305      * Get a BatchedUpdateHandle object suitable for then sending updates in batch.
306      */
307     public static BatchedUpdateHandle<$classname> getBatchedUpdateHandle(";
308     if (@addableSql) {
309         $output .= " String sqlAddition ";
310     }
311     $output .= ") {
312         PreparedStatement ps = null;
313         try {";
314     $output .= $ps_creator;
315     $output .= "            return new BatchedUpdateHandle<$classname>( ps );
316         } catch ( SQLException se ) {
317             log.error( \"failed to created batched update handle\\n\" + DBUtils.prettyPrint( se ) );
318             return null;
319         }
320     }
321
322     /**
323      * Add an update for a given BatchedUpdateHandle object.
324      * WARNING: do not let too many updates accumulate before performing the 
325      * batched update, because of memory exhaustion risk.
326      */
327     public static void addBatchedUpdate( BatchedUpdateHandle<$classname> handle";
328     if ($perform_params) {
329         $output .=  ", $perform_params ";
330     } else {
331         $output .= ' ';
332     }
333     $output .= ") {
334         PreparedStatement ps = handle.getPreparedStatement();
335         try {
336 ";
337     $output .= $params;
338     $output .= "            ps.addBatch();
339         } catch ( SQLException se ) {
340             log.error( \"failed to add batched update\\n\" + DBUtils.prettyPrint( se ) );
341         }
342     }
343
344     /**
345      * Perform the updates added to the given BatchedUpdateHandle object.
346      * WARNING: do not let too many updates accumulate before performing the 
347      * batched update, because of memory exhaustion risk.
348      */
349     public static int[] performBatchedUpdate( BatchedUpdateHandle<$classname> handle ) {
350         try {
351             return BatchHelper.performBatchedUpdates( handle.getPreparedStatement() );
352         } catch ( SQLException se ) {
353             log.error( \"failed to perform batched updates\\n\" + DBUtils.prettyPrint( se ) );
354             return null;
355         }
356     }
357
358     private $classname() {}
359
360 }\n";
361     
362     } elsif ($kind eq 'uniqueSelect') {
363         my $indent = ' ' x length($classname);
364         $output .= "            ResultSet rs = executeQuery( ps );
365             if ( rs.next() ) {
366                 $classname ret
367                     = new $classname( " . join(",\n                          $indent  ",
368                                         map { "DBSelectHelpers.$type2getter{$_->{attributes}{type}}( \"$_->{colname}\", rs )" } @out)
369                                         . " );
370                 ps.close();
371                 return ret;
372             } else {
373                 return null;
374             }
375         } catch ( SQLException se ) {
376             String thissql = ps == null ? \"\" : ps.toString().replaceAll( \"\\\\s+\", \" \" );
377             $logerror( \"failed to perform SQL request\" + thissql + \"\\n\" + DBUtils.prettyPrint( se ) );
378             return null;
379         }
380     }\n\n";
381     
382         foreach my $param (@out) {
383             $output .= "    private $param->{attributes}{type} $param->{attributes}{varname};\n";
384         }
385     
386         $output .= "
387     private $classname( " . join(', ', map { $_->{attributes}{type} . ' ' . $_->{attributes}{varname} } @out) . " ) {\n";
388         foreach my $param (@out) {
389             $output .= "        this.$param->{attributes}{varname} = $param->{attributes}{varname};\n";
390         }
391         $output .= "    }\n\n";
392         foreach my $param (@out) {
393             my $methname = $param->{attributes}{name};
394             $methname =~ s/(.)/uc($1)/e;
395             $methname =~ s/_(.)/uc($1)/ge;
396             $output .= "    public $param->{attributes}{type} get$methname() {
397         return $param->{attributes}{varname};
398     }\n";
399         }
400         $output .= "\n";
401         foreach my $param (@outsugar) {
402             my $methname = $param->{attributes}{name};
403             $methname =~ s/(.)/uc($1)/e;
404             $methname =~ s/_(.)/uc($1)/ge;
405             $output .= "    public $param->{attributes}{type} get$methname" . ($methname =~ /\(/ ? '' : '()') . " {\n";
406             if ($param->{attributes}{code} =~ /return/) {
407                 $output .= $param->{attributes}{code};
408             } else {
409                 $output .= "        return $param->{attributes}{code};\n";
410             }
411             $output .= "    }\n";
412         }
413         $output .= "}\n";
414     
415     } else {
416         $output .= "            ResultSet rs = executeQuery( ps );
417             $classname ret = new $classname();
418             while ( rs.next() ) {
419                 ret.add( " . join(",\n                         ",
420                                   map { "DBSelectHelpers.$type2getter{$_->{attributes}{type}}( \"$_->{colname}\", rs )" } @out)
421                            . " );
422             }
423             ps.close();";
424         if (@postOrdering) {
425             $output .= "
426             Collections.sort( ret.rows );";
427         } 
428             $output .= "
429             return ret;
430         } catch ( SQLException se ) {
431             String thissql = ps == null ? \"\" : ps.toString().replaceAll( \"\\\\s+\", \" \" );
432             $logerror( \"failed to perform SQL request\" + thissql + \"\\n\" + DBUtils.prettyPrint( se ) );
433             return null;
434         }
435     }\n";
436         if (@postOrdering) {
437             $output .= "
438     protected class RowContainer implements Comparable<RowContainer> {\n";
439         } else {
440             $output .= "
441     protected class RowContainer {\n";
442         }
443         foreach my $param (@out) {
444             $output .= "        public $param->{attributes}{type} $param->{attributes}{varname};\n";
445         }
446         $output .= "        public RowContainer( " . join(', ', map { "$_->{attributes}{type} $_->{attributes}{varname}" } @out) . " ) {\n";
447         foreach my $param (@out) {
448             $output .= "            this.$param->{attributes}{varname} = $param->{attributes}{varname};\n";
449         }
450         $output .= "        }\n";
451         if (@postOrdering) {
452             $output .= "
453         public int compareTo( RowContainer o ) {\n";
454             if ($postOrdering[0]{attributes}{code} =~ /\breturn\b/) {
455                 $output .= "            $postOrdering[0]{attributes}{code}\n";
456             } else {
457                 $output .= "            return " . $postOrdering[0]{attributes}{code} . ";\n";
458             }
459             $output .= "
460         }";
461         }
462         $output .= "
463     }
464
465     private $classname() {}\n\n";
466     
467         $output .= "    private void add( " . join(', ', map { $_->{attributes}{type} . ' ' . $_->{attributes}{varname} } @out) . " ) {
468         rows.add( new RowContainer( " . join(', ', map { $_->{attributes}{varname} } @out) . " ) );
469     }\n\n";
470     
471         foreach my $param (@out) {
472             my $methname = $param->{attributes}{name};
473             $methname =~ s/(.)/uc($1)/e;
474             $methname =~ s/_(.)/uc($1)/ge;
475             $output .= "    public $param->{attributes}{type} get$methname() {
476         if ( currentRow == null ) {
477             log.error( \"No current row! #next wasn't called? row number = \" + getRow() + \"; rows = \" + getRows() + \"\\n\"
478                        + DBUtils.backtrace() );
479             return null;
480         }
481         return currentRow.$param->{attributes}{varname};
482     }\n";
483         }
484     
485         foreach my $param (@outsugar) {
486             my $methname = $param->{attributes}{name};
487             $methname =~ s/(.)/uc($1)/e;
488             $methname =~ s/_(.)/uc($1)/ge;
489             $output .= "    public $param->{attributes}{type} get$methname " . ($methname =~ /\(/ ? '' : '()') . " {
490         if ( currentRow == null ) {
491             log.error( \"No current row! #next wasn't called? row number = \" + getRow() + \"; rows = \" + getRows() + \"\\n\"
492                        + DBUtils.backtrace() );
493             return null;
494         }\n";
495             if ($param->{attributes}{code} =~ /return/) {
496                 $output .= $param->{attributes}{code};
497             } else {
498                 $output .= "        return $param->{attributes}{code};\n";
499             }
500             $output .= "\n}\n";
501         }
502         $output .= "}\n";
503     }
504     
505     my $file = $input;
506     $file =~ s/\.db\.xml$/\.java/;
507     output($file, $output);
508 }
509
510
511 sub member { my $e = shift; foreach (@_) { $e eq $_ and return 1 } 0 }
512 sub basename { local $_ = shift; s|/*\s*$||; s|.*/||; $_ }
513 sub output { my $f = shift; open(my $F, ">$f") or die "output in file $f failed: $!\n"; print $F $_ foreach @_; 1 }
514 sub trim { $_[0] =~ s/^\s+//; $_[0] =~ s/\s+$//; return $_[0]; }
515 sub timing {
516     my $dur = gettimeofday() - $time;
517 #    print STDERR $_[0] . " in " . $dur . "\n";
518     $time = gettimeofday();
519 }