[Git][java-team/tomcat-jakartaee-migration][master] 6 commits: Converted debian/watch to the version 5 format
Emmanuel Bourg (@ebourg)
gitlab at salsa.debian.org
Sat Dec 20 10:49:07 GMT 2025
Emmanuel Bourg pushed to branch master at Debian Java Maintainers / tomcat-jakartaee-migration
Commits:
8c1762eb by Emmanuel Bourg at 2025-12-20T11:18:09+01:00
Converted debian/watch to the version 5 format
- - - - -
316be028 by Emmanuel Bourg at 2025-12-20T11:18:31+01:00
New upstream version 1.0.10
- - - - -
3fb141cc by Emmanuel Bourg at 2025-12-20T11:18:31+01:00
Update upstream source from tag 'upstream/1.0.10'
Update to upstream version '1.0.10'
with Debian dir b86b27697fb06fc223b2e3f0b39170bc572aeeec
- - - - -
dd5d8151 by Emmanuel Bourg at 2025-12-20T11:41:20+01:00
New dependency on libeclipse-osgi-java
- - - - -
680e81a5 by Emmanuel Bourg at 2025-12-20T11:41:27+01:00
Standards-Version updated to 4.7.2
- - - - -
1e4cbff7 by Emmanuel Bourg at 2025-12-20T11:41:36+01:00
Upload to unstable
- - - - -
21 changed files:
- .github/dependabot.yml
- .github/workflows/ci.yml
- .github/workflows/coverage.yml
- .gitignore
- CHANGES.md
- debian/changelog
- debian/control
- debian/maven.rules
- debian/watch
- pom.xml
- + src/main/java/org/apache/tomcat/jakartaee/CacheEntry.java
- src/main/java/org/apache/tomcat/jakartaee/ManifestConverter.java
- src/main/java/org/apache/tomcat/jakartaee/Migration.java
- src/main/java/org/apache/tomcat/jakartaee/MigrationCLI.java
- + src/main/java/org/apache/tomcat/jakartaee/MigrationCache.java
- src/main/resources/org/apache/tomcat/jakartaee/LocalStrings.properties
- src/test/java/org/apache/tomcat/jakartaee/ClassConverterTest.java
- src/test/java/org/apache/tomcat/jakartaee/ManifestConverterTest.java
- + src/test/java/org/apache/tomcat/jakartaee/MigrationCacheTest.java
- src/test/java/org/apache/tomcat/jakartaee/MigrationTest.java
- + src/test/resources/MANIFEST.test.MF
Changes:
=====================================
.github/dependabot.yml
=====================================
@@ -9,3 +9,8 @@ updates:
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ # Check for updates to GitHub Actions every week
+ interval: "weekly"
=====================================
.github/workflows/ci.yml
=====================================
@@ -39,15 +39,15 @@ jobs:
name: JDK${{ matrix.java }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout at v3
- - uses: actions/cache at v3.0.8
+ - uses: actions/checkout at v5
+ - uses: actions/cache at v4.3.0
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set up JDK ${{ matrix.java }}
- uses: actions/setup-java at v3
+ uses: actions/setup-java at v5
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
@@ -56,7 +56,7 @@ jobs:
continue-on-error:
true
- name: Upload logs
- uses: actions/upload-artifact at v4
+ uses: actions/upload-artifact at v5
with:
name: JDK${{ matrix.java }}-${{ matrix.os }}-logs
path: output/build/logs/TEST*.txt
=====================================
.github/workflows/coverage.yml
=====================================
@@ -33,21 +33,21 @@ jobs:
java: [ 8 ]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout at v3
- - uses: actions/cache at v3.0.8
+ - uses: actions/checkout at v5
+ - uses: actions/cache at v4.3.0
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set up JDK ${{ matrix.java }}
- uses: actions/setup-java at v3
+ uses: actions/setup-java at v5
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
- name: Build
run: mvn -V test jacoco:report --file pom.xml --no-transfer-progress
- name: Upload coverage to Codecov
- uses: codecov/codecov-action at v3
+ uses: codecov/codecov-action at v5
with:
files: ./target/site/jacoco/jacoco.xml
=====================================
.gitignore
=====================================
@@ -1,4 +1,5 @@
.classpath
+.checkstyle
.project
.settings
target
=====================================
CHANGES.md
=====================================
@@ -1,5 +1,15 @@
# Tomcat Migration Tool for Jakarta EE - Changelog
+## 1.0.10
+- When migrating files in place, don't replace the original file if no conversion has taken place. Based on PR[#78] by Semiao Marco.
+- When converting a file in an archive, update the last modified time for that archive entry. Based on PR[#78] by Semiao Marco.
+- Correctly handle OSGi headers. PR[#54] by Kyle Smith.
+- Add an option to cache migrated JARs. PR[#87] by Aaron Cosand.
+- Update ASF parent POM 34. (dependabot/markt)
+- Update Commons BCEL to 6.11.0. (dependabot/remm)
+- Update Commons Compress to 1.28.0. (dependabot/remm)
+- Update Commons IO to 2.21.0. (dependabot/remm)
+
## 1.0.9
- Update the JaCoCo Maven plugin to 0.8.12. (dependabot/markt)
- Update Commons BCEL to 6.10.0. (dependabot/markt)
=====================================
debian/changelog
=====================================
@@ -1,3 +1,12 @@
+tomcat-jakartaee-migration (1.0.10-2) unstable; urgency=medium
+
+ * New upstream release
+ - New dependency on libeclipse-osgi-java
+ * Converted debian/watch to the version 5 format
+ * Standards-Version updated to 4.7.2
+
+ -- Emmanuel Bourg <ebourg at apache.org> Sat, 20 Dec 2025 11:41:30 +0100
+
tomcat-jakartaee-migration (1.0.9-1) unstable; urgency=medium
* New upstream release
=====================================
debian/control
=====================================
@@ -9,10 +9,11 @@ Build-Depends:
junit4 (>= 4.12),
libbcel-java (>= 6.4.1),
libcommons-io-java (>= 2.6),
+ libeclipse-osgi-java,
libmaven-antrun-plugin-java,
libmaven-shade-plugin-java,
maven-debian-helper (>= 2.1)
-Standards-Version: 4.7.0
+Standards-Version: 4.7.2
Vcs-Git: https://salsa.debian.org/java-team/tomcat-jakartaee-migration.git
Vcs-Browser: https://salsa.debian.org/java-team/tomcat-jakartaee-migration
Homepage: https://tomcat.apache.org
=====================================
debian/maven.rules
=====================================
@@ -2,3 +2,4 @@
junit junit jar s/4\..*/4.x/ * *
org.apache.tomcat jakartaee-migration jar s/.*/debian/ * *
s/ant/org.apache.ant/ * * s/.*/debian/ * *
+s/org.eclipse.platform/org.eclipse.osgi/ org.eclipse.osgi * s/.*/debian/ * *
=====================================
debian/watch
=====================================
@@ -1,3 +1,4 @@
-version=4
-opts="mode=git,repack,compression=xz" \
-https://github.com/apache/tomcat-jakartaee-migration refs/tags/([\d\.]+)
+Version: 5
+Template: GitHub
+Owner: apache
+Project: tomcat-jakartaee-migration
=====================================
pom.xml
=====================================
@@ -21,12 +21,12 @@
<parent>
<groupId>org.apache</groupId>
<artifactId>apache</artifactId>
- <version>33</version>
+ <version>35</version>
</parent>
<groupId>org.apache.tomcat</groupId>
<artifactId>jakartaee-migration</artifactId>
- <version>1.0.9</version>
+ <version>1.0.10</version>
<name>Apache Tomcat Migration Tool for Jakarta EE</name>
<description>The aim of the tool is to take a web application written for Java EE 8 that
@@ -62,7 +62,7 @@
<scm>
<connection>scm:git:https://gitbox.apache.org/repos/asf/tomcat-jakartaee-migration.git</connection>
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/tomcat-jakartaee-migration.git</developerConnection>
- <tag>1.0.9</tag>
+ <tag>1.0.10</tag>
<url>https://gitbox.apache.org/repos/asf?p=tomcat-jakartaee-migration.git</url>
</scm>
@@ -77,17 +77,17 @@
<dependency>
<groupId>org.apache.bcel</groupId>
<artifactId>bcel</artifactId>
- <version>6.10.0</version>
+ <version>6.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
- <version>1.27.1</version>
+ <version>1.28.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
- <version>2.18.0</version>
+ <version>2.21.0</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
@@ -95,6 +95,11 @@
<version>1.10.15</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.platform</groupId>
+ <artifactId>org.eclipse.osgi</artifactId>
+ <version>3.18.600</version>
+ </dependency>
<!-- Test dependencies -->
<dependency>
@@ -103,6 +108,7 @@
<version>4.13.2</version>
<scope>test</scope>
</dependency>
+
</dependencies>
<profiles>
@@ -270,12 +276,25 @@
<exclude>META-INF/**</exclude>
</excludes>
</filter>
+ <filter>
+ <artifact>commons-codec:*</artifact>
+ <excludes>
+ <exclude>META-INF/**</exclude>
+ </excludes>
+ </filter>
<filter>
<artifact>commons-io:*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</filter>
+ <filter>
+ <artifact>org.eclipse.platform:*</artifact>
+ <excludes>
+ <exclude>META-INF/**</exclude>
+ <exclude>module-info.class</exclude>
+ </excludes>
+ </filter>
</filters>
<relocations>
<relocation>
@@ -301,7 +320,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
- <version>0.8.12</version>
+ <version>0.8.14</version>
<executions>
<execution>
<goals>
=====================================
src/main/java/org/apache/tomcat/jakartaee/CacheEntry.java
=====================================
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.tomcat.jakartaee;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Represents a single cache entry with operations for reading and writing.
+ * Package-private - only created by MigrationCache.
+ */
+class CacheEntry {
+
+ private static final StringManager sm = StringManager.getManager(CacheEntry.class);
+
+ private final String hash;
+ private final boolean exists;
+ private final File cacheFile;
+ private final File tempFile;
+
+ CacheEntry(String hash, boolean exists, File cacheFile, File tempFile) {
+ this.hash = hash;
+ this.exists = exists;
+ this.cacheFile = cacheFile;
+ this.tempFile = tempFile;
+ }
+
+ /**
+ * Check if this entry exists in the cache.
+ * @return true if cached
+ */
+ public boolean exists() {
+ return exists;
+ }
+
+ /**
+ * Get the hash for this cache entry.
+ * @return the hash string
+ */
+ public String getHash() {
+ return hash;
+ }
+
+ /**
+ * Copy cached content to destination output stream.
+ * @param dest the destination output stream
+ * @throws IOException if an I/O error occurs
+ */
+ public void copyToDestination(OutputStream dest) throws IOException {
+ if (!exists) {
+ throw new IllegalStateException(sm.getString("cacheEntry.copyNotExist"));
+ }
+ try (FileInputStream fis = new FileInputStream(cacheFile)) {
+ IOUtils.copy(fis, dest);
+ }
+ }
+
+ /**
+ * Begin storing to cache - returns an output stream to a temp file.
+ * @return output stream to write converted content to
+ * @throws IOException if an I/O error occurs
+ */
+ public OutputStream beginStore() throws IOException {
+ return new FileOutputStream(tempFile);
+ }
+
+ /**
+ * Commit the store operation - move temp file to final cache location.
+ * @throws IOException if an I/O error occurs
+ */
+ public void commitStore() throws IOException {
+ if (!tempFile.exists()) {
+ throw new IOException(sm.getString("cacheEntry.tempNotExist", tempFile));
+ }
+ // Ensure parent directory exists
+ File parentDir = cacheFile.getParentFile();
+ if (!parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ // Atomic rename
+ if (!tempFile.renameTo(cacheFile)) {
+ throw new IOException(sm.getString("cacheEntry.tempRenameFail", tempFile, cacheFile));
+ }
+ }
+
+ /**
+ * Get the size of the cached file in bytes.
+ * @return the file size in bytes
+ */
+ public long getFileSize() {
+ return cacheFile.length();
+ }
+
+ /**
+ * Rollback the store operation - delete temp file.
+ */
+ public void rollbackStore() {
+ if (tempFile.exists()) {
+ tempFile.delete();
+ }
+ }
+}
=====================================
src/main/java/org/apache/tomcat/jakartaee/ManifestConverter.java
=====================================
@@ -33,19 +33,25 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
+import org.eclipse.osgi.util.ManifestElement;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
/**
* Updates Manifests.
*/
public class ManifestConverter implements Converter {
+ private static final String JAKARTA_SERVLET = "jakarta.servlet";
+ private static final Pattern SERVLET_PATTERN = Pattern.compile("jakarta.servlet([^,]*);version=\"(.*?)\"");
private static final Logger logger = Logger.getLogger(ManifestConverter.class.getCanonicalName());
private static final StringManager sm = StringManager.getManager(ManifestConverter.class);
/**
* Manifest converter constructor.
*/
- public ManifestConverter() {}
+ public ManifestConverter() {
+ }
@Override
public boolean accepts(String filename) {
@@ -63,7 +69,8 @@ public class ManifestConverter implements Converter {
Manifest srcManifest = new Manifest(new ByteArrayInputStream(srcBytes));
Manifest destManifest = new Manifest(srcManifest);
- // Only consider profile conversions, allowing Migration.hasConverted to be true only when there are actual
+ // Only consider profile conversions, allowing Migration.hasConverted to be true
+ // only when there are actual
// conversions made
boolean converted = updateValues(destManifest, profile);
removeSignatures(destManifest);
@@ -80,7 +87,6 @@ public class ManifestConverter implements Converter {
return converted;
}
-
private void removeSignatures(Manifest manifest) {
manifest.getMainAttributes().remove(Attributes.Name.SIGNATURE_VERSION);
List<String> signatureEntries = new ArrayList<>();
@@ -98,7 +104,6 @@ public class ManifestConverter implements Converter {
}
}
-
private boolean isCryptoSignatureEntry(Attributes attributes) {
for (Object attributeKey : attributes.keySet()) {
if (attributeKey.toString().endsWith("-Digest")) {
@@ -108,7 +113,6 @@ public class ManifestConverter implements Converter {
return false;
}
-
private boolean updateValues(Manifest manifest, EESpecProfile profile) {
boolean converted = updateValues(manifest.getMainAttributes(), profile);
for (Attributes attributes : manifest.getEntries().values()) {
@@ -117,7 +121,6 @@ public class ManifestConverter implements Converter {
return converted;
}
-
private boolean updateValues(Attributes attributes, EESpecProfile profile) {
boolean converted = false;
// Update version info
@@ -128,11 +131,27 @@ public class ManifestConverter implements Converter {
// Purposefully avoid setting result
}
// Update package names in values
- for (Entry<Object,Object> entry : attributes.entrySet()) {
+ for (Entry<Object, Object> entry : attributes.entrySet()) {
String newValue = profile.convert((String) entry.getValue());
- newValue = replaceVersion(newValue);
+ String header = entry.getKey().toString();
+ try {
+ // Need to be careful with OSGI headers.
+ // Specifically, Export-Package cannot specify a version range.
+ // There may be other weird things as well (like directives that have
+ // jakarta.servlet packages).
+ if (Constants.IMPORT_PACKAGE.equals(header)) {
+ newValue = processImportPackage(newValue);
+ } else if (Constants.EXPORT_PACKAGE.equals(header)) {
+ newValue = processExportPackage(newValue);
+ } else {
+ newValue = replaceVersion(newValue);
+ }
+ } catch (BundleException e) {
+ newValue = replaceVersion(newValue, !Constants.EXPORT_PACKAGE.equals(header));
+ }
+
// Object comparison is deliberate
- if (newValue != entry.getValue()) {
+ if (!newValue.equals(entry.getValue())) {
entry.setValue(newValue);
converted = true;
}
@@ -140,12 +159,46 @@ public class ManifestConverter implements Converter {
return converted;
}
+ private String processExportPackage(String value) throws BundleException {
+ return processOSGIHeader(value, Constants.EXPORT_PACKAGE, "5.0.0");
+ }
+
+ private String processImportPackage(String value) throws BundleException {
+ return processOSGIHeader(value, Constants.IMPORT_PACKAGE, "[5.0.0,7.0.0)");
+ }
+
+ private String processOSGIHeader(String value, String header, String replacement) throws BundleException {
+ List<String> packages = new ArrayList<>();
+ ManifestElement[] elements = ManifestElement.parseHeader(header, value);
+ for (ManifestElement element : elements) {
+ if (element.getValue().startsWith(JAKARTA_SERVLET)) {
+ String oldVersion = element.getAttribute(Constants.VERSION_ATTRIBUTE);
+ if (oldVersion != null) {
+ packages.add(element.toString().replace(oldVersion, replacement));
+ } else {
+ packages.add(element.toString());
+ }
+ } else {
+ packages.add(element.toString());
+ }
+ }
+ if (packages.isEmpty()) {
+ return value;
+ }
+ return String.join(",", packages);
+ }
+
private String replaceVersion(String entryValue) {
- if (entryValue.contains("jakarta.servlet")) {
+ return replaceVersion(entryValue, true);
+ }
+
+ private String replaceVersion(String entryValue, boolean range) {
+ if (entryValue.contains(JAKARTA_SERVLET)) {
StringBuffer builder = new StringBuffer();
- Matcher matcher = Pattern.compile("jakarta.servlet([^,]*);version=\"(.*?)\"").matcher(entryValue);
+ Matcher matcher = SERVLET_PATTERN.matcher(entryValue);
while (matcher.find()) {
- matcher.appendReplacement(builder, "jakarta.servlet$1;version=\"[5.0.0,7.0.0)\"");
+ String version = range ? "[5.0.0,7.0.0)" : "5.0.0";
+ matcher.appendReplacement(builder, "jakarta.servlet$1;version=\"" + version + "\"");
}
matcher.appendTail(builder);
return builder.toString();
=====================================
src/main/java/org/apache/tomcat/jakartaee/Migration.java
=====================================
@@ -24,6 +24,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
@@ -106,6 +107,7 @@ public class Migration {
private File destination;
private final List<Converter> converters;
private final Set<String> excludes = new HashSet<>();
+ private MigrationCache cache;
/**
* Construct a new migration tool instance.
@@ -210,6 +212,14 @@ public class Migration {
this.destination = destination;
}
+ /**
+ * Set the migration cache for storing pre-converted archives.
+ * @param cache the migration cache instance (null to disable caching)
+ */
+ public void setCache(MigrationCache cache) {
+ this.cache = cache;
+ }
+
/**
* <b>NOTE</b>:
@@ -256,6 +266,12 @@ public class Migration {
}
}
state = State.COMPLETE;
+
+ // Finalize cache operations (save metadata and prune expired entries)
+ if (cache != null) {
+ cache.finalizeCacheOperations();
+ }
+
logger.log(Level.INFO, sm.getString("migration.done",
Long.valueOf(TimeUnit.MILLISECONDS.convert(System.nanoTime() - t1, TimeUnit.NANOSECONDS))));
}
@@ -279,32 +295,37 @@ public class Migration {
}
private void migrateFile(File src, File dest) throws IOException {
- boolean inplace = src.equals(dest);
- if (!inplace) {
- try (InputStream is = new FileInputStream(src);
- OutputStream os = new FileOutputStream(dest)) {
- migrateStream(src.getAbsolutePath(), is, os);
- }
- } else {
+ if (src.equals(dest)) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream((int) (src.length() * 1.05));
try (InputStream is = new FileInputStream(src)) {
- migrateStream(src.getAbsolutePath(), is, buffer);
+ if (migrateStream(src.getAbsolutePath(), is, buffer)) {
+ converted = true;
+ } else {
+ return;
+ }
}
try (OutputStream os = new FileOutputStream(dest)) {
os.write(buffer.toByteArray());
}
+ } else {
+ try (InputStream is = new FileInputStream(src);
+ OutputStream os = new FileOutputStream(dest)) {
+ converted = migrateStream(src.getAbsolutePath(), is, os);
+ }
}
}
- private void migrateArchiveStreaming(InputStream src, OutputStream dest) throws IOException {
+ private boolean migrateArchiveStreaming(InputStream src, OutputStream dest) throws IOException {
+ boolean convertedArchive = false;
try (ZipArchiveInputStream srcZipStream = new ZipArchiveInputStream(CloseShieldInputStream.wrap(src));
ZipArchiveOutputStream destZipStream = new ZipArchiveOutputStream(CloseShieldOutputStream.wrap(dest))) {
ZipArchiveEntry srcZipEntry;
CRC32 crc32 = new CRC32();
while ((srcZipEntry = srcZipStream.getNextEntry()) != null) {
+ boolean convertedStream = false;
String srcName = srcZipEntry.getName();
if (isSignatureFile(srcName)) {
logger.log(Level.WARNING, sm.getString("migration.skipSignatureFile", srcName));
@@ -322,12 +343,15 @@ public class Migration {
String destName = profile.convert(srcName);
if (srcZipEntry.getMethod() == ZipEntry.STORED) {
ByteArrayOutputStream tempBuffer = new ByteArrayOutputStream((int) (srcZipEntry.getSize() * 1.05));
- migrateStream(srcName, srcZipStream, tempBuffer);
+ convertedStream = migrateStream(srcName, srcZipStream, tempBuffer);
crc32.update(tempBuffer.toByteArray(), 0, tempBuffer.size());
MigrationZipArchiveEntry destZipEntry = new MigrationZipArchiveEntry(srcZipEntry);
destZipEntry.setName(destName);
destZipEntry.setSize(tempBuffer.size());
destZipEntry.setCrc(crc32.getValue());
+ if (convertedStream) {
+ destZipEntry.setLastModifiedTime(FileTime.fromMillis(System.currentTimeMillis()));
+ }
destZipStream.putArchiveEntry(destZipEntry);
tempBuffer.writeTo(destZipStream);
destZipStream.closeArchiveEntry();
@@ -336,15 +360,21 @@ public class Migration {
MigrationZipArchiveEntry destZipEntry = new MigrationZipArchiveEntry(srcZipEntry);
destZipEntry.setName(destName);
destZipStream.putArchiveEntry(destZipEntry);
- migrateStream(srcName, srcZipStream, destZipStream);
+ convertedStream = migrateStream(srcName, srcZipStream, destZipStream);
+ if (convertedStream) {
+ destZipEntry.setLastModifiedTime(FileTime.fromMillis(System.currentTimeMillis()));
+ }
destZipStream.closeArchiveEntry();
}
+ convertedArchive = convertedArchive || convertedStream;
}
}
+ return convertedArchive;
}
- private void migrateArchiveInMemory(InputStream src, OutputStream dest) throws IOException {
+ private boolean migrateArchiveInMemory(InputStream src, OutputStream dest) throws IOException {
+ boolean convertedArchive = false;
// Read the source into memory
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copy(src, baos);
@@ -367,14 +397,20 @@ public class Migration {
MigrationZipArchiveEntry destZipEntry = new MigrationZipArchiveEntry(srcZipEntry);
destZipEntry.setName(destName);
destZipStream.putArchiveEntry(destZipEntry);
- migrateStream(srcName, srcZipFile.getInputStream(srcZipEntry), destZipStream);
+ boolean convertedStream = migrateStream(srcName, srcZipFile.getInputStream(srcZipEntry), destZipStream);
+ if (convertedStream) {
+ destZipEntry.setLastModifiedTime(FileTime.fromMillis(System.currentTimeMillis()));
+ }
destZipStream.closeArchiveEntry();
+ convertedArchive = convertedArchive || convertedStream;
}
}
// Write the destination back to the stream
ByteArrayInputStream bais = new ByteArrayInputStream(destByteChannel.array(), 0, (int) destByteChannel.size());
IOUtils.copy(bais, dest);
+
+ return convertedArchive;
}
@@ -388,28 +424,78 @@ public class Migration {
}
- private void migrateStream(String name, InputStream src, OutputStream dest) throws IOException {
+ private boolean migrateStream(String name, InputStream src, OutputStream dest) throws IOException {
+ boolean convertedStream = false;
if (isExcluded(name)) {
Util.copy(src, dest);
logger.log(Level.INFO, sm.getString("migration.skip", name));
} else if (isArchive(name)) {
- if (zipInMemory) {
- logger.log(Level.INFO, sm.getString("migration.archive.memory", name));
- migrateArchiveInMemory(src, dest);
- logger.log(Level.INFO, sm.getString("migration.archive.complete", name));
- } else {
- logger.log(Level.INFO, sm.getString("migration.archive.stream", name));
- migrateArchiveStreaming(src, dest);
- logger.log(Level.INFO, sm.getString("migration.archive.complete", name));
+ // Only cache nested archives (e.g., JARs inside WARs), not top-level files
+ // Top-level files will have absolute paths starting with "/"
+ boolean isNestedArchive = !name.startsWith("/") && !name.startsWith("\\");
+
+ CacheEntry cacheEntry = null;
+ if (isNestedArchive && cache != null) {
+ // Buffer source to compute hash and check cache
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ IOUtils.copy(src, buffer);
+ byte[] sourceBytes = buffer.toByteArray();
+
+ // Get cache entry (computes hash and marks as accessed)
+ cacheEntry = cache.getCacheEntry(sourceBytes, profile);
+
+ if (cacheEntry.exists()) {
+ // Cache hit! Copy cached result to dest and return
+ logger.log(Level.INFO, sm.getString("cache.hit", name, cacheEntry.getHash()));
+ cacheEntry.copyToDestination(dest);
+ return true;
+ }
+
+ // Cache miss - use buffered source for conversion
+ logger.log(Level.FINE, sm.getString("cache.miss", name, cacheEntry.getHash()));
+ src = new ByteArrayInputStream(sourceBytes);
+ }
+
+ // Process archive - stream directly to destination (and cache if needed)
+ OutputStream targetOutputStream = dest;
+ if (cacheEntry != null) {
+ // Tee output to both destination and cache temp file
+ targetOutputStream = new org.apache.commons.io.output.TeeOutputStream(dest, cacheEntry.beginStore());
+ }
+
+ try {
+ if (zipInMemory) {
+ logger.log(Level.INFO, sm.getString("migration.archive.memory", name));
+ convertedStream = migrateArchiveInMemory(src, targetOutputStream);
+ logger.log(Level.INFO, sm.getString("migration.archive.complete", name));
+ } else {
+ logger.log(Level.INFO, sm.getString("migration.archive.stream", name));
+ convertedStream = migrateArchiveStreaming(src, targetOutputStream);
+ logger.log(Level.INFO, sm.getString("migration.archive.complete", name));
+ }
+
+ // Commit to cache on success
+ if (cacheEntry != null) {
+ cacheEntry.commitStore();
+ logger.log(Level.FINE, sm.getString("cache.store", cacheEntry.getHash(),
+ Long.valueOf(cacheEntry.getFileSize())));
+ }
+ } catch (IOException e) {
+ // Rollback cache on error
+ if (cacheEntry != null) {
+ cacheEntry.rollbackStore();
+ }
+ throw e;
}
} else {
for (Converter converter : converters) {
if (converter.accepts(name)) {
- converted = converted | converter.convert(name, src, dest, profile);
+ convertedStream = converter.convert(name, src, dest, profile);
break;
}
}
}
+ return convertedStream;
}
private boolean isArchive(String fileName) {
=====================================
src/main/java/org/apache/tomcat/jakartaee/MigrationCLI.java
=====================================
@@ -38,6 +38,9 @@ public class MigrationCLI {
private static final String PROFILE_ARG = "-profile=";
private static final String ZIPINMEMORY_ARG = "-zipInMemory";
private static final String MATCHEXCLUDESPATH_ARG ="-matchExcludesAgainstPathName";
+ private static final String CACHE_ARG = "-cache";
+ private static final String CACHE_LOCATION_ARG = "-cacheLocation=";
+ private static final String CACHE_RETENTION_ARG = "-cacheRetention=";
/**
* Build the migration tool CLI instance.
@@ -55,7 +58,12 @@ public class MigrationCLI {
System.setProperty("java.util.logging.SimpleFormatter.format", "%5$s%n");
Migration migration = new Migration();
- // Process argumnets
+ // Cache settings - disabled by default
+ File cacheDir = null;
+ boolean enableCache = false;
+ int cacheRetentionDays = 30; // Default retention period
+
+ // Process arguments
List<String> arguments = new ArrayList<>(Arrays.asList(args));
// Process the custom log level if present
@@ -95,6 +103,29 @@ public class MigrationCLI {
} else if (argument.equals(MATCHEXCLUDESPATH_ARG)) {
iter.remove();
migration.setMatchExcludesAgainstPathName(true);
+ } else if (argument.equals(CACHE_ARG)) {
+ iter.remove();
+ enableCache = true;
+ // Use default cache directory if not specified via -cacheLocation
+ if (cacheDir == null) {
+ cacheDir = new File(System.getProperty("user.home"), ".migration-cache");
+ }
+ } else if (argument.startsWith(CACHE_LOCATION_ARG)) {
+ iter.remove();
+ enableCache = true;
+ String cachePath = argument.substring(CACHE_LOCATION_ARG.length());
+ cacheDir = new File(cachePath);
+ } else if (argument.startsWith(CACHE_RETENTION_ARG)) {
+ iter.remove();
+ String retentionStr = argument.substring(CACHE_RETENTION_ARG.length());
+ try {
+ cacheRetentionDays = Integer.parseInt(retentionStr);
+ if (cacheRetentionDays < 1) {
+ invalidArguments();
+ }
+ } catch (NumberFormatException e) {
+ invalidArguments();
+ }
}
}
@@ -108,6 +139,11 @@ public class MigrationCLI {
migration.setSource(new File(source));
migration.setDestination(new File(dest));
+ if (enableCache) {
+ MigrationCache migrationCache = new MigrationCache(cacheDir, cacheRetentionDays);
+ migration.setCache(migrationCache);
+ }
+
migration.execute();
}
=====================================
src/main/java/org/apache/tomcat/jakartaee/MigrationCache.java
=====================================
@@ -0,0 +1,461 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.tomcat.jakartaee;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Cache for storing and retrieving pre-converted archive files.
+ *
+ * <h2>Cache Structure</h2>
+ * <p>The cache organizes files in a directory structure based on hash values:</p>
+ * <pre>
+ * {cacheDir}/
+ * ├── cache-metadata.txt # Metadata file tracking access times
+ * ├── {XX}/ # Subdirectory named by first 2 chars of hash
+ * │ └── {hash}.jar # Cached converted archive (full SHA-256 hash)
+ * ├── {YY}/
+ * │ └── {hash}.jar
+ * └── temp-{uuid}.tmp # Temporary files during conversion
+ * </pre>
+ *
+ * <h2>Cache Key</h2>
+ * <p>Each cache entry is keyed by a SHA-256 hash computed from:</p>
+ * <ul>
+ * <li>The migration profile name (e.g., "TOMCAT", "EE")</li>
+ * <li>The pre-conversion archive content (as bytes)</li>
+ * </ul>
+ * <p>This ensures that the same archive converted with different profiles
+ * produces different cache entries.</p>
+ *
+ * <h2>Metadata Format</h2>
+ * <p>The {@code cache-metadata.txt} file tracks access times for cache pruning:</p>
+ * <pre>
+ * # Migration cache metadata - hash|last_access_date
+ * {hash}|{YYYY-MM-DD}
+ * {hash}|{YYYY-MM-DD}
+ * </pre>
+ *
+ * <h2>Temporary Files</h2>
+ * <p>During conversion, output is written to temporary files named {@code temp-{uuid}.tmp}.
+ * These files are cleaned up on startup to handle crashes or unexpected shutdowns.</p>
+ */
+public class MigrationCache {
+
+ private static final Logger logger = Logger.getLogger(MigrationCache.class.getCanonicalName());
+ private static final StringManager sm = StringManager.getManager(MigrationCache.class);
+ private static final String METADATA_FILE = "cache-metadata.txt";
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
+
+ private final File cacheDir;
+ private final int retentionDays;
+ private final Map<String, LocalDate> cacheMetadata;
+ private final File metadataFile;
+
+ /**
+ * Construct a new migration cache.
+ *
+ * @param cacheDir the directory to store cached files (null to disable caching)
+ * @param retentionDays the number of days to retain cached files
+ * @throws IOException if the cache directory cannot be created
+ */
+ public MigrationCache(File cacheDir, int retentionDays) throws IOException {
+ this.retentionDays = retentionDays;
+ this.cacheMetadata = new HashMap<>();
+ this.cacheDir = cacheDir;
+ this.metadataFile = cacheDir == null ? null : new File(cacheDir, METADATA_FILE);
+
+ if (cacheDir == null) {
+ throw new IllegalStateException(sm.getString("cache.nullDirectory"));
+ }
+
+ // Create cache directory if it doesn't exist
+ if (!cacheDir.exists()) {
+ if (!cacheDir.mkdirs()) {
+ throw new IOException(sm.getString("cache.cannotCreate", cacheDir.getAbsolutePath()));
+ }
+ }
+
+ if (!cacheDir.isDirectory()) {
+ throw new IOException(sm.getString("cache.notDirectory", cacheDir.getAbsolutePath()));
+ }
+
+ // Load existing metadata
+ loadMetadata();
+
+ // Clean up any orphaned temp files from previous crashes
+ cleanupTempFiles();
+
+ logger.log(Level.INFO,
+ sm.getString("cache.enabled", cacheDir.getAbsolutePath(), Integer.valueOf(retentionDays)));
+ }
+
+ /**
+ * Clean up any temporary files left over from previous crashes or unexpected shutdowns.
+ * Scans the cache directory for temp-*.tmp files and deletes them.
+ */
+ private void cleanupTempFiles() {
+ File[] files = cacheDir.listFiles();
+ if (files != null) {
+ int cleanedCount = 0;
+ for (File file : files) {
+ if (file.isFile() && file.getName().startsWith("temp-") && file.getName().endsWith(".tmp")) {
+ if (file.delete()) {
+ cleanedCount++;
+ logger.log(Level.FINE, sm.getString("cache.tempfile.cleaned", file.getName()));
+ } else {
+ logger.log(Level.WARNING, sm.getString("cache.tempfile.cleanFailed", file.getName()));
+ }
+ }
+ }
+ if (cleanedCount > 0) {
+ logger.log(Level.INFO, sm.getString("cache.tempfiles.cleaned", Integer.valueOf(cleanedCount)));
+ }
+ }
+ }
+
+ /**
+ * Load cache metadata from disk.
+ * Format: hash|YYYY-MM-DD
+ * If file doesn't exist or is corrupt, assumes all existing cached jars were accessed today.
+ */
+ private void loadMetadata() {
+ LocalDate today = LocalDate.now();
+
+ if (!metadataFile.exists()) {
+ // Metadata file doesn't exist - scan cache directory and assume all files accessed today
+ logger.log(Level.FINE, sm.getString("cache.metadata.notFound"));
+ scanCacheDirectory(today);
+ return;
+ }
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(metadataFile))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty() || line.startsWith("#")) {
+ continue;
+ }
+
+ String[] parts = line.split("\\|");
+ if (parts.length == 2) {
+ String hash = parts[0];
+ try {
+ LocalDate lastAccessed = LocalDate.parse(parts[1], DATE_FORMATTER);
+ cacheMetadata.put(hash, lastAccessed);
+ } catch (DateTimeParseException e) {
+ logger.log(Level.WARNING, sm.getString("cache.metadata.invalidDate", line));
+ }
+ } else {
+ logger.log(Level.WARNING, sm.getString("cache.metadata.invalidLine", line));
+ }
+ }
+
+ // Check for any cached files not in metadata and add them with today's date
+ Set<String> existingHashes = scanCacheDirectory(null);
+ for (String hash : existingHashes) {
+ if (!cacheMetadata.containsKey(hash)) {
+ cacheMetadata.put(hash, today);
+ }
+ }
+
+ logger.log(Level.FINE, sm.getString("cache.metadata.loaded", Integer.valueOf(cacheMetadata.size())));
+ } catch (IOException e) {
+ // Corrupt or unreadable - assume all cached files accessed today
+ logger.log(Level.WARNING, sm.getString("cache.metadata.loadError"), e);
+ cacheMetadata.clear();
+ scanCacheDirectory(today);
+ }
+ }
+
+ /**
+ * Scan cache directory for existing cache files and return their hashes.
+ * If accessDate is not null, adds all found hashes to metadata with that date.
+ *
+ * @param accessDate the date to use for all found files (null to not update metadata)
+ * @return set of hashes found in cache directory
+ */
+ private Set<String> scanCacheDirectory(LocalDate accessDate) {
+ Set<String> hashes = new HashSet<>();
+
+ File[] subdirs = cacheDir.listFiles();
+ if (subdirs != null) {
+ for (File subdir : subdirs) {
+ if (subdir.isDirectory()) {
+ File[] files = subdir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile() && file.getName().endsWith(".jar")) {
+ String hash = file.getName().substring(0, file.getName().length() - 4);
+ hashes.add(hash);
+ if (accessDate != null) {
+ cacheMetadata.put(hash, accessDate);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return hashes;
+ }
+
+ /**
+ * Get a cache entry for the given source bytes and profile.
+ * This computes the hash, checks if cached, and marks the entry as accessed.
+ *
+ * @param sourceBytes the pre-conversion content
+ * @param profile the migration profile being used
+ * @return a CacheEntry object with all operations for this entry
+ * @throws IOException if an I/O error occurs
+ */
+ public CacheEntry getCacheEntry(byte[] sourceBytes, EESpecProfile profile) throws IOException {
+ // Compute hash once (includes profile)
+ String hash = computeHash(sourceBytes, profile);
+
+ // Get cache file location
+ File cachedFile = getCacheFile(hash);
+ boolean exists = cachedFile.exists();
+
+ // Create temp file for storing
+ File tempFile = new File(cacheDir, "temp-" + UUID.randomUUID() + ".tmp");
+
+ // Mark as accessed now
+ updateAccessTime(hash);
+
+ return new CacheEntry(hash, exists, cachedFile, tempFile);
+ }
+
+
+ /**
+ * Get the cache file for a given hash.
+ *
+ * @param hash the hash string
+ * @return the cache file
+ */
+ private File getCacheFile(String hash) {
+ // Use subdirectories based on first 2 chars of hash to avoid too many files in one directory
+ String subdir = hash.substring(0, 2);
+ File subdirFile = new File(cacheDir, subdir);
+ if (!subdirFile.exists()) {
+ subdirFile.mkdirs();
+ }
+ return new File(subdirFile, hash + ".jar");
+ }
+
+ /**
+ * Compute SHA-256 hash of the given bytes combined with the profile name.
+ * The profile is included to ensure different profiles produce different cache entries.
+ *
+ * @param bytes the bytes to hash
+ * @param profile the migration profile
+ * @return the hash as a hex string
+ * @throws IOException if hashing fails
+ */
+ private String computeHash(byte[] bytes, EESpecProfile profile) throws IOException {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ // Include profile name in hash to differentiate between profiles
+ digest.update(profile.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ digest.update(bytes);
+ byte[] hashBytes = digest.digest();
+
+ // Convert to hex string
+ StringBuilder sb = new StringBuilder();
+ for (byte b : hashBytes) {
+ sb.append(String.format("%02x", Byte.valueOf(b)));
+ }
+ return sb.toString();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IOException(sm.getString("cache.hashError"), e);
+ }
+ }
+
+ /**
+ * Clear the cache directory.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public void clear() throws IOException {
+ deleteDirectory(cacheDir);
+ cacheDir.mkdirs();
+ logger.log(Level.INFO, sm.getString("cache.cleared"));
+ }
+
+ /**
+ * Recursively delete a directory.
+ *
+ * @param dir the directory to delete
+ * @throws IOException if an I/O error occurs
+ */
+ private void deleteDirectory(File dir) throws IOException {
+ if (dir.isDirectory()) {
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ deleteDirectory(file);
+ }
+ }
+ }
+ if (!Files.deleteIfExists(dir.toPath()) && dir.exists()) {
+ throw new IOException(sm.getString("cache.deleteFailed", dir.getAbsolutePath()));
+ }
+ }
+
+ /**
+ * Update the access time for a cache entry.
+ *
+ * @param hash the hash of the cache entry
+ */
+ private void updateAccessTime(String hash) {
+ cacheMetadata.put(hash, LocalDate.now());
+ }
+
+ /**
+ * Save cache metadata to disk.
+ * Format: hash|YYYY-MM-DD
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ private void saveMetadata() throws IOException {
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(metadataFile))) {
+ writer.write("# Migration cache metadata - hash|last_access_date\n");
+ for (Map.Entry<String, LocalDate> entry : cacheMetadata.entrySet()) {
+ writer.write(entry.getKey());
+ writer.write("|");
+ writer.write(entry.getValue().format(DATE_FORMATTER));
+ writer.write("\n");
+ }
+ }
+
+ logger.log(Level.FINE, sm.getString("cache.metadata.saved", Integer.valueOf(cacheMetadata.size())));
+ }
+
+ /**
+ * Prune cache entries that haven't been accessed within the retention period.
+ * This should be called after migration completes.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public void pruneCache() throws IOException {
+ LocalDate cutoffDate = LocalDate.now().minusDays(retentionDays);
+ int prunedCount = 0;
+ long prunedSize = 0;
+
+ Set<String> toRemove = new HashSet<>();
+
+ for (Map.Entry<String, LocalDate> entry : cacheMetadata.entrySet()) {
+ String hash = entry.getKey();
+ LocalDate lastAccessed = entry.getValue();
+
+ if (lastAccessed.isBefore(cutoffDate)) {
+ File cachedFile = getCacheFile(hash);
+ if (cachedFile.exists()) {
+ long fileSize = cachedFile.length();
+ if (cachedFile.delete()) {
+ prunedSize += fileSize;
+ prunedCount++;
+ toRemove.add(hash);
+ logger.log(Level.FINE, sm.getString("cache.pruned.entry", hash, lastAccessed));
+ } else {
+ logger.log(Level.WARNING, sm.getString("cache.pruned.failed", hash));
+ }
+ } else {
+ // File doesn't exist, remove from metadata anyway
+ toRemove.add(hash);
+ }
+ }
+ }
+
+ // Remove pruned entries from metadata
+ for (String hash : toRemove) {
+ cacheMetadata.remove(hash);
+ }
+
+ // Save updated metadata
+ saveMetadata();
+
+ if (prunedCount > 0) {
+ logger.log(Level.INFO, sm.getString("cache.pruned.summary", Integer.valueOf(prunedCount),
+ Long.valueOf(prunedSize / 1024 / 1024), Integer.valueOf(retentionDays)));
+ } else {
+ logger.log(Level.FINE, sm.getString("cache.pruned.none", Integer.valueOf(retentionDays)));
+ }
+ }
+
+ /**
+ * Finalize cache operations - save metadata and perform cleanup.
+ * Should be called after migration completes.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public void finalizeCacheOperations() throws IOException {
+ // Save updated metadata
+ saveMetadata();
+
+ // Prune expired entries
+ pruneCache();
+ }
+
+ /**
+ * Get cache statistics.
+ *
+ * @return a string describing cache size and entry count
+ */
+ public String getStats() {
+ long totalSize = 0;
+ int entryCount = 0;
+
+ File[] subdirs = cacheDir.listFiles();
+ if (subdirs != null) {
+ for (File subdir : subdirs) {
+ if (subdir.isDirectory()) {
+ File[] files = subdir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile()) {
+ totalSize += file.length();
+ entryCount++;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return sm.getString("cache.stats", Integer.valueOf(entryCount), Long.valueOf(totalSize / 1024 / 1024));
+ }
+}
=====================================
src/main/resources/org/apache/tomcat/jakartaee/LocalStrings.properties
=====================================
@@ -55,7 +55,17 @@ where options includes:\n\
\ -matchExcludesAgainstPathName\n\
\ By default, exclusions are matched against file name. If this\n\
\ option is enabled, exclusions will be matched against the full\n\
-\ path.
+\ path.\n\
+\ -cache\n\
+\ Enable caching of converted archives. This avoids re-processing\n\
+\ unchanged bundled libraries. Cache is stored in ~/.migration-cache\n\
+\ by default, or use -cacheLocation to specify a custom directory.\n\
+\ -cacheLocation=<directory path>\n\
+\ Specify a custom directory for caching converted archives.\n\
+\ Implies -cache.\n\
+\ -cacheRetention=<days>\n\
+\ Number of days to retain cached files (default: 30, minimum: 1).\n\
+\ Cache entries not accessed within this period will be removed.
migration.warnSignatureRemoval=Removed cryptographic signature from JAR file
@@ -68,4 +78,29 @@ manifestConverter.converted=Migrated manifest file [{0}]
manifestConverter.updated=Updated manifest file [{0}]
manifestConverter.updatedVersion=Updated manifest version to [{0}]
manifestConverter.removeSignature=Remove cryptographic signature for [{0}]
-manifestConverter.noConversion=No manifest conversion necessary for [{0}]
\ No newline at end of file
+manifestConverter.noConversion=No manifest conversion necessary for [{0}]
+
+cache.cannotCreate=Cannot create cache directory [{0}]
+cache.notDirectory=[{0}] is not a directory
+cache.nullDirectory=The cache storage directory may not be null
+cache.enabled=Migration cache enabled at [{0}] with {1} day retention period
+cache.hit=Cache hit for archive [{0}] (hash: {1})
+cache.miss=Cache miss for archive [{0}] (hash: {1})
+cache.store=Stored converted archive in cache (hash: {0}, size: {1} bytes)
+cache.hashError=Error computing hash for cache
+cache.cleared=Cache cleared successfully
+cache.stats=Cache contains {0} entries, total size: {1} MB
+cache.metadata.notFound=Cache metadata file not found, initializing all cached files with current date
+cache.metadata.loaded=Loaded {0} entries from cache metadata
+cache.metadata.saved=Saved {0} entries to cache metadata
+cache.metadata.loadError=Error loading cache metadata, assuming all cached files accessed today
+cache.metadata.invalidLine=Invalid line in cache metadata: {0}
+cache.metadata.invalidDate=Invalid date in cache metadata: {0}
+cache.pruned.entry=Pruned cache entry {0} (last accessed: {1})
+cache.pruned.failed=Failed to delete cache entry {0}
+cache.pruned.summary=Pruned {0} cache entries totaling {1} MB (retention period: {2} days)
+cache.pruned.none=No cache entries to prune (retention period: {0} days)
+
+cacheEntry.copyNotExist=Cannot copy - cache entry does not exist
+cacheEntry.tempNotExist=Temporary file [{0}] does not exist
+cacheEntry.tempRenameFail=Failed to rename temporary file [{0}] to cache file [{1}]
\ No newline at end of file
=====================================
src/test/java/org/apache/tomcat/jakartaee/ClassConverterTest.java
=====================================
@@ -65,8 +65,8 @@ public class ClassConverterTest {
}
// Transform
- ClassConverter convertor = new ClassConverter(EESpecProfiles.TOMCAT);
- transformed = convertor.transform(this.getClass().getClassLoader(),
+ ClassConverter converter = new ClassConverter(EESpecProfiles.TOMCAT);
+ transformed = converter.transform(this.getClass().getClassLoader(),
"org.apache.tomcat.jakartaee.TesterConstants", null, null, original);
// Extract strings
=====================================
src/test/java/org/apache/tomcat/jakartaee/ManifestConverterTest.java
=====================================
@@ -14,12 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.apache.tomcat.jakartaee;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import org.junit.Test;
public class ManifestConverterTest {
@@ -34,4 +35,41 @@ public class ManifestConverterTest {
assertFalse(converter.accepts("xMETA-INF/MANIFEST.MF"));
assertFalse(converter.accepts("WEB-INF/bundles/com.example.bundle/xMETA-INF/MANIFEST.MF"));
}
+
+
+ @Test
+ public void testConvert() throws IOException {
+ ManifestConverter converter = new ManifestConverter();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ boolean converted = converter.convert("/MANIFEST.test.MF", getClass().getResourceAsStream("/MANIFEST.test.MF"),
+ os, EESpecProfiles.TOMCAT);
+ assertTrue(converted);
+
+ String result = os.toString("UTF-8");
+ System.out.println(result);
+ assertTrue(result.length() != 0);
+ result = result.replaceAll("\\s", "");
+
+ // Basic test
+ String imports = "jakarta.servlet;version=\"[5.0.0,7.0.0)\"";
+
+ // Test with directives
+ String imports2 = "jakarta.servlet.http;version=\"[5.0.0,7.0.0)\";resolution:=\"optional\"";
+ assertTrue(result.contains(imports));
+ assertTrue(result.contains(imports2));
+
+ // Test with directive and version
+ String exports = "jakarta.servlet;version=\"5.0.0\";uses:=\"org.eclipse.core.runtime\"";
+
+ // Same as above, with javax.servlet package in the directive
+ String exports2 = "jakarta.servlet.http;version=\"5.0.0\";uses:=\"jakarta.servlet\"";
+
+ // Export a different package that has javax.servlet in a directive so version
+ // isn't updated
+ String exports3 = "org.apache.tomcat.jakartaee.test;version=\"1.0.0\";uses:=\"jakarta.servlet\"";
+
+ assertTrue(result.contains(exports));
+ assertTrue(result.contains(exports2));
+ assertTrue(result.contains(exports3));
+ }
}
=====================================
src/test/java/org/apache/tomcat/jakartaee/MigrationCacheTest.java
=====================================
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.tomcat.jakartaee;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class MigrationCacheTest {
+
+ private File tempCacheDir;
+
+ @Before
+ public void setUp() throws Exception {
+ // Create a temporary cache directory for each test
+ tempCacheDir = Files.createTempDirectory("migration-cache-test").toFile();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // Clean up the temporary cache directory
+ if (tempCacheDir != null && tempCacheDir.exists()) {
+ FileUtils.deleteDirectory(tempCacheDir);
+ }
+ }
+
+ @Test
+ public void testCacheEnabledWithValidDirectory() throws Exception {
+ @SuppressWarnings("unused")
+ MigrationCache unused = new MigrationCache(tempCacheDir, 30);
+ assertTrue("Cache directory should exist", tempCacheDir.exists());
+ }
+
+ @Test
+ public void testCacheCreatesDirectory() throws Exception {
+ File newCacheDir = new File(tempCacheDir, "new-cache");
+ assertFalse("Cache directory should not exist yet", newCacheDir.exists());
+
+ @SuppressWarnings("unused")
+ MigrationCache unused = new MigrationCache(newCacheDir, 30);
+ assertTrue("Cache directory should be created", newCacheDir.exists());
+ }
+
+ @Test
+ public void testCacheMiss() throws Exception {
+ MigrationCache cache = new MigrationCache(tempCacheDir, 30);
+
+ byte[] sourceData = "test source content".getBytes(StandardCharsets.UTF_8);
+
+ // Get cache entry - should not exist
+ CacheEntry entry = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ assertFalse("Cache entry should not exist", entry.exists());
+ assertNotNull("Hash should be computed", entry.getHash());
+ }
+
+ @Test
+ public void testCacheHit() throws Exception {
+ MigrationCache cache = new MigrationCache(tempCacheDir, 30);
+
+ byte[] sourceData = "test source content".getBytes(StandardCharsets.UTF_8);
+ byte[] convertedData = "converted content".getBytes(StandardCharsets.UTF_8);
+
+ // Store in cache
+ CacheEntry entry1 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ assertFalse("Entry should not exist initially", entry1.exists());
+
+ try (OutputStream os = entry1.beginStore()) {
+ os.write(convertedData);
+ }
+ entry1.commitStore();
+
+ // Now check for cache hit
+ CacheEntry entry2 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ assertTrue("Entry should exist now", entry2.exists());
+
+ ByteArrayOutputStream destOutput = new ByteArrayOutputStream();
+ entry2.copyToDestination(destOutput);
+ assertArrayEquals("Cached content should match",
+ convertedData, destOutput.toByteArray());
+ }
+
+ @Test
+ public void testCacheStoresAndRetrieves() throws Exception {
+ MigrationCache cache = new MigrationCache(tempCacheDir, 30);
+
+ byte[] sourceData = "original jar content".getBytes(StandardCharsets.UTF_8);
+ byte[] convertedData = "migrated jar content".getBytes(StandardCharsets.UTF_8);
+
+ // Store the conversion result
+ CacheEntry entry1 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ try (OutputStream os = entry1.beginStore()) {
+ os.write(convertedData);
+ }
+ entry1.commitStore();
+
+ // Verify it was stored by trying to retrieve it
+ CacheEntry entry2 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ assertTrue("Should be cached", entry2.exists());
+
+ ByteArrayOutputStream destOutput = new ByteArrayOutputStream();
+ entry2.copyToDestination(destOutput);
+ assertArrayEquals("Retrieved content should match stored content",
+ convertedData, destOutput.toByteArray());
+ }
+
+ @Test
+ public void testCacheDifferentContent() throws Exception {
+ MigrationCache cache = new MigrationCache(tempCacheDir, 30);
+
+ byte[] sourceData1 = "content 1".getBytes(StandardCharsets.UTF_8);
+ byte[] convertedData1 = "converted 1".getBytes(StandardCharsets.UTF_8);
+ byte[] sourceData2 = "content 2".getBytes(StandardCharsets.UTF_8);
+
+ // Store first conversion
+ CacheEntry entry1 = cache.getCacheEntry(sourceData1, EESpecProfiles.TOMCAT);
+ try (OutputStream os = entry1.beginStore()) {
+ os.write(convertedData1);
+ }
+ entry1.commitStore();
+
+ // Check with different source content
+ CacheEntry entry2 = cache.getCacheEntry(sourceData2, EESpecProfiles.TOMCAT);
+ assertFalse("Should be cache miss for different content", entry2.exists());
+ }
+
+ @Test
+ public void testCacheClear() throws Exception {
+ MigrationCache cache = new MigrationCache(tempCacheDir, 30);
+
+ byte[] sourceData = "test content".getBytes(StandardCharsets.UTF_8);
+ byte[] convertedData = "converted content".getBytes(StandardCharsets.UTF_8);
+
+ // Store in cache
+ CacheEntry entry1 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ try (OutputStream os = entry1.beginStore()) {
+ os.write(convertedData);
+ }
+ entry1.commitStore();
+
+ // Verify it's cached
+ CacheEntry entry2 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ assertTrue("Should be cache hit before clear", entry2.exists());
+
+ // Clear the cache
+ cache.clear();
+
+ // Verify it's no longer cached
+ CacheEntry entry3 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ assertFalse("Should be cache miss after clear", entry3.exists());
+ }
+
+ @Test
+ public void testCacheStats() throws Exception {
+ MigrationCache cache = new MigrationCache(tempCacheDir, 30);
+
+ String stats = cache.getStats();
+ assertNotNull("Stats should not be null", stats);
+ assertTrue("Stats should contain entry count", stats.contains("0"));
+ }
+
+ @Test
+ public void testCacheWithLargeContent() throws Exception {
+ MigrationCache cache = new MigrationCache(tempCacheDir, 30);
+
+ // Create large content (1MB)
+ byte[] sourceData = new byte[1024 * 1024];
+ for (int i = 0; i < sourceData.length; i++) {
+ sourceData[i] = (byte) (i % 256);
+ }
+ byte[] convertedData = new byte[1024 * 1024];
+ for (int i = 0; i < convertedData.length; i++) {
+ convertedData[i] = (byte) ((i + 100) % 256);
+ }
+
+ // Store and retrieve
+ CacheEntry entry1 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ try (OutputStream os = entry1.beginStore()) {
+ os.write(convertedData);
+ }
+ entry1.commitStore();
+
+ CacheEntry entry2 = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ assertTrue("Should be cache hit for large content", entry2.exists());
+
+ ByteArrayOutputStream destOutput = new ByteArrayOutputStream();
+ entry2.copyToDestination(destOutput);
+ assertArrayEquals("Large content should be retrieved correctly",
+ convertedData, destOutput.toByteArray());
+ }
+
+ @Test
+ public void testCacheWithMultipleEntries() throws Exception {
+ MigrationCache cache = new MigrationCache(tempCacheDir, 30);
+
+ // Store multiple different entries
+ for (int i = 0; i < 5; i++) {
+ byte[] sourceData = ("source " + i).getBytes(StandardCharsets.UTF_8);
+ byte[] convertedData = ("converted " + i).getBytes(StandardCharsets.UTF_8);
+
+ CacheEntry entry = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ try (OutputStream os = entry.beginStore()) {
+ os.write(convertedData);
+ }
+ entry.commitStore();
+ }
+
+ // Verify all can be retrieved
+ for (int i = 0; i < 5; i++) {
+ byte[] sourceData = ("source " + i).getBytes(StandardCharsets.UTF_8);
+ byte[] expectedConverted = ("converted " + i).getBytes(StandardCharsets.UTF_8);
+
+ CacheEntry entry = cache.getCacheEntry(sourceData, EESpecProfiles.TOMCAT);
+ assertTrue("Should be cache hit for entry " + i, entry.exists());
+
+ ByteArrayOutputStream destOutput = new ByteArrayOutputStream();
+ entry.copyToDestination(destOutput);
+ assertArrayEquals("Content should match for entry " + i,
+ expectedConverted, destOutput.toByteArray());
+ }
+ }
+}
=====================================
src/test/java/org/apache/tomcat/jakartaee/MigrationTest.java
=====================================
@@ -21,6 +21,7 @@ import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.jar.JarFile;
import org.apache.commons.io.FileUtils;
@@ -233,31 +234,206 @@ public class MigrationTest {
@Test
public void testMigrateSignedJarFileRSA() throws Exception {
- testMigrateSignedJarFile("rsa");
+ testMigrateSignedJarFile("rsa", EESpecProfiles.TOMCAT);
}
@Test
public void testMigrateSignedJarFileDSA() throws Exception {
- testMigrateSignedJarFile("dsa");
+ testMigrateSignedJarFile("dsa", EESpecProfiles.TOMCAT);
}
@Test
public void testMigrateSignedJarFileEC() throws Exception {
- testMigrateSignedJarFile("ec");
+ testMigrateSignedJarFile("ec", EESpecProfiles.TOMCAT);
}
- private void testMigrateSignedJarFile(String algorithm) throws Exception {
- File jarFile = new File("target/test-classes/hellocgi-signed-" + algorithm + ".jar");
+ @Test
+ public void testNoopSignedJarFileRSA() throws Exception {
+ testMigrateSignedJarFile("rsa", EESpecProfiles.JEE8);
+ }
+
+ @Test
+ public void testNoopSignedJarFileDSA() throws Exception {
+ testMigrateSignedJarFile("dsa", EESpecProfiles.JEE8);
+ }
+
+ @Test
+ public void testNoopSignedJarFileEC() throws Exception {
+ testMigrateSignedJarFile("ec", EESpecProfiles.JEE8);
+ }
+
+ private void testMigrateSignedJarFile(String algorithm, EESpecProfile profile) throws Exception {
+ File jarFileSrc = new File("target/test-classes/hellocgi-signed-" + algorithm + ".jar");
+ File jarFileTmp = new File("target/test-classes/hellocgi-signed-" + algorithm + "-tmp.jar");
+ Files.copy(jarFileSrc.toPath(), jarFileTmp.toPath());
+
+ Migration migration = new Migration();
+ migration.setEESpecProfile(profile);
+ migration.setSource(jarFileTmp);
+ migration.setDestination(jarFileTmp);
+ migration.execute();
+
+ try (JarFile jar = new JarFile(jarFileTmp)) {
+ if (profile == EESpecProfiles.JEE8) {
+ assertNotNull("Digest removed from the manifest", jar.getManifest().getAttributes("org/apache/tomcat/jakartaee/HelloCGI.class"));
+ assertNotNull("Signature key removed", jar.getEntry("META-INF/" + algorithm.toUpperCase() + "." + algorithm.toUpperCase()));
+ assertNotNull("Signed manifest removed", jar.getEntry("META-INF/" + algorithm.toUpperCase() + ".SF"));
+ assertFalse("The JAR was converted", migration.hasConverted());
+ } else {
+ assertNull("Digest not removed from the manifest", jar.getManifest().getAttributes("org/apache/tomcat/jakartaee/HelloCGI.class"));
+ assertNull("Signature key not removed", jar.getEntry("META-INF/" + algorithm.toUpperCase() + "." + algorithm.toUpperCase()));
+ assertNull("Signed manifest not removed", jar.getEntry("META-INF/" + algorithm.toUpperCase() + ".SF"));
+ assertTrue("The JAR was not converted", migration.hasConverted());
+ }
+ } finally {
+ assertTrue("Unable to delete " + jarFileTmp.getAbsolutePath(), jarFileTmp.delete());
+ }
+ }
+
+ @Test
+ public void testMigrateJarWithCache() throws Exception {
+ File jarFile = new File("target/test-classes/hellocgi.jar");
+ File jarFileTarget = new File("target/test-classes/hellocgi-cached.jar");
+ File cacheDir = new File("target/test-classes/cache-test");
+
+ try {
+ // Clean up cache directory
+ if (cacheDir.exists()) {
+ FileUtils.deleteDirectory(cacheDir);
+ }
+
+ // First migration - cache miss
+ Migration migration1 = new Migration();
+ migration1.setSource(jarFile);
+ migration1.setDestination(jarFileTarget);
+ migration1.setCache(new MigrationCache(cacheDir, 30));
+ migration1.execute();
+
+ assertTrue("Target JAR should exist after first migration", jarFileTarget.exists());
+ assertTrue("Cache directory should be created", cacheDir.exists());
+
+ // Verify the migrated JAR works
+ File cgiapiFile = new File("target/test-classes/cgi-api.jar");
+ URLClassLoader classloader1 = new URLClassLoader(
+ new URL[]{jarFileTarget.toURI().toURL(), cgiapiFile.toURI().toURL()},
+ ClassLoader.getSystemClassLoader().getParent());
+ Class<?> cls1 = Class.forName("org.apache.tomcat.jakartaee.HelloCGI", true, classloader1);
+ assertEquals("jakarta.servlet.CommonGatewayInterface", cls1.getSuperclass().getName());
+
+ // Delete target and migrate again - cache hit
+ jarFileTarget.delete();
+ assertFalse("Target should be deleted", jarFileTarget.exists());
+
+ Migration migration2 = new Migration();
+ migration2.setSource(jarFile);
+ migration2.setDestination(jarFileTarget);
+ migration2.setCache(new MigrationCache(cacheDir, 30));
+ migration2.execute();
+
+ assertTrue("Target JAR should exist after second migration", jarFileTarget.exists());
+
+ // Verify the cached JAR works
+ URLClassLoader classloader2 = new URLClassLoader(
+ new URL[]{jarFileTarget.toURI().toURL(), cgiapiFile.toURI().toURL()},
+ ClassLoader.getSystemClassLoader().getParent());
+ Class<?> cls2 = Class.forName("org.apache.tomcat.jakartaee.HelloCGI", true, classloader2);
+ assertEquals("jakarta.servlet.CommonGatewayInterface", cls2.getSuperclass().getName());
+
+ // Note: We don't assert that duration2 < duration1 because the times are too short
+ // and can vary. The important thing is both migrations work correctly.
+ } finally {
+ // Clean up
+ if (cacheDir.exists()) {
+ FileUtils.deleteDirectory(cacheDir);
+ }
+ }
+ }
+
+ @Test
+ public void testMigrateJarWithCacheDisabled() throws Exception {
+ File jarFile = new File("target/test-classes/hellocgi.jar");
+ File jarFileTarget = new File("target/test-classes/hellocgi-nocache.jar");
Migration migration = new Migration();
migration.setSource(jarFile);
- migration.setDestination(jarFile);
+ migration.setDestination(jarFileTarget);
+ // Don't set cache - should work without caching
migration.execute();
- try (JarFile jar = new JarFile(jarFile)) {
- assertNull("Digest not removed from the manifest", jar.getManifest().getAttributes("org/apache/tomcat/jakartaee/HelloCGI.class"));
- assertNull("Signature key not removed", jar.getEntry("META-INF/" + algorithm.toUpperCase() + "." + algorithm.toUpperCase()));
- assertNull("Signed manifest not removed", jar.getEntry("META-INF/" + algorithm.toUpperCase() + ".SF"));
+ assertTrue("Target JAR should exist", jarFileTarget.exists());
+
+ File cgiapiFile = new File("target/test-classes/cgi-api.jar");
+ URLClassLoader classloader = new URLClassLoader(
+ new URL[]{jarFileTarget.toURI().toURL(), cgiapiFile.toURI().toURL()},
+ ClassLoader.getSystemClassLoader().getParent());
+ Class<?> cls = Class.forName("org.apache.tomcat.jakartaee.HelloCGI", true, classloader);
+ assertEquals("jakarta.servlet.CommonGatewayInterface", cls.getSuperclass().getName());
+ }
+
+ @Test
+ public void testMigrateCLIWithCacheOption() throws Exception {
+ File sourceFile = new File("target/test-classes/hellocgi.jar");
+ File targetFile = new File("target/test-classes/hellocgi-cli-cached.jar");
+ File cacheDir = new File("target/test-classes/cache-cli-test");
+
+ try {
+ // Clean up
+ if (cacheDir.exists()) {
+ FileUtils.deleteDirectory(cacheDir);
+ }
+ if (targetFile.exists()) {
+ targetFile.delete();
+ }
+
+ // Run with custom cache
+ MigrationCLI.main(new String[] {
+ "-cache",
+ "-cacheLocation=" + cacheDir.getAbsolutePath(),
+ sourceFile.getAbsolutePath(),
+ targetFile.getAbsolutePath()
+ });
+
+ assertTrue("Target file should exist", targetFile.exists());
+ assertTrue("Cache directory should be created", cacheDir.exists());
+
+ // Verify the migrated JAR works
+ File cgiapiFile = new File("target/test-classes/cgi-api.jar");
+ URLClassLoader classloader = new URLClassLoader(
+ new URL[]{targetFile.toURI().toURL(), cgiapiFile.toURI().toURL()},
+ ClassLoader.getSystemClassLoader().getParent());
+ Class<?> cls = Class.forName("org.apache.tomcat.jakartaee.HelloCGI", true, classloader);
+ assertEquals("jakarta.servlet.CommonGatewayInterface", cls.getSuperclass().getName());
+ } finally {
+ // Clean up
+ if (cacheDir.exists()) {
+ FileUtils.deleteDirectory(cacheDir);
+ }
}
}
+
+ @Test
+ public void testMigrateCLIWithNoCacheOption() throws Exception {
+ File sourceFile = new File("target/test-classes/hellocgi.jar");
+ File targetFile = new File("target/test-classes/hellocgi-cli-nocache.jar");
+
+ if (targetFile.exists()) {
+ targetFile.delete();
+ }
+
+ // Run without cache (no -cache option)
+ MigrationCLI.main(new String[] {
+ sourceFile.getAbsolutePath(),
+ targetFile.getAbsolutePath()
+ });
+
+ assertTrue("Target file should exist", targetFile.exists());
+
+ // Verify the migrated JAR works
+ File cgiapiFile = new File("target/test-classes/cgi-api.jar");
+ URLClassLoader classloader = new URLClassLoader(
+ new URL[]{targetFile.toURI().toURL(), cgiapiFile.toURI().toURL()},
+ ClassLoader.getSystemClassLoader().getParent());
+ Class<?> cls = Class.forName("org.apache.tomcat.jakartaee.HelloCGI", true, classloader);
+ assertEquals("jakarta.servlet.CommonGatewayInterface", cls.getSuperclass().getName());
+ }
}
=====================================
src/test/resources/MANIFEST.test.MF
=====================================
@@ -0,0 +1,9 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Version: 1.0.0.qualifier
+Bundle-SymbolicName: org.apache.tomcat.jakartaee.test
+Import-Package: javax.servlet;version="[2.0.0,5.0.0)",
+ javax.servlet.http;resolution:=optional;version="[2.0.0,5.0.0)"
+Export-Package: javax.servlet;uses:="org.eclipse.core.runtime";version="4.0.0",
+ javax.servlet.http;uses:="javax.servlet";version="4.0.0",
+ org.apache.tomcat.jakartaee.test;uses:="javax.servlet";version="1.0.0"
View it on GitLab: https://salsa.debian.org/java-team/tomcat-jakartaee-migration/-/compare/684ebeba60b89fa39986acac67004cc584b1c74d...1e4cbff7cdb66cc8736cf3c0d13dc8944dc430cf
--
View it on GitLab: https://salsa.debian.org/java-team/tomcat-jakartaee-migration/-/compare/684ebeba60b89fa39986acac67004cc584b1c74d...1e4cbff7cdb66cc8736cf3c0d13dc8944dc430cf
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20251220/23ebe944/attachment.htm>
More information about the pkg-java-commits
mailing list