001/* 002 * Copyright (c) 2008-2010 The Regents of the University of California. 003 * All rights reserved. 004 * 005 * '$Author: crawl $' 006 * '$Date: 2018-01-26 23:35:42 +0000 (Fri, 26 Jan 2018) $' 007 * '$Revision: 34653 $' 008 * 009 * Permission is hereby granted, without written agreement and without 010 * license or royalty fees, to use, copy, modify, and distribute this 011 * software and its documentation for any purpose, provided that the above 012 * copyright notice and the following two paragraphs appear in all copies 013 * of this software. 014 * 015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 019 * SUCH DAMAGE. 020 * 021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 026 * ENHANCEMENTS, OR MODIFICATIONS. 027 * 028 */ 029 030package org.kepler.util.sql; 031 032import java.io.File; 033import java.io.FileOutputStream; 034import java.io.IOException; 035import java.io.InputStream; 036import java.io.OutputStream; 037import java.net.InetSocketAddress; 038import java.net.Socket; 039import java.sql.Connection; 040import java.sql.DriverManager; 041import java.sql.SQLException; 042import java.sql.Statement; 043import java.util.Collections; 044import java.util.HashMap; 045import java.util.HashSet; 046import java.util.Iterator; 047import java.util.Map; 048import java.util.Properties; 049import java.util.Random; 050import java.util.Set; 051import java.util.UUID; 052import java.util.regex.Matcher; 053import java.util.regex.Pattern; 054 055import org.apache.commons.io.IOUtils; 056import org.apache.commons.logging.Log; 057import org.apache.commons.logging.LogFactory; 058import org.apache.tools.ant.types.FileSet; 059import org.apache.tools.ant.types.Resource; 060import org.apache.tools.ant.types.resources.FileResource; 061import org.hsqldb.Server; 062import org.hsqldb.server.ServerConstants; 063import org.kepler.build.modules.Module; 064import org.kepler.build.modules.ModuleTree; 065import org.kepler.build.project.ProjectLocator; 066import org.kepler.util.StatusNotifier; 067 068import ptolemy.util.FileUtilities; 069import ptolemy.util.MessageHandler; 070 071/** 072 * 073 * An implementation of DatabaseType for HSQL. 074 * 075 * @author Daniel Crawl 076 * @version $Id: HSQL.java 34653 2018-01-26 23:35:42Z crawl $ 077 * 078 */ 079 080public class HSQL extends DatabaseType 081{ 082 083 /** Only this package (DatabaseFactory) can instantiate. */ 084 protected HSQL() 085 { 086 super(); 087 } 088 089 /** Close a JDBC connection. */ 090 @Override 091 public void disconnect() throws SQLException 092 { 093 if(_connection != null) 094 { 095 // NOTE: set the write delay to flush any pending updates to disk. 096 // if this is not done, data could be lost. 097 // see: 098 // http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5410 099 // http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4325 100 // 101 _executeSQL(_SQL_SET_WRITE_DELAY); 102 103 synchronized(_urlMapLock) 104 { 105 String urlStr = _connectionToURLMap.remove(_connection); 106 107 // see if the url was in the map. only connections that access 108 // the db files directly are added to the map. 109 if(urlStr != null) 110 { 111 // it accesses the file directly 112 113 // see what the ref count is 114 Integer count = _urlToCountMap.remove(urlStr); 115 116 if(_isDebugging) 117 { 118 _log.debug("ref count is " + count + " for " + urlStr); 119 } 120 121 if(count.intValue() == 1) 122 { 123 if(_isDebugging) 124 { 125 _log.debug("shutting down for " + urlStr); 126 } 127 _executeSQL("SHUTDOWN"); 128 129 // remove the port file 130 131 // make sure the dbName is set 132 if(_dbName != null) 133 { 134 _deletePortFile(_dbName); 135 } 136 137 } 138 else 139 { 140 // decrement ref count 141 count--; 142 _urlToCountMap.put(urlStr, count); 143 } 144 } 145 } 146 147 if(_isDebugging) 148 { 149 _log.debug("closed connection " + _connection); 150 } 151 } 152 153 super.disconnect(); 154 } 155 156 /** Get the port for a database name. If dbPort is outside the range of 157 * ports previously used for Kepler (9001-9010), then dbPort is returned. 158 * If the port file exists in the database directory, then the port in 159 * this file is returned. Otherwise, this method chooses a random port, 160 * writes it to the port file, and returns the value. The port file is 161 * removed when the server is shut down either in shutdownServers(), or 162 * disconnect(). 163 */ 164 @Override 165 protected synchronized String _getPort(String dbPort, String dbName, String hostName) throws IOException 166 { 167 168 boolean choseRandom = false; 169 170 // make sure the host name is set 171 if (hostName != null && !hostName.trim().isEmpty()) 172 { 173 // check if db port is not null, and in 9001-9010 174 if(dbPort == null || dbPort.trim().isEmpty()) 175 { 176 choseRandom = true; 177 } 178 else 179 { 180 final int port = Integer.valueOf(dbPort).intValue(); 181 if(port >= 9001 && port <= 9010) 182 { 183 choseRandom = true; 184 } 185 } 186 } 187 188 if(choseRandom) { 189 190 dbPort = null; 191 192 // see if port file exists for this database 193 final File portFile = new File(dbName + _PORT_FILE_EXTENSION); 194 if(portFile.exists()) 195 { 196 final Properties properties = HSQLUtils.readPropertiesFile(portFile); 197 dbPort = properties.getProperty(_PORT_FILE_PASSWORD_PROP_NAME); 198 } 199 200 // see if the port file existed and we're already using this port. 201 // NOTE: by keeping a set of ports we've randomly chosen and checking 202 // it here, we can handle the (unlikely) case where the same port is 203 // in more than one port file. 204 if(dbPort != null) 205 { 206 String existingDbName = _serverPortToName.get(dbPort); 207 // see if the port is associated with a database 208 if(existingDbName == null) 209 { 210 // add the mapping 211 _serverPortToName.put(dbPort, dbName); 212 } 213 // see if the port is associated with a different database 214 else if(!dbName.equals(existingDbName)) 215 { 216 // set to null so we chose a different port. 217 dbPort = null; 218 } 219 } 220 221 222 if(dbPort == null) 223 { 224 int port = -1; 225 int tries = 10; 226 227 while(tries > 0) { 228 // pick random port between 10,000 and 60,000 229 port = _random.nextInt(50000) + 10000; 230 231 // see the port is in use 232 final Socket socket = new Socket(); 233 final InetSocketAddress addr = new InetSocketAddress("localhost", port); 234 try 235 { 236 socket.connect(addr); 237 socket.close(); 238 //System.out.println("port " + port + " already in use. tries = " + tries); 239 tries--; 240 } 241 catch(IOException e) 242 { 243 // connection failed 244 //System.out.println("port " + port + " not in use."); 245 break; 246 } 247 } 248 249 if(tries == 0) 250 { 251 throw new IOException("Could not choose random port for HSQL server."); 252 } 253 254 dbPort = String.valueOf(port); 255 256 _serverPortToName.put(dbPort, dbName); 257 258 // write to port file 259 final Properties properties = new Properties(); 260 properties.put(_PORT_FILE_PASSWORD_PROP_NAME, dbPort); 261 _writePropertiesFile(portFile, properties); 262 } 263 } 264 return dbPort; 265 266 } 267 268 269 /** Get the string used in SQL statements to modify the 270 * data type of a column. 271 */ 272 @Override 273 public String getColumnAlterStr() 274 { 275 return "ALTER"; 276 } 277 278 /** Adjust the name of a column. */ 279 @Override 280 public String getColumnName(String columnName) 281 { 282 // user is reserved in HSQL 2.x. 283 if(columnName.equalsIgnoreCase("user")) 284 { 285 return "\"USER\""; 286 } 287 return super.getColumnName(columnName); 288 } 289 290 /** Get a string representing the default time. */ 291 @Override 292 public String getDefaultTimeStr() 293 { 294 return "'1900-01-01 01:00:00'"; 295 } 296 297 /** Get a string representation of the false value for a boolean column. */ 298 @Override 299 public String getFalseValue() 300 { 301 return "FALSE"; 302 } 303 304 /** Get the name of the type. */ 305 @Override 306 public String getName() 307 { 308 return "HSQL"; 309 } 310 311 /** Get the primary file extension of the database. If the database 312 * is not file-based, this returns null. 313 */ 314 @Override 315 public String getPrimaryFileExtension() 316 { 317 return "data"; 318 } 319 320 /** Get a string representation of the true value for a boolean column. */ 321 @Override 322 public String getTrueValue() 323 { 324 return "TRUE"; 325 } 326 327 /** Returns true if database name should be an absolute path in the 328 * file system. 329 */ 330 @Override 331 public boolean needAbsolutePathForName() 332 { 333 return true; 334 } 335 336 /** Returns true if need host name for connect. */ 337 @Override 338 public boolean needHostForConnect() 339 { 340 return false; 341 } 342 343 /** Returns true if need password for connect. */ 344 @Override 345 public boolean needPasswordForConnect() 346 { 347 return false; 348 } 349 350 /** Returns true if need user name for connect. */ 351 @Override 352 public boolean needUserForConnect() 353 { 354 return false; 355 } 356 357 /** Rename a column. */ 358 @Override 359 public void renameColumn(String oldName, Column newColumn, String tableName) throws SQLException 360 { 361 String newName = getColumnName(newColumn.getName()); 362 String sqlStr = "ALTER TABLE " + getTableName(tableName) + 363 " ALTER COLUMN " + getColumnName(oldName) + " RENAME TO " + newName; 364 _executeSQL(sqlStr); 365 } 366 367 /** Change a column to allow null values. */ 368 @Override 369 public void setColumnNull(Column column, String tableName) throws SQLException 370 { 371 String sqlStr = "ALTER TABLE " + getTableName(tableName) + 372 " ALTER COLUMN " + getColumnName(column.getName()) + " SET NULL"; 373 _executeSQL(sqlStr); 374 } 375 376 /** Set not null constraint to a column. */ 377 @Override 378 public void setColumnNotNull(Column column, String tableName) throws SQLException 379 { 380 String sqlStr = "ALTER TABLE " + getTableName(tableName) + 381 " ALTER COLUMN " + getColumnName(column.getName()) + " SET NOT NULL"; 382 _executeSQL(sqlStr); 383 } 384 385 /** Set whether HSQL servers are run in a separate process. */ 386 public static void setForkServers(boolean fork) 387 { 388 _forkServers = fork; 389 } 390 391 /** Stop any running servers. */ 392 public static void shutdownServers() 393 { 394 for(Server server : _servers) 395 { 396 // remove the port file 397 _deletePortFile(server.getDatabasePath(0, true)); 398 399 final Connection connection = _getConnectionForServer(server); 400 if(connection != null) 401 { 402 Statement statement = null; 403 try 404 { 405 try 406 { 407 statement = connection.createStatement(); 408 statement.execute("SHUTDOWN"); 409 } 410 finally 411 { 412 if(statement != null) 413 { 414 statement.close(); 415 } 416 } 417 } 418 catch(SQLException e) 419 { 420 MessageHandler.error("Error shutting down database.", e); 421 } 422 finally 423 { 424 try { 425 connection.close(); 426 } catch (SQLException e) { 427 MessageHandler.error("Error closing connection " + 428 "while shutting down databases.", e); 429 } 430 } 431 } 432 } 433 _servers.clear(); 434 } 435 436 /////////////////////////////////////////////////////////////////// 437 // protected methods 438 439 /** Returns true if foreign keys are automatically indexed. */ 440 @Override 441 protected boolean _areForeignKeysIndexed() 442 { 443 return false; 444 } 445 446 /** Returns true if primary keys are automatically indexed. */ 447 @Override 448 protected boolean _arePrimaryKeysIndexed() 449 { 450 return false; 451 } 452 453 /** Return the alias for a database given the path. */ 454 private static String _getDatabaseAlias(String path) throws SQLException 455 { 456 int pathIndex = path.lastIndexOf(File.separator); 457 if(pathIndex == -1) 458 { 459 throw new SQLException("Database name must be an absolute " + 460 "path: " + path); 461 } 462 else 463 { 464 return path.substring(pathIndex + 1); 465 } 466 } 467 468 /** Return the database path without the file name given the path. */ 469 private static String _getDatabasePath(String path) throws SQLException 470 { 471 int pathIndex = path.lastIndexOf(File.separator); 472 if(pathIndex == -1) 473 { 474 throw new SQLException("Database name must be an absolute " + 475 "path: " + path); 476 } 477 else 478 { 479 return path.substring(0, pathIndex); 480 } 481 } 482 483 /** Get the driver class name. */ 484 @Override 485 protected String _getDriverName() 486 { 487 return "org.hsqldb.jdbcDriver"; 488 } 489 490 /** Get a JDBC URL. */ 491 @Override 492 protected String _getJDBCUrl(String hostName, String port, 493 String databaseName) throws SQLException 494 { 495 // see if the host name is set 496 if (hostName != null && hostName.length() > 0) 497 { 498 String hostAndPort = _combineHostAndPort(hostName, port); 499 500 // make sure the database name starts with a / 501 if(!databaseName.startsWith("/")) 502 { 503 databaseName = "/" + databaseName; 504 } 505 506 String alias = _getDatabaseAlias(databaseName); 507 return "jdbc:hsqldb:hsql://" + hostAndPort + "/" + alias + 508 ";filepath=hsqldb:file:" + databaseName; 509 } 510 return "jdbc:hsqldb:" + databaseName; 511 } 512 513 /** Get the SQL string of a column type. */ 514 @Override 515 protected String _getTypeString(Column column) 516 { 517 String retval = null; 518 519 switch(column.getType()) 520 { 521 case Boolean: 522 retval = "BOOLEAN"; 523 break; 524 case Blob: 525 // NOTE: we use BINARY instead of OBJECT so that we can 526 // query the size of the column. 527 // see DatabaseType.getColumnSize() 528 retval = "BINARY"; 529 break; 530 case Integer: 531 retval = "INTEGER"; 532 break; 533 case Timestamp: 534 retval = "TIMESTAMP"; 535 break; 536 case TextBlob: 537 case Varchar: 538 retval = "VARCHAR"; 539 break; 540 } 541 542 if(retval != null && column.isAutoIncrement()) 543 { 544 // set the starting value to be 1 since ResulSet.getInt() 545 // returns 0 if the value is NULL. 546 retval += " GENERATED BY DEFAULT AS IDENTITY(START WITH 1)"; 547 } 548 549 return retval; 550 } 551 552 /** Returns true if database uses a catalog. */ 553 @Override 554 protected boolean _hasCatalog() 555 { 556 return false; 557 } 558 559 /** Returns true if database supports auto-generated keys in its prepared 560 * statements. 561 */ 562 @Override 563 protected boolean _hasGeneratedKeys() 564 { 565 // HSQL 2.x supports generated keys 566 return true; 567 } 568 569 /** Returns true if table is cached. */ 570 @Override 571 protected boolean _isTableCached() 572 { 573 return true; 574 } 575 576 /** Returns true if column names should be capitalized. */ 577 @Override 578 protected boolean _needCapitalColumnNames() 579 { 580 return true; 581 } 582 583 /** Returns true if table names should be capitalized. */ 584 @Override 585 protected boolean _needCapitalTableNames() 586 { 587 return true; 588 } 589 590 /** Get a connection to the database. */ 591 @Override 592 protected synchronized Connection _getConnection(String jdbcURL) throws SQLException 593 { 594 // attempt to find the path, alias, and port in the jdbc url 595 Matcher matcher = _jdbcURLPattern.matcher(jdbcURL); 596 if(!matcher.matches()) 597 { 598 throw new SQLException("Could not parse JDBC URL " + 599 jdbcURL + "\n" + 600 "JDBC URL must be in form of: " + 601 "jdbc:hsqldb:hsql://hostname:port/alias;filepath=hsqldb:path"); 602 } 603 604 final String pathStr = matcher.group(3); 605 final String aliasStr = matcher.group(2); 606 final String dbPort = matcher.group(1); 607 608 String dbDir; 609 if(pathStr.startsWith("file:")) { 610 dbDir = pathStr.substring("file:".length()); 611 } else { 612 dbDir = pathStr; 613 } 614 615 final String databaseName = dbDir + File.separator + aliasStr; 616 617 final String userName = DEFAULT_USER_NAME; 618 String passwd; 619 try { 620 passwd = HSQLUtils.getAuthFilePassword(databaseName); 621 } catch (IOException e) { 622 throw new SQLException("Error reading auth file for " + aliasStr + ": " + e.getMessage()); 623 } 624 625 // check if database needs to be upgraded. 626 try { 627 _upgradeIfNecessary(databaseName, aliasStr, userName, passwd); 628 } catch(Exception e) { 629 throw new SQLException("Error upgrading database " + aliasStr, e); 630 } 631 632 try 633 { 634 Connection connection = DriverManager.getConnection(jdbcURL, userName, passwd); 635 _updateConnectionReferenceCount(connection, jdbcURL); 636 return connection; 637 } 638 catch(SQLException e) 639 { 640 String sqlState = e.getSQLState(); 641 642 if(_isDebugging) 643 { 644 _log.debug("failed = " + sqlState); 645 } 646 647 // see if the server is not running 648 if(sqlState.equals(_SERVER_NOT_RUNNING_SQL_STATE) || 649 // HSQL 2.x error code 650 sqlState.equals(_SERVER_NOT_RUNNING_SQL_STATE_2)) 651 { 652 653 StatusNotifier.log("Starting HSQL Server for " + aliasStr); 654 655 // start a server 656 int serverState = _launchDBServer(pathStr, aliasStr, dbPort); 657 if(serverState != ServerConstants.SERVER_STATE_ONLINE) 658 { 659 throw new SQLException("Unable to start HSQL server for " + 660 jdbcURL); 661 } 662 663 try 664 { 665 Connection retval = DriverManager.getConnection(jdbcURL, userName, passwd); 666 667 //System.out.println("new conn " + URL + " is " + conn); 668 _serverConnectionSet.add(retval); 669 _initializeNewURL(retval); 670 return retval; 671 } 672 catch(SQLException e2) 673 { 674 throw new SQLException("Unable to start HSQL server for " + 675 jdbcURL + ": " + e2.getMessage()); 676 } 677 } 678 else 679 { 680 throw e; 681 } 682 } 683 } 684 685 /** Get a connection to the database. */ 686 @Override 687 protected synchronized Connection _getConnection(String dbURL, String dbPort, 688 String databaseName, String userName, String passwd) 689 throws SQLException 690 { 691 692 // HSQL defaults to user "sa" 693 userName = DEFAULT_USER_NAME; 694 695 // read the password from the auth file unless it was specified 696 // in the configuration file 697 if(passwd == null || passwd.trim().isEmpty()) 698 { 699 try 700 { 701 passwd = HSQLUtils.getAuthFilePassword(databaseName); 702 //System.out.println("passwd " + passwd); 703 } 704 catch (IOException e) 705 { 706 throw new SQLException("Error reading auth file.", e); 707 } 708 } 709 710 String pathStr = _getDatabasePath(databaseName); 711 String aliasStr = _getDatabaseAlias(databaseName); 712 713 try { 714 _upgradeIfNecessary(databaseName, aliasStr, userName, passwd); 715 } catch(Exception e) { 716 throw new SQLException("Error upgrading database " + aliasStr, e); 717 } 718 719 // record the database path so that in disconnect() we can remove 720 // the port file when the database is shut down. 721 _dbName = databaseName; 722 723 Connection conn = null; 724 try 725 { 726 if(_isDebugging) 727 { 728 _log.debug("getting connection for " + dbURL); 729 } 730 731 //System.out.println("connecting to " + dbURL + " with passwd " + passwd); 732 conn = DriverManager.getConnection(dbURL, userName, passwd); 733 734 if(_isDebugging) 735 { 736 _log.debug("success"); 737 } 738 739 //System.out.println("already running hsql url = " + URL); 740 } 741 catch(SQLException e) 742 { 743 String sqlState = e.getSQLState(); 744 String errorMessage = e.getMessage(); 745 746 if(_isDebugging) 747 { 748 _log.debug("failed = " + sqlState); 749 } 750 751 // see if the server is not running 752 if(sqlState.equals(_SERVER_NOT_RUNNING_SQL_STATE) || 753 // HSQL 2.x error code 754 sqlState.equals(_SERVER_NOT_RUNNING_SQL_STATE_2)) 755 { 756 StatusNotifier.log("Starting HSQL Server for " + aliasStr); 757 758 // start a server 759 int serverState = _launchDBServer(pathStr, aliasStr, dbPort); 760 if(serverState != ServerConstants.SERVER_STATE_ONLINE) 761 { 762 throw new SQLException("Unable to start HSQL server for " + 763 dbURL, e); 764 } 765 766 try 767 { 768 // create a connection using the existing password, if any 769 String existingPasswd = HSQLUtils.getAuthFilePassword(_dbName); 770 771 //System.out.println("existingPasswd " + existingPasswd); 772 773 conn = DriverManager.getConnection(dbURL, userName, existingPasswd); 774 775 _log.info("started HSQL server at " + dbURL); 776 //new Exception().printStackTrace(); 777 _serverConnectionSet.add(conn); 778 _initializeNewURL(conn); 779 } 780 catch(SQLException e2) 781 { 782 throw new SQLException("Unable to start HSQL server for " + 783 dbURL, e2); 784 } 785 catch(IOException e3) 786 { 787 throw new SQLException("Error reading auth file.", e3); 788 } 789 } 790 else if(errorMessage != null && errorMessage.equals("Access is denied")) 791 { 792 // if the database does not exist, and the password is set, 793 // getConnection() fails with Access is denied. 794 // new databases must be created with no password. 795 conn = DriverManager.getConnection(dbURL, userName, ""); 796 } 797 else 798 { 799 throw e; 800 } 801 } 802 803 if (conn != null) 804 { 805 if(_isDebugging) 806 { 807 _log.debug("opened connection " + conn + " url = " + dbURL); 808 } 809 810 boolean isNew = _updateConnectionReferenceCount(conn, dbURL); 811 812 if(isNew) { 813 try 814 { 815 _changePassword(conn, databaseName, passwd); 816 } 817 catch (IOException e) 818 { 819 throw new SQLException("Error writing auth file.", e); 820 } 821 } 822 823 return conn; 824 } 825 else 826 { 827 throw new SQLException( 828 "Failed to connect to url \"" 829 + dbURL 830 + "\" as user \"" 831 + userName 832 + ". Perhaps there was an error launching the db server. " 833 + "In addition, sometimes the database " 834 + "cache can become corrupt, so another " 835 + "solution is to remove the \"" + pathStr 836 + "\" directory."); 837 } 838 } 839 840 /////////////////////////////////////////////////////////////////// 841 // private methods 842 843 /** Change the password for the database. The new password is written to the 844 * auth file. 845 * @param connection the jdbc connection 846 * @param databaseName the database name 847 * @param passwd the new password for the database. if null or empty, 848 * one is randomly generated. 849 */ 850 private static void _changePassword(Connection connection, 851 String databaseName, String passwd) throws IOException, SQLException 852 { 853 854 String newPasswd; 855 856 if(passwd == null || passwd.isEmpty()) 857 { 858 // generate a random password 859 newPasswd = UUID.randomUUID().toString(); 860 } 861 else 862 { 863 newPasswd = passwd; 864 } 865 866 867 //System.out.println("changing password to " + newPasswd); 868 869 870 final Properties properties = new Properties(); 871 properties.setProperty(HSQLUtils.AUTH_FILE_PASSWORD_PROP_NAME, newPasswd); 872 873 // write the properties file 874 final File authFile = new File(databaseName + HSQLUtils.AUTH_FILE_EXTENSION); 875 _writePropertiesFile(authFile, properties); 876 877 // remove read permissions for everyone but owner 878 authFile.setReadable(false, false); 879 authFile.setReadable(true, true); 880 881 try(Statement statement = connection.createStatement();) { 882 statement.execute("ALTER USER " + DEFAULT_USER_NAME + " SET PASSWORD \"" + newPasswd + "\""); 883 // checkpoint the database so the password change is made persistent. 884 statement.execute("CHECKPOINT"); 885 } 886 } 887 888 /** Write properties to a file. */ 889 private static void _writePropertiesFile(File file, Properties properties) throws IOException 890 { 891 // make sure the containing directory exists. 892 File parentDir = file.getParentFile(); 893 if(!parentDir.exists() && !parentDir.isDirectory() && 894 !parentDir.mkdirs()) { 895 throw new IOException("Unable to create directories for " + parentDir); 896 } 897 898 OutputStream outputStream = null; 899 try 900 { 901 outputStream = new FileOutputStream(file); 902 properties.store(outputStream, null); 903 } 904 finally 905 { 906 if(outputStream != null) 907 { 908 outputStream.close(); 909 } 910 } 911 } 912 913 /** Get a Connection object for a Server. Returns null on error. */ 914 private static Connection _getConnectionForServer(Server server) 915 { 916 String host = server.getAddress(); 917 //replace 0.0.0.0 to be localhost to avoid errors on some machines 918 if (host.equals("0.0.0.0")) 919 host = "localhost"; 920 final int port = server.getPort(); 921 final String alias = server.getDatabaseName(0, true); 922 final String path = server.getDatabasePath(0, true); 923 try { 924 // read the password from the auth file 925 String passwd = HSQLUtils.getAuthFilePassword(path); 926 927 // XXX this assume user is sa and password is either in auth file or empty 928 // a better solution is to store the username and password when starting 929 // the server. 930 return DriverManager.getConnection("jdbc:hsqldb:hsql://" + 931 host + ":" + port + "/" + alias, DEFAULT_USER_NAME, passwd); 932 } catch (Exception e) { 933 MessageHandler.error("Could not get Connection object for HSQL server " + server, e); 934 } 935 return null; 936 } 937 938 /** Start the HSQL Server */ 939 private static int _launchDBServer(String dbNamePath, String dbAlias, 940 String dbPort) 941 { 942 943 if(_forkServers) 944 { 945 System.out.println("spawning HSQL server for " + dbNamePath); 946 947 // find the hsql jar 948 String classpath = System.getProperty("java.class.path"); 949 String[] jars = classpath.split(File.pathSeparator); 950 String hsqlJar = null; 951 for(String jar : jars) 952 { 953 if(jar.matches(".*hsqldb-[\\d\\.]+\\.jar$")) 954 { 955 hsqlJar = jar; 956 break; 957 } 958 } 959 960 if(hsqlJar == null) 961 { 962 MessageHandler.error("Unable to find HSQL jar in class path."); 963 return ServerConstants.SERVER_STATE_SHUTDOWN; 964 } 965 966 // NOTE: the database argument must include the file name of 967 // the database. when using the Server API to start the server 968 // (see below), the database argument does NOT include the file 969 // name, but uses the alias as the file name. 970 971 ProcessBuilder procBuilder = new ProcessBuilder("java", 972 "-cp", 973 hsqlJar, 974 "org.hsqldb.Server", 975 "-address", 976 "localhost", 977 "-port", 978 dbPort, 979 "-dbname.0", 980 dbAlias, 981 "-database.0", 982 dbNamePath + File.separator + dbAlias); 983 procBuilder.redirectErrorStream(true); 984 985 //for(String str : procBuilder.command()) 986 //System.out.print(str + " "); 987 //System.out.println(); 988 989 try { 990 /*Process proc =*/ procBuilder.start(); 991 992 // sleep a few seconds so that it has time to start before we 993 // try to connect 994 // XXX this may not be long enough 995 Thread.sleep(3000); 996 return ServerConstants.SERVER_STATE_ONLINE; 997 } catch (Exception e) { 998 MessageHandler.error("Error starting HSQL server.", e); 999 return ServerConstants.SERVER_STATE_SHUTDOWN; 1000 } 1001 } 1002 else 1003 { 1004 Server server = new Server(); 1005 1006 if (!_isDebugging) 1007 { 1008 server.setLogWriter(null); 1009 server.setErrWriter(null); 1010 } 1011 else 1012 { 1013 _log.debug("starting server for " + dbNamePath); 1014 } 1015 1016 // the file name is full path and alias. 1017 String dbFileName = dbNamePath + File.separator + dbAlias; 1018 1019 server.setDatabasePath(0, dbFileName); 1020 server.setDatabaseName(0, dbAlias); 1021 1022 if(dbPort != null && dbPort.length() > 0) 1023 { 1024 try 1025 { 1026 int port = Integer.parseInt(dbPort); 1027 server.setPort(port); 1028 } 1029 catch(NumberFormatException e) 1030 { 1031 System.out.print("ERROR: bad port " + dbPort + ": " + 1032 e.getMessage()); 1033 } 1034 } 1035 1036 server.setSilent(true); 1037 server.setTrace(false); 1038 server.setNoSystemExit(true); 1039 server.start(); 1040 1041 _servers.add(server); 1042 1043 return server.getState(); 1044 } 1045 } 1046 1047 /** Checkpoint any servers to compact their size. This rewrites 1048 * the backing files for the databases. 1049 */ 1050 public static void checkpointAllServers() 1051 { 1052 for(Connection connection : _serverConnectionSet) 1053 { 1054 Statement statement = null; 1055 try 1056 { 1057 try 1058 { 1059 //long startTime = System.nanoTime(); 1060 1061 //System.out.print("checkpointing...."); 1062 1063 statement = connection.createStatement(); 1064 statement.execute("CHECKPOINT DEFRAG"); 1065 1066 //long estimatedTime = System.nanoTime() - startTime; 1067 1068 //System.out.println("done; took " + 1069 //(double)(estimatedTime / 1000000000) + "s"); 1070 1071 } 1072 finally 1073 { 1074 if(statement != null) 1075 { 1076 statement.close(); 1077 } 1078 } 1079 } 1080 catch(SQLException e) 1081 { 1082 //System.out.println("Error checkpointing database: " + 1083 //e.getMessage()); 1084 } 1085 } 1086 } 1087 1088 /** Increment the reference count for a URL. */ 1089 private boolean _updateConnectionReferenceCount(Connection connection, String urlStr) throws SQLException 1090 { 1091 // the reference count should be incremented for URLs with direct file access 1092 // or if the URL is for a server and servers are run in a separate process. 1093 if((!_forkServers && !urlStr.startsWith("jdbc:hsqldb:hsql") && !urlStr.startsWith("jdbc:hsqldb:http")) || 1094 (_forkServers && urlStr.startsWith("jdbc:hsqldb:hsql"))) 1095 { 1096 if(_isDebugging) 1097 { 1098 _log.debug(urlStr + " appears to access file directly."); 1099 } 1100 1101 boolean isNew = false; 1102 1103 // it does not, so we are accessing the database file 1104 // directly, and must issue shutdown in disconnect(). 1105 1106 synchronized(_urlMapLock) 1107 { 1108 Integer count = _urlToCountMap.get(urlStr); 1109 if(count == null) 1110 { 1111 count = Integer.valueOf(1); 1112 isNew = true; 1113 } 1114 else 1115 { 1116 count++; 1117 } 1118 1119 _urlToCountMap.put(urlStr, count); 1120 _connectionToURLMap.put(connection, urlStr); 1121 } 1122 1123 if(isNew) 1124 { 1125 _initializeNewURL(connection); 1126 } 1127 1128 return isNew; 1129 } 1130 1131 return false; 1132 } 1133 1134 /** Check if an HSQL database is 1.x and upgrade it to 2.x. 1135 * 1136 * @param databaseName the full path and name (but not the extension) of the database file. 1137 * @param aliasStr the database alias 1138 * @param userName the user name to access the database 1139 * @param passwd the password to access the database 1140 * 1141 */ 1142 private void _upgradeIfNecessary(String databaseName, String aliasStr, String userName, String passwd) 1143 throws Exception 1144 { 1145 1146 File propertiesFile = new File(databaseName + ".properties"); 1147 1148 // see if the database exists 1149 if(propertiesFile.exists()) { 1150 1151 // check the version in the properties file 1152 Properties properties = HSQLUtils.readPropertiesFile(propertiesFile); 1153 1154 final String version = properties.getProperty("version"); 1155 if(version.startsWith("1")) { 1156 1157 StatusNotifier.log("Upgrading " + aliasStr + " (this can take several minutes)."); 1158 1159 // see if a log file exists. if so, run CleanupHSQL1, which commits the 1160 // transactions in the log file. 1161 1162 final File logFile = new File(databaseName + ".log"); 1163 if(logFile.exists()) { 1164 1165 System.out.println("Log file found for " + aliasStr + ". Will run CleanupHSQL1."); 1166 1167 // find the hsql 1 jar 1168 ModuleTree tree = ModuleTree.instance(); 1169 Module module = tree.getModuleByStemName("core"); 1170 if(module == null) { 1171 throw new Exception("Could not find core module in current suite."); 1172 } 1173 1174 String hsql1Jar = null; 1175 1176 FileSet files = new FileSet(); 1177 files.setProject(ProjectLocator.getAntProject()); 1178 files.setDir(module.getLibDir()); 1179 files.setIncludes("**/" + _HSQLDB_1_JAR); 1180 Iterator<Resource> iterator = files.iterator(); 1181 if(iterator.hasNext()) { 1182 Resource resource = iterator.next(); 1183 if(resource instanceof FileResource) { 1184 File file = ((FileResource)resource).getFile(); 1185 hsql1Jar = file.getAbsolutePath(); 1186 } 1187 } 1188 1189 if(hsql1Jar == null) { 1190 throw new Exception("Could not find " + _HSQLDB_1_JAR + "."); 1191 } 1192 1193 // find CleanupHSQL1 jar 1194 1195 String directory = new File(hsql1Jar).getParent(); 1196 File cleanupJar = new File(directory, _CLEANUP_HSQL_1_JAR); 1197 1198 if(!cleanupJar.exists()) { 1199 throw new Exception("Could not find " + _CLEANUP_HSQL_1_JAR); 1200 } 1201 1202 ProcessBuilder builder = new ProcessBuilder("java", "-cp", 1203 hsql1Jar + ":" + cleanupJar, "org.kepler.util.sql.CleanupHSQL1", 1204 databaseName, userName); 1205 builder.redirectErrorStream(true); 1206 Process process = builder.start(); 1207 try(InputStream stdoutStream = process.getInputStream();) { 1208 IOUtils.copy(stdoutStream, System.out); 1209 } 1210 1211 int rc = process.waitFor(); 1212 if(rc != 0) { 1213 System.out.println("WARNING: CleanupHSQL1 returned " + rc); 1214 } 1215 } 1216 1217 // NOTE: need to make password upper-case for 1.x 1218 1219 try(Connection connection = DriverManager.getConnection("jdbc:hsqldb:file:" + databaseName, 1220 userName, passwd.toUpperCase()); 1221 Statement statement = connection.createStatement();) { 1222 statement.execute("SHUTDOWN COMPACT"); 1223 } 1224 1225 // change the password since 2.x passwords are case-sensitive 1226 1227 //System.out.println("changing password to " + passwd); 1228 1229 try(Connection connection = DriverManager.getConnection("jdbc:hsqldb:file:" + databaseName, 1230 userName, passwd.toUpperCase()); 1231 Statement statement = connection.createStatement();) { 1232 statement.execute("ALTER USER " + userName + " SET PASSWORD \"" + passwd + "\""); 1233 // checkpoint the database so the password change is made persistent. 1234 statement.execute("CHECKPOINT"); 1235 statement.execute("SHUTDOWN"); 1236 } 1237 } 1238 1239 } 1240 } 1241 1242 /** Perform initialization for a URL that has not yet been used. */ 1243 private void _initializeNewURL(Connection connection) throws SQLException 1244 { 1245 //System.out.println("initializing for new URL. connection = " + connection); 1246 1247 // increase number of rows cached in memory 1248 _executeSQL(_SET_CACHE_SCALE, connection); 1249 1250 // HSQL 2.x: do not require VARCHAR columns to require a size. 1251 // this is for backwards-compatibility with HSQL 1.x. 1252 _executeSQL("SET DATABASE SQL SIZE FALSE", connection); 1253 1254 // HSQL 2.x: make shutdown faster by incrementally backing up the .data file. 1255 _executeSQL("SET FILES BACKUP INCREMENT TRUE", connection); 1256 } 1257 1258 /** Execute an SQL statement for a Connection. 1259 * TODO move to parent class 1260 */ 1261 protected void _executeSQL(String sqlStr, Connection connection) throws SQLException 1262 { 1263 try(Statement statement = connection.createStatement();) { 1264 statement.execute(sqlStr); 1265 } 1266 } 1267 1268 /** Delete the port file for a database. */ 1269 protected static void _deletePortFile(String dbName) 1270 { 1271 String portFileName = dbName + _PORT_FILE_EXTENSION; 1272 File portFile = new File(portFileName); 1273 if(portFile.exists() && !portFile.delete()) 1274 { 1275 System.out.println("WARNING: Could not delete port file " + portFileName); 1276 } 1277 } 1278 1279 /////////////////////////////////////////////////////////////////// 1280 // private variables 1281 1282 /** A regex to find the port, alias, and file path in an hsql jdbc url. */ 1283 private static final Pattern _jdbcURLPattern = 1284 Pattern.compile("jdbc:hsqldb:hsql://[\\w/]+:(\\d+)/(\\w+)\\;filepath=hsqldb\\:(.*)"); 1285 1286 /** SQL state string returned in SQLException when trying to connect 1287 * to a HSQL server that is not running. 1288 */ 1289 private static final String _SERVER_NOT_RUNNING_SQL_STATE = "08000"; 1290 private static final String _SERVER_NOT_RUNNING_SQL_STATE_2 = "08001"; 1291 1292 /** SQL statement to set write delay. */ 1293 private static final String _SQL_SET_WRITE_DELAY = "SET WRITE_DELAY TRUE"; 1294 1295 /** A collection of HSQL server connections that we start. */ 1296 private static final Set<Connection> _serverConnectionSet = 1297 new HashSet<Connection>(); 1298 1299 /** A collection of Server objects. */ 1300 private static final Set<Server> _servers = Collections.synchronizedSet(new HashSet<Server>()); 1301 1302 /** A mapping of URLs to a count. (Only for URLs that directly access the file.) */ 1303 private static final Map<String,Integer> _urlToCountMap = new HashMap<String,Integer>(); 1304 1305 /** A mapping of Connections to URL strings. (Only for URLs that directly access the file.) */ 1306 private static final Map<Connection,String> _connectionToURLMap = new HashMap<Connection,String>(); 1307 1308 /** A lock to synchronize access for _urlToCountMap and _connectionToURLMap. */ 1309 private static final Object _urlMapLock = new Object(); 1310 1311 /** SQL statement to set the maximum number of rows of cached tables that 1312 * are held in memory. Setting to 18 improves performance. 1313 * see: http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5429#c2 1314 */ 1315 private static final String _SET_CACHE_SCALE = "SET PROPERTY \"hsqldb.cache_scale\" 18"; 1316 1317 /** If true, HSQL servers are run in a separate process. */ 1318 private static boolean _forkServers = false; 1319 1320 /** Logging */ 1321 private static final Log _log = LogFactory.getLog(HSQL.class.getName()); 1322 private static final boolean _isDebugging = _log.isDebugEnabled(); 1323 1324 /** Extension of the port file containing the server port. */ 1325 private static final String _PORT_FILE_EXTENSION = ".port"; 1326 1327 /** Name of port property in port file. */ 1328 private static final String _PORT_FILE_PASSWORD_PROP_NAME = "port"; 1329 1330 /** Random number generator used to generate random port numbers. */ 1331 private static final Random _random = new Random(); 1332 1333 /** A mapping of ports randomly chosen for the server to database names. */ 1334 private static final Map<String,String> _serverPortToName = new HashMap<String,String>(); 1335 1336 /** The database path and name. */ 1337 private String _dbName; 1338 1339 /** Default user name. */ 1340 private static final String DEFAULT_USER_NAME = "sa"; 1341 1342 /** The name of the HSQL 1.x jar. */ 1343 private static final String _HSQLDB_1_JAR = "hsqldb-1.8.1.3.jar"; 1344 1345 /** The name of the cleanup jar. */ 1346 private static final String _CLEANUP_HSQL_1_JAR = "CleanupHSQL1.jar"; 1347 1348}