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 }