Compare commits

..

26 commits

Author SHA1 Message Date
e45fc71ab9 Updated dependencies 2023-06-05 12:10:51 -07:00
090ccbff18
Merge pull request #4 from aSemy/refactor/split_lib_app
split up app/lib
2023-06-05 11:55:32 -07:00
Adam
9b34b5684c tidy up sonar config 2023-06-04 21:10:11 +02:00
Adam
9be2d3897e use sonar {} extension instead of sonarqube {} 2023-06-04 21:01:41 +02:00
Adam
231bca79fb Merge branch 'master' into refactor/split_lib_app 2023-06-04 20:40:23 +02:00
Adam
6dbd5f7e15 Merge branch 'master' into refactor/split_lib_app 2023-06-04 20:36:06 +02:00
Adam
1986a8e56d try only adding sonar to :lib 2023-06-04 17:47:18 +02:00
Adam
f1622d0c0a try only setting sonar properties on the root project 2023-06-04 17:24:12 +02:00
Adam
c1efd8a955 don't skip :lib? 2023-06-04 17:18:29 +02:00
Adam
97a23a195f try skipping projects... 2023-06-04 16:57:29 +02:00
Adam
76750cfd71 try adding sonar to the root project again... 2023-06-04 16:38:17 +02:00
Adam
3b6fffdc52 remove Kover & Sonar from base project 2023-06-02 09:43:57 +02:00
Adam
09a58f4142 remove SonarQube from Kotlin/JVM convention 2023-06-02 09:40:15 +02:00
Adam
13c73903c3 only enable SonarQube in :app 2023-06-02 09:33:55 +02:00
Adam
86ae661788 enable verbose sonar to try and help debugging 2023-06-02 09:22:48 +02:00
Adam
bccfd0003f enable sonarqube on Kotlin projects 2023-06-02 09:15:31 +02:00
Adam
e53dc8c546 only run sonar task on the root project 2023-06-02 09:13:17 +02:00
Adam
7ddfa061c6 tidy up/add some docs 2023-06-02 09:13:09 +02:00
Adam
78403becf2 refactor sonar build config 2023-06-02 08:49:55 +02:00
228b3fb4e3
Merge branch 'master' into refactor/split_lib_app 2023-05-31 15:33:15 -07:00
Adam
12c1c4f12a tidy fatJar task 2023-05-31 23:46:57 +02:00
Adam
62cc110651 fix base archive name by adding -lib suffix 2023-05-31 23:30:51 +02:00
Adam
fc3a5648ed tidy build config, update pom.xmls 2023-05-31 23:27:56 +02:00
Adam
1b40145970 fix main class val 2023-05-31 23:26:04 +02:00
Adam
ae8ffb91bb tidy up build config files 2023-05-31 23:24:41 +02:00
Adam
b2316b0029 split up app/lib 2023-05-31 23:21:34 +02:00
17 changed files with 479 additions and 185 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

@ -5,8 +5,8 @@ plugins {
dependencies { dependencies {
implementation("com.github.ben-manes:gradle-versions-plugin:0.46.0") implementation("com.github.ben-manes:gradle-versions-plugin:0.46.0")
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.0") implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.0")
implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.8.10") implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.8.20")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
implementation("org.jetbrains.kotlinx:kover-gradle-plugin:0.7.0") implementation("org.jetbrains.kotlinx:kover-gradle-plugin:0.7.1")
implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.0.3129") implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.0.3129")
} }

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",
) )