markt-asf commented on code in PR #87:
URL:
https://github.com/apache/tomcat-jakartaee-migration/pull/87#discussion_r2526901851
##########
src/main/java/org/apache/tomcat/jakartaee/MigrationCLI.java:
##########
@@ -108,6 +138,11 @@ public static void main(String[] args) throws IOException {
migration.setSource(new File(source));
migration.setDestination(new File(dest));
+ // Only enable cache if -cache argument was provided
+ if (enableCache && cacheDir != null) {
Review Comment:
Is check on cacheDir required here?
##########
src/main/java/org/apache/tomcat/jakartaee/MigrationCache.java:
##########
@@ -0,0 +1,431 @@
+/*
+ * 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.
+ * Uses SHA-256 hashing of the pre-conversion content to identify files.
+ */
+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 boolean enabled;
+ 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
{
+ if (cacheDir == null) {
+ this.cacheDir = null;
+ this.enabled = false;
+ this.retentionDays = 0;
+ this.cacheMetadata = new HashMap<>();
+ this.metadataFile = null;
+ } else {
+ this.cacheDir = cacheDir;
+ this.enabled = true;
+ this.retentionDays = retentionDays;
+ this.cacheMetadata = new HashMap<>();
+ this.metadataFile = new File(cacheDir, METADATA_FILE);
+
+ // 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();
+
+ logger.log(Level.INFO, sm.getString("cache.enabled",
cacheDir.getAbsolutePath(), retentionDays));
+ }
+ }
+
+ /**
+ * 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",
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() && !subdir.getName().equals(".")) {
Review Comment:
The check for "." is not necessary.
##########
src/main/java/org/apache/tomcat/jakartaee/Migration.java:
##########
@@ -211,6 +212,16 @@ public void setDestination(File destination) {
this.destination = destination;
}
+ /**
+ * Set the cache directory for storing pre-converted archives.
+ * @param cacheDir the cache directory (null to disable caching)
+ * @param retentionDays the number of days to retain cached files
+ * @throws IOException if the cache directory cannot be created
+ */
+ public void setCache(File cacheDir, int retentionDays) throws IOException {
+ this.cache = new MigrationCache(cacheDir, retentionDays);
+ }
+
Review Comment:
If you pass in an instance of MigrationCache here, you can pass in null to
disable caching, simplify the code by removing the enabled flag and allow for
future, alternative Migration cache implementations. Not sure of there is a
need to go as far as making MigrationCache an interface with an implementation.
##########
src/main/java/org/apache/tomcat/jakartaee/Migration.java:
##########
@@ -415,14 +432,61 @@ private boolean migrateStream(String name, InputStream
src, OutputStream dest) t
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));
- convertedStream = migrateArchiveInMemory(src, dest);
- logger.log(Level.INFO,
sm.getString("migration.archive.complete", name));
- } else {
- logger.log(Level.INFO,
sm.getString("migration.archive.stream", name));
- convertedStream = 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 && cache.isEnabled()) {
+ // 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);
+
+ if (cacheEntry.exists()) {
+ // Cache hit! Copy cached result to dest and return
Review Comment:
Currently the cache is keyed solely on the hash of the input archive. It
also needs to include the profile as the output may vary depending on the
conversion profile being used.
##########
src/main/java/org/apache/tomcat/jakartaee/MigrationCache.java:
##########
@@ -0,0 +1,431 @@
+/*
+ * 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.
+ * Uses SHA-256 hashing of the pre-conversion content to identify files.
+ */
+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 boolean enabled;
+ 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
{
+ if (cacheDir == null) {
+ this.cacheDir = null;
+ this.enabled = false;
+ this.retentionDays = 0;
+ this.cacheMetadata = new HashMap<>();
+ this.metadataFile = null;
+ } else {
+ this.cacheDir = cacheDir;
+ this.enabled = true;
+ this.retentionDays = retentionDays;
+ this.cacheMetadata = new HashMap<>();
+ this.metadataFile = new File(cacheDir, METADATA_FILE);
+
+ // 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();
+
+ logger.log(Level.INFO, sm.getString("cache.enabled",
cacheDir.getAbsolutePath(), retentionDays));
+ }
+ }
+
+ /**
+ * 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",
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() && !subdir.getName().equals(".")) {
+ 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;
+ }
+
+ /**
+ * Check if caching is enabled.
+ *
+ * @return true if caching is enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Get a cache entry for the given source bytes.
+ * This computes the hash, checks if cached, and marks the entry as
accessed.
+ *
+ * @param sourceBytes the pre-conversion content
+ * @return a CacheEntry object with all operations for this entry
+ * @throws IOException if an I/O error occurs
+ */
+ public CacheEntry getCacheEntry(byte[] sourceBytes) throws IOException {
+ if (!enabled) {
+ throw new IllegalStateException("Cache is not enabled");
+ }
+
+ // Compute hash once
+ String hash = computeHash(sourceBytes);
+
+ // 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
Review Comment:
Structure of cache needs to be documented more prominently than this.
##########
src/main/java/org/apache/tomcat/jakartaee/Migration.java:
##########
@@ -415,14 +432,61 @@ private boolean migrateStream(String name, InputStream
src, OutputStream dest) t
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));
- convertedStream = migrateArchiveInMemory(src, dest);
- logger.log(Level.INFO,
sm.getString("migration.archive.complete", name));
- } else {
- logger.log(Level.INFO,
sm.getString("migration.archive.stream", name));
- convertedStream = 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 && cache.isEnabled()) {
+ // 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);
+
+ 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(), 0));
Review Comment:
The file size is not zero. Also needs conversion to Integer to avoid the
auto-boxing warning.
##########
src/main/java/org/apache/tomcat/jakartaee/MigrationCache.java:
##########
@@ -0,0 +1,431 @@
+/*
+ * 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.
+ * Uses SHA-256 hashing of the pre-conversion content to identify files.
+ */
+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 boolean enabled;
+ 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
{
+ if (cacheDir == null) {
+ this.cacheDir = null;
+ this.enabled = false;
+ this.retentionDays = 0;
+ this.cacheMetadata = new HashMap<>();
+ this.metadataFile = null;
+ } else {
+ this.cacheDir = cacheDir;
+ this.enabled = true;
+ this.retentionDays = retentionDays;
+ this.cacheMetadata = new HashMap<>();
+ this.metadataFile = new File(cacheDir, METADATA_FILE);
+
+ // 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();
+
+ logger.log(Level.INFO, sm.getString("cache.enabled",
cacheDir.getAbsolutePath(), retentionDays));
+ }
+ }
+
+ /**
+ * 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",
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() && !subdir.getName().equals(".")) {
+ 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;
+ }
+
+ /**
+ * Check if caching is enabled.
+ *
+ * @return true if caching is enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Get a cache entry for the given source bytes.
+ * This computes the hash, checks if cached, and marks the entry as
accessed.
+ *
+ * @param sourceBytes the pre-conversion content
+ * @return a CacheEntry object with all operations for this entry
+ * @throws IOException if an I/O error occurs
+ */
+ public CacheEntry getCacheEntry(byte[] sourceBytes) throws IOException {
+ if (!enabled) {
+ throw new IllegalStateException("Cache is not enabled");
+ }
+
+ // Compute hash once
+ String hash = computeHash(sourceBytes);
+
+ // 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.
+ *
+ * @param bytes the bytes to hash
+ * @return the hash as a hex string
+ * @throws IOException if hashing fails
+ */
+ private String computeHash(byte[] bytes) throws IOException {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(bytes);
+
+ // Convert to hex string
+ StringBuilder sb = new StringBuilder();
+ for (byte b : hashBytes) {
+ sb.append(String.format("%02x", 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 {
+ if (!enabled) {
+ return;
+ }
+
+ 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);
+ }
+ }
+ }
+ Files.deleteIfExists(dir.toPath());
Review Comment:
Need to check return value here.
##########
src/main/java/org/apache/tomcat/jakartaee/MigrationCache.java:
##########
@@ -0,0 +1,431 @@
+/*
+ * 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.
+ * Uses SHA-256 hashing of the pre-conversion content to identify files.
+ */
+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 boolean enabled;
+ 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
{
+ if (cacheDir == null) {
+ this.cacheDir = null;
+ this.enabled = false;
+ this.retentionDays = 0;
+ this.cacheMetadata = new HashMap<>();
+ this.metadataFile = null;
+ } else {
+ this.cacheDir = cacheDir;
+ this.enabled = true;
+ this.retentionDays = retentionDays;
+ this.cacheMetadata = new HashMap<>();
+ this.metadataFile = new File(cacheDir, METADATA_FILE);
+
+ // 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();
+
+ logger.log(Level.INFO, sm.getString("cache.enabled",
cacheDir.getAbsolutePath(), retentionDays));
+ }
+ }
+
+ /**
+ * 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",
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() && !subdir.getName().equals(".")) {
+ 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;
+ }
+
+ /**
+ * Check if caching is enabled.
+ *
+ * @return true if caching is enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Get a cache entry for the given source bytes.
+ * This computes the hash, checks if cached, and marks the entry as
accessed.
+ *
+ * @param sourceBytes the pre-conversion content
+ * @return a CacheEntry object with all operations for this entry
+ * @throws IOException if an I/O error occurs
+ */
+ public CacheEntry getCacheEntry(byte[] sourceBytes) throws IOException {
+ if (!enabled) {
+ throw new IllegalStateException("Cache is not enabled");
+ }
+
+ // Compute hash once
+ String hash = computeHash(sourceBytes);
+
+ // 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");
Review Comment:
Is there any clean-up of these temporary files if the conversion crashes?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]