Introduction
Probably all of you who are reading this article are familiar with Spring Data Rest framework, which simplifies building REST like APIs. To enlist all of its features and advantages I would have to use hundreds of words, but all of that has already been written on many blogs around the internet. That is why I will focus on its two main limitations.
Filtering resources
One of the features of Spring Data Rest is exporting query methods as RESTful endpoints. That is awesome for simple cases eg. to supply your API with an endpoint to filter users by their username, you just have to write one line of code. Unfortunately those query methods are indivisible and cannot be combined with each other. That implies, that developers solving some complex cases, like queries with optional parameters, have to either write multiple query methods or write a custom method and export it with a controller.
Creating and updating resources
Model and view in application should be separated. Spring Data Rest handles that problem really good when it comes to GET
requests. It provides Projections mechanism which perfectly separates entities from a view. Unfortunately there is no similar feature for creating and updating resources.
OpenRest
To fill the gap described in the last two Introduction paragraphs I have created an extension to Spring Data Rest called OpenRest with basically two main features: exporting predicates, instead of full queries, that could be combined with each other by client at request time. Second feature, that OpenRest comes with is Data Transfer Objects for POST
, PUT
and PATCH
requests. Since Spring Data Rest is a great piece of code, one of my main principles while writing OpenRest was change as little as possible, and let users to switch it off and use basic features of Spring Data Rest when needed.
Usage example
The best way to present features of OpenRest is to do it with an sample application. I will explain only the most important parts of the library. Everything else you could find in OpenRest documentation on https://github.com/konik32/openrest. Let's build a simple app for managing clients of sample company with departments.
Configuration
To enable OpenRest features you have to annotate your main configuration class with @EnableOpenRest
.
@SpringBootApplication
@EnableOpenRest
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
Model
@Embeddable
public class Address {
private String city;
private String street;
private String zip;
private String homeNr;
}
@Embeddable
public class CompanyData {
private String nip;
private String regon;
private String krs;
}
@Table(name = "contactPersons")
@Entity
public class ContactPerson extends AbstractPersistable<Long> {
private String name;
private String surname;
private String email;
private String phoneNr;
}
@Table(name = "clients")
@Entity
public class Client extends AbstractPersistable<Long> {
private String name;
private String phoneNr;
@Embedded
private Address address;
@Embedded
private CompanyData companyData;
@ManyToOne
private Department department;
@ManyToMany
@JoinTable(...)
private Set<Product>products;
public void addProduct(Product product) {
...
}
}
@Table(name = "departments")
@Entity
public class Department extends AbstractPersistable<Long> {
private String name;
@Embedded
private Address address;
@OneToMany
private List<ContactPerson>contactPersons;
private Boolean active;
public void addContactPerson(ContactPersoncontactPerson) {...}
}
Repositories
To export entities as resources we have to create simple Spring Data Rest repositories interface and extend PredicateContextQueryDslRepository<Entity>
.
Data Transfer Objects
In OpenRest creating and updating resources is done through DTO
s. We declare a classes with fields that will be set from content of POST
, PUT
, PATCH
request. Entities' objects will be then created from/merged with DTO
by mapping fields with the same name (POST
, PUT
requests) or via getters/setters pairs (PATCH
requests) (other fields are omitted). Of course nesting DTO
s is possible.
@Data
@Dto(entityType = Client.class, name = "clientDto", type = DtoType.CREATE)
public class ClientDto {
private String name;
@Valid
private AddressDto address;
@Valid
@ValidateExpression("#{@validators.validateCompanyDataDto(dto.companyData)}")
private CompanyDataDto companyData;
private Department department;
}
If automatic mapping DTO
's fields with entity's fields is not enough and you need more manual control over the process you can declare custom creator/merger. All you have to do is to implement EntityFromDtoCreator<Entity,DTO>
interface and pass it's type to @Dto
annotation. For example:
@Data
@Dto(entityType = Address.class, name = "addressDto", type = DtoType.BOTH, entityCreatorType=AddressDtoCreator.class)
public class AddressDto {
@Pattern(regexp="^(.*)[ ]+(.*), ([0-9]{2}-[0-9]{3})[ ]+(.*)$")
private String address;
}
@Component
public class AddressDtoCreator implements EntityFromDtoCreator<Address, AddressDto> {
private static final Pattern ADDRESS_PATTERN = Pattern.compile("^(.*)[ ]+(.*), ([0-9]{2}-[0-9]{3})[ ]+(.*)$");
@Override
public Address create(AddressDto from, DtoInformation dtoInfo) {
Address address = new Address();
Matcher matcher = ADDRESS_PATTERN.matcher(from.getAddress().trim());
if (matcher.find()) {
address.setStreet(matcher.group(1));
address.setHomeNr(matcher.group(2));
address.setZip(matcher.group(3));
address.setCity(matcher.group(4));
return address;
}
return null;
}
}
After these four steps, and implementing the rest of DTO
s, we can create client resource with following JSON
request:
POST /clients?dto=clientDto
{
"name": "client 1",
"address": {
"address": "Krakowska 57, 33-300 Warszawa"
},
"companyData": {
"nip": "23232323",
"regon": "213123",
"krs": "123123"
},
"department": "/departments/1"
}
OpenRest handles multiple DTO
s for single entity, so we have to pass dto query parameter with the name of DTO
we want to use. Dto
parameter is required, when it is missing OpenRest throws an exception.
Now, let's look at ContactPerson
class. It is an entity that could be associated with many other entities. In this case the association would be unidirectional like in Department
entity. If we would like to create a ContactPerson
resource and connect it to its association we would have to make two requests or create a custom controller. In OpenRest we can make use of DTO
and event handlers to achieve above goal.
@Dto(entityType = ContactPerson.class, name = "contactPersonDto", type = DtoType.BOTH)
@Data
public class ContactPersonDto {
private String name;
private String surname;
private String email;
private String phoneNr;
}
@Getter
@Setter
@Dto(entityType = ContactPerson.class, name = "departmentContactPersonDto", type = DtoType.CREATE)
public class DepartmentContactPersonDto extends ContactPersonDto {
@NotNull
private Department department;
}
@RepositoryEventHandler(ContactPerson.class)
@Component
public class ContactPersonEventHandler {
@Autowired
private DepartmentRepositorydepartmentRepository;
@HandleAfterCreateWithDto(dto = DepartmentContactPersonDto.class)
public void addContactPersonToCounty(ContactPerson cp, DepartmentContactPersonDto dto) {
dto.getDepartment().addContactPerson(cp);
departmentRepository.save(dto.getDepartment());
}
}
POST /contactPersons?dto=departmentContactPersonDto
{
"name": "Jan",
"surname": "Kowalski",
"email": "jan.kowalski@example.com",
"department": "/departments/1"
}
To see how updating resources with DTO
s works we can analyze example of changing user password.
@Dto(entityType=User.class, type=DtoType.MERGE, name="updatePasswordDto")
@Data
public class UpdatePasswordDto {
private String password;
@ValidateExpression("#{@validators.validatePassword(dto.oldPassword)}")
private String oldPassword;
@ValidateExpression("#{dto.confirmPassword.equals(dto.password)}")
private String confirmPassword;
}
PATCH /users/1?dto=updatePasswordDto
{
"password": "newPassword",
"oldPassword": "password",
"confirmPassword": "newPassword"
}
At first glance OpenRest DTO
mechanism might seem to be against DRY principle. For simple cases it surely is, but for complex ones (eg. when you need additional fields to calculate, validate entity's fields) separating view from model has many advantages and sometimes is unavoidable.
Filtering resources
After creating some resources it's time to display filtered lists. Earlier we declared a client repository and since PredicateContextQueryDslRepository<Entity>
always comes in pair with ExpressionRepository
we have to declare it also.
@RepositoryRestResource(path = "clients")
public interface ClientRepository extends PagingAndSortingRepository<Client, Long>,PredicateContextQueryDslRepository<Client> {}
@ExpressionRepository(Client.class)
public class ClientExpressionRepository{}
Now we can display a paginated list of all clients by GET
request:
GET /clients?orest
If request is meant to be handled by OpenRest, it needs orest
query parameter. As a matter of fact requests without this parameter won't be accepted. The above request returns paginated list of all clients. Let's add a search method for clients that are supported by one of our company's departments.
@ExpressionRepository(Client.class)
public class ClientExpressionRepository{
@ExpressionMethod(searchMethod = true)
public BooleanExpression departmentIdEq(Long departmentId) {
return QClient.client.department.id.eq(departmentId);
}
}
And GET
request:
GET /clients/search/departmentIdEq(1)?orest
In our example one department handles clients in whole country. It would be nice to add some filters to find client in Cracow that name starts with "Media". All we need to do is to write two expression methods
@ExpressionMethod
public BooleanExpression cityEq(String city){
return QClient.client.address.city.eq(city);
}
@ExpressionMethod
public BooleanExpression nameStartsWith(String name){
return QClient.client.name.startsWith(name);
}
and do a GET
request concatenating names of predefined predicates with logical operators ;and; ;or;
GET /clients/search/departmentIdEq(1)?orest&filters=cityEq(Cracow);and;nameLike(Media)
other examples
GET /clients?orest&filters=cityEq(Cracow);or;nameLike(Media)
GET /clients?orest&filters=departmentIdEq(Cracow);or;nameLike(Media);and;cityEq(Cracow)
Our sample company has some closed departments. To filter them out from every request we can create a static filter.
@StaticFilter
@ExpressionMethod
public BooleanExpression active() {
return QDepartment.department.active.eq(true);
}
Since ExpressionRepositories
are beans, authorization of certain endpoints (eg. /clients/search/departmentIdEq(1)
) is easy. It could be done using @PreAuthorize
annotation added to ExpressionMethod
.
Conclusion
In this article I presented you some simple examples of how to use core features of OpenRest, but there are more of them. If I got your interest please visit https://github.com/konik32/openrest, read the documentation, clone example, experiment with it and leave me some feedback.