Restructed with Bilinks, Constants, and Utils classes.
This commit is contained in:
parent
6e1240e38b
commit
d77b3cce72
8 changed files with 366 additions and 233 deletions
|
@ -2,12 +2,12 @@
|
||||||
<SmellBaseline>
|
<SmellBaseline>
|
||||||
<Blacklist></Blacklist>
|
<Blacklist></Blacklist>
|
||||||
<Whitelist>
|
<Whitelist>
|
||||||
<ID>ComplexMethod:Bitly.kt$Bitly$executeCall</ID>
|
<ID>ComplexMethod:Utils.kt$Utils.Companion$call</ID>
|
||||||
<ID>FunctionParameterNaming:Bitly.kt$Bitly$bitlink_id: String</ID>
|
<ID>FunctionParameterNaming:Bitlinks.kt$Bitlinks$bitlink_id: String</ID>
|
||||||
<ID>FunctionParameterNaming:Bitly.kt$Bitly$group_guid: String = ""</ID>
|
<ID>FunctionParameterNaming:Bitlinks.kt$Bitlinks$group_guid: String = ""</ID>
|
||||||
<ID>FunctionParameterNaming:Bitly.kt$Bitly$long_url: String</ID>
|
<ID>FunctionParameterNaming:Bitlinks.kt$Bitlinks$long_url: String</ID>
|
||||||
<ID>NestedBlockDepth:Bitly.kt$Bitly$executeCall</ID>
|
<ID>NestedBlockDepth:Utils.kt$Utils.Companion$call</ID>
|
||||||
<ID>NestedBlockDepth:Bitly.kt$Bitly$logApiError</ID>
|
<ID>NestedBlockDepth:Utils.kt$Utils.Companion$logApiError</ID>
|
||||||
<ID>ReturnCount:Bitly.kt$Bitly$private fun parseJsonResponse(response: String, key: String, default: String, isJson: Boolean): String</ID>
|
<ID>ReturnCount:Bitlinks.kt$Bitlinks$private fun parseJsonResponse(response: String, key: String, default: String, isJson: Boolean): String</ID>
|
||||||
</Whitelist>
|
</Whitelist>
|
||||||
</SmellBaseline>
|
</SmellBaseline>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import net.thauvin.erik.bitly.Bitly;
|
||||||
|
|
||||||
public class BitlySample {
|
public class BitlySample {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.out.println(new Bitly("YOUR_API_KEY").shorten("https://erik.thauvin.net/blog"));
|
System.out.println(new Bitly("YOUR_API_KEY").bitlinks().shorten("https://erik.thauvin.net/blog"));
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,6 @@ import net.thauvin.erik.bitly.Bitly
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
println(Bitly("YOUR_API_KEY").shorten("https://erik.thauvin.net/blog"))
|
println(Bitly("YOUR_API_KEY").bitlinks().shorten("https://erik.thauvin.net/blog"))
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
|
|
125
src/main/kotlin/net/thauvin/erik/bitly/Bitlinks.kt
Normal file
125
src/main/kotlin/net/thauvin/erik/bitly/Bitlinks.kt
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Bitlinks.kt
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020, 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.bitly
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitlinks methods implementation.
|
||||||
|
*
|
||||||
|
* See the [Bitly API](https://dev.bitly.com/v4/#tag/Bitlinks) for more information.
|
||||||
|
*/
|
||||||
|
class Bitlinks(private val accessToken: String) {
|
||||||
|
/**
|
||||||
|
* Expands a Bitlink.
|
||||||
|
*
|
||||||
|
* See the [Bit.ly API](https://dev.bitly.com/v4/#operation/expandBitlink) for more information.
|
||||||
|
*
|
||||||
|
* @param bitlink_id The bitlink ID.
|
||||||
|
* @param isJson Returns the full JSON API response if `true`
|
||||||
|
* @return THe long URL or JSON API response.
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun expand(bitlink_id: String, isJson: Boolean = false): String {
|
||||||
|
var longUrl = if (isJson) "{}" else ""
|
||||||
|
if (bitlink_id.isNotBlank()) {
|
||||||
|
val response = Utils.call(
|
||||||
|
accessToken,
|
||||||
|
Utils.buildEndPointUrl("/expand"),
|
||||||
|
mapOf(Pair("bitlink_id", bitlink_id.removePrefix("http://").removePrefix("https://"))),
|
||||||
|
Methods.POST
|
||||||
|
)
|
||||||
|
longUrl = parseJsonResponse(response, "long_url", longUrl, isJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
return longUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JSONObject.getString(key: String, default: String): String {
|
||||||
|
return if (this.has(key))
|
||||||
|
this.getString(key)
|
||||||
|
else
|
||||||
|
default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortens a long URL.
|
||||||
|
*
|
||||||
|
* See the [Bit.ly API](https://dev.bitly.com/v4/#operation/createBitlink) for more information.
|
||||||
|
*
|
||||||
|
* @param long_url The long URL.
|
||||||
|
* @param group_guid The group UID.
|
||||||
|
* @param domain The domain for the short URL, defaults to `bit.ly`.
|
||||||
|
* @param isJson Returns the full JSON API response if `true`
|
||||||
|
* @return THe short URL or JSON API response.
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun shorten(long_url: String, group_guid: String = "", domain: String = "", isJson: Boolean = false): String {
|
||||||
|
var bitlink = if (isJson) "{}" else ""
|
||||||
|
if (!Utils.validateUrl(long_url)) {
|
||||||
|
Utils.logger.severe("Please specify a valid URL to shorten.")
|
||||||
|
} else {
|
||||||
|
val params: HashMap<String, String> = HashMap()
|
||||||
|
if (group_guid.isNotBlank()) {
|
||||||
|
params["group_guid"] = group_guid
|
||||||
|
}
|
||||||
|
if (domain.isNotBlank()) {
|
||||||
|
params["domain"] = domain
|
||||||
|
}
|
||||||
|
params["long_url"] = long_url
|
||||||
|
|
||||||
|
val response = Utils.call(accessToken, Utils.buildEndPointUrl("/shorten"), params)
|
||||||
|
|
||||||
|
bitlink = parseJsonResponse(response, "link", bitlink, isJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitlink
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseJsonResponse(response: String, key: String, default: String, isJson: Boolean): String {
|
||||||
|
if (response.isNotEmpty()) {
|
||||||
|
if (isJson) {
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return JSONObject(response).getString(key, default)
|
||||||
|
} catch (jse: JSONException) {
|
||||||
|
Utils.logger.log(Level.SEVERE, "An error occurred parsing the response from bitly.", jse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return default
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,25 +32,13 @@
|
||||||
|
|
||||||
package net.thauvin.erik.bitly
|
package net.thauvin.erik.bitly
|
||||||
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import org.json.JSONException
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.MalformedURLException
|
|
||||||
import java.net.URL
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import java.util.logging.Level
|
|
||||||
import java.util.logging.Logger
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP methods.
|
* HTTP methods.
|
||||||
*/
|
*/
|
||||||
enum class Methods {
|
enum class Methods {
|
||||||
DELETE, GET, PATCH, POST
|
DELETE, GET, PATCH, POST
|
||||||
|
@ -62,35 +50,9 @@ enum class Methods {
|
||||||
* @constructor Creates new instance.
|
* @constructor Creates new instance.
|
||||||
*/
|
*/
|
||||||
open class Bitly() {
|
open class Bitly() {
|
||||||
/** Constants for this package. **/
|
|
||||||
object Constants {
|
|
||||||
/** The Bitly API base URL. **/
|
|
||||||
const val API_BASE_URL = "https://api-ssl.bitly.com/v4"
|
|
||||||
|
|
||||||
/** The API access token environment variable. **/
|
|
||||||
const val ENV_ACCESS_TOKEN = "BITLY_ACCESS_TOKEN"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The API access token. **/
|
/** The API access token. **/
|
||||||
var accessToken: String = System.getenv(Constants.ENV_ACCESS_TOKEN) ?: ""
|
var accessToken: String = System.getenv(Constants.ENV_ACCESS_TOKEN) ?: ""
|
||||||
|
|
||||||
/** The logger instance. **/
|
|
||||||
val logger: Logger by lazy { Logger.getLogger(Bitly::class.java.simpleName) }
|
|
||||||
|
|
||||||
private var client: OkHttpClient
|
|
||||||
|
|
||||||
init {
|
|
||||||
client = if (logger.isLoggable(Level.FINE)) {
|
|
||||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
|
||||||
redactHeader("Authorization")
|
|
||||||
}
|
|
||||||
OkHttpClient.Builder().addInterceptor(httpLoggingInterceptor).build()
|
|
||||||
} else {
|
|
||||||
OkHttpClient.Builder().build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance using an [API Access Token][accessToken].
|
* Creates a new instance using an [API Access Token][accessToken].
|
||||||
*
|
*
|
||||||
|
@ -105,7 +67,7 @@ open class Bitly() {
|
||||||
* Creates a new instance using a [Properties][properties] and [Property Key][key].
|
* Creates a new instance using a [Properties][properties] and [Property Key][key].
|
||||||
*
|
*
|
||||||
* @param properties The properties.
|
* @param properties The properties.
|
||||||
* @param key The property key.
|
* @param key The property key containing the [API Access Token][accessToken].
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
|
@ -117,7 +79,7 @@ open class Bitly() {
|
||||||
* Creates a new instance using a [Properties File Path][propertiesFilePath] and [Property Key][key].
|
* Creates a new instance using a [Properties File Path][propertiesFilePath] and [Property Key][key].
|
||||||
*
|
*
|
||||||
* @param propertiesFilePath The properties file path.
|
* @param propertiesFilePath The properties file path.
|
||||||
* @param key The property key.
|
* @param key The property key containing the [API Access Token][accessToken].
|
||||||
*/
|
*/
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(propertiesFilePath: Path, key: String = Constants.ENV_ACCESS_TOKEN) : this() {
|
constructor(propertiesFilePath: Path, key: String = Constants.ENV_ACCESS_TOKEN) : this() {
|
||||||
|
@ -134,194 +96,24 @@ open class Bitly() {
|
||||||
* Creates a new instance using a [Properties File][propertiesFile] and [Property Key][key].
|
* Creates a new instance using a [Properties File][propertiesFile] and [Property Key][key].
|
||||||
*
|
*
|
||||||
* @param propertiesFile The properties file.
|
* @param propertiesFile The properties file.
|
||||||
* @param key The property key.
|
* @param key The property key containing the [API Access Token][accessToken].
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(propertiesFile: File, key: String = Constants.ENV_ACCESS_TOKEN) : this(propertiesFile.toPath(), key)
|
constructor(propertiesFile: File, key: String = Constants.ENV_ACCESS_TOKEN) : this(propertiesFile.toPath(), key)
|
||||||
|
|
||||||
/**
|
/** Bitlinks accessor. **/
|
||||||
* Builds the full API endpoint URL using the [Constants.API_BASE_URL].
|
fun bitlinks(): Bitlinks = Bitlinks(accessToken)
|
||||||
*
|
|
||||||
* @param endPointPath The REST method path. (eg. `/shorten', '/user`)
|
|
||||||
*/
|
|
||||||
fun buildEndPointUrl(endPointPath: String): String {
|
|
||||||
return if (endPointPath.startsWith('/')) {
|
|
||||||
"${Constants.API_BASE_URL}$endPointPath"
|
|
||||||
} else {
|
|
||||||
"${Constants.API_BASE_URL}/$endPointPath"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes an API call.
|
* Executes an API call.
|
||||||
*
|
*
|
||||||
* @param endPoint The API endpoint. (eg. `/shorten`, `/user`)
|
* @param endPoint The REST endpoint. (eg. `https://api-ssl.bitly.com/v4/shorten`)
|
||||||
* @param params The request parameters kev/value map.
|
* @param params The request parameters kev/value map.
|
||||||
* @param method The submission [Method][Methods].
|
* @param method The submission [Method][Methods].
|
||||||
* @return The response (JSON) from the API.
|
* @return The response (JSON) from the API.
|
||||||
*/
|
*/
|
||||||
fun call(endPoint: String, params: Map<String, String>, method: Methods = Methods.POST): String {
|
fun call(endPoint: String, params: Map<String, String>, method: Methods = Methods.POST): String {
|
||||||
var response = ""
|
return Utils.call(accessToken, endPoint, params, method)
|
||||||
if (endPoint.isBlank()) {
|
|
||||||
logger.severe("Please specify a valid API endpoint.")
|
|
||||||
} else if (accessToken.isBlank()) {
|
|
||||||
logger.severe("Please specify a valid API access token.")
|
|
||||||
} else {
|
|
||||||
val apiUrl = endPoint.toHttpUrlOrNull()
|
|
||||||
if (apiUrl != null) {
|
|
||||||
val builder = when (method) {
|
|
||||||
Methods.POST, Methods.PATCH -> {
|
|
||||||
val formBody = JSONObject(params).toString()
|
|
||||||
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
|
||||||
Request.Builder().apply {
|
|
||||||
url(apiUrl.newBuilder().build())
|
|
||||||
if (method == Methods.POST) {
|
|
||||||
post(formBody)
|
|
||||||
} else {
|
|
||||||
patch(formBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Methods.DELETE -> Request.Builder().url(apiUrl.newBuilder().build()).delete()
|
|
||||||
else -> {
|
|
||||||
val httpUrl = apiUrl.newBuilder().apply {
|
|
||||||
params.forEach {
|
|
||||||
addQueryParameter(it.key, it.value)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
Request.Builder().url(httpUrl)
|
|
||||||
}
|
|
||||||
}.addHeader("Authorization", "Bearer $accessToken")
|
|
||||||
|
|
||||||
val result = client.newCall(builder.build()).execute()
|
|
||||||
|
|
||||||
val body = result.body?.string()
|
|
||||||
if (body != null) {
|
|
||||||
if (!result.isSuccessful && body.isNotEmpty()) {
|
|
||||||
logApiError(body, result.code)
|
|
||||||
}
|
|
||||||
response = body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expands a Bitlink.
|
|
||||||
*
|
|
||||||
* See the [Bit.ly API](https://dev.bitly.com/v4/#operation/expandBitlink) for more information.
|
|
||||||
*
|
|
||||||
* @param bitlink_id The bitlink ID.
|
|
||||||
* @param isJson Returns the full JSON API response if `true`
|
|
||||||
* @return THe long URL or JSON API response.
|
|
||||||
*/
|
|
||||||
@JvmOverloads
|
|
||||||
fun expand(bitlink_id: String, isJson: Boolean = false): String {
|
|
||||||
var longUrl = if (isJson) "{}" else ""
|
|
||||||
if (bitlink_id.isNotBlank()) {
|
|
||||||
val response = call(
|
|
||||||
buildEndPointUrl("/expand"),
|
|
||||||
mapOf(
|
|
||||||
Pair(
|
|
||||||
"bitlink_id",
|
|
||||||
bitlink_id.removePrefix("http://").removePrefix("https://")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Methods.POST
|
|
||||||
)
|
|
||||||
longUrl = parseJsonResponse(response, "long_url", longUrl, isJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
return longUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JSONObject.getString(key: String, default: String): String {
|
|
||||||
return if (this.has(key))
|
|
||||||
this.getString(key)
|
|
||||||
else
|
|
||||||
default
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shortens a long URL.
|
|
||||||
*
|
|
||||||
* See the [Bit.ly API](https://dev.bitly.com/v4/#operation/createBitlink) for more information.
|
|
||||||
*
|
|
||||||
* @param long_url The long URL.
|
|
||||||
* @param group_guid The group UID.
|
|
||||||
* @param domain The domain for the short URL, defaults to `bit.ly`.
|
|
||||||
* @param isJson Returns the full JSON API response if `true`
|
|
||||||
* @return THe short URL or JSON API response.
|
|
||||||
*/
|
|
||||||
@JvmOverloads
|
|
||||||
fun shorten(long_url: String, group_guid: String = "", domain: String = "", isJson: Boolean = false): String {
|
|
||||||
var bitlink = if (isJson) "{}" else ""
|
|
||||||
if (!validateUrl(long_url)) {
|
|
||||||
logger.severe("Please specify a valid URL to shorten.")
|
|
||||||
} else {
|
|
||||||
val params: HashMap<String, String> = HashMap()
|
|
||||||
if (group_guid.isNotBlank()) {
|
|
||||||
params["group_guid"] = group_guid
|
|
||||||
}
|
|
||||||
if (domain.isNotBlank()) {
|
|
||||||
params["domain"] = domain
|
|
||||||
}
|
|
||||||
params["long_url"] = long_url
|
|
||||||
|
|
||||||
val response = call(buildEndPointUrl("/shorten"), params)
|
|
||||||
|
|
||||||
bitlink = parseJsonResponse(response, "link", bitlink, isJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
return bitlink
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logApiError(body: String, resultCode: Int) {
|
|
||||||
try {
|
|
||||||
with(JSONObject(body)) {
|
|
||||||
if (has("message")) {
|
|
||||||
logger.severe(getString("message") + " ($resultCode)")
|
|
||||||
}
|
|
||||||
if (has("description")) {
|
|
||||||
val description = getString("description")
|
|
||||||
if (description.isNotBlank()) {
|
|
||||||
logger.severe(description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ignore: JSONException) {
|
|
||||||
logger.severe("An error occurred parsing the error response from bitly.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseJsonResponse(response: String, key: String, default: String, isJson: Boolean): String {
|
|
||||||
if (response.isNotEmpty()) {
|
|
||||||
if (isJson) {
|
|
||||||
return response
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return JSONObject(response).getString(key, default)
|
|
||||||
} catch (ignore: JSONException) {
|
|
||||||
logger.severe("An error occurred parsing the response from bitly.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return default
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateUrl(url: String): Boolean {
|
|
||||||
var isValid = url.isNotBlank()
|
|
||||||
if (isValid) {
|
|
||||||
try {
|
|
||||||
URL(url)
|
|
||||||
} catch (e: MalformedURLException) {
|
|
||||||
logger.log(Level.FINE, "Invalid URL: $url", e)
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isValid
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
44
src/main/kotlin/net/thauvin/erik/bitly/Constants.kt
Normal file
44
src/main/kotlin/net/thauvin/erik/bitly/Constants.kt
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Constants.kt
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020, 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.bitly
|
||||||
|
|
||||||
|
/** Constants for this package. **/
|
||||||
|
class Constants private constructor() {
|
||||||
|
companion object Constants {
|
||||||
|
/** The Bitly API base URL. **/
|
||||||
|
const val API_BASE_URL = "https://api-ssl.bitly.com/v4"
|
||||||
|
|
||||||
|
/** The API access token environment variable. **/
|
||||||
|
const val ENV_ACCESS_TOKEN = "BITLY_ACCESS_TOKEN"
|
||||||
|
}
|
||||||
|
}
|
172
src/main/kotlin/net/thauvin/erik/bitly/Utils.kt
Normal file
172
src/main/kotlin/net/thauvin/erik/bitly/Utils.kt
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* Utils.kt
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020, 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.bitly
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.net.MalformedURLException
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.logging.Level
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
/** Useful functions. */
|
||||||
|
class Utils private constructor() {
|
||||||
|
companion object {
|
||||||
|
/** The logger instance. **/
|
||||||
|
val logger: Logger by lazy { Logger.getLogger(Bitly::class.java.simpleName) }
|
||||||
|
|
||||||
|
private val client: OkHttpClient = if (logger.isLoggable(Level.FINE)) {
|
||||||
|
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
redactHeader("Authorization")
|
||||||
|
}
|
||||||
|
OkHttpClient.Builder().addInterceptor(httpLoggingInterceptor).build()
|
||||||
|
} else {
|
||||||
|
OkHttpClient.Builder().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the full API endpoint URL using the [Constants.API_BASE_URL].
|
||||||
|
*
|
||||||
|
* @param endPointPath The REST request path. (eg. `/shorten', '/user`)
|
||||||
|
*/
|
||||||
|
fun buildEndPointUrl(endPointPath: String): String {
|
||||||
|
return if (endPointPath.startsWith('/')) {
|
||||||
|
"${Constants.API_BASE_URL}$endPointPath"
|
||||||
|
} else {
|
||||||
|
"${Constants.API_BASE_URL}/$endPointPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes an API call.
|
||||||
|
*
|
||||||
|
* @param accessToken The API access token.
|
||||||
|
* @param endPoint The REST endpoint. (eg. `https://api-ssl.bitly.com/v4/shorten`)
|
||||||
|
* @param params The request parameters kev/value map.
|
||||||
|
* @param method The submission [Method][Methods].
|
||||||
|
* @return The response (JSON) from the API.
|
||||||
|
*/
|
||||||
|
fun call(
|
||||||
|
accessToken: String,
|
||||||
|
endPoint: String,
|
||||||
|
params: Map<String, String>,
|
||||||
|
method: Methods = Methods.POST
|
||||||
|
): String {
|
||||||
|
var response = ""
|
||||||
|
if (endPoint.isBlank()) {
|
||||||
|
logger.severe("Please specify a valid API endpoint.")
|
||||||
|
} else if (accessToken.isBlank()) {
|
||||||
|
logger.severe("Please specify a valid API access token.")
|
||||||
|
} else {
|
||||||
|
val apiUrl = endPoint.toHttpUrlOrNull()
|
||||||
|
if (apiUrl != null) {
|
||||||
|
val builder = when (method) {
|
||||||
|
Methods.POST, Methods.PATCH -> {
|
||||||
|
val formBody = JSONObject(params).toString()
|
||||||
|
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||||
|
Request.Builder().apply {
|
||||||
|
url(apiUrl.newBuilder().build())
|
||||||
|
if (method == Methods.POST) {
|
||||||
|
post(formBody)
|
||||||
|
} else {
|
||||||
|
patch(formBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Methods.DELETE -> Request.Builder().url(apiUrl.newBuilder().build()).delete()
|
||||||
|
else -> {
|
||||||
|
val httpUrl = apiUrl.newBuilder().apply {
|
||||||
|
params.forEach {
|
||||||
|
addQueryParameter(it.key, it.value)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
Request.Builder().url(httpUrl)
|
||||||
|
}
|
||||||
|
}.addHeader("Authorization", "Bearer $accessToken")
|
||||||
|
|
||||||
|
val result = client.newCall(builder.build()).execute()
|
||||||
|
|
||||||
|
val body = result.body?.string()
|
||||||
|
if (body != null) {
|
||||||
|
if (!result.isSuccessful && body.isNotEmpty()) {
|
||||||
|
logApiError(body, result.code)
|
||||||
|
}
|
||||||
|
response = body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logApiError(body: String, resultCode: Int) {
|
||||||
|
try {
|
||||||
|
with(JSONObject(body)) {
|
||||||
|
if (has("message")) {
|
||||||
|
logger.severe(getString("message") + " ($resultCode)")
|
||||||
|
}
|
||||||
|
if (has("description")) {
|
||||||
|
val description = getString("description")
|
||||||
|
if (description.isNotBlank()) {
|
||||||
|
logger.severe(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (jse: JSONException) {
|
||||||
|
logger.log(Level.SEVERE, "An error occurred parsing the error response from bitly.", jse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a URL.
|
||||||
|
*/
|
||||||
|
fun validateUrl(url: String): Boolean {
|
||||||
|
if (url.isNotBlank()) {
|
||||||
|
try {
|
||||||
|
URL(url)
|
||||||
|
return true
|
||||||
|
} catch (e: MalformedURLException) {
|
||||||
|
logger.log(Level.FINE, "Invalid URL: $url", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,7 +51,7 @@ class BitlyTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun before() {
|
fun before() {
|
||||||
with(bitly.logger) {
|
with(Utils.logger) {
|
||||||
level = Level.FINE
|
level = Level.FINE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,33 +62,33 @@ class BitlyTest {
|
||||||
if (System.getenv("CI") == "true") {
|
if (System.getenv("CI") == "true") {
|
||||||
test.accessToken = ""
|
test.accessToken = ""
|
||||||
}
|
}
|
||||||
assertEquals("", test.shorten(blog))
|
assertEquals("", test.bitlinks().shorten(blog))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `token should be valid`() {
|
fun `token should be valid`() {
|
||||||
val test = Bitly().apply { accessToken = "12345679" }
|
val test = Bitly().apply { accessToken = "12345679" }
|
||||||
assertEquals("{\"message\":\"FORBIDDEN\"}", test.shorten("https://erik.thauvin.net/blog", isJson = true))
|
assertEquals("{\"message\":\"FORBIDDEN\"}", test.bitlinks().shorten("https://erik.thauvin.net/blog", isJson = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `long url should be valid`() {
|
fun `long url should be valid`() {
|
||||||
assertEquals("", bitly.shorten(""))
|
assertEquals("", bitly.bitlinks().shorten(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `shorten = expand`() {
|
fun `shorten = expand`() {
|
||||||
val shortUrl = bitly.shorten(blog, domain = "bit.ly")
|
val shortUrl = bitly.bitlinks().shorten(blog, domain = "bit.ly")
|
||||||
assertEquals(blog, bitly.expand(shortUrl))
|
assertEquals(blog, bitly.bitlinks().expand(shortUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `as json`() {
|
fun `as json`() {
|
||||||
assertTrue(bitly.shorten(blog, isJson = true).startsWith("{\"created_at\":"))
|
assertTrue(bitly.bitlinks().shorten(blog, isJson = true).startsWith("{\"created_at\":"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `get user`() {
|
fun `get user`() {
|
||||||
assertTrue(bitly.call(bitly.buildEndPointUrl("user"), emptyMap(), Methods.GET).contains("\"login\":"))
|
assertTrue(bitly.call(Utils.buildEndPointUrl("user"), emptyMap(), Methods.GET).contains("\"login\":"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue