diff --git a/.gitignore b/.gitignore index bd0f220..404be72 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /log/ /build/ /.gradle/ +gradle.properties +.claude/ diff --git a/build.gradle b/build.gradle index f6a2d2d..19e30a2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,8 @@ -apply plugin: 'java' -apply plugin: 'maven' -apply plugin: 'com.bmuschko.nexus' -apply plugin: 'jacoco' - -buildscript { - repositories { - maven { - url 'https://plugins.gradle.org/m2/' - } - } - dependencies { - classpath 'com.bmuschko:gradle-nexus-plugin:2.3.1' - } +plugins { + id 'java' + id 'jacoco' + id 'maven-publish' + id 'signing' } // --- JAVADOC --- @@ -19,22 +10,21 @@ buildscript { javadoc { options.encoding = 'UTF-8' failOnError = false - exclude '**/PojoTest.java' + exclude '**/PojoTest.java' } // --- JACOCO --- jacocoTestReport { reports { - xml.enabled true - html.enabled true - } + xml.required = true + html.required = true + } } // --- CONFIGURATIONS --- configurations { - runtime ecj } @@ -46,8 +36,6 @@ configurations.all { repositories { mavenCentral() - jcenter() - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } // --- SOURCE --- @@ -72,30 +60,36 @@ sourceSets { // --- DEPENDENCIES --- dependencies { - + // ============== COMPILATION ============== - + testImplementation 'junit:junit:4.12' ecj 'org.eclipse.jdt.core.compiler:ecj:4.4.2' // ================ TESTING ================ // https://mvnrepository.com/artifact/com.openpojo/openpojo - testImplementation group: 'com.openpojo', name: 'openpojo', version: '0.8.10' - + testImplementation group: 'com.openpojo', name: 'openpojo', version: '0.8.10' + // =============== MOLECULER =============== // Moleculer core (required) implementation group: 'com.github.berkesa', name: 'moleculer-java', version: '1.2.28' - + // Moleculer developer console (optional) implementation group: 'com.github.berkesa', name: 'moleculer-java-repl', version: '1.3.1' - + + // datatree-* is declared as 'runtime' scope in moleculer-java's POM, so + // Gradle 7.x correctly excludes it from the compile classpath. We add it + // explicitly so the io.datatree API is available when compiling this module. + compileOnly group: 'com.github.berkesa', name: 'datatree-core', version: '1.1.2' + compileOnly group: 'com.github.berkesa', name: 'datatree-promise', version: '1.0.10' + // ================ LOGGING ================ - + // http://mvnrepository.com/artifact/org.slf4j/slf4j-api implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.30' - + // http://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 implementation group: 'org.slf4j', name: 'slf4j-jdk14', version: '1.7.30' @@ -106,18 +100,18 @@ dependencies { implementation group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.30' // =============== CDI FRAMEWORKS =============== - + // --- SPRING DEPENDENCY INJECTION FRAMEWORK --- // https://mvnrepository.com/artifact/org.springframework/spring-context implementation (group: 'org.springframework', name: 'spring-context', version: '5.3.7') { - exclude group: 'org.springframework', module: 'spring-jcl' + exclude group: 'org.springframework', module: 'spring-jcl' } - + // ============ WEB CONNECTORS ============= - + // --- NETTY SERVER --- - + // https://mvnrepository.com/artifact/io.netty/netty-handler implementation group: 'io.netty', name: 'netty-handler', version: '4.1.65.Final' @@ -125,12 +119,12 @@ dependencies { implementation group: 'io.netty', name: 'netty-codec-http', version: '4.1.65.Final' // --- WEBSOCKET API --- - + // https://mvnrepository.com/artifact/org.java-websocket/Java-WebSocket implementation group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.2' - + // ========== ASYNC HTTP CLIENT ============ - + // https://mvnrepository.com/artifact/org.apache.httpcomponents/httpasyncclient implementation group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.4' @@ -138,15 +132,15 @@ dependencies { implementation group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.5.13' // ============ JETTY SERVER =============== - + // https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/javax-websocket-server-impl implementation group: 'org.eclipse.jetty.websocket', name: 'javax-websocket-server-impl', version: '9.4.41.v20210516' - + // https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-continuation implementation group: 'org.eclipse.jetty', name: 'jetty-continuation', version: '9.4.41.v20210516' - + // ========== MULTIPART PARSER ============= - + // https://mvnrepository.com/artifact/org.synchronoss.cloud/nio-multipart-parser implementation group: 'org.synchronoss.cloud', name: 'nio-multipart-parser', version: '1.1.0' @@ -156,42 +150,42 @@ dependencies { // https://mvnrepository.com/artifact/com.github.berkesa/datatree-templates implementation group: 'com.github.berkesa', name: 'datatree-templates', version: '1.1.4' - + // --- FREEMARKER --- - + // https://mvnrepository.com/artifact/org.freemarker/freemarker implementation group: 'org.freemarker', name: 'freemarker', version: '2.3.31' - + // --- JADE --- - + // https://mvnrepository.com/artifact/de.neuland-bfi/jade4j implementation group: 'de.neuland-bfi', name: 'jade4j', version: '1.3.2' // --- MUSTACHE --- - + // https://mvnrepository.com/artifact/com.github.spullara.mustache.java/compiler implementation group: 'com.github.spullara.mustache.java', name: 'compiler', version: '0.9.10' // --- THYMELEAF --- - + // https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf implementation group: 'org.thymeleaf', name: 'thymeleaf', version: '3.0.12.RELEASE' // --- PEBBLE --- - + // https://mvnrepository.com/artifact/com.mitchellbosecke/pebble implementation group: 'com.mitchellbosecke', name: 'pebble', version: '2.4.0' // --- HANDLEBARS --- - + // https://mvnrepository.com/artifact/com.github.jknack/handlebars implementation group: 'com.github.jknack', name: 'handlebars', version: '4.2.0' - + // --- VELOCITY --- - + // https://mvnrepository.com/artifact/org.apache.velocity/velocity-engine-core implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3' - + } sourceCompatibility = 1.8 @@ -199,88 +193,156 @@ targetCompatibility = 1.8 group = 'com.github.berkesa' -// version = '1.3.12.1-SNAPSHOT' -version = '1.3.12' -// + JAR version - -modifyPom { - project { - artifactId 'moleculer-java-web' - name 'Java API gateway service for Moleculer' - description 'Non-blocking J2EE and Netty connector with WebSocket and SSL support.' - url 'https://moleculer-java.github.io/moleculer-java-web/' - inceptionYear '2019' - - scm { - url 'https://moleculer-java.github.io/moleculer-java-web/' - connection 'scm:https://github.com/moleculer-java/moleculer-java-web.git' - developerConnection 'scm:git://github.com/moleculer-java/moleculer-java-web.git' - } +// version = '1.3.13-SNAPSHOT' +version = '1.3.13' - licenses { - license { - name 'The MIT License' - url 'http://www.opensource.org/licenses/MIT' - distribution 'repo' - } - } - - dependencies { - dependency { - groupId 'com.github.berkesa' - artifactId 'moleculer-java' - version '1.2.28' - scope 'runtime' - } - dependency { - groupId 'org.synchronoss.cloud' - artifactId 'nio-multipart-parser' - version '1.1.0' - scope 'runtime' - } - dependency { - groupId 'io.netty' - artifactId 'netty-handler' - version '4.1.65.Final' - scope 'runtime' +// --- SOURCES AND JAVADOC JARS (required by Maven Central) --- + +java { + withJavadocJar() + withSourcesJar() +} + +// The main source set includes *.ico as resources from src/main/java, causing +// duplicates when building the sources JAR (same file via java + resources srcDir). +sourcesJar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +// --- PUBLISHING --- + +publishing { + publications { + mavenJava(MavenPublication) { + artifactId = 'moleculer-java-web' + from components.java + + // Replace auto-generated dependencies with explicit POM dependencies. + // The project has many optional dependencies (template engines, Jetty, + // Spring, etc.) that end-users should pull in themselves. Only the 4 + // required runtime deps are declared in the published POM. + pom.withXml { + def root = asNode() + def existingDeps = root.dependencies + if (existingDeps) { + root.remove(existingDeps[0]) + } + def depsNode = root.appendNode('dependencies') + [ + ['com.github.berkesa', 'moleculer-java', '1.2.28'], + ['org.synchronoss.cloud', 'nio-multipart-parser', '1.1.0'], + ['io.netty', 'netty-handler', '4.1.65.Final'], + ['io.netty', 'netty-codec-http', '4.1.65.Final'], + ].each { groupId, artifactId, version -> + def dep = depsNode.appendNode('dependency') + dep.appendNode('groupId', groupId) + dep.appendNode('artifactId', artifactId) + dep.appendNode('version', version) + dep.appendNode('scope', 'runtime') + } } - dependency { - groupId 'io.netty' - artifactId 'netty-codec-http' - version '4.1.65.Final' - scope 'runtime' + + pom { + name = 'Java API gateway service for Moleculer' + description = 'Non-blocking J2EE and Netty connector with WebSocket and SSL support.' + url = 'https://moleculer-java.github.io/moleculer-java-web/' + inceptionYear = '2019' + + scm { + url = 'https://moleculer-java.github.io/moleculer-java-web/' + connection = 'scm:https://github.com/moleculer-java/moleculer-java-web.git' + developerConnection = 'scm:git://github.com/moleculer-java/moleculer-java-web.git' + } + + licenses { + license { + name = 'The MIT License' + url = 'http://www.opensource.org/licenses/MIT' + distribution = 'repo' + } + } + + developers { + developer { + id = 'berkesa' + name = 'Andras Berkes' + email = 'andras.berkes@programmer.net' + } + } } } + } - developers { - developer { - id 'berkesa' - name 'Andras Berkes' - email 'andras.berkes@programmer.net' - } + // Local staging repository — artifacts are assembled here before bundling. + repositories { + maven { + name = 'staging' + url = layout.buildDirectory.dir('staging-deploy') } } } -nexus { - sign = true - repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' - snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots' +// --- SIGNING --- + +signing { + sign publishing.publications.mavenJava } -// --- COMPILATION --- +// --- MAVEN CENTRAL PORTAL UPLOAD --- +// +// Usage: ./gradlew publishToMavenCentralPortal +// +// Requires in ~/.gradle/gradle.properties: +// sonatypeUsername= +// sonatypePassword= + +tasks.register('bundleStagingRepo', Zip) { + dependsOn 'publishAllPublicationsToStagingRepository' + description = 'Bundles the local staging repository into a ZIP for Central Portal upload.' + archiveFileName = "bundle-${project.version}.zip" + destinationDirectory = layout.buildDirectory.dir('bundle') + from layout.buildDirectory.dir('staging-deploy') +} -compileJava { - options.fork = true - options.forkOptions.with { - executable = 'java' - jvmArgs = ['-classpath', project.configurations.ecj.asPath, 'org.eclipse.jdt.internal.compiler.batch.Main', '-nowarn'] - } +tasks.register('publishToMavenCentralPortal') { + dependsOn 'bundleStagingRepo' + description = 'Uploads the signed artifact bundle to the Maven Central Portal.' + group = 'publishing' + doLast { + def secretsFile = file('.gradle/secrets.xml') + if (!secretsFile.exists()) { + throw new GradleException("Credentials file not found: ${secretsFile.absolutePath}") + } + def xml = new XmlSlurper().parse(secretsFile) + def user = xml.username.text() + def pass = xml.password.text() + if (!user || !pass) { + throw new GradleException("Could not read username/password from ${secretsFile.absolutePath}") + } + def token = Base64.encoder.encodeToString("${user}:${pass}".bytes) + def bundleFile = bundleStagingRepo.archiveFile.get().asFile + + println "Uploading ${bundleFile.name} (${bundleFile.length()} bytes) to Maven Central Portal..." + + exec { + commandLine 'curl', + '--fail', '--silent', '--show-error', '--location', + '-H', "Authorization: Bearer ${token}", + '-F', "bundle=@${bundleFile.absolutePath}", + 'https://central.sonatype.com/api/v1/publisher/upload?publishingType=AUTOMATIC' + } + + println "Upload complete. Check https://central.sonatype.com/publishing/deployments for status." + } } +// --- COMPILATION --- +// Standard javac is used. The ECJ dependency is kept for reference but the +// old fork-based ECJ hack is not compatible with Gradle 7.x's compilation daemon. + // --- JAR --- jar { - baseName = 'moleculer-web' - version = '1.3.12' -} \ No newline at end of file + archiveBaseName = 'moleculer-web' + archiveVersion = '1.3.13' +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4fd2076..ba5b117 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip diff --git a/src/main/java/services/moleculer/web/common/GatewayUtils.java b/src/main/java/services/moleculer/web/common/GatewayUtils.java index 3393a40..d726fdc 100644 --- a/src/main/java/services/moleculer/web/common/GatewayUtils.java +++ b/src/main/java/services/moleculer/web/common/GatewayUtils.java @@ -98,7 +98,11 @@ public static final void sendError(WebResponse rsp, Throwable cause) { } Tree json = error.toTree(); byte[] body = json.toBinary(); - rsp.setStatus(error.getCode()); + int statusCode = error.getCode(); + if (statusCode < 100 || statusCode > 599) { + statusCode = 500; + } + rsp.setStatus(statusCode); rsp.setHeader(CACHE_CONTROL, NO_CACHE); rsp.setHeader(CONTENT_TYPE, CONTENT_TYPE_JSON); rsp.setHeader(CONTENT_LENGTH, Integer.toString(body.length)); diff --git a/src/main/java/services/moleculer/web/netty/NettyWebResponse.java b/src/main/java/services/moleculer/web/netty/NettyWebResponse.java index e403b01..d58e6ca 100644 --- a/src/main/java/services/moleculer/web/netty/NettyWebResponse.java +++ b/src/main/java/services/moleculer/web/netty/NettyWebResponse.java @@ -159,6 +159,20 @@ public void send(byte[] bytes) throws IOException { */ @Override public boolean end() { + + // If no response body has been written (e.g. "204 No Content", or an + // action that returned null), the response is complete with zero + // length. Emit an explicit "Content-Length: 0" so the message is + // properly framed and the keep-alive connection can be reused. Without + // it, the block below closes the channel (no Content-Length is present) + // but no "Connection: close" header is sent, so pooled HTTP clients + // (browsers, Node http.Agent/axios, ...) reuse the socket and fail the + // next request with "ECONNRESET" / "socket hang up". + if (first.get() && (req == null || req.parser == null) + && (headers == null || headers.get(CONTENT_LENGTH) == null)) { + setHeader(CONTENT_LENGTH, "0"); + } + sendHeaders(); if (req != null && req.parser != null) { try {