/** * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.appjet.ajstdlib; import scala.collection.mutable.ArrayBuffer; import java.sql.{DriverManager, SQLException, Statement}; import net.appjet.oui.{profiler, config, NoninheritedDynamicVariable}; import com.mchange.v2.c3p0._; class SQLBase(driverClass: String, url: String, userName: String, password: String) { def isMysql:Boolean = (url.startsWith("jdbc:mysql:")); def isDerby:Boolean = (url.startsWith("jdbc:derby:")); if (isDerby) { System.setProperty("derby.system.home", config.derbyHome); val f = new java.io.File(config.derbyHome); if (! f.exists) { if (! f.mkdirs()) throw new RuntimeException("Couldn't create internal database storage directory: "+config.derbyHome); } if (! f.isDirectory) throw new RuntimeException("Internal database storage directory is not a directory: "+config.derbyHome); if (! f.canWrite) throw new RuntimeException("Can't write to internal database storage directory: "+config.derbyHome); } val cpds = new ComboPooledDataSource(); cpds.setDriverClass(driverClass); cpds.setJdbcUrl(url+(if (isMysql) "?useUnicode=true&characterEncoding=utf8" else "")); // derby does not require a password if (!isDerby) { cpds.setUser(userName); cpds.setPassword(password); } cpds.setMaxPoolSize(config.jdbcPoolSize); cpds.setMaxConnectionAge(6*60*60); // 6 hours if (config.devMode) { cpds.setAutomaticTestTable("cpds_testtable"); cpds.setTestConnectionOnCheckout(true); } // { // // register db driver // try { // new JDCConnectionDriver(driverClass, url+"?useUnicode=true&characterEncoding=utf8", userName, password); // } catch { // case e => { // e.printStackTrace(); // Runtime.getRuntime.halt(1); // } // } // } private def getConnectionFromPool = { val c = cpds.getConnection(); c.setAutoCommit(true); c; } // Creates a dynamic variable whose .value depends on the innermost // .withValue(){} on the call-stack. private val currentConnection = new NoninheritedDynamicVariable[Option[java.sql.Connection]](None); def withConnection[A](block: java.sql.Connection=>A): A = { currentConnection.value match { case Some(c) => { block(c); } case None => { val t1 = profiler.time; val c = getConnectionFromPool; profiler.recordCumulative("getConnection", profiler.time-t1); try { currentConnection.withValue(Some(c)) { block(c); } } finally { c.close; } } } } private val currentlyInTransaction = new NoninheritedDynamicVariable(false); def inTransaction[A](block: java.sql.Connection=>A): A = { withConnection(c => { if (currentlyInTransaction.value) { return block(c); } else { currentlyInTransaction.withValue(true) { c.setAutoCommit(false); c.setTransactionIsolation(java.sql.Connection.TRANSACTION_REPEATABLE_READ); try { val result = block(c); c.commit(); c.setAutoCommit(true); result; } catch { case e@net.appjet.oui.AppGeneratedStopException => { c.commit(); c.setAutoCommit(true); throw e; } case (e:org.mozilla.javascript.WrappedException) if (e.getWrappedException == net.appjet.oui.AppGeneratedStopException) => { c.commit(); c.setAutoCommit(true); throw e; } case e => { //println("inTransaction() caught error:"); //e.printStackTrace(); try { c.rollback(); c.setAutoCommit(true); } catch { case ex => { println("Could not rollback transaction because: "+ex.toString()); } } throw e; } } } } }); } def closing[A](closable: java.sql.Statement)(block: =>A): A = { try { block } finally { closable.close(); } } def closing[A](closable: java.sql.ResultSet)(block: =>A): A = { try { block } finally { closable.close(); } } def tableName(t: String) = id(t); val identifierQuoteString = withConnection(_.getMetaData.getIdentifierQuoteString); def quoteIdentifier(s: String) = identifierQuoteString+s+identifierQuoteString; private def id(s: String) = quoteIdentifier(s); def longTextType = if (isDerby) "CLOB" else "MEDIUMTEXT"; // derby seems to do things intelligently w.r.t. case-sensitivity and unicode support. def createTableOptions = if (isMysql) " ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin" else ""; // creates table if it doesn't exist already def createJSONTable(table: String) { withConnection { c=> val s = c.createStatement; if (! doesTableExist(c, table)) { closing(s) { s.execute("CREATE TABLE "+tableName(table)+" ("+ id("ID")+" VARCHAR(128) PRIMARY KEY NOT NULL, "+ id("JSON")+" "+longTextType+" NOT NULL"+ ")"+createTableOptions); } } } } // requires: table exists // returns null if key doesn't exist def getJSON(table: String, key: String): String = { withConnection { c=> val s = c.prepareStatement("SELECT "+id("JSON")+" FROM "+tableName(table)+" WHERE "+id("ID")+" = ?"); closing(s) { s.setString(1, key); var resultSet = s.executeQuery(); closing(resultSet) { if (! resultSet.next()) { null; } else { resultSet.getString(1); } } } } } def getAllJSON(table: String, start: Int, count: Int): Array[Object] = { withConnection { c => val s = c.prepareStatement("SELECT "+id("ID")+","+id("JSON")+" FROM "+tableName(table)+ " ORDER BY "+id("ID")+" DESC"+ " LIMIT ? OFFSET ?"); closing(s) { s.setInt(2, start); s.setInt(1, count); var resultSet = s.executeQuery(); var output = new ArrayBuffer[Object]; closing(resultSet) { while (resultSet.next()) { output += new { val id = resultSet.getString(1); val value = resultSet.getString(2) }; } output.toArray; } } } } def getAllJSONKeys(table: String): Array[String] = { withConnection { c => val s = c.prepareStatement("SELECT "+id("ID")+" FROM "+tableName(table)); closing(s) { var resultSet = s.executeQuery(); var output = new ArrayBuffer[String]; closing(resultSet) { while (resultSet.next()) { output += resultSet.getString(1); } output.toArray; } } } } // requires: table exists // inserts key if it doesn't exist def putJSON(table: String, key: String, json: String) { withConnection { c=> val update = c.prepareStatement("UPDATE "+tableName(table)+" SET "+id("JSON")+"=? WHERE "+id("ID")+"=?"); closing(update) { update.setString(1, json); update.setString(2, key); update.executeUpdate(); if (update.getUpdateCount == 0) { val insert = c.prepareStatement( "INSERT INTO "+tableName(table)+" ("+id("ID")+", "+id("JSON")+") values (?,?)"); closing(insert) { insert.setString(1, key); insert.setString(2, json); insert.executeUpdate(); } } } } } def deleteJSON(table: String, key: String) { // requires: table exists withConnection { c=> val update = c.prepareStatement("DELETE FROM "+tableName(table)+" WHERE "+id("ID")+"=?"); closing(update) { update.setString(1, key); update.executeUpdate(); } } } private def metaName(table: String) = table+"_META"; private def metaTableName(table: String) = tableName(metaName(table)); private def textTableName(table: String) = tableName(table+"_TEXT"); private def escapeSearchString(dbm: java.sql.DatabaseMetaData, s: String): String = { val e = dbm.getSearchStringEscape(); s.replace("_", e+"_").replace("%", e+"%"); } private final val PAGE_SIZE = 20; def doesTableExist(connection: java.sql.Connection, table: String): Boolean = { val databaseMetadata = connection.getMetaData; val tables = databaseMetadata.getTables(null, null, escapeSearchString(databaseMetadata, table), null); closing(tables) { tables.next(); } } def autoIncrementClause = if (isDerby) "GENERATED BY DEFAULT AS IDENTITY" else "AUTO_INCREMENT"; // creates table if it doesn't exist already def createStringArrayTable(table: String) { withConnection { c=> if (! doesTableExist(c, metaName(table))) { // check to see if the *_META table exists // create tables and indices val s = c.createStatement; closing(s) { s.execute("CREATE TABLE "+metaTableName(table)+" ("+ id("ID")+" VARCHAR(128) PRIMARY KEY NOT NULL, "+ id("NUMID")+" INT UNIQUE "+autoIncrementClause+" "+ ")"+createTableOptions); val defaultOffsets = (1 to PAGE_SIZE).map(x=>"").mkString(","); s.execute("CREATE TABLE "+textTableName(table)+" ("+ ""+id("NUMID")+" INT, "+id("PAGESTART")+" INT, "+id("OFFSETS")+" VARCHAR(256) NOT NULL DEFAULT '"+defaultOffsets+ "', "+id("DATA")+" "+longTextType+" NOT NULL"+ ")"+createTableOptions); s.execute("CREATE INDEX "+id(table+"-NUMID-PAGESTART")+" ON "+textTableName(table)+"("+id("NUMID")+", "+id("PAGESTART")+")"); } } } } // requires: table exists // returns: null if key or (key,index) doesn't exist, else the value def getStringArrayElement(table: String, key: String, index: Int): String = { val (pageStart, offset) = getPageStartAndOffset(index); val page = new StringArrayPage(table, key, pageStart, true); page.data(offset); } // requires: table exists // returns: an array of the mappings present in the page that should hold the // particular (key,index) mapping. the array may be empty or otherwise not // contain the given (key,index). def getPageStringArrayElements(table: String, key: String, index: Int): Array[IndexValueMapping] = { val (pageStart, offset) = getPageStartAndOffset(index); val page = new StringArrayPage(table, key, pageStart, true); val buf = new scala.collection.mutable.ListBuffer[IndexValueMapping]; for(i <- 0 until page.data.length) { val s = page.data(i); if (s ne null) { val n = pageStart + i; buf += IndexValueMapping(n, s); } } buf.toArray; } // requires: table exists // creates key if doesn't exist // value may be null def putStringArrayElement(table: String, key: String, index: Int, value: String) { val (pageStart, offset) = getPageStartAndOffset(index); val page = new StringArrayPage(table, key, pageStart, false); page.data(offset) = value; page.updateDB(); } def putMultipleStringArrayElements(table: String, key: String): Multiputter = new Multiputter { var currentPage = None:Option[StringArrayPage]; def flushPage() { if (currentPage.isDefined) { val page = currentPage.get; page.updateDB(); currentPage = None; } } def finish() { flushPage(); } def put(index: Int, value: String) { try { val (pageStart, offset) = getPageStartAndOffset(index); if (currentPage.isEmpty || currentPage.get.pageStart != pageStart) { flushPage(); currentPage = Some(new StringArrayPage(table, key, pageStart, false)); } currentPage.get.data(offset) = value; } catch { case e => { e.printStackTrace; throw e } } } } trait Multiputter { def put(index: Int, value: String); def finish(); } case class IndexValueMapping(index: Int, value: String); def clearStringArray(table: String, key: String) { withConnection { c=> val numid = getStringArrayNumId(c, table, key, false); if (numid >= 0) { { val s = c.prepareStatement("DELETE FROM "+textTableName(table)+" WHERE "+id("NUMID")+"=?"); closing(s) { s.setInt(1, numid); s.executeUpdate(); } } { val s = c.prepareStatement("DELETE FROM "+metaTableName(table)+" WHERE "+id("NUMID")+"=?"); closing(s) { s.setInt(1, numid); s.executeUpdate(); } } } } } private def getPageStartAndOffset(index: Int): (Int,Int) = { val pageStart = (index / PAGE_SIZE) * PAGE_SIZE; (pageStart, index - pageStart); } // requires: table exists // returns: numid of new string array private def newStringArray(c: java.sql.Connection, table: String, key: String): Int = { val s = c.prepareStatement("INSERT INTO "+metaTableName(table)+" ("+id("ID")+") VALUES (?)", Statement.RETURN_GENERATED_KEYS); closing(s) { s.setString(1, key); s.executeUpdate(); val resultSet = s.getGeneratedKeys; if (resultSet == null) error("No generated numid for insert"); closing(resultSet) { if (! resultSet.next()) error("No generated numid for insert"); resultSet.getInt(1); } } } def getStringArrayNumId(c: java.sql.Connection, table: String, key: String, creating: Boolean): Int = { val s = c.prepareStatement("SELECT "+id("NUMID")+" FROM "+metaTableName(table)+" WHERE "+id("ID")+"=?"); closing(s) { s.setString(1, key); val resultSet = s.executeQuery(); closing(resultSet) { if (! resultSet.next()) { if (creating) { newStringArray(c, table, key); } else { -1 } } else { resultSet.getInt(1); } } } } def getStringArrayAllKeys(table: String): Array[String] = { withConnection { c=> val s = c.prepareStatement("SELECT "+id("ID")+" FROM "+metaTableName(table)); closing(s) { val resultSet = s.executeQuery(); closing(resultSet) { val buf = new ArrayBuffer[String]; while (resultSet.next()) { buf += resultSet.getString(1); } buf.toArray; } } } } private class StringArrayPage(table: String, key: String, val pageStart: Int, readonly: Boolean) { val data = new Array[String](PAGE_SIZE); private val numid = withConnection { c=> val nid = getStringArrayNumId(c, table, key, ! readonly); if (nid >= 0) { val s = c.prepareStatement( "SELECT "+id("OFFSETS")+","+id("DATA")+" FROM "+textTableName(table)+" WHERE "+id("NUMID")+"=? AND "+id("PAGESTART")+"=?"); closing(s) { s.setInt(1, nid); s.setInt(2, pageStart); val resultSet = s.executeQuery(); closing(resultSet) { if (! resultSet.next()) { if (! readonly) { val insert = c.prepareStatement("INSERT INTO "+textTableName(table)+ " ("+id("NUMID")+", "+id("PAGESTART")+", "+id("DATA")+") VALUES (?,?,'')"); closing(insert) { insert.setInt(1, nid); insert.setInt(2, pageStart); insert.executeUpdate(); } } } else { val offsetsField = resultSet.getString(1); val dataField = resultSet.getString(2); val offsetStrings = offsetsField.split(",", -1); var i = 0; var idx = 0; while (i < PAGE_SIZE) { val nstr = offsetStrings(i); if (nstr != "") { val n = nstr.toInt; data(i) = dataField.substring(idx, idx+n); idx += n; } i += 1; } } } } } nid; } def updateDB() { if (readonly) { error("this is a readonly StringArrayPage"); } // assert: the relevant row of the TEXT table exists if (data.find(_ ne null).isEmpty) { withConnection { c=> val update = c.prepareStatement("DELETE FROM "+textTableName(table)+ " WHERE "+id("NUMID")+"=? AND "+id("PAGESTART")+"=?"); closing(update) { update.setInt(1, numid); update.setInt(2, pageStart); update.executeUpdate(); } } } else { val offsetsStr = data.map(s => if (s eq null) "" else s.length.toString).mkString(","); val dataStr = data.map(s => if (s eq null) "" else s).mkString(""); withConnection { c=> val s = c.prepareStatement("UPDATE "+textTableName(table)+ " SET "+id("OFFSETS")+"=?, "+id("DATA")+"=? WHERE "+id("NUMID")+"=? AND "+id("PAGESTART")+"=?"); closing(s) { s.setString(1, offsetsStr); s.setString(2, dataStr); s.setInt(3, numid); s.setInt(4, pageStart); s.executeUpdate(); } } } } } def close { if (isDerby) { cpds.close(); try { DriverManager.getConnection("jdbc:derby:;shutdown=true"); } catch { case e: SQLException => if (e.getErrorCode() != 50000) throw e } } } }