[Git][java-team/tomcat-jakartaee-migration][upstream] New upstream version 1.0.10

Emmanuel Bourg (@ebourg) gitlab at salsa.debian.org
Sat Dec 20 10:49:31 GMT 2025



Emmanuel Bourg pushed to branch upstream at Debian Java Maintainers / tomcat-jakartaee-migration


Commits:
316be028 by Emmanuel Bourg at 2025-12-20T11:18:31+01:00
New upstream version 1.0.10
- - - - -


17 changed files:

- .github/dependabot.yml
- .github/workflows/ci.yml
- .github/workflows/coverage.yml
- .gitignore
- CHANGES.md
- 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)


=====================================
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/-/commit/316be028bfa66f76b8e151cc73dbb4c92ac281de

-- 
View it on GitLab: https://salsa.debian.org/java-team/tomcat-jakartaee-migration/-/commit/316be028bfa66f76b8e151cc73dbb4c92ac281de
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/cdd7a3aa/attachment.htm>


More information about the pkg-java-commits mailing list