added a few experimental alternatives implementations of string templates for string/html/jdbc etc for KT-1565 seems there's a few ways we could do it...

This commit is contained in:
James Strachan
2012-03-12 20:46:44 +00:00
parent 3748507ac1
commit f927b38a14
17 changed files with 563 additions and 25 deletions
@@ -8,11 +8,7 @@ import java.sql.*
fun <T> Connection.statement(block: (Statement) -> T): T { fun <T> Connection.statement(block: (Statement) -> T): T {
val statement = createStatement() val statement = createStatement()
if (statement != null) { if (statement != null) {
try { return statement.use(block)
return block(statement)
} finally {
statement.close()
}
} else { } else {
throw IllegalStateException("No Statement returned from $this") throw IllegalStateException("No Statement returned from $this")
} }
@@ -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 <T> 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()
}
}
@@ -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 <T> Statement.use(block : (Statement) -> T) : T {
try {
return block(this)
} finally {
close()
}
}
@@ -1,37 +1,38 @@
package test.kotlin.jdbc package test.kotlin.jdbc
import kotlin.jdbc.* import kotlin.jdbc. *
import kotlin.test.* import kotlin.test. *
import junit.framework.TestCase import junit.framework.TestCase
import org.h2.jdbcx.JdbcDataSource import org.h2.jdbcx.JdbcDataSource
import javax.sql.DataSource import javax.sql.DataSource
import org.h2.jdbcx.JdbcConnectionPool import org.h2.jdbcx.JdbcConnectionPool
fun createDataSource(): DataSource { val dataSource = createDataSource()
val pool = JdbcConnectionPool.create("jdbc:h2:mem:KotlinJdbcTest;DB_CLOSE_DELAY=-1", "user", "password")
if (pool == null) { 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") throw IllegalStateException("No DataSource created")
} else { } else {
return pool
}
}
class JdbcTest : TestCase() {
val dataSource = createDataSource()
fun testDatabase() {
dataSource.update("create table foo (id int primary key, name varchar(100))") 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 (1, 'James')"))
assertEquals(1, dataSource.update("insert into foo (id, name) values (2, 'Andrey')")) 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") { dataSource.query("select * from foo") {
for (row in it) { for (row in it) {
println("id ${row[1]} and name: ${row[2]}") println("id ${row[1]} and name: ${row[2]}")
} }
} }
}
fun testQueryWithNamedColumnAccess() {
// query using names // query using names
dataSource.query("select * from foo") { dataSource.query("select * from foo") {
for (row in it) { for (row in it) {
@@ -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 <T> 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 <T> 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<Binding>()
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)
}
*/
}
}
@@ -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
@@ -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<T>(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<HtmlBuilder>(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("&lt;")
else if (c == '>') output.append("&gt;")
else if (c == '&') output.append("&amp;")
else if (c == '"') output.append("&quot;")
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() ?: ""
}
@@ -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<String>, val expressions : Array<Any?>) {
/**
* 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("&lt;")
else if (c == '>') buffer.append("&gt;")
else if (c == '&') buffer.append("&amp;")
else if (c == '"') buffer.append("&quot;")
else buffer.append(c)
}
}
}
@@ -1,4 +1,4 @@
package kotlin.template package kotlin.template.experiment3
import kotlin.dom.toXmlString import kotlin.dom.toXmlString
import org.w3c.dom.Node import org.w3c.dom.Node
@@ -1,4 +1,4 @@
package kotlin.template package kotlin.template.experiment3
/** /**
* Creates a string template from a string with $ expressions inside. * Creates a string template from a string with $ expressions inside.
@@ -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("<h1>$foo</h1> <p>hey $bar</p>")
val actual = toHtml{
it.text("<h1>")
it.expression(foo)
it.text("</h1> <p>hey ")
it.expression(bar)
it.text("</p>")
}
assertEquals("<h1>James</h1> <p>hey x &gt; 1</p>", actual)
println("Generated HTML: $actual")
}
}
@@ -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)
}
}
@@ -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 = "<h1>$foo</h1> <p>hey $bar</p>".toHtml()
val actual = StringTemplate(array("<h1>", "</h1> <p>hey ", "</p>"), array(foo, bar)).toHtml()
assertEquals("<h1>James</h1> <p>hey x &gt; 1</p>", actual)
}
}
@@ -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)
}
}
@@ -1,9 +1,10 @@
package test.template package test.template
import junit.framework.TestCase import kotlin.template.experiment3.*
import kotlin.template.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import junit.framework.TestCase
class HtmlTemplateTest : TestCase() { class HtmlTemplateTest : TestCase() {
fun testTemplate(): Unit { fun testTemplate(): Unit {
val foo = "James" val foo = "James"
@@ -1,9 +1,10 @@
package test.template package test.template
import junit.framework.TestCase import kotlin.template.experiment3.*
import kotlin.template.StringTemplate
import kotlin.test.assertEquals import kotlin.test.assertEquals
import junit.framework.TestCase
class StringTemplateTest : TestCase() { class StringTemplateTest : TestCase() {
fun testTemplate(): Unit { fun testTemplate(): Unit {
val name = "James" val name = "James"
@@ -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<T>(val constantText : Array<String>, 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<StringBuilder>) : 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<StringBuilder>(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)
}
}
}