/** * @fileoverview Fields and methods of the jala.db package. */ // Define the global namespace for Jala modules if (!global.jala) { global.jala = {}; } /** * HelmaLib dependencies */ app.addRepository("modules/helma/Database.js"); /** * Namespace declarations */ jala.db = { "metadata": {} }; /** * Static helper method that converts the object passed as argument * to a connection property string. * @param {Object} props The property object to convert * @returns A connection property string * @type String * @private */ jala.db.getPropertyString = function(props) { if (props != null) { res.push(); for (var i in props) { res.write(";"); res.write(i); res.write("="); res.write(props[i]); } return res.pop(); } return null; }; /** * Returns an array of table names. The optional patterns can contain * "_" for matching a single character or "%" for any character sequence. * @param {java.sql.DatabaseMetaData} dbMetadata The metadata to use for retrieval. * @param {String} tablePattern An optional table name pattern (defaults to "%") * @param {String} schemaPattern An optional schema name pattern (defaults to "%") * @returns An array containing the table names * @type Array */ jala.db.metadata.getTableNames = function(dbMetadata, tablePattern, schemaPattern) { var result = []; var tableMeta = null; try { tableMeta = dbMetadata.getTables(null, (schemaPattern || "%"), (tablePattern || "%"), null); while (tableMeta.next()) { result.push(tableMeta.getString("TABLE_NAME")); } return result; } finally { if (tableMeta !== null) { tableMeta.close(); } } return null; }; /** * Returns an array containing table metadata. * @param {java.sql.DatabaseMetaData} dbMetadata The metadata to use for retrieval * @param {String} tablePattern Optional table name pattern * @param {String} schemaPattern Optional schema name pattern * @returns An array containing table metadata. Each one is represented by a * javascript object containing the following properties: * * @type Array */ jala.db.metadata.getTables = function(dbMetadata, tablePattern, schemaPattern) { var result = []; var tableMeta = null; try { tableMeta = dbMetadata.getTables(null, (schemaPattern || "%"), (tablePattern || "%"), null); while (tableMeta.next()) { var tableName = tableMeta.getString("TABLE_NAME"); var schemaName = tableMeta.getString("TABLE_SCHEM") || null; result.push({ "name": tableName, "schema": schemaName, "columns": jala.db.metadata.getColumns(dbMetadata, tableName, schemaName), "keys": jala.db.metadata.getPrimaryKeys(dbMetadata, tableName, schemaName) }); } return result; } finally { if (tableMeta != null) { tableMeta.close(); } } return null; }; /** * Returns the column metadata of a table (or multiple tables, if a tableName * pattern matching several tables is specified). * @param {java.sql.DatabaseMetaData} dbMetadata The metadata to use for retrieval * @param {String} tablePattern Optional table name pattern * @param {String} schemaPattern Optional schema name pattern * @param {String} columnPattern Optional column name pattern * @returns An array containing column metadata. Each one is represented by a * javascript object containing the following properties: * * @type Array */ jala.db.metadata.getColumns = function(dbMetadata, tablePattern, schemaPattern, columnPattern) { var result = []; var columnMeta = null; try { columnMeta = dbMetadata.getColumns(null, schemaPattern || null, tablePattern || null, columnPattern || "%"); while (columnMeta.next()) { result.push({ "name": columnMeta.getString("COLUMN_NAME"), "type": columnMeta.getInt("DATA_TYPE"), "length": columnMeta.getInt("COLUMN_SIZE"), "nullable": (columnMeta.getInt("NULLABLE") == dbMetadata.typeNoNulls) ? false : true, "default": columnMeta.getString("COLUMN_DEF"), "precision": columnMeta.getInt("DECIMAL_DIGITS"), "scale": columnMeta.getInt("NUM_PREC_RADIX") }); } return result; } finally { if (columnMeta != null) { columnMeta.close(); } } return null; }; /** * Returns an array containing the primary key names of the specified table. * @param {java.sql.DatabaseMetaData} dbMetadata The metadata to use for retrieval * @param {String} tableName The name of the table * @param {String} schemaName Optional name of the schema * @returns An array containing the primary key column names * @type Array */ jala.db.metadata.getPrimaryKeys = function(dbMetadata, tableName, schemaName) { var result = []; var keyMeta = null; try { // retrieve the primary keys of the table var keyMeta = dbMetadata.getPrimaryKeys(null, schemaName || null, tableName); while (keyMeta.next()) { result.push(keyMeta.getString("COLUMN_NAME")); } return result; } finally { if (keyMeta != null) { keyMeta.close(); } } return null; }; /** * Returns the table metadata of the given database. The optional patterns * can contain "_" for matching a single character or "%" for any * character sequence. * @param {helma.Database} database The database to connect to * @param {String} schemaPattern An optional schema name pattern * @param {String} tablePattern An optional table name pattern * @returns An array containing the metadata of all matching tables (see {@link #getTables}) * @type Array */ jala.db.getTableMetadata = function(database, tablePattern, schemaPattern) { var conn = null; try { conn = database.getConnection(); return jala.db.metadata.getTables(conn.getMetaData(), tablePattern, schemaPattern); } finally { if (conn != null) { conn.close(); } } return null; }; /***************************************** *** D A T A B A S E S E R V E R *** *****************************************/ /** * Returns a new Server instance. * @class Instances of this class represent a H2 database listener that * allows multiple databases to be accessed via tcp. *
Important: You need the h2.jar in directory "lib/ext" * of your helma installation for this library to work, which you can get * at http://www.h2database.com/. * @param {helma.File} baseDir The directory where the database files * are located or should be stored * @param {Number} port The port to listen on (defaults to 9001) * @param {Boolean} createOnDemand If true this server will create non-existing * databases on-the-fly, if false it only accepts connections to already * existing databases in the given base directory * @param {Boolean} makePublic If true this database is reachable from outside, * if false it's only reachable from localhost * @param {Boolean} useSsl If true SSL will be used to encrypt the connection * @returns A newly created Server instance * @constructor */ jala.db.Server = function(baseDir, port) { /** * Private variable containing the h2 server instance * @type org.h2.tools.Server * @private */ var server = null; /** * An object containing configuration properties used when creating * the server instance * @private */ var config = { "baseDir": baseDir.getAbsolutePath(), "tcpPort": port || 9092, "tcpSSL": false, "ifExists": true, "tcpAllowOthers": false, "log": false }; /** * Returns the wrapped database server instance * @returns The wrapped database server * @type org.h2.tools.Server * @private */ this.getServer = function() { return server; }; /** * Returns the directory used by this server instance * @returns The directory where the databases used by this * server are located in * @type helma.File */ this.getDirectory = function() { return baseDir; }; /** * Returns the port this server listens on * @returns The port this server listens on * @type Number */ this.getPort = function() { return config.tcpPort; }; /** * Returns the config of this server * @returns The config of this server * @private */ this.getConfig = function() { return config; }; /** * Starts the database server. * @returns True in case the server started successfully, false otherwise * @type Boolean */ this.start = function() { if (server != null && server.isRunning(true)) { throw "jala.db.Server: already listening on port " + this.getPort(); } // convert properties into an array var config = this.getConfig(); var args = []; for (var propName in config) { args.push("-" + propName); args.push(config[propName].toString()); } // create the server instance server = Packages.org.h2.tools.Server.createTcpServer(args); try { server.start(); } catch (e) { app.logger.error("jala.db.Server: unable to start server, reason: " + e); return false; } app.logger.info("jala.db.Server: listening on localhost:" + this.getPort()); return true; }; return this; }; /** @ignore */ jala.db.Server.prototype.toString = function() { return "[Jala Database Server]"; }; /** * Stops the database server. * @returns True if stopping the server was successful, false otherwise * @type Boolean */ jala.db.Server.prototype.stop = function() { try { this.getServer().stop(); app.logger.info("jala.db.Server: stopped listening on localhost:" + this.getPort()); } catch (e) { app.logger.error("jala.db.Server: Unable to stop, reason: " + e); return false; } return true; }; /** * Returns true if the database server is running. * @returns True if the database server is running * @type Boolean */ jala.db.Server.prototype.isRunning = function() { return this.getServer().isRunning(true); }; /** * Toggles the use of Ssl encryption within this server. This should be set * before starting the server. * @param {Boolean} bool If true SSL encryption will be used, false * otherwise. If no argument is given, this method returns the * current setting. * @returns The current setting if no argument is given, or void * @type Boolean */ jala.db.Server.prototype.useSsl = function(bool) { var config = this.getConfig(); if (bool != null) { config.tcpSSL = (bool === true); } else { return config.tcpSSL; } return; }; /** * If called with boolean true as argument, this server creates databases * on-the-fly, otherwise it only accepts connections to already existing * databases. This should be set before starting the server. * @param {Boolean} bool If true this server creates non-existing databases * on demand, if false it only allows connections to existing databases. * If no argument is given, this method returns the current setting. * @returns The current setting if no argument is given, or void * @type Boolean */ jala.db.Server.prototype.createOnDemand = function(bool) { var config = this.getConfig(); if (bool != null) { config.ifExists = (bool === false); } else { return !config.ifExists; } return; }; /** * If called with boolean true as argument, this server accepts connections * from outside localhost. This should be set before starting the server. * @param {Boolean} bool If true this server accepts connections from outside * localhost. If no argument is given, this method returns the current setting. * @returns The current setting if no argument is given, or void * @type Boolean */ jala.db.Server.prototype.isPublic = function(bool) { var config = this.getConfig(); if (bool != null) { config.tcpAllowOthers = (bool === true); } else { return config.tcpAllowOthers; } return; }; /** * Returns the JDBC Url to use for connections to a given database. * @param {String} name An optional name of a database running * @param {Object} props Optional connection properties to add * @returns The JDBC Url to use for connecting to a database * within this sever * @type String */ jala.db.Server.prototype.getUrl = function(name, props) { res.push(); res.write("jdbc:h2:"); res.write(this.useSsl() ? "ssl" : "tcp"); res.write("://localhost:"); res.write(this.getPort()); res.write("/"); res.write(name); res.write(jala.db.getPropertyString(props)) return res.pop(); }; /** * Returns a properties object containing the connection properties * of the database with the given name. * @param {String} name The name of the database * @param {String} username Optional username to use for this connection * @param {String} password Optional password to use for this connection * @param {Object} props An optional parameter object containing * connection properties to add to the connection Url. * @returns A properties object containing the connection properties * @type helma.util.ResourceProperties */ jala.db.Server.prototype.getProperties = function(name, username, password, props) { var rp = new Packages.helma.util.ResourceProperties(); rp.put(name + ".url", this.getUrl(name, props)); rp.put(name + ".driver", "org.h2.Driver"); rp.put(name + ".user", username || "sa"); rp.put(name + ".password", password || ""); return rp; }; /** * Returns a connection to a database within this server. * @param {String} name The name of the database running * within this server * @param {String} username Optional username to use for this connection * @param {String} password Optional password to use for this connection * @param {Object} props An optional parameter object * containing connection properties to add to the connection Url. * @returns A connection to the specified database * @type helma.Database */ jala.db.Server.prototype.getConnection = function(name, username, password, props) { var rp = this.getProperties(name, username, password, props); var dbSource = new Packages.helma.objectmodel.db.DbSource(name, rp); return new helma.Database(dbSource); }; /***************************** *** D A T A T Y P E *** *****************************/ /** * Returns a newly created DataType instance. * @class Instances of this class represent a data type. Each instance * contains the code number as defined in java.sql.Types, the name of * the data type as defined in java.sql.Types and optional creation parameters * allowed for this data type. * @param {Number} type The sql code number of this data type * @param {String} typeName The type name of this data type, as used within sql statements * @param {String} params Optional creation parameters allowed for this data type. * @returns A newly created instance of DataType. * @constructor * @private */ jala.db.DataType = function(type, typeName, params) { /** * Returns the sql type code number as defined in java.sql.Types * @returns The sql type code number of this data type * @type Number */ this.getType = function() { return type; }; /** * Returns the type name of this data type, which can be * used in sql queries. * @returns The type name of this data type * @type String */ this.getTypeName = function() { return typeName; }; /** * Returns the creation parameter string of this data type * @returns The creation parameter string of this data type * @type String */ this.getParams = function() { return params; }; /** @ignore */ this.toString = function() { return "[DataType " + " CODE: " + code + ", SQL: " + sqlType + ", PARAMS: " + params + "]"; }; return this; }; /** * Returns true if values for this data type should be surrounded * by (single) quotes. * @returns True if values for this data type should be surrounded * by quotes, false if not * @type Boolean */ jala.db.DataType.prototype.needsQuotes = function() { switch (this.getType()) { case java.sql.Types.CHAR: case java.sql.Types.VARCHAR: case java.sql.Types.LONGVARCHAR: case java.sql.Types.BINARY: case java.sql.Types.VARBINARY: case java.sql.Types.LONGVARBINARY: case java.sql.Types.DATE: case java.sql.Types.TIME: case java.sql.Types.TIMESTAMP: return true; default: return false; } }; /*********************************** *** R A M D A T A B A S E *** ***********************************/ /** * Returns a newly created RamDatabase instance. * @class Instances of this class represent an in-memory sql database. *
Important: You need the h2.jar in directory "lib/ext" * of your helma installation for this library to work, which you can get * at http://www.h2database.com/. * @param {String} name The name of the database. If not given a private * un-named database is created, that can only be accessed through this instance * of jala.db.RamDatabase * @param {String} username Optional username (defaults to "sa"). This username * is used when creating the database, so the same should be used when * creating subsequent instances of jala.db.RamDatabase pointing to a named * database. * @param {String} password Optional password (defaults to ""). * @returns A newly created instance of RamDatabase * @constructor */ jala.db.RamDatabase = function(name, username, password) { /** * Returns the name of the database * @returns The name of the database * @type String */ this.getName = function() { return name; }; /** * Returns the username of this database * @returns The username of this database * @type String */ this.getUsername = function() { return username || "sa"; }; /** * Returns the password of this database * @returns The password of this database * @type String */ this.getPassword = function() { return password || ""; }; return; }; /** @ignore */ jala.db.RamDatabase.prototype.toString = function() { return "[Jala RamDatabase " + this.getName() + "]"; } /** * Returns the JDBC Url to connect to this database * @param {Object} props Optional connection properties to add * @returns The JDBC url to use for connecting to this database * @type String */ jala.db.RamDatabase.prototype.getUrl = function(props) { var url = "jdbc:h2:" + this.getDatabasePath(); if (props != null) { url += jala.db.getPropertyString(props); } return url; }; /** * Returns the path of this database, which is used by jala.db.Server * when adding the database to its set of hosted databases. * @returns The path of this database within a server instance * @type String * @private */ jala.db.RamDatabase.prototype.getDatabasePath = function() { return "mem:" + this.getName(); } /** * Returns a properties object containing the connection properties * for this database. * @param {Object} props An optional parameter object containing * connection properties to add to the connection Url. * @returns A properties object containing the connection properties * @type helma.util.ResourceProperties */ jala.db.RamDatabase.prototype.getProperties = function(props) { var name = this.getName(); var rp = new Packages.helma.util.ResourceProperties(); rp.put(name + ".url", this.getUrl(props)); rp.put(name + ".driver", "org.h2.Driver"); rp.put(name + ".user", this.getUsername()); rp.put(name + ".password", this.getPassword()); return rp; }; /** * Returns a connection to this database * @param {Object} An optional parameter object containing connection * properties to add to the connection Url. * @returns A connection to this database * @type helma.Database */ jala.db.RamDatabase.prototype.getConnection = function(props) { var name = this.getName(); var rp = this.getProperties(props); var dbSource = new Packages.helma.objectmodel.db.DbSource(name, rp); return new helma.Database(dbSource); }; /** * Stops this in-process database by issueing a "SHUTDOWN" sql command. */ jala.db.RamDatabase.prototype.shutdown = function() { var conn = this.getConnection(); conn.execute("SHUTDOWN"); return; }; /** * Creates a table in this database. * @param {String} name The name of the table * @param {Array} columns The columns to create in the table. Each column * must be described using an object containing the following properties: * * @param {String} primaryKey The name of the column that contains * the primary key * @private */ jala.db.RamDatabase.prototype.createTable = function(tableName, columns, primaryKey) { res.push(); res.write("CREATE TABLE "); res.write(tableName); res.write(" ("); var column, dataType, params; for (var i=0;i 0) { res.write("("); res.write(arr.join(",")); res.write(")"); } } if (column["default"]) { res.write(" DEFAULT "); if (dataType.needsQuotes() === true) { res.write("'"); res.write(column["default"]); res.write("'"); } else { res.write(column["default"]); } } if (column.nullable === false) { res.write(" NOT NULL"); } if (i < columns.length - 1) { res.write(", "); } } if (primaryKey != null) { res.write(", PRIMARY KEY ("); if (primaryKey instanceof Array) { res.write(primaryKey.join(", ")); } else { res.write(primaryKey); } res.write(")"); } res.write(")"); var sql = res.pop(); try { var conn = this.getConnection(); conn.execute(sql); app.logger.info("Successfully created table " + tableName); app.logger.debug("Sql statement used: " + sql); return true; } catch (e) { app.logger.error("Unable to create table " + tableName + ", reason: " + e); return false; } }; /** * Drops the table with the given name * @param {String} tableName The name of the table * @returns True if the table was successfully dropped, false otherwise * @type Boolean */ jala.db.RamDatabase.prototype.dropTable = function(tableName) { var conn = this.getConnection(); var sql = "DROP TABLE " + tableName; conn.execute(sql); return; }; /** * Returns true if the table exists already in the database * @param {String} name The name of the table * @returns True if the table exists, false otherwise * @type Boolean */ jala.db.RamDatabase.prototype.tableExists = function(name) { var conn = this.getConnection().getConnection(); var meta = conn.getMetaData(); var t = meta.getTables(null, "PUBLIC", "%", null); var tableName; try { while (t.next()) { tableName = t.getString(3).toUpperCase(); if (tableName.toLowerCase() === name.toLowerCase()) { return true; } } return false; } finally { if (t != null) { t.close(); } } }; /** * Copies all tables in the database passed as argument into this embedded database. * If any of the tables already exists in this database, they will be removed before * re-created. Please mind that this method ignores any indexes in the source database, * but respects the primary key settings. * @param {helma.Database} database The database to copy the tables from * @param {Array} tables An optional array containing the names of the tables to copy. * If not given all tables are copied */ jala.db.RamDatabase.prototype.copyTables = function(database, tables) { // retrieve the metadata for all tables in this schema var conn = null; try { conn = database.getConnection(); var dbMetadata = conn.getMetaData(); if (tables === null || tables === undefined) { // no tables specified, so copy all available tables = jala.db.metadata.getTableNames(dbMetadata); } for each (var tableName in tables) { // drop the table if it exists if (this.tableExists(tableName)) { this.dropTable(tableName); } // retrieve the table metadata and create the table var metadata = jala.db.metadata.getTables(dbMetadata, tableName); if (metadata !== null && metadata.length > 0) { this.createTable(metadata[0].name, metadata[0].columns, metadata[0].keys); } } } finally { if (conn != null) { conn.close(); } } return; }; /** * Returns an array containing all available data types. * @returns All available data types * @type Array * @see jala.db.DataType * @private */ jala.db.RamDatabase.prototype.getDataTypes = function() { // data types are cached for performance reasons if (!arguments.callee.cache) { // java.sql data types arguments.callee.cache = []; var con = this.getConnection().getConnection(); var meta = con.getMetaData(); var rs = meta.getTypeInfo(); var code, name, params; while (rs.next()) { code = rs.getInt("DATA_TYPE"); name = rs.getString("TYPE_NAME"); params = rs.getString("CREATE_PARAMS"); arguments.callee.cache.push(new jala.db.DataType(code, name, params)); } } return arguments.callee.cache; }; /** * Returns the data type for the code passed as argument * @param {Number} type The type code as defined in java.sql.Types * @returns The data type object for the code * @type jala.db.DataType * @private */ jala.db.RamDatabase.prototype.getDataType = function(type) { var types = this.getDataTypes(); var dataType; for (var i=0;iImportant: You need the h2.jar in directory "lib/ext" * of your helma installation for this library to work, which you can get * at http://www.h2database.com/. * @param {String} name The name of the database. This name is used as * prefix for all database files * @param {helma.File} directory The directory where the database files * should be stored in. * @param {String} username Optional username (defaults to "sa"). This username * is used when creating the database, so the same should be used when * creating subsequent instances of jala.db.FileDatabase pointing to the * same database * @param {String} password Optional password (defaults to ""). * @returns A newly created FileDatabase instance * @constructor */ jala.db.FileDatabase = function(name, directory, username, password) { /** * Returns the name of the database. This name is used as prefix * for all files of this database in the specified directory * @returns The name of the database * @type String */ this.getName = function() { return name; }; /** * Returns the directory where the database files are stored. * @returns The directory where this database is stored. * @type helma.File */ this.getDirectory = function() { return directory; }; /** * Returns the username of this database * @returns The username of this database * @type String */ this.getUsername = function() { return username || "sa"; }; /** * Returns the password of this database * @returns The password of this database * @type String */ this.getPassword = function() { return password || ""; }; if (!name || typeof(name) != "string" || !directory || !(directory instanceof helma.File)) { throw "jala.db.FileDatabase: Missing or invalid arguments" } else if (!directory.exists()) { throw "jala.db.FileDatabase: directory '" + directory + "' does not exist"; } return this; }; // extend RamDatabase jala.db.FileDatabase.prototype = new jala.db.RamDatabase(); /** @ignore */ jala.db.FileDatabase.prototype.toString = function() { return "[Jala FileDatabase '" + this.getName() + "' in " + this.getDirectory().getAbsolutePath() + "]"; }; /** * Returns the path of this database, which is used when adding * the database to a server instance. * @returns The path of this database within a server instance * @type String * @private */ jala.db.FileDatabase.prototype.getDatabasePath = function() { var directory = new helma.File(this.getDirectory(), this.getName()); return "file:" + directory.getAbsolutePath(); }; /** * Deletes all files of this database on disk. Note that this also * closes the database before removing it. * @returns True in case the database was removed successfully, false otherwise * @type Boolean */ jala.db.FileDatabase.prototype.remove = function() { var directory = this.getDirectory(); try { // shut down the database this.shutdown(); Packages.org.h2.tools.DeleteDbFiles.execute( directory.getAbsolutePath(), this.getName(), false ); } catch(e) { app.logger.error("jala.db: Unable to delete database in " + directory.getAbsolutePath() + ", reason: " + e); return false; } return true; }; /** * Creates a backup of this database, using the file passed as argument. The * result will be a zipped file containing the database files * @param {helma.File} file The file to write the backup to * @returns True if the database backup was created successfully, false otherwise * @type Boolean */ jala.db.FileDatabase.prototype.backup = function(file) { try { Packages.org.h2.tools.Backup.execute( file.getAbsolutePath(), this.getDirectory().getAbsolutePath(), this.getName(), false ); } catch (e) { app.logger.error("jala.db: Unable to backup database to '" + file.getAbsolutePath() + ", reason: " + e); return false; } return true; }; /** * Restores this database using a backup on disk. * @param {helma.File} backupFile The backup file to use for restore * @returns True if the database was successfully restored, false otherwise * @type Boolean */ jala.db.FileDatabase.prototype.restore = function(backupFile) { try { Packages.org.h2.tools.Restore.execute( backupFile.getAbsolutePath(), this.getDirectory().getAbsolutePath(), this.getName(), false ); } catch (e) { app.logger.error("jala.db: Unable to restore database using '" + backupFile.getAbsolutePath() + ", reason: " + e); return false; } return true; };