diff --git a/cqp-api/build.gradle.kts b/cqp-api/build.gradle.kts index 8c71e47..6e35e33 100644 --- a/cqp-api/build.gradle.kts +++ b/cqp-api/build.gradle.kts @@ -1,187 +1,25 @@ -import java.net.HttpURLConnection -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - plugins { `java-library` - `maven-publish` - signing id("com.diffplug.spotless") version "7.0.2" + id("com.quantori.cqp-build") } group = "com.quantori" -description = "Chem query platform. Compound quick search" -version = "0.0.10" +description = "Chem query platform. Storage API" +version = "0.0.11" repositories { mavenLocal() mavenCentral() } -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } - withSourcesJar() - withJavadocJar() -} - -publishing { - publications { - create("mavenJava") { - from(components["java"]) - - pom { - artifactId = "cqp-api" - name = project.name - description = project.description - packaging = "jar" - url = "https://github.com/quantori/chem-query-platform" - - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - - developers { - developer { - id = "artem.chukin" - name = "Artem Chukin" - email = "artem.chukin@quantori.com" - } - developer { - id = "dmitriy gusev" - name = "Dmitriy Gusev" - email = "dmitriy.gusev@quantori.com" - } - - developer { - id = "valeriy burmistrov" - name = "Valeriy Burmistrov" - email = "valeriy.burmistrov@quantori.com" - } - - developer { - id = "boris sukhodoev" - name = "Boris Sukhodoev" - email = "boris.sukhodoev@quantori.com" - } - } - - scm { - connection = "scm:git:git://github.com/quantori/chem-query-platform.git" - developerConnection = "scm:git:ssh://github.com/quantori/chem-query-platform.git" - url = "https://github.com/quantori/chem-query-platform" - } - } - } - } - repositories { - maven { - name = "localStaging" - // change URLs to point to your repos, e.g. http://my.org/repo - val releasesRepoUrl = uri(layout.buildDirectory.dir("repos/releases")) - val snapshotsRepoUrl = uri(layout.buildDirectory.dir("repos/snapshots")) - url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl - } - } -} - -tasks.test { - useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - } -} - -artifacts { - archives(tasks.named("javadocJar")) - archives(tasks.named("sourcesJar")) -} - -signing { - setRequired { - !version.toString().endsWith("SNAPSHOT") && gradle.taskGraph.hasTask("publish") - } - val signingSecretKey = findProperty("signing.secretKey") as String? ?: System.getenv("GPG_SIGNING_SECRET_KEY") - val signingPassword = findProperty("signing.password") as String? ?: System.getenv("GPG_SIGNING_PASSWORD") - - useInMemoryPgpKeys(signingSecretKey, signingPassword) - sign(publishing.publications["mavenJava"]) - sign(configurations.archives.get()) -} - -// Fix Javadoc warnings on JDK 9+ (optional but recommended) -if (JavaVersion.current().isJava9Compatible) { - tasks.withType().configureEach { - (options as StandardJavadocDocletOptions).addStringOption("Xdoclint:none", "-quiet") - (options as StandardJavadocDocletOptions).addBooleanOption("html5", true) - } -} - -@OptIn(ExperimentalEncodingApi::class) -fun String.toBase64(): String { - return Base64.encode(this.toByteArray(Charsets.UTF_8)) -} - -val publishToLocalStaging = tasks.getByName("publishMavenJavaPublicationToLocalStagingRepository") - -publishToLocalStaging.outputs.dir(layout.buildDirectory.dir("repos/releases")) - -val zipBundle by tasks.registering(Zip::class) { - archiveFileName = "central-bundle.zip" - destinationDirectory = project.layout.buildDirectory.dir("distributions") - inputs.files(publishToLocalStaging.outputs.files) - from(publishToLocalStaging.outputs.files.files) -} - -val uploadToMavenCentral by tasks.registering { - val url = uri("https://central.sonatype.com/api/v1/publisher/upload").toURL() - val boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW" - inputs.file(zipBundle.map { it.archiveFile }.get()) - doLast { - val file = zipBundle.get().archiveFile.get().asFile - val mavenCentralUsername = - findProperty("mavenCentralUsername") as String? ?: System.getenv("MAVEN_CENTRAL_USERNAME") - val mavenCentralPassword = - findProperty("mavenCentralPassword") as String? ?: System.getenv("MAVEN_CENTRAL_PASSWORD") - val token = "$mavenCentralUsername:$mavenCentralPassword\n".toBase64() - - val connection = (url.openConnection() as HttpURLConnection).apply { - requestMethod = "POST" - doOutput = true - setRequestProperty("Authorization", "Bearer $token") - setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary") - } - - val outputStream = connection.outputStream - outputStream.bufferedWriter().use { writer -> - writer.append("--$boundary\r\n") - writer.append("Content-Disposition: form-data; name=\"bundle\"; filename=\"${file.name}\"\r\n") - writer.append("Content-Type: application/octet-stream\r\n\r\n") - writer.flush() - - file.inputStream().use { it.copyTo(outputStream) } - - writer.append("\r\n--$boundary--\r\n") - writer.flush() - } - - val responseCode = connection.responseCode - println("Response Code: $responseCode") - println("Response Message: ${connection.inputStream.bufferedReader().readText()}") - } -} - dependencies { implementation("commons-codec:commons-codec:1.15") - compileOnly(libs.jackson) implementation(libs.javax.validation) implementation(libs.bundles.indigo) implementation(libs.common.text) + compileOnly(libs.jackson) compileOnly(libs.lombok) annotationProcessor(libs.lombok) diff --git a/cqp-api/src/test/java/com/quantori/cqp/api/ProcessChiralStructuresTest.java b/cqp-api/src/test/java/com/quantori/cqp/api/ProcessChiralStructuresTest.java new file mode 100644 index 0000000..b1645aa --- /dev/null +++ b/cqp-api/src/test/java/com/quantori/cqp/api/ProcessChiralStructuresTest.java @@ -0,0 +1,130 @@ +package com.quantori.cqp.api; + +import static org.junit.Assert.assertTrue; + +import com.epam.indigo.Indigo; +import com.quantori.cqp.api.indigo.IndigoMatcher; +import com.quantori.cqp.api.indigo.IndigoProvider; +import com.quantori.cqp.api.model.ExactParams; +import com.quantori.cqp.api.model.SubstructureParams; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ProcessChiralStructuresTest { + + private final IndigoProvider indigoProvider = new IndigoProvider(100, 5); + + @ParameterizedTest + @ValueSource(strings = { + "C1C=CC=C2C=CC=CC=12", + "N1([C@@]2(SC(=N1)C(=O)C)N(N=C(c1ccc(cc1)Br)CC2)c1ccccc1)c1c(cc(cc1)Cl)Cl", + """ + X-17641 + MACCS-II09195910443D 1 0.00000 0.00000 0 + MOE2004 3D + 38 42 0 0 0 0 999 V2000 \s + 9.7920 2.1040 0.0000 C 0 0 0 0 0 + 4.2870 7.1000 0.0000 C 0 0 0 0 0 + 8.5870 7.0150 0.0000 C 0 0 0 0 0 + 7.3550 2.1040 0.0000 N 0 0 0 0 0 + 7.3550 4.9100 0.0000 C 0 0 0 0 0 + 8.5660 4.2090 0.0000 C 0 0 0 0 0 + 9.7920 6.3200 0.0000 C 0 0 0 0 0 + 8.5660 8.4170 0.0000 O 0 0 0 0 0 + 8.5660 0.0070 0.0000 N 0 0 0 0 0 + 8.5660 2.8130 0.0000 C 0 0 2 0 0 + 7.3550 9.1050 0.0000 C 0 0 0 0 0 + 12.2010 2.1040 0.0000 O 0 0 0 0 0 + 0.7160 3.5140 0.0000 C 0 0 0 0 0 + 6.0370 0.2620 0.0000 S 0 0 0 0 0 + 4.2020 4.7400 0.0000 F 0 0 0 0 0 + 2.7850 4.7400 0.0000 C 0 0 0 0 0 + 11.0040 0.0000 0.0000 C 0 0 0 0 0 + 11.0040 4.2090 0.0000 O 0 0 0 0 0 + 5.5900 3.8690 0.0000 O 0 0 0 0 0 + 5.0160 9.2460 0.0000 C 0 0 0 0 0 + 2.0830 5.9520 0.0000 C 0 0 0 0 0 + 5.6970 7.0850 0.0000 O 0 0 0 0 0 + 7.3550 6.2990 0.0000 C 0 0 0 0 0 + 9.7920 4.9100 0.0000 C 0 0 0 0 0 + 3.8690 8.4250 0.0000 C 0 0 0 0 0 + 0.0140 2.3030 0.0000 Cl 0 0 0 0 0 + 6.0370 2.5300 0.0000 C 0 0 0 0 0 + 3.8050 1.4030 0.0000 C 0 0 0 0 0 + 6.1570 8.3960 0.0000 C 0 0 0 0 0 + 2.0830 3.5290 0.0000 C 0 0 0 0 0 + 5.2010 1.4030 0.0000 C 0 0 0 0 0 + 9.7920 0.6870 0.0000 C 0 0 0 0 0 + 13.4130 2.8060 0.0000 C 0 0 0 0 0 + 11.0040 2.8060 0.0000 C 0 0 0 0 0 + 0.0000 4.7400 0.0000 C 0 0 0 0 0 + 7.3550 0.6870 0.0000 C 0 0 0 0 0 + 0.7160 5.9590 0.0000 C 0 0 0 0 0 + 7.3550 10.5010 0.0000 O 0 0 0 0 0 + 4 36 1 0 0 0 + 4 27 1 0 0 0 + 4 10 1 0 0 0 + 36 9 2 0 0 0 + 36 14 1 0 0 0 + 1 10 1 0 0 0 + 1 32 2 0 0 0 + 1 34 1 0 0 0 + 27 31 1 0 0 0 + 27 19 2 0 0 0 + 31 14 1 0 0 0 + 31 28 2 0 0 0 + 9 32 1 0 0 0 + 10 6 1 0 0 0 + 32 17 1 0 0 0 + 28 30 1 0 0 0 + 30 13 2 0 0 0 + 30 16 1 0 0 0 + 11 29 1 0 0 0 + 11 8 1 0 0 0 + 11 38 2 0 0 0 + 34 18 2 0 0 0 + 34 12 1 0 0 0 + 29 22 1 0 0 0 + 29 20 2 0 0 0 + 6 24 2 0 0 0 + 6 5 1 0 0 0 + 8 3 1 0 0 0 + 22 2 1 0 0 0 + 13 26 1 0 0 0 + 13 35 1 0 0 0 + 16 15 1 0 0 0 + 16 21 2 0 0 0 + 20 25 1 0 0 0 + 2 25 2 0 0 0 + 24 7 1 0 0 0 + 5 23 2 0 0 0 + 3 23 1 0 0 0 + 3 7 2 0 0 0 + 12 33 1 0 0 0 + 37 21 1 0 0 0 + 37 35 2 0 0 0 + M END + """ + }) + void test_exact_match(String structure) { + var indigoObject = new Indigo().loadMolecule(structure); + var exactParams = ExactParams.builder().searchQuery(structure).build(); + assertTrue(new IndigoMatcher(indigoProvider).isExactMatch(indigoObject.serialize(), exactParams)); + } + + @ParameterizedTest + @ValueSource(strings = { + "N1([C@@](C=C(C1=O)Nc1ccc(I)cc1)(C(=O)OCC)C)c1ccc(cc1)I", + "N1([C@@]2(SC(=N1)C(=O)C)N(N=C(c1ccc(cc1)Br)CC2)c1ccccc1)c1c(cc(cc1)Cl)Cl", + "c1(c(c2c([nH]c1=O)ccc(c2)Cl)c1ccccc1)C1=NN(C(Nc2ccccc2)=S)[C@H](C1)c1ccc(cc1)OC", + "c1(c(c2c([nH]c1=O)ccc(c2)Cl)c1ccccc1)C1=NN([C@H](C1)c1cc(c(cc1)OC)OC)C(=O)CCC(=O)O", + "N1(N=C(c2c(c3c(nc2C)ccc(c3)Br)c2ccccc2)C[C@H]1c1ccc(cc1)C)C(=O)CCC(=O)O", + "c12n(c(c3c(n1c(nn2)SCC(c1ccc(cc1)Cl)=O)sc1c3CC[C@H](C1)C(C)(C)C)=O)c1c(C)cccc1", + "c12n([C@H](C(F)(F)F)C[C@H](N2)c2ccc(cc2)Br)ncc1C(Nc1cc(c(c(c1)OC)OC)OC)=O" + }) + void test_sub_search(String structure) { + var indigoObject = new Indigo().loadMolecule(structure); + var substructureParams = SubstructureParams.builder().searchQuery(structure).heteroatoms(false).build(); + assertTrue(new IndigoMatcher(indigoProvider).isSubstructureMatch(indigoObject.serialize(), substructureParams)); + } +} diff --git a/cqp-api/src/test/java/com/quantori/cqp/api/ProcessStructureTest.java b/cqp-api/src/test/java/com/quantori/cqp/api/ProcessStructureTest.java new file mode 100644 index 0000000..235adde --- /dev/null +++ b/cqp-api/src/test/java/com/quantori/cqp/api/ProcessStructureTest.java @@ -0,0 +1,65 @@ +package com.quantori.cqp.api; + +import com.quantori.cqp.api.indigo.IndigoMatcher; +import com.quantori.cqp.api.indigo.IndigoProvider; +import com.quantori.cqp.api.model.SubstructureParams; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +class ProcessStructureTest { + + private static Stream molecules() { + return Stream.of( + Arguments.of("C1C=CC=C2CCC=12>>C1CCC1", + "[NH2:1][C:2]1=[CH:3][CH:4]=[C:5]([C:8]2=[CH:9][N:10]=[C:11]([C:13]3([CH2:14][CH2:15][CH2:16]3)[O:17]" + + "[CH2:18][C:19](=[O:20])[O:21]C)[S:12]2)[CH:6]=[CH:7]1.[CH:23]1([CH2:24][C:25]2=[C:26]1[CH:27]=" + + "[CH:28][CH:29]=[CH:30]2)[C:31](=[O:32])O.CN1CCOCC1.O.ON1N=NC2=C1C=CC=C2.Cl.C(C)N=C=NCCCN(C)C>CN(C=O)" + + "C>[C:25]12=[CH:30][CH:29]=[CH:28][CH:27]=[C:26]1[CH:23]([C:31](=[O:32])[NH:1][C:2]1=[CH:3][CH:4]" + + "=[C:5]([C:8]3=[CH:9][N:10]=[C:11]([C:13]4([CH2:14][CH2:15][CH2:16]4)[O:17][CH2:18][C:19](=[O:20])" + + "[OH:21])[S:12]3)[CH:6]=[CH:7]1)[CH2:24]2 |f:3.4,5.6|"), + + Arguments.of("C1C=CC=C2CCC=12>>C1CCC1", + "[NH2:1][C:2]1=[CH:3][CH:4]=[C:5]([C:8]2=[CH:9][N:10]=[C:11]([C:13]3([CH2:14][CH2:15][CH2:16]3)[O:17]" + + "[CH2:18][C:19](=[O:20])[O:21]C)[S:12]2)[CH:6]=[CH:7]1.[CH:23]1([CH2:24][C:25]2=[C:26]1[CH:27]=" + + "[CH:28][CH:29]=[CH:30]2)[C:31](=[O:32])O.CN1CCOCC1.O.ON1N=NC2=C1C=CC=C2.Cl.C(C)N=C=NCCCN(C)C>CN(C=O)" + + "C>[C:25]12=[CH:30][CH:29]=[CH:28][CH:27]=[C:26]1[CH:23]([C:31](=[O:32])[NH:1][C:2]1=[CH:3][CH:4]=" + + "[C:5]([C:8]3=[CH:9][N:10]=[C:11]([C:13]4([CH2:14][CH2:15][CH2:16]4)[O:17][CH2:18][C:19](=[O:20])" + + "[OH:21])[S:12]3)[CH:6]=[CH:7]1)[CH2:24]2 |f:3.4,5.6|"), + + Arguments.of("C1C=CC=C2CCC=12>>C1CCC1", + "[NH2:1][C:2]1=[CH:3][CH:4]=[C:5]([C:8]2=[CH:9][N:10]=[C:11]([C:13]3([CH2:14][CH2:15][CH2:16]3)[O:17]" + + "[CH2:18][C:19](=[O:20])[O:21]C)[S:12]2)[CH:6]=[CH:7]1.[CH:23]1([CH2:24][C:25]2=[C:26]1[CH:27]=" + + "[CH:28][CH:29]=[CH:30]2)[C:31](=[O:32])O.CN1CCOCC1.O.ON1N=NC2=C1C=CC=C2.Cl.C(C)N=C=NCCCN(C)C>CN(C=O)" + + "C>[C:25]12=[CH:30][CH:29]=[CH:28][CH:27]=[C:26]1[CH:23]([C:31](=[O:32])[NH:1][C:2]1=[CH:3][CH:4]=" + + "[C:5]([C:8]3=[CH:9][N:10]=[C:11]([C:13]4([CH2:14][CH2:15][CH2:16]4)[O:17][CH2:18][C:19](=[O:20])" + + "[OH:21])[S:12]3)[CH:6]=[CH:7]1)[CH2:24]2 |f:3.4,5.6|\n"), + + Arguments.of("C1C=CC=[C:3]2[CH2:8][CH2:7][C:2]=12>>[CH2:2]1[CH2:7][CH2:8][CH2:3]1", + "[NH2:1][C:2]1=[CH:3][CH:4]=[C:5]([C:8]2=[CH:9][N:10]=[C:11]([C:13]3([CH2:14][CH2:15][CH2:16]3)[O:17]" + + "[CH2:18][C:19](=[O:20])[O:21]C)[S:12]2)[CH:6]=[CH:7]1.[CH:23]1([CH2:24][C:25]2=[C:26]1[CH:27]=" + + "[CH:28][CH:29]=[CH:30]2)[C:31](=[O:32])O.CN1CCOCC1.O.ON1N=NC2=C1C=CC=C2.Cl.C(C)N=C=NCCCN(C)C>CN(C=O)" + + "C>[C:25]12=[CH:30][CH:29]=[CH:28][CH:27]=[C:26]1[CH:23]([C:31](=[O:32])[NH:1][C:2]1=[CH:3][CH:4]=" + + "[C:5]([C:8]3=[CH:9][N:10]=[C:11]([C:13]4([CH2:14][CH2:15][CH2:16]4)[O:17][CH2:18][C:19](=[O:20])" + + "[OH:21])[S:12]3)[CH:6]=[CH:7]1)[CH2:24]2 |f:3.4,5.6|\n"), + + Arguments.of("C1C=CC=[C:3]2[CH2:8][CH2:7][C:2]=12>>[CH2:2]1[CH2:7][CH2:8][CH2:3]1", + "[NH2:1][C:2]1=[CH:3][CH:4]=[C:5]([C:8]2=[CH:9][N:10]=[C:11]([C:13]3([CH2:14][CH2:15][CH2:16]3)[O:17]" + + "[CH2:18][C:19](=[O:20])[O:21]C)[S:12]2)[CH:6]=[CH:7]1.[CH:23]1([CH2:24][C:25]2=[C:26]1[CH:27]=" + + "[CH:28][CH:29]=[CH:30]2)[C:31](=[O:32])O.CN1CCOCC1.O.ON1N=NC2=C1C=CC=C2.Cl.C(C)N=C=NCCCN(C)C>CN(C=O)" + + "C>[C:25]12=[CH:30][CH:29]=[CH:28][CH:27]=[C:26]1[CH:23]([C:31](=[O:32])[NH:1][C:2]1=[CH:3][CH:4]=" + + "[C:5]([C:8]3=[CH:9][N:10]=[C:11]([C:13]4([CH2:14][CH2:15][CH2:16]4)[O:17][CH2:18][C:19](=[O:20])" + + "[OH:21])[S:12]3)[CH:6]=[CH:7]1)[CH2:24]2 |f:3.4,5.6|") + ); + } + + @ParameterizedTest + @MethodSource("molecules") + void test(String source, String target) { + var substructureParams = SubstructureParams.builder().searchQuery(source).heteroatoms(false).build(); + Assertions.assertTrue(new IndigoMatcher(new IndigoProvider(100, 5)).isReactionSubstructureMatch(target, substructureParams)); + } +} diff --git a/cqp-api/src/test/java/com/quantori/cqp/api/indigo/IndigoProviderConcurrencyTest.java b/cqp-api/src/test/java/com/quantori/cqp/api/indigo/IndigoProviderConcurrencyTest.java new file mode 100644 index 0000000..363d358 --- /dev/null +++ b/cqp-api/src/test/java/com/quantori/cqp/api/indigo/IndigoProviderConcurrencyTest.java @@ -0,0 +1,119 @@ +package com.quantori.cqp.api.indigo; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.epam.indigo.Indigo; +import com.quantori.cqp.api.indigo.exception.ObjectPoolException; +import org.junit.jupiter.api.Test; + +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +class IndigoProviderConcurrencyTest { + + private final int maxPoolSize = 100; + private final IndigoProvider indigoProvider = new IndigoProvider(maxPoolSize, 5); + + @Test + void testIndigoProviderConcurrency_when4000Threads_thenOK() throws InterruptedException { + int totalThreads = 4000; + + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch finishLatch = new CountDownLatch(totalThreads); + ExecutorService executorService = Executors.newFixedThreadPool(totalThreads); + + // Spawn threads to acquire and release Indigo objects + for (int i = 0; i < totalThreads; i++) { + executorService.submit(() -> { + try { + startLatch.await(); // Wait until the start signal is given + Indigo indigo = indigoProvider.take(); + try { + simulateProcessing(); + } finally { + indigoProvider.offer(indigo); + } + } catch (Exception e) { + Thread.currentThread().interrupt(); + } finally { + finishLatch.countDown(); + } + }); + } + + startLatch.countDown(); + finishLatch.await(); + executorService.shutdown(); + assertTrue(executorService.awaitTermination(30, TimeUnit.SECONDS)); + } + + @Test + void testIndigoProviderConcurrency_when1000ThreadsAndDuplicates_thenOK() throws InterruptedException { + int totalThreads = 1000; + + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch finishLatch = new CountDownLatch(totalThreads); + ExecutorService executorService = Executors.newFixedThreadPool(totalThreads); + + // Spawn threads to acquire and release Indigo objects + for (int i = 0; i < totalThreads; i++) { + executorService.submit(() -> { + try { + startLatch.await(); // Wait until the start signal is given + Indigo indigo = indigoProvider.take(); + boolean offerDuplicate = false; + try { + simulateProcessing(); + if (Math.random() > 0.2) { + offerDuplicate = true; + indigoProvider.offer(indigo); + } + } finally { + if (offerDuplicate) { + assertThrows(ObjectPoolException.class, () -> indigoProvider.offer(indigo)); + } else { + indigoProvider.offer(indigo); + } + } + } catch (Exception e) { + Thread.currentThread().interrupt(); + } finally { + finishLatch.countDown(); + } + }); + } + + startLatch.countDown(); + finishLatch.await(); + executorService.shutdown(); + assertTrue(executorService.awaitTermination(30, TimeUnit.SECONDS)); + } + + @Test + void testIndigoProviderConcurrency_whenBadIndigoProvider_thenAssertThrowsOK() { + IndigoProvider badIndigoProvider = new IndigoProvider(1, 1); + badIndigoProvider.take(); + assertThrows(ObjectPoolException.class, badIndigoProvider::take); + } + + private void simulateProcessing() throws InterruptedException { + + // Simulate work by sleeping for a random duration between 200ms and 0.5s + if (Math.random() > 0.3) { + + int minWaitTime = 200; + int maxWaitTime = 500; + Random random = new Random(); + int waitTime = random.nextInt(maxWaitTime - minWaitTime + 1) + minWaitTime; + Thread.sleep(waitTime); + + } else { + + throw new RuntimeException(); + } + } +} diff --git a/cqp-build/build.gradle.kts b/cqp-build/build.gradle.kts index 8c6cae6..ce5e2ba 100644 --- a/cqp-build/build.gradle.kts +++ b/cqp-build/build.gradle.kts @@ -20,4 +20,6 @@ gradlePlugin { dependencies { compileOnly(gradleApi()) + implementation(gradleApi()) + implementation(localGroovy()) } diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index 8231069..6288db6 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -3,14 +3,20 @@ package com.quantori.cqp.build import org.gradle.api.JavaVersion import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.plugins.JavaLibraryPlugin import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.plugins.JavaPluginExtension -import org.gradle.api.plugins.JavaLibraryPlugin import org.gradle.external.javadoc.StandardJavadocDocletOptions import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.bundling.Zip import org.gradle.kotlin.dsl.* +import java.net.HttpURLConnection +import java.nio.charset.StandardCharsets +import java.util.Base64 + class CqpJavaLibraryPlugin : Plugin { override fun apply(project: Project) { project.pluginManager.apply(JavaLibraryPlugin::class) @@ -59,25 +65,17 @@ class CqpJavaLibraryPlugin : Plugin { } developers { - developer { - id.set("artem.chukin") - name.set("Artem Chukin") - email.set("artem.chukin@quantori.com") - } - developer { - id.set("dmitriy.gusev") - name.set("Dmitriy Gusev") - email.set("dmitriy.gusev@quantori.com") - } - developer { - id.set("valeriy.burmistrov") - name.set("Valeriy Burmistrov") - email.set("valeriy.burmistrov@quantori.com") - } - developer { - id.set("boris.sukhodoev") - name.set("Boris Sukhodoev") - email.set("boris.sukhodoev@quantori.com") + listOf( + Triple("artem.chukin", "Artem Chukin", "artem.chukin@quantori.com"), + Triple("dmitriy.gusev", "Dmitriy Gusev", "dmitriy.gusev@quantori.com"), + Triple("valeriy.burmistrov", "Valeriy Burmistrov", "valeriy.burmistrov@quantori.com"), + Triple("boris.sukhodoev", "Boris Sukhodoev", "boris.sukhodoev@quantori.com") + ).forEach { (id, name, email) -> + developer { + this.id.set(id) + this.name.set(name) + this.email.set(email) + } } } @@ -131,5 +129,64 @@ class CqpJavaLibraryPlugin : Plugin { events("passed", "skipped", "failed") } } + + fun String.toBase64(): String { + return Base64.getEncoder().encodeToString(this.toByteArray(StandardCharsets.UTF_8)) + } + + project.afterEvaluate { + val publishTask = project.tasks.findByName("publishMavenJavaPublicationToLocalStagingRepository") + + val zipBundle = project.tasks.register("zipBundle") { + group = "publishing" + description = "Zips the published files for Maven Central upload" + archiveFileName.set("central-bundle.zip") + destinationDirectory.set(project.layout.buildDirectory.dir("distributions").get().asFile) + dependsOn(publishTask) + from(project.layout.buildDirectory.dir("repos/releases").get().asFile) + } + + project.tasks.register("uploadToMavenCentral") { + group = "publishing" + description = "Uploads the central-bundle.zip to Maven Central via API" + dependsOn(zipBundle) + + doLast { + val file = zipBundle.get().archiveFile.get().asFile + val boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW" + val url = java.net.URL("https://central.sonatype.com/api/v1/publisher/upload") + + val username = project.findProperty("mavenCentralUsername") as String? + ?: System.getenv("MAVEN_CENTRAL_USERNAME") + val password = project.findProperty("mavenCentralPassword") as String? + ?: System.getenv("MAVEN_CENTRAL_PASSWORD") + val token = "$username:$password".toBase64() + + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.doOutput = true + connection.setRequestProperty("Authorization", "Bearer $token") + connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary") + + connection.outputStream.use { output -> + output.writer().use { writer -> + writer.append("--$boundary\r\n") + writer.append("Content-Disposition: form-data; name=\"bundle\"; filename=\"${file.name}\"\r\n") + writer.append("Content-Type: application/octet-stream\r\n\r\n") + writer.flush() + file.inputStream().copyTo(output) + writer.append("\r\n--$boundary--\r\n") + writer.flush() + } + } + + val responseCode = connection.responseCode + println("Response Code: $responseCode") + println("Response Message: ${connection.inputStream.bufferedReader().readText()}") + } + } + + project.tasks.findByName("publish")?.finalizedBy("uploadToMavenCentral") + } } }