001/*
002 * Copyright (c) 2008-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2015-10-28 21:02:57 +0000 (Wed, 28 Oct 2015) $' 
007 * '$Revision: 34135 $'
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.FilenameFilter;
034import java.io.IOException;
035import java.sql.Connection;
036import java.sql.SQLException;
037import java.sql.Statement;
038import java.util.Arrays;
039import java.util.HashMap;
040import java.util.LinkedList;
041import java.util.List;
042import java.util.Map;
043import java.util.regex.Matcher;
044import java.util.regex.Pattern;
045
046import org.apache.tools.ant.taskdefs.Copy;
047import org.kepler.build.modules.Module;
048import org.kepler.build.modules.ModuleTree;
049import org.kepler.build.util.Version;
050import org.kepler.configuration.ConfigurationManager;
051import org.kepler.configuration.ConfigurationProperty;
052import org.kepler.configuration.ConfigurationUtilities;
053import org.kepler.objectmanager.cache.CacheManager;
054import org.kepler.util.DotKeplerManager;
055
056import ptolemy.util.MessageHandler;
057
058/** 
059 *
060 * A factory to instantiate and enumerate database types.
061 *
062 * @author Daniel Crawl
063 * @version $Id: DatabaseFactory.java 34135 2015-10-28 21:02:57Z crawl $
064 *
065 */
066    
067public class DatabaseFactory
068{    
069    /** Convenience method to get connection to default kepler DB,
070     * which is currently HSQL.
071     * 
072     * FIXME this method should be deleted 
073     * 
074     * @return
075     * @throws SQLException
076     * @throws ClassNotFoundException
077     */
078    public static Connection getDBConnection() 
079        throws SQLException, ClassNotFoundException
080    {
081
082        ConfigurationProperty commonProperty = ConfigurationManager
083                .getInstance().getProperty(
084                        ConfigurationManager.getModule("common"));
085        List<ConfigurationProperty> hsqlList = commonProperty.findProperties(
086                "sqlEngineName", "hsql", true);
087        ConfigurationProperty hsqlProp = (ConfigurationProperty) hsqlList
088                .get(0);
089
090        String username = hsqlProp.getProperty("userName").getValue();
091        String password = hsqlProp.getProperty("password").getValue();
092        String url = hsqlProp.getProperty("url").getValue();
093        String dbPort = hsqlProp.getProperty("port").getValue();
094        
095        String dbAlias = hsqlProp.getProperty("dbName").getValue();
096        
097        DatabaseType dbType = getType("HSQL");
098        
099        String dbName = dbAlias;
100        
101        if(dbType.needAbsolutePathForName() &&
102            !dbAlias.startsWith("/"))
103        {
104                dbName = DotKeplerManager.getInstance().getCacheDirString()
105                + "cachedata" + File.separator + dbAlias;
106        }
107        
108        String fullUrl = null;
109        
110        if (url != null && url.endsWith("/")) 
111        {
112            fullUrl = url.substring(0, url.length()-1);
113        }
114        else
115        {
116            fullUrl = url;
117        }
118
119        String randomPort;
120        try
121        {
122            // NOTE: the third argument should be the hostname
123            // we use the url since there is not a separate entry
124            // for host name in the configuration file.
125            // the url may specify to directly access the file, but
126            // _getRandomPort will chose a random port. this isn't
127            // too bad since the port will not actually be used
128            // (since it's configured to directly access the file).
129            randomPort = dbType._getPort(dbPort, dbName, url);
130        }
131        catch (IOException e)
132        {
133            throw new SQLException("Error chosing random port.", e);
134        }
135        
136        if (randomPort != null && fullUrl != null)
137        {
138            fullUrl = fullUrl.concat(":" + randomPort);
139        }
140        
141        if(fullUrl != null)
142        {
143            fullUrl = fullUrl.concat("/" + dbAlias + ";filepath=hsqldb:file:" + dbName);
144        }
145        
146        // if the url is null, then set the url to directly access the file
147        // instead of starting the database server.
148        if(fullUrl == null)
149        {
150            fullUrl = "jdbc:hsqldb:file:" + dbName;
151        }
152        
153        // must load the driver
154        try {
155            Class.forName(dbType._getDriverName()).newInstance();
156        } catch (InstantiationException e) {
157            throw new SQLException("Error loading HSQL driver: " + e.getMessage());
158        } catch (IllegalAccessException e) {
159            throw new SQLException("Error loading HSQL driver: " + e.getMessage());
160        }
161        
162        //System.out.println("fullUrl = " + fullUrl);
163        
164        Connection conn = dbType._getConnection(fullUrl, randomPort, 
165                dbName, username, password);
166        
167        // set the schema name;
168        final String schemaName = CacheManager.getDatabaseSchemaName();
169        Statement statement = null;
170        try {
171            statement = conn.createStatement();
172            // try to set the schema
173            try {
174                statement.execute("SET SCHEMA " + schemaName);
175            } catch(SQLException e) {
176                // see if the exception looks like this schema does not exist
177                if(e.getMessage() != null && e.getMessage().startsWith("invalid schema name:")) {
178                    // create the schema and set it
179                    statement.execute("CREATE SCHEMA " + schemaName + " AUTHORIZATION DBA");
180                    statement.execute("SET SCHEMA " + schemaName);                    
181                } else {
182                    throw e;
183                }
184            }
185        } finally {
186            if(statement != null) {
187                statement.close();
188            }
189        }
190
191        return conn;
192    }
193    
194    /** Connect to a database from a ConfigurationProperty. */
195    public static DatabaseType getConnectedDatabaseType(
196        ConfigurationProperty dbProperty) throws SQLException
197    {
198        // get a map of the parameters
199        Map<String,String> map = ConfigurationUtilities.getPairsMap(dbProperty);   
200        return getConnectedDatabaseType(map, dbProperty.getModule().getStemName());
201    }
202    
203    /** Get a connected database from connection parameters. */
204    public static DatabaseType getConnectedDatabaseType(
205        Map<String,String> parameters, String moduleName) throws SQLException
206    {
207        // get the type
208        String typeStr = parameters.get(Parameter.TYPE.getName());
209        if(typeStr == null)
210        {
211            new Exception().printStackTrace(System.out);
212            throw new SQLException("Could not find the " +
213                Parameter.TYPE.getName() + " property.");
214        }
215        
216        DatabaseType dbType = getType(typeStr);
217        if(dbType == null)
218        {
219            throw new SQLException("Unsupported type of database: " +
220                    typeStr);
221        }
222        
223        Map<String,String> connectParameters = parameters;
224
225        // see if a jdbc url was specified
226        String jdbcURLStr = parameters.get(Parameter.JDBC_URL.getName());        
227        if(jdbcURLStr == null || jdbcURLStr.length() == 0)
228        {               
229            // check path name
230            final String dbName = parameters.get(Parameter.NAME.getName());
231    
232            if(dbType.needAbsolutePathForName())
233            {
234                File dbFileName = new File(dbName);
235                // see if it's a relative path
236                if(!dbFileName.isAbsolute())
237                {
238                        String dbPathStr;
239                        
240                    // prepend the absolute path to the persistent module
241                    // directory for owning module
242
243                    ModuleTree modules = ModuleTree.instance();
244                    Module module = modules.getModuleByStemName(moduleName);
245                    
246                    // make sure we found the module.
247                    if(module == null)
248                    {
249                        throw new SQLException("Could not find module: " +
250                            moduleName + " Is it in modules.txt?");
251                    }
252
253                    // see if we are running a released module
254                    if(Version.isVersioned(module.getName()))
255                    {
256                        Version version = Version.fromVersionString(module.getName());
257                        //System.out.println("version = " + version.getMajor() + "." + version.getMinor());
258                    
259                        dbPathStr = DotKeplerManager.getInstance().
260                                getPersistentModuleDirectory(moduleName) +
261                                File.separator + "db-" +
262                                version.getMajor() + "." + version.getMinor() +
263                                File.separator + dbName;
264                        
265                        // see if it exists
266                        String dbFullName = dbPathStr + "." + dbType.getPrimaryFileExtension();
267                        dbFileName = new File(dbFullName);
268                        if(!dbFileName.exists())
269                        {
270                                System.out.println("missing db for " + dbPathStr);
271                                
272                                // find previous version, if any
273                                String previousNameStr = _findPreviousVersion(module, 
274                                        dbName + "." + dbType.getPrimaryFileExtension());
275                                
276                                // see if there was a previous version
277                                if(previousNameStr != null)
278                                {
279                                        // upgrade
280                                        System.out.println("going to copy previous version in " + previousNameStr);
281                                        File oldDir = new File(DotKeplerManager.getInstance().
282                                                getPersistentModuleDirectory(moduleName) +
283                                                File.separator + previousNameStr);
284                                        
285                                        // determine the files to copy
286                                        String[] filesToCopy = oldDir.list(new FilenameFilter() {
287                                                                        public boolean accept(File dir, String name) {
288                                                                                return name.startsWith(dbName);
289                                                                        }                       
290                                        });
291                                        
292                                        File dbDestDir = new File(dbPathStr).getParentFile();
293                                        // make the destination directory if it does not exist.
294                                        if(!dbDestDir.exists())
295                                        {
296                                                dbDestDir.mkdir();
297                                        }
298                                        
299                                        // copy the files
300                                        for(String fileStr : filesToCopy)
301                                        {
302                                                File srcFile = new File(oldDir, fileStr);
303                                                File destFile = new File(dbDestDir, fileStr);
304                                                System.out.println("going to copy " + srcFile + " to " + destFile);
305                                                Copy copy = new Copy();
306                                                copy.setFile(srcFile);
307                                                copy.setTofile(destFile);
308                                                copy.execute();
309                                        }
310                                }                               
311                        }
312                    }
313                    else
314                    {
315                        // trunk
316                        //System.out.println("on trunk.");
317                        
318                        dbPathStr = DotKeplerManager.getInstance().
319                                getPersistentModuleDirectory(moduleName)
320                                + File.separator + dbName;
321                    }
322                                        
323                    // make a copy and replace the name.
324                    connectParameters = new HashMap<String,String>(parameters);
325                    connectParameters.put(Parameter.NAME.getName(), dbPathStr);
326                }
327            }
328        }
329
330        // make the connection
331        dbType.connect(connectParameters);            
332
333        return dbType;
334    }
335
336    /** Get the DatabaseType from a name. */
337    public static DatabaseType getType(String name)
338    {
339        DatabaseType retval = null;
340
341        if(name.equals("MySQL"))
342        {
343            retval =  new MySQL();
344        }
345        else if(name.equals("HSQL"))
346        {
347            retval =  new HSQL();
348        }
349        else if(name.equals("PostgreSQL"))
350        {
351            retval =  new PostgreSQL();
352        }
353        else if(name.equals("Oracle"))
354        {
355            retval =  new Oracle();
356        }
357        return retval;
358    }
359    
360    /** Get all the type names. */
361    public static List<String> getNames()
362    {
363        List<String> names = new LinkedList<String>();
364        names.add("MySQL");
365        names.add("HSQL");
366        names.add("PostgreSQL");
367        names.add("Oracle");
368        return names;
369    }
370
371    /** Shut down the server for the kepler cache database. */
372    public static void shutdownCacheServer() {
373        Connection connection = null;
374        try {
375            connection = getDBConnection();
376        } catch (Exception e) {
377            MessageHandler.error("Error stopping .kepler cache database.", e);
378            return;
379        }
380        
381        try {
382            Statement statement = null;
383            try {
384                statement = connection.createStatement();
385                statement.execute("SHUTDOWN");
386            } finally {
387                if(statement != null) {
388                    statement.close();
389                }
390            }
391        } catch (SQLException e) {
392            MessageHandler.error("Error stopping .kepler cache database.", e);
393        }
394        
395        // read the database name from the configuration
396        
397        ConfigurationProperty commonProperty = ConfigurationManager
398                .getInstance().getProperty(
399                        ConfigurationManager.getModule("common"));
400        List<ConfigurationProperty> hsqlList = commonProperty.findProperties(
401                "sqlEngineName", "hsql", true);
402        ConfigurationProperty hsqlProp = (ConfigurationProperty) hsqlList
403                .get(0);
404        
405        String dbName = hsqlProp.getProperty("dbName").getValue();
406        
407        DatabaseType dbType = getType("HSQL");
408        
409        // get the database path
410        
411        if(dbType.needAbsolutePathForName() &&
412            !dbName.startsWith("/"))
413        {
414            dbName = DotKeplerManager.getInstance().getCacheDirString()
415                + "cachedata" + File.separator + dbName;
416        }
417        
418        // remove the port file
419        HSQL._deletePortFile(dbName);
420        
421    }
422
423    public enum Parameter
424    {
425        /** User name of database connection. */
426        USER("DB User Name"),
427          
428        /** Password for database connection. */
429        PASSWD("Password"),
430          
431        /** Database host. */
432        HOST("DB Host"),
433          
434        /** Name of database (i.e., schema or sid). */
435        NAME("DB Name"),
436          
437        /** Type of database. */
438        TYPE("DB Type"),
439    
440        /** Port of database. */
441        PORT("DB Port"),
442
443        /** Name of table prefix. */
444        TABLEPREFIX("DB Table Prefix"),
445        
446        /** JDBC URL. This is optional; use to override other parameters. */
447        JDBC_URL("JDBC URL"),
448        
449        /** If false, do not create any indexes in schema.
450         *  NOTE: this has only been tested for MySQL.
451         */
452        CREATE_INDEXES("Create Indexes");
453
454          
455        Parameter(String name)
456        {
457            _name = name;
458        }
459          
460        /** Get the name of the parameter. */
461        public String getName()
462        {
463            return _name;
464        }
465          
466        /** Find the Parameter from a name. */
467        public static Parameter getType(String name)
468        {
469            for(Parameter parameter : values())
470            {
471                if(parameter.getName().equals(name))
472                {
473                    return parameter;
474                }
475            }
476            return null;
477        }
478          
479        private String _name;
480    }
481    
482    ///////////////////////////////////////////////////////////////////
483    ////                      private methods                    //////
484
485    /** Returns the directory containing the least oldest version of the
486     *  database. If no such directory exists, returns null. 
487     */
488    private static String _findPreviousVersion(Module module,
489        final String dbName)
490    {
491        File moduleDir = DotKeplerManager.getInstance().getPersistentModuleDirectory(module.getStemName());
492        
493        // get a list of possible directories
494        String[] choices = moduleDir.list(new FilenameFilter() {
495                public boolean accept(File dir, String name) {
496                        //System.out.println("checking " + name);
497                        // see if the name matches the pattern
498                        if(_DB_PATTERN.matcher(name).matches()) {
499                                File dbFile = new File(dir.getAbsolutePath() + 
500                                        File.separator + name + File.separator + dbName);
501                                // make sure the primary database file exists.
502                                return dbFile.exists();
503                        }
504                        return false;
505                }
506        });
507        Arrays.sort(choices);
508        
509        // find the closest version
510        String closestDir = null;
511        String moduleVersion = "db-" + 
512            Version.fromVersionString(module.getName()).getMajor() + "." +
513            Version.fromVersionString(module.getName()).getMinor();
514        
515        for(String choice : choices)
516        {
517                if(moduleVersion.compareTo(choice) > 0)
518                {
519                        //System.out.println(choice + " is before " + moduleVersion);
520                        closestDir = _compareTwoVersions(moduleVersion, closestDir, choice);
521                }
522        }
523        
524        //System.out.println("closest dir is " + closestDir);
525        
526        return closestDir;
527    }
528    
529    /** Check if a version is closer to another version.
530     * @param dbName The string against which to test.
531     * @param closest The currently nearest version to dbName.
532     * @param choice A version string to test.
533     * @return If closest has the nearest version to dbName, returns closest, otherwise
534     * returns choice.
535     */
536    private static String _compareTwoVersions(String dbName, String closest,
537        String choice)
538    {
539        String retval = null;
540        
541        // if there is no currently nearest value, select choice
542        if(closest == null)
543        {
544                retval = choice;
545        }
546        else
547        {
548                // compute the version sum of each
549                
550                Matcher dbMatcher = _DB_PATTERN.matcher(dbName);
551                dbMatcher.matches();
552                int dbSum = _computeVersionSum(Integer.valueOf(dbMatcher.group(1)),
553                        Integer.valueOf(dbMatcher.group(2)));
554                
555                Matcher closestMatcher = _DB_PATTERN.matcher(closest);
556                closestMatcher.matches();
557                int closestSum = _computeVersionSum(Integer.valueOf(closestMatcher.group(1)),
558                        Integer.valueOf(closestMatcher.group(2)));
559
560                Matcher choiceMatcher = _DB_PATTERN.matcher(choice);
561                choiceMatcher.matches();
562                int choiceSum = _computeVersionSum(Integer.valueOf(choiceMatcher.group(1)),
563                        Integer.valueOf(choiceMatcher.group(2)));
564                
565                // see which sum is smaller
566                if(dbSum - choiceSum < dbSum - closestSum)
567                {
568                        retval = choice;
569                }
570                else
571                {
572                        retval = closest;
573                }
574
575        }
576        
577        return retval;
578    }
579    
580    /** Compute a version sum from major and minor numbers. */
581    private static int _computeVersionSum(int major, int minor)
582    {
583        return (major * 10000) + minor;
584    }
585    
586    ///////////////////////////////////////////////////////////////////
587    ////                       private variables                   ////
588    
589    /** Pattern of directory name containing database. */
590    private final static Pattern _DB_PATTERN = Pattern.compile("db\\-(\\d+)\\.(\\d+)");
591}