From fc77b733996f913e7318745284825138cd04a7cf Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Wed, 27 Sep 2023 17:21:16 -0700 Subject: [PATCH] Added pin config builder. Closes #10 --- README.md | 46 +++++-- config/detekt/baseline.xml | 5 +- .../erik/pinboard/samples/JavaExample.java | 10 +- .../erik/pinboard/samples/KotlinExample.kt | 4 +- .../net/thauvin/erik/pinboard/PinConfig.kt | 112 ++++++++++++++++++ .../thauvin/erik/pinboard/PinboardPoster.kt | 30 ++++- .../erik/pinboard/PinboardPosterTest.kt | 49 ++++++-- 7 files changed, 225 insertions(+), 31 deletions(-) create mode 100644 src/main/kotlin/net/thauvin/erik/pinboard/PinConfig.kt diff --git a/README.md b/README.md index e3c34cb..14d1b5d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ -# [Pinboard](https://pinboard.in) Poster for Kotlin/Java +# [Pinboard](https://pinboard.in) Poster for Kotlin, Java and Android -[![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](https://opensource.org/licenses/BSD-3-Clause) [![Release](https://img.shields.io/github/release/ethauvin/pinboard-poster.svg)](https://github.com/ethauvin/pinboard-poster/releases/latest) [![Maven Central](https://img.shields.io/maven-central/v/net.thauvin.erik/pinboard-poster.svg?label=maven%20central)](https://search.maven.org/search?q=g:%22net.thauvin.erik%22%20AND%20a:%22pinboard-poster%22) +[![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](https://opensource.org/licenses/BSD-3-Clause) [![Release](https://img.shields.io/github/release/ethauvin/pinboard-poster.svg)](https://github.com/ethauvin/pinboard-poster/releases/latest) +[![Maven Central](https://img.shields.io/maven-central/v/net.thauvin.erik/pinboard-poster.svg?color=blue)](https://central.sonatype.com/artifact/net.thauvin.erik/pinboard-poster) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ethauvin_pinboard-poster&metric=alert_status)](https://sonarcloud.io/dashboard?id=ethauvin_pinboard-poster) [![GitHub CI](https://github.com/ethauvin/pinboard-poster/actions/workflows/gradle.yml/badge.svg)](https://github.com/ethauvin/pinboard-poster/actions/workflows/gradle.yml) [![CircleCI](https://circleci.com/gh/ethauvin/pinboard-poster/tree/master.svg?style=shield)](https://circleci.com/gh/ethauvin/pinboard-poster/tree/master) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ethauvin_pinboard-poster&metric=alert_status)](https://sonarcloud.io/dashboard?id=ethauvin_pinboard-poster) +[![GitHub CI](https://github.com/ethauvin/pinboard-poster/actions/workflows/gradle.yml/badge.svg)](https://github.com/ethauvin/pinboard-poster/actions/workflows/gradle.yml) +[![CircleCI](https://circleci.com/gh/ethauvin/pinboard-poster/tree/master.svg?style=shield)](https://circleci.com/gh/ethauvin/pinboard-poster/tree/master) -A small Kotlin/Java/Android library for posting to [Pinboard](https://pinboard.in). +A small library for posting to [Pinboard](https://pinboard.in). ## Examples @@ -15,19 +18,28 @@ A small Kotlin/Java/Android library for posting to [Pinboard](https://pinboard.i val poster = PinboardPoster("user:TOKEN") poster.addPin("https://www.example.com/foo", "This is a test") +poster.addPin("https://examples.com", "This is a test", tags = arrayOf("foo", "bar")) poster.deletePin("https:///www.example.com/bar") ``` + [View Example](https://github.com/ethauvin/pinboard-poster/blob/master/samples/kotlin/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt) ### Java + ```java final PinboardPoster poster = new PinBboardPoster("user:TOKEN"); poster.addPin("https://www.example.com/foo", "This is a test"); +poster.addPin(new AddConfig.Builder() + .url("https://example.com") + .description("This is a test") + .tags("foo", "bar") + .build()); poster.deletePin("https:///www.example.com/bar"); ``` + [View Example](https://github.com/ethauvin/pinboard-poster/blob/master/samples/java/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java) Your API authentication token is available on the [Pinboard settings page](https://pinboard.in/settings/password). @@ -45,24 +57,29 @@ dependencies { compile 'net.thauvin.erik:pinboard-poster:1.0.4' } ``` + [View Example](https://github.com/ethauvin/pinboard-poster/blob/master/samples/java/build.gradle) [View Kotlin DSL Example](https://github.com/ethauvin/pinboard-poster/blob/master/samples/kotlin/build.gradle.kts) -Instructions for using with Maven, Ivy, etc. can be found on [Maven Central](https://search.maven.org/artifact/net.thauvin.erik/pinboard-poster/1.0.4/jar). +Instructions for using with Maven, Ivy, etc. can be found on [Maven Central](https://central.sonatype.com/artifact/net.thauvin.erik/pinboard-poster). ## Adding The `addPin` function support all of the [Pinboard API parameters](https://pinboard.in/api/#posts_add): ```kotlin -poster.addPin(url = "https://www.example.com", - description = "This is the title", - extended = "This is the extended description.", - tags = "tag1 tag2 tag3", - dt = "2010-12-11T19:48:02Z", - replace = true, - shared = true, - toRead = false) +import java.time.ZonedDateTime + +poster.addPin( + url = "https://www.example.com", + description = "This is the title", + extended = "This is the extended description.", + tags = arrayOf("tag1", "tag2", "tag3"), + dt = ZonedDateTime.now(), + replace = true, + shared = true, + toRead = false +) ``` `url` and `description` are required. @@ -84,13 +101,16 @@ It returns `true` if the bookmark was deleted successfully, `false` otherwise. The library used [`java.util.logging`](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html) to log errors. Logging can be configured as follows: #### Kotlin + ```kotlin with(poster.logger) { addHandler(ConsoleHandler().apply { level = Level.FINE }) level = Level.FINE } ``` + #### Java + ```java final ConsoleHandler consoleHandler = new ConsoleHandler(); consoleHandler.setLevel(Level.FINE); diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 840b4b1..278860b 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -2,8 +2,11 @@ - LongParameterList:PinboardPoster.kt$PinboardPoster$( url: String, description: String, extended: String = "", tags: String = "", dt: String = "", replace: Boolean = true, shared: Boolean = true, toRead: Boolean = false ) + LongParameterList:PinConfig.kt$PinConfig$( val url: String, val description: String, val extended: String, val tags: Array<out String>, val dt: ZonedDateTime, val replace: Boolean, val shared: Boolean, val toRead: Boolean ) + LongParameterList:PinboardPoster.kt$PinboardPoster$( url: String, description: String, extended: String = "", vararg tags: String = emptyArray(), dt: ZonedDateTime = ZonedDateTime.now(), replace: Boolean = true, shared: Boolean = true, toRead: Boolean = false ) NestedBlockDepth:PinboardPoster.kt$PinboardPoster$private fun executeMethod(method: String, params: Map<String, String>): Boolean ThrowsCount:PinboardPoster.kt$PinboardPoster$@Throws(IOException::class) internal fun parseMethodResponse(method: String, response: String) + TooManyFunctions:PinConfig.kt$PinConfig$Builder + WildcardImport:PinboardPosterTest.kt$import org.testng.Assert.* diff --git a/samples/java/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java b/samples/java/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java index e6da4fc..1331a88 100644 --- a/samples/java/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java +++ b/samples/java/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java @@ -31,6 +31,7 @@ */ package net.thauvin.erik.pinboard.samples; +import net.thauvin.erik.pinboard.PinConfig; import net.thauvin.erik.pinboard.PinboardPoster; import java.nio.file.Paths; @@ -40,7 +41,7 @@ import java.util.logging.Logger; public class JavaExample { public static void main(String[] args) { - final String url = "http://www.example.com/pinboard"; + final String url = "httpz://example.com/pinboard"; final PinboardPoster poster; if (args.length == 1) { @@ -59,7 +60,12 @@ public class JavaExample { logger.setLevel(Level.FINE); // Add Pin - if (poster.addPin(url, "Testing", "Extended test", "test java")) { + if (poster.addPin(new PinConfig.Builder() + .url(url) + .description("Testing") + .extended("Extra") + .tags("test", "java") + .build())) { System.out.println("Added: " + url); } diff --git a/samples/kotlin/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt b/samples/kotlin/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt index 77c8ba5..73b3a83 100644 --- a/samples/kotlin/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt +++ b/samples/kotlin/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt @@ -37,7 +37,7 @@ import java.util.logging.ConsoleHandler import java.util.logging.Level fun main(args: Array) { - val url = "http://www.example.com/pinboard" + val url = "httpz://example.com/pinboard" val poster = if (args.size == 1) { // API Token is an argument @@ -54,7 +54,7 @@ fun main(args: Array) { } // Add Pin - if (poster.addPin(url, "Testing", "Extended test", "test kotlin")) { + if (poster.addPin(url, "Testing", "Extended test", tags = arrayOf("test", "kotlin"))) { println("Added: $url") } diff --git a/src/main/kotlin/net/thauvin/erik/pinboard/PinConfig.kt b/src/main/kotlin/net/thauvin/erik/pinboard/PinConfig.kt new file mode 100644 index 0000000..bb6a8fd --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/pinboard/PinConfig.kt @@ -0,0 +1,112 @@ +/* + * PinConfig.kt + * + * Copyright (c) 2017-2023, 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.pinboard + +import java.time.ZonedDateTime + +/** + * Provides a builder to add a pin. + */ +class PinConfig private constructor( + val url: String, + val description: String, + val extended: String, + val tags: Array, + val dt: ZonedDateTime, + val replace: Boolean, + val shared: Boolean, + val toRead: Boolean +) { + /** + * Configures the parameters to add a pin. + */ + data class Builder( + private var url: String = "", + private var description: String = "", + private var extended: String = "", + private var tags: Array = emptyArray(), + private var dt: ZonedDateTime = ZonedDateTime.now(), + private var replace: Boolean = true, + private var shared: Boolean = true, + private var toRead: Boolean = false + ) { + fun url(url: String) = apply { this.url = url } + fun description(description: String) = apply { this.description = description } + fun extended(extended: String) = apply { this.extended = extended } + fun tags(vararg tag: String) = apply { this.tags = tag } + fun dt(datetime: ZonedDateTime) = apply { this.dt = datetime } + fun replace(replace: Boolean) = apply { this.replace = replace } + fun shared(shared: Boolean) = apply { this.shared = shared } + fun toRead(toRead: Boolean) = apply { this.toRead = toRead } + + fun build() = PinConfig( + url, + description, + extended, + tags, + dt, + replace, + shared, + toRead + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Builder + + if (url != other.url) return false + if (description != other.description) return false + if (extended != other.extended) return false + if (!tags.contentEquals(other.tags)) return false + if (dt != other.dt) return false + if (replace != other.replace) return false + if (shared != other.shared) return false + if (toRead != other.toRead) return false + + return true + } + + override fun hashCode(): Int { + var result = url.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + extended.hashCode() + result = 31 * result + tags.contentHashCode() + result = 31 * result + dt.hashCode() + result = 31 * result + replace.hashCode() + result = 31 * result + shared.hashCode() + result = 31 * result + toRead.hashCode() + return result + } + } +} diff --git a/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt b/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt index c1d3005..0b9514d 100644 --- a/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt +++ b/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt @@ -1,7 +1,7 @@ /* * PinboardPoster.kt * - * Copyright (c) 2017-2022, Erik C. Thauvin (erik@thauvin.net) + * Copyright (c) 2017-2023, Erik C. Thauvin (erik@thauvin.net) * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -44,6 +44,8 @@ import java.net.MalformedURLException import java.net.URL import java.nio.file.Files import java.nio.file.Path +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter import java.util.* import java.util.logging.Level import java.util.logging.Logger @@ -132,6 +134,24 @@ open class PinboardPoster() { }.build() } + /** + * Adds a bookmark to Pinboard + * + * This method supports of all the [Pinboard API Parameters](https://pinboard.in/api/#posts_add). + */ + fun addPin(config: PinConfig): Boolean { + return addPin( + url = config.url, + description = config.description, + extended = config.extended, + tags = config.tags, + dt = config.dt, + replace = config.replace, + shared = config.shared, + toRead = config.toRead + ) + } + /** * Adds a bookmark to Pinboard. * @@ -153,8 +173,8 @@ open class PinboardPoster() { url: String, description: String, extended: String = "", - tags: String = "", - dt: String = "", + vararg tags: String = emptyArray(), + dt: ZonedDateTime = ZonedDateTime.now(), replace: Boolean = true, shared: Boolean = true, toRead: Boolean = false @@ -169,8 +189,8 @@ open class PinboardPoster() { "url" to url, "description" to description, "extended" to extended, - "tags" to tags, - "dt" to dt, + "tags" to tags.joinToString(","), + "dt" to DateTimeFormatter.ISO_INSTANT.format(dt.withNano(0)), "replace" to yesNo(replace), "shared" to yesNo(shared), "toread" to yesNo(toRead) diff --git a/src/test/kotlin/net/thauvin/erik/pinboard/PinboardPosterTest.kt b/src/test/kotlin/net/thauvin/erik/pinboard/PinboardPosterTest.kt index dd5477c..a44cf25 100644 --- a/src/test/kotlin/net/thauvin/erik/pinboard/PinboardPosterTest.kt +++ b/src/test/kotlin/net/thauvin/erik/pinboard/PinboardPosterTest.kt @@ -1,7 +1,7 @@ /* * PinboardPosterTest.kt * - * Copyright (c) 2017-2022, Erik C. Thauvin (erik@thauvin.net) + * Copyright (c) 2017-2023, Erik C. Thauvin (erik@thauvin.net) * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,20 +32,20 @@ package net.thauvin.erik.pinboard -import org.testng.Assert.assertFalse -import org.testng.Assert.assertTrue -import org.testng.Assert.expectThrows +import org.testng.Assert.* import org.testng.annotations.Test import java.io.IOException import java.nio.file.Files import java.nio.file.Paths -import java.util.Properties +import java.time.ZonedDateTime +import java.util.* import java.util.logging.Level class PinboardPosterTest { private val url = "http://www.example.com/?random=" + (1000..10000).random() private val desc = "This is a test." private val localProps = Paths.get("local.properties") + private val isCi = "true" == System.getenv("CI") @Test fun testAddPin() { @@ -61,10 +61,42 @@ class PinboardPosterTest { // assertFalse(poster.addPin(url, desc), "apiToken: ${poster.apiToken}") poster = PinboardPoster(localProps) - poster.logger.level = Level.FINE + if (!isCi) { + poster.logger.level = Level.FINE + } assertTrue(poster.addPin(url, desc), "apiToken: ${Constants.ENV_API_TOKEN}") } + @Test + fun testAddPinConfig() { + val poster = PinboardPoster(localProps) + if (!isCi) { + poster.logger.level = Level.FINE + } + + var config = PinConfig.Builder().url(url).description(desc).extended("extra") + + assertTrue(poster.addPin(config.build()), "apiToken: ${Constants.ENV_API_TOKEN}") + + config = config.tags("foo", "bar") + assertTrue(poster.addPin(config.build()), "tags(foo,bar)") + + config = config.shared(false) + assertTrue(poster.addPin(config.build()), "shared(false)") + + try { + assertFalse(poster.addPin(config.replace(false).build())) + } catch (e: IOException) { + assertTrue(e.message!!.contains("item already exists")) + } + + config = config.replace(true).toRead(true) + assertTrue(poster.addPin(config.build()), "toRead(true)") + + config = config.dt(ZonedDateTime.now()) + assertTrue(poster.addPin(config.build()), "dt(now)") + } + @Test fun testDeletePin() { val props = if (Files.exists(localProps)) { @@ -84,8 +116,9 @@ class PinboardPosterTest { assertFalse(poster.deletePin(url), "apiEndPoint: ") poster = PinboardPoster(localProps, Constants.ENV_API_TOKEN) - poster.logger.level = Level.FINE - + if (!isCi) { + poster.logger.level = Level.FINE + } poster.apiEndPoint = Constants.API_ENDPOINT assertTrue(poster.deletePin(url), "apiEndPoint: ${Constants.API_ENDPOINT}")