diff --git a/README.md b/README.md
index 50626c2..baab6e8 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ A simple Kotlin/Java library to retrieve jokes from [Sv443's JokeAPI](https://v2
## Examples (TL;DR)
```kotlin
-import net.thauvin.erik.jokeapi.JokeApi.Companion.getJoke
+import net.thauvin.erik.jokeapi.getJoke
val joke = getJoke()
val safe = getJoke(safe = true)
@@ -124,9 +124,7 @@ var config = new JokeConfig.Builder()
.safe(true)
.build();
var joke = JokeApi.getJoke(config);
-for (var j : joke.getJoke()) {
- System.out.println(j);
-}
+joke.getJoke().forEach(System.out::println);
```
## Extending
@@ -136,7 +134,7 @@ A generic `apiCall()` function is available to access other [JokeAPI endpoints](
For example to retrieve the French [language code](https://v2.jokeapi.dev/#langcode-endpoint):
```kotlin
-val lang = apiCall(
+val lang = JokeApi.apiCall(
endPoint = "langcode",
path = "french",
params = mapOf(Parameter.FORMAT to Format.YAML.value)
diff --git a/detekt-baseline.xml b/detekt-baseline.xml
index 8f4c3a4..7a212fe 100644
--- a/detekt-baseline.xml
+++ b/detekt-baseline.xml
@@ -2,24 +2,22 @@
- ComplexMethod:JokeApi.kt$JokeApi.Companion$@JvmStatic @Throws(HttpErrorException::class, IOException::class, IllegalArgumentException::class) fun getRawJokes( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, ): String
- LongParameterList:JokeApi.kt$JokeApi.Companion$( amount: Int, categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = false )
- LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, )
- LongParameterList:JokeApi.kt$JokeApi.Companion$( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = false )
- LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set<Category>, val language: Language, val flags: Set<Flag>, val type: Type, val format: Format, val search: String, val idRange: IdRange, val amount: Int = 1, val safe: Boolean, val splitNewLine: Boolean, )
+ ComplexMethod:JokeApi.kt$fun getRawJokes( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" ): String
+ LongParameterList:JokeApi.kt$( amount: Int, categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = false, auth: String = "" )
+ LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, format: Format = Format.JSON, search: String = "", idRange: IdRange = IdRange(), amount: Int = 1, safe: Boolean = false, auth: String = "" )
+ LongParameterList:JokeApi.kt$( categories: Set<Category> = setOf(Category.ANY), language: Language = Language.ENGLISH, flags: Set<Flag> = emptySet(), type: Type = Type.ALL, search: String = "", idRange: IdRange = IdRange(), safe: Boolean = false, splitNewLine: Boolean = false, auth: String = "" )
+ LongParameterList:JokeConfig.kt$JokeConfig$( val categories: Set<Category>, val language: Language, val flags: Set<Flag>, val type: Type, val format: Format, val search: String, val idRange: IdRange, val amount: Int, val safe: Boolean, val splitNewLine: Boolean, val auth: String )
LongParameterList:JokeException.kt$JokeException$( val internalError: Boolean, val code: Int, message: String, val causedBy: List<String>, val additionalInfo: String, val timestamp: Long, cause: Throwable? = null )
- MagicNumber:JokeApi.kt$JokeApi.Companion$200
- MagicNumber:JokeApi.kt$JokeApi.Companion$399
- MagicNumber:JokeApi.kt$JokeApi.Companion$400
- MagicNumber:JokeApi.kt$JokeApi.Companion$403
- MagicNumber:JokeApi.kt$JokeApi.Companion$404
- MagicNumber:JokeApi.kt$JokeApi.Companion$413
- MagicNumber:JokeApi.kt$JokeApi.Companion$414
- MagicNumber:JokeApi.kt$JokeApi.Companion$429
- MagicNumber:JokeApi.kt$JokeApi.Companion$500
- MagicNumber:JokeApi.kt$JokeApi.Companion$523
- TooManyFunctions:JokeApi.kt$JokeApi$Companion
+ MagicNumber:util.kt$200
+ MagicNumber:util.kt$399
+ MagicNumber:util.kt$400
+ MagicNumber:util.kt$403
+ MagicNumber:util.kt$404
+ MagicNumber:util.kt$413
+ MagicNumber:util.kt$414
+ MagicNumber:util.kt$429
+ MagicNumber:util.kt$500
+ MagicNumber:util.kt$523
TooManyFunctions:JokeConfig.kt$JokeConfig$Builder
- UtilityClassWithPublicConstructor:JokeApi.kt$JokeApi
diff --git a/pom.xml b/pom.xml
index 2a6b152..76f3c72 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
jokeapi
0.9-SNAPSHOT
jokeapi
- Kotlin/Java Wrapper for Sv443's JokeApi
+ Kotlin/Java Wrapper for Sv443's JokeAPI
https://github.com/ethauvin/jokeapi
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt
index be136c0..bd850fd 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeApi.kt
@@ -43,22 +43,17 @@ import net.thauvin.erik.jokeapi.models.Language
import net.thauvin.erik.jokeapi.models.Parameter
import net.thauvin.erik.jokeapi.models.Type
import org.json.JSONObject
-import java.io.IOException
-import java.net.HttpURLConnection
-import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
-import java.util.logging.Level
import java.util.logging.Logger
import java.util.stream.Collectors
/**
* Implements the [Sv443's JokeAPI](https://jokeapi.dev/).
*/
-class JokeApi {
+class JokeApi private constructor() {
companion object {
private const val API_URL = "https://v2.jokeapi.dev/"
- private const val JOKE_ENDPOINT = "joke"
@JvmStatic
val logger: Logger by lazy { Logger.getLogger(JokeApi::class.java.simpleName) }
@@ -70,8 +65,13 @@ class JokeApi {
*/
@JvmStatic
@JvmOverloads
- @Throws(HttpErrorException::class, IOException::class)
- fun apiCall(endPoint: String, path: String = "", params: Map = emptyMap()): String {
+ @Throws(HttpErrorException::class)
+ fun apiCall(
+ endPoint: String,
+ path: String = "",
+ params: Map = emptyMap(),
+ auth: String = ""
+ ): String {
val urlBuilder = StringBuilder("$API_URL$endPoint")
if (path.isNotEmpty()) {
@@ -98,99 +98,16 @@ class JokeApi {
}
}
}
- return fetchUrl(urlBuilder.toString())
- }
-
- /**
- * Returns one or more jokes.
- *
- * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
- * @see [getJoke]
- */
- @JvmStatic
- @Throws(HttpErrorException::class, IOException::class, IllegalArgumentException::class)
- fun getRawJokes(
- categories: Set = setOf(Category.ANY),
- language: Language = Language.ENGLISH,
- flags: Set = emptySet(),
- type: Type = Type.ALL,
- format: Format = Format.JSON,
- search: String = "",
- idRange: IdRange = IdRange(),
- amount: Int = 1,
- safe: Boolean = false,
- ): String {
- val params = mutableMapOf()
-
- // Categories
- val path = if (!categories.contains(Category.ANY)) {
- categories.stream().map(Category::value).collect(Collectors.joining(","))
- } else {
- Category.ANY.value
- }
-
- // Language
- if (language != Language.ENGLISH) {
- params[Parameter.LANG] = language.value
- }
-
- // Flags
- if (flags.isNotEmpty()) {
- if (flags.contains(Flag.ALL)) {
- params[Parameter.FLAGS] = Flag.ALL.value
- } else {
- params[Parameter.FLAGS] = flags.stream().map(Flag::value).collect(Collectors.joining(","))
- }
- }
-
- // Type
- if (type != Type.ALL) {
- params[Parameter.TYPE] = type.value
- }
-
- // Format
- if (format != Format.JSON) {
- params[Parameter.FORMAT] = format.value
- }
-
- // Contains
- if (search.isNotBlank()) {
- params[Parameter.CONTAINS] = search
- }
-
- // Range
- if (idRange.start >= 0) {
- if (idRange.end == -1 || idRange.start == idRange.end) {
- params[Parameter.RANGE] = idRange.start.toString()
- } else if (idRange.end > idRange.start) {
- params[Parameter.RANGE] = "${idRange.start}-${idRange.end}"
- } else {
- throw IllegalArgumentException("Invalid ID Range: ${idRange.start}, ${idRange.end}")
- }
- }
-
- // Amount
- if (amount > 1) {
- params[Parameter.AMOUNT] = amount.toString()
- } else if (amount <= 0) {
- throw IllegalArgumentException("Invalid Amount: $amount")
- }
-
- // Safe
- if (safe) {
- params[Parameter.SAFE] = ""
- }
-
- return apiCall(JOKE_ENDPOINT, path, params)
+ return fetchUrl(urlBuilder.toString(), auth)
}
/**
* Returns one or more jokes using a [configuration][JokeConfig].
*
- * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
+ * See the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
- @Throws(HttpErrorException::class, IOException::class, IllegalArgumentException::class)
+ @Throws(HttpErrorException::class)
fun getRawJokes(config: JokeConfig): String {
return getRawJokes(
categories = config.categories,
@@ -201,181 +118,20 @@ class JokeApi {
search = config.search,
idRange = config.idRange,
amount = config.amount,
- safe = config.safe
+ safe = config.safe,
+ auth = config.auth
)
}
- @Throws(HttpErrorException::class, IOException::class)
- internal fun fetchUrl(url: String): String {
- if (logger.isLoggable(Level.FINE)) {
- logger.fine(url)
- }
-
- val connection = URL(url).openConnection() as HttpURLConnection
- connection.setRequestProperty(
- "User-Agent", "Mozilla/5.0 (Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0"
- )
-
- if (connection.responseCode in 200..399) {
- val body = connection.inputStream.bufferedReader().readText()
- if (logger.isLoggable(Level.FINE)) {
- logger.fine(body)
- }
- return body
- } else {
- throw httpError(connection.responseCode)
- }
- }
-
- private fun httpError(responseCode: Int): HttpErrorException {
- val httpException: HttpErrorException
- when (responseCode) {
- 400 -> httpException = HttpErrorException(
- responseCode, "Bad Request", IOException(
- "The request you have sent to JokeAPI is formatted incorrectly and cannot be processed."
- )
- )
-
- 403 -> httpException = HttpErrorException(
- responseCode, "Forbidden", IOException(
- "You have been added to the blacklist due to malicious behavior and are not allowed"
- + " to send requests to JokeAPI anymore."
- )
- )
-
- 404 -> httpException = HttpErrorException(
- responseCode, "Not Found", IOException("The URL you have requested couldn't be found.")
- )
-
- 413 -> httpException = HttpErrorException(
- responseCode, "URI Too Long", IOException("The URL exceeds the maximum length of 250 characters.")
- )
-
- 414 -> httpException = HttpErrorException(
- responseCode,
- "Payload Too Large",
- IOException("The payload data sent to the server exceeds the maximum size of 5120 bytes.")
- )
-
- 429 -> httpException = HttpErrorException(
- responseCode, "Too Many Requests", IOException(
- "You have exceeded the limit of 120 requests per minute and have to wait a bit"
- + " until you are allowed to send requests again."
- )
- )
-
- 500 -> httpException = HttpErrorException(
- responseCode, "Internal Server Error", IOException(
- "There was a general internal error within JokeAPI. You can get more info from"
- + " the properties in the response text."
- )
- )
-
- 523 -> httpException = HttpErrorException(
- responseCode, "Origin Unreachable", IOException(
- "The server is temporarily offline due to maintenance or a dynamic IP update."
- + " Please be patient in this case."
- )
- )
-
- else -> httpException = HttpErrorException(responseCode, "Unknown HTTP Error")
- }
-
- return httpException
- }
-
- /**
- * Returns a [Joke] instance.
- *
- * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
- *
- * @param splitNewLine Split newline within [Type.SINGLE] joke.
- * @see [getRawJokes]
- */
- @JvmStatic
- @Throws(JokeException::class, HttpErrorException::class, IOException::class, IllegalArgumentException::class)
- fun getJoke(
- categories: Set = setOf(Category.ANY),
- language: Language = Language.ENGLISH,
- flags: Set = emptySet(),
- type: Type = Type.ALL,
- search: String = "",
- idRange: IdRange = IdRange(),
- safe: Boolean = false,
- splitNewLine: Boolean = false
- ): Joke {
- val json = JSONObject(
- getRawJokes(
- categories = categories,
- language = language,
- flags = flags,
- type = type,
- search = search,
- idRange = idRange,
- safe = safe
- )
- )
- if (json.getBoolean("error")) {
- throw parseError(json)
- } else {
- return parseJoke(json, splitNewLine)
- }
- }
-
- /**
- * Returns an array of [Joke] instances.
- *
- * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
- *
- * @param amount The required amount of jokes to return.
- * @param splitNewLine Split newline within [Type.SINGLE] joke.
- * @see [getRawJokes]
- */
- @JvmStatic
- @Throws(JokeException::class, HttpErrorException::class, IOException::class, IllegalArgumentException::class)
- fun getJokes(
- amount: Int,
- categories: Set = setOf(Category.ANY),
- language: Language = Language.ENGLISH,
- flags: Set = emptySet(),
- type: Type = Type.ALL,
- search: String = "",
- idRange: IdRange = IdRange(),
- safe: Boolean = false,
- splitNewLine: Boolean = false
- ): Array {
- val json = JSONObject(
- getRawJokes(
- categories = categories,
- language = language,
- flags = flags,
- type = type,
- search = search,
- idRange = idRange,
- amount = amount,
- safe = safe
- )
- )
- if (json.getBoolean("error")) {
- throw parseError(json)
- } else {
- return if (json.has("amount")) {
- val jokes = json.getJSONArray("jokes")
- Array(jokes.length()) { i -> parseJoke(jokes.getJSONObject(i), splitNewLine) }
- } else {
- arrayOf(parseJoke(json, splitNewLine))
- }
- }
- }
-
/**
* Retrieve a [Joke] instance using a [configuration][JokeConfig].
*
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
- @Throws(JokeException::class, HttpErrorException::class, IOException::class, IllegalArgumentException::class)
- fun getJoke(config: JokeConfig): Joke {
+ @JvmOverloads
+ @Throws(HttpErrorException::class, JokeException::class)
+ fun getJoke(config: JokeConfig = JokeConfig.Builder().build()): Joke {
return getJoke(
categories = config.categories,
language = config.language,
@@ -384,7 +140,8 @@ class JokeApi {
search = config.search,
idRange = config.idRange,
safe = config.safe,
- splitNewLine = config.splitNewLine
+ splitNewLine = config.splitNewLine,
+ auth = config.auth
)
}
@@ -394,7 +151,7 @@ class JokeApi {
* Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
*/
@JvmStatic
- @Throws(JokeException::class, HttpErrorException::class, IOException::class, IllegalArgumentException::class)
+ @Throws(HttpErrorException::class, JokeException::class)
fun getJokes(config: JokeConfig): Array {
return getJokes(
categories = config.categories,
@@ -405,51 +162,173 @@ class JokeApi {
idRange = config.idRange,
amount = config.amount,
safe = config.safe,
- splitNewLine = config.splitNewLine
- )
- }
-
- private fun parseError(json: JSONObject): JokeException {
- val causedBy = json.getJSONArray("causedBy")
- val causes = List(causedBy.length()) { i -> causedBy.getString(i) }
- return JokeException(
- internalError = json.getBoolean("internalError"),
- code = json.getInt("code"),
- message = json.getString("message"),
- causedBy = causes,
- additionalInfo = json.getString("additionalInfo"),
- timestamp = json.getLong("timestamp")
- )
- }
-
- private fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
- val jokes = mutableListOf()
- if (json.has("setup")) {
- jokes.add(json.getString("setup"))
- jokes.add(json.getString(("delivery")))
- } else {
- if (splitNewLine) {
- jokes.addAll(json.getString("joke").split("\n"))
- } else {
- jokes.add(json.getString("joke"))
- }
- }
- val enabledFlags = mutableSetOf()
- val jsonFlags = json.getJSONObject("flags")
- Flag.values().filter { it != Flag.ALL }.forEach {
- if (jsonFlags.has(it.value) && jsonFlags.getBoolean(it.value)) {
- enabledFlags.add(it)
- }
- }
- return Joke(
- category = Category.valueOf(json.getString("category").uppercase()),
- type = Type.valueOf(json.getString(Parameter.TYPE).uppercase()),
- joke = jokes,
- flags = enabledFlags,
- safe = json.getBoolean("safe"),
- id = json.getInt("id"),
- language = Language.valueOf(json.getString(Parameter.LANG).uppercase())
+ splitNewLine = config.splitNewLine,
+ auth = config.auth
)
}
}
}
+
+
+/**
+ * Returns a [Joke] instance.
+ *
+ * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
+ *
+ * @param splitNewLine Split newline within [Type.SINGLE] joke.
+ */
+fun getJoke(
+ categories: Set = setOf(Category.ANY),
+ language: Language = Language.ENGLISH,
+ flags: Set = emptySet(),
+ type: Type = Type.ALL,
+ search: String = "",
+ idRange: IdRange = IdRange(),
+ safe: Boolean = false,
+ splitNewLine: Boolean = false,
+ auth: String = ""
+): Joke {
+ val json = JSONObject(
+ getRawJokes(
+ categories = categories,
+ language = language,
+ flags = flags,
+ type = type,
+ search = search,
+ idRange = idRange,
+ safe = safe,
+ auth = auth
+ )
+ )
+ if (json.getBoolean("error")) {
+ throw parseError(json)
+ } else {
+ return parseJoke(json, splitNewLine)
+ }
+}
+
+/**
+ * Returns an array of [Joke] instances.
+ *
+ * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
+ *
+ * @param amount The required amount of jokes to return.
+ * @param splitNewLine Split newline within [Type.SINGLE] joke.
+ */
+fun getJokes(
+ amount: Int,
+ categories: Set = setOf(Category.ANY),
+ language: Language = Language.ENGLISH,
+ flags: Set = emptySet(),
+ type: Type = Type.ALL,
+ search: String = "",
+ idRange: IdRange = IdRange(),
+ safe: Boolean = false,
+ splitNewLine: Boolean = false,
+ auth: String = ""
+): Array {
+ val json = JSONObject(
+ getRawJokes(
+ categories = categories,
+ language = language,
+ flags = flags,
+ type = type,
+ search = search,
+ idRange = idRange,
+ amount = amount,
+ safe = safe,
+ auth = auth
+ )
+ )
+ if (json.getBoolean("error")) {
+ throw parseError(json)
+ } else {
+ return if (json.has("amount")) {
+ val jokes = json.getJSONArray("jokes")
+ Array(jokes.length()) { i -> parseJoke(jokes.getJSONObject(i), splitNewLine) }
+ } else {
+ arrayOf(parseJoke(json, splitNewLine))
+ }
+ }
+}
+
+/**
+ * Returns one or more jokes.
+ *
+ * Sse the [JokeAPI Documentation](https://jokeapi.dev/#joke-endpoint) for more details.
+ */
+fun getRawJokes(
+ categories: Set = setOf(Category.ANY),
+ language: Language = Language.ENGLISH,
+ flags: Set = emptySet(),
+ type: Type = Type.ALL,
+ format: Format = Format.JSON,
+ search: String = "",
+ idRange: IdRange = IdRange(),
+ amount: Int = 1,
+ safe: Boolean = false,
+ auth: String = ""
+): String {
+ val params = mutableMapOf()
+
+ // Categories
+ val path = if (!categories.contains(Category.ANY)) {
+ categories.stream().map(Category::value).collect(Collectors.joining(","))
+ } else {
+ Category.ANY.value
+ }
+
+ // Language
+ if (language != Language.ENGLISH) {
+ params[Parameter.LANG] = language.value
+ }
+
+ // Flags
+ if (flags.isNotEmpty()) {
+ if (flags.contains(Flag.ALL)) {
+ params[Parameter.FLAGS] = Flag.ALL.value
+ } else {
+ params[Parameter.FLAGS] = flags.stream().map(Flag::value).collect(Collectors.joining(","))
+ }
+ }
+
+ // Type
+ if (type != Type.ALL) {
+ params[Parameter.TYPE] = type.value
+ }
+
+ // Format
+ if (format != Format.JSON) {
+ params[Parameter.FORMAT] = format.value
+ }
+
+ // Contains
+ if (search.isNotBlank()) {
+ params[Parameter.CONTAINS] = search
+ }
+
+ // Range
+ if (idRange.start >= 0) {
+ if (idRange.end == -1 || idRange.start == idRange.end) {
+ params[Parameter.RANGE] = idRange.start.toString()
+ } else if (idRange.end > idRange.start) {
+ params[Parameter.RANGE] = "${idRange.start}-${idRange.end}"
+ } else {
+ throw IllegalArgumentException("Invalid ID Range: ${idRange.start}, ${idRange.end}")
+ }
+ }
+
+ // Amount
+ if (amount > 1) {
+ params[Parameter.AMOUNT] = amount.toString()
+ } else if (amount <= 0) {
+ throw IllegalArgumentException("Invalid Amount: $amount")
+ }
+
+ // Safe
+ if (safe) {
+ params[Parameter.SAFE] = ""
+ }
+
+ return JokeApi.apiCall("joke", path, params, auth)
+}
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt
index f27132e..ff6d83c 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/JokeConfig.kt
@@ -53,9 +53,10 @@ class JokeConfig private constructor(
val format: Format,
val search: String,
val idRange: IdRange,
- val amount: Int = 1,
+ val amount: Int,
val safe: Boolean,
val splitNewLine: Boolean,
+ val auth: String
) {
/**
* [Builds][build] a new configuration.
@@ -74,7 +75,8 @@ class JokeConfig private constructor(
var idRange: IdRange = IdRange(),
var amount: Int = 1,
var safe: Boolean = false,
- var splitNewLine: Boolean = false
+ var splitNewLine: Boolean = false,
+ var auth: String = ""
) {
fun categories(categories: Set) = apply { this.categories = categories }
fun language(language: Language) = apply { this.language = language }
@@ -86,9 +88,10 @@ class JokeConfig private constructor(
fun amount(amount: Int) = apply { this.amount = amount }
fun safe(safe: Boolean) = apply { this.safe = safe }
fun splitNewLine(splitNewLine: Boolean) = apply { this.splitNewLine = splitNewLine }
+ fun auth(auth: String) = apply { this.auth = auth }
fun build() = JokeConfig(
- categories, language, flags, type, format, search, idRange, amount, safe, splitNewLine
+ categories, language, flags, type, format, search, idRange, amount, safe, splitNewLine, auth
)
}
}
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt
index 29d7849..84b37fc 100644
--- a/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/exceptions/JokeException.kt
@@ -45,7 +45,7 @@ class JokeException @JvmOverloads constructor(
val additionalInfo: String,
val timestamp: Long,
cause: Throwable? = null
-) : Exception(message, cause) {
+) : RuntimeException(message, cause) {
companion object {
private const val serialVersionUID = 1L
}
diff --git a/src/main/kotlin/net/thauvin/erik/jokeapi/util.kt b/src/main/kotlin/net/thauvin/erik/jokeapi/util.kt
new file mode 100644
index 0000000..cb3b4b6
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/jokeapi/util.kt
@@ -0,0 +1,172 @@
+/*
+ * util.kt
+ *
+ * Copyright (c) 2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.jokeapi
+
+import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
+import net.thauvin.erik.jokeapi.exceptions.JokeException
+import net.thauvin.erik.jokeapi.models.Category
+import net.thauvin.erik.jokeapi.models.Flag
+import net.thauvin.erik.jokeapi.models.Joke
+import net.thauvin.erik.jokeapi.models.Language
+import net.thauvin.erik.jokeapi.models.Parameter
+import net.thauvin.erik.jokeapi.models.Type
+import org.json.JSONObject
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.logging.Level
+
+internal fun fetchUrl(url: String, auth: String = ""): String {
+ if (JokeApi.logger.isLoggable(Level.FINE)) {
+ JokeApi.logger.fine(url)
+ }
+
+ val connection = URL(url).openConnection() as HttpURLConnection
+ connection.setRequestProperty(
+ "User-Agent", "Mozilla/5.0 (Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0"
+ )
+ if (auth.isNotEmpty()) {
+ connection.setRequestProperty("Authentication", auth)
+ }
+
+ if (connection.responseCode in 200..399) {
+ val body = connection.inputStream.bufferedReader().readText()
+ if (JokeApi.logger.isLoggable(Level.FINE)) {
+ JokeApi.logger.fine(body)
+ }
+ return body
+ } else {
+ throw httpError(connection.responseCode)
+ }
+}
+
+private fun httpError(responseCode: Int): HttpErrorException {
+ val httpException: HttpErrorException
+ when (responseCode) {
+ 400 -> httpException = HttpErrorException(
+ responseCode, "Bad Request", IOException(
+ "The request you have sent to JokeAPI is formatted incorrectly and cannot be processed."
+ )
+ )
+
+ 403 -> httpException = HttpErrorException(
+ responseCode, "Forbidden", IOException(
+ "You have been added to the blacklist due to malicious behavior and are not allowed"
+ + " to send requests to JokeAPI anymore."
+ )
+ )
+
+ 404 -> httpException = HttpErrorException(
+ responseCode, "Not Found", IOException("The URL you have requested couldn't be found.")
+ )
+
+ 413 -> httpException = HttpErrorException(
+ responseCode, "URI Too Long", IOException("The URL exceeds the maximum length of 250 characters.")
+ )
+
+ 414 -> httpException = HttpErrorException(
+ responseCode,
+ "Payload Too Large",
+ IOException("The payload data sent to the server exceeds the maximum size of 5120 bytes.")
+ )
+
+ 429 -> httpException = HttpErrorException(
+ responseCode, "Too Many Requests", IOException(
+ "You have exceeded the limit of 120 requests per minute and have to wait a bit"
+ + " until you are allowed to send requests again."
+ )
+ )
+
+ 500 -> httpException = HttpErrorException(
+ responseCode, "Internal Server Error", IOException(
+ "There was a general internal error within JokeAPI. You can get more info from"
+ + " the properties in the response text."
+ )
+ )
+
+ 523 -> httpException = HttpErrorException(
+ responseCode, "Origin Unreachable", IOException(
+ "The server is temporarily offline due to maintenance or a dynamic IP update."
+ + " Please be patient in this case."
+ )
+ )
+
+ else -> httpException = HttpErrorException(responseCode, "Unknown HTTP Error")
+ }
+
+ return httpException
+}
+
+internal fun parseError(json: JSONObject): JokeException {
+ val causedBy = json.getJSONArray("causedBy")
+ val causes = List(causedBy.length()) { i -> causedBy.getString(i) }
+ return JokeException(
+ internalError = json.getBoolean("internalError"),
+ code = json.getInt("code"),
+ message = json.getString("message"),
+ causedBy = causes,
+ additionalInfo = json.getString("additionalInfo"),
+ timestamp = json.getLong("timestamp")
+ )
+}
+
+internal fun parseJoke(json: JSONObject, splitNewLine: Boolean): Joke {
+ val jokes = mutableListOf()
+ if (json.has("setup")) {
+ jokes.add(json.getString("setup"))
+ jokes.add(json.getString(("delivery")))
+ } else {
+ if (splitNewLine) {
+ jokes.addAll(json.getString("joke").split("\n"))
+ } else {
+ jokes.add(json.getString("joke"))
+ }
+ }
+ val enabledFlags = mutableSetOf()
+ val jsonFlags = json.getJSONObject("flags")
+ Flag.values().filter { it != Flag.ALL }.forEach {
+ if (jsonFlags.has(it.value) && jsonFlags.getBoolean(it.value)) {
+ enabledFlags.add(it)
+ }
+ }
+ return Joke(
+ category = Category.valueOf(json.getString("category").uppercase()),
+ type = Type.valueOf(json.getString(Parameter.TYPE).uppercase()),
+ joke = jokes,
+ flags = enabledFlags,
+ safe = json.getBoolean("safe"),
+ id = json.getInt("id"),
+ language = Language.valueOf(json.getString(Parameter.LANG).uppercase())
+ )
+}
+
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt
index e0a68d3..cfff0c8 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/ExceptionsTest.kt
@@ -44,8 +44,6 @@ import assertk.assertions.isNull
import assertk.assertions.prop
import assertk.assertions.size
import assertk.assertions.startsWith
-import net.thauvin.erik.jokeapi.JokeApi.Companion.fetchUrl
-import net.thauvin.erik.jokeapi.JokeApi.Companion.getJoke
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.exceptions.HttpErrorException
import net.thauvin.erik.jokeapi.exceptions.JokeException
@@ -59,8 +57,6 @@ import java.util.logging.ConsoleHandler
import java.util.logging.Level
internal class ExceptionsTest {
- private val httpStat = "https://httpstat.us"
-
@Test
fun `Validate Joke Exception`() {
val e = assertThrows {
@@ -78,29 +74,19 @@ internal class ExceptionsTest {
}
}
- @ParameterizedTest(name = "HTTP Status Code: {0}")
- @ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523])
- fun `Validate HTTP Error Exceptions`(input: Int) {
+ @ParameterizedTest
+ @ValueSource(ints = [400, 404, 403, 413, 414, 429, 500, 523, 666])
+ fun `Validate HTTP Exceptions`(code: Int) {
val e = assertThrows {
- fetchUrl("$httpStat/$input")
+ fetchUrl("https://httpstat.us/$code")
}
- assertThat(e, "fetchUrl($httpStat/$input)").all {
- prop(HttpErrorException::statusCode).isEqualTo(input)
+ assertThat(e, "fetchUrl($code)").all {
+ prop(HttpErrorException::statusCode).isEqualTo(code)
prop(HttpErrorException::message).isNotNull().isNotEmpty()
- prop(HttpErrorException::cause).isNotNull().assertThat(Throwable::message).isNotNull()
- }
- }
-
- @Test
- fun `Fetch Invalid URL`() {
- val statusCode = 999
- val e = assertThrows {
- fetchUrl("$httpStat/$statusCode")
- }
- assertThat(e, "fetchUrl($httpStat/$statusCode).statusCode").all {
- prop(HttpErrorException::statusCode).isEqualTo(statusCode)
- prop(HttpErrorException::message).isNotNull().isNotEmpty()
- prop(HttpErrorException::cause).isNull()
+ if (code < 600)
+ prop(HttpErrorException::cause).isNotNull().assertThat(Throwable::message).isNotNull()
+ else
+ prop(HttpErrorException::cause).isNull()
}
}
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt
index ea0a740..857949f 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokeTest.kt
@@ -49,7 +49,6 @@ import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
-import net.thauvin.erik.jokeapi.JokeApi.Companion.getJoke
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.exceptions.JokeException
import net.thauvin.erik.jokeapi.models.Category
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt
index 489aa74..bbc0bd5 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetJokesTest.kt
@@ -1,5 +1,5 @@
/*
- * GetJokeTest.kt
+ * GetJokeTests.kt
*
* Copyright (c) 2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@@ -44,7 +44,6 @@ import assertk.assertions.isNotNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
-import net.thauvin.erik.jokeapi.JokeApi.Companion.getJokes
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.models.Joke
import net.thauvin.erik.jokeapi.models.Language
@@ -79,10 +78,6 @@ internal class GetJokesTest {
@Test
fun `Get One Joke as Multiple`() {
val jokes = getJokes(amount = 1, safe = true)
- jokes.forEach {
- println(it.joke.joinToString("\n"))
- println("-".repeat(46))
- }
assertThat(jokes, "jokes").all {
size().isEqualTo(1)
index(0).all {
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt
index c0f07e1..b745da8 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/GetRawJokesTest.kt
@@ -1,5 +1,5 @@
/*
- * GetRawJokeTest.kt
+ * GetRawJokesTest.kt
*
* Copyright (c) 2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@@ -37,9 +37,9 @@ import assertk.assertThat
import assertk.assertions.doesNotContain
import assertk.assertions.isNotEmpty
import assertk.assertions.startsWith
-import net.thauvin.erik.jokeapi.JokeApi.Companion.getRawJokes
import net.thauvin.erik.jokeapi.JokeApi.Companion.logger
import net.thauvin.erik.jokeapi.models.Format
+import net.thauvin.erik.jokeapi.models.IdRange
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import java.util.logging.ConsoleHandler
@@ -74,6 +74,12 @@ internal class GetRawJokesTest {
assertContains(response, "\"amount\": 2", false, "getRawJoke(2)")
}
+ @Test
+ fun `Get Raw Invalid Jokes`() {
+ val response = getRawJokes(search = "foo", safe = true, amount = 2, idRange = IdRange(160, 161))
+ assertContains(response, "\"error\": true", false, "getRawJokes(foo)")
+ }
+
companion object {
@JvmStatic
@BeforeAll
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt
index 2ab52ca..90e7c73 100644
--- a/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/JokeConfigTest.kt
@@ -60,6 +60,15 @@ import java.util.logging.Level
import kotlin.test.assertContains
class JokeConfigTest {
+ @Test
+ fun `Get Joke with Default Builder`() {
+ val joke = getJoke()
+ assertThat(joke, "joke").all {
+ prop(Joke::id).isGreaterThanOrEqualTo(0)
+ prop(Joke::language).isEqualTo(Language.EN)
+ }
+ }
+
@Test
fun `Get Joke with Builder`() {
val id = 266
@@ -132,6 +141,47 @@ class JokeConfigTest {
}
}
+ @Test
+ fun `Validate Config`() {
+ val categories = setOf(Category.ANY)
+ val language = Language.CS
+ val flags = setOf(Flag.POLITICAL, Flag.RELIGIOUS)
+ val type = Type.TWOPART
+ val format = Format.XML
+ val search = "foo"
+ val idRange = IdRange(1, 20)
+ val amount = 10
+ val safe = true
+ val splitNewLine = true
+ val auth = "token"
+ val config = JokeConfig.Builder().apply {
+ categories(categories)
+ language(language)
+ flags(flags)
+ type(type)
+ format(format)
+ search(search)
+ idRange(idRange)
+ amount(amount)
+ safe(safe)
+ splitNewLine(splitNewLine)
+ auth(auth)
+ }.build()
+ assertThat(config, "config").all {
+ prop(JokeConfig::categories).isEqualTo(categories)
+ prop(JokeConfig::language).isEqualTo(language)
+ prop(JokeConfig::flags).isEqualTo(flags)
+ prop(JokeConfig::type).isEqualTo(type)
+ prop(JokeConfig::format).isEqualTo(format)
+ prop(JokeConfig::search).isEqualTo(search)
+ prop(JokeConfig::idRange).isEqualTo(idRange)
+ prop(JokeConfig::amount).isEqualTo(amount)
+ prop(JokeConfig::safe).isEqualTo(safe)
+ prop(JokeConfig::splitNewLine).isEqualTo(splitNewLine)
+ prop(JokeConfig::auth).isEqualTo(auth)
+ }
+ }
+
companion object {
@JvmStatic
@BeforeAll
diff --git a/src/test/kotlin/net/thauvin/erik/jokeapi/UtilTest.kt b/src/test/kotlin/net/thauvin/erik/jokeapi/UtilTest.kt
new file mode 100644
index 0000000..34bbedf
--- /dev/null
+++ b/src/test/kotlin/net/thauvin/erik/jokeapi/UtilTest.kt
@@ -0,0 +1,73 @@
+/*
+ * FetchUrlTest.kt
+ *
+ * Copyright (c) 2022, Erik C. Thauvin (erik@thauvin.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of this project nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.thauvin.erik.jokeapi
+
+import assertk.assertThat
+import assertk.assertions.contains
+import org.json.JSONException
+import org.json.JSONObject
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.util.logging.ConsoleHandler
+import java.util.logging.Level
+
+class UtilTest {
+ @Test
+ fun `Invalid JSON Error`() {
+ assertThrows { parseError(JSONObject("{}")) }
+ }
+
+ @Test
+ fun `Invalid JSON Joke`() {
+ assertThrows { parseJoke(JSONObject("{}"), false) }
+ }
+
+ @Test
+ fun `Validate Authentication Header`() {
+ val token = "AUTH-TOKEN"
+ val body = fetchUrl("https://postman-echo.com/get", token)
+ assertThat(body, "body").contains("\"authentication\":\"$token\"")
+ }
+
+ companion object {
+ @JvmStatic
+ @BeforeAll
+ fun beforeAll() {
+ with(JokeApi.logger) {
+ addHandler(ConsoleHandler().apply { level = Level.FINE })
+ level = Level.FINE
+ }
+ }
+ }
+}