diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8689e94..c781fdc 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,37 +1,62 @@
-version: 2
-jobs:
- build:
- docker:
- - image: circleci/openjdk:8-jdk
+version: 2.1
- working_directory: ~/repo
+orbs:
+ sdkman: joshdholtz/sdkman@0.2.0
- environment:
- JVM_OPTS: -Xmx3200m
- TERM: dumb
+defaults: &defaults
+ working_directory: ~/repo
+ environment:
+ JVM_OPTS: -Xmx3200m
+ TERM: dumb
+ CI_NAME: "CircleCI"
+commands:
+ build_and_test:
+ parameters:
+ reports-dir:
+ type: string
+ default: "build/reports/test_results"
steps:
- checkout
- - restore_cache:
- keys:
- - kobalt-dependencies-{{ checksum "kobalt/src/Build.kt" }}
- # fallback to using the latest cache if no exact match is found
- - kobalt-dependencies-
-
+ - sdkman/setup-sdkman
+ - sdkman/sdkman-install:
+ candidate: kotlin
+ version: 2.1.10
- run:
- name: Check Versions
- command: ./kobaltw checkVersions
-
- - save_cache:
- paths: ~/.kobalt
- key: kobalt-dependencies-{{ checksum "kobalt/src/Build.kt" }}
-
+ name: Download dependencies
+ command: ./bld download
- run:
- name: Assemble & Test
- command: ./kobaltw assemble test
-
- - store_artifacts:
- path: kobaltBuild/test-output/
- destination: test-output
+ name: Compile source
+ command: ./bld compile
+ - run:
+ name: Run tests
+ command: ./bld jacoco -reports-dir=<< parameters.reports-dir >>
- store_test_results:
- path: kobaltBuild/test-output/
+ path: << parameters.reports-dir >>
+ - store_artifacts:
+ path: build/reports/jacoco/test/html
+
+jobs:
+ bld_jdk17:
+ <<: *defaults
+
+ docker:
+ - image: cimg/openjdk:17.0
+
+ steps:
+ - build_and_test
+
+ bld_jdk21:
+ <<: *defaults
+
+ docker:
+ - image: cimg/openjdk:21.0
+
+ steps:
+ - build_and_test
+
+workflows:
+ bld:
+ jobs:
+ - bld_jdk17
+ - bld_jdk21
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a6971e1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,2 @@
+[*]
+insert_final_newline=true
diff --git a/.github/workflows/bld.yml b/.github/workflows/bld.yml
new file mode 100644
index 0000000..068e6c9
--- /dev/null
+++ b/.github/workflows/bld.yml
@@ -0,0 +1,74 @@
+name: bld-ci
+
+on: [ push, pull_request, workflow_dispatch ]
+
+env:
+ COVERAGE_JDK: "21"
+ COVERAGE_KOTLIN: "2.1.20"
+ PINBOARD_API_TOKEN: ${{ secrets.PINBOARD_API_TOKEN }}
+
+jobs:
+ build-bld-project:
+ strategy:
+ matrix:
+ java-version: [ 17, 21, 24 ]
+ kotlin-version: [ 1.9.25, 2.0.21, 2.1.20 ]
+ os: [ ubuntu-latest, windows-latest, macos-latest ]
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout source repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up JDK ${{ matrix.java-version }} with Kotlin ${{ matrix.kotlin-version }}
+ uses: actions/setup-java@v4
+ with:
+ distribution: "zulu"
+ java-version: ${{ matrix.java-version }}
+
+ - name: Download dependencies [bld example]
+ working-directory: examples/bld
+ run: ./bld download
+
+ - name: Compile and run [bld examples]
+ working-directory: examples/bld
+ run: |
+ ./bld compile run
+ ./bld run-java
+
+ - name: Run example [gradle java example]
+ working-directory: examples/gradle/java
+ if: matrix.java-version != '24'
+ run: |
+ ./gradlew run
+
+ - name: Run example [gradle kotlin example]
+ working-directory: examples/gradle/kotlin
+ if: matrix.java-version != '24'
+ run: |
+ ./gradlew run
+
+ - name: Download dependencies
+ run: ./bld download
+
+ - name: Compile source
+ run: ./bld compile
+
+ - name: Run tests
+ run: ./bld jacoco
+
+ - name: Remove pom.xml
+ if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN
+ && matrix.os == 'ubuntu-latest'
+ run: rm -rf pom.xml
+
+ - name: SonarCloud Scan
+ uses: sonarsource/sonarcloud-github-action@master
+ if: success() && matrix.java-version == env.COVERAGE_JDK && matrix.kotlin-version == env.COVERAGE_KOTLIN
+ && matrix.os == 'ubuntu-latest'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/.github_changelog_generator b/.github_changelog_generator
new file mode 100644
index 0000000..19e45d2
--- /dev/null
+++ b/.github_changelog_generator
@@ -0,0 +1 @@
+future-release=1.1.0
diff --git a/.gitignore b/.gitignore
index 7ad0aa5..3402a75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,30 +1,58 @@
-**/.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
-kobaltw*-test
-Thumbs.db
\ No newline at end of file
+.DS_Store
+build
+lib/bld/**
+!lib/bld/bld-wrapper.jar
+!lib/bld/bld-wrapper.properties
+lib/compile/
+lib/runtime/
+lib/standalone/
+lib/test/
+
+# IDEA ignores
+
+# User-specific
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+
+local.properties
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..10b9b0f
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,24 @@
+image: fedora:latest
+
+variables:
+ CI_NAME: "GitLab CI"
+
+stages:
+ - test
+
+before_script:
+ - dnf -qy update && dnf -y install zip
+ - curl -s "https://get.sdkman.io" | bash
+ - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config
+ - echo sdkman_auto_selfupdate=true >> $HOME/.sdkman/etc/config
+ - source "$HOME/.sdkman/bin/sdkman-init.sh"
+ - sdk install java
+ - sdk install kotlin
+ - source "$HOME/.sdkman/bin/sdkman-init.sh"
+
+test:
+ stage: test
+ script:
+ - ./bld download
+ - ./bld compile
+ - ./bld test
diff --git a/.idea/app.iml b/.idea/app.iml
new file mode 100644
index 0000000..2c1fe21
--- /dev/null
+++ b/.idea/app.iml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/bld.iml b/.idea/bld.iml
new file mode 100644
index 0000000..e63e11e
--- /dev/null
+++ b/.idea/bld.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/bld.xml
similarity index 54%
rename from .idea/encodings.xml
rename to .idea/bld.xml
index 97626ba..6600cee 100644
--- a/.idea/encodings.xml
+++ b/.idea/bld.xml
@@ -1,6 +1,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/.idea/copyright/Erik_s_Copyright_Notice.xml b/.idea/copyright/Erik_s_Copyright_Notice.xml
index 08660a1..ab1f825 100644
--- a/.idea/copyright/Erik_s_Copyright_Notice.xml
+++ b/.idea/copyright/Erik_s_Copyright_Notice.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
index a25e371..1419e40 100644
--- a/.idea/copyright/profiles_settings.xml
+++ b/.idea/copyright/profiles_settings.xml
@@ -1,15 +1,3 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/.idea/icon.svg b/.idea/icon.svg
new file mode 100644
index 0000000..94a6708
--- /dev/null
+++ b/.idea/icon.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 8ff795e..94f28ea 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,53 +1,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/.idea/kobalt.xml b/.idea/kobalt.xml
deleted file mode 100644
index 3e8b0d0..0000000
--- a/.idea/kobalt.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/libraries/bld.xml b/.idea/libraries/bld.xml
new file mode 100644
index 0000000..153a060
--- /dev/null
+++ b/.idea/libraries/bld.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/libraries/compile.xml b/.idea/libraries/compile.xml
new file mode 100644
index 0000000..99cc0c0
--- /dev/null
+++ b/.idea/libraries/compile.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/runtime.xml b/.idea/libraries/runtime.xml
new file mode 100644
index 0000000..d4069f2
--- /dev/null
+++ b/.idea/libraries/runtime.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/test.xml b/.idea/libraries/test.xml
new file mode 100644
index 0000000..57ed5ef
--- /dev/null
+++ b/.idea/libraries/test.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 115f0a1..2adb169 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,27 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index d5b3d2b..55adcb9 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,8 +2,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run Tests.xml b/.idea/runConfigurations/Run Tests.xml
new file mode 100644
index 0000000..2d4326e
--- /dev/null
+++ b/.idea/runConfigurations/Run Tests.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/scopes/Source.xml b/.idea/scopes/Source.xml
deleted file mode 100644
index 942e059..0000000
--- a/.idea/scopes/Source.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
deleted file mode 100644
index e96534f..0000000
--- a/.idea/uiDesigner.xml
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..35eb1dd 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 4a6654e..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-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 test
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..f25500c
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "java",
+ "name": "Run Tests",
+ "request": "launch",
+ "mainClass": "net.thauvin.erik.pinboard.PinboardPosterTest"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..ba429d0
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,15 @@
+{
+ "java.project.sourcePaths": [
+ "src/main/java",
+ "src/main/resources",
+ "src/test/java",
+ "src/test/resources",
+ "src/bld/java",
+ "src/bld/resources"
+ ],
+ "java.configuration.updateBuildConfiguration": "automatic",
+ "java.project.referencedLibraries": [
+ "${HOME}/.bld/dist/bld-2.2.1.jar",
+ "lib/**/*.jar"
+ ]
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..9c318e1
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,61 @@
+# Changelog
+
+## [1.1.0](https://github.com/ethauvin/pinboard-poster/tree/1.1.0) (2023-09-28)
+
+[Full Changelog](https://github.com/ethauvin/pinboard-poster/compare/1.0.3...1.1.0)
+
+**Implemented enhancements:**
+
+- Add pin config builder [\#10](https://github.com/ethauvin/pinboard-poster/issues/10)
+
+**Fixed bugs:**
+
+- Fixed potential resource leak [\#11](https://github.com/ethauvin/pinboard-poster/issues/11)
+
+## [1.0.3](https://github.com/ethauvin/pinboard-poster/tree/1.0.3) (2021-03-22)
+
+[Full Changelog](https://github.com/ethauvin/pinboard-poster/compare/1.0.2...1.0.3)
+
+**Fixed bugs:**
+
+- 1.0.2 only compiled for Java 15 [\#4](https://github.com/ethauvin/pinboard-poster/issues/4)
+
+## [1.0.2](https://github.com/ethauvin/pinboard-poster/tree/1.0.2) (2021-03-22)
+
+[Full Changelog](https://github.com/ethauvin/pinboard-poster/compare/1.0.1...1.0.2)
+
+**Implemented enhancements:**
+
+- Use HttpLoggingInterceptor instead of manually logging. [\#3](https://github.com/ethauvin/pinboard-poster/issues/3)
+
+**Fixed bugs:**
+
+- executeMethod should trap IO execeptions [\#2](https://github.com/ethauvin/pinboard-poster/issues/2)
+
+## [1.0.1](https://github.com/ethauvin/pinboard-poster/tree/1.0.1) (2019-05-27)
+
+[Full Changelog](https://github.com/ethauvin/pinboard-poster/compare/1.0.0...1.0.1)
+
+**Implemented enhancements:**
+
+- Implement better error reporting. [\#1](https://github.com/ethauvin/pinboard-poster/issues/1)
+
+## [1.0.0](https://github.com/ethauvin/pinboard-poster/tree/1.0.0) (2018-06-26)
+
+[Full Changelog](https://github.com/ethauvin/pinboard-poster/compare/0.9.3...1.0.0)
+
+## [0.9.3](https://github.com/ethauvin/pinboard-poster/tree/0.9.3) (2017-11-09)
+
+[Full Changelog](https://github.com/ethauvin/pinboard-poster/compare/0.9.2...0.9.3)
+
+## [0.9.2](https://github.com/ethauvin/pinboard-poster/tree/0.9.2) (2017-11-08)
+
+[Full Changelog](https://github.com/ethauvin/pinboard-poster/compare/0.9.1...0.9.2)
+
+## [0.9.1](https://github.com/ethauvin/pinboard-poster/tree/0.9.1) (2017-05-18)
+
+[Full Changelog](https://github.com/ethauvin/pinboard-poster/compare/2ee3568e40114e19b0956ea7d12c071d5c49b0d5...0.9.1)
+
+
+
+\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
diff --git a/LICENCE.txt b/LICENSE.txt
similarity index 95%
rename from LICENCE.txt
rename to LICENSE.txt
index 861c1ef..9926d00 100644
--- a/LICENCE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2017, Erik C. Thauvin (erik@thauvin.net)
+Copyright (c) 2017-2025, Erik C. Thauvin (erik@thauvin.net)
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -24,4 +24,4 @@ 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.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 9a9a920..cdf7df5 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,18 @@
-# [Pinboard](https://pinboard.in) Poster for Kotlin/Java
+# [Pinboard](https://pinboard.in) Poster for Kotlin, Java and Android
-[](http://opensource.org/licenses/BSD-3-Clause) [](https://github.com/ethauvin/pinboard-poster/releases/latest) [](https://bintray.com/ethauvin/maven/pinboard-poster/_latestVersion)
-[](https://www.versioneye.com/user/projects/591c0293b81f680038a784b3) [](https://travis-ci.org/ethauvin/pinboard-poster) [](https://circleci.com/gh/ethauvin/pinboard-poster/tree/master)
+[](https://opensource.org/licenses/BSD-3-Clause)
+[](https://kotlinlang.org/)
+[](https://rife2.com/bld)
+[](https://github.com/ethauvin/pinboard-poster/releases/latest)
+[](https://central.sonatype.com/artifact/net.thauvin.erik/pinboard-poster)
+[](https://oss.sonatype.org/content/repositories/snapshots/net/thauvin/erik/pinboard-poster/)
-A small Kotlin/Java library for posting to [Pinboard](https://pinboard.in).
+
+[](https://sonarcloud.io/dashboard?id=ethauvin_pinboard-poster)
+[](https://github.com/ethauvin/pinboard-poster/actions/workflows/bld.yml)
+[](https://circleci.com/gh/ethauvin/pinboard-poster/tree/master)
+
+A small library for posting to [Pinboard](https://pinboard.in).
## Examples
@@ -13,71 +22,80 @@ A small Kotlin/Java library for posting to [Pinboard](https://pinboard.in).
val poster = PinboardPoster("user:TOKEN")
-poster.addPin("http://www.example.com/foo", "This is a test")
-poster.deletePin("http:///www.example.com/bar")
+poster.addPin("https://example.com/foo", "This is a test")
+poster.addPin("https://example.com", "This is a test", tags = arrayOf("foo", "bar"))
+poster.deletePin("https://example.com/bar")
```
-[View Example](https://github.com/ethauvin/pinboard-poster/blob/master/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt#L219)
+
+[View Examples](https://github.com/ethauvin/pinboard-poster/blob/master/examples)
### 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");
+poster.addPin("https://example.com/foo", "This is a test");
+poster.addPin(new PinConfig.Builder("https://example.com", "This is a test")
+ .tags("foo", "bar")
+ .build());
+poster.deletePin("https://example.com/bar");
```
-[View Example](https://github.com/ethauvin/pinboard-poster/blob/master/src/main/java/net/thauvin/erik/pinboard/JavaExample.java)
+
+[View Examples](https://github.com/ethauvin/pinboard-poster/blob/master/examples)
Your API authentication token is available on the [Pinboard settings page](https://pinboard.in/settings/password).
-## Usage with Maven, Gradle and Kobalt
+## bld
-### Maven
+To use with [bld](https://rife2.com/bld), include the following dependency in your [build](https://github.com/ethauvin/pinboard-poster/blob/master/examples/bld/src/bld/java/net/thauvin/erik/pinboard/samples/ExampleBuild.java) file:
-To install and run from Maven, configure an artifact as follows:
+```java
+repositories = List.of(MAVEN_CENTRAL, SONATYPE_SNAPSHOTS_LEGACY);
-```xml
-
- net.thauvin.erik
- pinboard-poster
- 0.9.3
-
+scope(compile)
+ .include(dependency("net.thauvin.erik:pinboard-poster:1.2.0"));
```
+Be sure to use the [bld Kotlin extension](https://github.com/rife2/bld-kotlin) in your project.
-### Gradle
+[View Example](https://github.com/ethauvin/pinboard-poster/blob/master/examples/bld/)
-To install and run from Gradle, add the following to the build.gradle file:
+## Gradle, Maven, etc.
+
+To install and run from Gradle, add the following to the `build.gradle` file:
```gradle
+repositories {
+ mavenCentral()
+}
+
dependencies {
- compile 'net.thauvin.erik:pinboard-poster:0.9.3'
+ compile 'net.thauvin.erik:pinboard-poster:1.2.0'
}
```
-### Kobalt
+[View Examples](https://github.com/ethauvin/pinboard-poster/blob/master/examples/gradle/)
-To install and run from Kobalt, add the following to the Build.kt file:
-
-```gradle
-dependencies {
- compile("net.thauvin.erik:pinboard-poster:0.9.3")
-}
-```
+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 = "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)
+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.
@@ -89,7 +107,7 @@ It returns `true` if the bookmark was added successfully, `false` otherwise.
The `deletePin` function support all of the [Pinboard API parameters](https://pinboard.in/api/#posts_delete):
```kotlin
-poster.deletePin(url = "http://www.example.com/")
+poster.deletePin(url = "https://www.example.com/")
```
It returns `true` if the bookmark was deleted successfully, `false` otherwise.
@@ -99,19 +117,24 @@ 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
+ useParentHandlers = false
}
```
+
#### Java
+
```java
final ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.FINE);
final Logger logger = poster.getLogger();
logger.addHandler(consoleHandler);
logger.setLevel(Level.FINE);
+logger.setUseParentHandlers(false);
```
or using a logging properties file.
@@ -173,4 +196,22 @@ The API end point is automatically configured to `https://api.pinboard.in/v1/`.
```kotlin
poster.apiEndPoint = "https://www.example.com/v1"
-```
\ No newline at end of file
+```
+
+## Contributing
+
+If you want to contribute to this project, all you have to do is clone the GitHub
+repository:
+
+```console
+git clone git@github.com:ethauvin/pinboard-poster.git
+```
+
+Then use [bld](https://rife2.com/bld) to build:
+
+```console
+cd pinboard-poster
+./bld compile
+```
+
+The project has an [IntelliJ IDEA](https://www.jetbrains.com/idea/) project structure. You can just open it after all the dependencies were downloaded and peruse the code.
diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml
new file mode 100644
index 0000000..ace99d2
--- /dev/null
+++ b/bitbucket-pipelines.yml
@@ -0,0 +1,20 @@
+image: ubuntu:latest
+
+pipelines:
+ default:
+ - step:
+ name: Test with bld
+ script:
+ # Install latest Java & Kotlin via SDKMAN!
+ - apt-get update -qq && apt-get install -y curl zip
+ - curl -s "https://get.sdkman.io" | bash
+ - echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config
+ - echo sdkman_auto_selfupdate=true >> $HOME/.sdkman/etc/config
+ - source "$HOME/.sdkman/bin/sdkman-init.sh"
+ - sdk install java
+ - sdk install kotlin
+ - source "$HOME/.sdkman/bin/sdkman-init.sh"
+ # Download, compile and test with bld
+ - ./bld download
+ - ./bld compile
+ - ./bld test
diff --git a/bld b/bld
new file mode 100755
index 0000000..3c1df97
--- /dev/null
+++ b/bld
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+java -jar "$(dirname "$0")/lib/bld/bld-wrapper.jar" "$0" --build net.thauvin.erik.pinboard.PinboardPosterBuild "$@"
diff --git a/bld.bat b/bld.bat
new file mode 100644
index 0000000..1262822
--- /dev/null
+++ b/bld.bat
@@ -0,0 +1,4 @@
+@echo off
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+java -jar "%DIRNAME%/lib/bld/bld-wrapper.jar" "%0" --build net.thauvin.erik.pinboard.PinboardPosterBuild %*
diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml
new file mode 100644
index 0000000..3f97ec1
--- /dev/null
+++ b/config/detekt/baseline.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ LongParameterList:PinConfig.kt$PinConfig$( var url: String, var description: String, var extended: String = "", var tags: Array<out String> = emptyArray(), var dt: ZonedDateTime = ZonedDateTime.now(), var replace: Boolean = true, var shared: Boolean = true, var toRead: Boolean = false )
+ 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
+
+
diff --git a/examples/bld/.gitignore b/examples/bld/.gitignore
new file mode 100644
index 0000000..a2805aa
--- /dev/null
+++ b/examples/bld/.gitignore
@@ -0,0 +1,55 @@
+.gradle
+.DS_Store
+build
+lib/bld/**
+!lib/bld/bld-wrapper.jar
+!lib/bld/bld-wrapper.properties
+lib/compile/
+lib/runtime/
+lib/standalone/
+lib/test/
+
+# IDEA ignores
+
+# User-specific
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Editor-based Rest Client
+.idea/httpRequests
\ No newline at end of file
diff --git a/examples/bld/.idea/.gitignore b/examples/bld/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/examples/bld/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/examples/bld/.idea/.name b/examples/bld/.idea/.name
new file mode 100644
index 0000000..6782949
--- /dev/null
+++ b/examples/bld/.idea/.name
@@ -0,0 +1 @@
+pinboard-poster-examples-bld
\ No newline at end of file
diff --git a/examples/bld/.idea/app.iml b/examples/bld/.idea/app.iml
new file mode 100644
index 0000000..2d05353
--- /dev/null
+++ b/examples/bld/.idea/app.iml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/bld.iml b/examples/bld/.idea/bld.iml
new file mode 100644
index 0000000..e63e11e
--- /dev/null
+++ b/examples/bld/.idea/bld.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/bld.xml b/examples/bld/.idea/bld.xml
new file mode 100644
index 0000000..6600cee
--- /dev/null
+++ b/examples/bld/.idea/bld.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/inspectionProfiles/Project_Default.xml b/examples/bld/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..1e01b48
--- /dev/null
+++ b/examples/bld/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/libraries/bld.xml b/examples/bld/.idea/libraries/bld.xml
new file mode 100644
index 0000000..153a060
--- /dev/null
+++ b/examples/bld/.idea/libraries/bld.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/bld/.idea/libraries/compile.xml b/examples/bld/.idea/libraries/compile.xml
new file mode 100644
index 0000000..99cc0c0
--- /dev/null
+++ b/examples/bld/.idea/libraries/compile.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/libraries/runtime.xml b/examples/bld/.idea/libraries/runtime.xml
new file mode 100644
index 0000000..d4069f2
--- /dev/null
+++ b/examples/bld/.idea/libraries/runtime.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/libraries/test.xml b/examples/bld/.idea/libraries/test.xml
new file mode 100644
index 0000000..57ed5ef
--- /dev/null
+++ b/examples/bld/.idea/libraries/test.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/misc.xml b/examples/bld/.idea/misc.xml
new file mode 100644
index 0000000..f522a9e
--- /dev/null
+++ b/examples/bld/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/bld/.idea/modules.xml b/examples/bld/.idea/modules.xml
new file mode 100644
index 0000000..55adcb9
--- /dev/null
+++ b/examples/bld/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/runConfigurations/Run Tests.xml b/examples/bld/.idea/runConfigurations/Run Tests.xml
new file mode 100644
index 0000000..e5f873f
--- /dev/null
+++ b/examples/bld/.idea/runConfigurations/Run Tests.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.idea/vcs.xml b/examples/bld/.idea/vcs.xml
new file mode 100644
index 0000000..b2bdec2
--- /dev/null
+++ b/examples/bld/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/bld/.vscode/launch.json b/examples/bld/.vscode/launch.json
new file mode 100644
index 0000000..b183bbe
--- /dev/null
+++ b/examples/bld/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "java",
+ "name": "Run Tests",
+ "request": "launch",
+ "mainClass": "net.thauvin.erik.pinboard.samples.JavaExampleTest"
+ }
+ ]
+}
diff --git a/examples/bld/.vscode/settings.json b/examples/bld/.vscode/settings.json
new file mode 100644
index 0000000..ba429d0
--- /dev/null
+++ b/examples/bld/.vscode/settings.json
@@ -0,0 +1,15 @@
+{
+ "java.project.sourcePaths": [
+ "src/main/java",
+ "src/main/resources",
+ "src/test/java",
+ "src/test/resources",
+ "src/bld/java",
+ "src/bld/resources"
+ ],
+ "java.configuration.updateBuildConfiguration": "automatic",
+ "java.project.referencedLibraries": [
+ "${HOME}/.bld/dist/bld-2.2.1.jar",
+ "lib/**/*.jar"
+ ]
+}
diff --git a/examples/bld/README.md b/examples/bld/README.md
new file mode 100644
index 0000000..ad5401c
--- /dev/null
+++ b/examples/bld/README.md
@@ -0,0 +1,18 @@
+## Kotlin Example
+To compile & run the Kotlin example:
+
+```console
+./bld compile
+
+./bld run
+```
+
+## Java Example
+
+To compile & run the Java example:
+
+```console
+./bld compile
+
+./bld run-java
+```
diff --git a/examples/bld/bld b/examples/bld/bld
new file mode 100755
index 0000000..1a39c97
--- /dev/null
+++ b/examples/bld/bld
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+java -jar "$(dirname "$0")/lib/bld/bld-wrapper.jar" "$0" --build net.thauvin.erik.pinboard.samples.ExampleBuild "$@"
diff --git a/examples/bld/bld.bat b/examples/bld/bld.bat
new file mode 100644
index 0000000..6357553
--- /dev/null
+++ b/examples/bld/bld.bat
@@ -0,0 +1,4 @@
+@echo off
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+java -jar "%DIRNAME%/lib/bld/bld-wrapper.jar" "%0" --build net.thauvin.erik.pinboard.samples.ExampleBuild %*
diff --git a/examples/bld/lib/bld/bld-wrapper.jar b/examples/bld/lib/bld/bld-wrapper.jar
new file mode 100644
index 0000000..968a8ab
Binary files /dev/null and b/examples/bld/lib/bld/bld-wrapper.jar differ
diff --git a/examples/bld/lib/bld/bld-wrapper.properties b/examples/bld/lib/bld/bld-wrapper.properties
new file mode 100644
index 0000000..1f1009d
--- /dev/null
+++ b/examples/bld/lib/bld/bld-wrapper.properties
@@ -0,0 +1,7 @@
+bld.downloadExtensionJavadoc=false
+bld.downloadExtensionSources=true
+bld.downloadLocation=
+bld.extension-kotlin=com.uwyn.rife2:bld-kotlin:1.1.0-SNAPSHOT
+bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES
+bld.sourceDirectories=
+bld.version=2.2.1
diff --git a/examples/bld/src/bld/java/net/thauvin/erik/pinboard/samples/ExampleBuild.java b/examples/bld/src/bld/java/net/thauvin/erik/pinboard/samples/ExampleBuild.java
new file mode 100644
index 0000000..6fc8142
--- /dev/null
+++ b/examples/bld/src/bld/java/net/thauvin/erik/pinboard/samples/ExampleBuild.java
@@ -0,0 +1,54 @@
+package net.thauvin.erik.pinboard.samples;
+
+import rife.bld.BaseProject;
+import rife.bld.BuildCommand;
+import rife.bld.extension.CompileKotlinOperation;
+import rife.bld.extension.kotlin.CompileOptions;
+import rife.bld.operations.RunOperation;
+
+import java.util.List;
+
+import static rife.bld.dependencies.Repository.*;
+import static rife.bld.dependencies.Scope.compile;
+
+public class ExampleBuild extends BaseProject {
+ public ExampleBuild() {
+ pkg = "net.thauvin.erik.pinboard.samples";
+ name = "Example";
+ version = version(0, 1, 0);
+
+ mainClass = pkg + ".KotlinExampleKt";
+
+ javaRelease = 11;
+ downloadSources = true;
+ autoDownloadPurge = true;
+
+ repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL, SONATYPE_SNAPSHOTS_LEGACY);
+
+ scope(compile)
+ .include(dependency("net.thauvin.erik", "pinboard-poster", version(1, 2, 1, "SNAPSHOT")));
+ }
+
+ public static void main(String[] args) {
+ new ExampleBuild().start(args);
+ }
+
+ @Override
+ public void compile() throws Exception {
+ new CompileKotlinOperation()
+ .fromProject(this)
+ .compileOptions(new CompileOptions().verbose(true))
+ .execute();
+
+ // Also compile the Java source code
+ super.compile();
+ }
+
+ @BuildCommand(value = "run-java", summary = "Runs the Java example")
+ public void runJava() throws Exception {
+ new RunOperation()
+ .fromProject(this)
+ .mainClass(pkg + ".JavaExample")
+ .execute();
+ }
+}
diff --git a/examples/bld/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java b/examples/bld/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java
new file mode 100644
index 0000000..1ef224b
--- /dev/null
+++ b/examples/bld/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java
@@ -0,0 +1,47 @@
+package net.thauvin.erik.pinboard.samples;
+
+import net.thauvin.erik.pinboard.PinConfig;
+import net.thauvin.erik.pinboard.PinboardPoster;
+
+import java.nio.file.Paths;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class JavaExample {
+ public static void main(String[] args) {
+ final String url = "https://example.com/pinboard";
+ final PinboardPoster poster;
+
+ if (args.length == 1) {
+ // API Token is an argument
+ poster = new PinboardPoster(args[0]);
+ } else {
+ // API Token is in local.properties or PINBOARD_API_TOKEN environment variable
+ poster = new PinboardPoster(Paths.get("local.properties"));
+ }
+
+ // Set logging levels
+ final ConsoleHandler consoleHandler = new ConsoleHandler();
+ consoleHandler.setLevel(Level.FINE);
+ final Logger logger = poster.getLogger();
+ logger.addHandler(consoleHandler);
+ logger.setLevel(Level.FINE);
+ logger.setUseParentHandlers(false);
+
+ if (poster.validate()) {
+ // Add Pin
+ if (poster.addPin(new PinConfig.Builder(url, "Testing")
+ .extended("Extra")
+ .tags("test", "java")
+ .build())) {
+ System.out.println("Added: " + url);
+ }
+
+ // Delete Pin
+ if (poster.deletePin(url)) {
+ System.out.println("Deleted: " + url);
+ }
+ }
+ }
+}
diff --git a/examples/bld/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt b/examples/bld/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt
new file mode 100644
index 0000000..1b48e82
--- /dev/null
+++ b/examples/bld/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt
@@ -0,0 +1,37 @@
+package net.thauvin.erik.pinboard.samples
+
+import net.thauvin.erik.pinboard.PinboardPoster
+import java.nio.file.Paths
+import java.util.logging.ConsoleHandler
+import java.util.logging.Level
+
+fun main(args: Array) {
+ val url = "https://example.com/pinboard"
+
+ val poster = if (args.size == 1) {
+ // API Token is an argument
+ PinboardPoster(args[0])
+ } else {
+ // API Token is in local.properties or PINBOARD_API_TOKEN environment variable
+ PinboardPoster(Paths.get("local.properties"))
+ }
+
+ // Set logging levels
+ with(poster.logger) {
+ addHandler(ConsoleHandler().apply { level = Level.FINE })
+ level = Level.FINE
+ useParentHandlers = false
+ }
+
+ if (poster.validate()) {
+ // Add Pin
+ if (poster.addPin(url, "Testing", "Extended test", tags = arrayOf("test", "kotlin"))) {
+ println("Added: $url")
+ }
+
+ // Delete Pin
+ if (poster.deletePin(url)) {
+ println("Deleted: $url")
+ }
+ }
+}
diff --git a/examples/gradle/java/.editorconfig b/examples/gradle/java/.editorconfig
new file mode 100644
index 0000000..a6971e1
--- /dev/null
+++ b/examples/gradle/java/.editorconfig
@@ -0,0 +1,2 @@
+[*]
+insert_final_newline=true
diff --git a/examples/gradle/java/.gitignore b/examples/gradle/java/.gitignore
new file mode 100644
index 0000000..6322c7f
--- /dev/null
+++ b/examples/gradle/java/.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/examples/gradle/java/.idea/.gitignore b/examples/gradle/java/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/examples/gradle/java/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/examples/gradle/java/.idea/.name b/examples/gradle/java/.idea/.name
new file mode 100644
index 0000000..4f5dba1
--- /dev/null
+++ b/examples/gradle/java/.idea/.name
@@ -0,0 +1 @@
+pinboard-poster-examples-gradle-java
\ No newline at end of file
diff --git a/examples/gradle/java/.idea/compiler.xml b/examples/gradle/java/.idea/compiler.xml
new file mode 100644
index 0000000..fb7f4a8
--- /dev/null
+++ b/examples/gradle/java/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/java/.idea/inspectionProfiles/Project_Default.xml b/examples/gradle/java/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..1e01b48
--- /dev/null
+++ b/examples/gradle/java/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/java/.idea/jarRepositories.xml b/examples/gradle/java/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a529ef2
--- /dev/null
+++ b/examples/gradle/java/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/java/.idea/misc.xml b/examples/gradle/java/.idea/misc.xml
new file mode 100644
index 0000000..7adacb2
--- /dev/null
+++ b/examples/gradle/java/.idea/misc.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/examples/gradle/java/.idea/vcs.xml b/examples/gradle/java/.idea/vcs.xml
new file mode 100644
index 0000000..c2365ab
--- /dev/null
+++ b/examples/gradle/java/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/java/README.md b/examples/gradle/java/README.md
new file mode 100644
index 0000000..caf0563
--- /dev/null
+++ b/examples/gradle/java/README.md
@@ -0,0 +1,5 @@
+## Run the Example
+
+```console
+./gradlew run
+```
diff --git a/examples/gradle/java/build.gradle b/examples/gradle/java/build.gradle
new file mode 100644
index 0000000..e0eceee
--- /dev/null
+++ b/examples/gradle/java/build.gradle
@@ -0,0 +1,26 @@
+plugins {
+ id 'com.github.ben-manes.versions' version '0.51.0'
+ id 'java'
+ id 'application'
+}
+
+defaultTasks 'run'
+
+application {
+ mainClass = 'net.thauvin.erik.pinboard.samples.JavaExample'
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+}
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+ maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
+}
+
+dependencies {
+ implementation 'net.thauvin.erik:pinboard-poster:1.2.1-SNAPSHOT'
+}
diff --git a/examples/gradle/java/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/java/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..9bbc975
Binary files /dev/null and b/examples/gradle/java/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/gradle/java/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/java/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f853b
--- /dev/null
+++ b/examples/gradle/java/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/examples/gradle/java/gradlew b/examples/gradle/java/gradlew
new file mode 100755
index 0000000..faf9300
--- /dev/null
+++ b/examples/gradle/java/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/examples/gradle/java/gradlew.bat b/examples/gradle/java/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/examples/gradle/java/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/gradle/java/settings.gradle b/examples/gradle/java/settings.gradle
new file mode 100644
index 0000000..87b714a
--- /dev/null
+++ b/examples/gradle/java/settings.gradle
@@ -0,0 +1,10 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * The settings file is used to specify which projects to include in your build.
+ *
+ * Detailed information about configuring a multi-project build in Gradle can be found
+ * in the user guide at https://docs.gradle.org/4.8/userguide/multi_project_builds.html
+ */
+
+rootProject.name = 'pinboard-poster-examples-gradle-java'
diff --git a/examples/gradle/java/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java b/examples/gradle/java/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java
new file mode 100644
index 0000000..1ef224b
--- /dev/null
+++ b/examples/gradle/java/src/main/java/net/thauvin/erik/pinboard/samples/JavaExample.java
@@ -0,0 +1,47 @@
+package net.thauvin.erik.pinboard.samples;
+
+import net.thauvin.erik.pinboard.PinConfig;
+import net.thauvin.erik.pinboard.PinboardPoster;
+
+import java.nio.file.Paths;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class JavaExample {
+ public static void main(String[] args) {
+ final String url = "https://example.com/pinboard";
+ final PinboardPoster poster;
+
+ if (args.length == 1) {
+ // API Token is an argument
+ poster = new PinboardPoster(args[0]);
+ } else {
+ // API Token is in local.properties or PINBOARD_API_TOKEN environment variable
+ poster = new PinboardPoster(Paths.get("local.properties"));
+ }
+
+ // Set logging levels
+ final ConsoleHandler consoleHandler = new ConsoleHandler();
+ consoleHandler.setLevel(Level.FINE);
+ final Logger logger = poster.getLogger();
+ logger.addHandler(consoleHandler);
+ logger.setLevel(Level.FINE);
+ logger.setUseParentHandlers(false);
+
+ if (poster.validate()) {
+ // Add Pin
+ if (poster.addPin(new PinConfig.Builder(url, "Testing")
+ .extended("Extra")
+ .tags("test", "java")
+ .build())) {
+ System.out.println("Added: " + url);
+ }
+
+ // Delete Pin
+ if (poster.deletePin(url)) {
+ System.out.println("Deleted: " + url);
+ }
+ }
+ }
+}
diff --git a/examples/gradle/kotlin/.editorconfig b/examples/gradle/kotlin/.editorconfig
new file mode 100644
index 0000000..a6971e1
--- /dev/null
+++ b/examples/gradle/kotlin/.editorconfig
@@ -0,0 +1,2 @@
+[*]
+insert_final_newline=true
diff --git a/examples/gradle/kotlin/.gitignore b/examples/gradle/kotlin/.gitignore
new file mode 100644
index 0000000..6322c7f
--- /dev/null
+++ b/examples/gradle/kotlin/.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/examples/gradle/kotlin/.idea/.gitignore b/examples/gradle/kotlin/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/examples/gradle/kotlin/.idea/.name b/examples/gradle/kotlin/.idea/.name
new file mode 100644
index 0000000..7379aed
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/.name
@@ -0,0 +1 @@
+pinboard-post-examples-gradle-kotlin
\ No newline at end of file
diff --git a/examples/gradle/kotlin/.idea/compiler.xml b/examples/gradle/kotlin/.idea/compiler.xml
new file mode 100644
index 0000000..fb7f4a8
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/kotlin/.idea/inspectionProfiles/Project_Default.xml b/examples/gradle/kotlin/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..1e01b48
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/kotlin/.idea/jarRepositories.xml b/examples/gradle/kotlin/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a529ef2
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/kotlin/.idea/kotlin.iml b/examples/gradle/kotlin/.idea/kotlin.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/kotlin.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/kotlin/.idea/kotlinc.xml b/examples/gradle/kotlin/.idea/kotlinc.xml
new file mode 100644
index 0000000..6d0ee1c
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/kotlin/.idea/misc.xml b/examples/gradle/kotlin/.idea/misc.xml
new file mode 100644
index 0000000..034d4d4
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/misc.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/kotlin/.idea/modules.xml b/examples/gradle/kotlin/.idea/modules.xml
new file mode 100644
index 0000000..a168080
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/kotlin/.idea/vcs.xml b/examples/gradle/kotlin/.idea/vcs.xml
new file mode 100644
index 0000000..c2365ab
--- /dev/null
+++ b/examples/gradle/kotlin/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/gradle/kotlin/README.md b/examples/gradle/kotlin/README.md
new file mode 100644
index 0000000..caf0563
--- /dev/null
+++ b/examples/gradle/kotlin/README.md
@@ -0,0 +1,5 @@
+## Run the Example
+
+```console
+./gradlew run
+```
diff --git a/examples/gradle/kotlin/build.gradle.kts b/examples/gradle/kotlin/build.gradle.kts
new file mode 100644
index 0000000..f41881f
--- /dev/null
+++ b/examples/gradle/kotlin/build.gradle.kts
@@ -0,0 +1,32 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("application")
+ id("com.github.ben-manes.versions") version "0.51.0"
+ kotlin("jvm") version "2.1.20"
+}
+
+defaultTasks(ApplicationPlugin.TASK_RUN_NAME)
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+ maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
+}
+
+dependencies {
+ implementation("net.thauvin.erik:pinboard-poster:1.2.1-SNAPSHOT")
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+}
+
+application {
+ mainClass.set("net.thauvin.erik.pinboard.samples.KotlinExampleKt")
+}
+
+kotlin {
+ compilerOptions.jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
+}
diff --git a/examples/gradle/kotlin/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/kotlin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..9bbc975
Binary files /dev/null and b/examples/gradle/kotlin/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/gradle/kotlin/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/kotlin/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f853b
--- /dev/null
+++ b/examples/gradle/kotlin/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/examples/gradle/kotlin/gradlew b/examples/gradle/kotlin/gradlew
new file mode 100755
index 0000000..faf9300
--- /dev/null
+++ b/examples/gradle/kotlin/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/examples/gradle/kotlin/gradlew.bat b/examples/gradle/kotlin/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/examples/gradle/kotlin/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/gradle/kotlin/settings.gradle.kts b/examples/gradle/kotlin/settings.gradle.kts
new file mode 100644
index 0000000..5d8ff15
--- /dev/null
+++ b/examples/gradle/kotlin/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "pinboard-post-examples-gradle-kotlin"
diff --git a/examples/gradle/kotlin/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt b/examples/gradle/kotlin/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt
new file mode 100644
index 0000000..1b48e82
--- /dev/null
+++ b/examples/gradle/kotlin/src/main/kotlin/net/thauvin/erik/pinboard/samples/KotlinExample.kt
@@ -0,0 +1,37 @@
+package net.thauvin.erik.pinboard.samples
+
+import net.thauvin.erik.pinboard.PinboardPoster
+import java.nio.file.Paths
+import java.util.logging.ConsoleHandler
+import java.util.logging.Level
+
+fun main(args: Array) {
+ val url = "https://example.com/pinboard"
+
+ val poster = if (args.size == 1) {
+ // API Token is an argument
+ PinboardPoster(args[0])
+ } else {
+ // API Token is in local.properties or PINBOARD_API_TOKEN environment variable
+ PinboardPoster(Paths.get("local.properties"))
+ }
+
+ // Set logging levels
+ with(poster.logger) {
+ addHandler(ConsoleHandler().apply { level = Level.FINE })
+ level = Level.FINE
+ useParentHandlers = false
+ }
+
+ if (poster.validate()) {
+ // Add Pin
+ if (poster.addPin(url, "Testing", "Extended test", tags = arrayOf("test", "kotlin"))) {
+ println("Added: $url")
+ }
+
+ // Delete Pin
+ if (poster.deletePin(url)) {
+ println("Deleted: $url")
+ }
+ }
+}
diff --git a/kobalt/Build.kt.iml b/kobalt/Build.kt.iml
deleted file mode 100644
index 4c9b513..0000000
--- a/kobalt/Build.kt.iml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/kobalt/src/Build.kt b/kobalt/src/Build.kt
deleted file mode 100644
index 0439817..0000000
--- a/kobalt/src/Build.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-import com.beust.kobalt.buildScript
-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 net.thauvin.erik.kobalt.plugin.versioneye.versionEye
-import org.apache.maven.model.Developer
-import org.apache.maven.model.License
-import org.apache.maven.model.Model
-import org.apache.maven.model.Scm
-
-val bs = buildScript {
- plugins("net.thauvin.erik:kobalt-versioneye:", "net.thauvin.erik:kobalt-maven-local:")
-}
-
-val p = project {
- name = "pinboard-poster"
- group = "net.thauvin.erik"
- description = "Pinboard Poster for Kotlin/Java"
- artifactId = name
- version = "0.9.3"
-
- 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.51")
- compile("com.squareup.okhttp3:okhttp:3.9.0")
- }
-
- dependenciesTest {
- compile("org.testng:testng:6.12")
- }
-
- assemble {
- jar { }
- mavenJars { }
- }
-
- application {
- mainClass = "net.thauvin.erik.pinboard.PinboardPosterKt"
- ignoreErrorStream = true
- }
-
- application {
- taskName = "runJava"
- mainClass = "net.thauvin.erik.pinboard.JavaExample"
- ignoreErrorStream = true
- }
-
- 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
- }
-
- versionEye {
- org = "Thauvin"
- team = "Owners"
- pom = true
- }
-}
diff --git a/kobalt/wrapper/kobalt-wrapper.jar b/kobalt/wrapper/kobalt-wrapper.jar
deleted file mode 100644
index 6c6b435..0000000
Binary files a/kobalt/wrapper/kobalt-wrapper.jar and /dev/null differ
diff --git a/kobalt/wrapper/kobalt-wrapper.properties b/kobalt/wrapper/kobalt-wrapper.properties
deleted file mode 100644
index be1be45..0000000
--- a/kobalt/wrapper/kobalt-wrapper.properties
+++ /dev/null
@@ -1 +0,0 @@
-kobalt.version=1.0.91
\ No newline at end of file
diff --git a/kobaltw b/kobaltw
deleted file mode 100755
index c5186d5..0000000
--- a/kobaltw
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/usr/bin/env sh
-java -jar "`dirname "$0"`/kobalt/wrapper/kobalt-wrapper.jar" $*
diff --git a/kobaltw.bat b/kobaltw.bat
deleted file mode 100644
index d578071..0000000
--- a/kobaltw.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-@echo off
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-java -jar "%DIRNAME%/kobalt/wrapper/kobalt-wrapper.jar" %*
diff --git a/lib/bld/bld-wrapper.jar b/lib/bld/bld-wrapper.jar
new file mode 100644
index 0000000..7add961
Binary files /dev/null and b/lib/bld/bld-wrapper.jar differ
diff --git a/lib/bld/bld-wrapper.properties b/lib/bld/bld-wrapper.properties
new file mode 100644
index 0000000..fc9463a
--- /dev/null
+++ b/lib/bld/bld-wrapper.properties
@@ -0,0 +1,10 @@
+bld.downloadExtensionJavadoc=false
+bld.downloadExtensionSources=true
+bld.downloadLocation=
+bld.extension-detekt=com.uwyn.rife2:bld-detekt:0.9.10-SNAPSHOT
+bld.extension-dokka=com.uwyn.rife2:bld-dokka:1.0.4-SNAPSHOT
+bld.extension-jacoco=com.uwyn.rife2:bld-jacoco-report:0.9.10-SNAPSHOT
+bld.extension-kotlin=com.uwyn.rife2:bld-kotlin:1.1.0-SNAPSHOT
+bld.repositories=MAVEN_LOCAL,MAVEN_CENTRAL,RIFE2_SNAPSHOTS,RIFE2_RELEASES
+bld.sourceDirectories=
+bld.version=2.2.1
diff --git a/pinboard-poster.iml b/pinboard-poster.iml
deleted file mode 100644
index 6ebe439..0000000
--- a/pinboard-poster.iml
+++ /dev/null
@@ -1,133 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 58cd66a..64767c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,12 +1,12 @@
-
+
4.0.0
net.thauvin.erik
pinboard-poster
- 0.9.2
+ 1.2.1-SNAPSHOT
pinboard-poster
- Pinboard Poster for Kotlin/Java
+ A small library for posting to Pinboard
https://github.com/ethauvin/pinboard-poster
@@ -14,34 +14,49 @@
https://opensource.org/licenses/BSD-3-Clause
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ 2.1.20
+ compile
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-common
+ 2.1.20
+ compile
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+ 2.1.20
+ compile
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.12.0
+ compile
+
+
+ com.squareup.okhttp3
+ logging-interceptor
+ 4.12.0
+ compile
+
+
ethauvin
Erik C. Thauvin
erik@thauvin.net
+ https://erik.thauvin.net/
- https://github.com/ethauvin/pinboard-poster.git
- git@github.com:ethauvin/pinboard-poster.git
+ scm:git:https://github.com/ethauvin/pinboard-poster.git
+ scm:git:git@github.com:ethauvin/pinboard-poster.git
https://github.com/ethauvin/pinboard-poster
-
-
- org.jetbrains.kotlin
- kotlin-stdlib
- 1.1.51
-
-
- com.squareup.okhttp3
- okhttp
- 3.9.0
-
-
- org.testng
- testng
- 6.12
- test
-
-
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..20487a7
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,7 @@
+sonar.organization=ethauvin-github
+sonar.projectKey=ethauvin_pinboard-poster
+sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
+sonar.sources=src/main/kotlin/
+sonar.tests=src/test/kotlin/
+sonar.java.binaries=build/main,build/test
+sonar.java.libraries=lib/compile/*.jar
diff --git a/src/bld/java/net/thauvin/erik/pinboard/PinboardPosterBuild.java b/src/bld/java/net/thauvin/erik/pinboard/PinboardPosterBuild.java
new file mode 100644
index 0000000..d4689ac
--- /dev/null
+++ b/src/bld/java/net/thauvin/erik/pinboard/PinboardPosterBuild.java
@@ -0,0 +1,201 @@
+/*
+ * PinboardPosterBuild.java
+ *
+ * Copyright (c) 2017-2025, Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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 rife.bld.BuildCommand;
+import rife.bld.Project;
+import rife.bld.extension.CompileKotlinOperation;
+import rife.bld.extension.DetektOperation;
+import rife.bld.extension.DokkaOperation;
+import rife.bld.extension.JacocoReportOperation;
+import rife.bld.extension.dokka.LoggingLevel;
+import rife.bld.extension.dokka.OutputFormat;
+import rife.bld.extension.kotlin.CompileOptions;
+import rife.bld.operations.exceptions.ExitStatusException;
+import rife.bld.publish.PomBuilder;
+import rife.bld.publish.PublishDeveloper;
+import rife.bld.publish.PublishLicense;
+import rife.bld.publish.PublishScm;
+import rife.tools.exceptions.FileUtilsErrorException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static rife.bld.dependencies.Repository.*;
+import static rife.bld.dependencies.Scope.compile;
+import static rife.bld.dependencies.Scope.test;
+
+public class PinboardPosterBuild extends Project {
+ final File srcMainKotlin = new File(srcMainDirectory(), "kotlin");
+
+ public PinboardPosterBuild() {
+ pkg = "net.thauvin.erik";
+ name = "pinboard-poster";
+ version = version(1, 2, 1, "SNAPSHOT");
+
+ mainClass = pkg + ".PinboardPoster";
+
+ javaRelease = 11;
+ downloadSources = true;
+ autoDownloadPurge = true;
+ repositories = List.of(MAVEN_LOCAL, MAVEN_CENTRAL);
+
+ final var okHttp = version(4, 12, 0);
+ final var kotlin = version(2, 1, 20);
+ scope(compile)
+ // Kotlin
+ .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin))
+ .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-common", kotlin))
+ .include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk8", kotlin))
+ // OkHttp
+ .include(dependency("com.squareup.okhttp3", "okhttp", okHttp))
+ .include(dependency("com.squareup.okhttp3", "logging-interceptor", okHttp));
+ scope(test)
+ .include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", kotlin))
+ .include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 12, 1)))
+ .include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 12, 1)))
+ .include(dependency("org.junit.platform", "junit-platform-launcher", version(1, 12, 1)));
+
+ publishOperation()
+ .repository(version.isSnapshot() ? repository(SONATYPE_SNAPSHOTS_LEGACY.location())
+ .withCredentials(property("sonatype.user"), property("sonatype.password"))
+ : repository(SONATYPE_RELEASES_LEGACY.location())
+ .withCredentials(property("sonatype.user"), property("sonatype.password")))
+ .repository(repository("github"))
+ .info()
+ .groupId(pkg)
+ .artifactId(name)
+ .description("A small library for posting to Pinboard")
+ .url("https://github.com/ethauvin/" + name)
+ .developer(new PublishDeveloper()
+ .id("ethauvin")
+ .name("Erik C. Thauvin")
+ .email("erik@thauvin.net")
+ .url("https://erik.thauvin.net/")
+ )
+ .license(new PublishLicense()
+ .name("BSD 3-Clause")
+ .url("https://opensource.org/licenses/BSD-3-Clause")
+ )
+ .scm(new PublishScm()
+ .connection("scm:git:https://github.com/ethauvin/" + name + ".git")
+ .developerConnection("scm:git:git@github.com:ethauvin/" + name + ".git")
+ .url("https://github.com/ethauvin/" + name)
+ )
+ .signKey(property("sign.key"))
+ .signPassphrase(property("sign.passphrase"));
+
+ jarSourcesOperation().sourceDirectories(srcMainKotlin);
+ }
+
+ public static void main(final String[] args) {
+ // Enable detailed logging for the extensions
+ var level = Level.ALL;
+ var logger = Logger.getLogger("rife.bld.extension");
+ var consoleHandler = new ConsoleHandler();
+
+ consoleHandler.setLevel(level);
+ logger.addHandler(consoleHandler);
+ logger.setLevel(level);
+ logger.setUseParentHandlers(false);
+
+ new PinboardPosterBuild().start(args);
+ }
+
+ @BuildCommand(summary = "Compiles the Kotlin project")
+ @Override
+ public void compile() throws Exception {
+ new CompileKotlinOperation()
+ .fromProject(this)
+ .compileOptions(new CompileOptions().verbose(true))
+ .execute();
+ }
+
+ @BuildCommand(summary = "Checks source with Detekt")
+ public void detekt() throws ExitStatusException, IOException, InterruptedException {
+ new DetektOperation()
+ .fromProject(this)
+ .baseline("config/detekt/baseline.xml")
+ .execute();
+ }
+
+ @BuildCommand(value = "detekt-baseline", summary = "Creates the Detekt baseline")
+ public void detektBaseline() throws ExitStatusException, IOException, InterruptedException {
+ new DetektOperation()
+ .fromProject(this)
+ .baseline("config/detekt/baseline.xml")
+ .createBaseline(true)
+ .execute();
+ }
+
+ @BuildCommand(summary = "Generates JaCoCo Reports")
+ public void jacoco() throws Exception {
+ new JacocoReportOperation()
+ .fromProject(this)
+ .sourceFiles(srcMainKotlin)
+ .execute();
+ }
+
+ @Override
+ public void javadoc() throws ExitStatusException, IOException, InterruptedException {
+ new DokkaOperation()
+ .fromProject(this)
+ .loggingLevel(LoggingLevel.INFO)
+ .moduleName("CryptoPrice")
+ .moduleVersion(version.toString())
+ .outputDir(new File(buildDirectory(), "javadoc"))
+ .outputFormat(OutputFormat.JAVADOC)
+ .execute();
+ }
+
+ @Override
+ public void publish() throws Exception {
+ super.publish();
+ pomRoot();
+ }
+
+ @Override
+ public void publishLocal() throws Exception {
+ super.publishLocal();
+ pomRoot();
+ }
+
+ @BuildCommand(value = "pom-root", summary = "Generates the POM file in the root directory")
+ public void pomRoot() throws FileUtilsErrorException {
+ PomBuilder.generateInto(publishOperation().fromProject(this).info(), dependencies(),
+ new File(workDirectory, "pom.xml"));
+ }
+}
diff --git a/src/main/java/net/thauvin/erik/pinboard/JavaExample.java b/src/main/java/net/thauvin/erik/pinboard/JavaExample.java
deleted file mode 100644
index d252652..0000000
--- a/src/main/java/net/thauvin/erik/pinboard/JavaExample.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.logging.ConsoleHandler;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-public class JavaExample {
- public static void main(String[] args) {
- final String url = "http://www.example.com/pinboard";
- final Path localProps = Paths.get("local.properties");
- final PinboardPoster poster;
-
- if (args.length == 1) {
- // API Token is an argument
- poster = new PinboardPoster(args[0]);
- } else {
- // API Token is in local.properties or PINBOARD_API_TOKEN environment variable
- poster = new PinboardPoster(localProps);
- }
-
- // Set logging levels
- final ConsoleHandler consoleHandler = new ConsoleHandler();
- consoleHandler.setLevel(Level.FINE);
- final Logger logger = poster.getLogger();
- logger.addHandler(consoleHandler);
- logger.setLevel(Level.FINE);
-
- // Add Pin
- if (poster.addPin(url, "Testing", "Extended test", "test java")) {
- System.out.println("Added: " + url);
- }
-
- // Delete Pin
- 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/PinConfig.kt b/src/main/kotlin/net/thauvin/erik/pinboard/PinConfig.kt
new file mode 100644
index 0000000..a41e563
--- /dev/null
+++ b/src/main/kotlin/net/thauvin/erik/pinboard/PinConfig.kt
@@ -0,0 +1,144 @@
+/*
+ * PinConfig.kt
+ *
+ * Copyright (c) 2017-2025, Erik C. Thauvin (erik@thauvin.net)
+ *
+ * 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.
+ *
+ * Supports of all the [Pinboard API Parameters](https://pinboard.in/api/#posts_add).
+ */
+class PinConfig private constructor(builder: Builder) {
+ val url: String = builder.url
+ val description: String = builder.description
+ val extended = builder.extended
+ val tags = builder.tags
+ val dt = builder.dt
+ val replace = builder.replace
+ val shared = builder.shared
+ val toRead = builder.toRead
+
+ /**
+ * Configures the parameters to add a pin.
+ *
+ * @param url The URL of the bookmark.
+ * @param description The title of the bookmark.
+ */
+ data class Builder(var url: String, var description: String) {
+ var extended: String = ""
+ var tags: Array = emptyArray()
+ var dt: ZonedDateTime = ZonedDateTime.now()
+ var replace: Boolean = true
+ var shared: Boolean = true
+ var toRead: Boolean = false
+
+ /**
+ * The URL of the bookmark.
+ */
+ fun url(url: String): Builder = apply { this.url = url }
+
+ /**
+ * The title of the bookmark.
+ */
+ fun description(description: String): Builder = apply { this.description = description }
+
+ /**
+ * The description of the bookmark.
+ */
+ fun extended(extended: String): Builder = apply { this.extended = extended }
+
+ /**
+ * A list of up to 100 tags.
+ */
+ fun tags(vararg tag: String): Builder = apply { this.tags = tag }
+
+ /**
+ * The creation time of the bookmark.
+ */
+ fun dt(datetime: ZonedDateTime): Builder = apply { this.dt = datetime }
+
+ /**
+ * Replace any existing bookmark with the specified URL. Default `true`.
+ */
+ fun replace(replace: Boolean): Builder = apply { this.replace = replace }
+
+ /**
+ * Make bookmark public. Default is `true`.
+ */
+ fun shared(shared: Boolean): Builder = apply { this.shared = shared }
+
+ /**
+ * Mark the bookmark as unread. Default is `false`.
+ */
+ fun toRead(toRead: Boolean): Builder = apply { this.toRead = toRead }
+
+ /**
+ * Builds a new configuration.
+ */
+ fun build() = PinConfig(this)
+
+ 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
+ }
+
+ override fun toString(): String {
+ return "Builder(url='$url', description='$description', extended='$extended'," +
+ "tags=${tags.contentToString()}, dt=$dt, replace=$replace, shared=$shared, toRead=$toRead)"
+ }
+ }
+}
diff --git a/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt b/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt
index 495a11d..00d2cc1 100644
--- a/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt
+++ b/src/main/kotlin/net/thauvin/erik/pinboard/PinboardPoster.kt
@@ -1,8 +1,7 @@
/*
* PinboardPoster.kt
*
- * Copyright (c) 2017, Erik C. Thauvin (erik@thauvin.net)
- * All rights reserved.
+ * Copyright (c) 2017-2025, Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -29,44 +28,73 @@
* 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 net.thauvin.erik.pinboard.Constants.ENV_API_TOKEN
-import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
+import okhttp3.logging.HttpLoggingInterceptor
import org.xml.sax.InputSource
+import java.io.File
+import java.io.IOException
import java.io.StringReader
-import java.net.URL
+import java.net.URI
+import java.net.URISyntaxException
import java.nio.file.Files
import java.nio.file.Path
-import java.nio.file.Paths
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
import java.util.*
-import java.util.logging.ConsoleHandler
import java.util.logging.Level
import java.util.logging.Logger
import javax.xml.parsers.DocumentBuilderFactory
+/** Constants for this package. **/
object Constants {
+ /** The Pinboard API endpoint URL. **/
const val API_ENDPOINT = "https://api.pinboard.in/v1/"
- const val AUTH_TOKEN = "auth_token"
- const val DONE = "done"
+
+ /** The API token environment variable. **/
const val ENV_API_TOKEN = "PINBOARD_API_TOKEN"
}
+/**
+ * A small Kotlin/Java library for posting to [Pinboard](https://pinboard.in/).
+ *
+ * @constructor Creates a new instance.
+ *
+ * @author Erik C. Thauvin
+ */
open class PinboardPoster() {
+ /**
+ * Creates a new instance using an [API Token][apiToken].
+ *
+ * @param apiToken The API token.
+ */
constructor(apiToken: String) : this() {
this.apiToken = apiToken
}
- @Suppress("unused")
+ /**
+ * Creates a new instance using a [Properties][properties] and [Property Key][key].
+ *
+ * @param properties The properties.
+ * @param key The property key.
+ */
@JvmOverloads
- constructor(properties: Properties, key: String = ENV_API_TOKEN) : this() {
+ constructor(properties: Properties, key: String = Constants.ENV_API_TOKEN) : this() {
apiToken = properties.getProperty(key, apiToken)
}
+ /**
+ * Creates a new instance using a [Properties File Path][propertiesFilePath] and [Property Key][key].
+ *
+ * @param propertiesFilePath The properties file path.
+ * @param key The property key.
+ */
@JvmOverloads
- constructor(propertiesFilePath: Path, key: String = ENV_API_TOKEN) : this() {
+ constructor(propertiesFilePath: Path, key: String = Constants.ENV_API_TOKEN) : this() {
if (Files.exists(propertiesFilePath)) {
apiToken = Properties().apply {
Files.newInputStream(propertiesFilePath).use { nis ->
@@ -76,38 +104,93 @@ open class PinboardPoster() {
}
}
- var apiToken: String = if (System.getenv(ENV_API_TOKEN).isNullOrBlank()) "" else System.getenv(ENV_API_TOKEN)
+ /**
+ * Creates a new instance using a [Properties File][propertiesFile] and [Property Key][key].
+ *
+ * @param propertiesFile The properties file.
+ * @param key The property key.
+ */
+ @Suppress("unused")
+ @JvmOverloads
+ constructor(propertiesFile: File, key: String = Constants.ENV_API_TOKEN) : this(propertiesFile.toPath(), key)
+ /** The API token. **/
+ var apiToken: String = System.getenv(Constants.ENV_API_TOKEN) ?: ""
+
+ /** The API end point. **/
var apiEndPoint: String = Constants.API_ENDPOINT
+ /** The logger instance. **/
val logger: Logger by lazy { Logger.getLogger(PinboardPoster::class.java.simpleName) }
- private val client by lazy { OkHttpClient() }
+ private val client by lazy {
+ OkHttpClient.Builder().apply {
+ if (logger.isLoggable(Level.FINE)) {
+ addInterceptor(HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ })
+ }
+ }.build()
+ }
+ /**
+ * Adds a bookmark to Pinboard using a [PinConfig] builder.
+ */
+ 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.
+ *
+ * This method supports of all the [Pinboard API Parameters](https://pinboard.in/api/#posts_add).
+ *
+ * @param url The URL of the bookmark.
+ * @param description The title of the bookmark.
+ * @param extended The description of the bookmark.
+ * @param tags A list of up to 100 tags.
+ * @param dt The creation time of the bookmark.
+ * @param replace Replace any existing bookmark with the specified URL. Default `true`.
+ * @param shared Make bookmark public. Default is `true`.
+ * @param toRead Mark the bookmark as unread. Default is `false`.
+ *
+ * @return `true` if bookmark was successfully added.
+ */
@JvmOverloads
- fun addPin(url: String,
- description: String,
- extended: String = "",
- tags: String = "",
- dt: String = "",
- replace: Boolean = true,
- shared: Boolean = true,
- toRead: Boolean = false): Boolean {
+ fun addPin(
+ url: String,
+ description: String,
+ extended: String = "",
+ vararg tags: String = emptyArray(),
+ dt: ZonedDateTime = ZonedDateTime.now(),
+ replace: Boolean = true,
+ shared: Boolean = true,
+ toRead: Boolean = false
+ ): Boolean {
if (validate()) {
if (!validateUrl(url)) {
logger.severe("Please specify a valid URL to pin.")
} else if (description.isBlank()) {
- logger.severe("Please specify a valid description.")
+ logger.severe("Please specify a valid description to pin: `$url`")
} else {
- val params = listOf(
- Pair("url", url),
- Pair("description", description),
- Pair("extended", extended),
- Pair("tags", tags),
- Pair("dt", dt),
- Pair("replace", yesNo(replace)),
- Pair("shared", yesNo(shared)),
- Pair("toread", yesNo(toRead))
+ val params = mapOf(
+ "url" to url,
+ "description" to description,
+ "extended" to extended,
+ "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)
)
return executeMethod("posts/add", params)
}
@@ -116,108 +199,123 @@ open class PinboardPoster() {
return false
}
+ /**
+ * Deletes a bookmark on Pinboard.
+ *
+ * This method supports of all the [Pinboard API Parameters](https://pinboard.in/api/#posts_delete).
+ *
+ * @param url The URL of the bookmark to delete.
+ *
+ * @return `true` if bookmark was successfully deleted.
+ */
fun deletePin(url: String): Boolean {
if (validate()) {
if (!validateUrl(url)) {
logger.severe("Please specify a valid URL to delete.")
} else {
- return executeMethod("posts/delete", listOf(Pair("url", url)))
+ return executeMethod("posts/delete", mapOf("url" to url))
}
}
return false
}
- private fun executeMethod(method: String, params: List>): Boolean {
- val apiUrl = HttpUrl.parse(cleanEndPoint(method))
- if (apiUrl != null) {
- val httpUrl = apiUrl.newBuilder().apply {
- params.forEach {
- if (it.second.isNotBlank()) {
- addQueryParameter(it.first, it.second)
- }
- }
- addQueryParameter(Constants.AUTH_TOKEN, apiToken)
- }.build()
-
- val request = Request.Builder().url(httpUrl).build()
- val result = client.newCall(request).execute()
-
- logHttp(method, "HTTP Result: ${result.code()}")
-
- val response = result.body()?.string()
-
- if (response != null) {
- logHttp(method, "HTTP Response:\n$response")
- if (response.contains(Constants.DONE)) {
- return true
- } else {
- val factory = DocumentBuilderFactory.newInstance().apply {
- isValidating = false
- isIgnoringElementContentWhitespace = true
- isIgnoringComments = true
- isCoalescing = false
- isNamespaceAware = false
- }
-
- try {
- val document = factory.newDocumentBuilder().parse(InputSource(StringReader(response)))
-
- val code = document.getElementsByTagName("result")?.item(0)?.attributes?.getNamedItem("code")?.nodeValue
-
- if (code != null && code.isNotBlank()) {
- logger.severe("An error has occurred while executing $method: $code")
- } else {
- logger.severe("An error has occurred while executing $method.")
- }
- } catch (e: Exception) {
- logger.log(Level.SEVERE, "Could not parse $method XML response.", e)
- }
- }
- }
- } else {
- logger.severe("Invalid API end point: $apiEndPoint")
+ @Throws(IOException::class)
+ internal fun parseMethodResponse(method: String, response: String) {
+ val factory = DocumentBuilderFactory.newInstance().apply {
+ isValidating = false
+ isIgnoringElementContentWhitespace = true
+ isIgnoringComments = true
+ isCoalescing = false
+ isNamespaceAware = false
}
- return false
+ if (response.isEmpty()) {
+ throw IOException("Response for $method is empty.")
+ }
+
+ try {
+ val document = factory.newDocumentBuilder().parse(InputSource(StringReader(response)))
+
+ val code = document.getElementsByTagName("result")?.item(0)?.attributes?.getNamedItem("code")?.nodeValue
+
+ if (!code.isNullOrBlank()) {
+ throw IOException("An error has occurred while executing $method: $code")
+ } else {
+ throw IOException("An error has occurred while executing $method.")
+ }
+ } catch (e: org.xml.sax.SAXException) {
+ throw IOException("Could not parse $method response.", e)
+ } catch (e: IllegalArgumentException) {
+ throw IOException("Invalid input source for $method response", e)
+ }
}
private fun cleanEndPoint(method: String): String {
- return if (apiEndPoint.endsWith('/')) {
+ return if (apiEndPoint.last() == '/') {
"$apiEndPoint$method"
} else {
"$apiEndPoint/$method"
}
}
- private fun logHttp(method: String, msg: String) {
- logger.logp(Level.FINE, PinboardPoster::class.java.name, "executeMethod($method)", msg)
+ private fun executeMethod(method: String, params: Map): Boolean {
+ try {
+ val apiUrl = cleanEndPoint(method).toHttpUrlOrNull()
+ if (apiUrl != null) {
+ val httpUrl = apiUrl.newBuilder().apply {
+ params.forEach {
+ addQueryParameter(it.key, it.value)
+ }
+ addQueryParameter("auth_token", apiToken)
+ }.build()
+
+ val request = Request.Builder().url(httpUrl).build()
+ client.newCall(request).execute().use { result ->
+ result.body?.string()?.let { response ->
+ if (response.contains("done")) {
+ return true
+ } else {
+ parseMethodResponse(method, response)
+ }
+ }
+ }
+ } else {
+ logger.severe("Invalid API end point: $apiEndPoint")
+ }
+ } catch (e: IOException) {
+ logger.log(Level.SEVERE, e.message, e)
+ }
+
+ return false
}
- private fun validate(): Boolean {
- if (apiToken.isBlank() || !apiToken.contains(':')) {
+ /**
+ * Ensures that the API token and end point are valid.
+ */
+ fun validate(): Boolean {
+ var isValid = true
+ if (!apiToken.contains(':')) {
logger.severe("Please specify a valid API token. (eg. user:TOKEN)")
- return false
+ isValid = false
} else if (!validateUrl(apiEndPoint)) {
logger.severe("Please specify a valid API end point. (eg. ${Constants.API_ENDPOINT})")
- return false
+ isValid = false
}
- return true
+ return isValid
}
private fun validateUrl(url: String): Boolean {
- if (url.isBlank()) {
- return false
+ var isValid = url.isNotBlank()
+ if (isValid) {
+ try {
+ URI(url)
+ } catch (e: URISyntaxException) {
+ logger.log(Level.FINE, "Invalid URL: $url", e)
+ isValid = false
+ }
}
-
- try {
- URL(url)
- } catch (e: Exception) {
- logger.log(Level.FINE, "Invalid URL: $url", e)
- return false
- }
-
- return true
+ return isValid
}
private fun yesNo(bool: Boolean): String {
@@ -228,32 +326,3 @@ open class PinboardPoster() {
}
}
}
-
-fun main(args: Array) {
- val url = "http://www.example.com/pinboard"
- val localProp = Paths.get("local.properties")
-
- val poster = if (args.size == 1) {
- // API Token is an argument
- PinboardPoster(args[0])
- } else {
- // API Token is in local.properties or PINBOARD_API_TOKEN environment variable
- PinboardPoster(localProp)
- }
-
- // Set logging levels
- with(poster.logger) {
- addHandler(ConsoleHandler().apply { level = Level.FINE })
- level = Level.FINE
- }
-
- // Add Pin
- if (poster.addPin(url, "Testing", "Extended test", "test kotlin")) {
- println("Added: $url")
- }
-
- // Delete Pin
- if (poster.deletePin(url)) {
- println("Deleted: $url")
- }
-}
\ No newline at end of file
diff --git a/src/test/kotlin/net/thauvin/erik/pinboard/PinboardPosterTest.kt b/src/test/kotlin/net/thauvin/erik/pinboard/PinboardPosterTest.kt
index 06cf351..9281df5 100644
--- a/src/test/kotlin/net/thauvin/erik/pinboard/PinboardPosterTest.kt
+++ b/src/test/kotlin/net/thauvin/erik/pinboard/PinboardPosterTest.kt
@@ -1,8 +1,7 @@
/*
* PinboardPosterTest.kt
*
- * Copyright (c) 2017, Erik C. Thauvin (erik@thauvin.net)
- * All rights reserved.
+ * Copyright (c) 2017-2025, Erik C. Thauvin (erik@thauvin.net)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -29,43 +28,127 @@
* 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 org.testng.Assert
-import org.testng.annotations.Test
+import org.junit.jupiter.api.assertThrows
+import java.io.IOException
+import java.nio.file.Files
import java.nio.file.Paths
+import java.time.ZonedDateTime
+import java.util.*
+import java.util.logging.Level
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
class PinboardPosterTest {
- private val url = "http://www.foo.com/"
+ private val url = randomUrl()
private val desc = "This is a test."
private val localProps = Paths.get("local.properties")
+ private val isCi = "true" == System.getenv("CI")
+
+ private fun randomUrl(): String = "https://www.example.com/?random=" + (1000..10000).random()
@Test
fun testAddPin() {
var poster = PinboardPoster("")
+ poster.logger.level = Level.FINE
- Assert.assertFalse(poster.addPin(url, desc), "apiToken: ")
+ assertFalse(poster.addPin(url, desc), "apiToken: ")
poster.apiToken = "foo"
- Assert.assertFalse(poster.addPin(url, desc), "apiToken: ${poster.apiToken}")
+ assertFalse(poster.addPin(url, desc), "apiToken: ${poster.apiToken}")
- //poster.apiToken = "foo:TESTING"
- //Assert.assertFalse(poster.addPin(url, desc), "apiToken: ${poster.apiToken}")
+ // poster.apiToken = "foo:TESTING"
+ // assertFalse(poster.addPin(url, desc), "apiToken: ${poster.apiToken}")
poster = PinboardPoster(localProps)
- Assert.assertTrue(poster.addPin(url, desc), "apiToken: ${Constants.ENV_API_TOKEN}")
+ if (!isCi) {
+ poster.logger.level = Level.FINE
+ }
+
+ assertTrue(poster.validate(), "validate()")
+
+ assertTrue(poster.addPin(url, desc), "addPin($url, $desc)")
+
+ assertTrue(poster.deletePin(url), "deletePin($url)")
+ }
+
+ @Test
+ fun testAddPinConfig() {
+ val poster = PinboardPoster(localProps)
+ if (!isCi) {
+ poster.logger.level = Level.FINE
+ }
+
+ assertTrue(poster.validate(), "validate()")
+
+ var config = PinConfig.Builder(url, 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.description("Yet another test.").replace(true).toRead(true)
+ assertTrue(poster.addPin(config.build()), "toRead(true)")
+
+ config = config.dt(ZonedDateTime.now())
+ assertTrue(poster.addPin(config.build()), "dt(now)")
+
+ assertTrue(poster.deletePin(url), "deletePin($url)")
+
+ config = config.url(randomUrl())
+ assertTrue(poster.addPin(config.build()), "add($url)")
+ assertTrue(poster.deletePin(config.url), "delete($url)")
}
@Test
fun testDeletePin() {
- val poster = PinboardPoster(localProps)
+ val props = if (Files.exists(localProps)) {
+ Properties().apply {
+ Files.newInputStream(localProps).use { nis -> load(nis) }
+ }
+ } else {
+ Properties().apply {
+ setProperty(Constants.ENV_API_TOKEN, System.getenv(Constants.ENV_API_TOKEN))
+ }
+ }
+
+ var poster = PinboardPoster(props)
+ if (!isCi) {
+ poster.logger.level = Level.FINE
+ }
+
+ assertTrue(poster.validate(), "validate()")
poster.apiEndPoint = ""
- Assert.assertFalse(poster.deletePin(url), "apiEndPoint: ")
+ assertFalse(poster.deletePin(url), "apiEndPoint: ")
+
+ poster = PinboardPoster(localProps, Constants.ENV_API_TOKEN)
poster.apiEndPoint = Constants.API_ENDPOINT
- Assert.assertTrue(poster.deletePin(url), "apiEndPoint: ${Constants.API_ENDPOINT}")
+ assertTrue(poster.addPin(url, desc), "addPin($url, $desc)")
+ assertTrue(poster.deletePin(url), "deletePin($url)")
- Assert.assertFalse(poster.deletePin("foo.com"), "url: foo.com")
+ assertThrows {
+ poster.parseMethodResponse("post/delete", "")
+ }
+
+ assertThrows {
+ poster.parseMethodResponse("post/delete", "")
+ }
+
+ assertFalse(poster.deletePin("foo.com"), "deletePin(foo.com)")
}
}