Skip to content

Commit cc58769

Browse files
authored
Merge pull request mauricio#178 from SattaiLanfear/urlparse-updates
Reworked URLParser to process more URLs. Added MySQL URLParser
2 parents 8d28a01 + 226ed09 commit cc58769

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)