I have a controller where I am being passed a gatewayid for different providers. I have a BasePaymentService interface which is implemented by Payu service and Razorpay service for now.
I want to avoid using if condition for and have the strategy added without changing code and have the container inject correct strategy.
How do I add a strategy pattern here?
Would callback method be also a part of strategy here?
How do I account for different payment methods here? (cards, wallets)
BasePaymentService
public interface BasePaymentService {
PaymentDetail makePayment(PaymentDetail detail);
Payment callbackPayment(PaymentCallback detail);
}
Concrete PaymentService
#Service
#Slf4j
public class PayuPaymentService implements BasePaymentService {
#Autowired
PaymentRepository paymentRepository;
public PaymentDetail makePayment(PaymentDetail paymentDetail) {
PaymentUtil paymentUtil = new PaymentUtil();
paymentDetail = paymentUtil.populatePaymentDetail(paymentDetail);
savePaymentDetail(paymentDetail);
return paymentDetail;
}
#Override
public Payment callbackPayment(PaymentCallback paymentResponse) {
log.info("inside callback service >>>>>");
String msg = "Transaction failed.";
Payment payment = paymentRepository.findByTxnId(paymentResponse.getTxnid());
if(payment != null) {
log.info("in condition callback service");
//TODO validate the hash
PaymentStatus paymentStatus = null;
if(paymentResponse.getStatus().equals("failure")){
paymentStatus = PaymentStatus.Failed;
}else if(paymentResponse.getStatus().equals("success")) {
paymentStatus = PaymentStatus.Success;
msg = "Transaction success";
}
payment.setPaymentStatus(paymentStatus);
payment.setMihpayId(paymentResponse.getMihpayid());
payment.setMode(paymentResponse.getMode());
paymentRepository.save(payment);
}
return payment;
}
private void savePaymentDetail(PaymentDetail paymentDetail) {
log.info("in proceedPayment save");
Payment payment = new Payment();
payment.setAmount(Double.parseDouble(paymentDetail.getAmount()));
payment.setEmail(paymentDetail.getEmail());
payment.setName(paymentDetail.getName());
payment.setPaymentDate(new Date());
payment.setPaymentStatus(PaymentStatus.Pending);
payment.setPhone(paymentDetail.getPhone());
payment.setProductInfo(paymentDetail.getProductInfo());
payment.setTxnId(paymentDetail.getTxnId());
paymentRepository.save(payment);
}
}
Controller
#Api(value = "swipe: payment Service", tags = "Example")
#Validated
#RestController
#Slf4j
#RequestMapping(value = CommonConstants.BASE_CONTEXT_PATH)
public class CommonController {
#Autowired
private BasePaymentService paymentService;
#CrossOrigin(origins = "*")
#PostMapping(path = "/payment-details")
public #ResponseBody
PaymentDetail proceedPayment(#RequestBody PaymentDetail paymentDetail){
if(paymentDetail.getGatewayId().equalsIgnoreCase("payu") ){
paymentService.makePayment(paymentDetail);
}
else if(paymentDetail.getGatewayId().equalsIgnoreCase("rp")){
paymentService.makePayment(paymentDetail);
}
return paymentDetail;
}
#CrossOrigin(origins = "*" )
#RequestMapping(path = "/payment-response", method = RequestMethod.POST)
public #ResponseBody
Payment payuCallback(#RequestParam String mihpayid, #RequestParam String status, #RequestParam PaymentMode mode, #RequestParam String txnid, #RequestParam String hash, #RequestParam String amount, #RequestParam String productinfo, #RequestParam String firstname, #RequestParam String lastname, #RequestParam String email, #RequestParam String phone, #RequestParam String error, #RequestParam String bankcode, #RequestParam String PG_TYPE, #RequestParam String bank_ref_num, #RequestParam String unmappedstatus){
log.info("inside callback");
PaymentCallback paymentCallback = new PaymentCallback();
paymentCallback.setMihpayid(mihpayid);
paymentCallback.setTxnid(txnid);
paymentCallback.setMode(mode);
paymentCallback.setHash(hash);
paymentCallback.setStatus(status);
return paymentService.callbackPayment(paymentCallback);
}
}
I'm writing some code for user authorization. For users with 2 factored authorization enabled I'm writing code for 2fa secret update:
#RestController
public class CurrentUserController {
#PostMapping(value = "update-2fa-secret", produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] update2FaSecret() {
UserEntity user = userRepository.findOne(currentUserId);
if (user.is2FaEnabled() != Boolean.TRUE)
throw new HttpForbiddenException("2fa disabled for current user");
String secret = createNewSecret();
user.setSecret2Fa(secret);
userRepository.save(user);
return createQRCode(secret, user.getEmail());
}
}
And Exception:
#ResponseStatus(HttpStatus.FORBIDDEN)
public class HttpForbiddenException extends RuntimeException {
............
}
And when Exception happens I get response from the server with 406 Http status and without body (content).
I don't understand why this happens and how to solve it. Can somebody explain it to me please?
I've solved this issue in the next way:
#RestController
public class CurrentUserController {
#PostMapping(value = "update-2fa-secret", produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] update2FaSecret(HttpServletResponse response) {
UserEntity user = userRepository.findOne(currentUserId);
if (user.is2FaEnabled() != Boolean.TRUE) { //fix is here
response.setStatus(HttpStatus.FORBIDDEN.value()); //403
return new byte[0];
}
String secret = createNewSecret();
user.setSecret2Fa(secret);
userRepository.save(user);
return createQRCode(secret, user.getEmail());
}
}
I'm learning to make Bean Validation works in Spring MVC with Thymeleaf as default view. Every valid data can be saved properly. But when I tried an invalid data passed, Tomcat just showed HTTP Status 400 Error page. In Tomcat console showed something like validation but just became logging text in Tomcat console. Here is the controller that saves data (item).
#Controller
#RequestMapping("/item")
#SessionAttributes("item")
public class ItemController {
#Autowired
private ItemService itemService;
#Autowired
private ColorService colorService;
#ModelAttribute("allColors")
public List<Color> populateColors() {
return colorService.findAll();
}
#ModelAttribute("allItems")
public List<Item> populateItems() {
return itemService.findAll();
}
#RequestMapping(value = {"/image/{id}", "image/{id}"})
#ResponseBody
public byte[] showImage(#PathVariable("id") String id) {
return itemService.getItem(id).getImage();
}
#RequestMapping(value = {"", "/"}, method = RequestMethod.GET)
public String showAllItems() {
return "itemList";
}
#RequestMapping(value = {"add", "/add"}, method = RequestMethod.GET)
public String showItemAddForm(Model model) {
model.addAttribute("item", new Item());
return "itemAddForm";
}
#RequestMapping(value = {"add", "/add"}, method = RequestMethod.POST)
public String processAddItem(
#ModelAttribute("item") #Valid Item item,
RedirectAttributes model,
BindingResult errors,
SessionStatus session) {
if (errors.hasErrors()) {
return "itemAddForm";
}
itemService.saveItem(item);
session.setComplete();
model.addFlashAttribute("message", "Item has been added");
return "redirect:/item";
}
}
Is any wrong with the controller? How should I to make Bean Validation works with Spring and Thymeleaf?
I am new to Junit.Please help me to test Spring hibernate Controller with ContentType is application/json
Below is my Controller
#Controller
#RequestMapping(value="/users")
public class UserServiceImpl implements UserService{
private static Logger logger = Logger.getLogger(UserService.class);
private UserDao userDao;
#Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
#RequestMapping(method = RequestMethod.POST,headers = "content-type=application/json")
#ResponseBody
public long addUser(#RequestBody UserForm user) {
logger.info("Creating new user {}"+ user);
return userDao.create(user);
}
#RequestMapping(value = "/{userId}", method = RequestMethod.GET)
#ResponseBody
public User findUser(#PathVariable(value = "userId") String userId) {
logger.info("Reading user with id {}"+ userId);
User user = userDao.find(userId);
Validate.isTrue(user != null, "Unable to find user with id: " + userId);
return user;
}
#RequestMapping(value = "/{userId}", method = RequestMethod.PUT,headers = "content-type=application/json")
#ResponseStatus(value = HttpStatus.NO_CONTENT)
public void updateUser(#PathVariable(value = "userId") String userId, #RequestBody UserForm user) {
logger.info("Updating user with id {} with {}"+ userId +"->"+ user);
Validate.isTrue(userId.equals(user.getUserId()), "userId doesn't match URL userId: " + user.getUserId());
userDao.update(user);
}
#RequestMapping(value = "/{userId}", method = RequestMethod.DELETE)
#ResponseStatus(value = HttpStatus.NO_CONTENT)
public void deleteUser(#PathVariable(value = "userId") String userId) {
logger.info("Deleting user with id {}"+ userId);
userDao.delete(userId);
}
#RequestMapping(method = RequestMethod.GET)
#ResponseBody
public List<User> list() {
logger.info("Listing users");
return new ArrayList<User>(userDao.getUsers());
}
}
Can any one Send me the Junit Test case for Any one of the CRUD operations.
Thanks in Advance
Srikanth
If you just want to test your controller, then I would say that mock the DAO. You don't have to care about content types and such because Spring takes care of them. You are interested what the controller method is returning. If you want to test your DAO that User actually is saved to database, that's another story.
But just for testing that controller does what it is supposed to, something like this for example. Example uses EasyMock. I haven't compiled this example so it might have typos.
import static org.easymock.EasyMock.createNiceMock;
public class ControllerTest {
private UserServiceImpl userService;
private UserDao userDaoMock;
#Before
public void setup() {
userDaoMock = createNiceMock(UserDao.class);
userService = new UserServiceImpl();
userSerivce.setUserDao(userDaoMock);
}
#Test
public void testAddUser() {
UserForm userForm = new UserForm();
long expectedResult = 5L;
expect(userDaoMock.create(userForm)).andReturn(expectedResult);
replay(userDaoMock);
long actualResult = userService.addUser(userForm);
verify(userDaoMock);
assertEquals(expectedResult, actualResult);
}
}
For a simple RESTful JSON api implemented in Spring MVC, can I use Bean Validation (JSR-303) to validate the path variables passed into the handler method?
For example:
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") String customerNumber) {
...
}
Here, I need to validate the customerNumber variables's length using Bean validation. Is this possible with Spring MVC v3.x.x? If not, what's the best approach for this type of validations?
Thanks.
Spring does not support #javax.validation.Valid on #PathVariable annotated parameters in handler methods. There was an Improvement request, but it is still unresolved.
Your best bet is to just do your custom validation in the handler method body or consider using org.springframework.validation.annotation.Validated as suggested in other answers.
You can use like this:
use org.springframework.validation.annotation.Validated to valid RequestParam or PathVariable.
*
* Variant of JSR-303's {#link javax.validation.Valid}, supporting the
* specification of validation groups. Designed for convenient use with
* Spring's JSR-303 support but not JSR-303 specific.
*
step.1 init ValidationConfig
#Configuration
public class ValidationConfig {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
return processor;
}
}
step.2 Add #Validated to your controller handler class, Like:
#RequestMapping(value = "poo/foo")
#Validated
public class FooController {
...
}
step.3 Add validators to your handler method:
#RequestMapping(value = "{id}", method = RequestMethod.DELETE)
public ResponseEntity<Foo> delete(
#PathVariable("id") #Size(min = 1) #CustomerValidator int id) throws RestException {
// do something
return new ResponseEntity(HttpStatus.OK);
}
final step. Add exception resolver to your context:
#Component
public class BindExceptionResolver implements HandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
if (ex.getClass().equals(BindException.class)) {
BindException exception = (BindException) ex;
List<FieldError> fieldErrors = exception.getFieldErrors();
return new ModelAndView(new MappingJackson2JsonView(), buildErrorModel(request, response, fieldErrors));
}
}
}
The solution is simple:
#GetMapping(value = {"/", "/{hash:[a-fA-F0-9]{40}}"})
public String request(#PathVariable(value = "hash", required = false) String historyHash)
{
// Accepted requests: either "/" or "/{40 character long hash}"
}
And yes, PathVariables are ment to be validated, like any user input.
Instead of using #PathVariable, you can take advantage of Spring MVC ability to map path variables into a bean:
#RestController
#RequestMapping("/user")
public class UserController {
#GetMapping("/{id}")
public void get(#Valid GetDto dto) {
// dto.getId() is the path variable
}
}
And the bean contains the actual validation rules:
#Data
public class GetDto {
#Min(1) #Max(99)
private long id;
}
Make sure that your path variables ({id}) correspond to the bean fields (id);
#PathVariable is not meant to be validated in order to send back a readable message to the user. As principle a pathVariable should never be invalid. If a pathVariable is invalid the reason can be:
a bug generated a bad url (an href in jsp for example). No #Valid is
needed and no message is needed, just fix the code;
"the user" is manipulating the url.
Again, no #Valid is needed, no meaningful message to the user should
be given.
In both cases just leave an exception bubble up until it is catched by
the usual Spring ExceptionHandlers in order to generate a nice
error page or a meaningful json response indicating the error. In
order to get this result you can do some validation using custom editors.
Create a CustomerNumber class, possibly as immutable (implementing a CharSequence is not needed but allows you to use it basically as if it were a String)
public class CustomerNumber implements CharSequence {
private String customerNumber;
public CustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
#Override
public String toString() {
return customerNumber == null ? null : customerNumber.toString();
}
#Override
public int length() {
return customerNumber.length();
}
#Override
public char charAt(int index) {
return customerNumber.charAt(index);
}
#Override
public CharSequence subSequence(int start, int end) {
return customerNumber.subSequence(start, end);
}
#Override
public boolean equals(Object obj) {
return customerNumber.equals(obj);
}
#Override
public int hashCode() {
return customerNumber.hashCode();
}
}
Create an editor implementing your validation logic (in this case no whitespaces and fixed length, just as an example)
public class CustomerNumberEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text) && !StringUtils.containsWhitespace(text) && text.length() == YOUR_LENGTH) {
setValue(new CustomerNumber(text));
} else {
throw new IllegalArgumentException();
// you could also subclass and throw IllegalArgumentException
// in order to manage a more detailed error message
}
}
#Override
public String getAsText() {
return ((CustomerNumber) this.getValue()).toString();
}
}
Register the editor in the Controller
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(CustomerNumber.class, new CustomerNumberEditor());
// ... other editors
}
Change the signature of your controller method accepting CustomerNumber instead of String (whatever your ResponseObject is ...)
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") CustomerNumber customerNumber) {
...
}
You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field
Path variable may not be linked with any bean in your system. What do you want to annotate with JSR-303 annotations?
To validate path variable you should use this approach Problem validating #PathVariable url on spring 3 mvc
Actually there is a very simple solution to this. Add or override the same controller method with its request mapping not having the placeholder for the path variable and throw ResponseStatusException from it. Code given below
#RequestMapping(value = "/number")
#ResponseBody
public ResponseObject searchByNumber() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"customer number missing")
}