Click here to Skip to main content
15,212,504 members

OpenRest

Rate this:
0.00 (No votes)
Please Sign up or sign in to vote.
0.00 (No votes)
12 Sep 2015Apache
Spring Data Rest extension

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 DTOs. 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 DTOs 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 DTOs, 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 DTOs 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 DTOs 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.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0

Share

About the Author

No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
Article
Posted 12 Sep 2015

Tagged as

Stats

10.3K views
1 bookmarked