Static Files in Subfolder ClassPathResource - spring

I want to return static files:
1)
resource
|_______static
|________assets
|_________css
|__________bootstrap.min.css
The below is my configuration
#Bean
fun staticRouter(): RouterFunction<ServerResponse> {
return RouterFunctions.resources("/**", ClassPathResource("static/**/"))
}
When I access localhost:8080/assets/css/bootstrap.min.css, I get 404 not found
Did I write a wrong pattern?
2)
However, if I remove assets, it's working
resource
|_______static
|________css
|_________bootstrap.min.css
============================================================================
#Bean
fun staticRouter(): RouterFunction<ServerResponse> {
return RouterFunctions.resources("/**", ClassPathResource("static/"))
}
============================================================================
localhost:8080/css/bootstrap.min.css, 200 OK
I added assets because this is the requirement.

You can use
#Bean
fun staticRouter(): RouterFunction<ServerResponse> {
return RouterFunctions.resources("/{*filepaths}", ClassPathResource("static/**/"))
}
See https://www.baeldung.com/spring-5-mvc-url-matching#1-uri-variable-syntax-foo-using-a-handler-method

Related

WebServerFactoryCustomizer is not hit in a Springboot Webflux app

Following Configure the Web Server , I add a NettyWebServerFactoryCustomizer
#Configuration
public class NettyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
#Override
public void customize(NettyReactiveWebServerFactory factory) {
factory.addServerCustomizers(httpServer -> {
return httpServer
.wiretap(true)
.metrics(true, s->s)
.doOnConnection(conn -> {
conn.addHandlerFirst(new ReadTimeoutHandler(50, TimeUnit.MILLISECONDS));
});
});
}
}
I have two questions:
When I run the app, the customize function is not hit. Where do I miss?
My purpose is to enable the Netty metrics, I can't find any documents about config the metrics in the application.yml file. so I add the NettyWebServerFactoryCustomizer.
The second parameter of .metrics(true, s->s) is a uriTagValue, Are there any example about how to pass in value? I just use s->s because I refer this, but this maybe can't avoid cardinality explosion, Are there any function like ServerWebExchange.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE) simple give us the templated URL?
I found the workaround of the question 1: define a bean instead of implementing WebServerFactoryCustomizer
#Bean
public ReactiveWebServerFactory reactiveWebServerFactory() {
NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory();
factory.addServerCustomizers(builder -> builder
.wiretap(true)
.metrics(true,s->s)
.accessLog(true)
.doOnConnection(conn -> {
conn.addHandlerFirst(new ReadTimeoutHandler(50, TimeUnit.MILLISECONDS));
}));
return factory;
}
About your question 2 : The second parameter of .metrics(true, s->s) is a uriTagValue, Are there any example about how to pass in value?
private static final Pattern URI_TEMPLATE_PATTERN = Pattern.compile("/test/.*");
#Bean
public ReactiveWebServerFactory reactiveWebServerFactory() {
NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory();
factory.addServerCustomizers(builder -> builder
.wiretap(true)
.metrics(true,
uriValue ->
{
Matcher matcher = URI_TEMPLATE_PATTERN .matcher(uriValue);
if (matcher.matches()) {
return "/test/";
}
return "/";
}
.accessLog(true)
.doOnConnection(conn -> {
conn.addHandlerFirst(new ReadTimeoutHandler(50, TimeUnit.MILLISECONDS));
}));
return factory;
}

How to Connect to Multiple Google spanner DB running on different host from Spring Boot App

Hi I am trying to connect my spring boot application to multiple google cloud spanner DB. I am able to connect with Single database by making entry in application.yml file. My requirement is tonnect with two spanner database in same application. Please help me.
The approach is a lengthy one.
The com.google.cloud:spring-cloud-gcp-starter-data-spanner lib autoconfigures using application.properties
To avoid this,
a. you use spring-cloud-gcp-data-spanner
b. You would have to define the spannerTemplate bean manually
Here is a quick code snippet
#Bean
public com.google.auth.Credentials getCredentials() {
try {
return new DefaultCredentialsProvider(Credentials::new).getCredentials();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
#Bean
public SpannerOptions spannerOptions() {
return SpannerOptions.newBuilder()
.setProjectId("sab-dev-anciq-ml-gol-8565")
.setSessionPoolOption(SessionPoolOptions.newBuilder().setMaxSessions(10).build())
.setCredentials(getCredentials())
.build();
}
#Bean
public Spanner spanner(SpannerOptions spannerOptions) {
return spannerOptions.getService();
}
#Bean
public DatabaseId databaseId() {
return DatabaseId.of("your_gcp_project_id",
"instance_name",
"db_name");
}
#Bean
public DatabaseClient spannerDatabaseClient(Spanner spanner, DatabaseId databaseId) {
return spanner.getDatabaseClient(databaseId);
}
#Bean
public SpannerMappingContext spannerMappingContext(Gson gson) {
return new SpannerMappingContext(gson);
}
#Bean
public SpannerEntityProcessor spannerConverter(SpannerMappingContext mappingContext) {
return new ConverterAwareMappingSpannerEntityProcessor(mappingContext);
}
#Bean
public SpannerSchemaUtils spannerSchemaUtils(
SpannerMappingContext spannerMappingContext, SpannerEntityProcessor spannerEntityProcessor) {
return new SpannerSchemaUtils(spannerMappingContext, spannerEntityProcessor, true);
}
#Bean
public SpannerMutationFactory spannerMutationFactory(
SpannerEntityProcessor spannerEntityProcessor,
SpannerMappingContext spannerMappingContext,
SpannerSchemaUtils spannerSchemaUtils) {
return new SpannerMutationFactoryImpl(
spannerEntityProcessor, spannerMappingContext, spannerSchemaUtils);
}
#Bean("hostSpannerTemplate")
public SpannerTemplate spannerTemplateForHostProject(
DatabaseClient databaseClient,
SpannerMappingContext mappingContext,
SpannerEntityProcessor spannerEntityProcessor,
SpannerMutationFactory spannerMutationFactory,
SpannerSchemaUtils spannerSchemaUtils) {
return new SpannerTemplate(
() -> databaseClient,
mappingContext,
spannerEntityProcessor,
spannerMutationFactory,
spannerSchemaUtils);
}
You can now use spannerTemplateForHostProject as a bean to do your curd ops related to host project
Similiarly using Qualifiers you can now define another spannerTemplate that uses databaseClient2 using qualifier in argument

How to get current resource name

I am using MultiResourceItemReader in order to read and eventually write a list of CSV files to the database.
#StepScope
#Bean
public MultiResourceItemReader<DailyExport> multiResourceItemReader(#Value("#{stepExecutionContext[listNotLoadedFilesPath]}") List<String> notLoadedFilesPath) {
logger.info("** start multiResourceItemReader **");
// cast List of not loaded files to array of resources
List <Resource>tmpList = new ArrayList<Resource>();
notLoadedFilesPath.stream().forEach(fullPath -> {
Resource resource = new FileSystemResource(fullPath);
tmpList.add(resource);
});
Resource [] resourceArr = tmpList.toArray(new Resource[tmpList.size()]);
MultiResourceItemReader<DailyExport> multiResourceItemReader = new MultiResourceItemReader<>();
multiResourceItemReader.setName("dailyExportMultiReader");
multiResourceItemReader.setDelegate(reader(dailyExportMapper()));
multiResourceItemReader.setResources(resourceArr);
return multiResourceItemReader;
}
#Bean
public FlatFileItemReader<DailyExport> reader(FieldSetMapper<DailyExport> testClassRowMapper) {
logger.info("** start reader **");
// Create reader instance
FlatFileItemReader<DailyExport> reader = new FlatFileItemReaderBuilder<DailyExport>()
.name("dailyExportReader")
.linesToSkip(1).fieldSetMapper(testClassRowMapper)
.delimited().delimiter("|").names(dailyExportMetadata)
.build();
return reader;
}
Everything is working well but I also need to store the current file\resource name.
I found this API getCurrentResource but I couldn't figure how to use it. Is there a way to get the current resource during the process stage?
public class DailyExportItemProcessor implements ItemProcessor<DailyExport, DailyExport>{
#Autowired
public MultiResourceItemReader<DailyExport> multiResourceItemReader;
#Override
public DailyExport process(DailyExport item) throws Exception {
// multiResourceItemReader.getCurrent ??
return item;
}
Thank you
ResourceAware is what you need, it allows you set the original resource on the item so you can get access to it in the processor (or anywhere else where the item is in scope):
class DailyExport implement ResourceAware {
private Resource resource;
// getter/setter for resource
}
then in the processor:
public class DailyExportItemProcessor implements ItemProcessor<DailyExport, DailyExport>{
#Override
public DailyExport process(DailyExport item) throws Exception {
Resource currentResource = item.getResource();
// do something with the item/resource
return item;
}
}

Springdoc GroupedOpenApi not following global parameters set with OperationCustomizer

When using GroupedOpenApi to define an API group, the common set of parameters that are added to every endpoint is not present in the parameters list.
Below are the respective codes
#Bean
public GroupedOpenApi v1Apis() {
return GroupedOpenApi.builder().group("v1 APIs")
// hide all v2 APIs
.pathsToExclude("/api/v2/**", "/v2/**")
// show all v1 APIs
.pathsToMatch("/api/v1/**", "/v1/**")
.build();
}
And the class to add the Standard Headers to all the endpoints
#Component
public class GlobalHeaderAdder implements OperationCustomizer {
#Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
operation.addParametersItem(new Parameter().$ref("#/components/parameters/ClientID"));
operation.addSecurityItem(new SecurityRequirement().addList("Authorization"));
List<Parameter> parameterList = operation.getParameters();
if (parameterList!=null && !parameterList.isEmpty()) {
Collections.rotate(parameterList, 1);
}
return operation;
}
}
Actual Output
Expected Output
Workaround
Adding the paths to be included/excluded in the application properties file solves the error. But something at the code level will be much appreciated.
Attach the required OperationCustomizerobject while building the Api Group.
#Bean
public GroupedOpenApi v1Apis(GlobalHeaderAdder globalHeaderAdder) {
return GroupedOpenApi.builder().group("v1 APIs")
// hide all v2 APIs
.pathsToExclude("/api/v2/**", "/v2/**")
// show all v1 APIs
.pathsToMatch("/api/v1/**", "/v1/**")
.addOperationCustomizer(globalHeaderAdded)
.build();
}
Edit: Answer updated with reference to #Value not providing values from application properties Spring Boot
Alternative to add and load OperationCustomizer in the case you declare yours open api groups by properties springdoc.group-configs[0].group= instead definition by Java code in a Spring Configuration GroupedOpenApi.builder().
#Bean
public Map<String, GroupedOpenApi> configureGroupedsOpenApi(Map<String, GroupedOpenApi> groupedsOpenApi, OperationCustomizer operationCustomizer) {
groupedsOpenApi.forEach((id, groupedOpenApi) -> groupedOpenApi.getOperationCustomizers()
.add(operationCustomizer));
return groupedsOpenApi;
}

Mandatory header for all API in openapi 3.0

I am using OpenAPI 3.0 with Spring-boot 5 and therefore have no configuration YAML. I have a header that contains the client Identification ID(This is not an authentication header). I want to make that a mandatory header param. Added below OpenAPI configuration
#Configuration
public class OpenAPIConfiguration {
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addParameters("myCustomHeader", new Parameter().in("header").schema(new StringSchema()).required(true).description("myCustomHeader").name("myCustomHeader")))
.info(new Info()
.title("My Rest Application")
.version("1.2.26"));
}
}
However, the swagger UI does not show the required param in any API. Can someone help as to what I am doing wrong?
Adding parameter definition to a custom OpenAPI bean will not work because the parameter won't get propagated to the operations definitions. You can achieve your goal using OperationCustomizer:
#Bean
public OperationCustomizer customize() {
return (operation, handlerMethod) -> operation.addParametersItem(
new Parameter()
.in("header")
.required(true)
.description("myCustomHeader")
.name("myCustomHeader"));
}
The OperationCustomizer interface was introduced in the springdoc-openapi 1.2.22. In previous versions you would need to use OpenApiCustomiser:
#Component
public class MyOpenApiCustomizer implements OpenApiCustomiser {
private static final List<Function<PathItem, Operation>> OPERATION_GETTERS = Arrays.asList(
PathItem::getGet, PathItem::getPost, PathItem::getDelete, PathItem::getHead,
PathItem::getOptions, PathItem::getPatch, PathItem::getPut);
private Stream<Operation> getOperations(PathItem pathItem) {
return OPERATION_GETTERS.stream()
.map(getter -> getter.apply(pathItem))
.filter(Objects::nonNull);
}
#Override
public void customise(OpenAPI openApi) {
openApi.getPaths().values().stream()
.flatMap(this::getOperations)
.forEach(this::customize);
}
private void customize(Operation operation) {
operation.addParametersItem(
new Parameter()
.in("header")
.required(true)
.description("myCustomHeader")
.name("myCustomHeader"));
}
}

Resources