Merge pull request #4 from aSemy/refactor/split_lib_app

split up app/lib
This commit is contained in:
Erik C. Thauvin 2023-06-05 11:55:32 -07:00 committed by GitHub
commit 090ccbff18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 477 additions and 183 deletions

View file

@ -46,4 +46,4 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew sonar --info run: ./gradlew sonar --info -Dsonar.verbose=true

113
app/build.gradle.kts Normal file
View file

@ -0,0 +1,113 @@
/*
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* 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
*
* http://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.
*/
import org.jetbrains.dokka.gradle.DokkaTask
plugins {
buildsrc.conventions.lang.`kotlin-jvm`
buildsrc.conventions.publishing
buildsrc.conventions.sonarqube
id("application")
id("com.github.ben-manes.versions")
}
description = "A simple defensive application to encode/decode URL components"
val deployDir = project.layout.projectDirectory.dir("deploy")
val urlEncoderMainClass = "net.thauvin.erik.urlencoder.UrlEncoder"
dependencies {
implementation(projects.lib)
kover(projects.lib)
// testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
}
base {
archivesName.set(rootProject.name)
}
application {
mainClass.set(urlEncoderMainClass)
}
tasks {
jar {
manifest {
attributes["Main-Class"] = urlEncoderMainClass
}
}
val fatJar by registering(Jar::class) {
group = LifecycleBasePlugin.BUILD_GROUP
dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources"))
archiveClassifier.set("all")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest { attributes(mapOf("Main-Class" to application.mainClass)) }
from(sourceSets.main.get().output)
dependsOn(configurations.runtimeClasspath)
from(configurations.runtimeClasspath.map { classpath ->
classpath.incoming.artifacts.artifactFiles.files.filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
build {
dependsOn(fatJar)
}
withType<GenerateMavenPom>().configureEach {
destination = file("$projectDir/pom.xml")
}
clean {
delete(deployDir)
}
withType<DokkaTask>().configureEach {
dokkaSourceSets {
named("main") {
moduleName.set("UrlEncoder Application")
}
}
}
val copyToDeploy by registering(Sync::class) {
group = PublishingPlugin.PUBLISH_TASK_GROUP
from(configurations.runtimeClasspath) {
exclude("annotations-*.jar")
}
from(jar)
into(deployDir)
}
register("deploy") {
description = "Copies all needed files to the 'deploy' directory."
group = PublishingPlugin.PUBLISH_TASK_GROUP
dependsOn(build, copyToDeploy)
}
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
artifactId = rootProject.name
artifact(tasks.javadocJar)
}
}
}

8
app/detekt-baseline.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexCondition:UrlEncoder.kt$UrlEncoder$hasOption &amp;&amp; args.size == 2 || !hasOption &amp;&amp; args.size == 1</ID>
<ID>MaxLineLength:UrlEncoder.kt$UrlEncoder$*</ID>
</CurrentIssues>
</SmellBaseline>

58
app/pom.xml Normal file
View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>net.thauvin.erik</groupId>
<artifactId>urlencoder</artifactId>
<version>1.3.1-SNAPSHOT</version>
<name>UrlEncoder for Kotlin</name>
<description>A simple defensive application to encode/decode URL components</description>
<url>https://github.com/ethauvin/urlencoder</url>
<licenses>
<license>
<name>The Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<developers>
<developer>
<id>gbevin</id>
<name>Geert Bevin</name>
<email>gbevin@uwyn.com</email>
<url>https://github.com/gbevin</url>
</developer>
<developer>
<id>ethauvin</id>
<name>Erik C. Thauvin</name>
<email>erik@thauvin.net</email>
<url>https://erik.thauvin.net/</url>
</developer>
</developers>
<scm>
<connection>scm:git://github.com/ethauvin/urlencoder.git</connection>
<developerConnection>scm:git@github.com:ethauvin/urlencoder.git</developerConnection>
<url>https://github.com/ethauvin/urlencoder</url>
</scm>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/ethauvin/urlencoder/issues</url>
</issueManagement>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>1.8.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.thauvin.erik</groupId>
<artifactId>urlencoder-lib</artifactId>
<version>1.3.1-SNAPSHOT</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,106 @@
/*
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* 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
*
* http://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.
*/
package net.thauvin.erik.urlencoder
import kotlin.system.exitProcess
/**
* Most defensive approach to URL encoding and decoding.
*
* - Rules determined by combining the unreserved character set from
* [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#page-13) with the percent-encode set from
* [application/x-www-form-urlencoded](https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set).
*
* - Both specs above support percent decoding of two hexadecimal digits to a binary octet, however their unreserved
* set of characters differs and `application/x-www-form-urlencoded` adds conversion of space to `+`, which has the
* potential to be misunderstood.
*
* - This library encodes with rules that will be decoded correctly in either case.
*
* @author Geert Bevin (gbevin(remove) at uwyn dot com)
* @author Erik C. Thauvin (erik@thauvin.net)
**/
object UrlEncoder {
internal val usage =
"Usage : java -jar urlencoder-*all.jar [-ed] text" + System.lineSeparator() +
"Encode and decode URL components defensively." + System.lineSeparator() +
" -e encode (default) " + System.lineSeparator() +
" -d decode"
/**
* Encodes and decodes URLs from the command line.
*
* - `java -jar urlencoder-*all.jar [-ed] text`
*/
@JvmStatic
fun main(args: Array<String>) {
try {
val result = processMain(args)
if (result.status == 1) {
System.err.println(result.output)
} else {
println(result.output)
}
exitProcess(result.status)
} catch (e: IllegalArgumentException) {
System.err.println("${UrlEncoder::class.java.simpleName}: ${e.message}")
exitProcess(1)
}
}
internal data class MainResult(var output: String = usage, var status: Int = 1)
internal fun processMain(args: Array<String>): MainResult {
val result = MainResult()
if (args.isNotEmpty() && args[0].isNotEmpty()) {
val hasDecode = (args[0] == "-d")
val hasOption = (hasDecode || args[0] == "-e")
if (hasOption && args.size == 2 || !hasOption && args.size == 1) {
val arg = if (hasOption) args[1] else args[0]
if (hasDecode) {
result.output = decode(arg)
} else {
result.output = UrlEncoderUtil.encode(arg)
}
result.status = 0
}
}
return result
}
/**
* Transforms a provided [String] into a new string, containing decoded URL characters in the UTF-8
* encoding.
*/
@JvmStatic
@JvmOverloads
fun decode(source: String, plusToSpace: Boolean = false): String =
// delegate to UrlEncoderFunctions for backwards compatibility
UrlEncoderUtil.decode(source, plusToSpace)
/**
* Transforms a provided [String] object into a new string, containing only valid URL characters in the UTF-8
* encoding.
*
* - Letters, numbers, unreserved (`_-!.'()*`) and allowed characters are left intact.
*/
@JvmStatic
@JvmOverloads
fun encode(source: String, allow: String = "", spaceToPlus: Boolean = false): String =
UrlEncoderUtil.encode(source, allow, spaceToPlus)
}

View file

@ -17,14 +17,10 @@
package net.thauvin.erik.urlencoder package net.thauvin.erik.urlencoder
import net.thauvin.erik.urlencoder.UrlEncoder.decode
import net.thauvin.erik.urlencoder.UrlEncoder.encode
import net.thauvin.erik.urlencoder.UrlEncoder.processMain import net.thauvin.erik.urlencoder.UrlEncoder.processMain
import net.thauvin.erik.urlencoder.UrlEncoder.usage import net.thauvin.erik.urlencoder.UrlEncoder.usage
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
@ -34,7 +30,6 @@ import org.junit.jupiter.params.provider.ValueSource
import java.util.stream.Stream import java.util.stream.Stream
class UrlEncoderTest { class UrlEncoderTest {
private val same = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_."
companion object { companion object {
@JvmStatic @JvmStatic
@ -54,67 +49,6 @@ class UrlEncoderTest {
) )
} }
@ParameterizedTest(name = "decode({0}) should be {1}")
@MethodSource("validMap")
fun `Decode URL`(expected: String, source: String) {
assertEquals(expected, decode(source))
}
@ParameterizedTest(name = "decode({0})")
@MethodSource("invalid")
fun `Decode with Exception`(source: String) {
assertThrows(IllegalArgumentException::class.java, { decode(source) }, "decode($source)")
}
@Test
fun `Decode when None needed`() {
assertSame(same, decode(same))
assertEquals("", decode(""), "decode('')")
assertEquals(" ", decode(" "), "decode(' ')")
}
@Test
fun `Decode with Plus to Space`() {
assertEquals("foo bar", decode("foo+bar", true))
assertEquals("foo bar foo", decode("foo+bar++foo", true))
assertEquals("foo bar foo", decode("foo+%20bar%20+foo", true))
assertEquals("foo + bar", decode("foo+%2B+bar", plusToSpace = true))
assertEquals("foo+bar", decode("foo%2Bbar", plusToSpace = true))
}
@ParameterizedTest(name = "encode({0}) should be {1}")
@MethodSource("validMap")
fun `Encode URL`(source: String, expected: String) {
assertEquals(expected, encode(source))
}
@Test
fun `Encode Empty or Blank`() {
assertTrue(encode("", allow = "").isEmpty(), "encode('','')")
assertEquals("", encode(""), "encode('')")
assertEquals("%20", encode(" "), "encode(' ')")
}
@Test
fun `Encode when None needed`() {
assertSame(same, encode(same))
assertSame(same, encode(same, allow = ""), "with empty allow")
}
@Test
fun `Encode with Allow Arg`() {
assertEquals("?test=a%20test", encode("?test=a test", allow = "=?"), "encode(x, =?)")
assertEquals("aaa", encode("aaa", "a"), "encode(aaa, a)")
assertEquals(" ", encode(" ", " "), "encode(' ', ' ')")
}
@Test
fun `Encode with Space to Plus`() {
assertEquals("foo+bar", encode("foo bar", spaceToPlus = true))
assertEquals("foo+bar++foo", encode("foo bar foo", spaceToPlus = true))
assertEquals("foo bar", encode("foo bar", " ", true))
}
@ParameterizedTest(name = "processMain(-d {1}) should be {0}") @ParameterizedTest(name = "processMain(-d {1}) should be {0}")
@MethodSource("validMap") @MethodSource("validMap")
fun `Main Decode`(expected: String, source: String) { fun `Main Decode`(expected: String, source: String) {

View file

@ -17,6 +17,7 @@
plugins { plugins {
buildsrc.conventions.base buildsrc.conventions.base
buildsrc.conventions.sonarqube
} }
group = "net.thauvin.erik" group = "net.thauvin.erik"

View file

@ -1,15 +1,10 @@
package buildsrc.conventions.lang package buildsrc.conventions.lang
import buildsrc.utils.Rife2TestListener import buildsrc.utils.Rife2TestListener
import org.gradle.api.JavaVersion
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.sonarqube.gradle.SonarTask
/** /**
* Common configuration for Kotlin/JVM projects * Common configuration for Kotlin/JVM projects
@ -20,7 +15,8 @@ import org.sonarqube.gradle.SonarTask
plugins { plugins {
id("buildsrc.conventions.base") id("buildsrc.conventions.base")
kotlin("jvm") kotlin("jvm")
id("buildsrc.conventions.code-quality") id("io.gitlab.arturbosch.detekt")
id("org.jetbrains.kotlinx.kover")
} }
java { java {
@ -39,4 +35,11 @@ tasks.withType<KotlinCompile>().configureEach {
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
useJUnitPlatform() useJUnitPlatform()
val testsBadgeApiKey = providers.gradleProperty("testsBadgeApiKey")
addTestListener(Rife2TestListener(testsBadgeApiKey))
testLogging {
exceptionFormat = TestExceptionFormat.FULL
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
} }

View file

@ -1,5 +1,8 @@
package buildsrc.conventions.lang package buildsrc.conventions.lang
import buildsrc.utils.Rife2TestListener
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
@ -13,6 +16,8 @@ import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
plugins { plugins {
id("buildsrc.conventions.base") id("buildsrc.conventions.base")
kotlin("multiplatform") kotlin("multiplatform")
id("io.gitlab.arturbosch.detekt")
id("org.jetbrains.kotlinx.kover")
} }
@ -43,3 +48,12 @@ kotlin {
} }
} }
} }
tasks.withType<Test>().configureEach {
val testsBadgeApiKey = providers.gradleProperty("testsBadgeApiKey")
addTestListener(Rife2TestListener(testsBadgeApiKey))
testLogging {
exceptionFormat = TestExceptionFormat.FULL
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
}

View file

@ -36,7 +36,7 @@ publishing {
licenses { licenses {
license { license {
name.set("The Apache License, Version 2.0") name.set("The Apache License, Version 2.0")
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
} }
} }
developers { developers {
@ -84,6 +84,8 @@ signing {
sign(publishing.publications) sign(publishing.publications)
setRequired({ setRequired({
// only enable signing for non-snapshot versions, or when publishing to a non-local repo, otherwise
// publishing to Maven Local requires signing for users without access to the signing key.
!isSnapshotVersion() || gradle.taskGraph.hasTask("publish") !isSnapshotVersion() || gradle.taskGraph.hasTask("publish")
}) })
} }
@ -99,6 +101,7 @@ tasks.withType<AbstractPublishToMaven>().configureEach {
} }
val javadocJar by tasks.registering(Jar::class) { val javadocJar by tasks.registering(Jar::class) {
description = "Generate Javadoc using Dokka"
dependsOn(tasks.dokkaJavadoc) dependsOn(tasks.dokkaJavadoc)
from(tasks.dokkaJavadoc) from(tasks.dokkaJavadoc)
archiveClassifier.set("javadoc") archiveClassifier.set("javadoc")

View file

@ -17,18 +17,23 @@
package buildsrc.conventions package buildsrc.conventions
import buildsrc.utils.Rife2TestListener
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.sonarqube.gradle.SonarTask import org.sonarqube.gradle.SonarTask
/**
* Convention plugin for SonarQube analysis.
*
* SonarQube depends on an aggregated XML coverage report from
* [Kotlinx Kover](https://github.com/Kotlin/kotlinx-kover).
* See the Kover docs for
* [how to aggregate coverage reports](https://kotlin.github.io/kotlinx-kover/gradle-plugin/#multiproject-build).
*/
plugins { plugins {
id("org.sonarqube") id("org.sonarqube")
id("io.gitlab.arturbosch.detekt")
id("org.jetbrains.kotlinx.kover") id("org.jetbrains.kotlinx.kover")
} }
sonarqube { sonar {
properties { properties {
property("sonar.projectName", rootProject.name) property("sonar.projectName", rootProject.name)
property("sonar.projectKey", "ethauvin_${rootProject.name}") property("sonar.projectKey", "ethauvin_${rootProject.name}")
@ -43,14 +48,6 @@ sonarqube {
} }
tasks.withType<SonarTask>().configureEach { tasks.withType<SonarTask>().configureEach {
// workaround for https://github.com/Kotlin/kotlinx-kover/issues/394
dependsOn(tasks.matching { it.name == "koverXmlReport" }) dependsOn(tasks.matching { it.name == "koverXmlReport" })
} }
tasks.withType<Test>().configureEach {
val testsBadgeApiKey = providers.gradleProperty("testsBadgeApiKey")
addTestListener(Rife2TestListener(testsBadgeApiKey))
testLogging {
exceptionFormat = TestExceptionFormat.FULL
events = setOf(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
}

View file

@ -1,17 +1,32 @@
import org.jetbrains.dokka.gradle.DokkaTask /*
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* 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
*
* http://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.
*/
import org.jetbrains.dokka.gradle.DokkaTask
plugins { plugins {
buildsrc.conventions.lang.`kotlin-jvm` buildsrc.conventions.lang.`kotlin-jvm`
buildsrc.conventions.publishing buildsrc.conventions.publishing
id("application") buildsrc.conventions.sonarqube
id("com.github.ben-manes.versions") id("com.github.ben-manes.versions")
} }
description = "A simple defensive library to encode/decode URL components" description = "A simple defensive library to encode/decode URL components"
val deployDir = project.layout.projectDirectory.dir("deploy") val deployDir = project.layout.projectDirectory.dir("deploy")
val urlEncoderMainClass = "net.thauvin.erik.urlencoder.UrlEncoder"
dependencies { dependencies {
// testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") // testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25")
@ -19,50 +34,27 @@ dependencies {
} }
base { base {
archivesName.set(rootProject.name) archivesName.set("${rootProject.name}-lib")
}
application {
mainClass.set(urlEncoderMainClass)
} }
tasks { tasks {
jar {
manifest {
attributes["Main-Class"] = urlEncoderMainClass
}
}
val fatJar by registering(Jar::class) {
group = LifecycleBasePlugin.BUILD_GROUP
dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources"))
archiveClassifier.set("all")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest { attributes(mapOf("Main-Class" to application.mainClass)) }
val sourcesMain = sourceSets.main.get()
val contents = configurations.runtimeClasspath.get()
.map { if (it.isDirectory) it else zipTree(it) } + sourcesMain.output
from(contents)
}
build {
dependsOn(fatJar)
}
withType<GenerateMavenPom>().configureEach { withType<GenerateMavenPom>().configureEach {
destination = file("$projectDir/pom.xml") destination = file("$projectDir/pom.xml")
} }
clean {
delete(deployDir)
}
withType<DokkaTask>().configureEach { withType<DokkaTask>().configureEach {
dokkaSourceSets { dokkaSourceSets {
named("main") { named("main") {
moduleName.set("UrlEncoder API") moduleName.set("UrlEncoder Library")
} }
} }
} }
val copyToDeploy by registering(Sync::class) { val copyToDeploy by registering(Sync::class) {
description = "Copies all needed files to the 'deploy' directory."
group = PublishingPlugin.PUBLISH_TASK_GROUP group = PublishingPlugin.PUBLISH_TASK_GROUP
from(configurations.runtimeClasspath) { from(configurations.runtimeClasspath) {
exclude("annotations-*.jar") exclude("annotations-*.jar")
@ -76,17 +68,13 @@ tasks {
group = PublishingPlugin.PUBLISH_TASK_GROUP group = PublishingPlugin.PUBLISH_TASK_GROUP
dependsOn(build, copyToDeploy) dependsOn(build, copyToDeploy)
} }
clean {
delete(deployDir)
}
} }
publishing { publishing {
publications { publications {
create<MavenPublication>("mavenJava") { create<MavenPublication>("mavenJava") {
from(components["java"]) from(components["java"])
artifactId = rootProject.name artifactId = "${rootProject.name}-lib"
artifact(tasks.javadocJar) artifact(tasks.javadocJar)
} }
} }

View file

@ -2,14 +2,13 @@
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues/> <ManuallySuppressedIssues/>
<CurrentIssues> <CurrentIssues>
<ID>ComplexCondition:UrlEncoder.kt$UrlEncoder$hasOption &amp;&amp; args.size == 2 || !hasOption &amp;&amp; args.size == 1</ID> <ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$0x80</ID>
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$0x80</ID> <ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$0xFF</ID>
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$0xFF</ID> <ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$16</ID>
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$16</ID> <ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$3</ID>
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$3</ID> <ID>MagicNumber:UrlEncoderUtil.kt$UrlEncoderUtil$4</ID>
<ID>MagicNumber:UrlEncoder.kt$UrlEncoder$4</ID> <ID>MaxLineLength:UrlEncoderUtil.kt$UrlEncoderUtil$*</ID>
<ID>MaxLineLength:UrlEncoder.kt$UrlEncoder$*</ID> <ID>NestedBlockDepth:UrlEncoderUtil.kt$UrlEncoderUtil$@JvmStatic @JvmOverloads fun decode(source: String, plusToSpace: Boolean = false): String</ID>
<ID>NestedBlockDepth:UrlEncoder.kt$UrlEncoder$@JvmStatic @JvmOverloads fun decode(source: String, plusToSpace: Boolean = false): String</ID> <ID>NestedBlockDepth:UrlEncoderUtil.kt$UrlEncoderUtil$@JvmStatic @JvmOverloads fun encode(source: String, allow: String = "", spaceToPlus: Boolean = false): String</ID>
<ID>NestedBlockDepth:UrlEncoder.kt$UrlEncoder$@JvmStatic @JvmOverloads fun encode(source: String, allow: String = "", spaceToPlus: Boolean = false): String</ID>
</CurrentIssues> </CurrentIssues>
</SmellBaseline> </SmellBaseline>

View file

@ -19,7 +19,6 @@ package net.thauvin.erik.urlencoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.BitSet import java.util.BitSet
import kotlin.system.exitProcess
/** /**
* Most defensive approach to URL encoding and decoding. * Most defensive approach to URL encoding and decoding.
@ -37,13 +36,8 @@ import kotlin.system.exitProcess
* @author Geert Bevin (gbevin(remove) at uwyn dot com) * @author Geert Bevin (gbevin(remove) at uwyn dot com)
* @author Erik C. Thauvin (erik@thauvin.net) * @author Erik C. Thauvin (erik@thauvin.net)
**/ **/
object UrlEncoder { object UrlEncoderUtil {
private val hexDigits = "0123456789ABCDEF".toCharArray() private val hexDigits = "0123456789ABCDEF".toCharArray()
internal val usage =
"Usage : java -jar urlencoder-*all.jar [-ed] text" + System.lineSeparator() +
"Encode and decode URL components defensively." + System.lineSeparator() +
" -e encode (default) " + System.lineSeparator() +
" -d decode"
// see https://www.rfc-editor.org/rfc/rfc3986#page-13 // see https://www.rfc-editor.org/rfc/rfc3986#page-13
// and https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set // and https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
@ -200,45 +194,4 @@ object UrlEncoder {
return out?.toString() ?: source return out?.toString() ?: source
} }
/**
* Encodes and decodes URLs from the command line.
*
* - `java -jar urlencoder-*all.jar [-ed] text`
*/
@JvmStatic
fun main(args: Array<String>) {
try {
val result = processMain(args)
if (result.status == 1) {
System.err.println(result.output)
} else {
println(result.output)
}
exitProcess(result.status)
} catch (e: IllegalArgumentException) {
System.err.println("${UrlEncoder::class.java.simpleName}: ${e.message}")
exitProcess(1)
}
}
internal data class MainResult(var output: String = usage, var status: Int = 1)
internal fun processMain(args: Array<String>): MainResult {
val result = MainResult()
if (args.isNotEmpty() && args[0].isNotEmpty()) {
val hasDecode = (args[0] == "-d")
val hasOption = (hasDecode || args[0] == "-e")
if (hasOption && args.size == 2 || !hasOption && args.size == 1) {
val arg = if (hasOption) args[1] else args[0]
if (hasDecode) {
result.output = decode(arg)
} else {
result.output = encode(arg)
}
result.status = 0
}
}
return result
}
} }

View file

@ -0,0 +1,114 @@
/*
* Copyright 2001-2023 Geert Bevin (gbevin[remove] at uwyn dot com)
* Copyright 2022-2023 Erik C. Thauvin (erik@thauvin.net)
*
* 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
*
* http://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.
*/
package net.thauvin.erik.urlencoder
import net.thauvin.erik.urlencoder.UrlEncoderUtil.decode
import net.thauvin.erik.urlencoder.UrlEncoderUtil.encode
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.MethodSource
import java.util.stream.Stream
class UrlEncoderUtilTest {
private val same = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_."
companion object {
@JvmStatic
fun invalid() = arrayOf("sdkjfh%", "sdkjfh%6", "sdkjfh%xx", "sdfjfh%-1")
@JvmStatic
fun validMap(): Stream<Arguments> = Stream.of(
arguments("a test &", "a%20test%20%26"),
arguments(
"!abcdefghijklmnopqrstuvwxyz%%ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.~=",
"%21abcdefghijklmnopqrstuvwxyz%25%25ABCDEFGHIJKLMNOPQRSTUVQXYZ0123456789-_.%7E%3D"
),
arguments("%#okékÉȢ smile!😁", "%25%23ok%C3%A9k%C3%89%C8%A2%20smile%21%F0%9F%98%81"),
arguments(
"\uD808\uDC00\uD809\uDD00\uD808\uDF00\uD808\uDD00", "%F0%92%80%80%F0%92%94%80%F0%92%8C%80%F0%92%84%80"
)
)
}
@ParameterizedTest(name = "decode({0}) should be {1}")
@MethodSource("validMap")
fun `Decode URL`(expected: String, source: String) {
assertEquals(expected, decode(source))
}
@ParameterizedTest(name = "decode({0})")
@MethodSource("invalid")
fun `Decode with Exception`(source: String) {
assertThrows(IllegalArgumentException::class.java, { decode(source) }, "decode($source)")
}
@Test
fun `Decode when None needed`() {
assertSame(same, decode(same))
assertEquals("", decode(""), "decode('')")
assertEquals(" ", decode(" "), "decode(' ')")
}
@Test
fun `Decode with Plus to Space`() {
assertEquals("foo bar", decode("foo+bar", true))
assertEquals("foo bar foo", decode("foo+bar++foo", true))
assertEquals("foo bar foo", decode("foo+%20bar%20+foo", true))
assertEquals("foo + bar", decode("foo+%2B+bar", plusToSpace = true))
assertEquals("foo+bar", decode("foo%2Bbar", plusToSpace = true))
}
@ParameterizedTest(name = "encode({0}) should be {1}")
@MethodSource("validMap")
fun `Encode URL`(source: String, expected: String) {
assertEquals(expected, encode(source))
}
@Test
fun `Encode Empty or Blank`() {
assertTrue(encode("", allow = "").isEmpty(), "encode('','')")
assertEquals("", encode(""), "encode('')")
assertEquals("%20", encode(" "), "encode(' ')")
}
@Test
fun `Encode when None needed`() {
assertSame(same, encode(same))
assertSame(same, encode(same, allow = ""), "with empty allow")
}
@Test
fun `Encode with Allow Arg`() {
assertEquals("?test=a%20test", encode("?test=a test", allow = "=?"), "encode(x, =?)")
assertEquals("aaa", encode("aaa", "a"), "encode(aaa, a)")
assertEquals(" ", encode(" ", " "), "encode(' ', ' ')")
}
@Test
fun `Encode with Space to Plus`() {
assertEquals("foo+bar", encode("foo bar", spaceToPlus = true))
assertEquals("foo+bar++foo", encode("foo bar foo", spaceToPlus = true))
assertEquals("foo bar", encode("foo bar", " ", true))
}
}

View file

@ -20,6 +20,9 @@ dependencyResolutionManagement {
} }
} }
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include( include(
":app",
":lib", ":lib",
) )