Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0777ea4
modernize by windsurf - Claude Opus 4.6 Thinking 1M
dunedodo Feb 25, 2026
add2ed1
refresh test suites
dunedodo Apr 21, 2026
dbe93f3
remove secrets
dunedodo Apr 21, 2026
6cdd8d7
Fix error. Round 1
Apr 22, 2026
0f18302
Fix error. Round 1
Apr 22, 2026
6ab233b
Fix error. Round 2
Apr 22, 2026
f9c3f4d
Fix java Compatibility and destroy() function
Apr 22, 2026
49a5817
Fix test cleanup failures with short retention windows
Apr 23, 2026
aa79448
fix: useGpgCmd() for ECC keys, remove signingSecretKeyRingFile prompt
dunedodo Apr 27, 2026
a03f614
chore: ignore local workspace files
dunedodo Apr 27, 2026
c2a6e55
update release
dunedodo Apr 27, 2026
6413341
chore: ignore test.properties
dunedodo Apr 28, 2026
ada39fb
fix: expose smart-client-ecs as api so Vdc is on consumers' compile c…
dunedodo Apr 28, 2026
fda14b0
chore: ignore local release-notes drafts
dunedodo Apr 28, 2026
23bb6dd
feat: full HttpUrlConnectorProvider support for S3 and encryption cli…
dunedodo May 8, 2026
07e0cde
chore: bump smart-client-ecs dependency to 4.0.0-rc.3
dunedodo May 8, 2026
ab6efbb
Implement review comments and fix testcases
Jun 25, 2026
09e63d0
Changed smart client GA release dependency
Jun 26, 2026
f4652bc
Fixed key distribution case
Jun 26, 2026
94b86ef
Fixed key distribution case
Jun 26, 2026
1f3cde9
Merge pull request #130 from EMCECS/feature-java-modernization-claude…
sushilpawar999 Jun 26, 2026
7fcf8ff
Fix url encoding to make ensure backword compatibility is intact
Jun 26, 2026
90a1ab7
Fix review comments of source and test
Jun 27, 2026
ebb7bd4
Fix review comments of source and test
Jun 27, 2026
9c71bf7
Fix review comments of source and testcases
Jun 28, 2026
0573f76
Fix review comments of source and testcases
Jun 28, 2026
81c7058
Merge pull request #131 from EMCECS/feature-java-modernization-claude…
sushilpawar999 Jun 28, 2026
cdb22fa
Fix review comments of source and testcases
Jun 29, 2026
984d269
Addressed review comments for ChecksumFilterTest, GeoPinningTest
Jun 29, 2026
f26e755
Addressed review comments for ChecksumFilterTest, GeoPinningTest
Jun 29, 2026
a6c7269
Fix review comments of source and testcases
Jun 29, 2026
2f25d05
Fix review comments of source and testcases
Jun 29, 2026
6245ae3
Avoid race condition from testcase
Jun 29, 2026
ccf10f7
Refactor test case
Jun 29, 2026
60c3862
Added mockito-junit-jupiter
Jun 29, 2026
b6a6cc7
Merge pull request #132 from EMCECS/feature-java-modernization-claude…
sweetymahale Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ build
*.iws
dependency-reduced-pom.xml
.gradle
test.properties
234 changes: 152 additions & 82 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ import com.github.jk1.license.render.InventoryHtmlReportRenderer
plugins {
id 'idea'
id 'eclipse'
id 'java'
id 'net.saliman.cobertura' version '4.0.0'
id 'com.github.jk1.dependency-license-report' version '1.17'
id 'java-library'
id 'jacoco'
id 'com.github.jk1.dependency-license-report' version '2.9'
id 'distribution'
id 'signing'
id 'maven'
id 'org.ajoberstar.git-publish' version '3.0.1'
id 'nebula.release' version '15.3.1'
id 'maven-publish'
id 'org.ajoberstar.git-publish' version '4.2.2'
id 'nebula.release' version '21.0.0'
}

group 'com.emc.ecs'
Expand All @@ -62,17 +62,18 @@ repositories {
}

