Complete common module migration: AutoJob CDI interceptor, InternalApiClient HttpClient, shims; :common compiles
This commit is contained in:
@@ -6,15 +6,16 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Service
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class EndpointConfiguration {
|
||||
|
||||
@@ -52,9 +53,10 @@ public class EndpointConfiguration {
|
||||
private Map<String, Set<String>> endpointAlternatives = new ConcurrentHashMap<>();
|
||||
private final boolean runningProOrHigher;
|
||||
|
||||
@Inject
|
||||
public EndpointConfiguration(
|
||||
ApplicationProperties applicationProperties,
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher) {
|
||||
@Named("runningProOrHigher") boolean runningProOrHigher) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
init();
|
||||
|
||||
@@ -9,7 +9,8 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -47,7 +48,7 @@ import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
|
||||
* <li>Rotated tables (90°/270° pages) may produce incorrect bounds.
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class TabulaTableParser implements TableParser {
|
||||
|
||||
|
||||
+24
-21
@@ -2,31 +2,27 @@ package stirling.software.common.annotations;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import jakarta.enterprise.util.Nonbinding;
|
||||
import jakarta.interceptor.InterceptorBinding;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
|
||||
// TODO: Migration required - Spring meta-annotation machinery has no JAX-RS drop-in.
|
||||
// This annotation is a *composed* meta-annotation: it stamps @RequestMapping(method=POST)
|
||||
// onto the target method and uses @AliasFor to forward value()/consumes() to that
|
||||
// @RequestMapping. JAX-RS meta-annotations do NOT carry @Path/@POST/@Consumes, and there is
|
||||
// no @AliasFor equivalent, so the framework code that reads this annotation (the auto-job
|
||||
// scanner / endpoint registrar) must be reworked to read these attributes directly and
|
||||
// build JAX-RS routing itself. Keeping the Spring imports/annotations below intact preserves
|
||||
// current behavior until that caller-side rewrite happens; converting them in isolation here
|
||||
// would silently break endpoint registration. Editing callers is out of scope for this file.
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
|
||||
/**
|
||||
* Shortcut for a POST endpoint that is executed through the Stirling "auto‑job" framework.
|
||||
*
|
||||
* <p>MIGRATION (Spring -> Quarkus): this was a Spring composed meta-annotation that stamped
|
||||
* {@code @RequestMapping(method=POST)} onto the target via {@code @AliasFor}. JAX-RS does not honour
|
||||
* {@code @Path}/{@code @POST}/{@code @Consumes} through meta-annotations, so this annotation no
|
||||
* longer provides routing. It is now a CDI {@link InterceptorBinding} handled by
|
||||
* {@code AutoJobInterceptor}. <b>Controllers using {@code @AutoJobPostMapping} must additionally
|
||||
* declare their own JAX-RS {@code @POST} + {@code @Path(value)} + {@code @Consumes(consumes)}.</b>
|
||||
* The {@link #value()}/{@link #consumes()} members are retained so a scanner/controller can read the
|
||||
* intended routing.
|
||||
*
|
||||
* <p>Behaviour notes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The endpoint is registered with {@code POST} and, by default, consumes {@code
|
||||
* multipart/form-data} unless you override {@link #consumes()}.
|
||||
* <li>When the client supplies {@code ?async=true} the call is handed to {@link
|
||||
* stirling.software.common.service.JobExecutorService JobExecutorService} where it may be
|
||||
* queued, retried, tracked and subject to time‑outs. For synchronous (default) invocations
|
||||
@@ -36,21 +32,23 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
* GET /api/v1/general/job/{id}</code>.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Unless stated otherwise an attribute only affects <em>async</em> execution.
|
||||
* <p>Unless stated otherwise an attribute only affects <em>async</em> execution. All members are
|
||||
* {@code @Nonbinding} so the single {@code AutoJobInterceptor} matches every annotated method; the
|
||||
* interceptor reads the actual values reflectively from the target method.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
@InterceptorBinding
|
||||
@RequestBody(required = true)
|
||||
public @interface AutoJobPostMapping {
|
||||
|
||||
/** Alias for {@link RequestMapping#value} – the path mapping of the endpoint. */
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "value")
|
||||
/** The path mapping of the endpoint (controllers must mirror this on a JAX-RS {@code @Path}). */
|
||||
@Nonbinding
|
||||
String[] value() default {};
|
||||
|
||||
/** MIME types this endpoint accepts. Defaults to {@code multipart/form-data}. */
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
||||
@Nonbinding
|
||||
String[] consumes() default {MediaType.MULTIPART_FORM_DATA};
|
||||
|
||||
/**
|
||||
@@ -59,6 +57,7 @@ public @interface AutoJobPostMapping {
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
@Nonbinding
|
||||
long timeout() default -1;
|
||||
|
||||
/**
|
||||
@@ -67,6 +66,7 @@ public @interface AutoJobPostMapping {
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
@Nonbinding
|
||||
int retryCount() default 1;
|
||||
|
||||
/**
|
||||
@@ -74,6 +74,7 @@ public @interface AutoJobPostMapping {
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
@Nonbinding
|
||||
boolean trackProgress() default true;
|
||||
|
||||
/**
|
||||
@@ -82,6 +83,7 @@ public @interface AutoJobPostMapping {
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
@Nonbinding
|
||||
boolean queueable() default false;
|
||||
|
||||
/**
|
||||
@@ -92,5 +94,6 @@ public @interface AutoJobPostMapping {
|
||||
* AutoJobPostMappingWeightTest} fails the build if any endpoint leaves it unset. Runtime
|
||||
* readers clamp the value into {@code [1, 100]}.
|
||||
*/
|
||||
@Nonbinding
|
||||
int resourceWeight() default Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
@@ -7,22 +7,17 @@ import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
// TODO: Migration required - org.springframework.web.multipart.MultipartFile has no JAX-RS
|
||||
// drop-in. The type is used internally via FileStorage (retrieveFile/storeFile return/accept
|
||||
// MultipartFile) and PDFFile.getFileInput()/setFileInput(MultipartFile). Changing the type here
|
||||
// would ripple into FileStorage and PDFFile public signatures, so the original type is kept until
|
||||
// those collaborators are migrated.
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.interceptor.AroundInvoke;
|
||||
import jakarta.interceptor.Interceptor;
|
||||
import jakarta.interceptor.InvocationContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
@@ -30,18 +25,17 @@ import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
|
||||
// TODO: Migration required - @Aspect / @Around is Spring AOP (AspectJ proxy weaving). Quarkus/Arc
|
||||
// has no equivalent for advising arbitrary annotated methods this way; this should be reworked as a
|
||||
// CDI @InterceptorBinding + @Interceptor (jakarta.interceptor) or a JAX-RS filter. The aspect
|
||||
// pointcut and ProceedingJoinPoint logic are kept intact pending that rework.
|
||||
@Aspect
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
// @Order(20) -> @Priority: lower precedence, executes AFTER audit aspects populate MDC.
|
||||
// TODO: Migration required - @Priority only orders CDI interceptors/decorators, not Spring AOP
|
||||
// aspects; ordering here is inert until this class is reworked into a CDI interceptor.
|
||||
/**
|
||||
* MIGRATION (Spring AOP -> CDI interceptor): was an {@code @Aspect} with {@code @Around} advice on
|
||||
* {@code @AutoJobPostMapping}. Reworked into a CDI {@link Interceptor} bound by the
|
||||
* {@code @AutoJobPostMapping} {@code @InterceptorBinding}; {@code @Around}/{@code ProceedingJoinPoint}
|
||||
* became {@code @AroundInvoke}/{@link InvocationContext}. {@code @Priority(20)} now meaningfully
|
||||
* orders this interceptor (runs after lower-priority audit interceptors populate MDC).
|
||||
*/
|
||||
@Interceptor
|
||||
@AutoJobPostMapping
|
||||
@Priority(20)
|
||||
@Slf4j
|
||||
public class AutoJobAspect {
|
||||
|
||||
private static final Duration RETRY_BASE_DELAY = Duration.ofMillis(100);
|
||||
@@ -50,10 +44,20 @@ public class AutoJobAspect {
|
||||
private final HttpServletRequest request;
|
||||
private final FileStorage fileStorage;
|
||||
|
||||
@Around("@annotation(autoJobPostMapping)")
|
||||
public Object wrapWithJobExecution(
|
||||
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) throws Exception {
|
||||
// This aspect will run before any audit aspects due to @Order(0)
|
||||
@Inject
|
||||
public AutoJobAspect(
|
||||
JobExecutorService jobExecutorService,
|
||||
HttpServletRequest request,
|
||||
FileStorage fileStorage) {
|
||||
this.jobExecutorService = jobExecutorService;
|
||||
this.request = request;
|
||||
this.fileStorage = fileStorage;
|
||||
}
|
||||
|
||||
@AroundInvoke
|
||||
public Object wrapWithJobExecution(InvocationContext ctx) throws Exception {
|
||||
AutoJobPostMapping autoJobPostMapping =
|
||||
ctx.getMethod().getAnnotation(AutoJobPostMapping.class);
|
||||
// Extract parameters from the request and annotation
|
||||
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
||||
log.debug(
|
||||
@@ -74,7 +78,8 @@ public class AutoJobAspect {
|
||||
trackProgress);
|
||||
|
||||
// Process arguments in-place to avoid type mismatch issues
|
||||
Object[] args = processArgsInPlace(joinPoint.getArgs(), async);
|
||||
Object[] args = processArgsInPlace(ctx.getParameters(), async);
|
||||
ctx.setParameters(args);
|
||||
|
||||
// Extract queueable and resourceWeight parameters and validate
|
||||
boolean queueable = autoJobPostMapping.queueable();
|
||||
@@ -93,7 +98,7 @@ public class AutoJobAspect {
|
||||
// The trackProgress flag controls whether detailed progress is
|
||||
// stored
|
||||
// for REST API queries, not WebSocket notifications
|
||||
return joinPoint.proceed(args);
|
||||
return ctx.proceed();
|
||||
} catch (Throwable ex) {
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution: {}",
|
||||
@@ -114,7 +119,7 @@ public class AutoJobAspect {
|
||||
} else {
|
||||
// Use retry logic
|
||||
return executeWithRetries(
|
||||
joinPoint,
|
||||
ctx,
|
||||
args,
|
||||
async,
|
||||
timeout,
|
||||
@@ -126,7 +131,7 @@ public class AutoJobAspect {
|
||||
}
|
||||
|
||||
private Object executeWithRetries(
|
||||
ProceedingJoinPoint joinPoint,
|
||||
InvocationContext ctx,
|
||||
Object[] args,
|
||||
boolean async,
|
||||
long timeout,
|
||||
@@ -171,7 +176,7 @@ public class AutoJobAspect {
|
||||
}
|
||||
|
||||
// Attempt to execute the operation
|
||||
return joinPoint.proceed(args);
|
||||
return ctx.proceed();
|
||||
|
||||
} catch (Throwable ex) {
|
||||
lastException = ex;
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
// TODO: Migration required - this class implements the Spring SPI
|
||||
// org.springframework.core.io.support.PropertySourceFactory, which is used together with
|
||||
// Spring's @PropertySource(factory = YamlPropertySourceFactory.class) to load YAML into the
|
||||
// Spring Environment. Quarkus/MicroProfile Config (SmallRye) has no PropertySourceFactory,
|
||||
// PropertySource, PropertiesPropertySource, EncodedResource, or YamlPropertiesFactoryBean
|
||||
// equivalent; YAML is handled natively via application.yaml + ConfigSource. There is no
|
||||
// faithful drop-in conversion, so the original Spring logic is intentionally left intact.
|
||||
// Replacement path: implement an org.eclipse.microprofile.config.spi.ConfigSource (or use
|
||||
// io.smallrye.config.source.yaml.YamlConfigSource) and register it, then delete this class
|
||||
// and its @PropertySource usages. The Spring imports below cannot be removed without breaking
|
||||
// the SPI contract this class implements.
|
||||
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
import org.springframework.core.io.support.PropertySourceFactory;
|
||||
|
||||
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||
|
||||
@Override
|
||||
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) {
|
||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||
factory.setResources(encodedResource.getResource());
|
||||
Properties properties = factory.getObject();
|
||||
|
||||
return new PropertiesPropertySource(
|
||||
encodedResource.getResource().getFilename(), properties);
|
||||
}
|
||||
}
|
||||
+13
-48
@@ -15,15 +15,9 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
// TODO: Migration required - the Spring property-source machinery below
|
||||
// (ConfigurableEnvironment, PropertySource, EncodedResource) has no direct
|
||||
// Quarkus/MicroProfile-Config drop-in. The dynamicYamlPropertySource method keeps these
|
||||
// three Spring imports until the YAML-into-config wiring is reimplemented via a custom
|
||||
// org.eclipse.microprofile.config.spi.ConfigSource plus @io.smallrye.config.ConfigMapping.
|
||||
// Resource/ClassPathResource/FileSystemResource are now the local shim types, not Spring.
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
// Resource/ClassPathResource/FileSystemResource are the local migration shim types (used by the
|
||||
// SAML cert/key accessors), not Spring. The former Spring property-source imports
|
||||
// (ConfigurableEnvironment/PropertySource/EncodedResource) were dropped with dynamicYamlPropertySource.
|
||||
import stirling.software.common.model.io.ClassPathResource;
|
||||
import stirling.software.common.model.io.FileSystemResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
@@ -41,7 +35,6 @@ import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.YamlPropertySourceFactory;
|
||||
import stirling.software.common.constants.JwtConstants;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
@@ -87,44 +80,16 @@ public class ApplicationProperties {
|
||||
private Cluster cluster = new Cluster();
|
||||
private Policies policies = new Policies();
|
||||
|
||||
// TODO: Migration required - this was a Spring @Bean that injected the
|
||||
// ConfigurableEnvironment and registered an extra YAML PropertySource at runtime so
|
||||
// settings.yml could override (or be overridden by) other property sources. Quarkus has
|
||||
// no ConfigurableEnvironment/PropertySource model; the equivalent is a custom
|
||||
// org.eclipse.microprofile.config.spi.ConfigSource (registered via ConfigSourceProvider
|
||||
// or @io.smallrye.config.ConfigMapping). The original logic is preserved verbatim below
|
||||
// and is currently never invoked by CDI since the @Bean annotation has been removed.
|
||||
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||
throws IOException {
|
||||
String configPath = InstallationPathConfig.getSettingsPath();
|
||||
log.debug("Attempting to load settings from: {}", configPath);
|
||||
|
||||
File file = new File(configPath);
|
||||
if (!file.exists()) {
|
||||
log.error("Warning: Settings file does not exist at: {}", configPath);
|
||||
}
|
||||
|
||||
Resource resource = new FileSystemResource(configPath);
|
||||
if (!resource.exists()) {
|
||||
throw new FileNotFoundException("Settings file not found at: " + configPath);
|
||||
}
|
||||
|
||||
EncodedResource encodedResource = new EncodedResource(resource);
|
||||
PropertySource<?> propertySource =
|
||||
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
|
||||
|
||||
boolean saasActive = Arrays.asList(environment.getActiveProfiles()).contains("saas");
|
||||
if (saasActive) {
|
||||
// Saas-pinned values in application-saas.properties must beat settings.yml.
|
||||
environment.getPropertySources().addLast(propertySource);
|
||||
} else {
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
}
|
||||
|
||||
log.debug("Loaded properties: {}", propertySource.getSource());
|
||||
|
||||
return propertySource;
|
||||
}
|
||||
// REMOVED (Spring -> Quarkus): dynamicYamlPropertySource(ConfigurableEnvironment).
|
||||
// This was a Spring @Bean that registered settings.yml as an extra runtime PropertySource on the
|
||||
// ConfigurableEnvironment (added first, or last under the "saas" profile). Quarkus has no
|
||||
// ConfigurableEnvironment/PropertySource model and the @Bean had already been removed, so the
|
||||
// method was dead code referencing Spring-only types.
|
||||
// TODO: Migration required - reimplement external settings.yml loading as a custom
|
||||
// org.eclipse.microprofile.config.spi.ConfigSource (registered via a ConfigSourceProvider /
|
||||
// META-INF/services), giving it an ordinal that reproduces the old precedence: higher than the
|
||||
// application defaults normally, but lower than application-saas.properties under the saas
|
||||
// profile. Wire it in ConfigInitializer.
|
||||
|
||||
/**
|
||||
* Initialize fileUploadLimit from environment variables if not set in settings.yml. Supports
|
||||
|
||||
@@ -6,6 +6,8 @@ import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import stirling.software.common.model.io.Resource;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for Spring's {@code org.springframework.web.multipart.MultipartFile}.
|
||||
*
|
||||
@@ -37,6 +39,20 @@ public interface MultipartFile {
|
||||
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
/**
|
||||
* The content as a {@link Resource}. The default is a stream-backed resource; file-backed
|
||||
* implementations (e.g. {@code FileUploadMultipartFile}) override this to enable zero-copy fast
|
||||
* paths.
|
||||
*/
|
||||
default Resource getResource() {
|
||||
try {
|
||||
return new stirling.software.common.model.io.InputStreamResource(
|
||||
getInputStream(), getOriginalFilename());
|
||||
} catch (IOException e) {
|
||||
throw new java.io.UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
default void transferTo(File dest) throws IOException {
|
||||
transferTo(dest.toPath());
|
||||
}
|
||||
|
||||
+7
@@ -4,6 +4,8 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
import stirling.software.common.model.io.InputStreamResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
|
||||
/**
|
||||
* In-memory {@link MultipartFile} backed by a byte array. Useful for tests and for code paths that
|
||||
@@ -58,4 +60,9 @@ public class ByteArrayMultipartFile implements MultipartFile {
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getResource() {
|
||||
return new InputStreamResource(new ByteArrayInputStream(content), originalFilename);
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -8,6 +8,8 @@ import java.nio.file.Path;
|
||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
import stirling.software.common.model.io.FileSystemResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
|
||||
/**
|
||||
* Adapts a Quarkus REST {@link FileUpload} (the inbound multipart representation at the JAX-RS
|
||||
@@ -62,6 +64,12 @@ public class FileUploadMultipartFile implements MultipartFile {
|
||||
return Files.newInputStream(delegate.uploadedFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getResource() {
|
||||
// File-backed: enables FileStorage's zero-copy fast path.
|
||||
return new FileSystemResource(delegate.uploadedFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferTo(Path dest) throws IOException {
|
||||
Files.copy(delegate.uploadedFile(), dest);
|
||||
|
||||
@@ -1,52 +1,37 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpTimeoutException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
// TODO: Migration required - the HTTP dispatch in this class is built entirely on Spring's
|
||||
// RestTemplate (RestTemplate.execute/httpEntityCallback with SimpleClientHttpRequestFactory,
|
||||
// RequestCallback, ResponseExtractor and ResourceAccessException). The faithful Quarkus target is
|
||||
// java.net.http.HttpClient (HttpClient.Builder for the connect/read timeouts, multipart body built
|
||||
// manually, ConnectException/HttpTimeoutException replacing ResourceAccessException). That rewrite
|
||||
// is blocked here because the public API surface uses Spring HTTP types that cannot change without
|
||||
// editing callers:
|
||||
// - post(String, MultiValueMap<String, Object>) : ResponseEntity<Resource>
|
||||
// is called by, and its MultiValueMap argument / ResponseEntity<Resource> result are consumed
|
||||
// by, McpOperationExecutor and PolicyExecutor (app/proprietary) and asserted directly by
|
||||
// InternalApiClientTest (which mocks RestTemplate.execute + ResponseExtractor).
|
||||
// - ResponseEntity<Resource> -> jakarta.ws.rs.core.Response, MultiValueMap -> a manual
|
||||
// Map<String,List<Object>>/form encoding, HttpHeaders/HttpStatus/HttpEntity/MediaType/HttpMethod
|
||||
// -> jakarta.ws.rs.core equivalents, all in lockstep with those callers.
|
||||
// These Spring imports are intentionally retained until that cross-file migration is scheduled;
|
||||
// they are listed explicitly (no wildcard) so each retained type is auditable.
|
||||
import stirling.software.common.model.io.FileSystemResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RequestCallback;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import org.eclipse.microprofile.config.Config;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.io.FileSystemResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@@ -54,6 +39,12 @@ import stirling.software.common.util.TempFileManager;
|
||||
* Dispatches HTTP POST requests to internal Stirling API endpoints via loopback. Used by
|
||||
* PipelineProcessor and AiWorkflowService to execute tool operations programmatically without
|
||||
* leaving the JVM network stack.
|
||||
*
|
||||
* <p>MIGRATION (Spring -> Quarkus): the HTTP dispatch was rebuilt on {@link java.net.http.HttpClient}
|
||||
* (replacing Spring's {@code RestTemplate}/{@code SimpleClientHttpRequestFactory}). The multipart
|
||||
* body is encoded manually; {@code MultiValueMap<String,Object>} became {@code
|
||||
* Map<String,List<Object>>} and {@code ResponseEntity<Resource>} became {@link Response}.
|
||||
* {@code ResourceAccessException} timeout handling is now driven by {@link HttpTimeoutException}.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
@@ -76,7 +67,7 @@ public class InternalApiClient {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final Config config;
|
||||
private final Duration readTimeout;
|
||||
private final RestTemplate restTemplate;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
public InternalApiClient(
|
||||
ServletContext servletContext,
|
||||
@@ -94,10 +85,10 @@ public class InternalApiClient {
|
||||
// because this is a loopback call; if connecting takes longer than a few seconds the
|
||||
// local server is itself unhealthy.
|
||||
this.readTimeout = Duration.ofSeconds(internalApi.getReadTimeoutSeconds());
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(Duration.ofSeconds(internalApi.getConnectTimeoutSeconds()));
|
||||
factory.setReadTimeout(readTimeout);
|
||||
this.restTemplate = new RestTemplate(factory);
|
||||
this.httpClient =
|
||||
HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(internalApi.getConnectTimeoutSeconds()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,73 +96,105 @@ public class InternalApiClient {
|
||||
* prefixes (e.g. {@code /api/v1/misc/compress-pdf}).
|
||||
*
|
||||
* @param endpointPath API path (e.g. {@code /api/v1/general/rotate-pdf})
|
||||
* @param body multipart form body (fileInput + parameters)
|
||||
* @return response with the result file as a {@link TempFileResource} body
|
||||
* @param body multipart form body (fileInput + parameters): each value is either a {@link
|
||||
* Resource} (file part) or a {@code String} (form field)
|
||||
* @return JAX-RS {@link Response} with the result file as a {@link TempFileResource} entity
|
||||
*/
|
||||
public ResponseEntity<Resource> post(String endpointPath, MultiValueMap<String, Object> body) {
|
||||
public Response post(String endpointPath, Map<String, List<Object>> body) {
|
||||
validateUrl(endpointPath);
|
||||
String url = getBaseUrl() + endpointPath;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
String boundary = "----StirlingBoundary" + Long.toHexString(System.nanoTime());
|
||||
byte[] multipartBody = encodeMultipart(body, boundary);
|
||||
|
||||
HttpRequest.Builder requestBuilder =
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.timeout(readTimeout)
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(multipartBody));
|
||||
|
||||
String apiKey = getApiKeyForUser();
|
||||
if (apiKey != null && !apiKey.isEmpty()) {
|
||||
headers.add("X-API-KEY", apiKey);
|
||||
requestBuilder.header("X-API-KEY", apiKey);
|
||||
}
|
||||
|
||||
// A no-file ai/tools call (e.g. create-pdf-from-html-agent) sends only string params, so
|
||||
// without this RestTemplate would use urlencoded instead of the multipart the controller
|
||||
// expects. File-bearing calls get the right multipart content-type from RestTemplate.
|
||||
boolean isAiTool = endpointPath.startsWith("/api/v1/ai/tools/");
|
||||
boolean hasFilePart =
|
||||
body.values().stream()
|
||||
.flatMap(java.util.List::stream)
|
||||
.anyMatch(v -> v instanceof Resource);
|
||||
if (isAiTool && !hasFilePart) {
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
}
|
||||
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
RequestCallback requestCallback = restTemplate.httpEntityCallback(entity, Resource.class);
|
||||
|
||||
try {
|
||||
return restTemplate.execute(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
requestCallback,
|
||||
response -> {
|
||||
try {
|
||||
TempFile tempFile =
|
||||
tempFileManager.createManagedTempFile("internal-api");
|
||||
Files.copy(
|
||||
response.getBody(),
|
||||
tempFile.getPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
String filename = extractFilename(response.getHeaders());
|
||||
TempFileResource resource = new TempFileResource(tempFile, filename);
|
||||
return ResponseEntity.status(response.getStatusCode())
|
||||
.headers(response.getHeaders())
|
||||
.body(resource);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
});
|
||||
} catch (ResourceAccessException e) {
|
||||
// RestTemplate wraps low-level I/O failures in ResourceAccessException. Only the
|
||||
// SocketTimeoutException-rooted case is a real timeout; other I/O failures (connection
|
||||
// refused, DNS, etc.) propagate as-is so the upstream generic handler can describe
|
||||
// them accurately.
|
||||
if (e.getCause() instanceof java.net.SocketTimeoutException) {
|
||||
throw new InternalApiTimeoutException(endpointPath, readTimeout, e);
|
||||
HttpResponse<InputStream> response =
|
||||
httpClient.send(
|
||||
requestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream());
|
||||
try (InputStream responseBody = response.body()) {
|
||||
TempFile tempFile = tempFileManager.createManagedTempFile("internal-api");
|
||||
Files.copy(
|
||||
responseBody,
|
||||
tempFile.getPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
String filename =
|
||||
extractFilename(
|
||||
response.headers().firstValue("Content-Disposition").orElse(null));
|
||||
TempFileResource resource = new TempFileResource(tempFile, filename);
|
||||
Response.ResponseBuilder rb = Response.status(response.statusCode()).entity(resource);
|
||||
response.headers()
|
||||
.map()
|
||||
.forEach((k, vs) -> vs.forEach(v -> rb.header(k, v)));
|
||||
return rb.build();
|
||||
}
|
||||
throw e;
|
||||
} catch (HttpTimeoutException e) {
|
||||
throw new InternalApiTimeoutException(endpointPath, readTimeout, e);
|
||||
} catch (ConnectException e) {
|
||||
throw new UncheckedIOException(new IOException("Internal API connection failed", e));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Internal API dispatch interrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Encode a multipart/form-data body. File parts are {@link Resource}; others are form fields. */
|
||||
private static byte[] encodeMultipart(Map<String, List<Object>> body, String boundary) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
for (Map.Entry<String, List<Object>> entry : body.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
for (Object value : entry.getValue()) {
|
||||
baos.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
if (value instanceof Resource resource) {
|
||||
String fn = resource.getFilename() != null ? resource.getFilename() : name;
|
||||
baos.write(
|
||||
("Content-Disposition: form-data; name=\""
|
||||
+ name
|
||||
+ "\"; filename=\""
|
||||
+ fn
|
||||
+ "\"\r\n"
|
||||
+ "Content-Type: application/octet-stream\r\n\r\n")
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
try (InputStream in = resource.getInputStream()) {
|
||||
in.transferTo(baos);
|
||||
}
|
||||
baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
baos.write(
|
||||
("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n")
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
baos.write(
|
||||
String.valueOf(value).getBytes(StandardCharsets.UTF_8));
|
||||
baos.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
baos.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the filename from a response's {@code Content-Disposition} header. Returns {@code
|
||||
* null} if the header is missing or has no filename.
|
||||
* Extract the filename from a {@code Content-Disposition} header value. Returns {@code null} if
|
||||
* the header is missing or has no filename.
|
||||
*/
|
||||
private static String extractFilename(HttpHeaders headers) {
|
||||
String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);
|
||||
private static String extractFilename(String contentDisposition) {
|
||||
if (contentDisposition == null || contentDisposition.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
@@ -190,14 +213,11 @@ public class InternalApiClient {
|
||||
}
|
||||
|
||||
private String getBaseUrl() {
|
||||
// Resolve the port lazily so desktop mode (server.port=0, OS-assigned) dispatches to the
|
||||
// actual bound port. The "local.server.port" config key is published once the web server
|
||||
// is up; fall back to the configured server.port for early calls (tests, non-web contexts).
|
||||
// TODO: Migration required - verify the runtime exposes "local.server.port"/"server.port"
|
||||
// via MicroProfile Config under Quarkus (Quarkus uses "quarkus.http.port" and, for
|
||||
// random-port test/dev runs, "quarkus.http.test-port"); this lazy port lookup assumed
|
||||
// Spring Boot's WebServerInitializedEvent populating "local.server.port".
|
||||
String port = config.getOptionalValue("local.server.port", String.class).orElse(null);
|
||||
// Resolve the port lazily so desktop mode dispatches to the actual bound port.
|
||||
// TODO: Migration required - verify Quarkus exposes the bound port via config. Quarkus uses
|
||||
// "quarkus.http.port" and, for random-port test/dev runs, "quarkus.http.test-port"; the old
|
||||
// "local.server.port"/"server.port" keys came from Spring Boot's WebServerInitializedEvent.
|
||||
String port = config.getOptionalValue("quarkus.http.port", String.class).orElse(null);
|
||||
if (port == null) {
|
||||
port = config.getOptionalValue("server.port", String.class).orElse("8080");
|
||||
}
|
||||
|
||||
@@ -14,11 +14,6 @@ import stirling.software.common.model.io.Resource;
|
||||
// JAX-RS drop-in for these utility method params; changing the type would ripple to
|
||||
// callers, so it is kept as-is for now.
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
// TODO: Migration required - org.springframework.web.servlet.mvc.method.annotation
|
||||
// .StreamingResponseBody is a Spring MVC servlet type with no JAX-RS equivalent here;
|
||||
// it is still consumed by FileStorage#storeFromStreamingBody (whose signature has not yet
|
||||
// been migrated), so the result-handling branch below keeps this type as-is for now.
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
@@ -26,6 +21,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.StreamingOutput;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
@@ -248,18 +244,15 @@ public class JobExecutorService {
|
||||
String fileId = fileStorage.storeBytes((byte[]) body, filename);
|
||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||
log.debug("Stored Response<byte[]> result with fileId: {}", fileId);
|
||||
} else if (body instanceof StreamingResponseBody streamingBody) {
|
||||
// TODO: Migration required - FileStorage#storeFromStreamingBody still accepts the
|
||||
// Spring StreamingResponseBody type. Once that signature is migrated to
|
||||
// jakarta.ws.rs.core.StreamingOutput, this branch should test for StreamingOutput
|
||||
// (the type a JAX-RS Response carries for streamed bodies) instead.
|
||||
} else if (body instanceof StreamingOutput streamingBody) {
|
||||
// JAX-RS Response carries a StreamingOutput for streamed bodies (migrated from
|
||||
// Spring's StreamingResponseBody).
|
||||
String filename = extractResponseFilename(response);
|
||||
String contentType = extractResponseContentType(response);
|
||||
|
||||
String fileId = fileStorage.storeFromStreamingBody(streamingBody, filename);
|
||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||
log.debug(
|
||||
"Stored Response<StreamingResponseBody> result with fileId: {}", fileId);
|
||||
log.debug("Stored Response<StreamingOutput> result with fileId: {}", fileId);
|
||||
} else if (body instanceof Resource resource) {
|
||||
String filename = extractResponseFilename(response);
|
||||
String contentType = extractResponseContentType(response);
|
||||
|
||||
@@ -19,12 +19,10 @@ import java.util.regex.Pattern;
|
||||
// ClassPathUtils or build-time indexing). org.springframework.core.io.ResourceLoader and
|
||||
// org.springframework.core.io.support.ResourcePatternUtils have no Quarkus/Jakarta shim. They are
|
||||
// used only by getResourcesFromLocationPattern below; that method's public signature and the Spring
|
||||
// resource-pattern glob resolver are relied on by callers, so the imports and logic are kept intact
|
||||
// until the glob rewrite (which ripples to callers) is done. The shim
|
||||
// stirling.software.common.model.io.Resource is already used for the return type.
|
||||
// MIGRATION: Spring ResourceLoader/ResourcePatternUtils glob scanning replaced by java.nio
|
||||
// directory listing (file:) + classloader lookup (classpath:); see getResourcesFromLocationPattern.
|
||||
import stirling.software.common.model.io.FileSystemResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.ResourcePatternUtils;
|
||||
|
||||
// TODO: Migration required - org.springframework.web.multipart.MultipartFile has no servlet/JAX-RS
|
||||
// drop-in for these utility method params. The public signatures of convertMultipartFileToFile and
|
||||
@@ -259,22 +257,60 @@ public class GeneralUtils {
|
||||
return safeName;
|
||||
}
|
||||
|
||||
// Get resources from a location pattern
|
||||
// TODO: Migration required - replace Spring classpath scanning with Quarkus (e.g. io.quarkus
|
||||
// ClassPathUtils or build-time indexing). The ResourceLoader param and ResourcePatternUtils glob
|
||||
// resolver have no Quarkus/Jakarta drop-in; the logic is preserved verbatim pending a
|
||||
// filesystem/classpath glob rewrite that also touches every caller of this method.
|
||||
public Resource[] getResourcesFromLocationPattern(
|
||||
String locationPattern, ResourceLoader resourceLoader) throws Exception {
|
||||
// Normalize the path for file resources
|
||||
String pattern = locationPattern;
|
||||
if (pattern.startsWith("file:")) {
|
||||
String rawPath = pattern.substring(5).replace("\\*", "").replace("/*", "");
|
||||
Path normalizePath = Paths.get(rawPath).normalize();
|
||||
pattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
|
||||
/**
|
||||
* Resolve files matching a location pattern. Supports {@code file:<dir>/<glob>} and {@code
|
||||
* classpath:<dir>/<glob>} (e.g. {@code *} or {@code *.woff2}).
|
||||
*
|
||||
* <p>MIGRATION (Spring -> Quarkus): replaced Spring's {@code ResourceLoader} +
|
||||
* {@code ResourcePatternUtils} pattern resolver. The {@code ResourceLoader} parameter was
|
||||
* removed. {@code file:} patterns are resolved with {@link java.nio.file.Files#list}; {@code
|
||||
* classpath:} patterns are resolved via the classloader and only support directory resources
|
||||
* that live on the filesystem.
|
||||
*
|
||||
* <p>TODO: Migration required - {@code classpath:} resolution does not enumerate entries inside a
|
||||
* packaged JAR. For uber-jar deployments, prefer serving these assets from
|
||||
* {@code META-INF/resources/} or build a Jandex/build-time index of the matching files.
|
||||
*/
|
||||
public static Resource[] getResourcesFromLocationPattern(String locationPattern)
|
||||
throws Exception {
|
||||
String body = locationPattern;
|
||||
boolean classpath = false;
|
||||
if (body.startsWith("file:")) {
|
||||
body = body.substring(5);
|
||||
} else if (body.startsWith("classpath:")) {
|
||||
body = body.substring(10);
|
||||
classpath = true;
|
||||
}
|
||||
return ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||
.getResources(pattern);
|
||||
body = body.replace("\\", "/");
|
||||
int lastSlash = body.lastIndexOf('/');
|
||||
String dirPart = lastSlash >= 0 ? body.substring(0, lastSlash) : "";
|
||||
String glob = lastSlash >= 0 ? body.substring(lastSlash + 1) : body;
|
||||
if (glob.isEmpty()) {
|
||||
glob = "*";
|
||||
}
|
||||
|
||||
Path dir;
|
||||
if (classpath) {
|
||||
URL url = GeneralUtils.class.getClassLoader().getResource(dirPart);
|
||||
if (url == null || !"file".equals(url.getProtocol())) {
|
||||
// Not a filesystem-backed classpath dir (e.g. inside a jar) - see TODO above.
|
||||
return new Resource[0];
|
||||
}
|
||||
dir = Paths.get(url.toURI());
|
||||
} else {
|
||||
dir = Paths.get(dirPart).normalize();
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(dir)) {
|
||||
return new Resource[0];
|
||||
}
|
||||
PathMatcher matcher = dir.getFileSystem().getPathMatcher("glob:" + glob);
|
||||
List<Resource> resources = new ArrayList<>();
|
||||
try (var stream = Files.list(dir)) {
|
||||
stream.filter(p -> Files.isRegularFile(p) && matcher.matches(p.getFileName()))
|
||||
.forEach(p -> resources.add(new FileSystemResource(p)));
|
||||
}
|
||||
return resources.toArray(new Resource[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -221,7 +221,7 @@ public class UIDataController {
|
||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||
try {
|
||||
Resource[] resources =
|
||||
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
|
||||
GeneralUtils.getResourcesFromLocationPattern(locationPattern);
|
||||
return Arrays.stream(resources)
|
||||
.map(
|
||||
resource -> {
|
||||
|
||||
Reference in New Issue
Block a user