So far, we have written code to interact with the DB. Next up is to work on the code for the controller. We will have both types of controller—one that returns the view name (Thymeleaf template in our case) with the data for the view populated in the model object, and the other that exposes the RESTful APIs. We will need to add the following dependency to pom.xml
:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency>
Note
Adding spring-webmvc
to the dependency will automatically include spring-core
, spring-beans
, and spring-context
dependencies. So we can remove them from the pom.xml
.
To be able to make use of the Spring MVC features, we need to have one class that has been annotated with @Configuration
, to be annotated with @EnableWebMvc
. The @EnableWebMvc
annotation, imports the Spring MVC configuration from the WebMvcConfigurationSupport
class present in the Spring MVC framework. If we need to override any of the default imported configuration, we would have to implement the WebMvcConfigurer
interface present in the Spring MVC framework and override the required methods.
We will create an AppConfiguration
class with the following definition:
@EnableWebMvc @Configuration @ComponentScan(basePackages = "com.nilangpatel.worldgdp") public class AppConfiguration implements WebMvcConfigurer{ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("/static/"); } }
In the previous configuration, a few important things to note are as follows:
@EnableWebMvc
: This imports the Spring MVC related configuration.@ComponentScan
: This is used for declaring the packages that have to be scanned for Spring components (which can be@Configuration
,@Service
,@Controller
,@Component
, and so on). If no package is defined, then it scans starting from the package where the class is defined.WebMvcConfigurer
: We are going to implement this interface to override some of the default Spring MVC configuration seen in the previous code.
As we will be deploying the application to Tomcat, we need to provide the servlet configuration to the application server. We will look at how to deploy to Tomcat in a separate section, but now we will look at the Java configuration, which is sufficient to deploy the application to Tomcat or any application server without the need for an additional web.xml
. The Java class definition is given in the following:
public class WorldApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return null; } @Override protected Class<?>[] getServletConfigClasses() { return new Class[] {AppConfiguration.class}; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } }
The AbstractAnnotationConfigDispatcherServletInitializer
abstract class is an implementation of the WebApplicationInitializer
interface that is used to register Spring's DispatcherServlet
instance and uses the other @Configuration
classes to configure the DispatcherServlet
.
We just need to override the getRootConfigClasses()
, getServletConfigClasses()
, and getServletMappings()
methods. The first two methods point to the configuration classes that need to load into the servlet context, and the last method is used to provide the servlet mapping for DispatcherServlet
.
DispatcherServlet
follows the front controller pattern, where there is a single servlet registered to handle all the web requests. This servlet uses the RequestHandlerMapping
and invokes the corresponding implementation based on the URL mapped to the implementation.
We need to make a small update to the Maven WAR plugin so that it doesn't fail if there is no web.xml
found. This can be done by updating the <plugins>
tag in the pom.xml
file, as shown in the following:
<build> <finalName>worldgdp</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <executions> <execution> <id>default-war</id> <phase>prepare-package</phase> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </execution> </executions> </plugin> </plugins> </build>
Now we are all set to implement our controllers. We will show you how to deploy to Tomcat once we have implemented all the RESTful API controllers.
Let's define the RESTful API controller for the country resource. The following is the template for the controller:
@RestController @RequestMapping("/api/countries") @Slf4j public class CountryAPIController { @Autowired CountryDAO countryDao; @Autowired WorldBankApiClient worldBankApiClient; @GetMapping public ResponseEntity<?> getCountries( @RequestParam(name="search", required = false) String searchTerm, @RequestParam(name="continent", required = false) String continent, @RequestParam(name="region", required = false) String region, @RequestParam(name="pageNo", required = false) Integer pageNo ){ //logic to fetch contries from CountryDAO return ResponseEntity.ok(); } @PostMapping(value = "/{countryCode}", consumes = {MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity<?> editCountry( @PathVariable String countryCode, @Valid @RequestBody Country country ){ //logic to edit existing country return ResponseEntity.ok(); } @GetMapping("/{countryCode}/gdp") public ResponseEntity<?> getGDP(@PathVariable String countryCode){ //logic to get GDP by using external client return ResponseEntity.ok(); } }
The following are a few things to note from the previous code:
@RestController
: This is used to annotate a class as a controller with each of the RESTful methods returning the data in the response body.@RequestMapping
: This is for assigning the root URL for accessing the resources.@GetMapping
and@PostMapping
: These are used to assign the HTTP verbs that will be used to invoke the resources. The URL for the resources are passed within the annotation, along with other request headers that consume and produce information.
Let's implement each of the methods in order, starting with getCountries()
, as shown in the following code:
@GetMapping public ResponseEntity<?> getCountries( @RequestParam(name="search", required = false) String searchTerm, @RequestParam(name="continent", required = false) String continent, @RequestParam(name="region", required = false) String region, @RequestParam(name="pageNo", required = false) Integer pageNo ){ try { Map<String, Object> params = new HashMap<String, Object>(); params.put("search", searchTerm); params.put("continent", continent); params.put("region", region); if ( pageNo != null ) { params.put("pageNo", pageNo.toString()); } List<Country> countries = countryDao.getCountries(params); Map<String, Object> response = new HashMap<String, Object>(); response.put("list", countries); response.put("count", countryDao.getCountriesCount(params)); return ResponseEntity.ok(response); }catch(Exception ex) { log.error("Error while getting countries", ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("Error while getting countries"); } }
The following are some of the things to note from the previous code:
@RequestParam
: This annotation is used to declare request parameters accepted by the controller endpoint. The parameters can be provided with a default value and can also be made mandatory.ResponseEntity
: This class is used to return the response body, along with other response parameters such as status, headers, and so on.
Next up is the API for editing country details, as follows:
@PostMapping("/{countryCode}") public ResponseEntity<?> editCountry( @PathVariable String countryCode, @Valid @RequestBody Country country ){ try { countryDao.editCountryDetail(countryCode, country); Country countryFromDb = countryDao.getCountryDetail(countryCode); return ResponseEntity.ok(countryFromDb); }catch(Exception ex) { log.error("Error while editing the country: {} with data: {}", countryCode, country, ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("Error while editing the country"); } }
The following are a few things to note from the previous code implementation:
@PathVariable
: This is used to declare any variable that needs to be part of the URL path of the controller endpoint. In our case, we want the country code to be part of the URL. So the URL will be of the/api/countries/IND
form.@Valid
: This triggers the Bean Validation API to check for the restrictions on each of the class properties. If the data from the client is not valid, it returns a400
.@RequestBody
: This is used to capture the data sent in the request body and theJackson
library is used to convert the JSON data in the request body to the corresponding Java object.
The rest of the API implementation can be found in the CountryAPIController
class. The tests for the API controller can be found in the CountryAPIControllerTest
class, which is available in the source code of this book.
For the city resource we would need the following APIs:
- Get cities for a given country
- Add a new city to the country
- Delete the city from the country
The code for this controller can be found in the CityAPIController
class and the tests for the API controller can be found in the CityAPIControllerTest
class, which is available in the source code of this book.
For the CountryLanguage
resource we need the following APIs:
- Get languages for a country
- Add a language for a country
- Delete a language from the country
The code for this controller can be found in the CountryLanguageAPIController
class and the tests for the API controller can be found in the CountryLanguageAPIControllerTest
class, which is available in the source code of this book.