Swagger-UI (v3) with org.springdoc does show and interpret enums incorrectly - maven

I have a problem with my Swagger-UI: It does show Enums as intended.
Instead of a simple representation like CATEGORY1 it shows the full class like CATEGORY1(name=Cat 1) and also uses it in the requests like http://localhost:8080/file/byCategory?category=Category.CATEGORY1%28name%3DCat%201%29
I figured that I can send Requests (e.g. with Postman) with the correct Enum-descriptions and the server would respond, so the api itself works.
The dependencies important for this issue:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-data-rest</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webmvc-core</artifactId>
<version>1.5.0</version>
</dependency>
I also use Spring Boot (2.4.0) and some other dependencies which shouldn't be part of the issue.
My Controller:
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
#Slf4j
#RestController
#RequestMapping("/file")
#CrossOrigin(origins = "http://localhost:4200")
#Tag(name = "Files")
public class GridFSController {
private final GridFsService service;
#Autowired
public GridFSController(GridFsService service) {
this.service = service;
}
#PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
#Operation(summary = "Upload a document")
#ResponseBody
public ResponseEntity<String> upload(#RequestParam("file") MultipartFile file, #RequestParam("category") Category category) {
try {
if (ObjectUtils.isEmpty(file)) {
return ResponseEntity.ok().body("The uploaded file cannot be empty");
} else {
return ResponseEntity.ok().body(service.saveFile(file, category.getName()));
}
} catch (Exception e) {
log.error("[Upload Failed]", e);
return ResponseEntity.badRequest().body(e.getMessage());
}
}
#GetMapping(value = "/download", produces = MediaType.MULTIPART_FORM_DATA_VALUE)
#Operation(summary = "Download a document by its name")
public ResponseEntity<String> download(HttpServletResponse response, #RequestParam("fileName") String fileName) {
try {
service.downLoad(response, fileName);
return ResponseEntity.ok().body("SUCCESS");
} catch (Exception e) {
log.error("[Download Failed]", e.getMessage());
return ResponseEntity.badRequest().body(e.getMessage());
}
}
#GetMapping("/byCategory")
#Operation(summary = "Get information about all documents in a category")
public ResponseEntity<List<FileMetaDomain>> getByCategory(#RequestParam("category") Category category) {
List<FileMetaDomain> allFilesForCategory = service.getAllFilesForCategory(category.getName());
return ResponseEntity.ok(allFilesForCategory);
}
}
My Enum:
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
#ToString
#AllArgsConstructor(access = AccessLevel.PRIVATE)
#Getter
public enum Category {
CATEGORY1("Cat 1"),
CATEGORY2("Cat 2"),
CATEGORY3("Cat 3");
private final String name;
}
My Swagger-UI however looks like this:
As mentioned, it works with Postman (even though at this point the response is just an empty array):

The point is the toString() method overriding. Try to remove the #ToString annotation.
Your Category should be:
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
#AllArgsConstructor(access = AccessLevel.PRIVATE)
#Getter
public enum Category {
CATEGORY1("Cat 1"),
CATEGORY2("Cat 2"),
CATEGORY3("Cat 3");
private final String name;
}
And then the swagger-ui will show the enum values dropdown list:
Update:
If you need to keep the toString() method, you have to enrich the category parameter description on your controller. Your GET /byCategory endpoint code should look like:
#GetMapping("/byCategory")
#Operation(summary = "Get information about all documents in a category")
public ResponseEntity<List<FileMetaDomain>> getByCategory(#Parameter(name = "category", in = ParameterIn.QUERY, schema = #Schema(type = "string", allowableValues = {"CATEGORY1", "CATEGORY2", "CATEGORY3"})) Category category) {
List<FileMetaDomain> allFilesForCategory = service.getAllFilesForCategory(category.getName());
return ResponseEntity.ok(allFilesForCategory);
}

Related

Spring Boot controllers are returning a 404

