io.youi.net.URL.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of youi-core_sjs0.6_2.13 Show documentation
Show all versions of youi-core_sjs0.6_2.13 Show documentation
Core functionality leveraged and shared by most other sub-projects of YouI.
The newest version!
package io.youi.net
import io.circe.Decoder.Result
import io.circe.{Decoder, DecodingFailure, Encoder, HCursor, Json}
import scala.reflect.macros.blackbox
import scala.util.matching.Regex
import scala.language.experimental.macros
case class URL(protocol: Protocol = Protocol.Http,
host: String = "localhost",
port: Int = 80,
path: Path = Path.empty,
parameters: Parameters = Parameters.empty,
fragment: Option[String] = None) extends Location {
lazy val ip: Option[IP] = IP.get(host)
lazy val tld: Option[String] = {
val lastDot = host.lastIndexOf('.')
if (lastDot != -1 && ip.isEmpty) {
Some(host.substring(lastDot + 1))
} else {
None
}
}
def replaceBase(base: String): URL = URL(s"$base${encoded.pathAndArgs}")
def replacePathAndParams(pathAndParams: String): URL = URL(s"$base$pathAndParams")
def withProtocol(protocol: Protocol): URL = copy(protocol = protocol)
def withPart(part: String): URL = if (part.indexOf("://") != -1) {
URL(part)
} else if (part.startsWith("//")) {
URL(s"${protocol.scheme}:$part")
} else if (part.startsWith("?")) {
copy(parameters = Parameters.parse(part))
} else if (part.startsWith("/") || part.startsWith("..")) {
val index = part.indexOf('?')
if (index == -1) {
withPath(part).copy(parameters = Parameters.empty)
} else {
val path = part.substring(0, index)
val params = part.substring(index + 1)
withPath(path).copy(parameters = Parameters.parse(params))
}
} else {
URL(s"$toString/$part")
}
def withPath(path: String, absolutize: Boolean = true): URL = {
val updated = this.path.append(path).absolute
copy(path = updated)
}
def withPath(path: Path): URL = copy(path = path)
def withFragment(fragment: String): URL = copy(fragment = Option(fragment))
def withoutFragment(): URL = copy(fragment = None)
def withParam(key: String, value: String, append: Boolean = true): URL = {
copy(parameters = parameters.withParam(key, value, append))
}
def withParams(params: Map[String, String], append: Boolean = false): URL = {
var u = this
params.foreach {
case (key, value) => u = u.withParam(key, value, append)
}
u
}
def appendParam(key: String, value: String): URL = copy(parameters = parameters.appendParam(key, value))
def replaceParam(key: String, values: List[String]): URL = copy(parameters = parameters.replaceParam(key, values))
def removeParam(key: String): URL = copy(parameters = parameters.removeParam(key))
def paramList(key: String): List[String] = parameters.values(key)
def param(key: String): Option[String] = paramList(key).headOption
def clearParams(): URL = copy(parameters = Parameters.empty)
lazy val base: String = {
val b = new StringBuilder
b.append(protocol.scheme)
b.append("://")
b.append(host)
if (!protocol.defaultPort.contains(port) && port != -1) {
b.append(s":$port") // Not using the default port for the protocol
}
b.toString()
}
lazy val encoded: URLParts = new URLParts(encoded = true)
lazy val decoded: URLParts = new URLParts(encoded = false)
/**
* Encodes this URL as a complete path. This is primarily useful for caching to a file while avoiding duplicates with
* the same file name. For example:
*
* http://www.example.com/some/path/file.txt
*
* Would be encoded to:
*
* /www.example.com/some/path/file.txt
*
* @param includePort whether the port should be included as a part of the path. Defaults to false.
*/
def asPath(includePort: Boolean = false): String = if (includePort) {
s"/$host/$port${path.encoded}"
} else {
s"/$host${path.encoded}"
}
override def equals(obj: scala.Any): Boolean = obj match {
case url: URL => url.toString == toString
case _ => false
}
override def toString: String = encoded.asString
class URLParts(encoded: Boolean) {
def base: String = URL.this.base
lazy val pathAndArgs: String = {
val b = new StringBuilder
b.append(path)
b.append(if (encoded) parameters.encoded else parameters.decoded)
fragment.foreach { f =>
b.append('#')
b.append(f)
}
b.toString()
}
lazy val asString: String = s"$base$pathAndArgs"
override def toString: String = asString
}
}
object URL {
var DefaultProtocol: Protocol = Protocol.Https
var ValidateTLD: Boolean = true
implicit val encoder: Encoder[URL] = new Encoder[URL] {
override def apply(url: URL): Json = Json.fromString(url.toString)
}
implicit val decoder: Decoder[URL] = new Decoder[URL] {
override def apply(c: HCursor): Result[URL] = c.value.asString match {
case Some(s) => Right(URL(s))
case None => Left(DecodingFailure(s"Cannot decode URL from ${c.value}", Nil))
}
}
def build(protocol: String,
host: String,
port: Int,
path: String,
parameters: List[(String, List[String])],
fragment: Option[String]): URL = {
val params = Parameters(parameters.map(t => t._1 -> Param(t._2)))
URL(Protocol(protocol), host, port, Path.parse(path), params, fragment)
}
def apply(url: String): URL = apply(url, absolutizePath = true)
def get(url: String): Either[URLParseException, URL] = get(url, absolutizePath = true)
def apply(url: String, absolutizePath: Boolean): URL = get(url, absolutizePath)
.getOrElse(throw MalformedURLException(s"Unable to parse URL: [$url].", url))
def get(url: String, absolutizePath: Boolean): Either[URLParseException, URL] = try {
val colonIndex1 = url.indexOf(':')
val protocol = if (url.startsWith("//")) {
DefaultProtocol
} else if (colonIndex1 != -1) {
Protocol(url.substring(0, colonIndex1))
} else {
DefaultProtocol
}
val slashIndex = url.indexOf('/', colonIndex1 + 3)
val hostAndPort = if (slashIndex == -1) {
if (colonIndex1 == -1) {
url
} else {
url.substring(colonIndex1 + 3)
}
} else {
url.substring(colonIndex1 + 3, slashIndex)
}
val colonIndex2 = hostAndPort.indexOf(':')
val (host, port) = if (colonIndex2 == -1) {
hostAndPort -> protocol.defaultPort.getOrElse(-1)
} else {
hostAndPort.substring(0, colonIndex2) -> hostAndPort.substring(colonIndex2 + 1).toInt
}
val questionIndex = url.indexOf('?')
val hashIndex = url.indexOf('#')
val pathString = if (slashIndex == -1) {
"/"
} else if (questionIndex == -1 && hashIndex == -1) {
url.substring(slashIndex)
} else if (questionIndex != -1) {
url.substring(slashIndex, questionIndex)
} else {
url.substring(slashIndex, hashIndex)
}
val path = Path.parse(pathString, absolutizePath)
val parameters = if (questionIndex == -1) {
Parameters.empty
} else {
val endIndex = if (hashIndex == -1) url.length else hashIndex
val query = url.substring(questionIndex + 1, endIndex)
Parameters.parse(query)
}
val fragment = if (hashIndex != -1) {
Some(url.substring(hashIndex + 1))
} else {
None
}
val u = URL(protocol = protocol, host = host, port = port, path = path, parameters = parameters, fragment = fragment)
if (ValidateTLD) {
u.tld match {
case Some(tld) if !TopLevelDomains.isValid(tld) => Left(new URLParseException(s"Invalid top-level domain: [$tld]", None))
case _ => Right(u)
}
} else {
Right(u)
}
} catch {
case t: Throwable => Left(new URLParseException(s"Unable to parse URL [$url]. Exception: ${t.getMessage}", Some(t)))
}
private val unreservedCharacters = Set('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'-', '_', '.', '~'
)
private val encodedRegex = """%([a-zA-Z0-9]{2})""".r
def encode(part: String): String = part.map {
case c if unreservedCharacters.contains(c) => c
case c => s"%${c.toLong.toHexString.toUpperCase}"
}.mkString
def decode(part: String): String = try {
encodedRegex.replaceAllIn(part.replace("\\", "\\\\"), (m: Regex.Match) => {
val g = m.group(1)
val code = Integer.parseInt(g, 16)
val c = code.toChar
if (c == '\\') {
"\\\\"
} else {
c.toString
}
})
} catch {
case t: Throwable => throw new RuntimeException(s"Failed to decode: [$part]", t)
}
def interpolate(c: blackbox.Context)(args: c.Expr[Any]*): c.Expr[URL] = {
import c.universe._
c.prefix.tree match {
case Apply(_, List(Apply(_, rawParts))) => {
val parts = rawParts map { case t @ Literal(Constant(const: String)) => (const, t.pos) }
val b = new StringBuilder
parts.zipWithIndex.foreach {
case ((raw, _), index) => {
if (index > 0) {
c.abort(
c.enclosingPosition,
"URL interpolation can only contain string literals. Use URL.apply for runtime parsing."
)
}
b.append(raw)
}
}
val url = URL(b.toString())
val protocol = url.protocol.scheme
val host = url.host
val port = url.port
val path = url.path.decoded
val parameters = url.parameters.entries.map(t => t._1 -> t._2.values)
val fragment = url.fragment
c.Expr[URL](q"URL.build(protocol = $protocol, host = $host, port = $port, path = $path, parameters = $parameters, fragment = $fragment)")
}
case _ => c.abort(c.enclosingPosition, "Bad usage of url interpolation.")
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy