diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index d9004668d7..7939fea76e 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -24,9 +24,9 @@ import java.util.UUID; // custom ConfigSource, and java.nio / ClassLoader#getResourceAsStream for resources). import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.Resource; +import stirling.software.common.model.io.ClassPathResource; +import stirling.software.common.model.io.FileSystemResource; +import stirling.software.common.model.io.Resource; import org.springframework.core.io.support.EncodedResource; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/app/common/src/main/java/stirling/software/common/model/io/ClassPathResource.java b/app/common/src/main/java/stirling/software/common/model/io/ClassPathResource.java new file mode 100644 index 0000000000..3984e2e618 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/io/ClassPathResource.java @@ -0,0 +1,64 @@ +package stirling.software.common.model.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** Classpath-backed {@link Resource} (migration shim for Spring's {@code ClassPathResource}). */ +public class ClassPathResource implements Resource { + + private final String path; + private final ClassLoader classLoader; + + public ClassPathResource(String path) { + this(path, ClassPathResource.class.getClassLoader()); + } + + public ClassPathResource(String path, ClassLoader classLoader) { + this.path = path.startsWith("/") ? path.substring(1) : path; + this.classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader(); + } + + @Override + public InputStream getInputStream() throws IOException { + InputStream is = classLoader.getResourceAsStream(path); + if (is == null) { + throw new IOException("class path resource [" + path + "] cannot be opened"); + } + return is; + } + + @Override + public boolean exists() { + return classLoader.getResource(path) != null; + } + + @Override + public String getFilename() { + int sep = path.lastIndexOf('/'); + return sep != -1 ? path.substring(sep + 1) : path; + } + + @Override + public long contentLength() throws IOException { + try (InputStream is = getInputStream()) { + long count = 0; + byte[] buf = new byte[8192]; + int read; + while ((read = is.read(buf)) != -1) { + count += read; + } + return count; + } + } + + @Override + public File getFile() throws IOException { + URL url = classLoader.getResource(path); + if (url == null || !"file".equals(url.getProtocol())) { + throw new IOException("class path resource [" + path + "] is not a filesystem file"); + } + return new File(url.getFile()); + } +} diff --git a/app/common/src/main/java/stirling/software/common/model/io/FileSystemResource.java b/app/common/src/main/java/stirling/software/common/model/io/FileSystemResource.java new file mode 100644 index 0000000000..19335d2267 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/io/FileSystemResource.java @@ -0,0 +1,56 @@ +package stirling.software.common.model.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +/** File-backed {@link Resource} (migration shim for Spring's {@code FileSystemResource}). */ +public class FileSystemResource implements Resource { + + private final Path path; + + public FileSystemResource(Path path) { + this.path = path; + } + + public FileSystemResource(File file) { + this.path = file.toPath(); + } + + public FileSystemResource(String path) { + this.path = Path.of(path); + } + + @Override + public InputStream getInputStream() throws IOException { + return Files.newInputStream(path); + } + + @Override + public boolean exists() { + return Files.exists(path); + } + + @Override + public String getFilename() { + Path name = path.getFileName(); + return name == null ? null : name.toString(); + } + + @Override + public long contentLength() throws IOException { + return Files.size(path); + } + + @Override + public boolean isFile() { + return true; + } + + @Override + public File getFile() { + return path.toFile(); + } +} diff --git a/app/common/src/main/java/stirling/software/common/model/io/InputStreamResource.java b/app/common/src/main/java/stirling/software/common/model/io/InputStreamResource.java new file mode 100644 index 0000000000..da1ab6ceb8 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/io/InputStreamResource.java @@ -0,0 +1,50 @@ +package stirling.software.common.model.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * Stream-backed {@link Resource} (migration shim for Spring's {@code InputStreamResource}). As with + * Spring, the stream can only be read once. + */ +public class InputStreamResource implements Resource { + + private final InputStream inputStream; + private final String filename; + + public InputStreamResource(InputStream inputStream) { + this(inputStream, null); + } + + public InputStreamResource(InputStream inputStream, String filename) { + this.inputStream = inputStream; + this.filename = filename; + } + + @Override + public InputStream getInputStream() { + return inputStream; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public String getFilename() { + return filename; + } + + @Override + public long contentLength() throws IOException { + // Spring's InputStreamResource also cannot report length without consuming the stream. + return -1; + } + + @Override + public File getFile() throws IOException { + throw new IOException("InputStreamResource is not backed by a file"); + } +} diff --git a/app/common/src/main/java/stirling/software/common/model/io/Resource.java b/app/common/src/main/java/stirling/software/common/model/io/Resource.java new file mode 100644 index 0000000000..9394d9c7b8 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/io/Resource.java @@ -0,0 +1,40 @@ +package stirling.software.common.model.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * Migration compatibility shim for Spring's {@code org.springframework.core.io.Resource}. + * + *
Quarkus/Jakarta has no single {@code Resource} abstraction. Rather than rewrite the many + * public method signatures across the codebase that accept or return {@code Resource}, this + * interface mirrors the subset of Spring's API the codebase actually uses + * ({@code getInputStream/exists/getFile/getFilename/contentLength/isFile}) together with the + * {@link FileSystemResource}, {@link InputStreamResource} and {@link ClassPathResource} + * implementations. Converting a file is then just an import swap. + * + *
TODO: Migration required - longer term, prefer {@code java.nio.file.Path} / {@code InputStream}
+ * directly at the boundaries and retire this shim.
+ */
+public interface Resource {
+
+ InputStream getInputStream() throws IOException;
+
+ boolean exists();
+
+ String getFilename();
+
+ long contentLength() throws IOException;
+
+ /** Whether this resource is backed by a real file in the filesystem. */
+ default boolean isFile() {
+ return false;
+ }
+
+ /**
+ * @return the underlying file.
+ * @throws IOException if the resource is not file-backed.
+ */
+ File getFile() throws IOException;
+}
diff --git a/app/common/src/main/java/stirling/software/common/service/FileStorage.java b/app/common/src/main/java/stirling/software/common/service/FileStorage.java
index d5d96c39d3..93e53a1885 100644
--- a/app/common/src/main/java/stirling/software/common/service/FileStorage.java
+++ b/app/common/src/main/java/stirling/software/common/service/FileStorage.java
@@ -14,7 +14,7 @@ import jakarta.enterprise.context.ApplicationScoped;
// TODO: Migration required - org.springframework.core.io.Resource is used in public method
// signatures (storeFile, storeFromResource) and for behavior (isFile()/getFile()/getInputStream()).
// No drop-in JAX-RS/Jakarta equivalent; converting would ripple into callers. Keeping the type.
-import org.springframework.core.io.Resource;
+import stirling.software.common.model.io.Resource;
// TODO: Migration required - org.springframework.web.multipart.MultipartFile is used in public
// method signatures (storeFile, retrieveFile). No servlet/JAX-RS drop-in for utility method
// params; changing it would ripple widely into callers. Keeping the type.
diff --git a/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java b/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java
index 2902430dae..f6c3acfba1 100644
--- a/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java
+++ b/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java
@@ -16,8 +16,8 @@ import java.util.regex.Pattern;
// non-trivial and would ripple to callers (PipelineProcessor, AiWorkflowService) that pass
// MultiValueMap bodies and consume ResponseEntity The returned {@link Resource} owns the supplied {@link TempFile} — the file is deleted
- * when Spring closes the response {@link InputStream} after writing the body. This is a
- * synchronous equivalent of the previous {@code StreamingResponseBody} pattern and avoids the
- * async-dispatch hazards (response-committed races, filter incompatibility, silent write
- * failures) that {@code StreamingResponseBody} introduced.
+ * MIGRATION (Spring -> JAX-RS): previously returned {@code ResponseEntity The temp file is deleted when Spring closes the response body stream.
- *
- * @param outputTempFile The temporary file to be sent as a response.
- * @param docName The name of the document.
- * @return A ResponseEntity whose body streams the file, deleting it on close.
- */
- public static ResponseEntity The temp file is deleted when Spring closes the response body stream.
- *
- * @param outputTempFile The temporary file to be sent as a response.
- * @param docName The name of the document.
- * @return A ResponseEntity whose body streams the file, deleting it on close.
- */
- public static ResponseEntity The returned {@link ResponseEntity} carries a {@link ManagedTempFileResource} as its body.
- * Spring's {@code ResourceHttpMessageConverter} calls {@link Resource#getInputStream()} once
- * and closes the returned stream after writing — at which point the underlying {@link TempFile}
- * is deleted. Everything runs synchronously on the request thread, so write failures propagate
- * through normal Spring error handling and are logged, rather than silently truncating the
- * response.
- *
- * @param outputTempFile The temporary file to be sent as a response.
- * @param docName The name of the document.
- * @param mediaType The content type to set on the response.
- * @return A ResponseEntity whose body streams the file, deleting it on close.
+ * The body is a {@link StreamingOutput} that copies the temp file to the client and deletes
+ * the backing {@link TempFile} once the transfer completes (or fails). This replaces the former
+ * Spring {@code ResponseEntity Single-use contract: {@link #getInputStream()} is intended to be called once by
- * Spring's {@code ResourceHttpMessageConverter} on the normal write path. After the returned
- * stream is closed the backing temp file is deleted, so subsequent {@code getInputStream()}
- * calls will either see a deleted file (tests that mock {@link TempFile#close()} are an
- * exception) or fail at read time. Callers that need to re-read the body must copy it first.
- */
- // TODO: Migration required - extends Spring's FileSystemResource and is consumed by Spring's
- // ResourceHttpMessageConverter; no JAX-RS equivalent. Kept intact pending the Response +
- // StreamingOutput rework described on fileToWebResponse.
- public static final class ManagedTempFileResource extends FileSystemResource {
-
- private final TempFile tempFile;
-
- public ManagedTempFileResource(TempFile tempFile) {
- super(tempFile.getFile());
- this.tempFile = tempFile;
- }
-
- @Override
- public InputStream getInputStream() throws IOException {
- InputStream source;
- try {
- source = super.getInputStream();
- } catch (IOException e) {
- // Opening the input stream already failed; make sure we don't leak the temp file.
- try {
- tempFile.close();
- } catch (Exception closeEx) {
- e.addSuppressed(closeEx);
- }
- throw e;
- }
- return new ClosingInputStream(source, tempFile);
- }
- }
-
- /**
- * Stream wrapper that deletes its backing {@link TempFile} on close. Logs — but does not
- * swallow — any IOException that happens while reading the body, so upstream handlers can
- * surface the failure to the client.
- */
- private static final class ClosingInputStream extends FilterInputStream {
-
- private final TempFile tempFile;
- private boolean closed;
-
- ClosingInputStream(InputStream delegate, TempFile tempFile) {
- super(delegate);
- this.tempFile = tempFile;
- }
-
- @Override
- public int read() throws IOException {
- try {
- return super.read();
- } catch (IOException e) {
- log.error(
- "Failed to read temp response body {} while streaming to client",
- tempFile.getAbsolutePath(),
- e);
- throw e;
- }
- }
-
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- try {
- return super.read(b, off, len);
- } catch (IOException e) {
- log.error(
- "Failed to read temp response body {} while streaming to client",
- tempFile.getAbsolutePath(),
- e);
- throw e;
- }
- }
-
- @Override
- public void close() throws IOException {
- if (closed) {
- return;
- }
- closed = true;
- try {
- super.close();
- } finally {
- try {
- tempFile.close();
- } catch (Exception closeEx) {
- log.warn(
- "Failed to clean up temp file {} after streaming response",
- tempFile.getAbsolutePath(),
- closeEx);
- }
- }
- }
- }
+ // REMOVED (Spring -> JAX-RS): ManagedTempFileResource (extended Spring FileSystemResource) and
+ // ClosingInputStream. Their job - delete the TempFile after the response body is written - is now
+ // done inline by the StreamingOutput's finally block in fileToWebResponse, which is the idiomatic
+ // JAX-RS lifecycle and removes the dependency on Spring's ResourceHttpMessageConverter.
}
diff --git a/app/common/src/main/java/stirling/software/common/util/ZipExtractionUtils.java b/app/common/src/main/java/stirling/software/common/util/ZipExtractionUtils.java
index 8aa721ec74..32ec630bda 100644
--- a/app/common/src/main/java/stirling/software/common/util/ZipExtractionUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/ZipExtractionUtils.java
@@ -16,8 +16,8 @@ import java.util.zip.ZipInputStream;
// (PipelineProcessor, AiWorkflowService). Replacing them with InputStream/java.nio would ripple
// across callers that this single-file migration is not permitted to edit. Keeping the Spring core
// io abstraction until callers are migrated together.
-import org.springframework.core.io.FileSystemResource;
-import org.springframework.core.io.Resource;
+import stirling.software.common.model.io.FileSystemResource;
+import stirling.software.common.model.io.Resource;
import io.github.pixee.security.ZipSecurity;
diff --git a/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java
index ddcdbbf75a..d58a0dacfe 100644
--- a/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java
+++ b/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java
@@ -11,7 +11,7 @@ import java.util.List;
// because the abstract superclass ReplaceAndInvertColorStrategy.replace() declares this return
// type. Migrating it (e.g. to java.io.InputStream) would change the public API of the parent
// class and all sibling strategies/callers, which is out of scope for this single-file migration.
-import org.springframework.core.io.InputStreamResource;
+import stirling.software.common.model.io.InputStreamResource;
// TODO: Migration required - org.springframework.web.multipart.MultipartFile is retained because
// the superclass ctor ReplaceAndInvertColorStrategy(MultipartFile, ReplaceAndInvert) requires it.
// No JAX-RS drop-in exists for this constructor param without rippling changes to the parent and
diff --git a/app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java
index f1122a069d..31cd913e09 100644
--- a/app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java
+++ b/app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java
@@ -23,7 +23,7 @@ import org.apache.pdfbox.text.TextPosition;
// replace() method declared in the parent ReplaceAndInvertColorStrategy (not in scope for
// this migration). Changing it here would break the @Override and ripple to all callers, so
// the Spring type is retained until the parent and its callers are migrated together.
-import org.springframework.core.io.InputStreamResource;
+import stirling.software.common.model.io.InputStreamResource;
// TODO: Migration required - MultipartFile is the constructor parameter type that must match
// the parent ReplaceAndInvertColorStrategy(MultipartFile, ReplaceAndInvert) constructor (not in
// scope for this migration). There is no JAX-RS drop-in for this widely used public signature;
diff --git a/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java
index aa1e17b8a7..03d1be1223 100644
--- a/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java
+++ b/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java
@@ -21,7 +21,7 @@ import org.apache.pdfbox.rendering.PDFRenderer;
// drop-in here. The abstract parent ReplaceAndInvertColorStrategy.replace() returns
// InputStreamResource and editing that signature would ripple to all subclasses and callers; per
// task rules, only this file may change, so the type is kept until the parent is migrated.
-import org.springframework.core.io.InputStreamResource;
+import stirling.software.common.model.io.InputStreamResource;
// TODO: Migration required - org.springframework.web.multipart.MultipartFile has no JAX-RS drop-in
// for these constructor params. The parent class ReplaceAndInvertColorStrategy declares the same
// MultipartFile constructor parameter; changing the public signature here would diverge from the
diff --git a/app/common/src/main/java/stirling/software/common/util/misc/ReplaceAndInvertColorStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/ReplaceAndInvertColorStrategy.java
index 865ddb3d4e..feeb8831a5 100644
--- a/app/common/src/main/java/stirling/software/common/util/misc/ReplaceAndInvertColorStrategy.java
+++ b/app/common/src/main/java/stirling/software/common/util/misc/ReplaceAndInvertColorStrategy.java
@@ -6,7 +6,7 @@ import java.io.IOException;
// type of the public abstract replace() method, implemented by subclasses and consumed by
// callers. Replacing it (e.g. with InputStream/StreamingOutput) would ripple across the
// strategy subclasses and their callers, so it is kept until they are migrated together.
-import org.springframework.core.io.InputStreamResource;
+import stirling.software.common.model.io.InputStreamResource;
// TODO: Migration required - org.springframework.web.multipart.MultipartFile is required by
// the inherited PDFFile.setFileInput(MultipartFile) (PDFFile keeps this type for the same
// reason); changing this constructor parameter would ripple to callers and the API binding