Skip to content

Commit 226ed09

Browse files
committed
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.
1 parent 773a28d commit 226ed09

File tree

10 files changed

+726
-133
lines changed

10 files changed

+726
-133
lines changed

db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import scala.concurrent.duration._
2525

2626
object Configuration {
2727
val DefaultCharset = CharsetUtil.UTF_8
28+
29+
@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")
2830
val Default = new Configuration("postgres")
2931
}
3032

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2016 Maurício Linhares
3+
*
4+
* Maurício Linhares licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.github.mauricio.async.db.exceptions
18+
19+
/**
20+
* Thrown to indicate that a URL Parser could not understand the provided URL.
21+
*/
22+
class UnableToParseURLException(message: String, base: Throwable) extends RuntimeException(message, base) {
23+
def this(message: String) = this(message, null)
24+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright 2016 Maurício Linhares
3+
*
4+
* Maurício Linhares licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package com.github.mauricio.async.db.util
17+
18+
import java.net.{URI, URISyntaxException, URLDecoder}
19+
import java.nio.charset.Charset
20+
21+
import com.github.mauricio.async.db.exceptions.UnableToParseURLException
22+
import com.github.mauricio.async.db.{Configuration, SSLConfiguration}
23+
import org.slf4j.LoggerFactory
24+
25+
import scala.util.matching.Regex
26+
27+
/**
28+
* Common parser assisting methods for PG and MySQL URI parsers.
29+
*/
30+
abstract class AbstractURIParser {
31+
import AbstractURIParser._
32+
33+
protected val logger = LoggerFactory.getLogger(getClass)
34+
35+
/**
36+
* Parses out userInfo into a tuple of optional username and password
37+
*
38+
* @param userInfo the optional user info string
39+
* @return a tuple of optional username and password
40+
*/
41+
final protected def parseUserInfo(userInfo: Option[String]): (Option[String], Option[String]) = userInfo.map(_.split(":", 2).toList) match {
42+
case Some(user :: pass :: Nil) (Some(user), Some(pass))
43+
case Some(user :: Nil) (Some(user), None)
44+
case _ (None, None)
45+
}
46+
47+
/**
48+
* A Regex that will match the base name of the driver scheme, minus jdbc:.
49+
* Eg: postgres(?:ul)?
50+
*/
51+
protected val SCHEME: Regex
52+
53+
/**
54+
* The default for this particular URLParser, ie: appropriate and specific to PG or MySQL accordingly
55+
*/
56+
val DEFAULT: Configuration
57+
58+
59+
/**
60+
* Parses the provided url and returns a Configuration based upon it. On an error,
61+
* @param url the URL to parse.
62+
* @param charset the charset to use.
63+
* @return a Configuration.
64+
*/
65+
@throws[UnableToParseURLException]("if the URL does not match the expected type, or cannot be parsed for any reason")
66+
def parseOrDie(url: String,
67+
charset: Charset = DEFAULT.charset): Configuration = {
68+
try {
69+
val properties = parse(new URI(url).parseServerAuthority)
70+
71+
assembleConfiguration(properties, charset)
72+
} catch {
73+
case e: URISyntaxException =>
74+
throw new UnableToParseURLException(s"Failed to parse URL: $url", e)
75+
}
76+
}
77+
78+
79+
/**
80+
* Parses the provided url and returns a Configuration based upon it. On an error,
81+
* a default configuration is returned.
82+
* @param url the URL to parse.
83+
* @param charset the charset to use.
84+
* @return a Configuration.
85+
*/
86+
def parse(url: String,
87+
charset: Charset = DEFAULT.charset
88+
): Configuration = {
89+
try {
90+
parseOrDie(url, charset)
91+
} catch {
92+
case e: Exception =>
93+
logger.warn(s"Connection url '$url' could not be parsed.", e)
94+
// Fallback to default to maintain current behavior
95+
DEFAULT
96+
}
97+
}
98+
99+
/**
100+
* Assembles a configuration out of the provided property map. This is the generic form, subclasses may override to
101+
* handle additional properties.
102+
* @param properties the extracted properties from the URL.
103+
* @param charset the charset passed in to parse or parseOrDie.
104+
* @return
105+
*/
106+
protected def assembleConfiguration(properties: Map[String, String], charset: Charset): Configuration = {
107+
DEFAULT.copy(
108+
username = properties.getOrElse(USERNAME, DEFAULT.username),
109+
password = properties.get(PASSWORD),
110+
database = properties.get(DBNAME),
111+
host = properties.getOrElse(HOST, DEFAULT.host),
112+
port = properties.get(PORT).map(_.toInt).getOrElse(DEFAULT.port),
113+
ssl = SSLConfiguration(properties),
114+
charset = charset
115+
)
116+
}
117+
118+
119+
protected def parse(uri: URI): Map[String, String] = {
120+
uri.getScheme match {
121+
case SCHEME() =>
122+
val userInfo = parseUserInfo(Option(uri.getUserInfo))
123+
124+
val port = Some(uri.getPort).filter(_ > 0)
125+
val db = Option(uri.getPath).map(_.stripPrefix("/")).filterNot(_.isEmpty)
126+
val host = Option(uri.getHost)
127+
128+
val builder = Map.newBuilder[String, String]
129+
builder ++= userInfo._1.map(USERNAME -> _)
130+
builder ++= userInfo._2.map(PASSWORD -> _)
131+
builder ++= port.map(PORT -> _.toString)
132+
builder ++= db.map(DBNAME -> _)
133+
builder ++= host.map(HOST -> unwrapIpv6address(_))
134+
135+
// Parse query string parameters and just append them, overriding anything previously set
136+
builder ++= (for {
137+
qs <- Option(uri.getQuery).toSeq
138+
parameter <- qs.split('&')
139+
Array(name, value) = parameter.split('=')
140+
if name.nonEmpty && value.nonEmpty
141+
} yield URLDecoder.decode(name, "UTF-8") -> URLDecoder.decode(value, "UTF-8"))
142+
143+
144+
builder.result
145+
case "jdbc" =>
146+
handleJDBC(uri)
147+
case _ =>
148+
throw new UnableToParseURLException("Unrecognized URI scheme")
149+
}
150+
}
151+
152+
/**
153+
* This method breaks out handling of the jdbc: prefixed uri's, allowing them to be handled differently
154+
* without reimplementing all of parse.
155+
*/
156+
protected def handleJDBC(uri: URI): Map[String, String] = parse(new URI(uri.getSchemeSpecificPart))
157+
158+
159+
final protected def unwrapIpv6address(server: String): String = {
160+
if (server.startsWith("[")) {
161+
server.substring(1, server.length() - 1)
162+
} else server
163+
}
164+
165+
}
166+
167+
object AbstractURIParser {
168+
// Constants and value names
169+
val PORT = "port"
170+
val DBNAME = "database"
171+
val HOST = "host"
172+
val USERNAME = "user"
173+
val PASSWORD = "password"
174+
}
175+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2016 Maurício Linhares
3+
*
4+
* Maurício Linhares licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package com.github.mauricio.async.db.mysql.util
17+
18+
import com.github.mauricio.async.db.util.AbstractURIParser
19+
import com.github.mauricio.async.db.Configuration
20+
21+
/**
22+
* The MySQL URL parser.
23+
*/
24+
object URLParser extends AbstractURIParser {
25+
26+
/**
27+
* The default configuration for MySQL.
28+
*/
29+
override val DEFAULT = Configuration(
30+
username = "root",
31+
host = "127.0.0.1", //Matched JDBC default
32+
port = 3306,
33+
password = None,
34+
database = None
35+
)
36+
37+
override protected val SCHEME = "^mysql$".r
38+
39+
}

0 commit comments

Comments
 (0)