fm.common.QueryParams.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fm-common_2.11 Show documentation
Show all versions of fm-common_2.11 Show documentation
Common Scala classes that we use at Frugal Mechanic / Eluvio
The newest version!
/*
* Copyright 2020 Frugal Mechanic (http://frugalmechanic.com)
*
* Licensed 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 fm.common
import scala.util.Try
object QueryParams {
def get(uri: URI): Option[QueryParams] = Try{ apply(uri) }.toOption
/** Create Query Params form a URI */
def apply(uri: URI): QueryParams = apply(uri.getRawQuery)
def get(query: String): Option[QueryParams] = Try{ apply(query) }.toOption
/**
* Create a QueryParams instance given a URL or Query String
*
* @param queryString the URI/URL or Query String to extract Query Parameters from
*/
def apply(queryString: String): QueryParams = {
if (queryString.isNullOrBlank) return empty
val questionIdx: Int = queryString.indexOf('?')
val hashIdx: Int = queryString.indexOf('#')
// Ignore anything before the "?" and after the "#"
val fixedQueryString: String = if (questionIdx >= 0 || hashIdx >= 0) {
val question: Int = if (questionIdx >= 0) questionIdx+1 else 0
val hash: Int = if (hashIdx >= 0) hashIdx else queryString.length
queryString.substring(question, hash)
} else queryString
// Should support ";" in addition to "&" per: http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2
val rawParams: Array[String] = fixedQueryString.replace(';','&').split('&')
val params: Seq[(String, String)] = ImmutableArray.unsafeWrapArray(rawParams).map { (param: String) =>
val idx: Int = param.indexOf('=')
if (idx < 0) {
(StringEscapeUtils.decodeURIComponent(param), null)
} else {
(StringEscapeUtils.decodeURIComponent(param.substring(0, idx)), StringEscapeUtils.decodeURIComponent(param.substring(idx + 1)))
}
}
apply(params)
}
def apply(params: Map[String, Seq[String]]): QueryParams = {
apply(withoutNullPairsOrKeys(params.toSeq).flatMap{ case (k, vals) => vals.map{ v => (k, v) } })
}
def apply(head: (String, String), rest: (String,String)*): QueryParams = apply(head +: rest)
// Note: all creation of QueryParams should go through this to filter out nulls
def apply(params: Seq[(String, String)]): QueryParams = new QueryParams(withoutNullPairsOrKeys(params))
private def withoutNullPairsOrKeys[T](params: Seq[(String,T)]): Seq[(String,T)] = {
params.filterNot{ (pair: (String,T)) => null == pair || null == pair._1 }
}
/** An empty instance of QueryParams */
val empty: QueryParams = new QueryParams(Nil)
def newBuilder: QueryParamsBuilder = new QueryParamsBuilder()
}
/**
* Represents immutable query parameters from a query string (e.g. "?foo=bar&asd=qwe").
*
* This class distinguishes between 3 different types of values for a key:
* - null - "?foo"
* - blank - "?foo="
* - non-blank - "?foo=bar"
*/
final class QueryParams private (private val params: Seq[(String, String)]) {
def toSeq: Seq[(String, String)] = params
def sorted: QueryParams = new QueryParams(params.sorted)
/**
* Optionally Returns the first non-null value for the given key
*/
def getFirst(key: String): Option[String] = nonNullValuesForKey(key).headOption
/**
* Optionally Returns the first non-blank value for the given key
*/
def getFirstNonBlank(key: String): Option[String] = nonBlankValuesForKey(key).headOption
/**
* Returns the first non-null value for the given key or throws a NoSuchElementException if none exists.
*/
def first(key: String): String = getFirst(key).getOrElse{ throw new NoSuchElementException(key) }
/**
* Returns the first non-blank value for the given key or throws a NoSuchElementException if none exists.
*/
def firstNonBlank(key: String): String = getFirstNonBlank(key).getOrElse{ throw new NoSuchElementException(key) }
/**
* Returns all values for the given key. An Empty Seq is returns if the key doesn't exist or there are no non-null values.
*/
def get(key: String): Seq[String] = nonNullValuesForKey(key)
/**
* Returns all non-blank values for the given key. An Empty Seq is returns if the key doesn't exist or there are no non-null values.
*/
def getNonBlank(key: String): Seq[String] = nonBlankValuesForKey(key)
/**
* Returns all values for the given key or throws a NoSuchElementException if the key doesn't exists. An Empty Seq is returned
* if the key exists but has no non-null values.
*/
def apply(key: String): Seq[String] = {
val vals: Seq[String] = get(key)
if (vals.nonEmpty || contains(key)) vals else throw new NoSuchElementException(key)
}
/**
* Returns all non-null values for the given key or throws a NoSuchElementException if the key doesn't have non-null values.
*/
def nonNull(key: String): Seq[String] = {
val vals: Seq[String] = nonNullValuesForKey(key)
if (vals.nonEmpty) vals else throw new NoSuchElementException(key)
}
/**
* Returns all non-blank values for the given key or throws a NoSuchElementException if the key doesn't have non-null values.
*/
def nonBlank(key: String): Seq[String] = {
val vals: Seq[String] = nonBlankValuesForKey(key)
if (vals.nonEmpty) vals else throw new NoSuchElementException(key)
}
/**
* Check for key existence
*/
def contains(key: String): Boolean = hasKey(key)
/**
* Check for key/value pair existence
*/
def contains(key: String, value: String): Boolean = contains((key, value))
/**
* Check for key/value pair existence
*/
def contains(elem: (String, String)): Boolean = params.exists { _ === elem }
/**
* Returns true if there is a matching key (which doesn't need to have a value)
*/
def hasKey(key: String): Boolean = allKeys.contains(key)
/**
* Returns true if there is a matching key (which also has a value)
*
* NOTE: The value can be blank (e.g. "?foo=" is blank vs "?foo" is null)
*/
def hasKeyWithValue(key: String): Boolean = keysWithValues.contains(key)
/**
* Returns true if there is a matching key which has the given value
*
* Note: This is an alias for contains() but seems like a more natural name
* to use in code.
*/
def hasKeyWithValue(key: String, value: String): Boolean = contains(key, value)
/**
* Returns true if there is a matching key (which also has a non-blank value)
*/
def hasKeyWithNonBlankValue(key: String): Boolean = keysWithNonBlankValues.contains(key)
/**
* Make sure the key (without a value) exists
*/
def updated(key: String): QueryParams = updated(key, null)
/**
* If the key doesn't exist then add it, otherwise replace the first occurance
* of the key with the new value and remove any other values.
*/
def updated(key: String, value: String): QueryParams = {
if (null == key) return this
var found: Boolean = false
val newParams: Seq[(String, String)] = params.flatMap{ case (k,v) =>
if (key === k) {
if (!found) {
found = true
Some((k, value))
} else None
} else Some((k,v))
}
if (!found) QueryParams(newParams :+ (key -> value)) else QueryParams(newParams)
}
/**
* Update multiple key/value pairs
*/
def updated(other: QueryParams): QueryParams = updated(other.params:_*)
/**
* Update multiple key/value pairs
*/
def updated(kvPairs: (String, String)*): QueryParams = {
var tmp: QueryParams = this
QueryParams.withoutNullPairsOrKeys(kvPairs).foreach{ case (k,v) => tmp = tmp.updated(k, v) }
tmp
}
/**
* Add a key/value pair
*
* @return A new QueryParams instance with the added key/value pair
*/
def add(key: String, value: String): QueryParams = {
if (null == key) return this
QueryParams(params ++ Seq(key -> value))
}
/**
* Add multiple key/value pairs
*
* @return A new QueryParams instance with the added key/value pair
*/
def add(kvPairs: (String, String)*): QueryParams = QueryParams(params ++ kvPairs)
/**
* Add multiple key/value pairs
*
* @return A new QueryParams instance with the added key/value pair
*/
def add(other: QueryParams): QueryParams = QueryParams(params ++ other.params)
/**
* Remove any params with blank values
*
* @return A new QueryParams instance without blank values
*/
def withoutBlankValues(): QueryParams = QueryParams(params.filter{ case (_, v: String) => v.isNotNullOrBlank })
/**
* Remove a key/value pair based on the key
*
* @return A new QueryParams instance without the keys
*/
def remove(keys: String*): QueryParams = QueryParams(params.filterNot{ case (k: String, _) => keys.contains(k) })
/**
* Replaces the entry for the given key only if it was previously mapped to some value.
*/
def replace(key: String, value: String): QueryParams = if (hasKey(key)) updated(key, value) else this
private def nonNullValuesForKey(key: String): Seq[String] = params.filter{ case (k, v) => k === key && v != null }.map{ case (k, v) => v }.toList
private def nonBlankValuesForKey(key: String): Seq[String] = params.filter{ case (k, v) => k === key && v.isNotNullOrBlank }.map{ case (k, v) => v }.toList
/**
* The unique set of keys with or without values
*/
def allKeys: Set[String] = params.map{ case (k, v) => k }.toSet
/**
* The unique set of keys with non-null values (blank is a valid value)
*/
def keysWithValues: Set[String] = params.filterNot{ case (k, v) => null == v }.map{ case (k, v) => k }.toSet
/**
* The unique set of keys with non-blank values
*/
def keysWithNonBlankValues: Set[String] = params.filterNot{ case (k, v) => v.isNullOrBlank }.map{ case (k, v) => k }.toSet
/**
* The unique set of keys without values
*/
def keysWithoutValues: Set[String] = params.filter{ case (k, v) => null == v }.map{ case (k, v) => k }.toSet
/**
* Convert to a Map[String, Seq[String]]
*/
def toMap: Map[String, Seq[String]] = {
val res: Map[String,Seq[(String,String)]] = params.groupBy{ case (k, v) => k }
val res2: Map[String,Seq[String]] = res.mapValuesStrict{ (values: Seq[(String, String)]) => values.map{ _._2 }.filterNot{ _ === null } }
res2
}
/**
* Convert all keys to lower case
*/
def withLowerKeys: QueryParams = QueryParams(params.map { case (k, v) => (k.toLowerCase, v) })
/** Add a prefix to each key */
def withPrefixedKeys(prefix: String) = mapKeys{ prefix + _ }
def prettyString: String = {
"{"+toMap.toSeq.map{ case (key, values) =>
key+"=["+values.mkString(",")+"]"
}.mkString(", ")+"}"
}
/** An alias for toQueryString */
override def toString(): String = toQueryString()
/**
* Create a valid query string using the current parameters (everything after the ?).
*
* e.g.: foo=bar&hello=world
*/
def toQueryString(): String = {
params.map{ case (k, v) =>
StringEscapeUtils.encodeURIComponent(k) + (if (null == v) "" else "="+StringEscapeUtils.encodeURIComponent(v))
}.mkString("&")
}
def map(f: ((String, String)) => (String, String)): QueryParams = QueryParams(params.map(f))
def mapKeys(f: String => String): QueryParams = QueryParams(params.map{ case (key, value) => (f(key), value) })
def mapValues(f: String => String): QueryParams = QueryParams(params.map{ case (key, value) => (key, f(value)) })
def filter(f: ((String, String)) => Boolean): QueryParams = QueryParams(params.filter(f))
def filterNot(f: ((String, String)) => Boolean): QueryParams = QueryParams(params.filterNot(f))
def filterKeys(f: String => Boolean): QueryParams = QueryParams(params.filter{ case (key, _) => f(key) })
def filterValues(f: String => Boolean): QueryParams = QueryParams(params.filter{ case (_, value) => f(value) })
def foreach[U](f: ((String,String)) => U): Unit = params.foreach(f)
def apply(idx: Int): (String, String) = params(idx)
def length: Int = params.length
def isEmpty: Boolean = params.isEmpty
def nonEmpty: Boolean = params.nonEmpty
override def equals(obj: Any): Boolean = {
obj match {
case null => false
case other: QueryParams => other.params === params
case _ => false
}
}
override def hashCode(): Int = params.hashCode()
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy