SpringBoot custom ConstraintValidator not getting triggered - spring-boot

My SpringBoot app has the following OpenAPI generated code.
In my RestController I have a custom ConstraintValidator #ValidIndexName on the indexName path parameter.
Why does the ConstraintValidator IndexNameValidator code below not get called?
I have the spring-boot-starter-validation dependency in my pom.
#javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2020-03-23T16:08:36.286420Z[Europe/London]")
#Validated
#Api(value = "Document", description = "the Document API")
public interface DocumentApi {
#ApiOperation(value = "", nickname = "createDocument", notes = "", tags={ "Document", })
#ApiResponses(value = {
#ApiResponse(code = 202, message = "Accepted") })
#RequestMapping(value = "/docs/{indexName}/",
consumes = { "application/json" },
method = RequestMethod.POST)
ResponseEntity<Void> _createDocument(#ApiParam(value = "Elasticsearch index name",required=true) #PathVariable("indexName") String indexName,#ApiParam(value = "" ) #Valid #RequestBody Document document);
#ApiOperation(value = "", nickname = "updateDocument", notes = "", tags={ "Document", })
#ApiResponses(value = {
#ApiResponse(code = 202, message = "Accepted") })
#RequestMapping(value = "/docs/{indexName}/",
consumes = { "application/json" },
method = RequestMethod.PUT)
ResponseEntity<Void> _updateDocument(#ApiParam(value = "Elasticsearch index name",required=true) #PathVariable("indexName") String indexName,#ApiParam(value = "" ) #Valid #RequestBody Document document);
}
#RestController
#Validated
public class DocumentApiImpl implements DocumentApi {
#Override
public ResponseEntity<Void> _createDocument(String indexName, Document document) {
return createDocument(indexName, document);
}
public ResponseEntity<Void> createDocument(#ValidIndexName String indexName, Document document) {
System.out.println("indexName = " + indexName);
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
#Override
public ResponseEntity<Void> _updateDocument(String indexName, Document document) {
return createDocument(indexName, document);
}
public ResponseEntity<Void> updateDocument(#ValidIndexName String indexName, Document document) {
System.out.println("indexName = " + indexName);
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
}
#Target( { PARAMETER })
#Retention(RUNTIME)
#Constraint(validatedBy = IndexNameValidator.class)
#Documented
public #interface ValidIndexName {
String message() default "TenantId cannot be used as an index name.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
#Component
public class IndexNameValidator implements ConstraintValidator<ValidIndexName, String> {
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
System.out.println("Validating value = " + value);
if (StringUtils.isEmpty(value)) {
return false;
}
return true;
}
}

Related

WebFlux API-Layer Test returns 404

I'm trying to get started with Spring WebFlux with Spring Boot 3.0
I'm Building a Person API with an open api generator.
The Application runs and gives the expected results when it is tested manually.
But I'm not able to get the API layer unit tested.
This is my Test Class
#WebFluxTest(controllers = {PersonApiController.class})
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = {PersonMapperImpl.class, H2PersonRepository.class, PersonRepository.class})
#DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class PersonRouterTest {
#MockBean
private PersonService personService;
#Autowired
private WebTestClient client;
#ParameterizedTest
#CsvSource({"1234, Max Mustermann", "5678, Erika Musterfrau"})
void retrieve_a_name(String id, String name) {
when(personService.getPersonDataByID(1234)).thenReturn(Mono.just(new PersonData(1234, "Max Mustermann")));
when(personService.getPersonDataByID(5678)).thenReturn(Mono.just(new PersonData(5678, "Erika Musterfrau")));
client.get()
.uri(uriBuilder -> uriBuilder
.path("/persons/{id}")
.build(id))
.accept(MediaType.ALL)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.name").isEqualTo(name);
}
This is my Controller Class
#Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-12-
09T09:14:36.692713900+01:00[Europe/Vienna]")
#Controller
#RequestMapping("${openapi.openAPIDefinition.base-path:}")
public class PersonApiController implements PersonApi {
private final PersonApiDelegate delegate;
public PersonApiController(#Autowired(required = false) PersonApiDelegate delegate) {
this.delegate = Optional.ofNullable(delegate).orElse(new PersonApiDelegate() {});
}
#Override
public PersonApiDelegate getDelegate() {
return delegate;
}
}
The API interface:
#Tag(
name = "Person",
description = "the Person API"
)
public interface PersonApi {
default PersonApiDelegate getDelegate() {
return new PersonApiDelegate() {
};
}
#Operation(
operationId = "findPersonById",
summary = "Find Person by ID",
tags = {"Person"},
responses = {#ApiResponse(
responseCode = "200",
description = "successful operation",
content = {#Content(
mediaType = "application/json",
schema = #Schema(
implementation = PersonData.class
)
)}
)}
)
#RequestMapping(
method = {RequestMethod.GET},
value = {"/persons/{id}"},
produces = {"application/json"}
)
default Mono<ResponseEntity<PersonData>> findPersonById(#Parameter(name = "id",description = "Person ID",required = true) #PathVariable("id") Integer id, #Parameter(hidden = true) final ServerWebExchange exchange) {
return this.getDelegate().findPersonById(id, exchange);
}
#Operation(
operationId = "savePerson",
summary = "Creates a new Person",
tags = {"Person"},
responses = {#ApiResponse(
responseCode = "200",
description = "successful operatoin",
content = {#Content(
mediaType = "application/json",
schema = #Schema(
implementation = PersonData.class
)
)}
)}
)
#RequestMapping(
method = {RequestMethod.POST},
value = {"/persons"},
produces = {"application/json"},
consumes = {"application/json"}
)
default Mono<ResponseEntity<PersonData>> savePerson(#Parameter(name = "PersonData",description = "") #RequestBody(required = false) Mono<PersonData> personData, #Parameter(hidden = true) final ServerWebExchange exchange) {
return this.getDelegate().savePerson(personData, exchange);
}
}
and finally my delegate impl:
#Service
public class PersonDelegateImpl implements PersonApiDelegate {
public static final Mono<ResponseEntity<?>> RESPONSE_ENTITY_MONO = Mono.just(ResponseEntity.notFound().build());
private final PersonService service;
private final PersonMapper mapper;
public PersonDelegateImpl(PersonService service, PersonMapper mapper) {
this.service = service;
this.mapper = mapper;
}
public static <T> Mono<ResponseEntity<T>> toResponseEntity(Mono<T> mono) {
return mono.flatMap(t -> Mono.just(ResponseEntity.ok(t)))
.onErrorResume(t -> Mono.just(ResponseEntity.internalServerError().build()));
}
#Override
public Mono<ResponseEntity<PersonData>> findPersonById(Integer id, ServerWebExchange exchange) {
Mono<com.ebcont.talenttoolbackend.person.PersonData> personDataByID = service.getPersonDataByID(id);
return toResponseEntity(personDataByID.map(mapper::map));
}
#Override
public Mono<ResponseEntity<PersonData>> savePerson(Mono<PersonData> personData, ServerWebExchange exchange) {
return PersonApiDelegate.super.savePerson(personData, exchange);
If I run the test class I always get:
< 404 NOT_FOUND Not Found
< Content-Type: [application/json]
< Content-Length: [139]
{"timestamp":"2022-12-09T08:45:41.278+00:00","path":"/persons/1234","status":404,"error":"Not Found","message":null,"requestId":"4805b8b8"}
I have tried to change the Context Configuration but I did not get it to work.
I found the Problem, changing the Test Config to :
#WebFluxTest
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = {PersonMapperImpl.class, H2PersonRepository.class, PersonRepository.class, PersonApiController.class, PersonDelegateImpl.class})
#DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
Solved my Problem.
The Controller bean was not recognized. I had to add PersonApiCrontroller and PersonDelegateImpl to the Context Config. i then removed the PersonApiController from the #WebFluxTest annotation.

OpenApi Swagger not showing Exception as a response

I'm using OpenApi Swagger UI (v. 4.14) with SpringBoot. I'm getting all the info I need with the Swagger, except for the exception. Here's my code.
Garage class:
#Schema(description = "Details about the Car")
#Document("Garage")
public class Garage implements Serializable {
#Schema(description = "An ID of the car in the database", accessMode = Schema.AccessMode.READ_ONLY)
#Id
private String id;
#Schema(description = "The name of the car")
#Field("model")
protected String carModel;
#Schema(description = "Car's engine power output")
protected Integer hp;
#Schema(description = "Production year of the car")
#Field("Year")
protected Integer year;
#Schema(description = "The name of car's designer")
protected String designer;
// controllers, getters, setters, toString
Controller:
// some other code
#Operation(summary = "Deletes a car by its id")
#ApiResponses(value = {
#ApiResponse(responseCode = "200",
description = "A car is deleted from the Garage",
content = {#Content(
schema = #Schema(implementation = Garage.class),
mediaType = "application/json")}),
#ApiResponse(responseCode = "404",
description = "A car with this id is not in our garage",
content = #Content(
schema = #Schema(implementation = RestExceptionHandler.class),
mediaType = "application/json"))})
#DeleteMapping(path = "/deleteCar/{carId}")
public void deleteCarFromGarage(#PathVariable("carId") String id) {
garageService.deleteFromGarage(id);
}
// some other code
Exception handler:
#Schema(description = "Exception handling")
#RestControllerAdvice
public class RestExceptionHandler {
#Schema(description = "The ID is not valid")
#ExceptionHandler(value = {IllegalArgumentException.class})
public ResponseEntity<Object> resourceNotFoundException(IllegalArgumentException exception) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
}
}
I'm getting 200 responses every time, no matter do I delete a correct id, or an incorrect one.
EDIT: here's my deleteFromGarage method
public void deleteFromGarage(String id) {
garageRepository.deleteById(id);
}
I've edited my deleteFromGarage method, and that solved the issue
deleteFromGarage before:
public void deleteFromGarage(String id) {
garageRepository.deleteById(id);
}
deleteFromGarage now:
public void deleteFromGarage(String id) {
if (garageRepository.findById(id).isEmpty()) {
throw new IllegalArgumentException("The ID is not valid");
} else {
garageRepository.deleteById(id);
}
}

The resource identified by this request is able to generate responses only with features incompatible with the "accept" directive

I want to make a rest webservice call :
#RestController
#RequestMapping("stock")
public class StockController {
#Autowired
private StockService stockService;
#GetMapping(value = "/TOM", produces = "application/json")
public JsonModel getByLocAndItm(#RequestParam(required=false) String LOC, #RequestParam(required=false) String ITM) {
JsonModel jsonModel = new JsonModel();
List<com.java.oxalys.beans.Stock> stock = stockService.getByLocAndItm(LOC.split("|"), null);
jsonModel.setDatas(stock);
return jsonModel;
}
}
service :
#Service
public class StockServiceImpl implements StockService {
#Autowired
private StockDao stockDao;
#Override
public List<com.java.oxalys.beans.Stock> getByLocAndItm(String[] locs, String[] itms) {
return stockDao.getByLocAndItm(locs, itms);
}
}
DAO :
#Repository
public class StockDaoImpl implements StockDao {
#Override
public List<Stock> getByLocAndItm(String[] locs, String[] itms) {
List<Stock> ret = new ArrayList<Stock>();
String where = "";
String where_locs = "", sep_locs = "";
for(String loc : locs) {
where_locs += sep_locs + " s.LOC_0 = '" + loc + "'";
sep_locs = " or ";
}
where_locs = "(" + where_locs + ")";
where = where_locs;
Stock tmp = new Stock();
tmp.setLoc("eto");
tmp.setItmoxa(where);
ret.add(tmp);
return ret;
}
}
At runtime with postman : localhost:8080/Oxalys_WS/stock/TOM?LOC=BOQSCM171L then I get error The resource identified by this request is able to generate responses only with features incompatible with the "accept" directive present in the request header
So what is wrong ?

How to properly implement adding a new record into Elasticsearch using Spring Boot?

Getting started with Spring Boot / Spring Data / Elasticsearch application.
ES -> 6.1
Have a simple repository:
public interface BusinessMetadataRepository extends ElasticsearchRepository<BusinessMetadata, Long> {
List<BusinessMetadata> findByName(String name);
List<BusinessMetadata> findById(Long id);
}
And a Business Object:
import org.springframework.data.elasticsearch.annotations.Document;
#Document(indexName = "bsn", type = "mtd", shards = 1)
public class BusinessMetadata {
private Long id;
private String name;
public Long getId() {return id;}
public void setId(Long id) {this.id = id;}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public BusinessMetadata(Long id, String name) {
this.id = id;
this.name = name;
}
public BusinessMetadata() {
}
}
Elastic Configuration:
#Configuration
#EnableElasticsearchRepositories(basePackages = "com.discover.harmony.elastic.repository")
public class ElasticConfiguration {
#Bean
public NodeBuilder nodeBuilder() {
return new NodeBuilder();
}
#Bean
public ElasticsearchOperations elasticsearchTemplate() throws IOException {
File tmpDir = File.createTempFile("elastic", Long.toString(System.nanoTime()));
System.out.println("Temp directory: " + tmpDir.getAbsolutePath());
Settings.Builder elasticsearchSettings =
Settings.settingsBuilder()
.put("http.enabled", "true") // 1
.put("index.number_of_shards", "1")
.put("path.data", new File(tmpDir, "data").getAbsolutePath()) // 2
.put("path.logs", new File(tmpDir, "logs").getAbsolutePath()) // 2
.put("path.work", new File(tmpDir, "work").getAbsolutePath()) // 2
.put("path.home", tmpDir); // 3
return new ElasticsearchTemplate(nodeBuilder()
.local(true)
.settings(elasticsearchSettings.build())
.node()
.client());
}
}
My Rest Controller for doing search works fine:
#RestController
#RequestMapping("/rest/search")
public class SearchResource {
#Autowired
BusinessMetadataRepository businessMetadataRepository;
#GetMapping(value = "/name/{text}")
public List<BusinessMetadata> searchName(#PathVariable final String text) {
return businessMetadataRepository.findByName(text);
}
#GetMapping(value = "/all")
public List<BusinessMetadata> searchAll() {
List<BusinessMetadata> businessMetadataList = new ArrayList<>();
Iterable<BusinessMetadata> businessMetadata = businessMetadataRepository.findAll();
businessMetadata.forEach(businessMetadataList::add);
return businessMetadataList;
}
}
My Rest Controller for doing save:
#RestController
#RequestMapping("/rest/save")
public class SaveResource {
#Autowired
BusinessMetadataRepository businessMetadataRepository;
#GetMapping(value = "/name/{text}")
public void Save(String text) {
businessMetadataRepository.save(new BusinessMetadata((long)99, text));
}
}
When I test the save using Postman, I get this error:
{
"timestamp": 1514325625996,
"status": 405,
"error": "Method Not Allowed",
"exception": "org.springframework.web.HttpRequestMethodNotSupportedException",
"message": "Request method 'POST' not supported",
"path": "/rest/save/name/new-1"
}
What changes do I need to make in order to properly configure this project to support inserting new documents?
Based on the comment from AntJavaDev, I have modified my controller in the following way:
#RestController
#RequestMapping("/rest/save")
public class SaveResource {
#Autowired
BusinessMetadataRepository businessMetadataRepository;
#PostMapping("/name/{text}")
public void Save(#PathVariable String text) {
BusinessMetadata mtd = businessMetadataRepository.save(new BusinessMetadata(text));
}
}
The 2 key changes are: replace #GetMapping with #PostMapping, and include #PathVariable as a parameter qualifier.
Now it works as expected

#PathVariable Validation in Spring 4

How can i validate my path variable in spring. I want to validate id field, since its only single field i do not want to move to a Pojo
#RestController
public class MyController {
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public ResponseEntity method_name(#PathVariable String id) {
/// Some code
}
}
I tried doing adding validation to the path variable but its still not working
#RestController
#Validated
public class MyController {
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public ResponseEntity method_name(
#Valid
#Nonnull
#Size(max = 2, min = 1, message = "name should have between 1 and 10 characters")
#PathVariable String id) {
/// Some code
}
}
You need to create a bean in your Spring configuration:
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
You should leave the #Validated annotation on your controller.
And you need an Exceptionhandler in your MyController class to handle theConstraintViolationException :
#ExceptionHandler(value = { ConstraintViolationException.class })
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public String handleResourceNotFoundException(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
StringBuilder strBuilder = new StringBuilder();
for (ConstraintViolation<?> violation : violations ) {
strBuilder.append(violation.getMessage() + "\n");
}
return strBuilder.toString();
}
After those changes you should see your message when the validation hits.
P.S.: I just tried it with your #Size validation.
To archive this goal I have apply this workaround for getting a response message equals to a real Validator:
#GetMapping("/check/email/{email:" + Constants.LOGIN_REGEX + "}")
#Timed
public ResponseEntity isValidEmail(#Email #PathVariable(value = "email") String email) {
return userService.getUserByEmail(email).map(user -> {
Problem problem = Problem.builder()
.withType(ErrorConstants.CONSTRAINT_VIOLATION_TYPE)
.withTitle("Method argument not valid")
.withStatus(Status.BAD_REQUEST)
.with("message", ErrorConstants.ERR_VALIDATION)
.with("fieldErrors", Arrays.asList(new FieldErrorVM("", "isValidEmail.email", "not unique")))
.build();
return new ResponseEntity(problem, HttpStatus.BAD_REQUEST);
}).orElse(
new ResponseEntity(new UtilsValidatorResponse(EMAIL_VALIDA), HttpStatus.OK)
);
}

Resources