001    package org.apache.ojb.broker.util.dbhandling;
002    
003    /* Copyright 2004-2005 The Apache Software Foundation
004     *
005     * Licensed under the Apache License, Version 2.0 (the "License");
006     * you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    import java.io.*;
019    import java.util.HashMap;
020    import java.util.Iterator;
021    import java.util.StringTokenizer;
022    import java.util.zip.GZIPInputStream;
023    import java.util.zip.GZIPOutputStream;
024    
025    import org.apache.ojb.broker.metadata.JdbcConnectionDescriptor;
026    import org.apache.ojb.broker.platforms.PlatformException;
027    import org.apache.ojb.broker.util.logging.LoggerFactory;
028    import org.apache.tools.ant.Project;
029    import org.apache.tools.ant.taskdefs.SQLExec;
030    import org.apache.tools.ant.types.FileSet;
031    import org.apache.torque.task.TorqueDataModelTask;
032    import org.apache.torque.task.TorqueSQLExec;
033    import org.apache.torque.task.TorqueSQLTask;
034    
035    /**
036     * Provides basic database handling (drop, create, init) via torque.
037     * 
038     * @author Thomas Dudziak
039     */
040    public class TorqueDBHandling implements DBHandling
041    {
042        /** Torque db platforms */
043        protected static final String TORQUE_PLATFORM_DB2        = "db2";
044        protected static final String TORQUE_PLATFORM_HYPERSONIC = "hypersonic";
045        protected static final String TORQUE_PLATFORM_INTERBASE  = "interbase";
046        protected static final String TORQUE_PLATFORM_MSSQL      = "mssql";
047        protected static final String TORQUE_PLATFORM_MYSQL      = "mysql";
048        protected static final String TORQUE_PLATFORM_ORACLE     = "oracle";
049        protected static final String TORQUE_PLATFORM_POSTGRESQL = "postgresql";
050        protected static final String TORQUE_PLATFORM_SAPDB      = "sapdb";
051        protected static final String TORQUE_PLATFORM_SYBASE     = "sybase";
052    
053        /** The name of the db-creation script */
054        private static final String CREATION_SCRIPT_NAME = "create-db.sql";
055        /** The name of the torque database mapping file */
056        private static final String SQL_DB_MAP_NAME      = "sqldb.map";
057    
058        /** Mapping from ojb dbms to torque database setting */
059        private static HashMap _dbmsToTorqueDb = new HashMap();
060        
061        static
062        {
063            _dbmsToTorqueDb.put("db2",         TORQUE_PLATFORM_DB2);
064            _dbmsToTorqueDb.put("hsqldb",      TORQUE_PLATFORM_HYPERSONIC);
065            _dbmsToTorqueDb.put("firebird",    TORQUE_PLATFORM_INTERBASE);
066            _dbmsToTorqueDb.put("mssqlserver", TORQUE_PLATFORM_MSSQL);
067            _dbmsToTorqueDb.put("mysql",       TORQUE_PLATFORM_MYSQL);
068            _dbmsToTorqueDb.put("oracle",      TORQUE_PLATFORM_ORACLE);
069            _dbmsToTorqueDb.put("oracle9i",    TORQUE_PLATFORM_ORACLE);
070            _dbmsToTorqueDb.put("postgresql",  TORQUE_PLATFORM_POSTGRESQL);
071            _dbmsToTorqueDb.put("sapdb",       TORQUE_PLATFORM_SAPDB);
072            _dbmsToTorqueDb.put("sybaseasa",   TORQUE_PLATFORM_SYBASE);
073            _dbmsToTorqueDb.put("sybasease",   TORQUE_PLATFORM_SYBASE);
074            _dbmsToTorqueDb.put("sybase",      TORQUE_PLATFORM_SYBASE);
075        }
076        
077        /** The jdbc connection for communicating with the db */
078        private JdbcConnectionDescriptor _jcd;
079        /** The target database */
080        private String _targetDatabase;
081        /** The directory where we work in */
082        private File _workDir;
083        /** The compressed contents of the torque schemata */
084        private HashMap _torqueSchemata = new HashMap();
085        /** The compressed content of the creation script */
086        private byte[] _creationScript;
087        /** The compressed contents of the db initialization scripts */
088        private HashMap _initScripts = new HashMap();
089    
090        /**
091         * Creates a new handling object.
092         */
093        public TorqueDBHandling()
094        {}
095    
096        /**
097         * Sets the jdbc connection to use.
098         * 
099         * @param jcd The connection to use
100         * @throws PlatformException If the target database cannot be handled with torque
101         */
102        public void setConnection(JdbcConnectionDescriptor jcd) throws PlatformException
103        {
104            _jcd = jcd;
105    
106            String targetDatabase = (String)_dbmsToTorqueDb.get(_jcd.getDbms().toLowerCase());
107    
108            if (targetDatabase == null)
109            {
110                throw new PlatformException("Database "+_jcd.getDbms()+" is not supported by torque");
111            }
112            if (!targetDatabase.equals(_targetDatabase))
113            {
114                _targetDatabase = targetDatabase;
115                _creationScript = null;
116                _initScripts.clear();
117            }
118        }
119    
120        /**
121         * Returns the connection descriptor used by this handling object.
122         * 
123         * @return The connection descriptor
124         */
125        public JdbcConnectionDescriptor getConnection()
126        {
127            return _jcd;
128        }
129    
130        /**
131         * Returns the torque database platform used.
132         * 
133         * @return The target db platform
134         */
135        public String getTargetTorquePlatform()
136        {
137            return _targetDatabase;
138        }
139    
140        /**
141         * Adds the input files (in our case torque schema files) to use.
142         * 
143         * @param srcDir          The directory containing the files
144         * @param listOfFilenames The filenames in a comma-separated list
145         */
146        public void addDBDefinitionFiles(String srcDir, String listOfFilenames) throws IOException
147        {
148            StringTokenizer tokenizer = new StringTokenizer(listOfFilenames, ",");
149            File            dir       = new File(srcDir);
150            String          filename;
151            
152            while (tokenizer.hasMoreTokens())
153            {
154                filename = tokenizer.nextToken().trim();
155                if (filename.length() > 0)
156                {    
157                     _torqueSchemata.put("schema"+_torqueSchemata.size()+".xml",
158                                         readTextCompressed(new File(dir, filename)));
159                }
160            }
161        }
162    
163        /**
164         * Adds an input stream of a db definition (in our case of a torque schema file).
165         * 
166         * @param schemaStream The input stream
167         */
168        public void addDBDefinitionFile(InputStream schemaStream) throws IOException
169        {
170            _torqueSchemata.put("schema"+_torqueSchemata.size()+".xml",
171                                readStreamCompressed(schemaStream));
172        }
173    
174        /**
175         * Writes the torque schemata to files in the given directory and returns
176         * a comma-separated list of the filenames.
177         * 
178         * @param dir The directory to write the files to
179         * @return The list of filenames
180         * @throws IOException If an error occurred
181         */
182        private String writeSchemata(File dir) throws IOException
183        {
184            writeCompressedTexts(dir, _torqueSchemata);
185    
186            StringBuffer includes = new StringBuffer();
187    
188            for (Iterator it = _torqueSchemata.keySet().iterator(); it.hasNext();)
189            {
190                includes.append((String)it.next());
191                if (it.hasNext())
192                {
193                    includes.append(",");
194                }
195            }
196            return includes.toString();
197        }
198        
199        /**
200         * Creates the db-creation sql script (but does not perform it).
201         * 
202         * @throws PlatformException If some error occurred
203         */
204        public void createCreationScript() throws PlatformException
205        {
206            Project             project    = new Project();
207            TorqueDataModelTask modelTask  = new TorqueDataModelTask();
208            File                tmpDir     = null;
209            File                scriptFile = null;
210            
211            _creationScript = null;
212            try
213            {
214                tmpDir = new File(getWorkDir(), "schemas");
215                tmpDir.mkdir();
216    
217                String includes = writeSchemata(tmpDir);
218                
219                scriptFile = new File(tmpDir, CREATION_SCRIPT_NAME);
220    
221                project.setBasedir(tmpDir.getAbsolutePath());
222    
223                // populating with defaults
224                modelTask.setProject(project);
225                modelTask.setUseClasspath(true);
226                modelTask.setControlTemplate("sql/db-init/Control.vm");
227                modelTask.setOutputDirectory(tmpDir);
228                modelTask.setOutputFile(CREATION_SCRIPT_NAME);
229                modelTask.setTargetDatabase(_targetDatabase);
230    
231                FileSet files = new FileSet();
232    
233                files.setDir(tmpDir);
234                files.setIncludes(includes);
235                modelTask.addFileset(files);
236                modelTask.execute();
237    
238                _creationScript = readTextCompressed(scriptFile);
239    
240                deleteDir(tmpDir);
241            }
242            catch (Exception ex)
243            {
244                // clean-up
245                if ((tmpDir != null) && tmpDir.exists())
246                {
247                    deleteDir(tmpDir);
248                }
249                throw new PlatformException(ex);
250            }
251        }
252        
253        /**
254         * Creates the database.
255         * 
256         * @throws PlatformException If some error occurred
257         */
258        public void createDB() throws PlatformException
259        {
260            if (_creationScript == null)
261            {
262                createCreationScript();
263            }
264    
265            Project             project    = new Project();
266            TorqueDataModelTask modelTask  = new TorqueDataModelTask();
267            File                tmpDir     = null;
268            File                scriptFile = null;
269            
270            try
271            {
272                tmpDir = new File(getWorkDir(), "schemas");
273                tmpDir.mkdir();
274    
275                scriptFile = new File(tmpDir, CREATION_SCRIPT_NAME);
276    
277                writeCompressedText(scriptFile, _creationScript);
278    
279                project.setBasedir(tmpDir.getAbsolutePath());
280    
281                // we use the ant task 'sql' to perform the creation script
282                    SQLExec         sqlTask = new SQLExec();
283                    SQLExec.OnError onError = new SQLExec.OnError();
284            
285                    onError.setValue("continue");
286                    sqlTask.setProject(project);
287                    sqlTask.setAutocommit(true);
288                    sqlTask.setDriver(_jcd.getDriver());
289                    sqlTask.setOnerror(onError);
290                    sqlTask.setUserid(_jcd.getUserName());
291                    sqlTask.setPassword(_jcd.getPassWord() == null ? "" : _jcd.getPassWord());
292                    sqlTask.setUrl(getDBCreationUrl());
293                    sqlTask.setSrc(scriptFile);
294                    sqlTask.execute();
295    
296                    deleteDir(tmpDir);
297            }
298            catch (Exception ex)
299            {
300                // clean-up
301                if ((tmpDir != null) && tmpDir.exists())
302                {
303                    try
304                    {
305                        scriptFile.delete();
306                    }
307                    catch (NullPointerException e) 
308                    {
309                        LoggerFactory.getLogger(this.getClass()).error("NPE While deleting scriptFile [" + scriptFile.getName() + "]", e);
310                    }
311                }
312                throw new PlatformException(ex);
313            }
314        }
315    
316        /**
317         * Creates the initialization scripts (creation of tables etc.) but does
318         * not perform them.
319         * 
320         * @throws PlatformException If some error occurred
321         */
322        public void createInitScripts() throws PlatformException
323        {
324            Project       project   = new Project();
325            TorqueSQLTask sqlTask   = new TorqueSQLTask(); 
326            File          schemaDir = null;
327            File          sqlDir    = null;
328            
329            _initScripts.clear();
330            try
331            {
332                File tmpDir = getWorkDir();
333    
334                schemaDir = new File(tmpDir, "schemas");
335                sqlDir    = new File(tmpDir, "sql");
336                schemaDir.mkdir();
337                sqlDir.mkdir();
338    
339                String includes     = writeSchemata(schemaDir);
340                File   sqlDbMapFile = new File(sqlDir, SQL_DB_MAP_NAME);
341    
342                sqlDbMapFile.createNewFile();
343                project.setBasedir(sqlDir.getAbsolutePath());
344                
345                // populating with defaults
346                sqlTask.setProject(project);
347                sqlTask.setUseClasspath(true);
348                sqlTask.setBasePathToDbProps("sql/base/");
349                sqlTask.setControlTemplate("sql/base/Control.vm");
350                sqlTask.setOutputDirectory(sqlDir);
351                // we put the report in the parent directory as we don't want
352                // to read it in later on
353                sqlTask.setOutputFile("../report.sql.generation");
354                sqlTask.setSqlDbMap(SQL_DB_MAP_NAME);
355                sqlTask.setTargetDatabase(_targetDatabase);
356    
357                FileSet files = new FileSet();
358                
359                files.setDir(schemaDir);
360                files.setIncludes(includes);
361                sqlTask.addFileset(files);
362                sqlTask.execute();
363    
364                readTextsCompressed(sqlDir, _initScripts);
365                deleteDir(schemaDir);
366                deleteDir(sqlDir);
367            }
368            catch (Exception ex)
369            {
370                // clean-up
371                if ((schemaDir != null) && schemaDir.exists())
372                {
373                    deleteDir(schemaDir);
374                }
375                if ((sqlDir != null) && sqlDir.exists())
376                {
377                    deleteDir(sqlDir);
378                }
379                throw new PlatformException(ex);
380            }
381        }
382    
383        /**
384         * Creates the tables according to the schema files.
385         * 
386         * @throws PlatformException If some error occurred
387         */
388        public void initDB() throws PlatformException
389        {
390            if (_initScripts.isEmpty())
391            {
392                createInitScripts();
393            }
394    
395            Project       project   = new Project();
396            TorqueSQLTask sqlTask   = new TorqueSQLTask(); 
397            File          outputDir = null;
398            
399            try
400            {
401                outputDir = new File(getWorkDir(), "sql");
402    
403                outputDir.mkdir();
404                writeCompressedTexts(outputDir, _initScripts);
405    
406                project.setBasedir(outputDir.getAbsolutePath());
407    
408                // executing the generated sql, but this time with a torque task 
409                TorqueSQLExec         sqlExec = new TorqueSQLExec();
410                TorqueSQLExec.OnError onError = new TorqueSQLExec.OnError();
411    
412                sqlExec.setProject(project);
413                onError.setValue("continue");
414                sqlExec.setAutocommit(true);
415                sqlExec.setDriver(_jcd.getDriver());
416                sqlExec.setOnerror(onError);
417                sqlExec.setUserid(_jcd.getUserName());
418                sqlExec.setPassword(_jcd.getPassWord() == null ? "" : _jcd.getPassWord());
419                sqlExec.setUrl(getDBManipulationUrl());
420                sqlExec.setSrcDir(outputDir.getAbsolutePath());
421                sqlExec.setSqlDbMap(SQL_DB_MAP_NAME);
422                sqlExec.execute();
423                
424                deleteDir(outputDir);
425            }
426            catch (Exception ex)
427            {
428                // clean-up
429                if (outputDir != null)
430                {
431                    deleteDir(outputDir);
432                }
433                throw new PlatformException(ex);
434            }
435        }
436    
437        /**
438         * Template-and-Hook method for generating the url required by the jdbc driver
439         * to allow for creating a database (as opposed to accessing an already-existing
440         * database).
441         *
442         */
443        protected String getDBCreationUrl()
444        {
445            JdbcConnectionDescriptor jcd = getConnection();
446    
447            // currently I only know about specifics for mysql
448            if (TORQUE_PLATFORM_MYSQL.equals(getTargetTorquePlatform()))
449            {
450                // we have to remove the db name as the jdbc driver would try to connect to
451                // a non-existing db
452                // the db-alias has this form: [host&port]/[dbname]?[options]
453                String dbAliasPrefix = jcd.getDbAlias();
454                String dbAliasSuffix = "";
455                int    questionPos   = dbAliasPrefix.indexOf('?');
456    
457                if (questionPos > 0)
458                {
459                    dbAliasSuffix = dbAliasPrefix.substring(questionPos);
460                    dbAliasPrefix = dbAliasPrefix.substring(0, questionPos);
461                }
462    
463                int slashPos = dbAliasPrefix.lastIndexOf('/');
464    
465                if (slashPos > 0)
466                {
467                    // it is important that the slash at the end is present
468                    dbAliasPrefix = dbAliasPrefix.substring(0, slashPos + 1);
469                }
470                return jcd.getProtocol()+":"+jcd.getSubProtocol()+":"+dbAliasPrefix+dbAliasSuffix;
471            }
472            else if (TORQUE_PLATFORM_POSTGRESQL.equals(getTargetTorquePlatform()))
473            {
474                // we have to replace the db name with 'template1'
475                // the db-alias has this form: [host&port]/[dbname]?[options]
476                String dbAliasPrefix = jcd.getDbAlias();
477                String dbAliasSuffix = "";
478                int    questionPos   = dbAliasPrefix.indexOf('?');
479    
480                if (questionPos > 0)
481                {
482                    dbAliasSuffix = dbAliasPrefix.substring(questionPos);
483                    dbAliasPrefix = dbAliasPrefix.substring(0, questionPos);
484                }
485    
486                int slashPos = dbAliasPrefix.lastIndexOf('/');
487    
488                if (slashPos > 0)
489                {
490                    // it is important that the slash at the end is present
491                    dbAliasPrefix = dbAliasPrefix.substring(0, slashPos + 1);
492                }
493                else
494                {
495                    dbAliasPrefix += "/";
496                }
497                dbAliasPrefix += "template1";
498                if (dbAliasSuffix.length() > 0)
499                {
500                    dbAliasPrefix += "/";
501                }
502                return jcd.getProtocol()+":"+jcd.getSubProtocol()+":"+dbAliasPrefix+dbAliasSuffix;
503                
504            }
505            else
506            {
507                return jcd.getProtocol()+":"+jcd.getSubProtocol()+":"+jcd.getDbAlias();
508            }
509        }
510    
511        /**
512         * Template-and-Hook method for generating the url required by the jdbc driver
513         * to allow for modifying an existing database.
514         *
515         */
516        protected String getDBManipulationUrl()
517        {
518            JdbcConnectionDescriptor jcd = getConnection();
519    
520            return jcd.getProtocol()+":"+jcd.getSubProtocol()+":"+jcd.getDbAlias();
521        }
522    
523        /**
524         * Reads the given text file and compressed its content.
525         * 
526         * @param file The file
527         * @return A byte array containing the GZIP-compressed content of the file
528         * @throws IOException If an error ocurred
529         */
530        private byte[] readTextCompressed(File file) throws IOException
531        {
532            return readStreamCompressed(new FileInputStream(file));
533        }
534    
535        /**
536         * Reads the given text stream and compressed its content.
537         * 
538         * @param stream The input stream
539         * @return A byte array containing the GZIP-compressed content of the stream
540         * @throws IOException If an error ocurred
541         */
542        private byte[] readStreamCompressed(InputStream stream) throws IOException
543        {
544            ByteArrayOutputStream bao    = new ByteArrayOutputStream();
545            GZIPOutputStream      gos    = new GZIPOutputStream(bao);
546            OutputStreamWriter    output = new OutputStreamWriter(gos);
547            BufferedReader        input  = new BufferedReader(new InputStreamReader(stream));
548            String                line;
549    
550            while ((line = input.readLine()) != null)
551            {
552                output.write(line);
553                output.write('\n');
554            }
555            input.close();
556            stream.close();
557            output.close();
558            gos.close();
559            bao.close();
560            return bao.toByteArray();
561        }
562    
563        /**
564         * Reads the text files in the given directory and puts their content
565         * in the given map after compressing it. Note that this method does not
566         * traverse recursivly into sub-directories.
567         * 
568         * @param dir     The directory to process
569         * @param results Map that will receive the contents (indexed by the relative filenames)
570         * @throws IOException If an error ocurred
571         */
572        private void readTextsCompressed(File dir, HashMap results) throws IOException
573        {
574            if (dir.exists() && dir.isDirectory())
575            {
576                File[] files = dir.listFiles();
577    
578                for (int idx = 0; idx < files.length; idx++)
579                {
580                    if (files[idx].isDirectory())
581                    {
582                        continue;
583                    }
584                    results.put(files[idx].getName(), readTextCompressed(files[idx]));
585                }
586            }
587        }
588    
589        /**
590         * Uncompresses the given textual content and writes it to the given file.
591         * 
592         * @param file              The file to write to
593         * @param compressedContent The content
594         * @throws IOException If an error occurred
595         */
596        private void writeCompressedText(File file, byte[] compressedContent) throws IOException
597        {
598            ByteArrayInputStream bais   = new ByteArrayInputStream(compressedContent);
599            GZIPInputStream      gis    = new GZIPInputStream(bais);
600            BufferedReader       input  = new BufferedReader(new InputStreamReader(gis));
601            BufferedWriter       output = new BufferedWriter(new FileWriter(file));
602            String               line;
603    
604            while ((line = input.readLine()) != null)
605            {
606                output.write(line);
607                output.write('\n');
608            }
609            input.close();
610            gis.close();
611            bais.close();
612            output.close();
613        }
614        
615        /**
616         * Uncompresses the textual contents in the given map and and writes them to the files
617         * denoted by the keys of the map.
618         * 
619         * @param dir      The base directory into which the files will be written 
620         * @param contents The map containing the contents indexed by the filename
621         * @throws IOException If an error occurred
622         */
623        private void writeCompressedTexts(File dir, HashMap contents) throws IOException
624        {
625            String filename;
626    
627            for (Iterator nameIt = contents.keySet().iterator(); nameIt.hasNext();)
628            {
629                filename = (String)nameIt.next();
630                writeCompressedText(new File(dir, filename), (byte[])contents.get(filename));
631            }
632        }
633        
634        /**
635         * Sets the working directory.
636         * 
637         * @param dir The directory
638         * @throws IOException If the directory does not exist or cannot be written/read
639         */
640        public void setWorkDir(String dir) throws IOException
641        {
642            File workDir = new File(dir);
643    
644            if (!workDir.exists() || !workDir.canWrite() || !workDir.canRead())
645            {
646                throw new IOException("Cannot access directory "+dir);
647            }
648            _workDir = workDir;
649        }
650    
651        /**
652         * Returns the temporary directory used by java.
653         * 
654         * @return The temporary directory
655         * @throws IOException If an io error occurred
656         */
657        private File getWorkDir() throws IOException
658        {
659            if (_workDir == null)
660            {    
661                File   dummy   = File.createTempFile("dummy", ".log");
662                String workDir = dummy.getPath().substring(0, dummy.getPath().lastIndexOf(File.separatorChar));
663        
664                if ((workDir == null) || (workDir.length() == 0))
665                {
666                    workDir = ".";
667                }
668                dummy.delete();
669                _workDir = new File(workDir);
670            }
671            return _workDir;
672        }
673    
674        /**
675         * Little helper function that recursivly deletes a directory.
676         * 
677         * @param dir The directory
678         */
679        private void deleteDir(File dir)
680        {
681            if (dir.exists() && dir.isDirectory())
682            {
683                File[] files = dir.listFiles();
684    
685                for (int idx = 0; idx < files.length; idx++)
686                {
687                    if (!files[idx].exists())
688                    {
689                        continue;
690                    }
691                    if (files[idx].isDirectory())
692                    {
693                        deleteDir(files[idx]);
694                    }
695                    else
696                    {
697                        files[idx].delete();
698                    }
699                }
700                dir.delete();
701            }
702        }
703    }