dependencies {
implementation 'com.emc.ecs:smart-client-ecs:3.0.6'
implementation 'com.sun.jersey.contribs:jersey-apache-client4:1.19.4'
// NOTE: Jackson 2.13 dropped support for JAX-RS 1.x, and we use Jersey client 1.x, so we are stuck on Jackson 1.12.x
// ref: https://github.com/FasterXML/jackson-jaxrs-providers/issues/90#issuecomment-1081368194
implementation 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.12.7'
implementation 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.12.7'
api 'com.emc.ecs:smart-client-ecs:4.0.0'
implementation 'org.glassfish.jersey.core:jersey-client:2.47'
implementation 'org.glassfish.jersey.connectors:jersey-apache-connector:2.47'
implementation 'org.glassfish.jersey.inject:jersey-hk2:2.47'
implementation 'org.glassfish.jersey.media:jersey-media-jaxb:2.47'
implementation 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.17.3'
implementation 'com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.17.3'
implementation('com.emc.ecs:object-transform:1.1.0') {
exclude group: 'org.slf4j', module: 'slf4j-log4j12'
}
implementation 'commons-codec:commons-codec:1.15'
implementation('org.dom4j:dom4j:2.1.3') {
implementation 'commons-codec:commons-codec:1.17.1'
implementation('org.dom4j:dom4j:2.1.4') {
// workaround for jdom2 bug (https://github.com/dom4j/dom4j/issues/99)
// NOTE: a component metadata rule will not solve the problem for library consumers - this is the only way
exclude module: 'pull-parser'
Expand All @@ -82,11 +83,15 @@ dependencies {
exclude module: 'stax-api'
exclude module: 'jaxb-api'
}
implementation 'org.slf4j:slf4j-api:1.7.36'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
testRuntimeOnly 'org.slf4j:jcl-over-slf4j:1.7.36'
testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0'
implementation 'org.slf4j:slf4j-api:2.0.16'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
testImplementation 'org.junit.vintage:junit-vintage-engine:5.11.4'
testImplementation 'org.mockito:mockito-core:5.14.2'
testImplementation 'org.mockito:mockito-junit-jupiter:5.14.2'
testImplementation 'org.apache.httpcomponents.client5:httpclient5:5.4.1'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly 'org.slf4j:jcl-over-slf4j:2.0.16'
testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3'
}

allprojects {
Expand All @@ -101,55 +106,25 @@ configurations {

[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'

sourceCompatibility = 1.8

def projectPom = {
project {
name project.name
description project.description
url githubProjectUrl

scm {
url githubProjectUrl
connection githubScmUrl
developerConnection githubScmUrl
}

licenses {
license {
name licenseName
url licenseUrl
distribution 'repo'
}
}

developers {
developer {
id 'EMCECS'
name 'Dell EMC ECS'
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

task writePom {
ext.pomFile = file("$buildDir/pom.xml")
outputs.file pomFile
doLast {
pom(projectPom).writeTo pomFile
}
test {
useJUnitPlatform()
maxHeapSize = '2g'
// some encryption + large file tests allocate a lot of transient buffers; give the forked JVM enough headroom
jvmArgs '-XX:+HeapDumpOnOutOfMemoryError'
}

jar {
doFirst {
manifest {
attributes 'Implementation-Version': project.version,
'Class-Path': configurations.runtime.collect { it.getName() }.join(' ')
'Class-Path': configurations.runtimeClasspath.collect { it.getName() }.join(' ')
}
}
into("META-INF/maven/$project.group/$project.name") {
from writePom
}
}

javadoc {
Expand All @@ -158,7 +133,7 @@ javadoc {

task javadocJar(type: Jar) {
archiveClassifier = 'javadoc'
from "${docsDir}/javadoc"
from javadoc.destinationDir
}
tasks.javadocJar.dependsOn javadoc

Expand Down Expand Up @@ -197,36 +172,69 @@ distributions {
}
}

signing {
required { gradle.taskGraph.hasTask(':uploadJars') }
sign configurations.jars
}

uploadJars {
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact javadocJar
artifact sourcesJar
pom {
name = project.name
description = project.description
url = githubProjectUrl
scm {
url = githubProjectUrl
connection = githubScmUrl
developerConnection = githubScmUrl
}
licenses {
license {
name = licenseName
url = licenseUrl
distribution = 'repo'
}
}
developers {
developer {
id = 'EMCECS'
name = 'Dell EMC ECS'
}
}
}
}
}
repositories {
mavenDeployer {
beforeDeployment { deployment -> signing.signPom(deployment) }

repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') {
authentication(userName: '', password: '')
maven {
// OSSRH is EOL (June 30 2025). Use the Central Portal OSSRH Staging API
// compatibility service. Credentials must now be a Portal user token
// (generate at https://central.sonatype.com/account) — not the old OSSRH password.
name = 'centralPortalOssrhStaging'
url = 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/'
credentials {
username = rootProject.findProperty('sonatypeUser') ?: ''
password = rootProject.findProperty('sonatypePass') ?: ''
}

pom projectPom
}
}
}

ext.aggregatedDocsDir = "$buildDir/aggregatedDocs"
signing {
useGpgCmd()
required { gradle.taskGraph.hasTask(':publishMavenJavaPublicationToCentralPortalOssrhStagingRepository') }
sign publishing.publications.mavenJava
}

ext.aggregatedDocsDir = "${layout.buildDirectory.get()}/aggregatedDocs"
task aggregateDocs {
doLast {
if (project.hasProperty('release.stage') && project.ext['release.stage'] == 'final') {
copy {
from docsDir
from javadoc.destinationDir
into "${aggregatedDocsDir}/latest"
}
}
copy {
from docsDir
from javadoc.destinationDir
into "${aggregatedDocsDir}/${project.version}"
}
}
Expand All @@ -243,7 +251,72 @@ gitPublish {
}
tasks.gitPublishPush.dependsOn aggregateDocs

tasks.release.dependsOn test, uploadJars, gitPublishPush, distZip
// After uploading via maven-publish (a "Maven-API-like" plugin that only does PUTs),
// we must POST to the manual upload endpoint so the deployment becomes visible in the
// Central Publisher Portal (https://central.sonatype.com/publishing/deployments).
//
// IMPORTANT details (verified against
// https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/):
// - The POST must come from the SAME IP address as the PUT uploads.
// - The /manual/* endpoints require a "Bearer <base64(user:pass)>" header
// (NOT Basic) — Sonatype's own quirk. Same credentials as the PUT upload,
// different scheme keyword.
// - publishing_type defaults to user_managed: deployment lands in the Portal
// as a "Pending" deployment that you Publish or Drop manually in the UI.
// Pass ?publishing_type=automatic to auto-release if validation passes
// (do NOT use this for an RC — review first).
task publishToPortal {
group = 'publishing'
description = 'Signals the OSSRH Staging API to transfer the staged deployment to the Central Publisher Portal for review/release'
dependsOn publishMavenJavaPublicationToCentralPortalOssrhStagingRepository
doLast {
def namespace = project.group as String // derived, not hard-coded
def portalUser = rootProject.findProperty('sonatypeUser') ?: ''
def portalPass = rootProject.findProperty('sonatypePass') ?: ''
if (!portalUser || !portalPass) {
throw new GradleException('sonatypeUser/sonatypePass not set — generate a Portal token at https://central.sonatype.com/account')
}
def endpoint = "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/${namespace}?publishing_type=user_managed"
def bearer = java.util.Base64.encoder.encodeToString(
"${portalUser}:${portalPass}".getBytes('UTF-8'))

logger.lifecycle(" Signalling Central Portal to transfer staged deployment for namespace: ${namespace}")
def conn = (java.net.HttpURLConnection) new URL(endpoint).openConnection()
try {
conn.requestMethod = 'POST'
conn.connectTimeout = 30_000 // 30 s
conn.readTimeout = 120_000 // 120 s — service may take time to inventory the repo
conn.setRequestProperty('Authorization', "Bearer ${bearer}")
conn.setRequestProperty('Content-Length', '0')
conn.setRequestProperty('Accept', 'application/json')
conn.connect()

def code = conn.responseCode
if (code in [200, 201, 202, 204]) {
logger.lifecycle(" Deployment transferred to Central Publisher Portal (HTTP ${code}).")
logger.lifecycle(' Review and publish at: https://central.sonatype.com/publishing/deployments')
} else {
def body = '(no body)'
try { if (conn.errorStream) body = new String(conn.errorStream.bytes, 'UTF-8') } catch (ignored) { }
def hint = (code == 401)
? ' → 401: Portal user token is invalid/expired. Regenerate at https://central.sonatype.com/account\n'
: (code == 404)
? " → 404: No staging repo found for IP. Did the upload actually run? Check that signing succeeded.\n"
: ''
throw new GradleException(
"Failed to transfer deployment to Central Publisher Portal: HTTP ${code}\n" +
"${hint}${body}\n" +
'Search staged repos with:\n' +
" curl -H 'Authorization: Bearer ${'<token>'}' \\\n" +
" 'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=any&profile_id=${namespace}'")
}
} finally {
conn.disconnect()
}
}
}

tasks.release.dependsOn test, publishToPortal, gitPublishPush, distZip

clean {
delete aggregatedDocsDir
Expand All @@ -252,9 +325,7 @@ clean {
// allow typing in credentials
// note: this only works when run without the Gradle daemon (--no-daemon)
gradle.taskGraph.whenReady { taskGraph ->
if (taskGraph.hasTask(':uploadJars')) {
if (!rootProject.hasProperty('signingSecretKeyRingFile'))
rootProject.ext.signingSecretKeyRingFile = new String(System.console().readLine('\nSecret key ring file: '))
if (taskGraph.hasTask(':publishMavenJavaPublicationToCentralPortalOssrhStagingRepository')) {
if (!rootProject.hasProperty('signingKeyId'))
rootProject.ext.signingKeyId = new String(System.console().readLine('\nSigning key id: '))
if (!rootProject.hasProperty('signingPass'))
Expand All @@ -264,10 +335,9 @@ gradle.taskGraph.whenReady { taskGraph ->
if (!rootProject.hasProperty('sonatypePass'))
rootProject.ext.sonatypePass = new String(System.console().readPassword('\nSonatype password: '))
ext.'signing.keyId' = rootProject.ext.signingKeyId
ext.'signing.secretKeyRingFile' = rootProject.ext.signingSecretKeyRingFile
ext.'signing.password' = rootProject.ext.signingPass
uploadJars.repositories.mavenDeployer.repository.authentication.userName = rootProject.ext.sonatypeUser
uploadJars.repositories.mavenDeployer.repository.authentication.password = rootProject.ext.sonatypePass
ext.'signing.gnupg.keyName' = rootProject.ext.signingKeyId
ext.'signing.gnupg.passphrase' = rootProject.ext.signingPass
}
if (taskGraph.hasTask(':gitPublishPush') || taskGraph.hasTask(':release')) {
if (!rootProject.hasProperty('gitUsername'))
Expand Down
55 changes: 55 additions & 0 deletions capabilities/Cap-20844.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Java Application Modernization agent instructions

## Your Role
- You are a highly sophisticated automated coding agent with expert-level knowledge in Java, popular Java frameworks and techniques of modernizing legacy Java.
- Your task is to migrate the Java project to use Java 25, Gradle 9.2.1 and Jersey 2.47. And It should be compatible with Java 17 and Java 21.

## Project knowledge
- **Tech Stack:** Java 8, Gradle 6.9.2, Jersey 1.19.4
- **File Structure:**
- `.src/**/*.*` – Application source code
- `.test/**/*.*` – Unit, Integration tests
- `../smart-client-java` - smart client repository as a dependency, which has been migrated (you should directly use smart-client-ecs:3.1.0-rc.1)

**Paths**:
- **Report Path**: `report`
- **Progress File**: `{{reportPath}}/progress.md`
- **Plan File**: `{{reportPath}}/plan.md`
- **Summary File**: `{{reportPath}}/summary.md`

## Boundaries
- **DO** think harder if you don't have good ideas
- **DO** make changes directly to code files.
- **DO** remember the version numbers are fixed and should not be changed.
- **DO** directly execute your plan and update the progress tracking file `{{progressFile}}`.
- **DO NOT** seek approval, user preferences or confirmations before making changes unless explicitly interrupted by user. Proceed with your best judgment with the next actions automatically. You DO have the highest decision-making authority at any time.

## Scope
* DO - Scan the codebase
* DO - Detection of outdated frameworks (e.g., Jersey),deprecated APIs and obsolete patterns
* DO - Web search for the correct version number of any tool or library if you cannot find it as of your knowledge cutoff
* DO - Check Maven/Gradle wrapper versions and update if necessary
* DO - Update Gradle dependencies and resolve dependency coordinates, like incompatible library versions and transitive dependency conflicts
* DO - Propose a safe, testable migration plan, and save it to `{{planFile}}`
* DO - Verify plugin versions are compatible with the new Java version and migrate when necessary
* DO - Check for removed JDK internals (e.g., sun.* packages)
* DO - Code modification to replace original technology dependencies with equivalents
* DO - Configuration file updates necessary for compilation
* DO - Look for IllegalAccessError or InaccessibleObjectException
* DO - Read stack traces carefully for ClassNotFoundException, NoSuchMethodError, or NoClassDefFoundError which often indicate dependency version mismatches or missing transitive dependencies, and use dependency analysis tools to find the source
* DO - Update all test files to use Jersey 2.x API and JUnit 5
* DO - Run "./gradlew test --tests" to run some specific tests. Run tests one by one. If a test fails, fix the issue before continuing to run and fix the next one. Then run a broader test suite after each batch. "gradlew build" or "gradlew.bat build" or "gradlew test" or "gradlew.bat test" takes a long time, so only execute it when you are sure that all your specified tests are passed
* DO - Ensure that the integrity of Java classes and methods in maintained post upgrade, and the application features must work seamlessly
* DO NOT - No Migration considerations on javax packages to jakarta packages
* Never adjust another Java version, Gradle version or Jersey version which is not defined in the task

## Success Criteria
* Codebase compiles successfully
* Code maintains functional consistency
* All tests are updated and pass
* All dependencies and imports are replaced
* All publishing configurations are updated
* No CVEs introduced during migration
* All old code files and project configurations are cleaned
* All migration tasks are tracked and completed
* Plan generated, progress tracked, and summary generated, and all the steps are all documented in the progress file
Loading