Spring Boot controllers are returning a 404 on all endpoints
trying to get basic controller returning data
Package structure is setup correctly all packages are sub packages of the main package
annotations look fine been using spring casually for a bit but im clueless im expecting at least hello world im assuming its some spring garbage goin on with it not finding the bean idk no luck finding anything out of conventional configuration. plz help thanks
package com.bookieburglar.api.services;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
//#EnableJpaRepositories(basePackages = "com.bookieburlgar.api.services")
#ComponentScan(basePackages = "com.bookieburglar.api.services")
#SpringBootApplication
public class BookieBurglarApplication {
public static void main(String[] args) {
SpringApplication.run(BookieBurglarApplication.class, args);
}
}
Odds.java
package com.bookieburglar.api.services.models;
import java.util.List;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
#Entity
#JsonIgnoreProperties(ignoreUnknown = true)
public class Odds {
#Id
#JsonProperty("id")
private String id;
#JsonProperty("sport_key")
private String sport_key;
#JsonProperty("sport_title")
private String sport_title;
#JsonProperty("commence_time")
private String commence_time;
#JsonProperty("home_team")
private String home_team;
#JsonProperty("away_team")
private String away_team;
#JsonProperty("bookmakers")
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "odds_id")
private List<Bookmaker> bookmakers;
public Odds(String id, String sportKey, String sportTitle, String commenceTime,
String homeTeam, String awayTeam, List<Bookmaker> bookmakers) {
this.id = id;
this.sport_key = sportKey;
this.sport_title = sportTitle;
this.commence_time = commenceTime;
this.home_team = homeTeam;
this.away_team = awayTeam;
this.bookmakers = bookmakers;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSportKey() {
return sport_key;
}
public void setSportKey(String sportKey) {
this.sport_key = sportKey;
}
public String getSportTitle() {
return sport_title;
}
public void setSportTitle(String sportTitle) {
this.sport_title = sportTitle;
}
public String getCommenceTime() {
return commence_time;
}
public void setCommenceTime(String commenceTime) {
this.commence_time = commenceTime;
}
public String getHomeTeam() {
return home_team;
}
public void setHomeTeam(String homeTeam) {
this.home_team = homeTeam;
}
public String getAwayTeam() {
return away_team;
}
public void setAwayTeam(String awayTeam) {
this.away_team = awayTeam;
}
public List<Bookmaker> getBookmakers() {
return bookmakers;
}
public void setBookmakers(List<Bookmaker> bookmakers) {
this.bookmakers = bookmakers;
}
}
OddsController
package com.bookieburglar.api.services.controllers;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.bookieburglar.api.services.services.OddsAPIService;
import com.bookieburglar.api.services.services.OddsService;
import com.bookieburglar.api.services.models.Odds;
import com.bookieburglar.api.services.repositories.OddsRepository;
#RestController
#RequestMapping("/Odds")
public class OddsController {
#Autowired
private OddsService oddsService;
#Autowired
private OddsAPIService oddsAPIService;
#GetMapping("/")
public String getOdds() {
return "WORLD";
//return (List<Odds>) OddsRepository.findAll();
}
// #GetMapping("/{id}")
// public Odds getOdds(#PathVariable String id) {
// return OddsRepository.findById(id).orElse(null);
// }
#PostMapping("/create")
public Odds createOdds(#RequestBody Odds Odds) {
System.out.println("frthoo");
return oddsService.saveOdds(Odds);
}
#GetMapping("/refresh")
#ResponseBody
public String refreshOdds() {
System.out.println("ttgb5");
//return oddsAPIService.refreshOdds();
return "yoo";
}
// #PutMapping("/{id}")
// public Odds updateOdds(#PathVariable String id, #RequestBody Odds Odds) {
// Odds.setId(id);
// return OddsRepository.save(Odds);
// }
//
// #DeleteMapping("/{id}")
// public void deleteOdds(#PathVariable String id) {
// OddsRepository.deleteById(id);
// }
}
OddsServices
package com.bookieburglar.api.services.services;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bookieburglar.api.services.models.Odds;
import com.bookieburglar.api.services.repositories.OddsRepository;
#Service
public class OddsService {
#Autowired
private OddsRepository oddsRepository;
public List<Odds> getAllOdds() {
return (List<Odds>) oddsRepository.findAll();
}
public Optional<Odds> findOddsById(String id) {
return oddsRepository.findById(id);
}
// public List<Odds> findOddsBySportTitle(String sportTitle) {
// return oddsRepository.findBySportTitle(sportTitle);
// }
//
// public List<Odds> findOddsByHomeTeam(String homeTeam) {
// return oddsRepository.findByHomeTeam(homeTeam);
// }
//
// public List<Odds> findOddsByAwayTeam(String awayTeam) {
// return oddsRepository.findByAwayTeam(awayTeam);
// }
public Odds saveOdds(Odds odds) {
return oddsRepository.save(odds);
}
public void deleteOdds(String id) {
oddsRepository.deleteById(id);
}
}
OddsRepository
package com.bookieburglar.api.services.repositories;
import java.util.List;
import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.bookieburglar.api.services.models.Odds;
#Repository
public interface OddsRepository extends JpaRepository<Odds, String> {
List<Odds> findAll();
// additional methods can be defined here, for example, to search for odds by sport key or teams
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bookieburglar.api</groupId>
<artifactId>services</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Bookie Burglar</name>
<description>API for BookieBurglar</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.persistence/javax.persistence-api -->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
In #ComponentScan annotation. Package name is incorrect.
#ComponentScan(basePackages = "com.bookieburlgar.api.services")
It should be
#ComponentScan(basePackages = "com.bookieburglar.api.services")

Request method 'GET' not supported There was an unexpected error (type=Method Not Allowed, status=405)

My scenario is as follows:
There are several entities, and I need to send custom designed email for each entity "by Id". For example, I am trying to send the email by this URL for "babs" entity:
http://localhost:8081/api/v1/test/babss/sendmail/b1d0c331-35ac-430d-87ca-1718b06351c3
Sample entity for BaBS:
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import javax.persistence.*;
import java.util.UUID;
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#Builder
#Entity
#Table(name = "babs")
public class BaBs {
#Id
#Column(name = "id")
private UUID id;
#Column(name = "accountCode")
private String accountCode;
#Column(name = "accountName")
private String accountName;
#Column(name = "eMail")
private String eMail;
#ManyToOne
#JsonBackReference
#JoinColumn(name = "accountid")
private Account account;
}
email Sending Service
import gokhan.mutabakatcore.models.BaBs;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.mail.MailException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
#Component
public class SentBaBsEmail {
#Autowired
private JavaMailSender sender;
public HttpStatus mailBaBs(BaBs baBs) throws MailException {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setTo(baBs.getEMail());
mail.setSubject("Trial Mail for Sending BaBs");
mail.setText("Normally Details from Object ");
sender.send(mail);
System.out.println("eMail sent");
return HttpStatus.GONE;
}
}
Corresponding part of BaBS Controller
import gokhan.mutabakatcore.models.BaBs;
import gokhan.mutabakatcore.services.BaBsService;
import gokhan.mutabakatcore.utils.SentBaBsEmail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
#RestController
#RequestMapping("/api/v1/test/babss")
public class BaBsController {
private final BaBsService baBsService;
public BaBsController(BaBsService baBsService) {
this.baBsService = baBsService;
}
#Autowired
SentBaBsEmail baBsEmail;
#RequestMapping(value = "/sendmail/{Id}", method = { RequestMethod.GET, RequestMethod.POST })
public ResponseEntity<?> eMailBaBsByCustomerId(#PathVariable("Id") UUID uuid){
try{
if (!baBsService.findById(uuid).equals(Optional.empty())){
// Converts Optional Object to Normal Object
BaBs baBs = (BaBs) toList(baBsService.findById(uuid));
baBsEmail.mailBaBs(baBs);
return new ResponseEntity(baBs ,HttpStatus.CREATED);
}
return new ResponseEntity<>("Unrecorded BaBS!", HttpStatus.BAD_REQUEST);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}
Application.properties Mailing part
#SMTP Email Properties
spring.mail.host=mail.xyz.com
spring.mail.port=587
spring.mail.username=info#xyz.com
spring.mail.password=password
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.starttls.enable=true
spring.mail.properties.mail.starttls.required=true
Relavant POM.xml dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
The error I got:
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sat Aug 06 08:39:31 EET 2022
There was an unexpected error (type=Method Not Allowed, status=405).
Request method 'GET' not supported
org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported
I'll appreciate if you can show me the issue...

Validation of #RequestParam in Spring REST controller doesn't work

I want to validete my GET method's params. I added tis dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
I wrote this controller:
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.Min;
#Validated
#RestController
public class TestController {
#GetMapping(value = "/test")
public String getTest(#Min(value = 3, message = "min 3")
#RequestParam("hz") String hz,
#RequestParam("hz1") String hz1) {
return "test";
}
}
I added configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
#Configuration
public class Config {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
the request works fine! Why?
http://localhost:8088/test?hz=12&hz1=123

#Transaction not working on multi datasource Spring Boot + MyBatis application

I'm trying to configure Spring Boot + MyBatis application which should work with several datasources. I tried to do it similar to sample here.
Querying and updating data is working, but when I wrote the unit test, I found that #Transactional is not working. Transactions must work between all databases. It means that, if one method with #Transactional make updates on both databases then everything should rollback in case of exception.
It is a sample application for testing purposes and co-workers. After successful configuring the new applications will be configured and developed in similar manner.
Maven:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aze.mybatis</groupId>
<artifactId>sample-one</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Below are the configuration classes:
package com.aze.mybatis.sampleone;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
#SpringBootApplication
#EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
Config to database BSCS (Oracle)
package com.aze.mybatis.sampleone.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
#Configuration
#MapperScan(basePackages = "com.aze.mybatis.sampleone.dao", annotationClass = BscsDataSource.class, sqlSessionFactoryRef = BscsDatabaseConfig.SQL_SESSION_FACTORY_NAME)
#EnableTransactionManagement
public class BscsDatabaseConfig {
static final String SQL_SESSION_FACTORY_NAME = "sessionFactoryBscs";
private static final String TX_MANAGER = "txManagerBscs";
#Bean(name = "dataSourceBscs")
#ConfigurationProperties(prefix = "bscs.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = TX_MANAGER)
public PlatformTransactionManager txManagerBscs() {
return new DataSourceTransactionManager(dataSource());
}
#Bean(name = BscsDatabaseConfig.SQL_SESSION_FACTORY_NAME)
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean.getObject();
}
}
Config to database ONSUBS (Oracle)
package com.aze.mybatis.sampleone.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
#Configuration
#MapperScan(basePackages = "com.aze.mybatis.sampleone.dao", annotationClass = OnsubsDataSource.class, sqlSessionFactoryRef = OnsubsDatabaseConfig.SQL_SESSION_FACTORY_NAME)
#EnableTransactionManagement
public class OnsubsDatabaseConfig {
static final String SQL_SESSION_FACTORY_NAME = "sessionFactoryOnsubs";
private static final String TX_MANAGER = "txManagerOnsubs";
#Bean(name = "dataSourceOnsubs")
#Primary
#ConfigurationProperties(prefix = "onsubs.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = TX_MANAGER)
#Primary
public PlatformTransactionManager txManagerOnsubs() {
return new DataSourceTransactionManager(dataSource());
}
#Bean(name = OnsubsDatabaseConfig.SQL_SESSION_FACTORY_NAME)
#Primary
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean.getObject();
}
}
Annotation for BSCS:
package com.aze.mybatis.sampleone.config;
public #interface BscsDataSource {
}
and ONSUBS:
package com.aze.mybatis.sampleone.config;
public #interface OnsubsDataSource {
}
Mapper interface that should work with ONSUBS:
package com.aze.mybatis.sampleone.dao;
import com.aze.mybatis.sampleone.config.OnsubsDataSource;
import com.aze.mybatis.sampleone.domain.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
#Mapper
#OnsubsDataSource
public interface PaymentDao {
Payment getPaymentById(#Param("paymentId") Integer paymentId);
}
and BSCS:
package com.aze.mybatis.sampleone.dao;
import com.aze.mybatis.sampleone.config.BscsDataSource;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
#Mapper
#BscsDataSource
public interface PostpaidCustomerDao {
PostpaidBalance getPostpaidBalance(#Param("customerId") Integer customerId);
// BigDecimal amount may be used as second parameter, but I want to show, how to work with two parameters where second is object
void updateDepositAmount(#Param("customerId") Integer customerId, #Param("balance") PostpaidBalance postpaidBalance);
void updateAzFdlLastModUser(#Param("customerId") Integer customerId, #Param("username") String username);
}
Below is a code with #Transactional
package com.aze.mybatis.sampleone.service;
import com.aze.mybatis.sampleone.dao.PaymentDao;
import com.aze.mybatis.sampleone.dao.PostpaidCustomerDao;
import com.aze.mybatis.sampleone.domain.Payment;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import com.aze.mybatis.sampleone.exception.DataNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
#Service
public class PaymentServiceImpl implements PaymentService {
private static final String MIN_DEPOSIT_AMOUNT = "150";
#Autowired
private PaymentDao paymentDao;
#Autowired
private PostpaidCustomerDao postpaidCustomerDao;
#Override
public PostpaidBalance getPostpaidBalance(Integer customerId) {
PostpaidBalance balance = postpaidCustomerDao.getPostpaidBalance(customerId);
if (balance == null) {
throw new DataNotFoundException(String.format("Can't find any balance information for customer with customer_id = %d", customerId));
}
return balance;
}
// Note. By default rolling back on RuntimeException and Error but not on checked exceptions
// If you want to rollback on check exception too then add "rollbackFor = Exception.class"
#Transactional(rollbackFor = Exception.class)
#Override
public void updateDepositAmount(Integer customerId, PostpaidBalance postpaidBalance, String username) {
postpaidCustomerDao.updateDepositAmount(customerId, postpaidBalance);
// In case of #Transactional annotation, you can use method from the same class if it doesn't change data on database
PostpaidBalance balance = getPostpaidBalance(customerId);
// This logic is for showing that how the #Transactional annotation works.
// Because of the exception, the previous transaction will rollback
if (balance.getDeposit().compareTo(new BigDecimal(MIN_DEPOSIT_AMOUNT)) == -1) {
throw new IllegalArgumentException("The customer can not have deposit less than " + MIN_DEPOSIT_AMOUNT);
}
// In case of #Transactional annotation, you must not (!!!) use method from the same (!) class if it changes data on database
// That is why, postpaidCustomerDao.updateAzFdlLastModUser() used here instead of this.updateAzFdlLastModUser()
postpaidCustomerDao.updateAzFdlLastModUser(customerId, username);
// If there is no exception, the transaction will commit
}
}
Below is the unit test code:
package com.aze.mybatis.sampleone.service;
import com.aze.mybatis.sampleone.Application;
import com.aze.mybatis.sampleone.config.BscsDatabaseConfig;
import com.aze.mybatis.sampleone.config.OnsubsDatabaseConfig;
import com.aze.mybatis.sampleone.domain.PostpaidBalance;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {Application.class, OnsubsDatabaseConfig.class, BscsDatabaseConfig.class})
#TestPropertySource(locations= "classpath:application.properties")
public class PaymentServiceImplTest extends Assert {
// My goal is not to write a full and right unit tests, but just show you examples of working with MyBatis
#Autowired
private PaymentService paymentService;
#Before
public void setUp() throws Exception {
assert paymentService != null;
}
#Test
public void updateDepositAmount() throws Exception {
final int customerId = 4301887; // not recommended way. Just for sample
final String username = "ITCSC";
boolean exceptionRaised = false;
PostpaidBalance balance = paymentService.getPostpaidBalance(customerId);
assertTrue("Find customer with deposit = 0", balance.getDeposit().compareTo(BigDecimal.ZERO) == 0);
balance.setDeposit(BigDecimal.TEN);
try {
paymentService.updateDepositAmount(customerId, balance, username);
} catch (Exception e) {
exceptionRaised = true;
}
assertTrue(exceptionRaised);
balance = paymentService.getPostpaidBalance(customerId);
// We check that transaction was rollback and amount was not changed
assertTrue(balance.getDeposit().compareTo(BigDecimal.ZERO) == 0);
final BigDecimal minDepositAmount = new BigDecimal("150");
balance.setDeposit(minDepositAmount);
paymentService.updateDepositAmount(customerId, balance, username);
balance = paymentService.getPostpaidBalance(customerId);
assertTrue(balance.getDeposit().compareTo(minDepositAmount) != -1);
}
}
Unit test fails on assertTrue(balance.getDeposit().compareTo(BigDecimal.ZERO) == 0);. The I check the database and see that first update postpaidCustomerDao.updateDepositAmount(customerId, postpaidBalance); was not rollback despite the #Transactional annotation.
Please help to solve problem.
If you have multiple TransactionManagers, you'll need to reference the one you want to use for #Transactional using Bean names or Qualifiers.
In your Java Config:
#Bean("myTM")
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(myDatasource());
}
In Services:
#Transactional("myTM")
public void insertWithException(JdbcTemplate jdbcTemplate) {
}
I debug'd my app to see how Spring chooses the TransactionManager. This is done in org.springframework.transaction.interceptor.TransactionAspectSupport#determineTransactionManager
/**
* Determine the specific transaction manager to use for the given transaction.
*/
protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null || this.beanFactory == null) {
return getTransactionManager();
}
String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(qualifier);
}
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
return determineQualifiedTransactionManager(this.transactionManagerBeanName);
}
else {
PlatformTransactionManager defaultTransactionManager = getTransactionManager();
if (defaultTransactionManager == null) {
defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
if (defaultTransactionManager == null) {
defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
this.transactionManagerCache.putIfAbsent(
DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
}
return defaultTransactionManager;
}
}
So it's just getting the default primary PlatformTransactionManager bean.

Spring Data ElasticSearch NullPointerException

I have just started with Spring Data ElasticSearch. I have implemented my own repository, but I get a null pointer exception if I try to save an entity. I have got the following code, this is only some test code.
package org.test.elasticsearch.models;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
#Document(indexName = "test", type = "book", shards = 1, replicas = 0)
public class Book {
#Id
private String id;
private String title;
private String author;
public Book(final String id, final String title, final String author) {
this.id = id;
this.title = title;
this.author = author;
}
public String getId() {
return this.id;
}
public void setId(final String id) {
this.id = id;
}
public String getTitle() {
return this.title;
}
public void setTitle(final String title) {
this.title = title;
}
public String getAuthor() {
return this.author;
}
public void setAuthor(final String author) {
this.author = author;
}
}
package org.test.elasticsearch.configs;
import org.elasticsearch.node.NodeBuilder;
import org.test.elasticsearch.repositories.implementations.BookRepositoryImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
#Configuration
#EnableElasticsearchRepositories("org.test.elasticsearch.repositories")
public class ElasticsearchConfiguration {
#Bean
public ElasticsearchOperations elasticsearchTemplate() {
final NodeBuilder nodeBuilder = new NodeBuilder();
return new ElasticsearchTemplate(nodeBuilder.local(true).clusterName("elasticsearch").node().client());
}
#Bean
public BookRepositoryImpl bookRepositoryImplementation() {
return new BookRepositoryImpl();
}
}
package org.test.elasticsearch.repositories;
import org.test.elasticsearch.models.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchCrudRepository;
public interface BookRepository extends ElasticsearchCrudRepository<Book, String>, BookRepositoryCustom {
// query methods
}
package org.test.webapp;
import org.test.elasticsearch.models.Book;
import org.test.elasticsearch.models.Book.BookBuilder;
import org.test.elasticsearch.repositories.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Test {
#Autowired
static BookRepository BookRepository;
public static void main(final String[] args) {
SpringApplication.run(test.class, args);
final Book testBook = new Book("12345", "TestTitle", "TestAuthor");
BookRepository.save(testBook);
}
}
That's my code. And I get the following message after running my spring boot application.
Exception in thread "main" java.lang.NullPointerException
at org.test.webapp.test.main(Test.java:24)
Does anyone have an idea? And another question: When should I use ElasticsearchTemplate with IndexQuery over a custom repository to save my entities?
Your problem is, actually, with Spring Boot: you are not using it properly. Your Test class should look like this (no "static" access to BookRepository and done differently):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.test.elasticsearch.models.Book;
import org.test.elasticsearch.repositories.BookRepository;
#Configuration
#ComponentScan(basePackages = {"org.test.elasticsearch.configs", "org.test.webapp"})
#EnableAutoConfiguration
public class Test implements CommandLineRunner {
#Autowired
private BookRepository bookRepository;
#Override
public void run(String... args) {
final Book testBook = new Book("12345", "TestTitle", "TestAuthor");
bookRepository.save(testBook);
}
public static void main(final String[] args) {
SpringApplication.run(Test.class, args);
}
}
Because, as I see it, you don't want to use a web application, so further you only need these dependencies in pom.xml. Nothing related to spring-boot-web:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>

Resources