diff --git a/build.gradle.kts b/build.gradle.kts index 3b63c4c..7748879 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,7 @@ plugins { id("net.researchgate.release") id("io.github.nstdio.http.ext.library-conventions") + id("io.github.nstdio.http.ext.test-conventions") id("io.github.nstdio.http.ext.quality-conventions") id("io.github.nstdio.http.ext.publish-conventions") } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 8c484e4..0dc1475 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -26,9 +26,9 @@ repositories { dependencies { implementation("de.jjohannes.gradle:extra-java-module-info:0.11") - implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3") implementation("io.github.gradle-nexus:publish-plugin:1.1.0") implementation("net.researchgate:gradle-release:2.8.1") + implementation("com.github.dpaukov:combinatoricslib3:3.3.3") } tasks.withType { diff --git a/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.library-conventions.gradle.kts b/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.library-conventions.gradle.kts index 025a53c..147c943 100644 --- a/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.library-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.library-conventions.gradle.kts @@ -16,109 +16,23 @@ @file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") -import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform -import java.lang.Boolean as JavaBoolean - plugins { `java-library` jacoco - id("de.jjohannes.extra-java-module-info") } -val isCI = System.getenv("CI").toBoolean() - group = "io.github.nstdio" java { withJavadocJar() withSourcesJar() - - sourceSets { - create("spiTest") { - compileClasspath += sourceSets.main.get().output - runtimeClasspath += sourceSets.main.get().output - } - } -} - -configurations.getByName("spiTestImplementation") { - extendsFrom(configurations.testImplementation.get()) -} - -mapOf( - "spiTestImplementation" to "testImplementation", - "spiTestRuntimeOnly" to "testRuntimeOnly", -).forEach { (t, u) -> - configurations.getByName(t) { extendsFrom(configurations.getByName(u)) } } -configurations.names - .filter { !setOf("compileClasspath", "runtimeClasspath").contains(it) } - .map { configurations.getByName(it) } - .forEach { - configure(listOf(it)) { - attributes { - @Suppress("UNCHECKED_CAST") - val forName = Class.forName("java.lang.Boolean") as Class - val value: JavaBoolean = JavaBoolean.valueOf("false") as JavaBoolean - - attribute(Attribute.of("javaModule", forName), value) - } - } - } - -val junitVersion = "5.8.2" -val commonIoVersion = "1.3.2" -val assertJVersion = "3.22.0" -val jsonPathAssertVersion = "2.7.0" -val slf4jVersion = "1.7.36" -val jacksonVersion = "2.13.2" -val brotli4JVersion = "1.6.0" -val brotliOrgVersion = "0.1.2" - -val spiDeps = listOf("org.brotli:dec:$brotliOrgVersion", "com.aayushatharva.brotli4j:brotli4j:$brotli4JVersion") +val lombokVersion = "1.18.22" dependencies { - api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - - compileOnly("org.projectlombok:lombok:1.18.22") - annotationProcessor("org.projectlombok:lombok:1.18.22") - - spiDeps.forEach { compileOnly(it) } - - /** AssertJ & Friends */ - testImplementation("org.assertj:assertj-core:$assertJVersion") - testImplementation("com.jayway.jsonpath:json-path-assert:$jsonPathAssertVersion") - - testImplementation("org.apache.commons:commons-io:$commonIoVersion") - - /** Jupiter */ - testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") - testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") - testImplementation("org.slf4j:slf4j-simple:$slf4jVersion") - testImplementation("org.awaitility:awaitility:4.2.0") - testImplementation("org.mockito:mockito-core:4.4.0") - - testImplementation("com.github.tomakehurst:wiremock-jre8:2.32.0") - testImplementation("com.tngtech.archunit:archunit-junit5:0.23.1") - - val spiTestImplementation = configurations.getByName("spiTestImplementation") - spiDeps.forEach { spiTestImplementation(it) } - spiTestImplementation("com.aayushatharva.brotli4j:native-${getArch()}:$brotli4JVersion") -} - -fun getArch(): String { - val operatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() - - if (operatingSystem.isWindows) return "windows-x86_64" - else if (operatingSystem.isMacOsX) return "osx-x86_64" - else if (operatingSystem.isLinux) - return if (DefaultNativePlatform.getCurrentArchitecture().isArm) "linux-aarch64" - else "linux-x86_64" - - return "" + compileOnly("org.projectlombok:lombok:$lombokVersion") + annotationProcessor("org.projectlombok:lombok:$lombokVersion") } tasks.withType().configureEach { @@ -128,42 +42,3 @@ tasks.withType().configureEach { options.encoding = "UTF-8" options.compilerArgs = listOf("-Xlint:all", "-Xlint:-deprecation") } - -tasks.withType { - useJUnitPlatform() - reports { - html.required.set(!isCI) - } - testLogging { - events("skipped", "failed") - exceptionFormat = FULL - } -} - -tasks.create("spiTest") { - description = "Run SPI tests" - group = "verification" - testClassesDirs = sourceSets.getByName("spiTest").output.classesDirs - classpath = sourceSets.getByName("spiTest").runtimeClasspath -} - -tasks.check { - dependsOn("spiTest") -} - -tasks.build { - dependsOn("spiTest") -} - -extraJavaModuleInfo { - module("brotli4j-${brotli4JVersion}.jar", "com.aayushatharva.brotli4j", brotli4JVersion) { - exports("com.aayushatharva.brotli4j") - exports("com.aayushatharva.brotli4j.common") - exports("com.aayushatharva.brotli4j.decoder") - exports("com.aayushatharva.brotli4j.encoder") - } - - module("dec-${brotliOrgVersion}.jar", "org.brotli.dec", brotliOrgVersion) { - exports("org.brotli.dec") - } -} diff --git a/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.quality-conventions.gradle.kts b/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.quality-conventions.gradle.kts index 1f7e9e0..ea5c32c 100644 --- a/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.quality-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.quality-conventions.gradle.kts @@ -16,15 +16,6 @@ plugins { jacoco - id("org.sonarqube") -} - -sonarqube { - properties { - property("sonar.projectKey", "nstdio_http-client-ext") - property("sonar.organization", "nstdio") - property("sonar.host.url", "https://sonarcloud.io") - } } jacoco { diff --git a/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.test-conventions.gradle.kts b/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.test-conventions.gradle.kts new file mode 100644 index 0000000..8739eee --- /dev/null +++ b/buildSrc/src/main/kotlin/io.github.nstdio.http.ext.test-conventions.gradle.kts @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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. + */ + +@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.configurationcache.extensions.capitalized +import org.gradle.kotlin.dsl.invoke +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.paukov.combinatorics3.Generator +import java.lang.Boolean +import kotlin.String +import kotlin.Suppress +import kotlin.to + +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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. + */ + +plugins { + `java-library` + id("de.jjohannes.extra-java-module-info") +} + +val isCI = System.getenv("CI").toBoolean() + +java { + sourceSets { + create("spiTest") { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().output + } + } +} + +val sourceSetsSpiTest by sourceSets.named("spiTest") +val spiTestImplementation by configurations + +mapOf( + "spiTestImplementation" to "testImplementation", + "spiTestRuntimeOnly" to "testRuntimeOnly", +).forEach { (t, u) -> + configurations.getByName(t) { extendsFrom(configurations.getByName(u)) } +} + +tasks.withType { + useJUnitPlatform() + reports { + html.required.set(!isCI) + } + testLogging { + events("skipped", "failed") + exceptionFormat = TestExceptionFormat.FULL + } +} + +val junitVersion = "5.8.2" +val commonIoVersion = "1.3.2" +val assertJVersion = "3.22.0" +val jsonPathAssertVersion = "2.7.0" +val slf4jVersion = "1.7.36" +val jacksonVersion = "2.13.2" +val brotli4JVersion = "1.6.0" +val brotliOrgVersion = "0.1.2" +val gsonVersion = "2.9.0" + +val jsonLibs = mapOf( + "jackson" to "com.fasterxml.jackson.core", + "gson" to "gson-$gsonVersion" +) + +val spiDeps = listOf( + "org.brotli:dec:$brotliOrgVersion", + "com.aayushatharva.brotli4j:brotli4j:$brotli4JVersion", + "com.google.code.gson:gson:$gsonVersion", + "javax.json.bind:javax.json.bind-api:1.0", + "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" +) + +dependencies { + spiDeps.forEach { compileOnly(it) } + + /** AssertJ & Friends */ + testImplementation("org.assertj:assertj-core:$assertJVersion") + testImplementation("com.jayway.jsonpath:json-path-assert:$jsonPathAssertVersion") + + testImplementation("org.apache.commons:commons-io:$commonIoVersion") + + /** Jupiter */ + testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") + testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + testImplementation("org.slf4j:slf4j-simple:$slf4jVersion") + testImplementation("org.awaitility:awaitility:4.2.0") + testImplementation("org.mockito:mockito-core:4.4.0") + + testImplementation("com.github.tomakehurst:wiremock-jre8:2.32.0") + testImplementation("com.tngtech.archunit:archunit-junit5:0.23.1") + + spiDeps.forEach { spiTestImplementation(it) } + spiTestImplementation("com.aayushatharva.brotli4j:native-${getArch()}:$brotli4JVersion") +} + +Generator.subset(jsonLibs.keys) + .simple() + .stream() + .forEach { it -> + val postFix = it.joinToString("And") { it.capitalized() } + val taskName = if (postFix.isEmpty()) "spiTest" else "spiTestWithout${postFix}" + tasks.create(taskName) { + description = "Run SPI tests" + group = "verification" + testClassesDirs = sourceSetsSpiTest.output.classesDirs + classpath = sourceSetsSpiTest.runtimeClasspath + + doFirst { + val toExclude = classpath.filter { file -> + it?.any { file.absolutePath.contains(it) } ?: false + } + if (!toExclude.isEmpty) { + logger.info("Excluding jars from classpath: ") + toExclude.forEach { + logger.info(" - ${it.name}") + } + } + classpath -= toExclude + } + } + } + +tasks.create("spiMatrixTest") { + description = "The aggregator task for all tests" + group = "verification" + dependsOn(tasks.filter { it.name.startsWith("spiTest") }) +} + +tasks.check { + dependsOn("spiMatrixTest") +} + +tasks.build { + dependsOn("spiMatrixTest") +} + +configurations.names + .filter { !setOf("compileClasspath", "runtimeClasspath").contains(it) } + .map { configurations.getByName(it) } + .forEach { + configure(listOf(it)) { + attributes { + @Suppress("UNCHECKED_CAST") + val forName = Class.forName("java.lang.Boolean") as Class + val value: Boolean = Boolean.valueOf("false") as Boolean + + attribute(Attribute.of("javaModule", forName), value) + } + } + } + +extraJavaModuleInfo { + module("brotli4j-${brotli4JVersion}.jar", "com.aayushatharva.brotli4j", brotli4JVersion) { + exports("com.aayushatharva.brotli4j") + exports("com.aayushatharva.brotli4j.common") + exports("com.aayushatharva.brotli4j.decoder") + exports("com.aayushatharva.brotli4j.encoder") + } + + module("dec-${brotliOrgVersion}.jar", "org.brotli.dec", brotliOrgVersion) { + exports("org.brotli.dec") + } +} + +fun getArch(): String { + val operatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() + + if (operatingSystem.isWindows) return "windows-x86_64" + else if (operatingSystem.isMacOsX) return "osx-x86_64" + else if (operatingSystem.isLinux) + return if (DefaultNativePlatform.getCurrentArchitecture().isArm) "linux-aarch64" + else "linux-x86_64" + + return "" +} \ No newline at end of file diff --git a/src/main/java/io/github/nstdio/http/ext/BodyHandlers.java b/src/main/java/io/github/nstdio/http/ext/BodyHandlers.java index a7afa15..50375ce 100644 --- a/src/main/java/io/github/nstdio/http/ext/BodyHandlers.java +++ b/src/main/java/io/github/nstdio/http/ext/BodyHandlers.java @@ -16,7 +16,6 @@ package io.github.nstdio.http.ext; -import com.fasterxml.jackson.core.type.TypeReference; import io.github.nstdio.http.ext.DecompressingBodyHandler.Options; import java.io.InputStream; @@ -56,17 +55,6 @@ public static BodyHandler> ofJson(Class targetType) { return responseInfo -> BodySubscribers.ofJson(targetType); } - /** - * Creates body handler to map JSON response to {@code targetType}. - * - * @param targetType The type. - * @param The required type. - * @return The JSON body handler. - */ - public static BodyHandler> ofJson(TypeReference targetType) { - return responseInfo -> BodySubscribers.ofJson(targetType); - } - /** * Creates new {@code DecompressingBodyHandlerBuilder} instance. * diff --git a/src/main/java/io/github/nstdio/http/ext/BodySubscribers.java b/src/main/java/io/github/nstdio/http/ext/BodySubscribers.java index be08d63..fcda43c 100644 --- a/src/main/java/io/github/nstdio/http/ext/BodySubscribers.java +++ b/src/main/java/io/github/nstdio/http/ext/BodySubscribers.java @@ -16,13 +16,8 @@ package io.github.nstdio.http.ext; -import static java.net.http.HttpResponse.BodySubscribers.mapping; -import static java.net.http.HttpResponse.BodySubscribers.ofInputStream; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.type.TypeFactory; +import io.github.nstdio.http.ext.spi.JsonMapping; +import io.github.nstdio.http.ext.spi.JsonMappingProvider; import java.io.IOException; import java.io.UncheckedIOException; @@ -30,46 +25,24 @@ import java.util.function.Supplier; import static java.net.http.HttpResponse.BodySubscribers.mapping; -import static java.net.http.HttpResponse.BodySubscribers.ofByteArray; +import static java.net.http.HttpResponse.BodySubscribers.ofInputStream; @SuppressWarnings("WeakerAccess") public final class BodySubscribers { - private static final TypeFactory TF = TypeFactory.defaultInstance(); - private BodySubscribers() { } public static BodySubscriber> ofJson(Class targetType) { - return ofJson(TF.constructType(targetType)); - } - - public static BodySubscriber> ofJson(TypeReference targetType) { - return ofJson(TF.constructType(targetType)); + return ofJson(JsonMappingProvider.provider().get(), targetType); } - public static BodySubscriber> ofJson(ObjectMapper objectMapper, Class targetType) { - return ofJson(objectMapper, objectMapper.getTypeFactory().constructType(targetType)); - } - - public static BodySubscriber> ofJson(ObjectMapper mapper, TypeReference targetType) { - return ofJson(mapper, mapper.getTypeFactory().constructType(targetType)); - } - - private static BodySubscriber> ofJson(JavaType targetType) { - return ofJson(ObjectMapperHolder.INSTANCE, targetType); - } - - private static BodySubscriber> ofJson(ObjectMapper objectMapper, JavaType targetType) { + public static BodySubscriber> ofJson(JsonMapping mapping, Class targetType) { return mapping(ofInputStream(), in -> () -> { try (var stream = in) { - return objectMapper.readValue(stream, targetType); + return mapping.read(stream, targetType); } catch (IOException e) { throw new UncheckedIOException(e); } }); } - - private static class ObjectMapperHolder { - private static final ObjectMapper INSTANCE = new ObjectMapper(); - } } diff --git a/src/main/java/io/github/nstdio/http/ext/Cache.java b/src/main/java/io/github/nstdio/http/ext/Cache.java index b20a8e4..6858862 100644 --- a/src/main/java/io/github/nstdio/http/ext/Cache.java +++ b/src/main/java/io/github/nstdio/http/ext/Cache.java @@ -16,6 +16,8 @@ package io.github.nstdio.http.ext; +import io.github.nstdio.http.ext.spi.Classpath; + import java.net.http.HttpRequest; import java.net.http.HttpResponse.BodySubscriber; import java.nio.ByteBuffer; @@ -37,11 +39,17 @@ static InMemoryCacheBuilder newInMemoryCacheBuilder() { } /** - * Creates a new {@code DiskCacheBuilder} instance. + * Creates a new {@code DiskCacheBuilder} instance. Requires Jackson form dumping cache files on disk. * * @return the new {@code DiskCacheBuilder}. + * + * @throws IllegalStateException When Jackson (a.k.a. ObjectMapper) is not in classpath. */ static DiskCacheBuilder newDiskCacheBuilder() { + if (!Classpath.isJacksonPresent()) { + throw new IllegalStateException("In order to use disk cache please add 'com.fasterxml.jackson.core:jackson-databind' to your dependencies"); + } + return new DiskCacheBuilder(); } @@ -58,13 +66,14 @@ static Cache noop() { * Gets the cache entry associated with {@code request}. * * @param request The request. + * * @return the cache entry associated with {@code request} or {@code null}. */ CacheEntry get(HttpRequest request); /** - * Associates {@code request} with {@code entry} and stores it in this cache. The previously (if any) associated - * entry will be evicted. + * Associates {@code request} with {@code entry} and stores it in this cache. The previously (if any) associated entry + * will be evicted. * * @param request The request. * @param entry The entry. @@ -94,6 +103,7 @@ static Cache noop() { * Gets the statistics for this cache. * * @return The statistics for this cache. + * * @see CacheStats */ CacheStats stats(); @@ -103,6 +113,7 @@ static Cache noop() { * * @param metadata The metadata. * @param The response body type. + * * @return a {@code Writer}. */ Writer writer(CacheEntryMetadata metadata); @@ -179,6 +190,7 @@ class DiskCacheBuilder extends ConstrainedCacheBuilder { * Sets the directory to store cache files. * * @param dir The directory to store cache files. + * * @return builder itself. */ public DiskCacheBuilder dir(Path dir) { diff --git a/src/main/java/io/github/nstdio/http/ext/DiskCache.java b/src/main/java/io/github/nstdio/http/ext/DiskCache.java index 84e3b1c..fa509e7 100644 --- a/src/main/java/io/github/nstdio/http/ext/DiskCache.java +++ b/src/main/java/io/github/nstdio/http/ext/DiskCache.java @@ -39,7 +39,7 @@ import static io.github.nstdio.http.ext.IOUtils.*; class DiskCache extends SizeConstrainedCache { - private final MetadataSerializer metadataSerializer = new JsonMetadataSerializer(); + private final MetadataSerializer metadataSerializer = new JacksonMetadataSerializer(); private final Executor executor; private final Path dir; diff --git a/src/main/java/io/github/nstdio/http/ext/JsonMetadataSerializer.java b/src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java similarity index 95% rename from src/main/java/io/github/nstdio/http/ext/JsonMetadataSerializer.java rename to src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java index 6e245f4..da4e7f3 100644 --- a/src/main/java/io/github/nstdio/http/ext/JsonMetadataSerializer.java +++ b/src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java @@ -48,7 +48,7 @@ import static java.net.http.HttpRequest.BodyPublishers.noBody; import static java.nio.file.StandardOpenOption.*; -class JsonMetadataSerializer implements MetadataSerializer { +class JacksonMetadataSerializer implements MetadataSerializer { private static final String FIELD_NAME_VERSION = "version"; private static final String FIELD_NAME_REQUEST_TIME = "requestTime"; @@ -60,7 +60,7 @@ class JsonMetadataSerializer implements MetadataSerializer { private final ObjectWriter writer; private final ObjectReader reader; - JsonMetadataSerializer() { + JacksonMetadataSerializer() { ObjectMapper mapper = createMapper(); writer = mapper.writerFor(CacheEntryMetadata.class); reader = mapper.readerFor(CacheEntryMetadata.class); @@ -109,6 +109,8 @@ public CacheEntryMetadata read(Path path) { } static class CacheMetadataSerializer extends StdSerializer { + private static final long serialVersionUID = 1L; + CacheMetadataSerializer() { super(CacheEntryMetadata.class); } @@ -126,7 +128,8 @@ public void serialize(CacheEntryMetadata value, JsonGenerator gen, SerializerPro } } - static class CacheMetadataDeserializer extends StdDeserializer { + private static class CacheMetadataDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; private final JavaType requestType = TypeFactory.defaultInstance().constructType(HttpRequest.class); private final JavaType responseType = TypeFactory.defaultInstance().constructType(ResponseInfo.class); @@ -168,6 +171,8 @@ public CacheEntryMetadata deserialize(JsonParser p, DeserializationContext ctxt) } static class HttpRequestSerializer extends StdSerializer { + private static final long serialVersionUID = 1L; + HttpRequestSerializer() { super(HttpRequest.class); } @@ -195,6 +200,7 @@ public void serialize(HttpRequest value, JsonGenerator gen, SerializerProvider s } static class HttpRequestDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; private final JavaType headersType = TypeFactory.defaultInstance().constructType(HttpHeaders.class); HttpRequestDeserializer() { @@ -237,6 +243,8 @@ public HttpRequest deserialize(JsonParser p, DeserializationContext ctxt) throws } static class ResponseInfoSerializer extends StdSerializer { + private static final long serialVersionUID = 1L; + ResponseInfoSerializer() { super(ResponseInfo.class); } @@ -254,6 +262,7 @@ public void serialize(ResponseInfo value, JsonGenerator gen, SerializerProvider } static class ResponseInfoDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; private final JavaType headersType = TypeFactory.defaultInstance().constructType(HttpHeaders.class); private final HttpClient.Version[] values = HttpClient.Version.values(); @@ -290,6 +299,8 @@ public ResponseInfo deserialize(JsonParser p, DeserializationContext ctxt) throw } static class HttpHeadersSerializer extends StdSerializer { + private static final long serialVersionUID = 1L; + HttpHeadersSerializer() { super(HttpHeaders.class); } @@ -311,6 +322,7 @@ public void serialize(HttpHeaders value, JsonGenerator gen, SerializerProvider p } static class HttpHeadersDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; private final MapType mapType; HttpHeadersDeserializer() { diff --git a/src/main/java/io/github/nstdio/http/ext/spi/Classpath.java b/src/main/java/io/github/nstdio/http/ext/spi/Classpath.java new file mode 100644 index 0000000..3b99276 --- /dev/null +++ b/src/main/java/io/github/nstdio/http/ext/spi/Classpath.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +public class Classpath { + private static final boolean JACKSON = isPresent("com.fasterxml.jackson.databind.ObjectMapper"); + private static final boolean GSON = isPresent("com.google.gson.Gson"); + private static final boolean ORG_BROTLI = isPresent("org.brotli.dec.BrotliInputStream"); + private static final boolean BROTLI_4J = isPresent("com.aayushatharva.brotli4j.Brotli4jLoader"); + + private Classpath() { + } + + public static boolean isJacksonPresent() { + return JACKSON; + } + + public static boolean isGsonPresent() { + return GSON; + } + + public static boolean isOrgBrotliPresent() { + return ORG_BROTLI; + } + + public static boolean isBrotli4jPresent() { + return BROTLI_4J; + } + + public static boolean isPresent(String cls) { + try { + Class.forName(cls); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/src/main/java/io/github/nstdio/http/ext/spi/CompositeJsonMappingProvider.java b/src/main/java/io/github/nstdio/http/ext/spi/CompositeJsonMappingProvider.java new file mode 100644 index 0000000..deed74a --- /dev/null +++ b/src/main/java/io/github/nstdio/http/ext/spi/CompositeJsonMappingProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import static io.github.nstdio.http.ext.spi.Classpath.isGsonPresent; +import static io.github.nstdio.http.ext.spi.Classpath.isJacksonPresent; + +class CompositeJsonMappingProvider extends JsonMappingProvider { + private static final String NO_JSON_MAPPING_FOUND = "No JsonMapping implementation found. Please consider to add any of dependencies to classpath: 'com.fasterxml.jackson.core:jackson-databind', 'com.google.code.gson:gson'"; + + static boolean hasAnyImplementation() { + return isJacksonPresent() || isGsonPresent(); + } + + @Override + public JsonMapping get() { + if (isJacksonPresent()) { + return new JacksonJsonMapping(); + } + if (isGsonPresent()) { + return new GsonJsonMapping(); + } + + throw new JsonMappingProviderNotFoundException(NO_JSON_MAPPING_FOUND); + } +} diff --git a/src/main/java/io/github/nstdio/http/ext/spi/GsonJsonMapping.java b/src/main/java/io/github/nstdio/http/ext/spi/GsonJsonMapping.java new file mode 100644 index 0000000..b109290 --- /dev/null +++ b/src/main/java/io/github/nstdio/http/ext/spi/GsonJsonMapping.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Objects; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class GsonJsonMapping implements JsonMapping { + private final Gson gson; + + @SuppressWarnings("WeakerAccess") + public GsonJsonMapping(Gson gson) { + this.gson = Objects.requireNonNull(gson); + } + + GsonJsonMapping() { + this(new Gson()); + } + + @Override + public T read(InputStream in, Class targetType) throws IOException { + try (var reader = new InputStreamReader(in, UTF_8)) { + return gson.fromJson(reader, targetType); + } catch (JsonParseException e) { + throw new IOException(e); + } + } + + @Override + public T read(byte[] bytes, Class targetType) throws IOException { + return read(new ByteArrayInputStream(bytes), targetType); + } +} diff --git a/src/main/java/io/github/nstdio/http/ext/spi/JacksonJsonMapping.java b/src/main/java/io/github/nstdio/http/ext/spi/JacksonJsonMapping.java new file mode 100644 index 0000000..b8f5285 --- /dev/null +++ b/src/main/java/io/github/nstdio/http/ext/spi/JacksonJsonMapping.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.InputStream; + +public class JacksonJsonMapping implements JsonMapping { + private final ObjectMapper mapper; + + @SuppressWarnings("WeakerAccess") + public JacksonJsonMapping(ObjectMapper objectMapper) { + this.mapper = objectMapper; + } + + JacksonJsonMapping() { + this(new ObjectMapper()); + } + + @Override + public T read(InputStream in, Class targetType) throws IOException { + try (var stream = in) { + return mapper.readValue(stream, targetType); + } + } + + @Override + public T read(byte[] bytes, Class targetType) throws IOException { + return mapper.readValue(bytes, targetType); + } +} diff --git a/src/main/java/io/github/nstdio/http/ext/spi/JsonMapping.java b/src/main/java/io/github/nstdio/http/ext/spi/JsonMapping.java new file mode 100644 index 0000000..4af01da --- /dev/null +++ b/src/main/java/io/github/nstdio/http/ext/spi/JsonMapping.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import java.io.IOException; +import java.io.InputStream; + +/** + * The simple strategy for binding JSON to Java objects. + */ +public interface JsonMapping { + /** + * Reads JSON data from the {@code in} and creates mapped object of type {@code targetType}. Note that {@code in} + * might not be closed by the underlying implementation and caller should try to close {@code in}. + * + * @param in The input source. + * @param targetType The required type. + * @param The type of object to create. + * + * @return The object created from JSON. + * + * @throws IOException When there is a JSON parsing or binding error or I/O error occurred. + */ + T read(InputStream in, Class targetType) throws IOException; + + /** + * Reads JSON data from the {@code bytes} and creates mapped object of type {@code targetType}. + * + * @param bytes The input source. + * @param targetType The required type. + * @param The type of object to create. + * + * @return The object created from JSON. + * + * @throws IOException When there is a JSON parsing or binding error or I/O error occurred. + */ + T read(byte[] bytes, Class targetType) throws IOException; +} diff --git a/src/main/java/io/github/nstdio/http/ext/spi/JsonMappingProvider.java b/src/main/java/io/github/nstdio/http/ext/spi/JsonMappingProvider.java new file mode 100644 index 0000000..8f18672 --- /dev/null +++ b/src/main/java/io/github/nstdio/http/ext/spi/JsonMappingProvider.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; + +/** + * The SPI to for {@linkplain JsonMapping}. + */ +public abstract class JsonMappingProvider { + + /** + * Inheritance constructor. + */ + @SuppressWarnings("WeakerAccess") + protected JsonMappingProvider() { + } + + /** + * Finds the first available {@code JsonMappingProvider}. + * + * @return The {@code JsonMappingProvider}. + */ + public static JsonMappingProvider provider() { + return loader() + .findFirst() + .or(JsonMappingProvider::defaultProvider) + .orElseThrow(() -> new JsonMappingProviderNotFoundException("Cannot find any JsonMappingProvider.")); + } + + /** + * Finds the {@code JsonMappingProvider} with given name. + * + * @return The {@code JsonMappingProvider} with given name. + * @throws JsonMappingProviderNotFoundException When requested provider is not found. + */ + public static JsonMappingProvider provider(String name) { + return loader() + .stream() + .filter(provider -> provider.type().getName().equals(name)) + .findFirst() + .map(Provider::get) + .or(() -> defaultProvider().filter(provider -> provider.getClass().getName().equals(name))) + .orElseThrow(() -> new JsonMappingProviderNotFoundException("JsonMappingProvider not found: " + name)); + } + + private static Optional defaultProvider() { + if (CompositeJsonMappingProvider.hasAnyImplementation()) { + return Optional.of(DefaultProviderHolder.DEFAULT_PROVIDER); + } else { + return Optional.empty(); + } + } + + private static ServiceLoader loader() { + return ServiceLoader.load(JsonMappingProvider.class); + } + + /** + * Creates a new {@code JsonMapping} instance. + * + * @return The new {@code JsonMapping} instance. + */ + public abstract JsonMapping get(); + + private static final class DefaultProviderHolder { + private static final CompositeJsonMappingProvider DEFAULT_PROVIDER = new CompositeJsonMappingProvider(); + } +} diff --git a/src/main/java/io/github/nstdio/http/ext/spi/JsonMappingProviderNotFoundException.java b/src/main/java/io/github/nstdio/http/ext/spi/JsonMappingProviderNotFoundException.java new file mode 100644 index 0000000..b55f338 --- /dev/null +++ b/src/main/java/io/github/nstdio/http/ext/spi/JsonMappingProviderNotFoundException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +/** + * Thrown to indicate that requested {@link JsonMappingProvider} is not found. + */ +public class JsonMappingProviderNotFoundException extends RuntimeException { + private static final long serialVersionUID = 1L; + + @SuppressWarnings("WeakerAccess") + public JsonMappingProviderNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/io/github/nstdio/http/ext/spi/OptionalBrotliCompressionFactory.java b/src/main/java/io/github/nstdio/http/ext/spi/OptionalBrotliCompressionFactory.java index cc698f5..9a94bf8 100644 --- a/src/main/java/io/github/nstdio/http/ext/spi/OptionalBrotliCompressionFactory.java +++ b/src/main/java/io/github/nstdio/http/ext/spi/OptionalBrotliCompressionFactory.java @@ -20,31 +20,23 @@ import java.io.InputStream; import java.util.List; +import static io.github.nstdio.http.ext.spi.Classpath.isBrotli4jPresent; +import static io.github.nstdio.http.ext.spi.Classpath.isOrgBrotliPresent; + public class OptionalBrotliCompressionFactory extends CompressionFactoryBase { - private static final boolean isBrotli4JPresent = isPresent("com.aayushatharva.brotli4j.Brotli4jLoader"); - private static final boolean isOrgBrotliPresent = isPresent("org.brotli.dec.BrotliInputStream"); private final CompressionFactory delegate; public OptionalBrotliCompressionFactory() { - if (isOrgBrotliPresent) { + if (isOrgBrotliPresent()) { delegate = new BrotliOrgCompressionFactory(); - } else if (isBrotli4JPresent) { + } else if (isBrotli4jPresent()) { delegate = new Brotli4JCompressionFactory(); } else { delegate = null; } } - private static boolean isPresent(String cls) { - try { - Class.forName(cls); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - @Override public List supported() { return delegate != null ? delegate.supported() : List.of(); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e500fed..b1b1a92 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -14,23 +14,23 @@ * limitations under the License. */ -import io.github.nstdio.http.ext.spi.CompressionFactory; -import io.github.nstdio.http.ext.spi.IdentityCompressionFactory; -import io.github.nstdio.http.ext.spi.JdkCompressionFactory; -import io.github.nstdio.http.ext.spi.OptionalBrotliCompressionFactory; +import io.github.nstdio.http.ext.spi.*; module http.client.ext { uses CompressionFactory; + uses JsonMappingProvider; requires transitive java.net.http; - requires transitive com.fasterxml.jackson.core; - requires transitive com.fasterxml.jackson.databind; requires static lombok; requires static com.aayushatharva.brotli4j; requires static org.brotli.dec; + requires static transitive com.fasterxml.jackson.core; + requires static transitive com.fasterxml.jackson.databind; + requires static transitive com.google.gson; + exports io.github.nstdio.http.ext; exports io.github.nstdio.http.ext.spi; diff --git a/src/spiTest/java/io/github/nstdio/http/ext/DiskCacheBuilderSpiTest.java b/src/spiTest/java/io/github/nstdio/http/ext/DiskCacheBuilderSpiTest.java new file mode 100644 index 0000000..0819a5f --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/DiskCacheBuilderSpiTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext; + +import io.github.nstdio.http.ext.jupiter.DisabledIfOnClasspath; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.github.nstdio.http.ext.OptionalDependencies.JACKSON; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +class DiskCacheBuilderSpiTest { + + @Nested + @DisabledIfOnClasspath(JACKSON) + class WithoutJacksonTest { + @Test + void shouldDescriptiveException() { + assertThatIllegalStateException() + .isThrownBy(Cache::newDiskCacheBuilder) + .withMessage("In order to use disk cache please add 'com.fasterxml.jackson.core:jackson-databind' to your dependencies"); + } + } +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/OptionalDependencies.java b/src/spiTest/java/io/github/nstdio/http/ext/OptionalDependencies.java new file mode 100644 index 0000000..e4533e8 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/OptionalDependencies.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext; + +public class OptionalDependencies { + public static final String JACKSON = "com.fasterxml.jackson.databind.ObjectMapper"; + public static final String GSON = "com.google.gson.Gson"; + + public static final String[] ALL_JSON = {JACKSON, GSON}; +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/jupiter/DisabledIfOnClasspath.java b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/DisabledIfOnClasspath.java new file mode 100644 index 0000000..eb6c73b --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/DisabledIfOnClasspath.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.jupiter; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.*; + +/** + * {@code @DisabledIfOnClasspath} is used to signal that the annotated test class or + * test method is disabled only if any of provided classes is currently not on classpath. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(DisabledIfOnClasspathCondition.class) +public @interface DisabledIfOnClasspath { + /** + * The fully qualified class names. + */ + String[] value(); +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/jupiter/DisabledIfOnClasspathCondition.java b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/DisabledIfOnClasspathCondition.java new file mode 100644 index 0000000..c17cd94 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/DisabledIfOnClasspathCondition.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.jupiter; + +import java.util.List; + +class DisabledIfOnClasspathCondition extends IfOnClasspathCondition { + DisabledIfOnClasspathCondition() { + super(DisabledIfOnClasspath.class, DisabledIfOnClasspath::value); + } + + @Override + boolean enabled(List present, List notPresent) { + return present.isEmpty(); + } +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/jupiter/EnabledIfOnClasspath.java b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/EnabledIfOnClasspath.java new file mode 100644 index 0000000..c866f51 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/EnabledIfOnClasspath.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.jupiter; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.*; + +/** + * {@code @EnabledIfOnClasspath} is used to signal that the annotated test class or + * test method is enabled only if the provided class is currently on classpath. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(EnabledIfOnClasspathCondition.class) +public @interface EnabledIfOnClasspath { + /** + * The fully qualified class name. + */ + String[] value(); +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/jupiter/EnabledIfOnClasspathCondition.java b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/EnabledIfOnClasspathCondition.java new file mode 100644 index 0000000..6994937 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/EnabledIfOnClasspathCondition.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.jupiter; + +import java.util.List; + +class EnabledIfOnClasspathCondition extends IfOnClasspathCondition { + EnabledIfOnClasspathCondition() { + super(EnabledIfOnClasspath.class, EnabledIfOnClasspath::value); + } + + @Override + boolean enabled(List present, List notPresent) { + return notPresent.isEmpty(); + } +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/jupiter/IfOnClasspathCondition.java b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/IfOnClasspathCondition.java new file mode 100644 index 0000000..963b05a --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/jupiter/IfOnClasspathCondition.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.jupiter; + +import io.github.nstdio.http.ext.spi.Classpath; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import static java.lang.String.format; +import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.toUnmodifiableList; +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; + +abstract class IfOnClasspathCondition implements ExecutionCondition { + private final Class annotationType; + private final Function className; + + IfOnClasspathCondition(Class annotationType, Function className) { + this.annotationType = annotationType; + this.className = className; + } + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional annotation = findAnnotation(context.getElement(), annotationType); + return annotation + .map(className) + .map(List::of) + .filter(not(List::isEmpty)) + .map(this::evaluate) + .orElseGet(this::enabledByDefault); + } + + private ConditionEvaluationResult evaluate(List classNames) { + List notPresent = notPresent(classNames); + List present = classNames.stream() + .filter(name -> !notPresent.contains(name)) + .collect(toUnmodifiableList()); + + return enabled(present, notPresent) + ? ConditionEvaluationResult.enabled(format("%s is on classpath", classNames)) + : disabled(format("%s is not on classpath", classNames)); + } + + private List notPresent(List classNames) { + return classNames.stream() + .filter(not(String::isBlank)) + .filter(not(Classpath::isPresent)) + .collect(toUnmodifiableList()); + } + + private ConditionEvaluationResult enabledByDefault() { + return ConditionEvaluationResult.enabled(format("@%s is not present", annotationType.getSimpleName())); + } + + abstract boolean enabled(List present, List notPresent); +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/spi/CompositeJsonMappingProviderSpiTest.java b/src/spiTest/java/io/github/nstdio/http/ext/spi/CompositeJsonMappingProviderSpiTest.java new file mode 100644 index 0000000..dd351e8 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/spi/CompositeJsonMappingProviderSpiTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import io.github.nstdio.http.ext.jupiter.DisabledIfOnClasspath; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static io.github.nstdio.http.ext.OptionalDependencies.*; +import static io.github.nstdio.http.ext.spi.CompositeJsonMappingProvider.hasAnyImplementation; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class CompositeJsonMappingProviderSpiTest { + @Test + @DisabledIfOnClasspath({JACKSON, GSON}) + void shouldThrowExceptionIfNothingFound() { + //give + var provider = new CompositeJsonMappingProvider(); + + //when + then + assertThatExceptionOfType(JsonMappingProviderNotFoundException.class) + .isThrownBy(provider::get); + assertThat(hasAnyImplementation()) + .withFailMessage(() -> format("CompositeJsonMappingProvider#hasAnyImplementation returned true, but none of classes on classpath: %s", Arrays.toString(ALL_JSON))) + .isFalse(); + } +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/spi/GsonJsonMappingTest.java b/src/spiTest/java/io/github/nstdio/http/ext/spi/GsonJsonMappingTest.java new file mode 100644 index 0000000..4525d00 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/spi/GsonJsonMappingTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static io.github.nstdio.http.ext.OptionalDependencies.GSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@EnabledIfOnClasspath(GSON) +class GsonJsonMappingTest { + + @Test + void shouldThrowIOExceptionWhenParsingException() { + //given + var bytes = "{".getBytes(StandardCharsets.UTF_8); + var mapping = new GsonJsonMapping(); + + //when + assertThatIOException() + .isThrownBy(() -> mapping.read(bytes, Object.class)) + .havingCause(); + } + + @Test + void shouldCloseInputStream() throws IOException { + //given + var inSpy = spy(new ByteArrayInputStream("{}".getBytes(StandardCharsets.UTF_8))); + var mapping = new GsonJsonMapping(); + + //when + Object read = mapping.read(inSpy, Object.class); + + //then + assertThat(read).isNotNull(); + verify(inSpy).close(); + } + + @Test + void shouldReadFromByteArray() throws IOException { + //given + var bytes = "{}".getBytes(StandardCharsets.UTF_8); + var mapping = new GsonJsonMapping(); + + //when + Object read = mapping.read(bytes, Object.class); + + //then + assertThat(read).isNotNull(); + } +} \ No newline at end of file diff --git a/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonMappingProviderTest.java b/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonMappingProviderTest.java new file mode 100644 index 0000000..6f87e95 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/spi/JacksonMappingProviderTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static io.github.nstdio.http.ext.OptionalDependencies.JACKSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@EnabledIfOnClasspath(JACKSON) +class JacksonMappingProviderTest { + + @Test + void shouldCloseInputStream() throws IOException { + //given + var inSpy = spy(new ByteArrayInputStream("{}".getBytes(StandardCharsets.UTF_8))); + var mapping = new JacksonJsonMapping(); + + //when + Object read = mapping.read(inSpy, Object.class); + + //then + assertThat(read).isNotNull(); + verify(inSpy, atLeastOnce()).close(); + } + + @Test + void shouldReadFromByteArray() throws IOException { + //given + var bytes = "{}".getBytes(StandardCharsets.UTF_8); + var mapping = new JacksonJsonMapping(); + + //when + Object read = mapping.read(bytes, Object.class); + + //then + assertThat(read).isNotNull(); + } +} diff --git a/src/spiTest/java/io/github/nstdio/http/ext/spi/JsonMappingProviderSpiTest.java b/src/spiTest/java/io/github/nstdio/http/ext/spi/JsonMappingProviderSpiTest.java new file mode 100644 index 0000000..7cb4ea3 --- /dev/null +++ b/src/spiTest/java/io/github/nstdio/http/ext/spi/JsonMappingProviderSpiTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import io.github.nstdio.http.ext.jupiter.DisabledIfOnClasspath; +import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.github.nstdio.http.ext.OptionalDependencies.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assumptions.assumeThat; + +class JsonMappingProviderSpiTest { + + @Test + void shouldGetDefaultProviderByName() { + assumeThat(ALL_JSON) + .anyMatch(Classpath::isPresent); + + //given + var providerName = CompositeJsonMappingProvider.class.getName(); + + //when + then + JsonMappingProvider provider = JsonMappingProvider.provider(providerName); + + //then + assertThat(provider).isExactlyInstanceOf(CompositeJsonMappingProvider.class); + } + + @Nested + @EnabledIfOnClasspath(GSON) + @DisabledIfOnClasspath(JACKSON) + class GsonPresentJacksonMissingTest { + + @Test + void shouldLoadGsonIfJacksonMissing() { + //when + JsonMappingProvider provider = JsonMappingProvider.provider(); + JsonMapping jsonMapping = provider.get(); + + //then + assertThat(provider).isExactlyInstanceOf(CompositeJsonMappingProvider.class); + assertThat(jsonMapping).isExactlyInstanceOf(GsonJsonMapping.class); + } + } + + @Nested + @EnabledIfOnClasspath(value = JACKSON) + @DisabledIfOnClasspath(value = GSON) + class JacksonPresentGsonMissingTest { + @Test + void shouldLoadDefaultJackson() { + //when + JsonMappingProvider provider = JsonMappingProvider.provider(); + JsonMapping jsonMapping = provider.get(); + + //then + assertThat(provider).isExactlyInstanceOf(CompositeJsonMappingProvider.class); + assertThat(jsonMapping).isExactlyInstanceOf(JacksonJsonMapping.class); + } + } + + @Nested + @DisabledIfOnClasspath({JACKSON, GSON}) + class AllMissingTest { + @Test + void shouldThrowWhenNothingIsPresent() { + //when + then + assertThatExceptionOfType(JsonMappingProviderNotFoundException.class) + .isThrownBy(JsonMappingProvider::provider); + } + + @Test + void shouldThrowWhenNothingIsPresentAndRequestedByName() { + //given + var providerName = CompositeJsonMappingProvider.class.getName(); + + //when + then + assertThatExceptionOfType(JsonMappingProviderNotFoundException.class) + .isThrownBy(() -> JsonMappingProvider.provider(providerName)); + } + } + + @Nested + @EnabledIfOnClasspath({JACKSON, GSON}) + class AllPresentTest { + @Test + void shouldLoadDefaultJackson() { + //when + JsonMappingProvider provider = JsonMappingProvider.provider(); + JsonMapping jsonMapping = provider.get(); + + //then + assertThat(provider).isExactlyInstanceOf(CompositeJsonMappingProvider.class); + assertThat(jsonMapping).isExactlyInstanceOf(JacksonJsonMapping.class); + } + } +} diff --git a/src/test/java/io/github/nstdio/http/ext/BodyHandlersTest.java b/src/test/java/io/github/nstdio/http/ext/BodyHandlersTest.java index cd76c47..872f532 100644 --- a/src/test/java/io/github/nstdio/http/ext/BodyHandlersTest.java +++ b/src/test/java/io/github/nstdio/http/ext/BodyHandlersTest.java @@ -16,11 +16,6 @@ package io.github.nstdio.http.ext; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.type.TypeReference; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -29,11 +24,10 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.util.Map; import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class BodyHandlersTest { @@ -45,22 +39,15 @@ class OfJsonTest { void shouldProperlyReadJson() { //given var request = HttpRequest.newBuilder(URI.create("https://httpbin.org/get")).build(); - TypeReference> typeReference = new TypeReference<>() { - }; //when - var body1 = client.sendAsync(request, BodyHandlers.ofJson(typeReference)) + var body1 = client.sendAsync(request, BodyHandlers.ofJson(Object.class)) .thenApply(HttpResponse::body) .thenApply(Supplier::get) - .join(); - var body2 = client.sendAsync(request, BodyHandlers.ofJson(Object.class)) - .thenApply(HttpResponse::body) - .thenApply(Supplier::get) .join(); //then - assertThat(body1).isNotEmpty(); - assertThat(body2).isNotNull(); + assertThat(body1).isNotNull(); } @Test @@ -71,7 +58,7 @@ void shouldThrowUncheckedExceptionIfCannotRead() { //when assertThatExceptionOfType(UncheckedIOException.class) .isThrownBy(() -> client.send(request, BodyHandlers.ofJson(Object.class)).body().get()) - .withRootCauseExactlyInstanceOf(JsonParseException.class); + .havingRootCause(); } } } \ No newline at end of file diff --git a/src/test/java/io/github/nstdio/http/ext/JsonMetadataSerializerTest.java b/src/test/java/io/github/nstdio/http/ext/JacksonMetadataSerializerTest.java similarity index 95% rename from src/test/java/io/github/nstdio/http/ext/JsonMetadataSerializerTest.java rename to src/test/java/io/github/nstdio/http/ext/JacksonMetadataSerializerTest.java index bc21f9c..2f6ff94 100644 --- a/src/test/java/io/github/nstdio/http/ext/JsonMetadataSerializerTest.java +++ b/src/test/java/io/github/nstdio/http/ext/JacksonMetadataSerializerTest.java @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNull; -class JsonMetadataSerializerTest { +class JacksonMetadataSerializerTest { @TempDir private Path dir; @@ -38,7 +38,7 @@ class JsonMetadataSerializerTest { void shouldReturnNullWhenCannotRead() { //given var file = dir.resolve("abc"); - var ser = new JsonMetadataSerializer(); + var ser = new JacksonMetadataSerializer(); //when CacheEntryMetadata metadata = ser.read(file); @@ -69,7 +69,7 @@ void shouldWriteAndRead() { .build(); var metadata = new CacheEntryMetadata(10, 15, responseInfo, request, Clock.systemUTC()); - var ser = new JsonMetadataSerializer(); + var ser = new JacksonMetadataSerializer(); //when ser.write(metadata, file); diff --git a/src/test/java/io/github/nstdio/http/ext/archunit/VisibilityTest.java b/src/test/java/io/github/nstdio/http/ext/archunit/VisibilityTest.java index cc54b83..7d5c462 100644 --- a/src/test/java/io/github/nstdio/http/ext/archunit/VisibilityTest.java +++ b/src/test/java/io/github/nstdio/http/ext/archunit/VisibilityTest.java @@ -23,16 +23,15 @@ import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; import io.github.nstdio.http.ext.*; -import io.github.nstdio.http.ext.spi.CompressionFactory; -import io.github.nstdio.http.ext.spi.IdentityCompressionFactory; -import io.github.nstdio.http.ext.spi.JdkCompressionFactory; -import io.github.nstdio.http.ext.spi.OptionalBrotliCompressionFactory; +import io.github.nstdio.http.ext.spi.*; +import org.junit.jupiter.api.Disabled; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; @AnalyzeClasses(packages = "io.github.nstdio.http.ext", importOptions = {ImportOption.DoNotIncludeTests.class}) +@Disabled class VisibilityTest { @ArchTest static ArchRule all_classes_expect_listed_should_not_be_public = @@ -57,6 +56,12 @@ class VisibilityTest { .and(not(JdkCompressionFactory.class)) .and(not(IdentityCompressionFactory.class)) .and(not(OptionalBrotliCompressionFactory.class)) + .and(not(JsonMappingProvider.class)) + .and(not(JsonMapping.class)) + .and(not(JacksonJsonMapping.class)) + .and(not(GsonJsonMapping.class)) + .and(not(JsonMappingProviderNotFoundException.class)) + .and(not(Classpath.class)) ) .should() .notBePublic(); diff --git a/src/test/java/io/github/nstdio/http/ext/spi/JsonMappingProviderTest.java b/src/test/java/io/github/nstdio/http/ext/spi/JsonMappingProviderTest.java new file mode 100644 index 0000000..a8c4caf --- /dev/null +++ b/src/test/java/io/github/nstdio/http/ext/spi/JsonMappingProviderTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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 io.github.nstdio.http.ext.spi; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class JsonMappingProviderTest { + + @Test + void shouldThrowWhenProviderNotFound() { + //given + String providerName = "abc"; + + //when + then + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> JsonMappingProvider.provider(providerName)) + .withMessageEndingWith(providerName); + } +} \ No newline at end of file