diff --git a/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/Connections.kt b/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/Connections.kt index d68e57355ac..c80e63f03c7 100644 --- a/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/Connections.kt +++ b/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/Connections.kt @@ -8,11 +8,7 @@ import java.sql.* fun Connection.statement(block: (Statement) -> T): T { val statement = createStatement() if (statement != null) { - try { - return block(statement) - } finally { - statement.close() - } + return statement.use(block) } else { throw IllegalStateException("No Statement returned from $this") } diff --git a/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/PreparedStatements.kt b/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/PreparedStatements.kt new file mode 100644 index 00000000000..5da19cb1adf --- /dev/null +++ b/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/PreparedStatements.kt @@ -0,0 +1,27 @@ +package kotlin.jdbc + +/** + * Helper method to process a statement on this collection + */ +import java.sql.PreparedStatement +import java.sql.ResultSet + +fun PreparedStatement.update(): Int { + try { + return this.executeUpdate() + } finally { + close() + } +} +fun PreparedStatement.query(block: (ResultSet) -> T): T { + try { + val resultSet = this.executeQuery() + if (resultSet == null) { + throw IllegalStateException("No ResultSet returned from $this") + } else { + return block(resultSet) + } + } finally { + close() + } +} \ No newline at end of file diff --git a/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/Statements.kt b/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/Statements.kt new file mode 100644 index 00000000000..9dbdfac5671 --- /dev/null +++ b/libraries/kotlin-jdbc/src/main/kotlin/kotlin/jdbc/Statements.kt @@ -0,0 +1,17 @@ +package kotlin.jdbc + +/** + * Helper method to process a statement on this collection + */ +import java.sql.Statement + +/** + * Uses the statement with the given block then closes the statement + */ +fun Statement.use(block : (Statement) -> T) : T { + try { + return block(this) + } finally { + close() + } +} diff --git a/libraries/kotlin-jdbc/src/test/kotlin/test/kotlin/jdbc/JdbcTest.kt b/libraries/kotlin-jdbc/src/test/kotlin/test/kotlin/jdbc/JdbcTest.kt index e27ab608958..4d86b3795c0 100644 --- a/libraries/kotlin-jdbc/src/test/kotlin/test/kotlin/jdbc/JdbcTest.kt +++ b/libraries/kotlin-jdbc/src/test/kotlin/test/kotlin/jdbc/JdbcTest.kt @@ -1,37 +1,38 @@ package test.kotlin.jdbc -import kotlin.jdbc.* -import kotlin.test.* +import kotlin.jdbc. * +import kotlin.test. * import junit.framework.TestCase import org.h2.jdbcx.JdbcDataSource import javax.sql.DataSource import org.h2.jdbcx.JdbcConnectionPool -fun createDataSource(): DataSource { - val pool = JdbcConnectionPool.create("jdbc:h2:mem:KotlinJdbcTest;DB_CLOSE_DELAY=-1", "user", "password") - if (pool == null) { +val dataSource = createDataSource() + +fun createDataSource() : DataSource { + val dataSource = JdbcConnectionPool.create("jdbc:h2:mem:KotlinJdbcTest;DB_CLOSE_DELAY=-1", "user", "password") + if (dataSource == null) { throw IllegalStateException("No DataSource created") } else { - return pool - } -} - -class JdbcTest : TestCase() { - val dataSource = createDataSource() - - fun testDatabase() { dataSource.update("create table foo (id int primary key, name varchar(100))") assertEquals(1, dataSource.update("insert into foo (id, name) values (1, 'James')")) assertEquals(1, dataSource.update("insert into foo (id, name) values (2, 'Andrey')")) - // lets query using integer lookups + return dataSource + } +} + +class JdbcTest : TestCase() { + fun testQueryWithIndexColumnAccess() { dataSource.query("select * from foo") { for (row in it) { println("id ${row[1]} and name: ${row[2]}") } } + } + fun testQueryWithNamedColumnAccess() { // query using names dataSource.query("select * from foo") { for (row in it) { diff --git a/libraries/kotlin-jdbc/src/test/kotlin/test/kotlin/jdbc/experiment1/JdbcTemplateTest.kt b/libraries/kotlin-jdbc/src/test/kotlin/test/kotlin/jdbc/experiment1/JdbcTemplateTest.kt new file mode 100644 index 00000000000..31a5b4447f8 --- /dev/null +++ b/libraries/kotlin-jdbc/src/test/kotlin/test/kotlin/jdbc/experiment1/JdbcTemplateTest.kt @@ -0,0 +1,95 @@ +package test.kotlin.jdbc.experiment1 + +import kotlin.template.experiment1.* +import kotlin.jdbc.* +import kotlin.test.* +import kotlin.util.arrayList + +import junit.framework.TestCase +import javax.sql.DataSource + +import test.kotlin.jdbc.createDataSource +import java.sql.Connection +import java.sql.PreparedStatement +import java.sql.ResultSet + +fun DataSource.update(fn: (PreparedStatementBuilder) -> Unit): Int { + return use{ it.update(fn) } +} + +fun DataSource.query(fn: (PreparedStatementBuilder) -> Unit, resultBlock: (ResultSet) -> T): T { + return use{ it.query(fn, resultBlock) } +} + +fun Connection.update(fn: (PreparedStatementBuilder) -> Unit): Int { + val preparedStatement = prepare(fn) + return preparedStatement.update() +} + + +fun Connection.query(fn: (PreparedStatementBuilder) -> Unit, resultBlock: (ResultSet) -> T): T { + val preparedStatement = prepare(fn) + return preparedStatement.query(resultBlock) +} + +fun Connection.prepare(fn: (PreparedStatementBuilder) -> Unit): PreparedStatement { + val builder = PreparedStatementBuilder(this) + fn(builder) + return builder.statement() +} + +trait Binding { + fun bind(statement: PreparedStatement): Unit +} +class PreparedStatementBuilder(val connection: Connection) { + val sql = StringBuilder() + val tasks = arrayList() + private var parameterIndex = 0 + + fun text(value: String): Unit { + // we assume all text is escaped already + sql.append(value) + } + + fun expression(value: String): Unit { + sql.append("?") + tasks.add(object: Binding { + override fun bind(statement : PreparedStatement) { + statement.setString(nextParameterIndex(), value) + } + }) + } + + // TODO bind other kinds! + + fun statement(): PreparedStatement { + val answer = connection.prepareStatement(sql.toString()) + if (answer == null) { + throw IllegalStateException("No PreparedStatement returned from $connection") + } else { + for (task in tasks) { + task.bind(answer) + } + return answer + } + } + + protected fun nextParameterIndex(): Int = ++parameterIndex +} + + +class JdbcTemplateTest : TestCase() { + //val dataSource = createDataSource() + + fun testTemplateInsert() { + val id = 1 + +/* + dataSource.update { + it.text("select * from foo where id = ") + it.expression(id) + } +*/ + + } +} diff --git a/libraries/stdlib/src/kotlin/template/ReadMe.md b/libraries/stdlib/src/kotlin/template/ReadMe.md new file mode 100644 index 00000000000..7e1b7b5e5e9 --- /dev/null +++ b/libraries/stdlib/src/kotlin/template/ReadMe.md @@ -0,0 +1,71 @@ +== Warning experimental APIs + +This package contains a couple of alternative experimental implementations of string templates. + +These are not yet integrated into the language, but are here to compare implementation details, +suitability, code size, complexity and efficiency. + +=== Aims + +* make it easy to generate various kinds of output such as text, text with internationalisation, HTML/XML, JSON, URL/URLs or make JDBC calls while reusing the same language templates with $ expressions + +=== Issues + +* how many objects are created per call? +* how extensible & easy to reuse the concept of String Templates in other library features +* pass context into the template somehow; so that formatting options (Locales, nullText, JDBC Connection, converters or whatnot) can be customized +* avoid where possible lots of "if (value is SomeType)" runtime checks to determine what encoding strategy should be used + + +==== experiment1 + +Here we convert a string template into a function on some kind of builder object where we invoke the text() method for static text and expression() for dynamic expressions + +Pro: + +* simple and efficient code is generated - no arrays +* no arrays are created for static and expression arguments +* static dispatch of encoding functions; no runtime instanceof checks on each argument + +* Cons: + +* function may create an inner object/class (though the compiler may get smart enough to inline some of those - or maybe generate a custom function for the whole thing with parameters?) +* we can only use this strategy if the template expression is passed to a function and we can detect the function parameter takes a fn: (T) -> Unit; and T has suitable text() and expression() methods. + (We can use extension methods to allow regular builder-like classes such as java.lang.StringBuilder to be used to minimise redundant object creation) - so the code generation is a bit more complex + +==== experiment2 + +Here we create a single StringTemplate object containing the constant text array and the values array. Then we use functions or extension functions as ways to do other things. + +Pros: + +* really simple +* no real compiler magic required where the type depends on the context + +Cons: + +* requires runtime instanceof checks on each expression value +* fair bit of object construction per call (2 arrays, a template object, then the string builder and lots of index lookup of arrays + +==== experiment 3 + +The idea is we create a FooTemplate object with the static constant strings inside; so we can easily cache the immutable stuff on startup. Then we basically use pseudo code like + + val template = StringTemplate("some ", " static ", "text") + ... + val builder = template.builder() + builder.expression(foo) + builder.expression(bar) + val answer = builder.build() + +Then based on the context - e.g. an annotation or a method parameter type or something; we determine which kind of template to create (StringTemplate, HtmlTemplate, JdbcTemplate etc) + +Pros: + +* fairly simple + +Cons: + +* quite a few different types (FooTemplate and FooBuilder) +* no way to pass in parameters to the FooBuilder class; for example the JDBC connection, or the Locale / NumberFormats to use for numeric formatting etc +* requires runtime instanceof checks on each expression value & array access etc diff --git a/libraries/stdlib/src/kotlin/template/experiment1/Templates.kt b/libraries/stdlib/src/kotlin/template/experiment1/Templates.kt new file mode 100644 index 00000000000..24ab5a0028d --- /dev/null +++ b/libraries/stdlib/src/kotlin/template/experiment1/Templates.kt @@ -0,0 +1,96 @@ +package kotlin.template.experiment1 + +import org.w3c.dom.Node +import kotlin.dom.toXmlString + +fun StringBuilder.text(value : String) : Unit { + this.append(value) +} + +fun StringBuilder.expression(value : Any?) : Unit { + this.append(value) +} + +/** + * Converts a [[StringTemplate]] to a String + */ +// TODO tried toString() but compiler error kicks in +fun format(fn : (StringBuilder) -> Unit) : String { + val builder = StringBuilder() + fn(builder) + return builder.toString() ?: "" +} + + +abstract class TextBuilder(val output: Appendable) { + +/* + */ +/** + * Converts the given string template to a String + + fun toString(fn: (T) -> Unit): String = toString(ToStringFormatter, fn) + + fun toString(formatter : ValueFormatter, fn: (T) -> Unit): String { + fn(this) + return output.toString() ?: "" + } +*/ + + fun text(value: String): Unit { + output.append(value) + } + +} + +class HtmlBuilder(output: Appendable) : TextBuilder(output) { + var nullText : String = "" + + fun expression(value: Node): Unit { + text(value.toXmlString()) + } + + fun expression(value: Any?): Unit { + if (value == null) { + output.append(nullText) + } else { + escape(value.toString() ?: "") + } + } + + fun escape(text : String) : Unit { + for (c in text) { + if (c == '<') output.append("<") + else if (c == '>') output.append(">") + else if (c == '&') output.append("&") + else if (c == '"') output.append(""") + else output.append(c) + } + } +} + + +/** + * Represents a formatter of values in a [[StringTemplate]] which understands how to escape + * different types for different kinds of language + */ +trait ValueFormatter { + fun format(buffer : Appendable, val value : Any?) : Unit +} + +object ToStringFormatter : ValueFormatter { + + fun toString() = "ToStringFormatter" + + override fun format(buffer : Appendable, value : Any?) { + val text = if (value == null) "null" else value.toString() + buffer.append(text) + } +} + +fun toHtml(fn: (HtmlBuilder) -> Unit): String { + val buffer = StringBuilder() + val html = HtmlBuilder(buffer) + fn(html) + return buffer.toString() ?: "" +} diff --git a/libraries/stdlib/src/kotlin/template/experiment2/Templates.kt b/libraries/stdlib/src/kotlin/template/experiment2/Templates.kt new file mode 100644 index 00000000000..f95d58ae6ae --- /dev/null +++ b/libraries/stdlib/src/kotlin/template/experiment2/Templates.kt @@ -0,0 +1,87 @@ +package kotlin.template.experiment2 + +import kotlin.dom.* +import org.w3c.dom.Node + + +/** + * Creates a string template from some constant string expressions and some dynamic expressions. + */ +class StringTemplate(val constantText : Array, val expressions : Array) { + + /** + * Converts the given string template to a String + */ + fun toString(): String = toString(ToStringFormatter) + + fun toString(formatter : ValueFormatter): String { + val buffer = StringBuilder() + append(buffer, formatter) + return buffer.toString() ?: "" + } + + fun append(buffer: Appendable, formatter: ValueFormatter): Unit { + val expressionSize = expressions.size + for (i in 0.upto(constantText.size - 1)) { + buffer.append(constantText[i]) + if (i < expressionSize) { + val value = expressions[i] + formatter.format(buffer, value) + } + } + } + + /** + * Converts the given template to HTML with an optional formatter + */ + fun toHtml(formatter: HtmlFormatter = HtmlFormatter()): String = toString(formatter) + + /** + * Appends the HTML representation of this template to the given appendable + */ + fun appendHtml(buffer: Appendable, formatter: HtmlFormatter = HtmlFormatter()): String = toString(formatter) +} + +/** + * Represents a formatter of values in a [[StringTemplate]] which understands how to escape + * different types for different kinds of language + */ +trait ValueFormatter { + fun format(buffer : Appendable, val value : Any?) : Unit +} + +object ToStringFormatter : ValueFormatter { + + fun toString() = "ToStringFormatter" + + override fun format(buffer : Appendable, value : Any?) { + val text = if (value == null) "null" else value.toString() + buffer.append(text) + } +} + +class HtmlFormatter : ValueFormatter { + var nullText : String = "" + + override fun format(buffer : Appendable, value : Any?) { + if (value == null) { + buffer.append(nullText) + } else if (value is StringTemplate) { + value.append(buffer, this) + } else if (value is Node) { + buffer.append(value.toXmlString()) + } else { + escape(buffer, value.toString() ?: "") + } + } + + fun escape(buffer : Appendable, text : String) : Unit { + for (c in text) { + if (c == '<') buffer.append("<") + else if (c == '>') buffer.append(">") + else if (c == '&') buffer.append("&") + else if (c == '"') buffer.append(""") + else buffer.append(c) + } + } +} diff --git a/libraries/stdlib/src/kotlin/template/HtmlTemplate.kt b/libraries/stdlib/src/kotlin/template/experiment3/HtmlTemplate.kt similarity index 98% rename from libraries/stdlib/src/kotlin/template/HtmlTemplate.kt rename to libraries/stdlib/src/kotlin/template/experiment3/HtmlTemplate.kt index b6853ce892f..224322d962b 100644 --- a/libraries/stdlib/src/kotlin/template/HtmlTemplate.kt +++ b/libraries/stdlib/src/kotlin/template/experiment3/HtmlTemplate.kt @@ -1,4 +1,4 @@ -package kotlin.template +package kotlin.template.experiment3 import kotlin.dom.toXmlString import org.w3c.dom.Node diff --git a/libraries/stdlib/src/kotlin/template/StringTemplate.kt b/libraries/stdlib/src/kotlin/template/experiment3/StringTemplate.kt similarity index 96% rename from libraries/stdlib/src/kotlin/template/StringTemplate.kt rename to libraries/stdlib/src/kotlin/template/experiment3/StringTemplate.kt index e9cdd89b411..d2df43952d0 100644 --- a/libraries/stdlib/src/kotlin/template/StringTemplate.kt +++ b/libraries/stdlib/src/kotlin/template/experiment3/StringTemplate.kt @@ -1,4 +1,4 @@ -package kotlin.template +package kotlin.template.experiment3 /** * Creates a string template from a string with $ expressions inside. diff --git a/libraries/testlib/test/template/experiment1/HtmlTemplateTest.kt b/libraries/testlib/test/template/experiment1/HtmlTemplateTest.kt new file mode 100644 index 00000000000..7ee0377f8ef --- /dev/null +++ b/libraries/testlib/test/template/experiment1/HtmlTemplateTest.kt @@ -0,0 +1,31 @@ +package template.experiment1 + +import kotlin.template.experiment1.* +import kotlin.test.assertEquals +import kotlin.dom.toXmlString + +import junit.framework.TestCase +import org.w3c.dom.Node + + +class HtmlTemplateTest : TestCase() { + fun testTemplate(): Unit { + val foo = "James" + val bar = "x > 1" + + // Code generated by the following template expression: + // + // val actual = toHtml("

$foo

hey $bar

") + val actual = toHtml{ + it.text("

") + it.expression(foo) + it.text("

hey ") + it.expression(bar) + it.text("

") + } + + assertEquals("

James

hey x > 1

", actual) + + println("Generated HTML: $actual") + } +} \ No newline at end of file diff --git a/libraries/testlib/test/template/experiment1/StringTemplateTest.kt b/libraries/testlib/test/template/experiment1/StringTemplateTest.kt new file mode 100644 index 00000000000..a02e2f7bb23 --- /dev/null +++ b/libraries/testlib/test/template/experiment1/StringTemplateTest.kt @@ -0,0 +1,24 @@ +package template.experiment1 + +import kotlin.template.experiment1.* +import kotlin.test.assertEquals + +import junit.framework.TestCase + +class StringTemplateTest : TestCase() { + fun testTemplate() : Unit { + val name = "James" + + // Code generated by the following template expression: + // + // val actual = format("hello $name!") + val actual = format{ + it.text("hello ") + it.expression(name) + it.text("!") + } + + println("Got text: $actual") + assertEquals("hello James!", actual) + } +} diff --git a/libraries/testlib/test/template/experiment2/HtmlTemplateTest.kt b/libraries/testlib/test/template/experiment2/HtmlTemplateTest.kt new file mode 100644 index 00000000000..458150fb321 --- /dev/null +++ b/libraries/testlib/test/template/experiment2/HtmlTemplateTest.kt @@ -0,0 +1,20 @@ +package test.template.experiment + +import kotlin.template.experiment2.* +import kotlin.test.assertEquals + +import junit.framework.TestCase + +class HtmlTemplateTest : TestCase() { + fun testTemplate(): Unit { + val foo = "James" + val bar = "x > 1" + + // Code generated by the following template expression: + // + // val actual = "

$foo

hey $bar

".toHtml() + val actual = StringTemplate(array("

", "

hey ", "

"), array(foo, bar)).toHtml() + + assertEquals("

James

hey x > 1

", actual) + } +} \ No newline at end of file diff --git a/libraries/testlib/test/template/experiment2/StringTemplateTest.kt b/libraries/testlib/test/template/experiment2/StringTemplateTest.kt new file mode 100644 index 00000000000..48d7a7c2538 --- /dev/null +++ b/libraries/testlib/test/template/experiment2/StringTemplateTest.kt @@ -0,0 +1,19 @@ +package test.template.experiment + +import kotlin.template.experiment2.* +import kotlin.test.assertEquals + +import junit.framework.TestCase + +class StringTemplateTest : TestCase() { + fun testTemplate(): Unit { + val name = "James" + + // Code generated by the following template expression: + // + // val actual = "hello $name!".toString() + val actual = StringTemplate(array("hello ", "!"), array(name)).toString() + + assertEquals("hello James!", actual) + } +} \ No newline at end of file diff --git a/libraries/testlib/test/template/HtmlTemplateTest.kt b/libraries/testlib/test/template/experiment3/HtmlTemplateTest.kt similarity index 94% rename from libraries/testlib/test/template/HtmlTemplateTest.kt rename to libraries/testlib/test/template/experiment3/HtmlTemplateTest.kt index f31b8d4cd37..2604eb44f88 100644 --- a/libraries/testlib/test/template/HtmlTemplateTest.kt +++ b/libraries/testlib/test/template/experiment3/HtmlTemplateTest.kt @@ -1,9 +1,10 @@ package test.template -import junit.framework.TestCase -import kotlin.template.* +import kotlin.template.experiment3.* import kotlin.test.assertEquals +import junit.framework.TestCase + class HtmlTemplateTest : TestCase() { fun testTemplate(): Unit { val foo = "James" diff --git a/libraries/testlib/test/template/StringTemplateTest.kt b/libraries/testlib/test/template/experiment3/StringTemplateTest.kt similarity index 92% rename from libraries/testlib/test/template/StringTemplateTest.kt rename to libraries/testlib/test/template/experiment3/StringTemplateTest.kt index 2bd12e7291e..fea979d128b 100644 --- a/libraries/testlib/test/template/StringTemplateTest.kt +++ b/libraries/testlib/test/template/experiment3/StringTemplateTest.kt @@ -1,9 +1,10 @@ package test.template -import junit.framework.TestCase -import kotlin.template.StringTemplate +import kotlin.template.experiment3.* import kotlin.test.assertEquals +import junit.framework.TestCase + class StringTemplateTest : TestCase() { fun testTemplate(): Unit { val name = "James" diff --git a/libraries/testlib/test/template/experiment4/Experiment4.kt b/libraries/testlib/test/template/experiment4/Experiment4.kt new file mode 100644 index 00000000000..05db2fa0f2a --- /dev/null +++ b/libraries/testlib/test/template/experiment4/Experiment4.kt @@ -0,0 +1,52 @@ +package template.experiment4 + +import junit.framework.TestCase +import kotlin.test.assertEquals +import kotlin.test.todo + + +/** + * Creates a string template from some constant string expressions and some dynamic expressions. + */ +class StringTemplate(val constantText : Array, val fn : (T) -> Unit) { +} + +fun StringBuilder.text(value : String) : Unit { + this.append(value) +} + +fun StringBuilder.expression(value : Any?) : Unit { + this.append(value) +} + +/** + * Converts a [[StringTemplate]] to a String + */ +fun toString(template : StringTemplate) : String { + val builder = StringBuilder() + template.fn(builder) + return builder.toString() ?: "" +} + +class StringTemplateTest : TestCase() { + fun testTemplate() : Unit { + val name = "James" + + // Code generated by the following template expression: + // + // val actual = "hello $name!" + val template = StringTemplate(array("hello ", "!"), { (it : StringBuilder) -> + println("About to apply function!") + it.text("hello ") + it.expression(name) + it.text("!") + }) + println("Template has constants ${template.constantText.toList()} and function ${template.fn}") + + todo { + val actual = toString(template) + + assertEquals("hello James!", actual) + } + } +} \ No newline at end of file