From 0f9a587ec3a9cd372f0b37f069848201236d2acf Mon Sep 17 00:00:00 2001 From: Alex Dupre Date: Mon, 7 Mar 2016 09:13:09 +0100 Subject: [PATCH 01/26] Add SSL support.. SSL is disabled by default to avoid POLA violations. It is possible to enable and control SSL behavior via url parameters: - `sslmode=` enable ssl (prefer/require/verify-ca/verify-full [recommended]) - `sslrootcert=` specifies trusted certificates (JDK cacert if missing) Client certificate authentication is not implemented, due to lack of time and interest, but it should be easy to add. --- .../mauricio/async/db/Configuration.scala | 2 + .../mauricio/async/db/SSLConfiguration.scala | 31 ++++++++ .../db/postgresql/codec/MessageDecoder.scala | 12 ++- .../db/postgresql/codec/MessageEncoder.scala | 3 +- .../codec/PostgreSQLConnectionHandler.scala | 59 ++++++++++++++- .../encoders/SSLMessageEncoder.scala | 16 ++++ .../encoders/StartupMessageEncoder.scala | 6 +- .../messages/backend/SSLResponseMessage.scala | 3 + .../messages/backend/ServerMessage.scala | 1 - .../frontend/InitialClientMessage.scala | 3 + .../messages/frontend/SSLRequestMessage.scala | 5 ++ .../messages/frontend/StartupMessage.scala | 4 +- .../async/db/postgresql/util/ParserURL.scala | 25 +++++-- .../async/db/postgresql/util/URLParser.scala | 10 +-- .../db/postgresql/DatabaseTestHelper.scala | 14 +++- .../db/postgresql/MessageDecoderSpec.scala | 2 +- .../PostgreSQLSSLConnectionSpec.scala | 51 +++++++++++++ .../db/postgresql/util/URLParserSpec.scala | 20 ++++- script/prepare_build.sh | 43 +++++++---- script/server.crt | 75 +++++++++++++++++++ script/server.key | 27 +++++++ 21 files changed, 364 insertions(+), 48 deletions(-) create mode 100644 db-async-common/src/main/scala/com/github/mauricio/async/db/SSLConfiguration.scala create mode 100644 postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/SSLMessageEncoder.scala create mode 100644 postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/SSLResponseMessage.scala create mode 100644 postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/InitialClientMessage.scala create mode 100644 postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/SSLRequestMessage.scala create mode 100644 postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLSSLConnectionSpec.scala create mode 100644 script/server.crt create mode 100644 script/server.key diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala index 841999e1..b032ac02 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala @@ -37,6 +37,7 @@ object Configuration { * @param port database port, defaults to 5432 * @param password password, defaults to no password * @param database database name, defaults to no database + * @param ssl ssl configuration * @param charset charset for the connection, defaults to UTF-8, make sure you know what you are doing if you * change this * @param maximumMessageSize the maximum size a message from the server could possibly have, this limits possible @@ -55,6 +56,7 @@ case class Configuration(username: String, port: Int = 5432, password: Option[String] = None, database: Option[String] = None, + ssl: SSLConfiguration = SSLConfiguration(), charset: Charset = Configuration.DefaultCharset, maximumMessageSize: Int = 16777216, allocator: ByteBufAllocator = PooledByteBufAllocator.DEFAULT, diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/SSLConfiguration.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/SSLConfiguration.scala new file mode 100644 index 00000000..9ae657fe --- /dev/null +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/SSLConfiguration.scala @@ -0,0 +1,31 @@ +package com.github.mauricio.async.db + +import java.io.File + +import SSLConfiguration.Mode + +/** + * + * Contains the SSL configuration necessary to connect to a database. + * + * @param mode whether and with what priority a SSL connection will be negotiated, default disabled + * @param rootCert path to PEM encoded trusted root certificates, None to use internal JDK cacerts, defaults to None + * + */ +case class SSLConfiguration(mode: Mode.Value = Mode.Disable, rootCert: Option[java.io.File] = None) + +object SSLConfiguration { + + object Mode extends Enumeration { + val Disable = Value("disable") // only try a non-SSL connection + val Prefer = Value("prefer") // first try an SSL connection; if that fails, try a non-SSL connection + val Require = Value("require") // only try an SSL connection, but don't verify Certificate Authority + val VerifyCA = Value("verify-ca") // only try an SSL connection, and verify that the server certificate is issued by a trusted certificate authority (CA) + val VerifyFull = Value("verify-full") // only try an SSL connection, verify that the server certificate is issued by a trusted CA and that the server host name matches that in the certificate + } + + def apply(properties: Map[String, String]): SSLConfiguration = SSLConfiguration( + mode = Mode.withName(properties.get("sslmode").getOrElse("disable")), + rootCert = properties.get("sslrootcert").map(new File(_)) + ) +} diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageDecoder.scala index 8a3d9fa5..5f210f72 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageDecoder.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageDecoder.scala @@ -17,7 +17,7 @@ package com.github.mauricio.async.db.postgresql.codec import com.github.mauricio.async.db.postgresql.exceptions.{MessageTooLongException} -import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage +import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, SSLResponseMessage} import com.github.mauricio.async.db.postgresql.parsers.{AuthenticationStartupParser, MessageParsersRegistry} import com.github.mauricio.async.db.util.{BufferDumper, Log} import java.nio.charset.Charset @@ -31,15 +31,21 @@ object MessageDecoder { val DefaultMaximumSize = 16777216 } -class MessageDecoder(charset: Charset, maximumMessageSize : Int = MessageDecoder.DefaultMaximumSize) extends ByteToMessageDecoder { +class MessageDecoder(sslEnabled: Boolean, charset: Charset, maximumMessageSize : Int = MessageDecoder.DefaultMaximumSize) extends ByteToMessageDecoder { import MessageDecoder.log private val parser = new MessageParsersRegistry(charset) + private var sslChecked = false + override def decode(ctx: ChannelHandlerContext, b: ByteBuf, out: java.util.List[Object]): Unit = { - if (b.readableBytes() >= 5) { + if (sslEnabled & !sslChecked) { + val code = b.readByte() + sslChecked = true + out.add(new SSLResponseMessage(code == 'S')) + } else if (b.readableBytes() >= 5) { b.markReaderIndex() diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageEncoder.scala index 5cf5d480..30195a11 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageEncoder.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageEncoder.scala @@ -44,12 +44,13 @@ class MessageEncoder(charset: Charset, encoderRegistry: ColumnEncoderRegistry) e override def encode(ctx: ChannelHandlerContext, msg: AnyRef, out: java.util.List[Object]) = { val buffer = msg match { + case SSLRequestMessage => SSLMessageEncoder.encode() + case message: StartupMessage => startupEncoder.encode(message) case message: ClientMessage => { val encoder = (message.kind: @switch) match { case ServerMessage.Close => CloseMessageEncoder case ServerMessage.Execute => this.executeEncoder case ServerMessage.Parse => this.openEncoder - case ServerMessage.Startup => this.startupEncoder case ServerMessage.Query => this.queryEncoder case ServerMessage.PasswordMessage => this.credentialEncoder case _ => throw new EncoderNotAvailableException(message) diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionHandler.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionHandler.scala index b53821ee..733cc5d1 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionHandler.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionHandler.scala @@ -17,6 +17,7 @@ package com.github.mauricio.async.db.postgresql.codec import com.github.mauricio.async.db.Configuration +import com.github.mauricio.async.db.SSLConfiguration.Mode import com.github.mauricio.async.db.column.{ColumnDecoderRegistry, ColumnEncoderRegistry} import com.github.mauricio.async.db.postgresql.exceptions._ import com.github.mauricio.async.db.postgresql.messages.backend._ @@ -38,6 +39,12 @@ import com.github.mauricio.async.db.postgresql.messages.backend.RowDescriptionMe import com.github.mauricio.async.db.postgresql.messages.backend.ParameterStatusMessage import io.netty.channel.socket.nio.NioSocketChannel import io.netty.handler.codec.CodecException +import io.netty.handler.ssl.{SslContextBuilder, SslHandler} +import io.netty.handler.ssl.util.InsecureTrustManagerFactory +import io.netty.util.concurrent.FutureListener +import javax.net.ssl.{SSLParameters, TrustManagerFactory} +import java.security.KeyStore +import java.io.FileInputStream object PostgreSQLConnectionHandler { final val log = Log.get[PostgreSQLConnectionHandler] @@ -79,7 +86,7 @@ class PostgreSQLConnectionHandler override def initChannel(ch: channel.Channel): Unit = { ch.pipeline.addLast( - new MessageDecoder(configuration.charset, configuration.maximumMessageSize), + new MessageDecoder(configuration.ssl.mode != Mode.Disable, configuration.charset, configuration.maximumMessageSize), new MessageEncoder(configuration.charset, encoderRegistry), PostgreSQLConnectionHandler.this) } @@ -120,13 +127,61 @@ class PostgreSQLConnectionHandler } override def channelActive(ctx: ChannelHandlerContext): Unit = { - ctx.writeAndFlush(new StartupMessage(this.properties)) + if (configuration.ssl.mode == Mode.Disable) + ctx.writeAndFlush(new StartupMessage(this.properties)) + else + ctx.writeAndFlush(SSLRequestMessage) } override def channelRead0(ctx: ChannelHandlerContext, msg: Object): Unit = { msg match { + case SSLResponseMessage(supported) => + if (supported) { + val ctxBuilder = SslContextBuilder.forClient() + if (configuration.ssl.mode >= Mode.VerifyCA) { + configuration.ssl.rootCert.fold { + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + val ks = KeyStore.getInstance(KeyStore.getDefaultType()) + val cacerts = new FileInputStream(System.getProperty("java.home") + "/lib/security/cacerts") + try { + ks.load(cacerts, "changeit".toCharArray) + } finally { + cacerts.close() + } + tmf.init(ks) + ctxBuilder.trustManager(tmf) + } { path => + ctxBuilder.trustManager(path) + } + } else { + ctxBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE) + } + val sslContext = ctxBuilder.build() + val sslEngine = sslContext.newEngine(ctx.alloc(), configuration.host, configuration.port) + if (configuration.ssl.mode >= Mode.VerifyFull) { + val sslParams = sslEngine.getSSLParameters() + sslParams.setEndpointIdentificationAlgorithm("HTTPS") + sslEngine.setSSLParameters(sslParams) + } + val handler = new SslHandler(sslEngine) + ctx.pipeline().addFirst(handler) + handler.handshakeFuture.addListener(new FutureListener[channel.Channel]() { + def operationComplete(future: io.netty.util.concurrent.Future[channel.Channel]) { + if (future.isSuccess()) { + ctx.writeAndFlush(new StartupMessage(properties)) + } else { + connectionDelegate.onError(future.cause()) + } + } + }) + } else if (configuration.ssl.mode < Mode.Require) { + ctx.writeAndFlush(new StartupMessage(properties)) + } else { + connectionDelegate.onError(new IllegalArgumentException("SSL is not supported on server")) + } + case m: ServerMessage => { (m.kind : @switch) match { diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/SSLMessageEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/SSLMessageEncoder.scala new file mode 100644 index 00000000..aeec7435 --- /dev/null +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/SSLMessageEncoder.scala @@ -0,0 +1,16 @@ +package com.github.mauricio.async.db.postgresql.encoders + +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled + +object SSLMessageEncoder { + + def encode(): ByteBuf = { + val buffer = Unpooled.buffer() + buffer.writeInt(8) + buffer.writeShort(1234) + buffer.writeShort(5679) + buffer + } + +} diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/StartupMessageEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/StartupMessageEncoder.scala index b8c97843..206fd2d3 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/StartupMessageEncoder.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/StartupMessageEncoder.scala @@ -21,13 +21,11 @@ import com.github.mauricio.async.db.util.ByteBufferUtils import java.nio.charset.Charset import io.netty.buffer.{Unpooled, ByteBuf} -class StartupMessageEncoder(charset: Charset) extends Encoder { +class StartupMessageEncoder(charset: Charset) { //private val log = Log.getByName("StartupMessageEncoder") - override def encode(message: ClientMessage): ByteBuf = { - - val startup = message.asInstanceOf[StartupMessage] + def encode(startup: StartupMessage): ByteBuf = { val buffer = Unpooled.buffer() buffer.writeInt(0) diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/SSLResponseMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/SSLResponseMessage.scala new file mode 100644 index 00000000..905ab688 --- /dev/null +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/SSLResponseMessage.scala @@ -0,0 +1,3 @@ +package com.github.mauricio.async.db.postgresql.messages.backend + +case class SSLResponseMessage(supported: Boolean) diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ServerMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ServerMessage.scala index c413ef4e..1fa5b9a2 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ServerMessage.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ServerMessage.scala @@ -43,7 +43,6 @@ object ServerMessage { final val Query = 'Q' final val RowDescription = 'T' final val ReadyForQuery = 'Z' - final val Startup = '0' final val Sync = 'S' } diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/InitialClientMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/InitialClientMessage.scala new file mode 100644 index 00000000..228c5e65 --- /dev/null +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/InitialClientMessage.scala @@ -0,0 +1,3 @@ +package com.github.mauricio.async.db.postgresql.messages.frontend + +trait InitialClientMessage diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/SSLRequestMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/SSLRequestMessage.scala new file mode 100644 index 00000000..c3bf84ff --- /dev/null +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/SSLRequestMessage.scala @@ -0,0 +1,5 @@ +package com.github.mauricio.async.db.postgresql.messages.frontend + +import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage + +object SSLRequestMessage extends InitialClientMessage diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/StartupMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/StartupMessage.scala index e4bb34c4..bb53390f 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/StartupMessage.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/StartupMessage.scala @@ -16,6 +16,4 @@ package com.github.mauricio.async.db.postgresql.messages.frontend -import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage - -class StartupMessage(val parameters: List[(String, Any)]) extends ClientMessage(ServerMessage.Startup) \ No newline at end of file +class StartupMessage(val parameters: List[(String, Any)]) extends InitialClientMessage diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ParserURL.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ParserURL.scala index ce5fa180..8172877e 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ParserURL.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ParserURL.scala @@ -16,28 +16,37 @@ object ParserURL { val PGPORT = "port" val PGDBNAME = "database" val PGHOST = "host" - val PGUSERNAME = "username" + val PGUSERNAME = "user" val PGPASSWORD = "password" val DEFAULT_PORT = "5432" - private val pgurl1 = """(jdbc:postgresql):(?://([^/:]*|\[.+\])(?::(\d+))?)?(?:/([^/?]*))?(?:\?user=(.*)&password=(.*))?""".r - private val pgurl2 = """(postgres|postgresql)://(.*):(.*)@(.*):(\d+)/(.*)""".r + private val pgurl1 = """(jdbc:postgresql):(?://([^/:]*|\[.+\])(?::(\d+))?)?(?:/([^/?]*))?(?:\?(.*))?""".r + private val pgurl2 = """(postgres|postgresql)://(.*):(.*)@(.*):(\d+)/([^/?]*)(?:\?(.*))?""".r def parse(connectionURL: String): Map[String, String] = { val properties: Map[String, String] = Map() + def parseOptions(optionsStr: String): Map[String, String] = + optionsStr.split("&").map { o => + o.span(_ != '=') match { + case (name, value) => name -> value.drop(1) + } + }.toMap + connectionURL match { - case pgurl1(protocol, server, port, dbname, username, password) => { + case pgurl1(protocol, server, port, dbname, params) => { var result = properties if (server != null) result += (PGHOST -> unwrapIpv6address(server)) if (dbname != null && dbname.nonEmpty) result += (PGDBNAME -> dbname) - if(port != null) result += (PGPORT -> port) - if(username != null) result = (result + (PGUSERNAME -> username) + (PGPASSWORD -> password)) + if (port != null) result += (PGPORT -> port) + if (params != null) result ++= parseOptions(params) result } - case pgurl2(protocol, username, password, server, port, dbname) => { - properties + (PGHOST -> unwrapIpv6address(server)) + (PGPORT -> port) + (PGDBNAME -> dbname) + (PGUSERNAME -> username) + (PGPASSWORD -> password) + case pgurl2(protocol, username, password, server, port, dbname, params) => { + var result = properties + (PGHOST -> unwrapIpv6address(server)) + (PGPORT -> port) + (PGDBNAME -> dbname) + (PGUSERNAME -> username) + (PGPASSWORD -> password) + if (params != null) result ++= parseOptions(params) + result } case _ => { logger.warn(s"Connection url '$connectionURL' could not be parsed.") diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala index f39f24ac..debcb6d9 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala @@ -16,14 +16,11 @@ package com.github.mauricio.async.db.postgresql.util -import com.github.mauricio.async.db.Configuration +import com.github.mauricio.async.db.{Configuration, SSLConfiguration} import java.nio.charset.Charset object URLParser { - private val Username = "username" - private val Password = "password" - import Configuration.Default def parse(url: String, @@ -35,11 +32,12 @@ object URLParser { val port = properties.get(ParserURL.PGPORT).getOrElse(ParserURL.DEFAULT_PORT).toInt new Configuration( - username = properties.get(Username).getOrElse(Default.username), - password = properties.get(Password), + username = properties.get(ParserURL.PGUSERNAME).getOrElse(Default.username), + password = properties.get(ParserURL.PGPASSWORD), database = properties.get(ParserURL.PGDBNAME), host = properties.getOrElse(ParserURL.PGHOST, Default.host), port = port, + ssl = SSLConfiguration(properties), charset = charset ) diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/DatabaseTestHelper.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/DatabaseTestHelper.scala index 40b35549..2659d372 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/DatabaseTestHelper.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/DatabaseTestHelper.scala @@ -18,10 +18,12 @@ package com.github.mauricio.async.db.postgresql import com.github.mauricio.async.db.util.Log import com.github.mauricio.async.db.{Connection, Configuration} +import java.io.File import java.util.concurrent.{TimeoutException, TimeUnit} -import scala.Some import scala.concurrent.duration._ import scala.concurrent.{Future, Await} +import com.github.mauricio.async.db.SSLConfiguration +import com.github.mauricio.async.db.SSLConfiguration.Mode object DatabaseTestHelper { val log = Log.get[DatabaseTestHelper] @@ -54,6 +56,16 @@ trait DatabaseTestHelper { withHandler(this.timeTestConfiguration, fn) } + def withSSLHandler[T](mode: SSLConfiguration.Mode.Value, host: String = "localhost", rootCert: Option[File] = Some(new File("script/server.crt")))(fn: (PostgreSQLConnection) => T): T = { + val config = new Configuration( + host = host, + port = databasePort, + username = "postgres", + database = databaseName, + ssl = SSLConfiguration(mode = mode, rootCert = rootCert)) + withHandler(config, fn) + } + def withHandler[T](configuration: Configuration, fn: (PostgreSQLConnection) => T): T = { val handler = new PostgreSQLConnection(configuration) diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/MessageDecoderSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/MessageDecoderSpec.scala index 14f0bed2..a033e3ee 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/MessageDecoderSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/MessageDecoderSpec.scala @@ -27,7 +27,7 @@ import java.util class MessageDecoderSpec extends Specification { - val decoder = new MessageDecoder(CharsetUtil.UTF_8) + val decoder = new MessageDecoder(false, CharsetUtil.UTF_8) "message decoder" should { diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLSSLConnectionSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLSSLConnectionSpec.scala new file mode 100644 index 00000000..2e38adbb --- /dev/null +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLSSLConnectionSpec.scala @@ -0,0 +1,51 @@ +package com.github.mauricio.async.db.postgresql + +import org.specs2.mutable.Specification +import com.github.mauricio.async.db.SSLConfiguration.Mode +import javax.net.ssl.SSLHandshakeException + +class PostgreSQLSSLConnectionSpec extends Specification with DatabaseTestHelper { + + "ssl handler" should { + + "connect to the database in ssl without verifying CA" in { + + withSSLHandler(Mode.Require, "127.0.0.1", None) { handler => + handler.isReadyForQuery must beTrue + } + + } + + "connect to the database in ssl verifying CA" in { + + withSSLHandler(Mode.VerifyCA, "127.0.0.1") { handler => + handler.isReadyForQuery must beTrue + } + + } + + "connect to the database in ssl verifying CA and hostname" in { + + withSSLHandler(Mode.VerifyFull) { handler => + handler.isReadyForQuery must beTrue + } + + } + + "throws exception when CA verification fails" in { + + withSSLHandler(Mode.VerifyCA, rootCert = None) { handler => + } must throwA[SSLHandshakeException] + + } + + "throws exception when hostname verification fails" in { + + withSSLHandler(Mode.VerifyFull, "127.0.0.1") { handler => + } must throwA[SSLHandshakeException] + + } + + } + +} diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala index 1e542f52..d0df6eaa 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala @@ -18,6 +18,8 @@ package com.github.mauricio.async.db.postgresql.util import org.specs2.mutable.Specification import com.github.mauricio.async.db.Configuration +import com.github.mauricio.async.db.SSLConfiguration +import com.github.mauricio.async.db.SSLConfiguration.Mode class URLParserSpec extends Specification { @@ -68,8 +70,20 @@ class URLParserSpec extends Specification { configuration.port === 9987 } - "create a connection from a heroku like URL using 'postgres' protocol" in { - val connectionUri = "postgres://john:doe@128.567.54.90:9987/my_database" + "create a connection with SSL enabled" in { + val connectionUri = "jdbc:postgresql://128.567.54.90:9987/my_database?sslmode=verify-full" + + val configuration = URLParser.parse(connectionUri) + configuration.username === Configuration.Default.username + configuration.password === None + configuration.database === Some("my_database") + configuration.host === "128.567.54.90" + configuration.port === 9987 + configuration.ssl.mode === Mode.VerifyFull + } + + "create a connection with SSL enabled and root CA from a heroku like URL using 'postgresql' protocol" in { + val connectionUri = "postgresql://john:doe@128.567.54.90:9987/my_database?sslmode=verify-ca&sslrootcert=server.crt" val configuration = URLParser.parse(connectionUri) configuration.username === "john" @@ -77,6 +91,8 @@ class URLParserSpec extends Specification { configuration.database === Some("my_database") configuration.host === "128.567.54.90" configuration.port === 9987 + configuration.ssl.mode === Mode.VerifyCA + configuration.ssl.rootCert.map(_.getPath) === Some("server.crt") } "create a connection with the available fields and named server" in { diff --git a/script/prepare_build.sh b/script/prepare_build.sh index 96aa8345..068ab389 100755 --- a/script/prepare_build.sh +++ b/script/prepare_build.sh @@ -1,5 +1,7 @@ #!/usr/bin/env sh +SCRIPTDIR=`dirname $0` + echo "Preparing MySQL configs" mysql -u root -e 'create database mysql_async_tests;' mysql -u root -e "create table mysql_async_tests.transaction_test (id varchar(255) not null, primary key (id))" @@ -10,26 +12,35 @@ mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'mysql_async_nopw'@'localhost' echo "preparing postgresql configs" -psql -c 'create database netty_driver_test;' -U postgres -psql -c 'create database netty_driver_time_test;' -U postgres -psql -c "alter database netty_driver_time_test set timezone to 'GMT'" -U postgres -psql -c "create table transaction_test ( id varchar(255) not null, constraint id_unique primary key (id))" -U postgres netty_driver_test -psql -c "CREATE USER postgres_md5 WITH PASSWORD 'postgres_md5'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_md5;" -U postgres -psql -c "CREATE USER postgres_cleartext WITH PASSWORD 'postgres_cleartext'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_cleartext;" -U postgres -psql -c "CREATE USER postgres_kerberos WITH PASSWORD 'postgres_kerberos'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_kerberos;" -U postgres -psql -d "netty_driver_test" -c "CREATE TYPE example_mood AS ENUM ('sad', 'ok', 'happy');" -U postgres +PGUSER=postgres +PGCONF=/etc/postgresql/9.1/main +PGDATA=/var/ramfs/postgresql/9.1/main + +psql -d "postgres" -c 'create database netty_driver_test;' -U $PGUSER +psql -d "postgres" -c 'create database netty_driver_time_test;' -U $PGUSER +psql -d "postgres" -c "alter database netty_driver_time_test set timezone to 'GMT'" -U $PGUSER +psql -d "netty_driver_test" -c "create table transaction_test ( id varchar(255) not null, constraint id_unique primary key (id))" -U $PGUSER +psql -d "postgres" -c "CREATE USER postgres_md5 WITH PASSWORD 'postgres_md5'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_md5;" -U $PGUSER +psql -d "postgres" -c "CREATE USER postgres_cleartext WITH PASSWORD 'postgres_cleartext'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_cleartext;" -U $PGUSER +psql -d "postgres" -c "CREATE USER postgres_kerberos WITH PASSWORD 'postgres_kerberos'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_kerberos;" -U $PGUSER +psql -d "netty_driver_test" -c "CREATE TYPE example_mood AS ENUM ('sad', 'ok', 'happy');" -U $PGUSER -sudo chmod 777 /etc/postgresql/9.1/main/pg_hba.conf +sudo chmod 666 $PGCONF/pg_hba.conf echo "pg_hba.conf goes as follows" -cat "/etc/postgresql/9.1/main/pg_hba.conf" +cat "$PGCONF/pg_hba.conf" -sudo echo "host all postgres 127.0.0.1/32 trust" > /etc/postgresql/9.1/main/pg_hba.conf -sudo echo "host all postgres_md5 127.0.0.1/32 md5" >> /etc/postgresql/9.1/main/pg_hba.conf -sudo echo "host all postgres_cleartext 127.0.0.1/32 password" >> /etc/postgresql/9.1/main/pg_hba.conf -sudo echo "host all postgres_kerberos 127.0.0.1/32 krb5" >> /etc/postgresql/9.1/main/pg_hba.conf +sudo echo "local all all trust" > $PGCONF/pg_hba.conf +sudo echo "host all postgres 127.0.0.1/32 trust" >> $PGCONF/pg_hba.conf +sudo echo "host all postgres_md5 127.0.0.1/32 md5" >> $PGCONF/pg_hba.conf +sudo echo "host all postgres_cleartext 127.0.0.1/32 password" >> $PGCONF/pg_hba.conf +sudo echo "host all postgres_kerberos 127.0.0.1/32 krb5" >> $PGCONF/pg_hba.conf echo "pg_hba.conf is now like" -cat "/etc/postgresql/9.1/main/pg_hba.conf" +cat "$PGCONF/pg_hba.conf" + +sudo chmod 600 $PGCONF/pg_hba.conf + +sudo cp -f $SCRIPTDIR/server.crt $SCRIPTDIR/server.key $PGDATA -sudo /etc/init.d/postgresql restart \ No newline at end of file +sudo /etc/init.d/postgresql restart diff --git a/script/server.crt b/script/server.crt new file mode 100644 index 00000000..aeef86f2 --- /dev/null +++ b/script/server.crt @@ -0,0 +1,75 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 9913731310682600948 (0x8994a61a13e775f4) + Signature Algorithm: sha1WithRSAEncryption + Issuer: CN=localhost + Validity + Not Before: Mar 6 08:12:28 2016 GMT + Not After : Apr 5 08:12:28 2016 GMT + Subject: CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:ce:26:60:f9:0d:0f:f1:d6:ed:3e:79:91:55:6a: + 18:63:23:96:f2:60:50:3d:e3:dd:72:e8:c2:54:17: + 50:be:f0:9c:32:95:39:75:b1:04:a7:bb:f5:10:a4: + eb:d0:10:e2:17:45:d3:f9:35:8e:b4:8f:14:97:8f: + 27:93:d7:20:05:e2:dc:68:64:bc:fd:f2:19:17:94: + e8:2f:a6:b2:54:3f:df:3e:e7:8f:f1:52:15:7a:30: + 81:4d:bb:6f:22:8c:ca:e1:cb:6a:72:6d:fa:89:50: + e7:ee:07:d1:84:8a:71:07:dc:3f:6f:1f:db:10:e9: + 93:ad:01:c5:2b:51:ce:58:ef:12:95:00:16:e8:d4: + 46:07:35:ee:10:47:c4:f7:ff:47:17:52:a5:bb:5c: + cb:3c:f6:6b:c8:e7:d9:7c:18:39:a1:8f:e0:45:82: + 88:b5:27:f3:58:cb:ba:30:c0:8a:77:5b:00:bf:09: + 10:b1:ad:aa:f4:1b:2c:a1:f9:a5:59:57:c8:ef:de: + 54:ad:35:af:67:7e:29:bc:9a:2a:d2:f0:b1:9c:34: + 3c:bc:64:c9:4c:93:2c:7d:29:f4:1a:ac:f3:44:42: + a4:c9:06:1e:a4:73:e6:aa:67:d0:e4:02:02:ba:51: + 1e:97:44:b8:4b:4e:55:cd:e6:24:49:08:ac:9b:09: + 19:31 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 2E:20:4D:E1:12:2A:B0:6F:52:7F:62:90:D4:78:7B:E3:7D:D5:60:10 + X509v3 Authority Key Identifier: + keyid:2E:20:4D:E1:12:2A:B0:6F:52:7F:62:90:D4:78:7B:E3:7D:D5:60:10 + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha1WithRSAEncryption + 9b:e8:50:8b:86:0f:bf:22:c6:b4:ef:3e:c9:a2:55:fb:69:fc: + ae:93:7b:5e:6a:b6:ed:5b:27:c2:9e:36:d6:f1:f1:0f:67:65: + 87:de:05:21:6e:0e:f4:df:ac:72:61:47:f8:fd:16:9b:3d:54: + ef:21:cf:b7:31:ba:bf:c9:1b:2c:a0:f9:f1:6b:45:5a:98:25: + b9:01:99:cf:e1:79:c5:6a:20:ce:ca:ca:3f:6d:56:f3:65:51: + 31:98:01:b9:96:99:04:9c:ab:ae:fb:3f:f8:ad:60:66:77:54: + b2:81:e3:7c:6b:c4:36:ae:ae:5c:c6:1a:09:5c:d6:13:da:2b: + ba:ef:3f:3e:b2:13:f2:51:15:c5:1b:9c:22:be:b4:55:9b:15: + 70:60:3d:98:6e:ef:53:4c:c7:20:60:3f:17:f3:cc:76:47:96: + 27:05:84:0e:db:21:e1:76:b7:9c:38:35:19:ef:52:d4:fc:bd: + ec:95:2e:eb:4b:5b:0b:c8:86:d7:23:c2:76:14:f3:93:6f:c0: + a9:b6:ca:f8:47:3e:9d:af:11:5d:73:79:68:70:26:f9:fd:39: + 60:c1:c3:c7:a9:fc:48:b5:c0:e6:b4:2e:07:de:6a:ca:ed:04: + 67:31:b8:0b:d0:48:fd:3b:4c:12:8a:34:5c:18:3f:38:85:f2: + 1c:96:39:50 +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJAImUphoT53X0MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xNjAzMDYwODEyMjhaFw0xNjA0MDUwODEyMjhaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAM4mYPkND/HW7T55kVVqGGMjlvJgUD3j3XLowlQXUL7wnDKVOXWxBKe79RCk +69AQ4hdF0/k1jrSPFJePJ5PXIAXi3GhkvP3yGReU6C+mslQ/3z7nj/FSFXowgU27 +byKMyuHLanJt+olQ5+4H0YSKcQfcP28f2xDpk60BxStRzljvEpUAFujURgc17hBH +xPf/RxdSpbtcyzz2a8jn2XwYOaGP4EWCiLUn81jLujDAindbAL8JELGtqvQbLKH5 +pVlXyO/eVK01r2d+KbyaKtLwsZw0PLxkyUyTLH0p9Bqs80RCpMkGHqRz5qpn0OQC +ArpRHpdEuEtOVc3mJEkIrJsJGTECAwEAAaNQME4wHQYDVR0OBBYEFC4gTeESKrBv +Un9ikNR4e+N91WAQMB8GA1UdIwQYMBaAFC4gTeESKrBvUn9ikNR4e+N91WAQMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJvoUIuGD78ixrTvPsmiVftp +/K6Te15qtu1bJ8KeNtbx8Q9nZYfeBSFuDvTfrHJhR/j9Fps9VO8hz7cxur/JGyyg ++fFrRVqYJbkBmc/hecVqIM7Kyj9tVvNlUTGYAbmWmQScq677P/itYGZ3VLKB43xr +xDaurlzGGglc1hPaK7rvPz6yE/JRFcUbnCK+tFWbFXBgPZhu71NMxyBgPxfzzHZH +licFhA7bIeF2t5w4NRnvUtT8veyVLutLWwvIhtcjwnYU85NvwKm2yvhHPp2vEV1z +eWhwJvn9OWDBw8ep/Ei1wOa0LgfeasrtBGcxuAvQSP07TBKKNFwYPziF8hyWOVA= +-----END CERTIFICATE----- diff --git a/script/server.key b/script/server.key new file mode 100644 index 00000000..0e226429 --- /dev/null +++ b/script/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAziZg+Q0P8dbtPnmRVWoYYyOW8mBQPePdcujCVBdQvvCcMpU5 +dbEEp7v1EKTr0BDiF0XT+TWOtI8Ul48nk9cgBeLcaGS8/fIZF5ToL6ayVD/fPueP +8VIVejCBTbtvIozK4ctqcm36iVDn7gfRhIpxB9w/bx/bEOmTrQHFK1HOWO8SlQAW +6NRGBzXuEEfE9/9HF1Klu1zLPPZryOfZfBg5oY/gRYKItSfzWMu6MMCKd1sAvwkQ +sa2q9BssofmlWVfI795UrTWvZ34pvJoq0vCxnDQ8vGTJTJMsfSn0GqzzREKkyQYe +pHPmqmfQ5AICulEel0S4S05VzeYkSQismwkZMQIDAQABAoIBAH80v3Hu1X/tl8eN +TFjgdtv2Ahbdx6XpDaTya7doC7NG1ZuA6UvuR2kZWkdC/SAOyvSBaiPFIKHaCGLd +OxbHEEORkV/5iYVJ9qHOiNeejTvfjepLCU9nz0ju1VsZ5aH0LtzVoIGry4UgH32J +5YdbxhOLnLj9dzggabe/9+KbQDEveGTzkIvSJ1nbts7c8IRp6t/1nBz54BhawUjJ +IbaEbCH/mEmiCOUP914SCAUEfmgbMhdx8dc4V9nyxK+bulF3WIEpVZU1zj5Rpyni +P8gQ1geI64Erd8oa4DJ5C77eLuKKk0JBCkgh5x3hiAxuvN0zxHxW2Q75c6x9uDr5 +DXi20GECgYEA+NRW6heYBJw7Lt7+cQCRG5/WFOX9TmmK9EAidVPULWO4NN4wLZxa +exW/epg8w1Y+u+BHOzFq9idJaHsoLZCmoNWMkZsP+AzeEkklee6wgur3/Zs1HqHZ +1VA3EmvOecz++3o69zcjd0nzgk9ADhjA2dAahKTnn5RESD1dFBWU2+sCgYEA1Bcv +PiQe6ce86FlSPr0TBFvIJl2dfjrQijL3dhZMo+1Y5VTShGBoAQKfBhJITSbsmaEz +UQ/4rBMyTN9bwvSwsDpQZw/Y0YKiSQIOr4J0jyotY5RN2AH3AlCX8CrhoOmBaLUd +n2SGx5keodnXn1/GPkuGPIa7xnGib/gdL2AaZFMCgYBV5AX0XByPStZrAXJW01lD +bdLZ9+GOFYRvd0vtr/gHiupk5WU/+T6KSiGEUdR3oOeatnogBpjjSwBd3lUqFUpP +LieNgzbp6pclPLaA9lFbf3wGwHJ/lmK47S11YF0vUgGaEMEV4KSPYql5i52SwByh +kuH0c2+4d9dyECx26FQv7QKBgQDBtX83oWP+n6hhCpu8o5IH7BAtQlmDHhKz9oLf +/tP28OO9abBwqWC0c4Fs2SviE4gLdRjak9zKxSmu3l3//N6XxlsDFo0wJcE1L0Tc +dikhTSNxjNVgUcMaASQUfgXfowXH7YvltboH+UjqCH4QmTgGU5KCG4jLYaQ74gA9 +8eeI8wKBgDfclcMsJnY6FpFoR0Ub9VOrdbKtD9nXSxhTSFKjrp4JM7SBN3u6NPJK +FgKZyQxd1bX/RBioN1prrZ3rbg+9awc65KhyfwtNxiurCBZhYObhKJv7lZyjNgsT +EALMKvB+fdpMtPZOVtUl0MbHEBblrJ+oy4TPT/kvMuCudF/5arcZ +-----END RSA PRIVATE KEY----- From efeb246ae1eefa4e020e993e02a058cf6edcacb2 Mon Sep 17 00:00:00 2001 From: Guilherme Campos Date: Tue, 15 Mar 2016 14:20:14 -0300 Subject: [PATCH 02/26] Rename LICENCE.txt to LICENSE.txt fixing filename typo --- LICENCE.txt => LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename LICENCE.txt => LICENSE.txt (99%) diff --git a/LICENCE.txt b/LICENSE.txt similarity index 99% rename from LICENCE.txt rename to LICENSE.txt index 61ca0ac4..fc389d02 100644 --- a/LICENCE.txt +++ b/LICENSE.txt @@ -199,4 +199,4 @@ 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. \ No newline at end of file + limitations under the License. From a43118ffcd8196055d1cb1a17d696e09b1a516e7 Mon Sep 17 00:00:00 2001 From: Mauricio Linhares Date: Thu, 17 Mar 2016 22:08:13 -0400 Subject: [PATCH 03/26] Wrapping up 0.2.19 --- CHANGELOG.md | 5 +++++ README.markdown | 8 ++++---- README.md | 3 --- project/Build.scala | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) delete mode 100644 README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab0d079..5baf54f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,11 @@ # Changelog +## 0.2.19 - 2016-03-17 + +* Always use `NUMERIC` when handling numbers in prepared statements in PostgreSQL; +* SSL support for PostgreSQL - @alexdupre - #85; + ## 0.2.18 - 2015-08-08 * Timeouts implemented queries for MySQL and PostgreSQL - @lifey - #147 diff --git a/README.markdown b/README.markdown index 73302b6c..b5fb56c8 100644 --- a/README.markdown +++ b/README.markdown @@ -55,7 +55,7 @@ You can view the project's [CHANGELOG here](CHANGELOG.md). And if you're in a hurry, you can include them in your build like this, if you're using PostgreSQL: ```scala -"com.github.mauricio" %% "postgresql-async" % "0.2.18" +"com.github.mauricio" %% "postgresql-async" % "0.2.19" ``` Or Maven: @@ -64,14 +64,14 @@ Or Maven: com.github.mauricio postgresql-async_2.11 - 0.2.18 + 0.2.19 ``` And if you're into MySQL: ```scala -"com.github.mauricio" %% "mysql-async" % "0.2.18" +"com.github.mauricio" %% "mysql-async" % "0.2.19" ``` Or Maven: @@ -80,7 +80,7 @@ Or Maven: com.github.mauricio mysql-async_2.11 - 0.2.18 + 0.2.19 ``` diff --git a/README.md b/README.md deleted file mode 100644 index 4fedd098..00000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# postgresql-async - -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mauricio/postgresql-async?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) \ No newline at end of file diff --git a/project/Build.scala b/project/Build.scala index a820fb76..386c4f5e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -49,7 +49,7 @@ object ProjectBuild extends Build { object Configuration { - val commonVersion = "0.2.19-SNAPSHOT" + val commonVersion = "0.2.19" val projectScalaVersion = "2.11.7" val specs2Version = "2.5" From 11e23e0eab0b50df61f3e693564fbf8deff1ef6a Mon Sep 17 00:00:00 2001 From: Mauricio Linhares Date: Thu, 17 Mar 2016 22:10:15 -0400 Subject: [PATCH 04/26] Updated readme and changelog --- CHANGELOG.md | 5 +++-- README.markdown | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5baf54f7..bafc831d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ -**Table of Contents** - [Changelog](#changelog) - - [0.2.17 - in progresss](#0217---in-progresss) + - [0.2.19 - 2016-03-17](#0219---2016-03-17) + - [0.2.18 - 2015-08-08](#0218---2015-08-08) + - [0.2.17 - 2015-07-13](#0217---2015-07-13) - [0.2.16 - 2015-01-04](#0216---2015-01-04) - [0.2.15 - 2014-09-12](#0215---2014-09-12) - [0.2.14 - 2014-08-30](#0214---2014-08-30) diff --git a/README.markdown b/README.markdown index b5fb56c8..b87583a2 100644 --- a/README.markdown +++ b/README.markdown @@ -1,8 +1,7 @@ -**Table of Contents** -- postgresql-async & mysql-async - async, Netty based, database drivers for MySQL and PostgreSQL written in Scala - 2.10 - 2.11 +- [[![Build Status](https://travis-ci.org/mauricio/postgresql-async.png)](https://travis-ci.org/mauricio/postgresql-async) postgresql-async & mysql-async - async, Netty based, database drivers for MySQL and PostgreSQL written in Scala 2.10 and 2.11](#!build-statushttpstravis-ciorgmauriciopostgresql-asyncpnghttpstravis-ciorgmauriciopostgresql-async-postgresql-async-&-mysql-async---async-netty-based-database-drivers-for-mysql-and-postgresql-written-in-scala-210-and-211) - [Abstractions and integrations](#abstractions-and-integrations) - [Include them as dependencies](#include-them-as-dependencies) - [Database connections and encodings](#database-connections-and-encodings) From 5e24cb0902860972a952c4b4d164e73568fe1d93 Mon Sep 17 00:00:00 2001 From: Mauricio Linhares Date: Thu, 17 Mar 2016 22:11:54 -0400 Subject: [PATCH 05/26] Kicking off next cycle --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index 386c4f5e..13c8df4b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -49,7 +49,7 @@ object ProjectBuild extends Build { object Configuration { - val commonVersion = "0.2.19" + val commonVersion = "0.2.20-SNAPSHOT" val projectScalaVersion = "2.11.7" val specs2Version = "2.5" From d6180b518b9c399c7392c2cac96d14bc5d3d8c7f Mon Sep 17 00:00:00 2001 From: Stephen Couchman Date: Mon, 29 Feb 2016 05:48:17 -0500 Subject: [PATCH 06/26] Added check to SingleThreadedAsyncObjectPool to ensure returned objects came from that pool. Fixed not destroying invalidated objects during test cycle. Fixed exception on multiple close attempts in SingleThreadedAsyncObjectPool to make consistent with simultaneous request execution path. Added generic spec for testing an AsyncObjectPool implementation, and applied it to SingleThreadedAsyncObjectPool to guard against the above problems reappearing. Added mock capabilities back for specs2. --- .../pool/SingleThreadedAsyncObjectPool.scala | 72 ++++-- .../db/pool/AbstractAsyncObjectPoolSpec.scala | 228 ++++++++++++++++++ project/Build.scala | 2 + 3 files changed, 277 insertions(+), 25 deletions(-) create mode 100644 db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala index 2b2e28d9..49f60593 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala @@ -16,11 +16,14 @@ package com.github.mauricio.async.db.pool +import java.util.concurrent.RejectedExecutionException + import com.github.mauricio.async.db.util.{Log, Worker} import java.util.concurrent.atomic.AtomicLong -import java.util.{TimerTask, Timer} +import java.util.{Timer, TimerTask} + import scala.collection.mutable.{ArrayBuffer, Queue, Stack} -import scala.concurrent.{Promise, Future} +import scala.concurrent.{Future, Promise} import scala.util.{Failure, Success} object SingleThreadedAsyncObjectPool { @@ -93,15 +96,30 @@ class SingleThreadedAsyncObjectPool[T]( def giveBack(item: T): Future[AsyncObjectPool[T]] = { val promise = Promise[AsyncObjectPool[T]]() this.mainPool.action { - this.checkouts -= item - this.factory.validate(item) match { - case Success(item) => { - this.addBack(item, promise) + // Ensure it came from this pool + val idx = this.checkouts.indexOf(item) + if(idx >= 0) { + this.checkouts.remove(idx) + this.factory.validate(item) match { + case Success(item) => { + this.addBack(item, promise) + } + case Failure(e) => { + this.factory.destroy(item) + promise.failure(e) + } } - case Failure(e) => { - this.checkouts -= item - this.factory.destroy(item) - promise.failure(e) + } else { + // It's already a failure but lets doublecheck why + val isFromOurPool = (item match { + case x: AnyRef => this.poolables.find(holder => x eq holder.item.asInstanceOf[AnyRef]) + case _ => this.poolables.find(holder => item == holder.item) + }).isDefined + + if(isFromOurPool) { + promise.failure(new IllegalStateException("This item has already been returned")) + } else { + promise.failure(new IllegalArgumentException("The returned item did not come from this pool.")) } } } @@ -112,25 +130,28 @@ class SingleThreadedAsyncObjectPool[T]( def isFull: Boolean = this.poolables.isEmpty && this.checkouts.size == configuration.maxObjects def close: Future[AsyncObjectPool[T]] = { - val promise = Promise[AsyncObjectPool[T]]() - - this.mainPool.action { - if (!this.closed) { - try { - this.timer.cancel() - this.mainPool.shutdown - this.closed = true - (this.poolables.map(i => i.item) ++ this.checkouts).foreach(item => factory.destroy(item)) + try { + val promise = Promise[AsyncObjectPool[T]]() + this.mainPool.action { + if (!this.closed) { + try { + this.timer.cancel() + this.mainPool.shutdown + this.closed = true + (this.poolables.map(i => i.item) ++ this.checkouts).foreach(item => factory.destroy(item)) + promise.success(this) + } catch { + case e: Exception => promise.failure(e) + } + } else { promise.success(this) - } catch { - case e: Exception => promise.failure(e) } - } else { - promise.success(this) } + promise.future + } catch { + case e: RejectedExecutionException if this.closed => + Future.successful(this) } - - promise.future } def availables: Traversable[T] = this.poolables.map(item => item.item) @@ -238,6 +259,7 @@ class SingleThreadedAsyncObjectPool[T]( case Failure(e) => { log.error("Failed to validate object", e) removals += poolable + factory.destroy(poolable.item) } } } diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala new file mode 100644 index 00000000..34ca0662 --- /dev/null +++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala @@ -0,0 +1,228 @@ +package com.github.mauricio.async.db.pool + +import com.github.mauricio.async.db.pool.AbstractAsyncObjectPoolSpec.Widget +import org.mockito.Mockito.reset +import org.specs2.mock.Mockito +import org.specs2.mutable.Specification + +import scala.concurrent.{Await, Future} +import scala.util.Failure + +import scala.reflect.runtime.universe.TypeTag +import scala.util.Try +import scala.concurrent.duration.{Duration, SECONDS} + +/** + * This spec is designed abstract to allow testing of any implementation of AsyncObjectPool, against the common + * requirements the interface expects. + * + * @tparam T the AsyncObjectPool being tested. + */ +abstract class AbstractAsyncObjectPoolSpec[T <: AsyncObjectPool[Widget]](implicit tag: TypeTag[T]) + extends Specification + with Mockito { + + import AbstractAsyncObjectPoolSpec._ + + protected def pool(factory: ObjectFactory[Widget] = new TestWidgetFactory, conf: PoolConfiguration = PoolConfiguration.Default): T + + // Evaluates to the type of AsyncObjectPool + s"the ${tag.tpe.erasure} variant of AsyncObjectPool" should { + + "successfully retrieve and return a Widget" in { + val p = pool() + val widget = Await.result(p.take, Duration.Inf) + + widget must not beNull + + val thePool = Await.result(p.giveBack(widget), Duration.Inf) + thePool must be(p) + } + + "reject Widgets that did not come from it" in { + val p = pool() + + Await.result(p.giveBack(Widget(null)), Duration.Inf) must throwAn[IllegalArgumentException] + } + + "scale contents" >> { + sequential + + val factory = spy(new TestWidgetFactory) + + val p = pool( + factory = factory, + conf = PoolConfiguration( + maxObjects = 5, + maxIdle = 2, + maxQueueSize = 5, + validationInterval = 2000 + )) + + + + var taken = Seq.empty[Widget] + "can take up to maxObjects" in { + taken = Await.result(Future.sequence(for (i <- 1 to 5) yield p.take), Duration.Inf) + + taken must have size 5 + taken.head must not beNull; + taken(1) must not beNull; + taken(2) must not beNull; + taken(3) must not beNull; + taken(4) must not beNull + } + + "does not attempt to expire taken items" in { + // Wait 3 seconds to ensure idle check has run at least once + there was after(3.seconds).no(factory).destroy(any[Widget]) + } + + reset(factory) // Considered bad form, but necessary as we depend on previous state in these tests + "takes maxObjects back" in { + val returns = Await.result(Future.sequence(for (widget <- taken) yield p.giveBack(widget)), Duration.Inf) + + returns must have size 5 + + returns.head must be(p) + returns(1) must be(p) + returns(2) must be(p) + returns(3) must be(p) + returns(4) must be(p) + } + + "protest returning an item that was already returned" in { + val resultFuture = p.giveBack(taken.head) + + Await.result(resultFuture, Duration.Inf) must throwAn[IllegalStateException] + } + + "destroy down to maxIdle widgets" in { + Thread.sleep(3000) + there were 5.times(factory).destroy(any[Widget]) + } + } + + "queue requests after running out" in { + val p = pool(conf = PoolConfiguration.Default.copy(maxObjects = 2, maxQueueSize = 1)) + + val widgets = Await.result(Future.sequence(for (i <- 1 to 2) yield p.take), Duration.Inf) + + val future = p.take + + // Wait five seconds + Thread.sleep(5000) + + val failedFuture = p.take + + // Cannot be done, would exceed maxObjects + future.isCompleted must beFalse + + Await.result(failedFuture, Duration.Inf) must throwA[PoolExhaustedException] + + Await.result(p.giveBack(widgets.head), Duration.Inf) must be(p) + + Await.result(future, Duration(5, SECONDS)) must be(widgets.head) + } + + "refuse to allow take after being closed" in { + val p = pool() + + Await.result(p.close, Duration.Inf) must be(p) + + Await.result(p.take, Duration.Inf) must throwA[PoolAlreadyTerminatedException] + } + + "allow being closed more than once" in { + val p = pool() + + Await.result(p.close, Duration.Inf) must be(p) + + Await.result(p.close, Duration.Inf) must be(p) + } + + + "destroy a failed widget" in { + val factory = spy(new TestWidgetFactory) + val p = pool(factory = factory) + + val widget = Await.result(p.take, Duration.Inf) + + widget must not beNull + + factory.validate(widget) returns Failure(new RuntimeException("This is a bad widget!")) + + Await.result(p.giveBack(widget), Duration.Inf) must throwA[RuntimeException](message = "This is a bad widget!") + + there was atLeastOne(factory).destroy(widget) + } + + "clean up widgets that die in the pool" in { + val factory = spy(new TestWidgetFactory) + // Deliberately make it impossible to expire (nearly) + val p = pool(factory = factory, conf = PoolConfiguration.Default.copy(maxIdle = Long.MaxValue, validationInterval = 2000)) + + val widget = Await.result(p.take, Duration.Inf) + + widget must not beNull + + Await.result(p.giveBack(widget), Duration.Inf) must be(p) + + there was atLeastOne(factory).validate(widget) + there were no(factory).destroy(widget) + + there was after(3.seconds).atLeastTwo(factory).validate(widget) + + factory.validate(widget) returns Failure(new RuntimeException("Test Exception, Not an Error")) + + there was after(3.seconds).one(factory).destroy(widget) + + Await.ready(p.take, Duration.Inf) + + there was two(factory).create + } + + } + +} + +object AbstractAsyncObjectPoolSpec { + + case class Widget(factory: TestWidgetFactory) + + class TestWidgetFactory extends ObjectFactory[Widget] { + + override def create: Widget = Widget(this) + + override def destroy(item: Widget) = {} + + override def validate(item: Widget): Try[Widget] = Try { + if (item.factory eq this) + item + else + throw new IllegalArgumentException("Not our item") + } + } + +} + + +class SingleThreadedAsyncObjectPoolSpec extends AbstractAsyncObjectPoolSpec[SingleThreadedAsyncObjectPool[Widget]] { + + import AbstractAsyncObjectPoolSpec._ + + override protected def pool(factory: ObjectFactory[Widget], conf: PoolConfiguration) = + new SingleThreadedAsyncObjectPool(factory, conf) + + "SingleThreadedAsyncObjectPool" should { + "successfully record a closed state" in { + val p = pool() + + Await.result(p.close, Duration.Inf) must be(p) + + p.isClosed must beTrue + } + + } + +} diff --git a/project/Build.scala b/project/Build.scala index 13c8df4b..84cd916f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -55,6 +55,7 @@ object Configuration { val specs2Dependency = "org.specs2" %% "specs2-core" % specs2Version % "test" val specs2JunitDependency = "org.specs2" %% "specs2-junit" % specs2Version % "test" + val specs2MockDependency = "org.specs2" %% "specs2-mock" % specs2Version % "test" val logbackDependency = "ch.qos.logback" % "logback-classic" % "1.1.6" % "test" val commonDependencies = Seq( @@ -65,6 +66,7 @@ object Configuration { "org.javassist" % "javassist" % "3.20.0-GA", specs2Dependency, specs2JunitDependency, + specs2MockDependency, logbackDependency ) From a2a11ac3ff3c8fcbdbaf18589cacaa7994e8269f Mon Sep 17 00:00:00 2001 From: varkockova Date: Mon, 18 Apr 2016 17:59:21 +0200 Subject: [PATCH 07/26] Time unit as a part of javadoc I wanted to know the time unit being used for idle and I had to look into the iplementation to be sure. I think it might be useful to have this in the javadoc directly. --- .../com/github/mauricio/async/db/pool/PoolConfiguration.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolConfiguration.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolConfiguration.scala index a245de5c..0ac567f2 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolConfiguration.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolConfiguration.scala @@ -25,7 +25,7 @@ object PoolConfiguration { * Defines specific pieces of a pool's behavior. * * @param maxObjects how many objects this pool will hold - * @param maxIdle how long are objects going to be kept as idle (not in use by clients of the pool) + * @param maxIdle number of milliseconds for which the objects are going to be kept as idle (not in use by clients of the pool) * @param maxQueueSize when there are no more objects, the pool can queue up requests to serve later then there * are objects available, this is the maximum number of enqueued requests * @param validationInterval pools will use this value as the timer period to validate idle objects. From 226ed09a6c5550e57f9b856ad2840e9a38134a2f Mon Sep 17 00:00:00 2001 From: Stephen Couchman Date: Mon, 25 Apr 2016 18:45:25 -0400 Subject: [PATCH 08/26] Reworked URLParser to process more URLs. Added MySQL URLParser Made URLParser stricter. Corrected test cases using illegal IP addresses. (ip's out of range) Now accepts JDBC style "jdbc:postgresql:dbname" Switched from fragile regex to java.net.URI parsing. Added parameter URL-format decoding. Deprecated ParserURL in PostgreSQL and converted it to an alias to PostgreSQL URLParser. Deprecated to 0.2.20, the version may need to be updated. --- .../mauricio/async/db/Configuration.scala | 2 + .../UnableToParseURLException.scala | 24 ++ .../async/db/util/AbstractURIParser.scala | 175 ++++++++++++ .../async/db/mysql/util/URLParser.scala | 39 +++ .../async/db/mysql/util/URLParserSpec.scala | 264 ++++++++++++++++++ .../db/postgresql/PostgreSQLConnection.scala | 13 +- .../async/db/postgresql/util/ParserURL.scala | 65 ----- .../async/db/postgresql/util/URLParser.scala | 88 ++++-- .../async/db/postgresql/util/package.scala | 29 ++ .../db/postgresql/util/URLParserSpec.scala | 160 ++++++++--- 10 files changed, 726 insertions(+), 133 deletions(-) create mode 100644 db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnableToParseURLException.scala create mode 100644 db-async-common/src/main/scala/com/github/mauricio/async/db/util/AbstractURIParser.scala create mode 100644 mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/URLParser.scala create mode 100644 mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/util/URLParserSpec.scala delete mode 100644 postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ParserURL.scala create mode 100644 postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/package.scala diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala index b032ac02..cde267cf 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala @@ -25,6 +25,8 @@ import scala.concurrent.duration._ object Configuration { val DefaultCharset = CharsetUtil.UTF_8 + + @deprecated("Use com.github.mauricio.async.db.postgresql.util.URLParser.DEFAULT or com.github.mauricio.async.db.mysql.util.URLParser.DEFAULT.", since = "0.2.20") val Default = new Configuration("postgres") } diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnableToParseURLException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnableToParseURLException.scala new file mode 100644 index 00000000..0d2799df --- /dev/null +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnableToParseURLException.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2016 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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 com.github.mauricio.async.db.exceptions + +/** + * Thrown to indicate that a URL Parser could not understand the provided URL. + */ +class UnableToParseURLException(message: String, base: Throwable) extends RuntimeException(message, base) { + def this(message: String) = this(message, null) +} \ No newline at end of file diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/AbstractURIParser.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/AbstractURIParser.scala new file mode 100644 index 00000000..e18de6e1 --- /dev/null +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/AbstractURIParser.scala @@ -0,0 +1,175 @@ +/* + * Copyright 2016 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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 com.github.mauricio.async.db.util + +import java.net.{URI, URISyntaxException, URLDecoder} +import java.nio.charset.Charset + +import com.github.mauricio.async.db.exceptions.UnableToParseURLException +import com.github.mauricio.async.db.{Configuration, SSLConfiguration} +import org.slf4j.LoggerFactory + +import scala.util.matching.Regex + +/** + * Common parser assisting methods for PG and MySQL URI parsers. + */ +abstract class AbstractURIParser { + import AbstractURIParser._ + + protected val logger = LoggerFactory.getLogger(getClass) + + /** + * Parses out userInfo into a tuple of optional username and password + * + * @param userInfo the optional user info string + * @return a tuple of optional username and password + */ + final protected def parseUserInfo(userInfo: Option[String]): (Option[String], Option[String]) = userInfo.map(_.split(":", 2).toList) match { + case Some(user :: pass :: Nil) ⇒ (Some(user), Some(pass)) + case Some(user :: Nil) ⇒ (Some(user), None) + case _ ⇒ (None, None) + } + + /** + * A Regex that will match the base name of the driver scheme, minus jdbc:. + * Eg: postgres(?:ul)? + */ + protected val SCHEME: Regex + + /** + * The default for this particular URLParser, ie: appropriate and specific to PG or MySQL accordingly + */ + val DEFAULT: Configuration + + + /** + * Parses the provided url and returns a Configuration based upon it. On an error, + * @param url the URL to parse. + * @param charset the charset to use. + * @return a Configuration. + */ + @throws[UnableToParseURLException]("if the URL does not match the expected type, or cannot be parsed for any reason") + def parseOrDie(url: String, + charset: Charset = DEFAULT.charset): Configuration = { + try { + val properties = parse(new URI(url).parseServerAuthority) + + assembleConfiguration(properties, charset) + } catch { + case e: URISyntaxException => + throw new UnableToParseURLException(s"Failed to parse URL: $url", e) + } + } + + + /** + * Parses the provided url and returns a Configuration based upon it. On an error, + * a default configuration is returned. + * @param url the URL to parse. + * @param charset the charset to use. + * @return a Configuration. + */ + def parse(url: String, + charset: Charset = DEFAULT.charset + ): Configuration = { + try { + parseOrDie(url, charset) + } catch { + case e: Exception => + logger.warn(s"Connection url '$url' could not be parsed.", e) + // Fallback to default to maintain current behavior + DEFAULT + } + } + + /** + * Assembles a configuration out of the provided property map. This is the generic form, subclasses may override to + * handle additional properties. + * @param properties the extracted properties from the URL. + * @param charset the charset passed in to parse or parseOrDie. + * @return + */ + protected def assembleConfiguration(properties: Map[String, String], charset: Charset): Configuration = { + DEFAULT.copy( + username = properties.getOrElse(USERNAME, DEFAULT.username), + password = properties.get(PASSWORD), + database = properties.get(DBNAME), + host = properties.getOrElse(HOST, DEFAULT.host), + port = properties.get(PORT).map(_.toInt).getOrElse(DEFAULT.port), + ssl = SSLConfiguration(properties), + charset = charset + ) + } + + + protected def parse(uri: URI): Map[String, String] = { + uri.getScheme match { + case SCHEME() => + val userInfo = parseUserInfo(Option(uri.getUserInfo)) + + val port = Some(uri.getPort).filter(_ > 0) + val db = Option(uri.getPath).map(_.stripPrefix("/")).filterNot(_.isEmpty) + val host = Option(uri.getHost) + + val builder = Map.newBuilder[String, String] + builder ++= userInfo._1.map(USERNAME -> _) + builder ++= userInfo._2.map(PASSWORD -> _) + builder ++= port.map(PORT -> _.toString) + builder ++= db.map(DBNAME -> _) + builder ++= host.map(HOST -> unwrapIpv6address(_)) + + // Parse query string parameters and just append them, overriding anything previously set + builder ++= (for { + qs <- Option(uri.getQuery).toSeq + parameter <- qs.split('&') + Array(name, value) = parameter.split('=') + if name.nonEmpty && value.nonEmpty + } yield URLDecoder.decode(name, "UTF-8") -> URLDecoder.decode(value, "UTF-8")) + + + builder.result + case "jdbc" => + handleJDBC(uri) + case _ => + throw new UnableToParseURLException("Unrecognized URI scheme") + } + } + + /** + * This method breaks out handling of the jdbc: prefixed uri's, allowing them to be handled differently + * without reimplementing all of parse. + */ + protected def handleJDBC(uri: URI): Map[String, String] = parse(new URI(uri.getSchemeSpecificPart)) + + + final protected def unwrapIpv6address(server: String): String = { + if (server.startsWith("[")) { + server.substring(1, server.length() - 1) + } else server + } + +} + +object AbstractURIParser { + // Constants and value names + val PORT = "port" + val DBNAME = "database" + val HOST = "host" + val USERNAME = "user" + val PASSWORD = "password" +} + diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/URLParser.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/URLParser.scala new file mode 100644 index 00000000..ba9c0333 --- /dev/null +++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/URLParser.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2016 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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 com.github.mauricio.async.db.mysql.util + +import com.github.mauricio.async.db.util.AbstractURIParser +import com.github.mauricio.async.db.Configuration + +/** + * The MySQL URL parser. + */ +object URLParser extends AbstractURIParser { + + /** + * The default configuration for MySQL. + */ + override val DEFAULT = Configuration( + username = "root", + host = "127.0.0.1", //Matched JDBC default + port = 3306, + password = None, + database = None + ) + + override protected val SCHEME = "^mysql$".r + +} diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/util/URLParserSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/util/URLParserSpec.scala new file mode 100644 index 00000000..b15ab779 --- /dev/null +++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/util/URLParserSpec.scala @@ -0,0 +1,264 @@ +/* + * Copyright 2016 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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 com.github.mauricio.async.db.mysql.util + +import java.nio.charset.Charset + +import com.github.mauricio.async.db.{Configuration, SSLConfiguration} +import com.github.mauricio.async.db.exceptions.UnableToParseURLException +import io.netty.buffer.{ByteBufAllocator, PooledByteBufAllocator} +import org.specs2.mutable.Specification + +import scala.concurrent.duration.Duration + +class URLParserSpec extends Specification { + + "mysql URLParser" should { + import URLParser.{DEFAULT, parse, parseOrDie} + + + "have a reasonable default" in { + // This is a deliberate extra step, protecting the DEFAULT from frivilous changes. + // Any change to DEFAULT should require a change to this test. + + DEFAULT === Configuration( + username = "root", + host = "127.0.0.1", //Matched JDBC default + port = 3306, + password = None, + database = None + ) + } + + + // Divided into sections + // =========== jdbc:mysql =========== + + "create a jdbc:mysql connection with the available fields" in { + val connectionUri = "jdbc:mysql://128.167.54.90:9987/my_database?user=john&password=doe" + + parse(connectionUri) === DEFAULT.copy( + username = "john", + password = Some("doe"), + database = Some("my_database"), + host = "128.167.54.90", + port = 9987 + ) + } + + "create a connection without port" in { + val connectionUri = "jdbc:mysql://128.167.54.90/my_database?user=john&password=doe" + + parse(connectionUri) === DEFAULT.copy( + username = "john", + password = Some("doe"), + database = Some("my_database"), + host = "128.167.54.90" + ) + } + + + "create a connection without username and password" in { + val connectionUri = "jdbc:mysql://128.167.54.90:9987/my_database" + + parse(connectionUri) === DEFAULT.copy( + database = Some("my_database"), + host = "128.167.54.90", + port = 9987 + ) + } + + "create a connection from a heroku like URL using 'mysql' protocol" in { + val connectionUri = "mysql://john:doe@128.167.54.90:9987/my_database" + + parse(connectionUri) === DEFAULT.copy( + username = "john", + password = Some("doe"), + database = Some("my_database"), + host = "128.167.54.90", + port = 9987 + ) + } + + "create a connection with the available fields and named server" in { + val connectionUri = "jdbc:mysql://localhost:9987/my_database?user=john&password=doe" + + parse(connectionUri) === DEFAULT.copy( + username = "john", + password = Some("doe"), + database = Some("my_database"), + host = "localhost", + port = 9987 + ) + } + + "create a connection from a heroku like URL with named server" in { + val connectionUri = "mysql://john:doe@psql.heroku.com:9987/my_database" + + val configuration = parse(connectionUri) + configuration.username === "john" + configuration.password === Some("doe") + configuration.database === Some("my_database") + configuration.host === "psql.heroku.com" + configuration.port === 9987 + } + + "create a connection with the available fields and ipv6" in { + val connectionUri = "jdbc:mysql://[::1]:9987/my_database?user=john&password=doe" + + val configuration = parse(connectionUri) + + configuration.username === "john" + configuration.password === Some("doe") + configuration.database === Some("my_database") + configuration.host === "::1" + configuration.port === 9987 + } + + "create a connection from a heroku like URL and with ipv6" in { + val connectionUri = "mysql://john:doe@[::1]:9987/my_database" + + val configuration = parse(connectionUri) + configuration.username === "john" + configuration.password === Some("doe") + configuration.database === Some("my_database") + configuration.host === "::1" + configuration.port === 9987 + } + + "create a connection with a missing hostname" in { + val connectionUri = "jdbc:mysql:/my_database?user=john&password=doe" + + parse(connectionUri) === DEFAULT.copy( + username = "john", + password = Some("doe"), + database = Some("my_database") + ) + } + + "create a connection with a missing database name" in { + val connectionUri = "jdbc:mysql://[::1]:9987/?user=john&password=doe" + + val configuration = parse(connectionUri) + + configuration.username === "john" + configuration.password === Some("doe") + configuration.database === None + configuration.host === "::1" + configuration.port === 9987 + } + + "create a connection with all default fields" in { + val connectionUri = "jdbc:mysql:" + + val configuration = parse(connectionUri) + + configuration.username === "root" + configuration.password === None + configuration.database === None + configuration.host === "127.0.0.1" + configuration.port === 3306 + } + + "create a connection with an empty (invalid) url" in { + val connectionUri = "" + + val configuration = parse(connectionUri) + + configuration.username === "root" + configuration.password === None + configuration.database === None + configuration.host === "127.0.0.1" + configuration.port === 3306 + } + + + "recognise a mysql:// uri" in { + parse("mysql://localhost:425/dbname") mustEqual DEFAULT.copy( + username = "root", + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "recognize a jdbc:mysql:// uri" in { + parse("jdbc:mysql://localhost:425/dbname") mustEqual DEFAULT.copy( + username = "root", + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "pull the username and password from URI credentials" in { + parse("jdbc:mysql://user:password@localhost:425/dbname") mustEqual DEFAULT.copy( + username = "user", + password = Some("password"), + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "pull the username and password from query string" in { + parse("jdbc:mysql://localhost:425/dbname?user=user&password=password") mustEqual DEFAULT.copy( + username = "user", + password = Some("password"), + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + // Included for consistency, so later changes aren't allowed to change behavior + "use the query string parameters to override URI credentials" in { + parse("jdbc:mysql://baduser:badpass@localhost:425/dbname?user=user&password=password") mustEqual DEFAULT.copy( + username = "user", + password = Some("password"), + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "successfully default the port to the mysql port" in { + parse("jdbc:mysql://baduser:badpass@localhost/dbname?user=user&password=password") mustEqual DEFAULT.copy( + username = "user", + password = Some("password"), + database = Some("dbname"), + port = 3306, + host = "localhost" + ) + } + + "reject malformed ip addresses" in { + val connectionUri = "mysql://john:doe@128.567.54.90:9987/my_database" + + val configuration = parse(connectionUri) + configuration.username === "root" + configuration.password === None + configuration.database === None + configuration.host === "127.0.0.1" + configuration.port === 3306 + + parseOrDie(connectionUri) must throwA[UnableToParseURLException] + } + + } + +} diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala index 8c58076b..ec89660c 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala @@ -17,8 +17,8 @@ package com.github.mauricio.async.db.postgresql import com.github.mauricio.async.db.QueryResult -import com.github.mauricio.async.db.column.{ColumnEncoderRegistry, ColumnDecoderRegistry} -import com.github.mauricio.async.db.exceptions.{InsufficientParametersException, ConnectionStillRunningQueryException} +import com.github.mauricio.async.db.column.{ColumnDecoderRegistry, ColumnEncoderRegistry} +import com.github.mauricio.async.db.exceptions.{ConnectionStillRunningQueryException, InsufficientParametersException} import com.github.mauricio.async.db.general.MutableResultSet import com.github.mauricio.async.db.pool.TimeoutScheduler import com.github.mauricio.async.db.postgresql.codec.{PostgreSQLConnectionDelegate, PostgreSQLConnectionHandler} @@ -26,14 +26,17 @@ import com.github.mauricio.async.db.postgresql.column.{PostgreSQLColumnDecoderRe import com.github.mauricio.async.db.postgresql.exceptions._ import com.github.mauricio.async.db.util._ import com.github.mauricio.async.db.{Configuration, Connection} -import java.util.concurrent.atomic.{AtomicLong,AtomicInteger,AtomicReference} +import java.util.concurrent.atomic.{AtomicInteger, AtomicLong, AtomicReference} + import messages.backend._ import messages.frontend._ -import scala.Some + import scala.concurrent._ import io.netty.channel.EventLoopGroup import java.util.concurrent.CopyOnWriteArrayList +import com.github.mauricio.async.db.postgresql.util.URLParser + object PostgreSQLConnection { final val Counter = new AtomicLong() final val ServerVersionKey = "server_version" @@ -42,7 +45,7 @@ object PostgreSQLConnection { class PostgreSQLConnection ( - configuration: Configuration = Configuration.Default, + configuration: Configuration = URLParser.DEFAULT, encoderRegistry: ColumnEncoderRegistry = PostgreSQLColumnEncoderRegistry.Instance, decoderRegistry: ColumnDecoderRegistry = PostgreSQLColumnDecoderRegistry.Instance, group : EventLoopGroup = NettyUtils.DefaultEventLoopGroup, diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ParserURL.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ParserURL.scala deleted file mode 100644 index 8172877e..00000000 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ParserURL.scala +++ /dev/null @@ -1,65 +0,0 @@ -/** - * - */ -package com.github.mauricio.async.db.postgresql.util - -import org.slf4j.LoggerFactory - -/** - * @author gciuloaica - * - */ -object ParserURL { - - private val logger = LoggerFactory.getLogger(ParserURL.getClass()) - - val PGPORT = "port" - val PGDBNAME = "database" - val PGHOST = "host" - val PGUSERNAME = "user" - val PGPASSWORD = "password" - - val DEFAULT_PORT = "5432" - - private val pgurl1 = """(jdbc:postgresql):(?://([^/:]*|\[.+\])(?::(\d+))?)?(?:/([^/?]*))?(?:\?(.*))?""".r - private val pgurl2 = """(postgres|postgresql)://(.*):(.*)@(.*):(\d+)/([^/?]*)(?:\?(.*))?""".r - - def parse(connectionURL: String): Map[String, String] = { - val properties: Map[String, String] = Map() - - def parseOptions(optionsStr: String): Map[String, String] = - optionsStr.split("&").map { o => - o.span(_ != '=') match { - case (name, value) => name -> value.drop(1) - } - }.toMap - - connectionURL match { - case pgurl1(protocol, server, port, dbname, params) => { - var result = properties - if (server != null) result += (PGHOST -> unwrapIpv6address(server)) - if (dbname != null && dbname.nonEmpty) result += (PGDBNAME -> dbname) - if (port != null) result += (PGPORT -> port) - if (params != null) result ++= parseOptions(params) - result - } - case pgurl2(protocol, username, password, server, port, dbname, params) => { - var result = properties + (PGHOST -> unwrapIpv6address(server)) + (PGPORT -> port) + (PGDBNAME -> dbname) + (PGUSERNAME -> username) + (PGPASSWORD -> password) - if (params != null) result ++= parseOptions(params) - result - } - case _ => { - logger.warn(s"Connection url '$connectionURL' could not be parsed.") - properties - } - } - - } - - private def unwrapIpv6address(server: String): String = { - if (server.startsWith("[")) { - server.substring(1, server.length() - 1) - } else server - } - -} diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala index debcb6d9..fcb9b3cf 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala @@ -1,46 +1,72 @@ -/* - * Copyright 2013 Maurício Linhares +/** * - * Maurício Linhares licenses this file to you 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 com.github.mauricio.async.db.postgresql.util -import com.github.mauricio.async.db.{Configuration, SSLConfiguration} +import java.net.URI import java.nio.charset.Charset -object URLParser { +import com.github.mauricio.async.db.{Configuration, SSLConfiguration} +import com.github.mauricio.async.db.util.AbstractURIParser - import Configuration.Default +/** + * The PostgreSQL URL parser. + */ +object URLParser extends AbstractURIParser { + import AbstractURIParser._ - def parse(url: String, - charset: Charset = Default.charset - ): Configuration = { + // Alias these for anyone still making use of them + @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.PORT", since = "0.2.20") + val PGPORT = PORT - val properties = ParserURL.parse(url) + @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.DBNAME", since = "0.2.20") + val PGDBNAME = DBNAME - val port = properties.get(ParserURL.PGPORT).getOrElse(ParserURL.DEFAULT_PORT).toInt + @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.HOST", since = "0.2.20") + val PGHOST = HOST - new Configuration( - username = properties.get(ParserURL.PGUSERNAME).getOrElse(Default.username), - password = properties.get(ParserURL.PGPASSWORD), - database = properties.get(ParserURL.PGDBNAME), - host = properties.getOrElse(ParserURL.PGHOST, Default.host), - port = port, - ssl = SSLConfiguration(properties), - charset = charset - ) + @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.USERNAME", since = "0.2.20") + val PGUSERNAME = USERNAME + @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.PASSWORD", since = "0.2.20") + val PGPASSWORD = PASSWORD + + @deprecated("Use com.github.mauricio.async.db.postgresql.util.URLParser.DEFAULT.port", since = "0.2.20") + val DEFAULT_PORT = "5432" + + /** + * The default configuration for PostgreSQL. + */ + override val DEFAULT = Configuration( + username = "postgres", + host = "localhost", + port = 5432, + password = None, + database = None, + ssl = SSLConfiguration() + ) + + override protected val SCHEME = "^postgres(?:ql)?$".r + + private val simplePGDB = "^postgresql:(\\w+)$".r + + override protected def handleJDBC(uri: URI): Map[String, String] = uri.getSchemeSpecificPart match { + case simplePGDB(db) => Map(DBNAME -> db) + case x => parse(new URI(x)) } + /** + * Assembles a configuration out of the provided property map. This is the generic form, subclasses may override to + * handle additional properties. + * + * @param properties the extracted properties from the URL. + * @param charset the charset passed in to parse or parseOrDie. + * @return + */ + override protected def assembleConfiguration(properties: Map[String, String], charset: Charset): Configuration = { + // Add SSL Configuration + super.assembleConfiguration(properties, charset).copy( + ssl = SSLConfiguration(properties) + ) + } } diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/package.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/package.scala new file mode 100644 index 00000000..5d321170 --- /dev/null +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/package.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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 com.github.mauricio.async.db.postgresql + +/** + * Contains package level aliases and type renames. + */ +package object util { + + /** + * Alias to help compatibility. + */ + @deprecated("Use com.github.mauricio.async.db.postgresql.util.URLParser", since = "0.2.20") + val ParserURL = URLParser + +} diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala index d0df6eaa..9d2d2828 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala @@ -17,79 +17,93 @@ package com.github.mauricio.async.db.postgresql.util import org.specs2.mutable.Specification -import com.github.mauricio.async.db.Configuration -import com.github.mauricio.async.db.SSLConfiguration import com.github.mauricio.async.db.SSLConfiguration.Mode +import com.github.mauricio.async.db.exceptions.UnableToParseURLException class URLParserSpec extends Specification { - "parser" should { + "postgresql URLParser" should { + import URLParser.{parse, parseOrDie, DEFAULT} - "create a connection with the available fields" in { - val connectionUri = "jdbc:postgresql://128.567.54.90:9987/my_database?user=john&password=doe" + // Divided into sections + // =========== jdbc:postgresql =========== - val configuration = URLParser.parse(connectionUri) + // https://jdbc.postgresql.org/documentation/80/connect.html + "recognize a jdbc:postgresql:dbname uri" in { + val connectionUri = "jdbc:postgresql:dbname" + + parse(connectionUri) mustEqual DEFAULT.copy( + database = Some("dbname") + ) + } + + "create a jdbc:postgresql connection with the available fields" in { + val connectionUri = "jdbc:postgresql://128.167.54.90:9987/my_database?user=john&password=doe" + + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") configuration.database === Some("my_database") - configuration.host === "128.567.54.90" + configuration.host === "128.167.54.90" configuration.port === 9987 } "create a connection without port" in { - val connectionUri = "jdbc:postgresql://128.567.54.90/my_database?user=john&password=doe" + val connectionUri = "jdbc:postgresql://128.167.54.90/my_database?user=john&password=doe" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") configuration.database === Some("my_database") - configuration.host === "128.567.54.90" + configuration.host === "128.167.54.90" configuration.port === 5432 } "create a connection without username and password" in { - val connectionUri = "jdbc:postgresql://128.567.54.90:9987/my_database" + val connectionUri = "jdbc:postgresql://128.167.54.90:9987/my_database" - val configuration = URLParser.parse(connectionUri) - configuration.username === Configuration.Default.username + val configuration = parse(connectionUri) + configuration.username === DEFAULT.username configuration.password === None configuration.database === Some("my_database") - configuration.host === "128.567.54.90" + configuration.host === "128.167.54.90" configuration.port === 9987 } + //========== postgresql:// ============== + "create a connection from a heroku like URL using 'postgresql' protocol" in { - val connectionUri = "postgresql://john:doe@128.567.54.90:9987/my_database" + val connectionUri = "postgresql://john:doe@128.167.54.90:9987/my_database" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") configuration.database === Some("my_database") - configuration.host === "128.567.54.90" + configuration.host === "128.167.54.90" configuration.port === 9987 } "create a connection with SSL enabled" in { - val connectionUri = "jdbc:postgresql://128.567.54.90:9987/my_database?sslmode=verify-full" + val connectionUri = "jdbc:postgresql://128.167.54.90:9987/my_database?sslmode=verify-full" - val configuration = URLParser.parse(connectionUri) - configuration.username === Configuration.Default.username + val configuration = parse(connectionUri) + configuration.username === DEFAULT.username configuration.password === None configuration.database === Some("my_database") - configuration.host === "128.567.54.90" + configuration.host === "128.167.54.90" configuration.port === 9987 configuration.ssl.mode === Mode.VerifyFull } "create a connection with SSL enabled and root CA from a heroku like URL using 'postgresql' protocol" in { - val connectionUri = "postgresql://john:doe@128.567.54.90:9987/my_database?sslmode=verify-ca&sslrootcert=server.crt" + val connectionUri = "postgresql://john:doe@128.167.54.90:9987/my_database?sslmode=verify-ca&sslrootcert=server.crt" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") configuration.database === Some("my_database") - configuration.host === "128.567.54.90" + configuration.host === "128.167.54.90" configuration.port === 9987 configuration.ssl.mode === Mode.VerifyCA configuration.ssl.rootCert.map(_.getPath) === Some("server.crt") @@ -98,7 +112,7 @@ class URLParserSpec extends Specification { "create a connection with the available fields and named server" in { val connectionUri = "jdbc:postgresql://localhost:9987/my_database?user=john&password=doe" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") configuration.database === Some("my_database") @@ -109,7 +123,7 @@ class URLParserSpec extends Specification { "create a connection from a heroku like URL with named server" in { val connectionUri = "postgresql://john:doe@psql.heroku.com:9987/my_database" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") configuration.database === Some("my_database") @@ -120,7 +134,7 @@ class URLParserSpec extends Specification { "create a connection with the available fields and ipv6" in { val connectionUri = "jdbc:postgresql://[::1]:9987/my_database?user=john&password=doe" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") @@ -132,7 +146,7 @@ class URLParserSpec extends Specification { "create a connection from a heroku like URL and with ipv6" in { val connectionUri = "postgresql://john:doe@[::1]:9987/my_database" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") configuration.database === Some("my_database") @@ -143,7 +157,7 @@ class URLParserSpec extends Specification { "create a connection with a missing hostname" in { val connectionUri = "jdbc:postgresql:/my_database?user=john&password=doe" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") @@ -155,7 +169,7 @@ class URLParserSpec extends Specification { "create a connection with a missing database name" in { val connectionUri = "jdbc:postgresql://[::1]:9987/?user=john&password=doe" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "john" configuration.password === Some("doe") @@ -167,7 +181,7 @@ class URLParserSpec extends Specification { "create a connection with all default fields" in { val connectionUri = "jdbc:postgresql:" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "postgres" configuration.password === None @@ -179,7 +193,7 @@ class URLParserSpec extends Specification { "create a connection with an empty (invalid) url" in { val connectionUri = "" - val configuration = URLParser.parse(connectionUri) + val configuration = parse(connectionUri) configuration.username === "postgres" configuration.password === None @@ -188,6 +202,88 @@ class URLParserSpec extends Specification { configuration.port === 5432 } + + "recognise a postgresql:// uri" in { + parse("postgresql://localhost:425/dbname") mustEqual DEFAULT.copy( + username = "postgres", + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "recognise a postgres:// uri" in { + parse("postgres://localhost:425/dbname") mustEqual DEFAULT.copy( + username = "postgres", + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "recognize a jdbc:postgresql:// uri" in { + parse("jdbc:postgresql://localhost:425/dbname") mustEqual DEFAULT.copy( + username = "postgres", + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "pull the username and password from URI credentials" in { + parse("jdbc:postgresql://user:password@localhost:425/dbname") mustEqual DEFAULT.copy( + username = "user", + password = Some("password"), + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "pull the username and password from query string" in { + parse("jdbc:postgresql://localhost:425/dbname?user=user&password=password") mustEqual DEFAULT.copy( + username = "user", + password = Some("password"), + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + // Included for consistency, so later changes aren't allowed to change behavior + "use the query string parameters to override URI credentials" in { + parse("jdbc:postgresql://baduser:badpass@localhost:425/dbname?user=user&password=password") mustEqual DEFAULT.copy( + username = "user", + password = Some("password"), + database = Some("dbname"), + port = 425, + host = "localhost" + ) + } + + "successfully default the port to the PostgreSQL port" in { + parse("jdbc:postgresql://baduser:badpass@localhost/dbname?user=user&password=password") mustEqual DEFAULT.copy( + username = "user", + password = Some("password"), + database = Some("dbname"), + port = 5432, + host = "localhost" + ) + } + + "reject malformed ip addresses" in { + val connectionUri = "postgresql://john:doe@128.567.54.90:9987/my_database" + + val configuration = parse(connectionUri) + configuration.username === "postgres" + configuration.password === None + configuration.database === None + configuration.host === "localhost" + configuration.port === 5432 + + parseOrDie(connectionUri) must throwA[UnableToParseURLException] + } + } } From 8fb137a6fec7feb92e171e8b8060256d7799e18a Mon Sep 17 00:00:00 2001 From: Mansheng Yang Date: Thu, 12 May 2016 15:35:40 +0800 Subject: [PATCH 09/26] Fixed ByteBuf leaks PostgreSQLConnection.onDataRow should release the raw ByteBufs after decoding the data --- .../async/db/postgresql/PostgreSQLConnection.scala | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala index 8c58076b..1c2c08fe 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala @@ -191,11 +191,16 @@ class PostgreSQLConnection var x = 0 while ( x < m.values.size ) { - items(x) = if ( m.values(x) == null ) { + val buf = m.values(x) + items(x) = if ( buf == null ) { null } else { - val columnType = this.currentQuery.get.columnTypes(x) - this.decoderRegistry.decode(columnType, m.values(x), configuration.charset) + try { + val columnType = this.currentQuery.get.columnTypes(x) + this.decoderRegistry.decode(columnType, buf, configuration.charset) + } finally { + buf.release() + } } x += 1 } From 07dadc804041c2564ce386e535047da9e8b0a8ae Mon Sep 17 00:00:00 2001 From: Julien Viet Date: Tue, 7 Jun 2016 17:04:09 +0200 Subject: [PATCH 10/26] Upgrade to Netty 4.1.0 --- .../async/db/mysql/codec/LittleEndianByteBufAllocator.scala | 2 ++ project/Build.scala | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/LittleEndianByteBufAllocator.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/LittleEndianByteBufAllocator.scala index 40b51f24..0fdc790a 100644 --- a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/LittleEndianByteBufAllocator.scala +++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/LittleEndianByteBufAllocator.scala @@ -66,6 +66,8 @@ class LittleEndianByteBufAllocator extends ByteBufAllocator { def compositeDirectBuffer(maxNumComponents: Int): CompositeByteBuf = allocator.compositeDirectBuffer(maxNumComponents) + def calculateNewCapacity(minNewCapacity: Int, maxCapacity: Int): Int = allocator.calculateNewCapacity(minNewCapacity, maxCapacity) + private def littleEndian(b: ByteBuf) = b.order(ByteOrder.LITTLE_ENDIAN) } diff --git a/project/Build.scala b/project/Build.scala index 84cd916f..cf8b6861 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -62,7 +62,7 @@ object Configuration { "org.slf4j" % "slf4j-api" % "1.7.18", "joda-time" % "joda-time" % "2.9.2", "org.joda" % "joda-convert" % "1.8.1", - "io.netty" % "netty-all" % "4.0.34.Final", + "io.netty" % "netty-all" % "4.1.0.Final", "org.javassist" % "javassist" % "3.20.0-GA", specs2Dependency, specs2JunitDependency, From eed80b673a02e6efd5bd0d2d7db2de724f4b3894 Mon Sep 17 00:00:00 2001 From: Julien Viet Date: Wed, 8 Jun 2016 21:43:29 +0200 Subject: [PATCH 11/26] Update to 4.1.1.Final --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index cf8b6861..e02f3b3e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -62,7 +62,7 @@ object Configuration { "org.slf4j" % "slf4j-api" % "1.7.18", "joda-time" % "joda-time" % "2.9.2", "org.joda" % "joda-convert" % "1.8.1", - "io.netty" % "netty-all" % "4.1.0.Final", + "io.netty" % "netty-all" % "4.1.1.Final", "org.javassist" % "javassist" % "3.20.0-GA", specs2Dependency, specs2JunitDependency, From d51a85b9ffa96b91f79372c22353ad4073282830 Mon Sep 17 00:00:00 2001 From: Mauricio Linhares Date: Fri, 10 Jun 2016 00:13:13 -0400 Subject: [PATCH 12/26] Closing 0.2.20 --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index e02f3b3e..f771f803 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -49,7 +49,7 @@ object ProjectBuild extends Build { object Configuration { - val commonVersion = "0.2.20-SNAPSHOT" + val commonVersion = "0.2.20" val projectScalaVersion = "2.11.7" val specs2Version = "2.5" From 7dc83b91c153b74a1c94329ada43b3e15c51bb7f Mon Sep 17 00:00:00 2001 From: Mauricio Linhares Date: Fri, 10 Jun 2016 00:23:03 -0400 Subject: [PATCH 13/26] Starting next development cycle --- README.markdown | 8 ++++---- project/Build.scala | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.markdown b/README.markdown index b87583a2..9977b309 100644 --- a/README.markdown +++ b/README.markdown @@ -54,7 +54,7 @@ You can view the project's [CHANGELOG here](CHANGELOG.md). And if you're in a hurry, you can include them in your build like this, if you're using PostgreSQL: ```scala -"com.github.mauricio" %% "postgresql-async" % "0.2.19" +"com.github.mauricio" %% "postgresql-async" % "0.2.20" ``` Or Maven: @@ -63,14 +63,14 @@ Or Maven: com.github.mauricio postgresql-async_2.11 - 0.2.19 + 0.2.20 ``` And if you're into MySQL: ```scala -"com.github.mauricio" %% "mysql-async" % "0.2.19" +"com.github.mauricio" %% "mysql-async" % "0.2.20" ``` Or Maven: @@ -79,7 +79,7 @@ Or Maven: com.github.mauricio mysql-async_2.11 - 0.2.19 + 0.2.20 ``` diff --git a/project/Build.scala b/project/Build.scala index f771f803..ca5bcb9e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -49,7 +49,7 @@ object ProjectBuild extends Build { object Configuration { - val commonVersion = "0.2.20" + val commonVersion = "0.2.21-SNAPSHOT" val projectScalaVersion = "2.11.7" val specs2Version = "2.5" From 3d0fdef82de66d9c804ceab712013bf1d44e908a Mon Sep 17 00:00:00 2001 From: volth Date: Fri, 8 Jul 2016 16:47:37 +0000 Subject: [PATCH 14/26] Support java.net.InetAddress (encoding and decoding) and user-defined types (encoding only) --- .../db/column/InetAddressEncoderDecoder.scala | 36 ++++++ .../db/postgresql/column/ColumnTypes.scala | 2 + .../PostgreSQLColumnDecoderRegistry.scala | 4 + .../PostgreSQLColumnEncoderRegistry.scala | 49 +++++---- .../async/db/postgresql/ArrayTypesSpec.scala | 103 ++++++++++++------ 5 files changed, 136 insertions(+), 58 deletions(-) create mode 100644 db-async-common/src/main/scala/com/github/mauricio/async/db/column/InetAddressEncoderDecoder.scala diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/InetAddressEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/InetAddressEncoderDecoder.scala new file mode 100644 index 00000000..ecac853d --- /dev/null +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/InetAddressEncoderDecoder.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2013 Maurício Linhares + * + * Maurício Linhares licenses this file to you 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 com.github.mauricio.async.db.column + +import java.net.InetAddress +import sun.net.util.IPAddressUtil.{textToNumericFormatV4,textToNumericFormatV6} + +object InetAddressEncoderDecoder extends ColumnEncoderDecoder { + + override def decode(value: String): Any = { + if (value contains ':') { + InetAddress.getByAddress(textToNumericFormatV6(value)) + } else { + InetAddress.getByAddress(textToNumericFormatV4(value)) + } + } + + override def encode(value: Any): String = { + value.asInstanceOf[InetAddress].getHostAddress + } + +} diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala index 29c6b736..93fef482 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala @@ -67,6 +67,8 @@ object ColumnTypes { final val UUIDArray = 2951 final val XMLArray = 143 + final val Inet = 869 + final val InetArray = 1041 } /* diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala index 606bb442..5b4a47a7 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala @@ -46,6 +46,7 @@ class PostgreSQLColumnDecoderRegistry( charset : Charset = CharsetUtil.UTF_8 ) e private final val timeWithTimestampArrayDecoder = new ArrayDecoder(TimeWithTimezoneEncoderDecoder) private final val intervalArrayDecoder = new ArrayDecoder(PostgreSQLIntervalEncoderDecoder) private final val uuidArrayDecoder = new ArrayDecoder(UUIDEncoderDecoder) + private final val inetAddressArrayDecoder = new ArrayDecoder(InetAddressEncoderDecoder) override def decode(kind: ColumnData, value: ByteBuf, charset: Charset): Any = { decoderFor(kind.dataType).decode(kind, value, charset) @@ -114,6 +115,9 @@ class PostgreSQLColumnDecoderRegistry( charset : Charset = CharsetUtil.UTF_8 ) e case XMLArray => this.stringArrayDecoder case ByteA => ByteArrayEncoderDecoder + case Inet => InetAddressEncoderDecoder + case InetArray => this.inetAddressArrayDecoder + case _ => StringEncoderDecoder } } diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala index 5292839c..c9f95f43 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala @@ -52,6 +52,8 @@ class PostgreSQLColumnEncoderRegistry extends ColumnEncoderRegistry { classOf[BigDecimal] -> (BigDecimalEncoderDecoder -> ColumnTypes.Numeric), classOf[java.math.BigDecimal] -> (BigDecimalEncoderDecoder -> ColumnTypes.Numeric), + classOf[java.net.InetAddress] -> (InetAddressEncoderDecoder -> ColumnTypes.Inet), + classOf[java.util.UUID] -> (UUIDEncoderDecoder -> ColumnTypes.UUID), classOf[LocalDate] -> ( DateEncoderDecoder -> ColumnTypes.Date ), @@ -104,17 +106,12 @@ class PostgreSQLColumnEncoderRegistry extends ColumnEncoderRegistry { if (encoder.isDefined) { encoder.get._1.encode(value) } else { - - val view: Option[Traversable[Any]] = value match { - case i: java.lang.Iterable[_] => Some(i.toIterable) - case i: Traversable[_] => Some(i) - case i: Array[_] => Some(i.toIterable) - case _ => None - } - - view match { - case Some(collection) => encodeArray(collection) - case None => { + value match { + case i: java.lang.Iterable[_] => encodeArray(i.toIterable) + case i: Traversable[_] => encodeArray(i) + case i: Array[_] => encodeArray(i.toIterable) + case p: Product => encodeComposite(p) + case _ => { this.classesSequence.find(entry => entry._1.isAssignableFrom(value.getClass)) match { case Some(parent) => parent._2._1.encode(value) case None => value.toString @@ -126,14 +123,9 @@ class PostgreSQLColumnEncoderRegistry extends ColumnEncoderRegistry { } - private def encodeArray(collection: Traversable[_]): String = { - val builder = new StringBuilder() - - builder.append('{') - - val result = collection.map { + private def encodeComposite(p: Product): String = { + p.productIterator.map { item => - if (item == null || item == None) { "NULL" } else { @@ -143,13 +135,22 @@ class PostgreSQLColumnEncoderRegistry extends ColumnEncoderRegistry { this.encode(item) } } + }.mkString("(", ",", ")") + } - }.mkString(",") - - builder.append(result) - builder.append('}') - - builder.toString() + private def encodeArray(collection: Traversable[_]): String = { + collection.map { + item => + if (item == null || item == None) { + "NULL" + } else { + if (this.shouldQuote(item)) { + "\"" + this.encode(item).replaceAllLiterally("\\", """\\""").replaceAllLiterally("\"", """\"""") + "\"" + } else { + this.encode(item) + } + } + }.mkString("{", ",", "}") } private def shouldQuote(value: Any): Boolean = { diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ArrayTypesSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ArrayTypesSpec.scala index e941e145..5391588c 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ArrayTypesSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ArrayTypesSpec.scala @@ -16,31 +16,45 @@ package com.github.mauricio.async.db.postgresql -import com.github.mauricio.async.db.column.TimestampWithTimezoneEncoderDecoder +import com.github.mauricio.async.db.column.{TimestampWithTimezoneEncoderDecoder, InetAddressEncoderDecoder} import org.specs2.mutable.Specification +import java.net.InetAddress class ArrayTypesSpec extends Specification with DatabaseTestHelper { - - val simpleCreate = """create temp table type_test_table ( - bigserial_column bigserial not null, - smallint_column integer[] not null, - text_column text[] not null, - timestamp_column timestamp with time zone[] not null, - constraint bigserial_column_pkey primary key (bigserial_column) - )""" + // `uniq` allows sbt to run the tests concurrently as there is no CREATE TEMP TYPE + def simpleCreate(uniq: String) = s"""DROP TYPE IF EXISTS dir_$uniq; + CREATE TYPE direction_$uniq AS ENUM ('in','out'); + DROP TYPE IF EXISTS endpoint_$uniq; + CREATE TYPE endpoint_$uniq AS (ip inet, port integer); + create temp table type_test_table_$uniq ( + bigserial_column bigserial not null, + smallint_column integer[] not null, + text_column text[] not null, + inet_column inet[] not null, + direction_column direction_$uniq[] not null, + endpoint_column endpoint_$uniq[] not null, + timestamp_column timestamp with time zone[] not null, + constraint bigserial_column_pkey primary key (bigserial_column) + )""" + def simpleDrop(uniq: String) = s"""drop table if exists type_test_table_$uniq; + drop type if exists endpoint_$uniq; + drop type if exists direction_$uniq""" val insert = - """insert into type_test_table - (smallint_column, text_column, timestamp_column) + """insert into type_test_table_cptat + (smallint_column, text_column, inet_column, direction_column, endpoint_column, timestamp_column) values ( '{1,2,3,4}', '{"some,\"comma,separated,text","another line of text","fake\,backslash","real\\,backslash\\",NULL}', + '{"127.0.0.1","2002:15::1"}', + '{"in","out"}', + '{"(\"127.0.0.1\",80)","(\"2002:15::1\",443)"}', '{"2013-04-06 01:15:10.528-03","2013-04-06 01:15:08.528-03"}' )""" - val insertPreparedStatement = """insert into type_test_table - (smallint_column, text_column, timestamp_column) - values (?,?,?)""" + val insertPreparedStatement = """insert into type_test_table_csaups + (smallint_column, text_column, inet_column, direction_column, endpoint_column, timestamp_column) + values (?,?,?,?,?,?)""" "connection" should { @@ -48,41 +62,62 @@ class ArrayTypesSpec extends Specification with DatabaseTestHelper { withHandler { handler => - executeDdl(handler, simpleCreate) - executeDdl(handler, insert, 1) - val result = executeQuery(handler, "select * from type_test_table").rows.get - result(0)("smallint_column") === List(1,2,3,4) - result(0)("text_column") === List("some,\"comma,separated,text", "another line of text", "fake,backslash", "real\\,backslash\\", null ) - result(0)("timestamp_column") === List( - TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:10.528-03"), - TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:08.528-03") - ) + try { + executeDdl(handler, simpleCreate("cptat")) + executeDdl(handler, insert, 1) + val result = executeQuery(handler, "select * from type_test_table_cptat").rows.get + result(0)("smallint_column") === List(1,2,3,4) + result(0)("text_column") === List("some,\"comma,separated,text", "another line of text", "fake,backslash", "real\\,backslash\\", null ) + result(0)("timestamp_column") === List( + TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:10.528-03"), + TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:08.528-03") + ) + } finally { + executeDdl(handler, simpleDrop("cptat")) + } } } "correctly send arrays using prepared statements" in { + case class Endpoint(ip: InetAddress, port: Int) val timestamps = List( TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:10.528-03"), TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:08.528-03") ) + val inets = List( + InetAddressEncoderDecoder.decode("127.0.0.1"), + InetAddressEncoderDecoder.decode("2002:15::1") + ) + val directions = List("in", "out") + val endpoints = List( + Endpoint(InetAddress.getByName("127.0.0.1"), 80), // case class + (InetAddress.getByName("2002:15::1"), 443) // tuple + ) val numbers = List(1,2,3,4) val texts = List("some,\"comma,separated,text", "another line of text", "fake,backslash", "real\\,backslash\\", null ) withHandler { handler => - executeDdl(handler, simpleCreate) - executePreparedStatement( - handler, - this.insertPreparedStatement, - Array( numbers, texts, timestamps ) ) - - val result = executeQuery(handler, "select * from type_test_table").rows.get - - result(0)("smallint_column") === numbers - result(0)("text_column") === texts - result(0)("timestamp_column") === timestamps + try { + executeDdl(handler, simpleCreate("csaups")) + executePreparedStatement( + handler, + this.insertPreparedStatement, + Array( numbers, texts, inets, directions, endpoints, timestamps ) ) + + val result = executeQuery(handler, "select * from type_test_table_csaups").rows.get + + result(0)("smallint_column") === numbers + result(0)("text_column") === texts + result(0)("inet_column") === inets + result(0)("direction_column") === "{in,out}" // user type decoding not supported + result(0)("endpoint_column") === """{"(127.0.0.1,80)","(2002:15::1,443)"}""" // user type decoding not supported + result(0)("timestamp_column") === timestamps + } finally { + executeDdl(handler, simpleDrop("csaups")) + } } } From 42ea62150132fe93f709d05384a6c52b64592d30 Mon Sep 17 00:00:00 2001 From: xuwei-k <6b656e6a69@gmail.com> Date: Sat, 24 Sep 2016 14:34:03 +0900 Subject: [PATCH 15/26] update specs2 3.8.5 --- .../mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala | 3 ++- .../github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala | 1 + .../github/mauricio/async/db/mysql/StoredProceduresSpec.scala | 3 ++- .../com/github/mauricio/async/db/mysql/TransactionSpec.scala | 1 + .../async/db/postgresql/PostgreSQLConnectionSpec.scala | 3 ++- .../mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala | 1 + project/Build.scala | 3 ++- 7 files changed, 11 insertions(+), 4 deletions(-) diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala index 34ca0662..7c8bfdc4 100644 --- a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala +++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala @@ -10,7 +10,8 @@ import scala.util.Failure import scala.reflect.runtime.universe.TypeTag import scala.util.Try -import scala.concurrent.duration.{Duration, SECONDS} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ /** * This spec is designed abstract to allow testing of any implementation of AsyncObjectPool, against the common diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala index acc952e7..0c6d85b4 100644 --- a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala +++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala @@ -18,6 +18,7 @@ package com.github.mauricio.async.db.pool import java.util.concurrent.{ScheduledFuture, TimeoutException} import com.github.mauricio.async.db.util.{ByteBufferUtils, ExecutorServiceUtils} import org.specs2.mutable.SpecificationWithJUnit +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.concurrent.{Future, Promise} diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala index 3d68563b..d8ff2142 100644 --- a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala +++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala @@ -19,6 +19,7 @@ package com.github.mauricio.async.db.mysql import com.github.mauricio.async.db.ResultSet import com.github.mauricio.async.db.util.FutureUtils._ import org.specs2.mutable.Specification +import scala.concurrent.ExecutionContext.Implicits.global class StoredProceduresSpec extends Specification with ConnectionHelper { @@ -129,4 +130,4 @@ class StoredProceduresSpec extends Specification with ConnectionHelper { } } } -} \ No newline at end of file +} diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala index 0ef2f86b..83548c9b 100644 --- a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala +++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala @@ -10,6 +10,7 @@ import com.github.mauricio.async.db.Connection import scala.concurrent.duration.Duration import scala.concurrent.{Await, Future} +import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{Success, Failure} object TransactionSpec { diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala index 2843e95e..0e050477 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala @@ -30,6 +30,7 @@ import org.specs2.mutable.Specification import scala.concurrent.duration._ import scala.concurrent.{Await, Future} +import scala.concurrent.ExecutionContext.Implicits.global object PostgreSQLConnectionSpec { val log = Log.get[PostgreSQLConnectionSpec] @@ -154,7 +155,7 @@ class PostgreSQLConnectionSpec extends Specification with DatabaseTestHelper { row(10) === DateEncoderDecoder.decode("1984-08-06") row(11) === TimeEncoderDecoder.Instance.decode("22:13:45.888888") row(12) === true - row(13) must beAnInstanceOf[java.lang.Long] + row(13).asInstanceOf[AnyRef] must beAnInstanceOf[java.lang.Long] row(13).asInstanceOf[Long] must beGreaterThan(0L) diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala index b71ebe65..c2471a75 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala @@ -22,6 +22,7 @@ import com.github.mauricio.async.db.pool.{ConnectionPool, PoolConfiguration} import com.github.mauricio.async.db.postgresql.exceptions.GenericDatabaseException import com.github.mauricio.async.db.postgresql.{PostgreSQLConnection, DatabaseTestHelper} import org.specs2.mutable.Specification +import scala.concurrent.ExecutionContext.Implicits.global object ConnectionPoolSpec { val Insert = "insert into transaction_test (id) values (?)" diff --git a/project/Build.scala b/project/Build.scala index ca5bcb9e..f4fbb02a 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -51,7 +51,7 @@ object Configuration { val commonVersion = "0.2.21-SNAPSHOT" val projectScalaVersion = "2.11.7" - val specs2Version = "2.5" + val specs2Version = "3.8.5" val specs2Dependency = "org.specs2" %% "specs2-core" % specs2Version % "test" val specs2JunitDependency = "org.specs2" %% "specs2-junit" % specs2Version % "test" @@ -82,6 +82,7 @@ object Configuration { :+ Opts.compile.unchecked :+ "-feature" , + testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "sequential"), scalacOptions in doc := Seq("-doc-external-doc:scala=http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/"), crossScalaVersions := Seq(projectScalaVersion, "2.10.6"), javacOptions := Seq("-source", "1.6", "-target", "1.6", "-encoding", "UTF8"), From 1b49d11ea458b48b3cdb975ebc5463456db1362f Mon Sep 17 00:00:00 2001 From: xuwei-k <6b656e6a69@gmail.com> Date: Sat, 24 Sep 2016 14:29:33 +0900 Subject: [PATCH 16/26] use List instead of Stack Stack in deprecated https://github.com/scala/scala/commit/44a22d7cc0c315b9feaee1d4cb5df7a66578b1ea --- .../async/db/pool/SingleThreadedAsyncObjectPool.scala | 10 ++++++---- .../async/db/postgresql/column/ArrayDecoder.scala | 9 +++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala index 49f60593..b4f25ae2 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala @@ -22,7 +22,7 @@ import com.github.mauricio.async.db.util.{Log, Worker} import java.util.concurrent.atomic.AtomicLong import java.util.{Timer, TimerTask} -import scala.collection.mutable.{ArrayBuffer, Queue, Stack} +import scala.collection.mutable.{ArrayBuffer, Queue} import scala.concurrent.{Future, Promise} import scala.util.{Failure, Success} @@ -52,7 +52,7 @@ class SingleThreadedAsyncObjectPool[T]( import SingleThreadedAsyncObjectPool.{Counter, log} private val mainPool = Worker() - private var poolables = new Stack[PoolableHolder[T]]() + private var poolables = List.empty[PoolableHolder[T]] private val checkouts = new ArrayBuffer[T](configuration.maxObjects) private val waitQueue = new Queue[Promise[T]]() private val timer = new Timer("async-object-pool-timer-" + Counter.incrementAndGet(), true) @@ -171,7 +171,7 @@ class SingleThreadedAsyncObjectPool[T]( */ private def addBack(item: T, promise: Promise[AsyncObjectPool[T]]) { - this.poolables.push(new PoolableHolder[T](item)) + this.poolables ::= new PoolableHolder[T](item) if (this.waitQueue.nonEmpty) { this.checkout(this.waitQueue.dequeue()) @@ -226,7 +226,9 @@ class SingleThreadedAsyncObjectPool[T]( case e: Exception => promise.failure(e) } } else { - val item = this.poolables.pop().item + val h :: t = this.poolables + this.poolables = t + val item = h.item this.checkouts += item promise.success(item) } diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoder.scala index d69eeba4..b62e9629 100644 --- a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoder.scala +++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoder.scala @@ -19,7 +19,7 @@ package com.github.mauricio.async.db.postgresql.column import com.github.mauricio.async.db.column.ColumnDecoder import com.github.mauricio.async.db.postgresql.util.{ArrayStreamingParserDelegate, ArrayStreamingParser} import scala.collection.IndexedSeq -import scala.collection.mutable.{ArrayBuffer, Stack} +import scala.collection.mutable.ArrayBuffer import com.github.mauricio.async.db.general.ColumnData import io.netty.buffer.{Unpooled, ByteBuf} import java.nio.charset.Charset @@ -32,12 +32,13 @@ class ArrayDecoder(private val decoder: ColumnDecoder) extends ColumnDecoder { buffer.readBytes(bytes) val value = new String(bytes, charset) - val stack = new Stack[ArrayBuffer[Any]]() + var stack = List.empty[ArrayBuffer[Any]] var current: ArrayBuffer[Any] = null var result: IndexedSeq[Any] = null val delegate = new ArrayStreamingParserDelegate { override def arrayEnded { - result = stack.pop() + result = stack.head + stack = stack.tail } override def elementFound(element: String) { @@ -63,7 +64,7 @@ class ArrayDecoder(private val decoder: ColumnDecoder) extends ColumnDecoder { case None => {} } - stack.push(current) + stack ::= current } } From 630c65930f6837ebc5ee5d93314e1fc70512a1e2 Mon Sep 17 00:00:00 2001 From: Sergey Samoylov Date: Fri, 30 Sep 2016 14:06:04 +0300 Subject: [PATCH 17/26] Fix for CLIENT_MULTI_RESULTS constant value --- .../scala/com/github/mauricio/async/db/mysql/util/MySQLIO.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/MySQLIO.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/MySQLIO.scala index 4587eb09..3b56ecc0 100644 --- a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/MySQLIO.scala +++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/MySQLIO.scala @@ -21,7 +21,7 @@ object MySQLIO { final val CLIENT_PROTOCOL_41 = 0x0200 final val CLIENT_CONNECT_WITH_DB = 0x0008 final val CLIENT_TRANSACTIONS = 0x2000 - final val CLIENT_MULTI_RESULTS = 0x200000 + final val CLIENT_MULTI_RESULTS = 0x20000 final val CLIENT_LONG_FLAG = 0x0001 final val CLIENT_PLUGIN_AUTH = 0x00080000 final val CLIENT_SECURE_CONNECTION = 0x00008000 From 2a2896fd22e8e833ba6deca1e7d85944a060f7b0 Mon Sep 17 00:00:00 2001 From: golem131 Date: Sat, 5 Nov 2016 14:02:11 +0300 Subject: [PATCH 18/26] Fix deprecation warning "constructor Slf4JLoggerFactory in class Slf4JLoggerFactory is deprecated: see corresponding Javadoc for more information" --- .../scala/com/github/mauricio/async/db/util/NettyUtils.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/NettyUtils.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/NettyUtils.scala index 32f736e3..c9e09f1a 100644 --- a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/NettyUtils.scala +++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/NettyUtils.scala @@ -20,7 +20,7 @@ import io.netty.util.internal.logging.{InternalLoggerFactory, Slf4JLoggerFactory object NettyUtils { - InternalLoggerFactory.setDefaultFactory(new Slf4JLoggerFactory()) + InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE) lazy val DefaultEventLoopGroup = new NioEventLoopGroup(0, DaemonThreadsFactory("db-async-netty")) } \ No newline at end of file From 4b6c380a35de8ee242188f58c3b7e71fad47917c Mon Sep 17 00:00:00 2001 From: golem131 Date: Mon, 7 Nov 2016 12:39:47 +0300 Subject: [PATCH 19/26] Wait until connection return to pool --- .../SingleThreadedAsyncObjectPoolSpec.scala | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/SingleThreadedAsyncObjectPoolSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/SingleThreadedAsyncObjectPoolSpec.scala index d99a60d1..75da1ebd 100644 --- a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/SingleThreadedAsyncObjectPoolSpec.scala +++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/SingleThreadedAsyncObjectPoolSpec.scala @@ -16,12 +16,14 @@ package com.github.mauricio.async.db.postgresql.pool -import com.github.mauricio.async.db.pool.{SingleThreadedAsyncObjectPool, PoolExhaustedException, PoolConfiguration} +import com.github.mauricio.async.db.pool.{AsyncObjectPool, PoolConfiguration, PoolExhaustedException, SingleThreadedAsyncObjectPool} import com.github.mauricio.async.db.postgresql.{DatabaseTestHelper, PostgreSQLConnection} import java.nio.channels.ClosedChannelException import java.util.concurrent.TimeUnit + import org.specs2.mutable.Specification -import scala.concurrent.Await + +import scala.concurrent.{Await, Future} import scala.concurrent.duration._ import scala.language.postfixOps import com.github.mauricio.async.db.exceptions.ConnectionStillRunningQueryException @@ -47,23 +49,36 @@ class SingleThreadedAsyncObjectPoolSpec extends Specification with DatabaseTestH pool => val connection = get(pool) - val promises = List(pool.take, pool.take, pool.take) + val promises: List[Future[PostgreSQLConnection]] = List(pool.take, pool.take, pool.take) pool.availables.size === 0 pool.inUse.size === 1 + pool.queued.size must be_<=(3) + + /* pool.take call checkout that call this.mainPool.action, + so enqueuePromise called in executorService, + so there is no guaranties that all promises in queue at that moment + */ + val deadline = 5.seconds.fromNow + while(pool.queued.size < 3 || deadline.hasTimeLeft) { + Thread.sleep(50) + } + pool.queued.size === 3 executeTest(connection) pool.giveBack(connection) - promises.foreach { + val pools: List[Future[AsyncObjectPool[PostgreSQLConnection]]] = promises.map { promise => val connection = Await.result(promise, Duration(5, TimeUnit.SECONDS)) executeTest(connection) pool.giveBack(connection) } + Await.ready(pools.last, Duration(5, TimeUnit.SECONDS)) + pool.availables.size === 1 pool.inUse.size === 0 pool.queued.size === 0 From f75679dd4a9e200636614122a73a18f876a56129 Mon Sep 17 00:00:00 2001 From: golem131 Date: Thu, 3 Nov 2016 17:29:11 +0300 Subject: [PATCH 20/26] Scala 2.12.1 support --- .travis.yml | 6 ++++++ project/Build.scala | 4 ++-- project/build.properties | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c1a7a84..378c49d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,12 @@ scala: jdk: - oraclejdk7 - oraclejdk8 + +matrix: + include: + - scala: 2.12.1 + jdk: oraclejdk8 + services: - postgresql - mysql diff --git a/project/Build.scala b/project/Build.scala index f4fbb02a..e1de52d9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -51,7 +51,7 @@ object Configuration { val commonVersion = "0.2.21-SNAPSHOT" val projectScalaVersion = "2.11.7" - val specs2Version = "3.8.5" + val specs2Version = "3.8.6" val specs2Dependency = "org.specs2" %% "specs2-core" % specs2Version % "test" val specs2JunitDependency = "org.specs2" %% "specs2-junit" % specs2Version % "test" @@ -84,7 +84,7 @@ object Configuration { , testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "sequential"), scalacOptions in doc := Seq("-doc-external-doc:scala=http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/"), - crossScalaVersions := Seq(projectScalaVersion, "2.10.6"), + crossScalaVersions := Seq(projectScalaVersion, "2.10.6", "2.12.1"), javacOptions := Seq("-source", "1.6", "-target", "1.6", "-encoding", "UTF8"), organization := "com.github.mauricio", version := commonVersion, diff --git a/project/build.properties b/project/build.properties index d638b4f3..e0cbc71d 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 0.13.8 \ No newline at end of file +sbt.version = 0.13.13 \ No newline at end of file From 2f4444e745c1d1164f6f78ab3244de16593c1a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Linhares?= Date: Mon, 9 Jan 2017 10:42:11 -0300 Subject: [PATCH 21/26] preparing for 0.2.21 --- CHANGELOG.md | 4 ++++ Vagrantfile | 13 ------------- project/Build.scala | 14 +++++++------- project/plugins.sbt | 4 +++- 4 files changed, 14 insertions(+), 21 deletions(-) delete mode 100644 Vagrantfile diff --git a/CHANGELOG.md b/CHANGELOG.md index bafc831d..9ac99d03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ # Changelog +## 0.2.20 - 2017-09-17 + +* Building for Scala 2.12; + ## 0.2.19 - 2016-03-17 * Always use `NUMERIC` when handling numbers in prepared statements in PostgreSQL; diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index 5498f80c..00000000 --- a/Vagrantfile +++ /dev/null @@ -1,13 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - - config.vm.box = "chef/centos-6.5" - config.vm.provision :shell, path: "bootstrap.sh" - config.vm.network :forwarded_port, host: 3307, guest: 3306 - -end diff --git a/project/Build.scala b/project/Build.scala index e1de52d9..86ac4278 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -50,20 +50,20 @@ object ProjectBuild extends Build { object Configuration { val commonVersion = "0.2.21-SNAPSHOT" - val projectScalaVersion = "2.11.7" + val projectScalaVersion = "2.12.1" val specs2Version = "3.8.6" val specs2Dependency = "org.specs2" %% "specs2-core" % specs2Version % "test" val specs2JunitDependency = "org.specs2" %% "specs2-junit" % specs2Version % "test" val specs2MockDependency = "org.specs2" %% "specs2-mock" % specs2Version % "test" - val logbackDependency = "ch.qos.logback" % "logback-classic" % "1.1.6" % "test" + val logbackDependency = "ch.qos.logback" % "logback-classic" % "1.1.8" % "test" val commonDependencies = Seq( - "org.slf4j" % "slf4j-api" % "1.7.18", - "joda-time" % "joda-time" % "2.9.2", + "org.slf4j" % "slf4j-api" % "1.7.22", + "joda-time" % "joda-time" % "2.9.7", "org.joda" % "joda-convert" % "1.8.1", - "io.netty" % "netty-all" % "4.1.1.Final", - "org.javassist" % "javassist" % "3.20.0-GA", + "io.netty" % "netty-all" % "4.1.6.Final", + "org.javassist" % "javassist" % "3.21.0-GA", specs2Dependency, specs2JunitDependency, specs2MockDependency, @@ -84,7 +84,7 @@ object Configuration { , testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "sequential"), scalacOptions in doc := Seq("-doc-external-doc:scala=http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/"), - crossScalaVersions := Seq(projectScalaVersion, "2.10.6", "2.12.1"), + crossScalaVersions := Seq(projectScalaVersion, "2.10.6", "2.11.8"), javacOptions := Seq("-source", "1.6", "-target", "1.6", "-encoding", "UTF8"), organization := "com.github.mauricio", version := commonVersion, diff --git a/project/plugins.sbt b/project/plugins.sbt index 4528f2d6..d271b7f7 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,6 +2,8 @@ addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0") addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") -addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") + +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.0") resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases" From 94a7ae428840e5ef948c8307591ab526521c753b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Linhares?= Date: Mon, 9 Jan 2017 11:14:22 -0300 Subject: [PATCH 22/26] Remove JDK7 from build targets --- .travis.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 378c49d0..3e334f1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,15 +2,11 @@ language: scala scala: - 2.10.4 - 2.11.7 + - 2.12.1 + jdk: - - oraclejdk7 - oraclejdk8 -matrix: - include: - - scala: 2.12.1 - jdk: oraclejdk8 - services: - postgresql - mysql From b62199294a01f6c350835b9b22fbb4954bbf3195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Linhares?= Date: Mon, 9 Jan 2017 13:53:30 -0300 Subject: [PATCH 23/26] Closing 0.2.21 --- CHANGELOG.md | 1 + project/Build.scala | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac99d03..ce4b61ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ ## 0.2.20 - 2017-09-17 * Building for Scala 2.12; +* Fix SFL4J deprecation warning - #201 - @golem131; ## 0.2.19 - 2016-03-17 diff --git a/project/Build.scala b/project/Build.scala index 86ac4278..f70240ff 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -49,7 +49,7 @@ object ProjectBuild extends Build { object Configuration { - val commonVersion = "0.2.21-SNAPSHOT" + val commonVersion = "0.2.21" val projectScalaVersion = "2.12.1" val specs2Version = "3.8.6" From f031625d5e38dae100045437bd23e6e6d6e9dc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Linhares?= Date: Mon, 9 Jan 2017 14:35:01 -0300 Subject: [PATCH 24/26] Starting next development cycle --- README.markdown | 8 ++++---- project/Build.scala | 2 +- project/plugins.sbt | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.markdown b/README.markdown index 9977b309..75d25168 100644 --- a/README.markdown +++ b/README.markdown @@ -54,7 +54,7 @@ You can view the project's [CHANGELOG here](CHANGELOG.md). And if you're in a hurry, you can include them in your build like this, if you're using PostgreSQL: ```scala -"com.github.mauricio" %% "postgresql-async" % "0.2.20" +"com.github.mauricio" %% "postgresql-async" % "0.2.21" ``` Or Maven: @@ -63,14 +63,14 @@ Or Maven: com.github.mauricio postgresql-async_2.11 - 0.2.20 + 0.2.21 ``` And if you're into MySQL: ```scala -"com.github.mauricio" %% "mysql-async" % "0.2.20" +"com.github.mauricio" %% "mysql-async" % "0.2.21" ``` Or Maven: @@ -79,7 +79,7 @@ Or Maven: com.github.mauricio mysql-async_2.11 - 0.2.20 + 0.2.21 ``` diff --git a/project/Build.scala b/project/Build.scala index f70240ff..b543b050 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -49,7 +49,7 @@ object ProjectBuild extends Build { object Configuration { - val commonVersion = "0.2.21" + val commonVersion = "0.2.22-SNAPSHOT" val projectScalaVersion = "2.12.1" val specs2Version = "3.8.6" diff --git a/project/plugins.sbt b/project/plugins.sbt index d271b7f7..0e9ec632 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,3 +7,5 @@ addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.0") resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases" + +// pgpSigningKey := Some(0xB98761578C650D77L) From ef3e27b8c34df8b5d4ef638c0113b4c951cb4c98 Mon Sep 17 00:00:00 2001 From: Dominik Dorn Date: Tue, 28 Feb 2017 20:15:31 +0100 Subject: [PATCH 25/26] updated README to mention Scala 2.12 support --- README.markdown | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index 75d25168..b15d4885 100644 --- a/README.markdown +++ b/README.markdown @@ -1,7 +1,7 @@ -- [[![Build Status](https://travis-ci.org/mauricio/postgresql-async.png)](https://travis-ci.org/mauricio/postgresql-async) postgresql-async & mysql-async - async, Netty based, database drivers for MySQL and PostgreSQL written in Scala 2.10 and 2.11](#!build-statushttpstravis-ciorgmauriciopostgresql-asyncpnghttpstravis-ciorgmauriciopostgresql-async-postgresql-async-&-mysql-async---async-netty-based-database-drivers-for-mysql-and-postgresql-written-in-scala-210-and-211) +- [[![Build Status](https://travis-ci.org/mauricio/postgresql-async.png)](https://travis-ci.org/mauricio/postgresql-async) postgresql-async & mysql-async - async, Netty based, database drivers for MySQL and PostgreSQL written in Scala 2.10, 2.11 and 2.12](#!build-statushttpstravis-ciorgmauriciopostgresql-asyncpnghttpstravis-ciorgmauriciopostgresql-async-postgresql-async-&-mysql-async---async-netty-based-database-drivers-for-mysql-and-postgresql-written-in-scala-210-and-211) - [Abstractions and integrations](#abstractions-and-integrations) - [Include them as dependencies](#include-them-as-dependencies) - [Database connections and encodings](#database-connections-and-encodings) @@ -22,7 +22,7 @@ -# [![Build Status](https://travis-ci.org/mauricio/postgresql-async.png)](https://travis-ci.org/mauricio/postgresql-async) postgresql-async & mysql-async - async, Netty based, database drivers for MySQL and PostgreSQL written in Scala 2.10 and 2.11 +# [![Build Status](https://travis-ci.org/mauricio/postgresql-async.png)](https://travis-ci.org/mauricio/postgresql-async) postgresql-async & mysql-async - async, Netty based, database drivers for MySQL and PostgreSQL written in Scala 2.10, 2.11 and 2.12 The main goal for this project is to implement simple, async, performant and reliable database drivers for PostgreSQL and MySQL in Scala. This is not supposed to be a JDBC replacement, these drivers aim to cover the common @@ -67,6 +67,15 @@ Or Maven: ``` +respectively for Scala 2.12: +```xml + + com.github.mauricio + postgresql-async_2.12 + 0.2.21 + +``` + And if you're into MySQL: ```scala @@ -82,6 +91,14 @@ Or Maven: 0.2.21 ``` +respectively for Scala 2.12: +```xml + + com.github.mauricio + mysql-async_2.12 + 0.2.21 + +``` ## Database connections and encodings From 5716ac43818b6be0dc4fcc2b2655dde3411cdbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Linhares?= Date: Tue, 21 Aug 2018 13:52:25 -0400 Subject: [PATCH 26/26] Adding message with project not being maintained anymore --- README.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index b15d4885..79f4b057 100644 --- a/README.markdown +++ b/README.markdown @@ -1,7 +1,7 @@ -- [[![Build Status](https://travis-ci.org/mauricio/postgresql-async.png)](https://travis-ci.org/mauricio/postgresql-async) postgresql-async & mysql-async - async, Netty based, database drivers for MySQL and PostgreSQL written in Scala 2.10, 2.11 and 2.12](#!build-statushttpstravis-ciorgmauriciopostgresql-asyncpnghttpstravis-ciorgmauriciopostgresql-async-postgresql-async-&-mysql-async---async-netty-based-database-drivers-for-mysql-and-postgresql-written-in-scala-210-and-211) +- This project is not being maintained anymore, feel free to fork and work on it - [Abstractions and integrations](#abstractions-and-integrations) - [Include them as dependencies](#include-them-as-dependencies) - [Database connections and encodings](#database-connections-and-encodings) @@ -22,7 +22,7 @@ -# [![Build Status](https://travis-ci.org/mauricio/postgresql-async.png)](https://travis-ci.org/mauricio/postgresql-async) postgresql-async & mysql-async - async, Netty based, database drivers for MySQL and PostgreSQL written in Scala 2.10, 2.11 and 2.12 +# [![Build Status](https://travis-ci.org/mauricio/postgresql-async.png)](https://travis-ci.org/mauricio/postgresql-async) This project is not being maintained anymore, feel free to fork and work on it The main goal for this project is to implement simple, async, performant and reliable database drivers for PostgreSQL and MySQL in Scala. This is not supposed to be a JDBC replacement, these drivers aim to cover the common