I am working on my first Spring-Boot app. Got a working UI Controller implemented below:
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.http.HttpStatus;
import java.util.List;
import java.util.ArrayList;
#Controller
public class UiController {
private ProductService productService;
private LocationService locationService;
private InventoryService inventoryService;
private CartService cartService;
public UiController(
ProductService productService,
LocationService locationService,
InventoryService inventoryService,
CartService cartService) {
this.productService = productService;
this.locationService = locationService;
this.inventoryService = inventoryService;
this.cartService = cartService;
}
#GetMapping("/")
public String home(Model model) {
model.addAttribute("products", productService.getAllProducts());
return "index";
}
#GetMapping("/brand/{brand}")
public String brand(Model model, #PathVariable String brand) {
List prods = productService.getProductByBrand(brand);
if (prods.size() == 0) throw new ItemNotFoundException();
model.addAttribute("products", prods);
return "index";
}
#ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such item") // 404
public class ItemNotFoundException extends RuntimeException {
// ...
}
#GetMapping("/product/{productId}")
public String product(Model model, #PathVariable String productId) {
Product prod = productService.getProduct(productId);
if (prod == null) throw new ItemNotFoundException();
ArrayList<Product> ps = new ArrayList<Product>();
ps.add(prod);
model.addAttribute("products", ps);
return "index";
}
}
I want to add a REST controller returning the same thing as HTML only I want the responses to be in JSON. When there is no data, I want an error return. Added the below:
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import java.util.List;
import java.util.ArrayList;
import com.google.gson.Gson;
#ApiController
public class ApiController {
private ProductService productService;
private LocationService locationService;
private InventoryService inventoryService;
private CartService cartService;
public ApiController(
ProductService productService,
LocationService locationService,
InventoryService inventoryService,
CartService cartService) {
this.productService = productService;
this.locationService = locationService;
this.inventoryService = inventoryService;
this.cartService = cartService;
}
#GetMapping("/rest")
public String home() {
List prods = productService.getAllProducts();
if (prods.size() == 0) throw new ItemNotFoundException();
return new Gson().toJson(prods);
}
#GetMapping("/rest/brand/{brand}")
public String brand(#PathVariable String brand) {
List prods = productService.getProductByBrand(brand);
if (prods.size() == 0) throw new ItemNotFoundException();
return new Gson().toJson(prods);
}
#ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such item") // 404
public class ItemNotFoundException extends RuntimeException {
// ...
}
#GetMapping("/rest/product/{productId}")
public String product(#PathVariable String productId) {
Product prod = productService.getProduct(productId);
if (prod == null) throw new ItemNotFoundException();
return new Gson().toJson(prod);
}
}
Apparently, autoconfig is working and my controller gets picked by the compiler. Only, I get the below error:
Compilation failure
ApiController.java:[21,2] incompatible types: com.rei.interview.ui.ApiController cannot be converted to java.lang.annotation.Annotation
What am I doing wrong and what should I do?
You did a simple mistake at the beginning of the controller.
The class must be annotated #RestController... not #ApiController
Change your code from
#ApiController
public class ApiController {
...
}
to
#RestController // <- Change annotation here
public class ApiController {
...
}
The error
ApiController.java:[21,2] incompatible types:
com.rei.interview.ui.ApiController cannot be converted to java.lang.annotation.Annotation
informs you that the annotation #ApiController is not a of type java.lang.annotation.Annotation
Related
I have a Spring Boot application and I have implemented a base controller that handles CRUD operations for all my entities. I have also created a BrandController that extends the base controller and a BrandRepository that implements CrudRepository. The problem is that when I try to access the endpoints for the BrandController such as /api/brands, I get a 404 error, but I can access them on /brands How can I fix this so that the endpoints are accessible with /api/entitys?
Here is the code for the BrandController:
package parc.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import parc.model.concrete.Brand;
import parc.repository.BrandRepository;
#RestController
#RequestMapping("/api/brands")
public class BrandController extends BaseController<Brand, BrandRepository> {
private final BrandRepository repository;
public BrandController(BrandRepository repository) {
super(repository);
this.repository = repository;
}
}
Here is the code for the BaseController:
package parc.controller;
import org.springframework.data.repository.CrudRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public class BaseController<T, R extends CrudRepository<T, Long>> {
private R repository;
public BaseController(R repository) {
this.repository = repository;
}
#GetMapping("/")
public List<T> getAll() {
return (List<T>) repository.findAll();
}
#PostMapping("/")
public T create(#RequestBody T entity) {
return repository.save(entity);
}
#GetMapping("/{id}")
public T getById(#PathVariable long id) {
return repository.findById(id).orElse(null);
}
#PutMapping("/{id}")
public T update(#PathVariable long id, #RequestBody T entity) {
return repository.save(entity);
}
#DeleteMapping("/{id}")
public void delete(#PathVariable long id) {
repository.deleteById(id);
}
}
And finally the code for the BrandRepository:
package parc.repository;
import org.springframework.data.repository.CrudRepository;
import parc.model.concrete.Brand;
public interface BrandRepository extends CrudRepository<Brand, Long> {
}
I'm not a pro in Spring Boot so I'll appreciate any kind of help!
What do you have in application.yml?
maybe setting the following code will work:
in application.yml:
server:
contextPath: /api
and the BrandController:
package parc.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import parc.model.concrete.Brand;
import parc.repository.BrandRepository;
#RestController
#RequestMapping("/brands")
public class BrandController extends BaseController<Brand, BrandRepository> {
private final BrandRepository repository;
public BrandController(BrandRepository repository) {
super(repository);
this.repository = repository;
}
}
I'm building a Spring application which has #RestController, like:
#RestController
#RequestMapping(value = "/master")
public class MyController {
#Autowired
private MyService service;
#PostMapping("/call")
public ResponseEntity<Boolean> apiCall(#RequestBody MyDTO myDto) { ;
return new ResponseEntity<Boolean>(service.apiCall(myDto), OK);
}
}
And a request object:
public class MyDTO {
#JsonProperty("emp_number")
private long empNumber;
#JsonProperty("office_id")
private long officeId;
// ....constructors, etc.
}
In request json I want officeId to be not null.
So far I've tried marking the officeId field as:
#com.fasterxml.jackson.databind.annotation.JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
#JsonProperty(required = true)
#javax.validation.constraints.NotNull
But in the request json, even if I miss office_id, it is not throwing any error.
What am I missing?
It could be that in your case you are deserializing request, and missing primitive properties which are referenced by constructor are assigned a default value
java defaults
You could try to use corresponding Long wrapper object for MyDTO instead of primitives or maybe deserialization feature FAIL_ON_NULL_FOR_PRIMITIVES
You have to change the type from primitive to wrapper otherwise the default value of 0 will be considered and validation will pass.
Annotate MyDTO with #Valid annotation.
When Spring Boot finds an argument annotated with #Valid, it automatically bootstraps the default JSR 380 implementation — Hibernate Validator — and validates the argument.
from here
Please add following dependency to pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
PS: i have made minor adjustments to response structure to see the error in response
Entire code is as follows:
package com.example.spring.java.springjavasamples;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.Map;
#SpringBootApplication
public class SpringJavaSamplesApplication {
public static void main(String[] args) {
SpringApplication.run(SpringJavaSamplesApplication.class, args);
}
}
#RestController
#RequestMapping(value = "/master")
class MyController {
private final MyService service;
public MyController(MyService service) {
this.service = service;
}
#PostMapping("/call")
public ResponseEntity<Map<String, Object>> apiCall(#Valid #RequestBody MyDTO myDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = new HashMap<>();
bindingResult.getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return prepareResponse("error", errors, HttpStatus.BAD_REQUEST);
} else {
return prepareResponse("data", service.apiCall(myDto), HttpStatus.OK);
}
}
private ResponseEntity<Map<String, Object>> prepareResponse(String key, Object data, HttpStatus status) {
Map<String, Object> map = new HashMap<>();
map.put(key, data);
return new ResponseEntity<>(map, status);
}
}
class MyDTO {
#NotNull(message = "Employee number cannot be null")
#JsonProperty("emp_number")
private Long empNumber;
#NotNull(message = "Office Id cannot be null")
#JsonProperty("office_id")
private Long officeId;
#Override
public String toString() {
return "MyDTO{" +
"empNumber=" + empNumber +
", officeId=" + officeId +
'}';
}
}
#Service
class MyService {
public String apiCall(MyDTO myDto) {
System.out.println("all valid: " + myDto);
return myDto.toString();
}
}
issue is when i am looking swagger for v1 there i can see one endpoint which is valid, but for v2 i have given two endpoints inside controller, but /allusers endpoint i am not able to see. below are the controller.
controller v1:
package com.springboot.rest.controller.v1;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.springboot.rest.dto.UserDto;
import com.springboot.rest.service.UserService;
#RestController(value = "userControllerV1")
#RequestMapping(value = "/userinfo", produces = "application/json")
public class UserController {
public static final String X_ACCEPT_VERSION_V1 = "X-Accept-Version" + "=" + "v1";
#Autowired
private UserService userService;
#GetMapping(value = "/allusers", headers = X_ACCEPT_VERSION_V1)
public List<UserDto> getUserinfo() {
List<UserDto> finalResults = userService.getAllUserInfo();
return finalResults;
}
}
controller v2:
package com.springboot.rest.controller.v2;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.springboot.rest.dto.UserDto;
import com.springboot.rest.service.UserService;
#RestController(value = "userControllerV2")
#RequestMapping(value = "/userinfo", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController {
public static final String X_ACCEPT_VERSION_V2 = "X-Accept-Version" + "=" + "v2";
#Autowired
private UserService userService;
#GetMapping(value = "/allusers", headers = X_ACCEPT_VERSION_V2)
public List<UserDto> getUserinfo() {
List<UserDto> finalResults = userService.getAllUserInfo();
return finalResults;
}
#GetMapping(value = "/message", headers = X_ACCEPT_VERSION_V2)
public String greetMessage() {
return userService.getGreetMessage();
}
}
and i don't want to change my getUserinfo() method, could anyone help?
URI paths for /allusers end point are same in both the controllers where as api endpoints should be unique through out the application. You can add version in the uri which will make it unique. For eg.
#RequestMapping(value = "/v2/userinfo", produces = MediaType.APPLICATION_JSON_VALUE)
I did many ways, but finally OpenApi and adding filter did it for me. below is the OpenApiConfig file and link for those who wants to achieve this.
#Configuration
public class OpenApiConfig {
#Bean
public OpenAPI customOpenApi() {
return new OpenAPI()
.components(new Components())
.info(new Info().title("User-Management Microservice")
.description("demo-microservice for user-management")
.termsOfService("www.abc.com")
.contact(new io.swagger.v3.oas.models.info.Contact()
.email("abc.com")
.name("user-management"))
.version("1.0"));
}
#Bean
public GroupedOpenApi v1OpenApi() {
String[] packagesToScan = {"com.springboot.rest.controller.v1"};
return GroupedOpenApi.builder().setGroup("v1 version").packagesToScan(packagesToScan).build();
}
#Bean
public GroupedOpenApi v2OpenApi() {
String[] packagesToScan = {"com.springboot.rest.controller.v2"};
return GroupedOpenApi.builder().setGroup("v2 version").packagesToScan(packagesToScan).build();
}
}
use below link for step by step explanation:
https://www.youtube.com/watch?v=Z4FwdCgik5M
I have tried a number of examples from the net and cannot get Spring to validate my query string parameter. It doesn't seem execute the REGEX / fail.
package my.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.Pattern;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
#RestController
public class MyController {
private static final String VALIDATION_REGEX = "^[0-9]+(,[0-9]+)*$";
#RequestMapping(value = "/my/{id}", method = GET)
public myResonseObject getMyParams(#PathVariable("id") String id,
#Valid #Pattern(regexp = VALIDATION_REGEX)
#RequestParam(value = "myparam", required = true) String myParam) {
// Do Stuff!
}
}
Current behaviour
PASS - /my/1?myparam=1
PASS - /my/1?myparam=1,2,3
PASS - /my/1?myparam=
PASS - /my/1?myparam=1,bob
Desired behaviour
PASS - /my/1?myparam=1
PASS - /my/1?myparam=1,2,3
FAIL - /my/1?myparam=
FAIL - /my/1?myparam=1,bob
Thanks
You need add #Validated to your class like this:
#RestController
#Validated
class Controller {
// ...
}
UPDATE:
you need to configure it properly.. add this bean to your context:
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
Example to handle exception:
#ControllerAdvice
#Component
public class GlobalExceptionHandler {
#ExceptionHandler
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
public Map handle(MethodArgumentNotValidException exception) {
return error(exception.getBindingResult().getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList()));
}
#ExceptionHandler
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
public Map handle(ConstraintViolationException exception) {
return error(exception.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList()));
}
private Map error(Object message) {
return Collections.singletonMap("error", message);
}
}
You can try this
#Pattern(regexp="^[0-9]+(,[0-9]+)*$")
private static final String VALIDATION_REGEX;
(pay attention for the final modifier)
or else
#Pattern()
private static final String VALIDATION_REGEX = "^[0-9]+(,[0-9]+)*$";
And then remove #Pattern(regexp = VALIDATION_REGEX) from your method and keep only the #Valid annotation:
public myResonseObject getMyParams(#PathVariable("id") String id, #Valid #RequestParam(value = "myparam", required = true) String myParam) {
I have a problem when trying to test the JSON output from a Spring REST Service using MockMvcResultMatchers where the returned object should contain a Long value.
The test will only pass when the value within the JSON object is is higher than Integer.MAX_VALUE. This seems a little odd to me as I feel that I should be able to test the full range of applicable values.
I understand that since JSON does not include type information it is performing a best guess at the type at de-serialisation, but I would have expected there to be a way to force the type for extraction when performing the comparison in the MockMvcResultMatchers.
Full code is below but the Test is:
#Test
public void testGetObjectWithLong() throws Exception {
Long id = 45l;
ObjectWithLong objWithLong = new ObjectWithLong(id);
Mockito.when(service.getObjectWithLong(String.valueOf(id))).thenReturn(objWithLong);
mockMvc.perform(MockMvcRequestBuilders.get("/Test/" + id))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$longvalue")
.value(Matchers.isA(Long.class)))
.andExpect(MockMvcResultMatchers.jsonPath("$longvalue")
.value(Matchers.equalTo(id)));
}
and the Result is:
java.lang.AssertionError: JSON path$longvalue
Expected: is an instance of java.lang.Long
but: <45> is a java.lang.Integer
at org.springframework.test.util.MatcherAssertionErrors.assertThat(MatcherAssertionErrors.java:80)
...
Any ideas or suggestions as to the proper way to fix this would be appreciated. Obviously I could just add Integer.MAX_VALUE to the id field in the test but that seems fragile.
Thanks in advance.
The following should be self contained apart from the third party libraries
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
#RunWith(MockitoJUnitRunner.class)
public class TestControllerTest {
private MockMvc mockMvc;
#Mock
private RandomService service;
#InjectMocks
private TestController controller = new TestController();
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setMessageConverters(new MappingJackson2HttpMessageConverter())
.build();
}
#Test
public void testGetObjectWithLong() throws Exception {
Long id = 45l;
ObjectWithLong objWithLong = new ObjectWithLong(id);
Mockito.when(service.getObjectWithLong(String.valueOf(id))).thenReturn(objWithLong);
mockMvc.perform(MockMvcRequestBuilders.get("/Test/" + id))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$longvalue").value(Matchers.isA(Long.class)))
.andExpect(MockMvcResultMatchers.jsonPath("$longvalue").value(Matchers.equalTo(id)));
}
#RestController
#RequestMapping(value = "/Test")
private class TestController {
#Autowired
private RandomService service;
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ObjectWithLong getObjectWithLong(#PathVariable final String id) {
return service.getObjectWithLong(id);
}
}
#Service
private class RandomService {
public ObjectWithLong getObjectWithLong(String id) {
return new ObjectWithLong(Long.valueOf(id));
}
}
private class ObjectWithLong {
private Long longvalue;
public ObjectWithLong(final Long theValue) {
this.longvalue = theValue;
}
public Long getLongvalue() {
return longvalue;
}
public void setLongvalue(Long longvalue) {
this.longvalue = longvalue;
}
}
}
You can use anyOf Matcher along with a Class match against the Number super class and set it up like
.andExpect(MockMvcResultMatchers.jsonPath("$longvalue")
.value(Matchers.isA(Number.class)))
.andExpect(MockMvcResultMatchers.jsonPath("$longvalue")
.value(Matchers.anyOf(
Matchers.equalTo((Number) id),
Matchers.equalTo((Number) id.intValue()))));