From a444b72b87862bbd5bd77fda59f66e0a952e6dac Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Tue, 25 Feb 2020 13:51:54 -0800 Subject: [PATCH] Initial commit. --- .editorconfig | 2 + .gitattributes | 5 + .gitignore | 87 ++++++ .idea/.gitignore | 8 + .idea/checkstyle-idea.xml | 16 ++ .idea/compiler.xml | 9 + .idea/encodings.xml | 6 + .idea/inspectionProfiles/Project_Default.xml | 53 ++++ .idea/jarRepositories.xml | 20 ++ .idea/misc.xml | 20 ++ .idea/modules.xml | 10 + .idea/modules/bitly-shorten_main.iml | 77 +++++ .idea/modules/bitly-shorten_test.iml | 92 ++++++ bitly-shorten.iml | 13 + build.gradle.kts | 272 ++++++++++++++++++ detekt-baseline.xml | 10 + gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 183 ++++++++++++ gradlew.bat | 103 +++++++ settings.gradle.kts | 10 + .../kotlin/net/thauvin/erik/bitly/Bitly.kt | 252 ++++++++++++++++ .../net/thauvin/erik/bitly/BitlyTest.kt | 72 +++++ version.properties | 8 + 23 files changed, 1333 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/checkstyle-idea.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/modules/bitly-shorten_main.iml create mode 100644 .idea/modules/bitly-shorten_test.iml create mode 100644 bitly-shorten.iml create mode 100644 build.gradle.kts create mode 100644 detekt-baseline.xml create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 src/main/kotlin/net/thauvin/erik/bitly/Bitly.kt create mode 100644 src/test/kotlin/net/thauvin/erik/bitly/BitlyTest.kt create mode 100644 version.properties 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/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6ec2ae2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# batch files are specific to windows and always crlf +*.bat eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b81e287 --- /dev/null +++ b/.gitignore @@ -0,0 +1,87 @@ +__pycache__ +!.vscode/extensions.json +!.vscode/launch.json +!.vscode/settings.json +!.vscode/tasks.json +!gradle-wrapper.jar +.classpath +.DS_Store +.gradle +.history +.kobalt +.mtj.tmp/ +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.nb-gradle +.project +.scannerwork +.settings +.vscode/* +*.class +*.code-workspace +*.ctxt +*.ear +*.iws +*.jar +*.log +*.nar +*.rar +*.sublime-* +*.tar.gz +*.war +*.zip +/**/.idea_modules/ +/**/.idea/**/caches/build_file_checksums.ser +/**/.idea/**/contentModel.xml +/**/.idea/**/dataSources.ids +/**/.idea/**/dataSources.local.xml +/**/.idea/**/dataSources/ +/**/.idea/**/dbnavigator.xml +/**/.idea/**/dictionaries +/**/.idea/**/dynamic.xml +/**/.idea/**/gradle.xml +/**/.idea/**/httpRequests +/**/.idea/**/libraries +/**/.idea/**/mongoSettings.xml +/**/.idea/**/replstate.xml +/**/.idea/**/shelf +/**/.idea/**/shelf/ +/**/.idea/**/sqlDataSources.xml +/**/.idea/**/tasks.xml +/**/.idea/**/uiDesigner.xml +/**/.idea/**/usage.statistics.xml +/**/.idea/**/workspace.xml +/**/.idea/$CACHE_FILE$ +/**/.idea/$PRODUCT_WORKSPACE_FILE$ +atlassian-ide-plugin.xml +bin/ +build/ +cmake-build-*/ +com_crashlytics_export_strings.xml +crashlytics-build.properties +crashlytics.properties +dependency-reduced-pom.xml +deploy/ +dist/ +ehthumbs.db +fabric.properties +gen/ +gradle.properties +hs_err_pid* +kobaltBuild +kobaltw*-test +lib/kotlin* +libs/ +local.properties +out/ +pom.xml.next +pom.xml.releaseBackup +pom.xml.tag +pom.xml.versionsBackup +proguard-project.txt +project.properties +release.properties +target/ +test-output +Thumbs.db +venv diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..21a0f73 --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..6d32514 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8ff795e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,53 @@ + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..efa4625 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8035e97 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c061a23 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/bitly-shorten_main.iml b/.idea/modules/bitly-shorten_main.iml new file mode 100644 index 0000000..7bcf554 --- /dev/null +++ b/.idea/modules/bitly-shorten_main.iml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/bitly-shorten_test.iml b/.idea/modules/bitly-shorten_test.iml new file mode 100644 index 0000000..85ebd24 --- /dev/null +++ b/.idea/modules/bitly-shorten_test.iml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bitly-shorten.iml b/bitly-shorten.iml new file mode 100644 index 0000000..9bad4e0 --- /dev/null +++ b/bitly-shorten.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..46d484a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,272 @@ +import com.jfrog.bintray.gradle.tasks.BintrayUploadTask +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.io.FileInputStream +import java.util.Properties + +plugins { + jacoco + `java-library` + `maven-publish` + id("com.github.ben-manes.versions") version "0.28.0" + id("com.jfrog.bintray") version "1.8.4" + id("io.gitlab.arturbosch.detekt") version "1.5.1" + id("net.thauvin.erik.gradle.semver") version "1.0.4" + id("org.jetbrains.dokka") version "0.10.1" + id("org.jetbrains.kotlin.jvm") version "1.3.61" + id("org.jetbrains.kotlin.kapt").version("1.3.61") + id("org.jmailen.kotlinter") version "2.3.1" + id("org.sonarqube") version "2.8" +} + +group = "net.thauvin.erik.bitly" +description = "Bitly Shortener for Kotlin/Java" + +val gitHub = "ethauvin/$name" +val mavenUrl = "https://github.com/$gitHub" +val deployDir = "deploy" +var isRelease = "release" in gradle.startParameter.taskNames + +var semverProcessor = "net.thauvin.erik:semver:1.2.0" + +val publicationName = "mavenJava" + +object VersionInfo { + const val okhttp = "4.3.1" +} + +val versions: VersionInfo by extra { VersionInfo } + +// Load local.properties +File("local.properties").apply { + if (exists()) { + FileInputStream(this).use { fis -> + Properties().apply { + load(fis) + forEach { (k, v) -> + extra[k as String] = v + } + } + } + } +} + +repositories { + jcenter() +} + +dependencies { + // Align versions of all Kotlin components + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + + // Use the Kotlin JDK 8 standard library. + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("com.squareup.okhttp3:okhttp:${versions.okhttp}") + implementation("com.squareup.okhttp3:logging-interceptor:${versions.okhttp}") + implementation("org.json:json:20190722") + + // Use the Kotlin test library. + testImplementation("org.jetbrains.kotlin:kotlin-test") + + // Use the Kotlin JUnit integration. + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") +} + +kapt { + arguments { + arg("semver.project.dir", projectDir) + } +} + +detekt { + baseline = project.rootDir.resolve("detekt-baseline.xml") +} + +kotlinter { + ignoreFailures = false + reporters = arrayOf("html") + experimentalRules = false + disabledRules = arrayOf("import-ordering") +} + +jacoco { + toolVersion = "0.8.3" +} + +sonarqube { + properties { + property("sonar.projectKey", "ethauvin_$name") + property("sonar.sourceEncoding", "UTF-8") + } +} + +val sourcesJar by tasks.creating(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets.getByName("main").allSource) +} + +val javadocJar by tasks.creating(Jar::class) { + dependsOn(tasks.dokka) + from(tasks.dokka) + archiveClassifier.set("javadoc") + description = "Assembles a JAR of the generated Javadoc." + group = JavaBasePlugin.DOCUMENTATION_GROUP +} + +tasks { + withType { + reports { + xml.isEnabled = true + html.isEnabled = true + } + } + + withType { + kotlinOptions.jvmTarget = "1.8" + } + + withType { + destination = file("$projectDir/pom.xml") + } + + assemble { + dependsOn(sourcesJar, javadocJar) + } + + clean { + doLast { + project.delete(fileTree(deployDir)) + } + } + + dokka { + outputFormat = "html" + outputDirectory = "$buildDir/javadoc" + + configuration { + sourceLink { + path = "src/main/kotlin" + url = "https://github.com/ethauvin/${project.name}/tree/master/src/main/kotlin" + lineSuffix = "#L" + } + + jdkVersion = 8 + + includes = listOf("config/dokka/packages.md") + includeNonPublic = false + } + } + + val copyToDeploy by registering(Copy::class) { + from(configurations.runtime) { + exclude("annotations-*.jar") + } + from(jar) + into(deployDir) + } + + register("deploy") { + description = "Copies all needed files to the $deployDir directory." + group = PublishingPlugin.PUBLISH_TASK_GROUP + dependsOn("build", "jar") + outputs.dir(deployDir) + inputs.files(copyToDeploy) + mustRunAfter("clean") + } + + val gitIsDirty by registering(Exec::class) { + description = "Fails if git has uncommitted changes." + group = "verification" + commandLine("git", "diff", "--quiet", "--exit-code") + } + + val gitTag by registering(Exec::class) { + description = "Tags the local repository with version ${project.version}" + group = PublishingPlugin.PUBLISH_TASK_GROUP + dependsOn(gitIsDirty) + if (isRelease) { + commandLine("git", "tag", "-a", project.version, "-m", "Version ${project.version}") + } + } + + val bintrayUpload by existing(BintrayUploadTask::class) { + dependsOn(publishToMavenLocal, gitTag) + } + + register("release") { + description = "Publishes version ${project.version} to Bintray." + group = PublishingPlugin.PUBLISH_TASK_GROUP + dependsOn("wrapper", bintrayUpload) + } + + "sonarqube" { + dependsOn("jacocoTestReport") + } +} + +fun findProperty(s: String) = project.findProperty(s) as String? +bintray { + user = findProperty("bintray.user") + key = findProperty("bintray.apikey") + publish = isRelease + setPublications(publicationName) + pkg.apply { + repo = "maven" + name = project.name + desc = description + websiteUrl = mavenUrl + issueTrackerUrl = "$mavenUrl/issues" + githubRepo = gitHub + githubReleaseNotesFile = "README.md" + vcsUrl = "$mavenUrl.git" + setLabels("kotlin", "java", "bitly", "bit.ly", "shorten", "url", "link", "bitlink") + publicDownloadNumbers = true + version.apply { + name = project.version as String + desc = description + vcsTag = project.version as String + gpg.apply { + sign = true + } + } + } +} + +publishing { + publications { + create(publicationName) { + from(components["java"]) + artifact(sourcesJar) + artifact(javadocJar) + pom.withXml { + asNode().apply { + appendNode("name", project.name) + appendNode("description", project.description) + appendNode("url", mavenUrl) + + appendNode("licenses").appendNode("license").apply { + appendNode("name", "BSD 3-Clause") + appendNode("url", "https://opensource.org/licenses/BSD-3-Clause") + } + + appendNode("developers").appendNode("developer").apply { + appendNode("id", "ethauvin") + appendNode("name", "Erik C. Thauvin") + appendNode("email", "erik@thauvin.net") + } + + appendNode("scm").apply { + appendNode("connection", "scm:git:$mavenUrl.git") + appendNode("developerConnection", "scm:git:git@github.com:$gitHub.git") + appendNode("url", mavenUrl) + } + + appendNode("issueManagement").apply { + appendNode("system", "GitHub") + appendNode("url", "$mavenUrl/issues") + } + } + } + } + } +} diff --git a/detekt-baseline.xml b/detekt-baseline.xml new file mode 100644 index 0000000..02dd689 --- /dev/null +++ b/detekt-baseline.xml @@ -0,0 +1,10 @@ + + + + + FunctionParameterNaming:Bitly.kt$Bitly$group_guid: String = "" + FunctionParameterNaming:Bitly.kt$Bitly$long_url: String + NestedBlockDepth:Bitly.kt$Bitly$executeCall + NestedBlockDepth:Bitly.kt$Bitly$shorten + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b7c8c5d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..2fe81a7 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..62bd9b9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@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 + +@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=. +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%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..9ed603b --- /dev/null +++ b/settings.gradle.kts @@ -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 manual at https://docs.gradle.org/6.2/userguide/multi_project_builds.html + */ + +rootProject.name = "bitly-shorten" diff --git a/src/main/kotlin/net/thauvin/erik/bitly/Bitly.kt b/src/main/kotlin/net/thauvin/erik/bitly/Bitly.kt new file mode 100644 index 0000000..7116310 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/bitly/Bitly.kt @@ -0,0 +1,252 @@ +package net.thauvin.erik.bitly + +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.logging.HttpLoggingInterceptor +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.net.MalformedURLException +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import java.util.Properties +import java.util.logging.Level +import java.util.logging.Logger + +/** + * The HTTP methods. + */ +enum class Methods { + DELETE, GET, PATCH, POST +} + +/** + * A simple implementation of the Bitly API v4. + * + * @constructor Creates new instance. + */ +open class Bitly() { + /** Constants for this package. **/ + object Constants { + /** The Bitly API base URL. **/ + const val API_BASE_URL = "https://api-ssl.bitly.com/v4" + + /** The API access token environment variable. **/ + const val ENV_ACCESS_TOKEN = "BITLY_ACCESS_TOKEN" + } + + /** The API access token. **/ + var accessToken: String = System.getenv(Constants.ENV_ACCESS_TOKEN) ?: "" + + /** The logger instance. **/ + val logger: Logger by lazy { Logger.getLogger(Bitly::class.java.simpleName) } + + private var client: OkHttpClient + + init { + if (logger.isLoggable(Level.FINE)) { + val httpLoggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + redactHeader("Authorization") + } + client = OkHttpClient.Builder().addInterceptor(httpLoggingInterceptor).build() + } else { + client = OkHttpClient.Builder().build() + } + } + + /** + * Creates a new instance using an [API Access Token][accessToken]. + * + * @param accessToken The API access token. + */ + @Suppress("unused") + constructor(accessToken: String) : this() { + this.accessToken = accessToken + } + + /** + * Creates a new instance using a [Properties][properties] and [Property Key][key]. + * + * @param properties The properties. + * @param key The property key. + */ + @Suppress("unused") + @JvmOverloads + constructor(properties: Properties, key: String = Constants.ENV_ACCESS_TOKEN) : this() { + accessToken = properties.getProperty(key, accessToken) + } + + /** + * 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 = Constants.ENV_ACCESS_TOKEN) : this() { + if (Files.exists(propertiesFilePath)) { + accessToken = Properties().apply { + Files.newInputStream(propertiesFilePath).use { nis -> + load(nis) + } + }.getProperty(key, accessToken) + } + } + + /** + * 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_ACCESS_TOKEN) : this(propertiesFile.toPath(), key) + + /** + * Builds the full API endpoint URL using the [Constants.API_BASE_URL]. + * + * @param endPointPath The REST method path. (eg. `/shorten', '/user`) + */ + fun buildEndPointUrl(endPointPath: String): String { + return if (endPointPath.startsWith('/')) { + "${Constants.API_BASE_URL}$endPointPath" + } else { + "${Constants.API_BASE_URL}/$endPointPath" + } + } + + /** + * Executes an API call. + * + * @param endPoint The API endpoint. (eg. `/shorten`, `/user`) + * @param params The request parameters kev/value map. + * @param method The submission [Method][Methods]. + * @return The response (JSON) from the API. + */ + fun executeCall(endPoint: String, params: Map, method: Methods = Methods.POST): String { + var returnValue = "" + if (endPoint.isBlank()) { + logger.severe("Please specify a valid API endpoint.") + } else if (accessToken.isBlank()) { + logger.severe("Please specify a valid API access token.") + } else { + val apiUrl = endPoint.toHttpUrlOrNull() + val builder: Request.Builder + if (apiUrl != null) { + if (method == Methods.POST || method == Methods.PATCH) { + val formBody = JSONObject(params).toString() + .toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) + builder = Request.Builder().apply { + url(apiUrl.newBuilder().build()) + if (method == Methods.POST) + post(formBody) + else + patch(formBody) + } + } else if (method == Methods.DELETE) { + builder = Request.Builder().url(apiUrl.newBuilder().build()).delete() + } else { + val httpUrl = apiUrl.newBuilder().apply { + params.forEach { + addQueryParameter(it.key, it.value) + } + }.build() + builder = Request.Builder().url(httpUrl) + } + builder.addHeader("Authorization", "Bearer $accessToken") + + val result = client.newCall(builder.build()).execute() + + val body = result.body?.string() + if (body != null) { + if (!result.isSuccessful && body.isNotEmpty()) { + logApiError(body, result.code) + } + returnValue = body + } + } + } + + return returnValue + } + + /** + * Shortens a long URL. + * + * See the [Bit.ly API](https://dev.bitly.com/v4/#operation/createBitlink) for more information. + * + * @param long_url The long URL. + * @param group_guid The group UID. + * @param domain The domain, defaults to `bit.ly`. + * @param isJson Returns the full JSON API response if `true` + * @return THe short URL or JSON API response. + */ + @JvmOverloads + fun shorten(long_url: String, group_guid: String = "", domain: String = "", isJson: Boolean = false): String { + var returnValue = if (isJson) "{}" else "" + if (!validateUrl(long_url)) { + logger.severe("Please specify a valid URL to shorten.") + } else { + val params: HashMap = HashMap() + if (group_guid.isNotBlank()) + params["group_guid"] = group_guid + if (domain.isNotBlank()) + params["domain"] = domain + params["long_url"] = long_url + + val response = executeCall(buildEndPointUrl("/shorten"), params) + + if (response.isNotEmpty()) { + if (isJson) { + returnValue = response + } else { + try { + val json = JSONObject(response) + if (json.has("link")) + returnValue = json.getString("link") + } catch (ignore: JSONException) { + logger.severe("An error occurred parsing the response from bitly.") + } + } + } + } + + return returnValue + } + + private fun logApiError(body: String, resultCode: Int) { + try { + val jsonResponse = JSONObject(body) + if (jsonResponse.has("message")) { + logger.severe(jsonResponse.getString("message") + " ($resultCode)") + } + if (jsonResponse.has("description")) { + val description = jsonResponse.getString("description") + if (description.isNotBlank()) { + logger.severe(description) + } + } + } catch (ignore: JSONException) { + logger.severe("An error occurred parsing the error response from bitly.") + } + } + + private fun validateUrl(url: String): Boolean { + var isValid = url.isNotBlank() + if (isValid) { + try { + URL(url) + } catch (e: MalformedURLException) { + logger.log(Level.FINE, "Invalid URL: $url", e) + isValid = false + } + } + return isValid + } +} diff --git a/src/test/kotlin/net/thauvin/erik/bitly/BitlyTest.kt b/src/test/kotlin/net/thauvin/erik/bitly/BitlyTest.kt new file mode 100644 index 0000000..32e308a --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/bitly/BitlyTest.kt @@ -0,0 +1,72 @@ +package net.thauvin.erik.bitly + +import org.junit.Before +import java.io.File +import java.io.FileInputStream +import java.util.Properties +import java.util.logging.Level +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +fun getKey(key: String): String { + var value = System.getenv(key) ?: "" + if (value.isBlank()) { + val localProps = File("local.properties") + if (localProps.exists()) + localProps.apply { + if (exists()) { + FileInputStream(this).use { fis -> + Properties().apply { + load(fis) + value = getProperty(key, "") + } + } + } + } + } + return value +} + +class BitlyTest { + private val bitly = Bitly(getKey(Bitly.Constants.ENV_ACCESS_TOKEN)) + + @Before + fun before() { + with(bitly.logger) { + level = Level.FINE + } + } + + @Test + fun `token should be specified`() { + val test = Bitly() + assertEquals("", test.shorten("https://erik.thauvin.net/blog/")) + } + + @Test + fun `token should be valid`() { + val test = Bitly().apply { accessToken = "12345679" } + assertEquals("{\"message\":\"FORBIDDEN\"}", test.shorten("https://erik.thauvin.net/blog", isJson = true)) + } + + @Test + fun `long url should be valid`() { + assertEquals("", bitly.shorten("")) + } + + @Test + fun `blog should be valid`() { + assertEquals("http://bit.ly/2SVHsnd", bitly.shorten("https://erik.thauvin.net/blog/", domain = "bit.ly")) + } + + @Test + fun `blog as json`() { + assertTrue(bitly.shorten("https://erik.thauvin.net/blog/", isJson = true).startsWith("{\"created_at\":")) + } + + @Test + fun `get user`() { + assertTrue(bitly.executeCall(bitly.buildEndPointUrl("user"), emptyMap(), Methods.GET).contains("\"login\":")) + } +} diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..ec4f9fe --- /dev/null +++ b/version.properties @@ -0,0 +1,8 @@ +#Generated by the Semver Plugin for Gradle +#Mon Feb 24 17:40:17 PST 2020 +version.buildmeta= +version.major=0 +version.minor=9 +version.patch=0 +version.prerelease=beta +version.semver=0.9.0-beta