commit 2ee3568e40114e19b0956ea7d12c071d5c49b0d5 Author: Erik C. Thauvin Date: Wed May 17 00:41:35 2017 -0700 Initial commit. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6ec2ae2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# batch files are specific to windows and always crlf +*.bat eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6322c7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +**/.idea/dictionaries +**/.idea/gradle.xml +**/.idea/libraries +**/.idea/tasks.xml +**/.idea/workspace.xml +*.sublime-* +*.iws +.classpath +.DS_Store +.gradle +.kobalt +.nb-gradle +.project +.settings +/bin +/build +/deploy +/dist +/gen +/gradle.properties +/local.properties +/out +/proguard-project.txt +/project.properties +/target +/test-output +ehthumbs.db +kobaltBuild +Thumbs.db \ No newline at end of file diff --git a/.idea/copyright/Erik_s_Copyright_Notice.xml b/.idea/copyright/Erik_s_Copyright_Notice.xml new file mode 100644 index 0000000..08660a1 --- /dev/null +++ b/.idea/copyright/Erik_s_Copyright_Notice.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..a25e371 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8ff795e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,53 @@ + + + + \ No newline at end of file diff --git a/.idea/kobalt.xml b/.idea/kobalt.xml new file mode 100644 index 0000000..7e18043 --- /dev/null +++ b/.idea/kobalt.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..115f0a1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d5b3d2b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/scopes/Source.xml b/.idea/scopes/Source.xml new file mode 100644 index 0000000..cf7c1f7 --- /dev/null +++ b/.idea/scopes/Source.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cda7257 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: java + +jdk: + - oraclejdk8 + +before_install: + - chmod +x kobaltw + +install: true + +cache: + directories: + - $HOME/.m2 + - $HOME/.kobalt + +before_cache: + - rm -rf .kobalt/* + +script: ./kobaltw clean assemble \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a6dbd5 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# [Pinboard](https://pinboard.in) Poster for Kotlin/Java + +[![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](http://opensource.org/licenses/BSD-3-Clause) [![Build Status](https://travis-ci.org/ethauvin/pinboard-poster.svg?branch=master)](https://travis-ci.org/ethauvin/pinboard-poster) [![Dependency Status](https://www.versioneye.com/user/projects/58ff729f6ac171425cd00acf/badge.svg?style=flat-square)](https://www.versioneye.com/user/projects/58ff729f6ac171425cd00acf) [![Download](https://api.bintray.com/packages/ethauvin/maven/pinboard-poster/images/download.svg) ](https://bintray.com/ethauvin/maven/pinboard-poster/_latestVersion) + +A small Kotlin/Java library for posting to [Pinboard](https://pinboard.in). + +## Examples + +### Kotlin + +```kotlin + +val poster = PinboardPoster("user:TOKEN") + +poster.addPin("http://www.example.com/foo", "This is a test") +poster.deletePin("http:///www.example.com/bar") + +``` + +### Java +```java + +final PinboardPoster poster = new PinBboardPoster("user:TOKEN"); + +poster.addPin("http://www.example.com/foo", "This is a test"); +poster.deletePin("http:///www.example.com/bar"); +``` + +Your API authentication token is available on the [Pinboard settings page](https://pinboard.in/settings/password). + +## Usage with Maven, Gradle and Kobalt + +### Maven + +To install and run from Maven, configure an artifact as follows: + +```xml + + net.thauvin.erik + pinboard-poster + 0.9.0 + +``` + +### Gradle + +To install and run from Gradle, add the following to the build.gradle file: + +```gradle +dependencies { + compileOnly 'net.thauvin.erik:pinboard-poster:0.9.0' +} +``` + +### Kobalt + +To install and run from Kobalt, add the following to the Build.kt file: + +```gradle +dependencies { + compile("net.thauvin.erik:pinboard-poster:0.9.0") +} +``` + +## Adding + +The `addPin` function support all of the [Pinboard API parameters](https://pinboard.in/api/#posts_add): + +```kotlin +poster.addPin(url = "http://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) +``` + +`url` and `description` are required. + +It returns `true` if the bookmark was added successfully, `false` otherwise. + +## Deleting + +The `deletePin` function support all of the [Pinboard API parameters](https://pinboard.in/api/#posts_delete): + +```kotlin +poster.deletePin(url = "http://www.example.com/") +``` + +It returns `true` if the bookmark was deleted successfully, `false` otherwise. + +## Logging + +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 +with(poster.logger) { + addHandler(ConsoleHandler().apply { level = Level.FINE }) + level = Level.FINE +} +``` + +or using a property file. + +## API End Point + +The API end point is automatically configured to `https://api.pinboard.in/v1/`. Since Pinboard uses the `del.ico.us` API, the library could potentially be used with another compatible service. To configure the API end point, use: + +```kotlin +poster.apiEndPoint = "https://www.example.com/v1" +``` \ No newline at end of file diff --git a/kobalt/Build.kt.iml b/kobalt/Build.kt.iml new file mode 100644 index 0000000..a17503c --- /dev/null +++ b/kobalt/Build.kt.iml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kobalt/src/Build.kt b/kobalt/src/Build.kt new file mode 100644 index 0000000..80e4a3a --- /dev/null +++ b/kobalt/src/Build.kt @@ -0,0 +1,90 @@ +import com.beust.kobalt.glob +import com.beust.kobalt.plugin.application.application +import com.beust.kobalt.plugin.packaging.assemble +import com.beust.kobalt.plugin.packaging.install +import com.beust.kobalt.plugin.publish.autoGitTag +import com.beust.kobalt.plugin.publish.bintray +import com.beust.kobalt.project +import org.apache.maven.model.Developer +import org.apache.maven.model.License +import org.apache.maven.model.Model +import org.apache.maven.model.Scm +import java.io.FileInputStream +import java.util.* + +val p = project { + name = "pinboard-poster" + group = "net.thauvin.erik" + artifactId = name + version = "0.9.0" + + val localProperties = Properties().apply { FileInputStream("local.properties").use { fis -> load(fis) } } + val apiToken = localProperties.getProperty("pinboard-api-token", "") + + pom = Model().apply { + description = project.description + url = "https://github.com/ethauvin/pinboard-poster" + licenses = listOf(License().apply { + name = "BSD 3-Clause" + url = "https://opensource.org/licenses/BSD-3-Clause" + }) + scm = Scm().apply { + url = "https://github.com/ethauvin/pinboard-poster" + connection = "https://github.com/ethauvin/pinboard-poster.git" + developerConnection = "git@github.com:ethauvin/pinboard-poster.git" + } + developers = listOf(Developer().apply { + id = "ethauvin" + name = "Erik C. Thauvin" + email = "erik@thauvin.net" + }) + } + + dependencies { + compile("org.jetbrains.kotlin:kotlin-stdlib:1.1.2-3") + compile("com.squareup.okhttp3:okhttp:3.8.0") + } + + dependenciesTest { + compile("org.testng:testng:6.11") + compile("org.jetbrains.kotlin:kotlin-test:1.1.2-3") + } + + assemble { + jar { } + mavenJars { } + } + + application { + mainClass = "net.thauvin.erik.pinboard.PinboardPosterKt" + args(apiToken) + } + + application { + taskName = "runJava" + mainClass = "net.thauvin.erik.pinboard.JavaExample" + args(apiToken) + } + + install { + target = "deploy" + include(from("kobaltBuild/libs"), to(target), glob("**/*")) + collect(compileDependencies).forEach { + copy(from(it.file.absolutePath), to(target)) + } + } + + autoGitTag { + enabled = true + //push = false + message = "Version $version" + } + + bintray { + publish = true + description = "Release version $version" + issueTrackerUrl = "https://github.com/ethauvin/pinboard-poster/issues" + vcsTag = version + sign = true + } +} diff --git a/kobalt/wrapper/kobalt-wrapper.jar b/kobalt/wrapper/kobalt-wrapper.jar new file mode 100644 index 0000000..6c6b435 Binary files /dev/null and b/kobalt/wrapper/kobalt-wrapper.jar differ diff --git a/kobalt/wrapper/kobalt-wrapper.properties b/kobalt/wrapper/kobalt-wrapper.properties new file mode 100644 index 0000000..cdcb2d1 --- /dev/null +++ b/kobalt/wrapper/kobalt-wrapper.properties @@ -0,0 +1 @@ +kobalt.version=1.0.86 \ No newline at end of file diff --git a/kobaltw b/kobaltw new file mode 100644 index 0000000..c5186d5 --- /dev/null +++ b/kobaltw @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +java -jar "`dirname "$0"`/kobalt/wrapper/kobalt-wrapper.jar" $* diff --git a/kobaltw.bat b/kobaltw.bat new file mode 100644 index 0000000..d578071 --- /dev/null +++ b/kobaltw.bat @@ -0,0 +1,4 @@ +@echo off +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +java -jar "%DIRNAME%/kobalt/wrapper/kobalt-wrapper.jar" %* diff --git a/lib/kotlin-reflect.jar b/lib/kotlin-reflect.jar new file mode 100644 index 0000000..5d6aea2 Binary files /dev/null and b/lib/kotlin-reflect.jar differ diff --git a/lib/kotlin-runtime-sources.jar b/lib/kotlin-runtime-sources.jar new file mode 100644 index 0000000..4113a1c Binary files /dev/null and b/lib/kotlin-runtime-sources.jar differ diff --git a/lib/kotlin-runtime.jar b/lib/kotlin-runtime.jar new file mode 100644 index 0000000..9c7cc8b Binary files /dev/null and b/lib/kotlin-runtime.jar differ diff --git a/pinboard-poster.iml b/pinboard-poster.iml new file mode 100644 index 0000000..557ef36 --- /dev/null +++ b/pinboard-poster.iml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/net/thauvin/erik/pinboard/JavaExample.java b/src/main/java/net/thauvin/erik/pinboard/JavaExample.java new file mode 100644 index 0000000..d4572dd --- /dev/null +++ b/src/main/java/net/thauvin/erik/pinboard/JavaExample.java @@ -0,0 +1,47 @@ +/* + * JavaExample.java + * + * Copyright (c) 2017, 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; + +public class JavaExample { + public static void main(String[] args) { + final String url = "http://www.example.com/pinboard"; + final PinboardPoster poster = new PinboardPoster(args[0]); + + if (poster.addPin(url, "Testing", "Extended test", "test koltin")) { + System.out.println("Added: " + url); + } + + if (poster.deletePin(url)) { + System.out.println("Deleted: " + url); + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt b/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt new file mode 100644 index 0000000..ffe4f0e --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt @@ -0,0 +1,203 @@ +/* + * PinboardPoster.kt + * + * Copyright (c) 2017, 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 okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import java.net.URL +import java.util.logging.ConsoleHandler +import java.util.logging.Level +import java.util.logging.Logger + +object Constants { + const val API_ENDPOINT = "https://api.pinboard.in/v1/" + const val AUTH_TOKEN = "auth_token" + const val DONE = "done" +} + +open class PinboardPoster(val apiToken: String) { + var apiEndPoint: String = Constants.API_ENDPOINT + + val logger: Logger by lazy { Logger.getLogger(PinboardPoster::class.java.simpleName) } + + private val client by lazy { OkHttpClient() } + + @JvmOverloads + fun addPin(url: String, + description: String, + extended: String = "", + tags: String = "", + dt: String = "", + replace: Boolean = true, + shared: Boolean = true, + toRead: Boolean = false): Boolean { + if (validate()) { + if (!validateUrl(url)) { + logger.log(Level.SEVERE, "Please specify a valid URL to pin.") + } else if (description.isBlank()) { + logger.log(Level.SEVERE, "Please specify a valid description.") + } else { + val apiUrl = HttpUrl.parse(cleanEndPoint("posts/add")) + if (apiUrl != null) { + val httpUrl = apiUrl.newBuilder().apply { + addQueryParameter("url", url) + addQueryParameter("description", description) + if (extended.isNotBlank()) { + addQueryParameter("extended", extended) + } + if (tags.isNotBlank()) { + addQueryParameter("tags", tags) + } + if (dt.isNotBlank()) { + addQueryParameter("dt", dt) + } + if (!replace) { + addQueryParameter("replace", "no") + } + if (!shared) { + addQueryParameter("shared", "no") + } + if (toRead) { + addQueryParameter("toread", "yes") + } + addQueryParameter(Constants.AUTH_TOKEN, apiToken) + }.build() + + val request = Request.Builder().url(httpUrl).build() + val result = client.newCall(request).execute() + + logger.log(Level.FINE, "HTTP Result: ${result.code()}") + + val response = result.body()?.string() + + if (response != null && response.contains(Constants.DONE)) { + logger.log(Level.FINE, "HTTP Response:\n$response") + return true + } + } else { + logger.log(Level.SEVERE, "Invalid API end point: $apiEndPoint") + } + } + } + + return false + } + + fun deletePin(url: String): Boolean { + if (validate()) { + if (!validateUrl(url)) { + logger.log(Level.SEVERE, "Please specify a valid URL to delete.") + } else { + val apiUrl = HttpUrl.parse(cleanEndPoint("posts/delete")) + if (apiUrl != null) { + val httpUrl = apiUrl.newBuilder().apply { + addQueryParameter("url", url) + addQueryParameter(Constants.AUTH_TOKEN, apiToken) + }.build() + + val request = Request.Builder().url(httpUrl).build() + val result = client.newCall(request).execute() + + logger.log(Level.FINE, "HTTP Result: ${result.code()}") + + val response = result.body()?.string() + + if (response != null && response.contains(Constants.DONE)) { + logger.log(Level.FINE, "HTTP Response:\n$response") + return true + } + } else { + logger.log(Level.SEVERE, "Invalid API end point: $apiEndPoint") + } + } + } + + return false + } + + private fun validateUrl(url: String): Boolean { + if (url.isBlank()) { + return false + } + + try { + URL(url) + } catch(e: Exception) { + logger.log(Level.FINE, "Invalid URL: $url", e) + return false + } + + return true + } + + private fun validate(): Boolean { + if (apiToken.isBlank() && !apiToken.contains(':')) { + logger.log(Level.SEVERE, "Please specify a valid API token. (eg. user:TOKEN)") + return false + } else if (!validateUrl(apiEndPoint)) { + logger.log(Level.SEVERE, "Please specify a valid API end point. (eg. ${Constants.API_ENDPOINT})") + return false + } + return true + } + + private fun cleanEndPoint(method: String): String { + if (apiEndPoint.endsWith('/')) { + return "$apiEndPoint$method" + } else { + return "$apiEndPoint/$method" + } + } +} + +fun main(args: Array) { + if (args.size == 1) { + val url = "http://www.example.com/pinboard" + val poster = PinboardPoster(args[0]) + + with(poster.logger) { + addHandler(ConsoleHandler().apply { level = Level.FINE }) + level = Level.FINE + } + + if (poster.addPin(url, "Testing", "Extended test", "test koltin")) { + println("Added: $url") + } + + if (poster.deletePin(url)) { + println("Deleted: $url") + } + } else { + println("Please specify a valid API token. (eg. user:TOKEN)") + } +} \ No newline at end of file