Merge pull request #6189 from rozagerardo/geroza/BAEL-11598_Update-and-move-rest-pagination-article
[BAEL-11598] Update and move code for the "REST Pagination in Spring" article
This commit is contained in:
commit
ef8ae28197
|
@ -2,3 +2,4 @@ Module for the articles that are part of the Spring REST E-book:
|
||||||
|
|
||||||
1. [Bootstrap a Web Application with Spring 5](https://www.baeldung.com/bootstraping-a-web-application-with-spring-and-java-based-configuration)
|
1. [Bootstrap a Web Application with Spring 5](https://www.baeldung.com/bootstraping-a-web-application-with-spring-and-java-based-configuration)
|
||||||
2. [Error Handling for REST with Spring](http://www.baeldung.com/exception-handling-for-rest-with-spring)
|
2. [Error Handling for REST with Spring](http://www.baeldung.com/exception-handling-for-rest-with-spring)
|
||||||
|
3. [REST Pagination in Spring](http://www.baeldung.com/rest-api-pagination-in-spring)
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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"
|
<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">
|
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>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.baeldung.web</groupId>
|
<groupId>com.baeldung.web</groupId>
|
||||||
|
@ -32,6 +33,30 @@
|
||||||
<groupId>org.springframework</groupId>
|
<groupId>org.springframework</groupId>
|
||||||
<artifactId>spring-jdbc</artifactId>
|
<artifactId>spring-jdbc</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-tx</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-commons</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- util -->
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>${guava.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
@ -58,5 +83,6 @@
|
||||||
<properties>
|
<properties>
|
||||||
<start-class>com.baeldung.SpringBootRestApplication</start-class>
|
<start-class>com.baeldung.SpringBootRestApplication</start-class>
|
||||||
<htmlunit.version>2.32</htmlunit.version>
|
<htmlunit.version>2.32</htmlunit.version>
|
||||||
|
<guava.version>27.0.1-jre</guava.version>
|
||||||
</properties>
|
</properties>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.baeldung.web;
|
package com.baeldung;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.baeldung.persistence;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
public interface IOperations<T extends Serializable> {
|
||||||
|
|
||||||
|
// read - all
|
||||||
|
|
||||||
|
Page<T> findPaginated(int page, int size);
|
||||||
|
|
||||||
|
// write
|
||||||
|
|
||||||
|
T create(final T entity);
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.baeldung.persistence.dao;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import com.baeldung.persistence.model.Foo;
|
||||||
|
|
||||||
|
public interface IFooDao extends JpaRepository<Foo, Long> {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package com.baeldung.persistence.model;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class Foo implements Serializable {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public Foo() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Foo(final String name) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(final long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(final String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (obj == null)
|
||||||
|
return false;
|
||||||
|
if (getClass() != obj.getClass())
|
||||||
|
return false;
|
||||||
|
final Foo other = (Foo) obj;
|
||||||
|
if (name == null) {
|
||||||
|
if (other.name != null)
|
||||||
|
return false;
|
||||||
|
} else if (!name.equals(other.name))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append("Foo [name=").append(name).append("]");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.baeldung.persistence.service;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
|
import com.baeldung.persistence.IOperations;
|
||||||
|
import com.baeldung.persistence.model.Foo;
|
||||||
|
|
||||||
|
public interface IFooService extends IOperations<Foo> {
|
||||||
|
|
||||||
|
Page<Foo> findPaginated(Pageable pageable);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.baeldung.persistence.service.common;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baeldung.persistence.IOperations;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public abstract class AbstractService<T extends Serializable> implements IOperations<T> {
|
||||||
|
|
||||||
|
// read - all
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<T> findPaginated(final int page, final int size) {
|
||||||
|
return getDao().findAll(PageRequest.of(page, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
// write
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T create(final T entity) {
|
||||||
|
return getDao().save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract PagingAndSortingRepository<T, Long> getDao();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.baeldung.persistence.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baeldung.persistence.dao.IFooDao;
|
||||||
|
import com.baeldung.persistence.model.Foo;
|
||||||
|
import com.baeldung.persistence.service.IFooService;
|
||||||
|
import com.baeldung.persistence.service.common.AbstractService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class FooService extends AbstractService<Foo> implements IFooService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IFooDao dao;
|
||||||
|
|
||||||
|
public FooService() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PagingAndSortingRepository<Foo, Long> getDao() {
|
||||||
|
return dao;
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom methods
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<Foo> findPaginated(Pageable pageable) {
|
||||||
|
return dao.findAll(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package com.baeldung.spring;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||||
|
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||||
|
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||||
|
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableTransactionManagement
|
||||||
|
@PropertySource({ "classpath:persistence-${envTarget:h2}.properties" })
|
||||||
|
@ComponentScan({ "com.baeldung.persistence" })
|
||||||
|
// @ImportResource("classpath*:springDataPersistenceConfig.xml")
|
||||||
|
@EnableJpaRepositories(basePackages = "com.baeldung.persistence.dao")
|
||||||
|
public class PersistenceConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Environment env;
|
||||||
|
|
||||||
|
public PersistenceConfig() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||||
|
final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
|
||||||
|
em.setDataSource(dataSource());
|
||||||
|
em.setPackagesToScan(new String[] { "com.baeldung.persistence.model" });
|
||||||
|
|
||||||
|
final HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
|
||||||
|
// vendorAdapter.set
|
||||||
|
em.setJpaVendorAdapter(vendorAdapter);
|
||||||
|
em.setJpaProperties(additionalProperties());
|
||||||
|
|
||||||
|
return em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DataSource dataSource() {
|
||||||
|
final DriverManagerDataSource dataSource = new DriverManagerDataSource();
|
||||||
|
dataSource.setDriverClassName(Preconditions.checkNotNull(env.getProperty("jdbc.driverClassName")));
|
||||||
|
dataSource.setUrl(Preconditions.checkNotNull(env.getProperty("jdbc.url")));
|
||||||
|
dataSource.setUsername(Preconditions.checkNotNull(env.getProperty("jdbc.user")));
|
||||||
|
dataSource.setPassword(Preconditions.checkNotNull(env.getProperty("jdbc.pass")));
|
||||||
|
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PlatformTransactionManager transactionManager() {
|
||||||
|
final JpaTransactionManager transactionManager = new JpaTransactionManager();
|
||||||
|
transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
|
||||||
|
|
||||||
|
return transactionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
|
||||||
|
return new PersistenceExceptionTranslationPostProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Properties additionalProperties() {
|
||||||
|
final Properties hibernateProperties = new Properties();
|
||||||
|
hibernateProperties.setProperty("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
|
||||||
|
hibernateProperties.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
|
||||||
|
// hibernateProperties.setProperty("hibernate.globally_quoted_identifiers", "true");
|
||||||
|
return hibernateProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.baeldung.spring;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.servlet.ViewResolver;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
|
||||||
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ComponentScan("com.baeldung.web")
|
||||||
|
@EnableWebMvc
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
public WebConfig() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ViewResolver viewResolver() {
|
||||||
|
final InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
|
||||||
|
viewResolver.setPrefix("/WEB-INF/view/");
|
||||||
|
viewResolver.setSuffix(".jsp");
|
||||||
|
return viewResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
@Override
|
||||||
|
public void addViewControllers(final ViewControllerRegistry registry) {
|
||||||
|
registry.addViewController("/graph.html");
|
||||||
|
registry.addViewController("/homepage.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
|
||||||
|
configurer.defaultContentType(MediaType.APPLICATION_JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,5 +27,4 @@ public class MyErrorController extends BasicErrorController {
|
||||||
HttpStatus status = getStatus(request);
|
HttpStatus status = getStatus(request);
|
||||||
return new ResponseEntity<>(body, status);
|
return new ResponseEntity<>(body, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
package com.baeldung.web.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class WebConfig {
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.baeldung.web.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
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.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import com.baeldung.persistence.model.Foo;
|
||||||
|
import com.baeldung.persistence.service.IFooService;
|
||||||
|
import com.baeldung.web.exception.MyResourceNotFoundException;
|
||||||
|
import com.baeldung.web.hateoas.event.PaginatedResultsRetrievedEvent;
|
||||||
|
import com.baeldung.web.hateoas.event.ResourceCreatedEvent;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = "/auth/foos")
|
||||||
|
public class FooController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IFooService service;
|
||||||
|
|
||||||
|
public FooController() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
// read - all
|
||||||
|
|
||||||
|
@RequestMapping(params = { "page", "size" }, method = RequestMethod.GET)
|
||||||
|
@ResponseBody
|
||||||
|
public List<Foo> findPaginated(@RequestParam("page") final int page, @RequestParam("size") final int size,
|
||||||
|
final UriComponentsBuilder uriBuilder, final HttpServletResponse response) {
|
||||||
|
final Page<Foo> resultPage = service.findPaginated(page, size);
|
||||||
|
if (page > resultPage.getTotalPages()) {
|
||||||
|
throw new MyResourceNotFoundException();
|
||||||
|
}
|
||||||
|
eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent<Foo>(Foo.class, uriBuilder, response, page,
|
||||||
|
resultPage.getTotalPages(), size));
|
||||||
|
|
||||||
|
return resultPage.getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pageable")
|
||||||
|
@ResponseBody
|
||||||
|
public List<Foo> findPaginatedWithPageable(Pageable pageable, final UriComponentsBuilder uriBuilder,
|
||||||
|
final HttpServletResponse response) {
|
||||||
|
final Page<Foo> resultPage = service.findPaginated(pageable);
|
||||||
|
if (pageable.getPageNumber() > resultPage.getTotalPages()) {
|
||||||
|
throw new MyResourceNotFoundException();
|
||||||
|
}
|
||||||
|
eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent<Foo>(Foo.class, uriBuilder, response,
|
||||||
|
pageable.getPageNumber(), resultPage.getTotalPages(), pageable.getPageSize()));
|
||||||
|
|
||||||
|
return resultPage.getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// write
|
||||||
|
|
||||||
|
@RequestMapping(method = RequestMethod.POST)
|
||||||
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
|
@ResponseBody
|
||||||
|
public Foo create(@RequestBody final Foo resource, final HttpServletResponse response) {
|
||||||
|
Preconditions.checkNotNull(resource);
|
||||||
|
final Foo foo = service.create(resource);
|
||||||
|
final Long idOfCreatedResource = foo.getId();
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(new ResourceCreatedEvent(this, response, idOfCreatedResource));
|
||||||
|
|
||||||
|
return foo;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.baeldung.web.controller;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
import org.springframework.web.util.UriTemplate;
|
||||||
|
|
||||||
|
import com.baeldung.web.util.LinkUtil;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = "/auth/")
|
||||||
|
public class RootController {
|
||||||
|
|
||||||
|
public RootController() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
// discover
|
||||||
|
|
||||||
|
@RequestMapping(value = "admin", method = RequestMethod.GET)
|
||||||
|
@ResponseStatus(value = HttpStatus.NO_CONTENT)
|
||||||
|
public void adminRoot(final HttpServletRequest request, final HttpServletResponse response) {
|
||||||
|
final String rootUri = request.getRequestURL()
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
final URI fooUri = new UriTemplate("{rootUri}/{resource}").expand(rootUri, "foo");
|
||||||
|
final String linkToFoo = LinkUtil.createLinkHeader(fooUri.toASCIIString(), "collection");
|
||||||
|
response.addHeader("Link", linkToFoo);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.baeldung.web.hateoas.event;
|
package com.baeldung.web.hateoas.event;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.baeldung.web.hateoas.event;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
public class ResourceCreatedEvent extends ApplicationEvent {
|
||||||
|
private final HttpServletResponse response;
|
||||||
|
private final long idOfNewResource;
|
||||||
|
|
||||||
|
public ResourceCreatedEvent(final Object source, final HttpServletResponse response, final long idOfNewResource) {
|
||||||
|
super(source);
|
||||||
|
|
||||||
|
this.response = response;
|
||||||
|
this.idOfNewResource = idOfNewResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
public HttpServletResponse getResponse() {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getIdOfNewResource() {
|
||||||
|
return idOfNewResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
package org.baeldung.web.hateoas.listener;
|
package com.baeldung.web.hateoas.listener;
|
||||||
|
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.baeldung.web.hateoas.event.PaginatedResultsRetrievedEvent;
|
|
||||||
import org.baeldung.web.util.LinkUtil;
|
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import com.baeldung.web.hateoas.event.PaginatedResultsRetrievedEvent;
|
||||||
|
import com.baeldung.web.util.LinkUtil;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
|
||||||
|
@ -27,32 +29,32 @@ class PaginatedResultsRetrievedDiscoverabilityListener implements ApplicationLis
|
||||||
public final void onApplicationEvent(final PaginatedResultsRetrievedEvent ev) {
|
public final void onApplicationEvent(final PaginatedResultsRetrievedEvent ev) {
|
||||||
Preconditions.checkNotNull(ev);
|
Preconditions.checkNotNull(ev);
|
||||||
|
|
||||||
addLinkHeaderOnPagedResourceRetrieval(ev.getUriBuilder(), ev.getResponse(), ev.getClazz(), ev.getPage(), ev.getTotalPages(), ev.getPageSize());
|
addLinkHeaderOnPagedResourceRetrieval(ev.getUriBuilder(), ev.getResponse(), ev.getClazz(), ev.getPage(),
|
||||||
|
ev.getTotalPages(), ev.getPageSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
// - note: at this point, the URI is transformed into plural (added `s`) in a hardcoded way - this will change in the future
|
// - note: at this point, the URI is transformed into plural (added `s`) in a hardcoded way - this will change in the future
|
||||||
final void addLinkHeaderOnPagedResourceRetrieval(final UriComponentsBuilder uriBuilder, final HttpServletResponse response, final Class clazz, final int page, final int totalPages, final int pageSize) {
|
final void addLinkHeaderOnPagedResourceRetrieval(final UriComponentsBuilder uriBuilder,
|
||||||
|
final HttpServletResponse response, final Class clazz, final int page, final int totalPages,
|
||||||
|
final int pageSize) {
|
||||||
plural(uriBuilder, clazz);
|
plural(uriBuilder, clazz);
|
||||||
|
|
||||||
final StringBuilder linkHeader = new StringBuilder();
|
final StringJoiner linkHeader = new StringJoiner(", ");
|
||||||
if (hasNextPage(page, totalPages)) {
|
if (hasNextPage(page, totalPages)) {
|
||||||
final String uriForNextPage = constructNextPageUri(uriBuilder, page, pageSize);
|
final String uriForNextPage = constructNextPageUri(uriBuilder, page, pageSize);
|
||||||
linkHeader.append(LinkUtil.createLinkHeader(uriForNextPage, LinkUtil.REL_NEXT));
|
linkHeader.add(LinkUtil.createLinkHeader(uriForNextPage, LinkUtil.REL_NEXT));
|
||||||
}
|
}
|
||||||
if (hasPreviousPage(page)) {
|
if (hasPreviousPage(page)) {
|
||||||
final String uriForPrevPage = constructPrevPageUri(uriBuilder, page, pageSize);
|
final String uriForPrevPage = constructPrevPageUri(uriBuilder, page, pageSize);
|
||||||
appendCommaIfNecessary(linkHeader);
|
linkHeader.add(LinkUtil.createLinkHeader(uriForPrevPage, LinkUtil.REL_PREV));
|
||||||
linkHeader.append(LinkUtil.createLinkHeader(uriForPrevPage, LinkUtil.REL_PREV));
|
|
||||||
}
|
}
|
||||||
if (hasFirstPage(page)) {
|
if (hasFirstPage(page)) {
|
||||||
final String uriForFirstPage = constructFirstPageUri(uriBuilder, pageSize);
|
final String uriForFirstPage = constructFirstPageUri(uriBuilder, pageSize);
|
||||||
appendCommaIfNecessary(linkHeader);
|
linkHeader.add(LinkUtil.createLinkHeader(uriForFirstPage, LinkUtil.REL_FIRST));
|
||||||
linkHeader.append(LinkUtil.createLinkHeader(uriForFirstPage, LinkUtil.REL_FIRST));
|
|
||||||
}
|
}
|
||||||
if (hasLastPage(page, totalPages)) {
|
if (hasLastPage(page, totalPages)) {
|
||||||
final String uriForLastPage = constructLastPageUri(uriBuilder, totalPages, pageSize);
|
final String uriForLastPage = constructLastPageUri(uriBuilder, totalPages, pageSize);
|
||||||
appendCommaIfNecessary(linkHeader);
|
linkHeader.add(LinkUtil.createLinkHeader(uriForLastPage, LinkUtil.REL_LAST));
|
||||||
linkHeader.append(LinkUtil.createLinkHeader(uriForLastPage, LinkUtil.REL_LAST));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linkHeader.length() > 0) {
|
if (linkHeader.length() > 0) {
|
||||||
|
@ -61,19 +63,35 @@ class PaginatedResultsRetrievedDiscoverabilityListener implements ApplicationLis
|
||||||
}
|
}
|
||||||
|
|
||||||
final String constructNextPageUri(final UriComponentsBuilder uriBuilder, final int page, final int size) {
|
final String constructNextPageUri(final UriComponentsBuilder uriBuilder, final int page, final int size) {
|
||||||
return uriBuilder.replaceQueryParam(PAGE, page + 1).replaceQueryParam("size", size).build().encode().toUriString();
|
return uriBuilder.replaceQueryParam(PAGE, page + 1)
|
||||||
|
.replaceQueryParam("size", size)
|
||||||
|
.build()
|
||||||
|
.encode()
|
||||||
|
.toUriString();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String constructPrevPageUri(final UriComponentsBuilder uriBuilder, final int page, final int size) {
|
final String constructPrevPageUri(final UriComponentsBuilder uriBuilder, final int page, final int size) {
|
||||||
return uriBuilder.replaceQueryParam(PAGE, page - 1).replaceQueryParam("size", size).build().encode().toUriString();
|
return uriBuilder.replaceQueryParam(PAGE, page - 1)
|
||||||
|
.replaceQueryParam("size", size)
|
||||||
|
.build()
|
||||||
|
.encode()
|
||||||
|
.toUriString();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String constructFirstPageUri(final UriComponentsBuilder uriBuilder, final int size) {
|
final String constructFirstPageUri(final UriComponentsBuilder uriBuilder, final int size) {
|
||||||
return uriBuilder.replaceQueryParam(PAGE, 0).replaceQueryParam("size", size).build().encode().toUriString();
|
return uriBuilder.replaceQueryParam(PAGE, 0)
|
||||||
|
.replaceQueryParam("size", size)
|
||||||
|
.build()
|
||||||
|
.encode()
|
||||||
|
.toUriString();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String constructLastPageUri(final UriComponentsBuilder uriBuilder, final int totalPages, final int size) {
|
final String constructLastPageUri(final UriComponentsBuilder uriBuilder, final int totalPages, final int size) {
|
||||||
return uriBuilder.replaceQueryParam(PAGE, totalPages).replaceQueryParam("size", size).build().encode().toUriString();
|
return uriBuilder.replaceQueryParam(PAGE, totalPages)
|
||||||
|
.replaceQueryParam("size", size)
|
||||||
|
.build()
|
||||||
|
.encode()
|
||||||
|
.toUriString();
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean hasNextPage(final int page, final int totalPages) {
|
final boolean hasNextPage(final int page, final int totalPages) {
|
||||||
|
@ -92,16 +110,11 @@ class PaginatedResultsRetrievedDiscoverabilityListener implements ApplicationLis
|
||||||
return (totalPages > 1) && hasNextPage(page, totalPages);
|
return (totalPages > 1) && hasNextPage(page, totalPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
final void appendCommaIfNecessary(final StringBuilder linkHeader) {
|
|
||||||
if (linkHeader.length() > 0) {
|
|
||||||
linkHeader.append(", ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// template
|
// template
|
||||||
|
|
||||||
protected void plural(final UriComponentsBuilder uriBuilder, final Class clazz) {
|
protected void plural(final UriComponentsBuilder uriBuilder, final Class clazz) {
|
||||||
final String resourceName = clazz.getSimpleName().toLowerCase() + "s";
|
final String resourceName = clazz.getSimpleName()
|
||||||
|
.toLowerCase() + "s";
|
||||||
uriBuilder.path("/auth/" + resourceName);
|
uriBuilder.path("/auth/" + resourceName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.baeldung.web.hateoas.listener;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.apache.http.HttpHeaders;
|
||||||
|
import com.baeldung.web.hateoas.event.ResourceCreatedEvent;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ResourceCreatedDiscoverabilityListener implements ApplicationListener<ResourceCreatedEvent> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(final ResourceCreatedEvent resourceCreatedEvent) {
|
||||||
|
Preconditions.checkNotNull(resourceCreatedEvent);
|
||||||
|
|
||||||
|
final HttpServletResponse response = resourceCreatedEvent.getResponse();
|
||||||
|
final long idOfNewResource = resourceCreatedEvent.getIdOfNewResource();
|
||||||
|
|
||||||
|
addLinkHeaderOnResourceCreation(response, idOfNewResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addLinkHeaderOnResourceCreation(final HttpServletResponse response, final long idOfNewResource) {
|
||||||
|
// final String requestUrl = request.getRequestURL().toString();
|
||||||
|
// final URI uri = new UriTemplate("{requestUrl}/{idOfNewResource}").expand(requestUrl, idOfNewResource);
|
||||||
|
|
||||||
|
final URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/{idOfNewResource}").buildAndExpand(idOfNewResource).toUri();
|
||||||
|
response.setHeader(HttpHeaders.LOCATION, uri.toASCIIString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.baeldung.web.util;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides some constants and utility methods to build a Link Header to be stored in the {@link HttpServletResponse} object
|
||||||
|
*/
|
||||||
|
public final class LinkUtil {
|
||||||
|
|
||||||
|
public static final String REL_COLLECTION = "collection";
|
||||||
|
public static final String REL_NEXT = "next";
|
||||||
|
public static final String REL_PREV = "prev";
|
||||||
|
public static final String REL_FIRST = "first";
|
||||||
|
public static final String REL_LAST = "last";
|
||||||
|
|
||||||
|
private LinkUtil() {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Link Header to be stored in the {@link HttpServletResponse} to provide Discoverability features to the user
|
||||||
|
*
|
||||||
|
* @param uri
|
||||||
|
* the base uri
|
||||||
|
* @param rel
|
||||||
|
* the relative path
|
||||||
|
*
|
||||||
|
* @return the complete url
|
||||||
|
*/
|
||||||
|
public static String createLinkHeader(final String uri, final String rel) {
|
||||||
|
return "<" + uri + ">; rel=\"" + rel + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.baeldung.web.util;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import com.baeldung.web.exception.MyResourceNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple static methods to be called at the start of your own methods to verify correct arguments and state. If the Precondition fails, an {@link HttpStatus} code is thrown
|
||||||
|
*/
|
||||||
|
public final class RestPreconditions {
|
||||||
|
|
||||||
|
private RestPreconditions() {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if some value was found, otherwise throw exception.
|
||||||
|
*
|
||||||
|
* @param expression
|
||||||
|
* has value true if found, otherwise false
|
||||||
|
* @throws MyResourceNotFoundException
|
||||||
|
* if expression is false, means value not found.
|
||||||
|
*/
|
||||||
|
public static void checkFound(final boolean expression) {
|
||||||
|
if (!expression) {
|
||||||
|
throw new MyResourceNotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if some value was found, otherwise throw exception.
|
||||||
|
*
|
||||||
|
* @param expression
|
||||||
|
* has value true if found, otherwise false
|
||||||
|
* @throws MyResourceNotFoundException
|
||||||
|
* if expression is false, means value not found.
|
||||||
|
*/
|
||||||
|
public static <T> T checkFound(final T resource) {
|
||||||
|
if (resource == null) {
|
||||||
|
throw new MyResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
server.port=8082
|
||||||
|
server.servlet.context-path=/spring-boot-rest
|
||||||
|
|
||||||
### Spring Boot default error handling configurations
|
### Spring Boot default error handling configurations
|
||||||
#server.error.whitelabel.enabled=false
|
#server.error.whitelabel.enabled=false
|
||||||
#server.error.include-stacktrace=always
|
#server.error.include-stacktrace=always
|
|
@ -0,0 +1,22 @@
|
||||||
|
## jdbc.X
|
||||||
|
#jdbc.driverClassName=com.mysql.jdbc.Driver
|
||||||
|
#jdbc.url=jdbc:mysql://localhost:3306/spring_hibernate4_01?createDatabaseIfNotExist=true
|
||||||
|
#jdbc.user=tutorialuser
|
||||||
|
#jdbc.pass=tutorialmy5ql
|
||||||
|
#
|
||||||
|
## hibernate.X
|
||||||
|
#hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
|
||||||
|
#hibernate.show_sql=false
|
||||||
|
#hibernate.hbm2ddl.auto=create-drop
|
||||||
|
|
||||||
|
|
||||||
|
# jdbc.X
|
||||||
|
jdbc.driverClassName=org.h2.Driver
|
||||||
|
jdbc.url=jdbc:h2:mem:security_permission;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||||
|
jdbc.user=sa
|
||||||
|
jdbc.pass=
|
||||||
|
|
||||||
|
# hibernate.X
|
||||||
|
hibernate.dialect=org.hibernate.dialect.H2Dialect
|
||||||
|
hibernate.show_sql=false
|
||||||
|
hibernate.hbm2ddl.auto=create-drop
|
|
@ -0,0 +1,10 @@
|
||||||
|
# jdbc.X
|
||||||
|
jdbc.driverClassName=com.mysql.jdbc.Driver
|
||||||
|
jdbc.url=jdbc:mysql://localhost:3306/spring_hibernate4_01?createDatabaseIfNotExist=true
|
||||||
|
jdbc.user=tutorialuser
|
||||||
|
jdbc.pass=tutorialmy5ql
|
||||||
|
|
||||||
|
# hibernate.X
|
||||||
|
hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
|
||||||
|
hibernate.show_sql=false
|
||||||
|
hibernate.hbm2ddl.auto=create-drop
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
http://www.springframework.org/schema/beans
|
||||||
|
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||||
|
http://www.springframework.org/schema/data/jpa
|
||||||
|
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"
|
||||||
|
>
|
||||||
|
|
||||||
|
<jpa:repositories base-package="com.baeldung.persistence.dao"/>
|
||||||
|
|
||||||
|
</beans>
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.baeldung;
|
||||||
|
|
||||||
|
public interface Consts {
|
||||||
|
int APPLICATION_PORT = 8082;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.baeldung.web;
|
package com.baeldung;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
|
@ -0,0 +1,103 @@
|
||||||
|
package com.baeldung.common.web;
|
||||||
|
|
||||||
|
import static com.baeldung.web.util.HTTPLinkHeaderUtil.extractURIByRel;
|
||||||
|
import static org.apache.commons.lang3.RandomStringUtils.randomNumeric;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import com.google.common.net.HttpHeaders;
|
||||||
|
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.response.Response;
|
||||||
|
|
||||||
|
public abstract class AbstractBasicLiveTest<T extends Serializable> extends AbstractLiveTest<T> {
|
||||||
|
|
||||||
|
public AbstractBasicLiveTest(final Class<T> clazzToSet) {
|
||||||
|
super(clazzToSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find - all - paginated
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenResourcesAreRetrievedPaged_then200IsReceived() {
|
||||||
|
create();
|
||||||
|
|
||||||
|
final Response response = RestAssured.get(getURL() + "?page=0&size=10");
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode(), is(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived() {
|
||||||
|
final String url = getURL() + "?page=" + randomNumeric(5) + "&size=10";
|
||||||
|
final Response response = RestAssured.get(url);
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode(), is(404));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources() {
|
||||||
|
create();
|
||||||
|
|
||||||
|
final Response response = RestAssured.get(getURL() + "?page=0&size=10");
|
||||||
|
|
||||||
|
assertFalse(response.body().as(List.class).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext() {
|
||||||
|
create();
|
||||||
|
create();
|
||||||
|
create();
|
||||||
|
|
||||||
|
final Response response = RestAssured.get(getURL() + "?page=0&size=2");
|
||||||
|
|
||||||
|
final String uriToNextPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "next");
|
||||||
|
assertEquals(getURL() + "?page=1&size=2", uriToNextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage() {
|
||||||
|
final Response response = RestAssured.get(getURL() + "?page=0&size=2");
|
||||||
|
|
||||||
|
final String uriToPrevPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "prev");
|
||||||
|
assertNull(uriToPrevPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious() {
|
||||||
|
create();
|
||||||
|
create();
|
||||||
|
|
||||||
|
final Response response = RestAssured.get(getURL() + "?page=1&size=2");
|
||||||
|
|
||||||
|
final String uriToPrevPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "prev");
|
||||||
|
assertEquals(getURL() + "?page=0&size=2", uriToPrevPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable() {
|
||||||
|
create();
|
||||||
|
create();
|
||||||
|
create();
|
||||||
|
|
||||||
|
final Response first = RestAssured.get(getURL() + "?page=0&size=2");
|
||||||
|
final String uriToLastPage = extractURIByRel(first.getHeader(HttpHeaders.LINK), "last");
|
||||||
|
|
||||||
|
final Response response = RestAssured.get(uriToLastPage);
|
||||||
|
|
||||||
|
final String uriToNextPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "next");
|
||||||
|
assertNull(uriToNextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// count
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.baeldung.common.web;
|
||||||
|
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.response.Response;
|
||||||
|
|
||||||
|
import static com.baeldung.Consts.APPLICATION_PORT;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import com.baeldung.test.IMarshaller;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.net.HttpHeaders;
|
||||||
|
|
||||||
|
public abstract class AbstractLiveTest<T extends Serializable> {
|
||||||
|
|
||||||
|
protected final Class<T> clazz;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IMarshaller marshaller;
|
||||||
|
|
||||||
|
public AbstractLiveTest(final Class<T> clazzToSet) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
Preconditions.checkNotNull(clazzToSet);
|
||||||
|
clazz = clazzToSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// template method
|
||||||
|
|
||||||
|
public abstract void create();
|
||||||
|
|
||||||
|
public abstract String createAsUri();
|
||||||
|
|
||||||
|
protected final void create(final T resource) {
|
||||||
|
createAsUri(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final String createAsUri(final T resource) {
|
||||||
|
final Response response = createAsResponse(resource);
|
||||||
|
Preconditions.checkState(response.getStatusCode() == 201, "create operation: " + response.getStatusCode());
|
||||||
|
|
||||||
|
final String locationOfCreatedResource = response.getHeader(HttpHeaders.LOCATION);
|
||||||
|
Preconditions.checkNotNull(locationOfCreatedResource);
|
||||||
|
return locationOfCreatedResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Response createAsResponse(final T resource) {
|
||||||
|
Preconditions.checkNotNull(resource);
|
||||||
|
|
||||||
|
final String resourceAsString = marshaller.encode(resource);
|
||||||
|
return RestAssured.given()
|
||||||
|
.contentType(marshaller.getMime())
|
||||||
|
.body(resourceAsString)
|
||||||
|
.post(getURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
protected String getURL() {
|
||||||
|
return "http://localhost:" + APPLICATION_PORT + "/spring-boot-rest/auth/foos";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.baeldung.spring;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ComponentScan("com.baeldung.test")
|
||||||
|
public class ConfigIntegrationTest implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
public ConfigIntegrationTest() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.baeldung.test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IMarshaller {
|
||||||
|
|
||||||
|
<T> String encode(final T entity);
|
||||||
|
|
||||||
|
<T> T decode(final String entityAsString, final Class<T> clazz);
|
||||||
|
|
||||||
|
<T> List<T> decodeList(final String entitiesAsString, final Class<T> clazz);
|
||||||
|
|
||||||
|
String getMime();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.baeldung.test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.baeldung.persistence.model.Foo;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
public final class JacksonMarshaller implements IMarshaller {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(JacksonMarshaller.class);
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public JacksonMarshaller() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
objectMapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final <T> String encode(final T resource) {
|
||||||
|
Preconditions.checkNotNull(resource);
|
||||||
|
String entityAsJSON = null;
|
||||||
|
try {
|
||||||
|
entityAsJSON = objectMapper.writeValueAsString(resource);
|
||||||
|
} catch (final IOException ioEx) {
|
||||||
|
logger.error("", ioEx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityAsJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final <T> T decode(final String resourceAsString, final Class<T> clazz) {
|
||||||
|
Preconditions.checkNotNull(resourceAsString);
|
||||||
|
|
||||||
|
T entity = null;
|
||||||
|
try {
|
||||||
|
entity = objectMapper.readValue(resourceAsString, clazz);
|
||||||
|
} catch (final IOException ioEx) {
|
||||||
|
logger.error("", ioEx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public final <T> List<T> decodeList(final String resourcesAsString, final Class<T> clazz) {
|
||||||
|
Preconditions.checkNotNull(resourcesAsString);
|
||||||
|
|
||||||
|
List<T> entities = null;
|
||||||
|
try {
|
||||||
|
if (clazz.equals(Foo.class)) {
|
||||||
|
entities = objectMapper.readValue(resourcesAsString, new TypeReference<List<Foo>>() {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
entities = objectMapper.readValue(resourcesAsString, List.class);
|
||||||
|
}
|
||||||
|
} catch (final IOException ioEx) {
|
||||||
|
logger.error("", ioEx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String getMime() {
|
||||||
|
return MediaType.APPLICATION_JSON.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.baeldung.test;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("test")
|
||||||
|
public class TestMarshallerFactory implements FactoryBean<IMarshaller> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Environment env;
|
||||||
|
|
||||||
|
public TestMarshallerFactory() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IMarshaller getObject() {
|
||||||
|
final String testMime = env.getProperty("test.mime");
|
||||||
|
if (testMime != null) {
|
||||||
|
switch (testMime) {
|
||||||
|
case "json":
|
||||||
|
return new JacksonMarshaller();
|
||||||
|
case "xml":
|
||||||
|
// If we need to implement xml marshaller we can include spring-rest-full XStreamMarshaller
|
||||||
|
throw new IllegalStateException();
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JacksonMarshaller();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<IMarshaller> getObjectType() {
|
||||||
|
return IMarshaller.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSingleton() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.baeldung.web;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||||
|
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
|
import org.springframework.test.context.support.AnnotationConfigContextLoader;
|
||||||
|
|
||||||
|
import com.baeldung.common.web.AbstractBasicLiveTest;
|
||||||
|
import com.baeldung.persistence.model.Foo;
|
||||||
|
import com.baeldung.spring.ConfigIntegrationTest;
|
||||||
|
|
||||||
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
|
@ContextConfiguration(classes = { ConfigIntegrationTest.class }, loader = AnnotationConfigContextLoader.class)
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
public class FooLiveTest extends AbstractBasicLiveTest<Foo> {
|
||||||
|
|
||||||
|
public FooLiveTest() {
|
||||||
|
super(Foo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void create() {
|
||||||
|
create(new Foo(randomAlphabetic(6)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String createAsUri() {
|
||||||
|
return createAsUri(new Foo(randomAlphabetic(6)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,19 +1,14 @@
|
||||||
package org.baeldung.web;
|
package com.baeldung.web;
|
||||||
|
|
||||||
|
import static com.baeldung.Consts.APPLICATION_PORT;
|
||||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||||
import static org.apache.commons.lang3.RandomStringUtils.randomNumeric;
|
import static org.apache.commons.lang3.RandomStringUtils.randomNumeric;
|
||||||
import static org.baeldung.Consts.APPLICATION_PORT;
|
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
import io.restassured.RestAssured;
|
|
||||||
import io.restassured.response.Response;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.baeldung.common.web.AbstractBasicLiveTest;
|
|
||||||
import org.baeldung.persistence.model.Foo;
|
|
||||||
import org.baeldung.spring.ConfigIntegrationTest;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
@ -21,6 +16,13 @@ import org.springframework.test.context.ContextConfiguration;
|
||||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||||
import org.springframework.test.context.support.AnnotationConfigContextLoader;
|
import org.springframework.test.context.support.AnnotationConfigContextLoader;
|
||||||
|
|
||||||
|
import com.baeldung.common.web.AbstractBasicLiveTest;
|
||||||
|
import com.baeldung.persistence.model.Foo;
|
||||||
|
import com.baeldung.spring.ConfigIntegrationTest;
|
||||||
|
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.response.Response;
|
||||||
|
|
||||||
@RunWith(SpringJUnit4ClassRunner.class)
|
@RunWith(SpringJUnit4ClassRunner.class)
|
||||||
@ContextConfiguration(classes = { ConfigIntegrationTest.class }, loader = AnnotationConfigContextLoader.class)
|
@ContextConfiguration(classes = { ConfigIntegrationTest.class }, loader = AnnotationConfigContextLoader.class)
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
|
@ -34,7 +36,7 @@ public class FooPageableLiveTest extends AbstractBasicLiveTest<Foo> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void create() {
|
public final void create() {
|
||||||
create(new Foo(randomAlphabetic(6)));
|
super.create(new Foo(randomAlphabetic(6)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -45,6 +47,8 @@ public class FooPageableLiveTest extends AbstractBasicLiveTest<Foo> {
|
||||||
@Override
|
@Override
|
||||||
@Test
|
@Test
|
||||||
public void whenResourcesAreRetrievedPaged_then200IsReceived() {
|
public void whenResourcesAreRetrievedPaged_then200IsReceived() {
|
||||||
|
this.create();
|
||||||
|
|
||||||
final Response response = RestAssured.get(getPageableURL() + "?page=0&size=10");
|
final Response response = RestAssured.get(getPageableURL() + "?page=0&size=10");
|
||||||
|
|
||||||
assertThat(response.getStatusCode(), is(200));
|
assertThat(response.getStatusCode(), is(200));
|
||||||
|
@ -70,7 +74,7 @@ public class FooPageableLiveTest extends AbstractBasicLiveTest<Foo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getPageableURL() {
|
protected String getPageableURL() {
|
||||||
return "http://localhost:" + APPLICATION_PORT + "/spring-rest-full/auth/foos/pageable";
|
return "http://localhost:" + APPLICATION_PORT + "/spring-boot-rest/auth/foos/pageable";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.baeldung.web;
|
||||||
|
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Suite;
|
||||||
|
|
||||||
|
@RunWith(Suite.class)
|
||||||
|
@Suite.SuiteClasses({
|
||||||
|
// @formatter:off
|
||||||
|
FooLiveTest.class
|
||||||
|
,FooPageableLiveTest.class
|
||||||
|
}) //
|
||||||
|
public class LiveTestSuiteLiveTest {
|
||||||
|
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.hasKey;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.isA;
|
import static org.hamcrest.Matchers.isA;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static com.baeldung.Consts.APPLICATION_PORT;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
@ -16,8 +17,8 @@ import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||||
|
|
||||||
public class ErrorHandlingLiveTest {
|
public class ErrorHandlingLiveTest {
|
||||||
|
|
||||||
private static final String BASE_URL = "http://localhost:8080";
|
private static final String BASE_URL = "http://localhost:" + APPLICATION_PORT + "/spring-boot-rest";
|
||||||
private static final String EXCEPTION_ENDPOINT = "/exception";
|
private static final String EXCEPTION_ENDPOINT = BASE_URL + "/exception";
|
||||||
|
|
||||||
private static final String ERROR_RESPONSE_KEY_PATH = "error";
|
private static final String ERROR_RESPONSE_KEY_PATH = "error";
|
||||||
private static final String XML_RESPONSE_KEY_PATH = "xmlkey";
|
private static final String XML_RESPONSE_KEY_PATH = "xmlkey";
|
||||||
|
@ -57,7 +58,7 @@ public class ErrorHandlingLiveTest {
|
||||||
try (WebClient webClient = new WebClient()) {
|
try (WebClient webClient = new WebClient()) {
|
||||||
webClient.getOptions()
|
webClient.getOptions()
|
||||||
.setThrowExceptionOnFailingStatusCode(false);
|
.setThrowExceptionOnFailingStatusCode(false);
|
||||||
HtmlPage page = webClient.getPage(BASE_URL + EXCEPTION_ENDPOINT);
|
HtmlPage page = webClient.getPage(EXCEPTION_ENDPOINT);
|
||||||
assertThat(page.getBody()
|
assertThat(page.getBody()
|
||||||
.asText()).contains("Whitelabel Error Page");
|
.asText()).contains("Whitelabel Error Page");
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.baeldung.web.util;
|
||||||
|
|
||||||
|
public final class HTTPLinkHeaderUtil {
|
||||||
|
|
||||||
|
private HTTPLinkHeaderUtil() {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
public static String extractURIByRel(final String linkHeader, final String rel) {
|
||||||
|
if (linkHeader == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String uriWithSpecifiedRel = null;
|
||||||
|
final String[] links = linkHeader.split(", ");
|
||||||
|
String linkRelation;
|
||||||
|
for (final String link : links) {
|
||||||
|
final int positionOfSeparator = link.indexOf(';');
|
||||||
|
linkRelation = link.substring(positionOfSeparator + 1, link.length()).trim();
|
||||||
|
if (extractTypeOfRelation(linkRelation).equals(rel)) {
|
||||||
|
uriWithSpecifiedRel = link.substring(1, positionOfSeparator - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uriWithSpecifiedRel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object extractTypeOfRelation(final String linkRelation) {
|
||||||
|
final int positionOfEquals = linkRelation.indexOf('=');
|
||||||
|
return linkRelation.substring(positionOfEquals + 2, linkRelation.length() - 1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ The "REST With Spring" Classes: http://bit.ly/restwithspring
|
||||||
The "Learn Spring Security" Classes: http://github.learnspringsecurity.com
|
The "Learn Spring Security" Classes: http://github.learnspringsecurity.com
|
||||||
|
|
||||||
### Relevant Articles:
|
### Relevant Articles:
|
||||||
- [REST Pagination in Spring](http://www.baeldung.com/rest-api-pagination-in-spring)
|
|
||||||
- [HATEOAS for a Spring REST Service](http://www.baeldung.com/rest-api-discoverability-with-spring)
|
- [HATEOAS for a Spring REST Service](http://www.baeldung.com/rest-api-discoverability-with-spring)
|
||||||
- [REST API Discoverability and HATEOAS](http://www.baeldung.com/restful-web-service-discoverability)
|
- [REST API Discoverability and HATEOAS](http://www.baeldung.com/restful-web-service-discoverability)
|
||||||
- [ETags for REST with Spring](http://www.baeldung.com/etags-for-rest-with-spring)
|
- [ETags for REST with Spring](http://www.baeldung.com/etags-for-rest-with-spring)
|
||||||
|
|
|
@ -3,8 +3,6 @@ package org.baeldung.persistence;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
|
|
||||||
public interface IOperations<T extends Serializable> {
|
public interface IOperations<T extends Serializable> {
|
||||||
|
|
||||||
// read - one
|
// read - one
|
||||||
|
@ -15,8 +13,6 @@ public interface IOperations<T extends Serializable> {
|
||||||
|
|
||||||
List<T> findAll();
|
List<T> findAll();
|
||||||
|
|
||||||
Page<T> findPaginated(int page, int size);
|
|
||||||
|
|
||||||
// write
|
// write
|
||||||
|
|
||||||
T create(final T entity);
|
T create(final T entity);
|
||||||
|
|
|
@ -2,13 +2,9 @@ package org.baeldung.persistence.service;
|
||||||
|
|
||||||
import org.baeldung.persistence.IOperations;
|
import org.baeldung.persistence.IOperations;
|
||||||
import org.baeldung.persistence.model.Foo;
|
import org.baeldung.persistence.model.Foo;
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
|
|
||||||
public interface IFooService extends IOperations<Foo> {
|
public interface IFooService extends IOperations<Foo> {
|
||||||
|
|
||||||
Foo retrieveByName(String name);
|
Foo retrieveByName(String name);
|
||||||
|
|
||||||
Page<Foo> findPaginated(Pageable pageable);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,6 @@ import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.baeldung.persistence.IOperations;
|
import org.baeldung.persistence.IOperations;
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@ -30,11 +28,6 @@ public abstract class AbstractService<T extends Serializable> implements IOperat
|
||||||
return Lists.newArrayList(getDao().findAll());
|
return Lists.newArrayList(getDao().findAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Page<T> findPaginated(final int page, final int size) {
|
|
||||||
return getDao().findAll(new PageRequest(page, size));
|
|
||||||
}
|
|
||||||
|
|
||||||
// write
|
// write
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -7,8 +7,6 @@ import org.baeldung.persistence.model.Foo;
|
||||||
import org.baeldung.persistence.service.IFooService;
|
import org.baeldung.persistence.service.IFooService;
|
||||||
import org.baeldung.persistence.service.common.AbstractService;
|
import org.baeldung.persistence.service.common.AbstractService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
@ -48,9 +46,4 @@ public class FooService extends AbstractService<Foo> implements IFooService {
|
||||||
return Lists.newArrayList(getDao().findAll());
|
return Lists.newArrayList(getDao().findAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Page<Foo> findPaginated(Pageable pageable) {
|
|
||||||
return dao.findAll(pageable);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,27 +6,20 @@ import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.baeldung.persistence.model.Foo;
|
import org.baeldung.persistence.model.Foo;
|
||||||
import org.baeldung.persistence.service.IFooService;
|
import org.baeldung.persistence.service.IFooService;
|
||||||
import org.baeldung.web.exception.MyResourceNotFoundException;
|
|
||||||
import org.baeldung.web.hateoas.event.PaginatedResultsRetrievedEvent;
|
|
||||||
import org.baeldung.web.hateoas.event.ResourceCreatedEvent;
|
import org.baeldung.web.hateoas.event.ResourceCreatedEvent;
|
||||||
import org.baeldung.web.hateoas.event.SingleResourceRetrievedEvent;
|
import org.baeldung.web.hateoas.event.SingleResourceRetrievedEvent;
|
||||||
import org.baeldung.web.util.RestPreconditions;
|
import org.baeldung.web.util.RestPreconditions;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.data.domain.Page;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
@ -72,30 +65,6 @@ public class FooController {
|
||||||
return service.findAll();
|
return service.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(params = { "page", "size" }, method = RequestMethod.GET)
|
|
||||||
@ResponseBody
|
|
||||||
public List<Foo> findPaginated(@RequestParam("page") final int page, @RequestParam("size") final int size, final UriComponentsBuilder uriBuilder, final HttpServletResponse response) {
|
|
||||||
final Page<Foo> resultPage = service.findPaginated(page, size);
|
|
||||||
if (page > resultPage.getTotalPages()) {
|
|
||||||
throw new MyResourceNotFoundException();
|
|
||||||
}
|
|
||||||
eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent<Foo>(Foo.class, uriBuilder, response, page, resultPage.getTotalPages(), size));
|
|
||||||
|
|
||||||
return resultPage.getContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/pageable")
|
|
||||||
@ResponseBody
|
|
||||||
public List<Foo> findPaginatedWithPageable(Pageable pageable, final UriComponentsBuilder uriBuilder, final HttpServletResponse response) {
|
|
||||||
final Page<Foo> resultPage = service.findPaginated(pageable);
|
|
||||||
if (pageable.getPageNumber() > resultPage.getTotalPages()) {
|
|
||||||
throw new MyResourceNotFoundException();
|
|
||||||
}
|
|
||||||
eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent<Foo>(Foo.class, uriBuilder, response, pageable.getPageNumber(), resultPage.getTotalPages(), pageable.getPageSize()));
|
|
||||||
|
|
||||||
return resultPage.getContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// write
|
// write
|
||||||
|
|
||||||
@RequestMapping(method = RequestMethod.POST)
|
@RequestMapping(method = RequestMethod.POST)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package org.baeldung.web.util;
|
package org.baeldung.web.util;
|
||||||
|
|
||||||
import org.baeldung.web.exception.MyResourceNotFoundException;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import org.baeldung.web.exception.MyResourceNotFoundException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple static methods to be called at the start of your own methods to verify correct arguments and state. If the Precondition fails, an {@link HttpStatus} code is thrown
|
* Simple static methods to be called at the start of your own methods to verify correct arguments and state. If the Precondition fails, an {@link HttpStatus} code is thrown
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,26 +1,19 @@
|
||||||
package org.baeldung.common.web;
|
package org.baeldung.common.web;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||||
import static org.apache.commons.lang3.RandomStringUtils.randomNumeric;
|
|
||||||
import static org.baeldung.web.util.HTTPLinkHeaderUtil.extractURIByRel;
|
|
||||||
import static org.hamcrest.Matchers.is;
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import io.restassured.RestAssured;
|
|
||||||
import io.restassured.response.Response;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.response.Response;
|
||||||
|
|
||||||
public abstract class AbstractBasicLiveTest<T extends Serializable> extends AbstractLiveTest<T> {
|
public abstract class AbstractBasicLiveTest<T extends Serializable> extends AbstractLiveTest<T> {
|
||||||
|
|
||||||
public AbstractBasicLiveTest(final Class<T> clazzToSet) {
|
public AbstractBasicLiveTest(final Class<T> clazzToSet) {
|
||||||
|
@ -104,71 +97,4 @@ public abstract class AbstractBasicLiveTest<T extends Serializable> extends Abst
|
||||||
// find - one
|
// find - one
|
||||||
|
|
||||||
// find - all
|
// find - all
|
||||||
|
|
||||||
// find - all - paginated
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenResourcesAreRetrievedPaged_then200IsReceived() {
|
|
||||||
final Response response = RestAssured.get(getURL() + "?page=0&size=10");
|
|
||||||
|
|
||||||
assertThat(response.getStatusCode(), is(200));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived() {
|
|
||||||
final String url = getURL() + "?page=" + randomNumeric(5) + "&size=10";
|
|
||||||
final Response response = RestAssured.get(url);
|
|
||||||
|
|
||||||
assertThat(response.getStatusCode(), is(404));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources() {
|
|
||||||
create();
|
|
||||||
|
|
||||||
final Response response = RestAssured.get(getURL() + "?page=0&size=10");
|
|
||||||
|
|
||||||
assertFalse(response.body().as(List.class).isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext() {
|
|
||||||
final Response response = RestAssured.get(getURL() + "?page=0&size=2");
|
|
||||||
|
|
||||||
final String uriToNextPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "next");
|
|
||||||
assertEquals(getURL() + "?page=1&size=2", uriToNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage() {
|
|
||||||
final Response response = RestAssured.get(getURL() + "?page=0&size=2");
|
|
||||||
|
|
||||||
final String uriToPrevPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "prev");
|
|
||||||
assertNull(uriToPrevPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious() {
|
|
||||||
create();
|
|
||||||
create();
|
|
||||||
|
|
||||||
final Response response = RestAssured.get(getURL() + "?page=1&size=2");
|
|
||||||
|
|
||||||
final String uriToPrevPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "prev");
|
|
||||||
assertEquals(getURL() + "?page=0&size=2", uriToPrevPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable() {
|
|
||||||
final Response first = RestAssured.get(getURL() + "?page=0&size=2");
|
|
||||||
final String uriToLastPage = extractURIByRel(first.getHeader(HttpHeaders.LINK), "last");
|
|
||||||
|
|
||||||
final Response response = RestAssured.get(uriToLastPage);
|
|
||||||
|
|
||||||
final String uriToNextPage = extractURIByRel(response.getHeader(HttpHeaders.LINK), "next");
|
|
||||||
assertNull(uriToNextPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// count
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
import io.restassured.RestAssured;
|
|
||||||
import io.restassured.response.Response;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@ -18,6 +16,9 @@ import org.springframework.http.MediaType;
|
||||||
|
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.response.Response;
|
||||||
|
|
||||||
public abstract class AbstractDiscoverabilityLiveTest<T extends Serializable> extends AbstractLiveTest<T> {
|
public abstract class AbstractDiscoverabilityLiveTest<T extends Serializable> extends AbstractLiveTest<T> {
|
||||||
|
|
||||||
public AbstractDiscoverabilityLiveTest(final Class<T> clazzToSet) {
|
public AbstractDiscoverabilityLiveTest(final Class<T> clazzToSet) {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import org.junit.runners.Suite;
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
FooDiscoverabilityLiveTest.class
|
FooDiscoverabilityLiveTest.class
|
||||||
,FooLiveTest.class
|
,FooLiveTest.class
|
||||||
,FooPageableLiveTest.class
|
|
||||||
}) //
|
}) //
|
||||||
public class LiveTestSuiteLiveTest {
|
public class LiveTestSuiteLiveTest {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue