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}