I make a REST webservice with SpringBoot web and the Morphia DAO (to use MongoDB).
As I did when I used Hibernate on MySQL, I would like to use Generic entities, repositories and endpoints so that I just have to set my entities, inherit Repositories and Services and let use the generated CRUD with REST calls.
It almost done, but I encounter a problem with the generic update of my entities with Morphia. Everything I've seen so far talks about manually setting a request with the fields that have to change ; but in the Hibernate way, we just setted the Id field, called persist() and it automatically knowed what changed and applied changes in database.
Here is some code.
BaseEntity.java
package org.beep.server.entity;
import org.mongodb.morphia.annotations.Entity;
@Entity
abstract public class BaseEntity {
public static class JsonViewContext {
public interface Summary {}
public interface Detailed extends Summary{}
}
protected String id;
public void setId(String id) {
this.id = id;
}
}
User.java (one of my final entities)
package org.beep.server.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.*;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import org.mongodb.morphia.annotations.*;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.ws.rs.FormParam;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
@Entity
@Indexes(
@Index(value="identifier", fields=@Field("email"))
)
@Builder
@NoArgsConstructor
@AllArgsConstructor
final public class User extends BaseEntity {
/**
* User's id
*/
@Id
@JsonView(JsonViewContext.Summary.class)
private String id;
/**
* User's email address
*/
@Getter @Setter
@JsonView(JsonViewContext.Summary.class)
@FormParam("email")
@Indexed
@Email
private String email;
/**
* User's hashed password
*/
@Getter
@JsonView(JsonViewContext.Detailed.class)
@FormParam("password")
@NotEmpty
private String password;
/**
* Sets the password after having hashed it
* @param clearPassword The clear password
*/
public void setPassword(String clearPassword) throws NoSuchAlgorithmException, InvalidKeySpecException {
PasswordEncoder encoder = new BCryptPasswordEncoder();
String hashedPassword = encoder.encode(clearPassword);
setHashedPassword(hashedPassword);
}
/**
* Directly sets the hashed password, whithout hashing it
* @param hashedPassword The hashed password
*/
protected void setHashedPassword(String hashedPassword) {
this.password = hashedPassword;
}
/**
* Converts the user to a UserDetail spring instance
*/
public UserDetails toUserDetails() {
return new org.springframework.security.core.userdetails.User(
getEmail(),
getPassword(),
true,
true,
true,
true,
AuthorityUtils.createAuthorityList("USER")
);
}
}
EntityRepository.java (my base repository, inheriting from the Morphia one)
package org.beep.server.repository;
import org.beep.server.entity.BaseEntity;
import org.bson.types.ObjectId;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.dao.BasicDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
public class EntityRepository<Entity extends BaseEntity> extends BasicDAO<Entity, ObjectId> {
@Autowired
protected EntityRepository(Datastore ds) {
super(ds);
}
}
UserRepository.java (my user repository)
package org.beep.server.repository;
import org.beep.server.entity.User;
import org.bson.types.ObjectId;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.dao.BasicDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository extends EntityRepository<User> {
@Autowired
protected UserRepository(Datastore ds) {
super(ds);
}
}
EntityService.java (the generic service, used from the Rest endpoints)
package org.beep.server.service;
import org.beep.server.entity.BaseEntity;
import org.beep.server.exception.EntityNotFoundException;
import org.beep.server.exception.UserEmailAlreadyExistsException;
import org.beep.server.repository.EntityRepository;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.query.Query;
import org.mongodb.morphia.query.UpdateOperations;
import java.util.List;
public abstract class EntityService<Entity extends BaseEntity, Repository extends EntityRepository> implements ServiceInterface<Entity> {
protected Repository repository;
public EntityService(Repository repository) {
this.repository = repository;
}
/**
* {@inheritDoc}
*/
public Entity create(Entity entity) throws UserEmailAlreadyExistsException {
repository.save(entity);
return entity;
}
/**
* {@inheritDoc}
*/
public void delete(String id) throws EntityNotFoundException {
//repository.deleteById(id).;
}
/**
* {@inheritDoc}
*/
public List<Entity> findAll() {
return repository.find().asList();
}
/**
* {@inheritDoc}
*/
public Entity findOneById(String id) throws EntityNotFoundException {
return (Entity) repository.get(id);
}
/**
* {@inheritDoc}
*/
public Entity update(String id, Entity entity) {
// Try to get the old entity, and to set the Id on the inputed one
// But how can I merge the two ? If I persist like that, I will just have the modified fields, others
// will be set to null...
Entity oldEntity = (Entity) repository.get(id);
entity.setId(id);
repository.save(entity);
// Create update operations works, but I have to set the changing fields manually...
// not so compatible with generics !
/*final Query<Entity> updateSelection = repository.createQuery().filter("_id",id);
repository.createUpdateOperations().
repository.update(updateSelection,entity);*/
return entity;
}
}
UserService.java
package org.beep.server.service;
import org.beep.server.entity.Message;
import org.beep.server.entity.User;
import org.beep.server.exception.EntityNotFoundException;
import org.beep.server.exception.UserEmailAlreadyExistsException;
import org.beep.server.repository.UserRepository;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.Key;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.ws.rs.BadRequestException;
import java.util.List;
import java.util.Optional;
@Service
public class UserService extends EntityService<User, UserRepository> {
@Autowired
public UserService(UserRepository repository) {
super(repository);
}
}
RestResource.java (my base Rest Endpoint)
package org.beep.server.api.rest.v1;
import com.fasterxml.jackson.annotation.JsonView;
import org.beep.server.entity.BaseEntity;
import org.beep.server.entity.User;
import org.beep.server.entity.BaseEntity;
import org.beep.server.exception.EntityNotFoundException;
import org.beep.server.exception.UserEmailAlreadyExistsException;
import org.beep.server.service.EntityService;
import org.beep.server.service.ServiceInterface;
import org.beep.server.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
public class RestResource<Entity extends BaseEntity, Service extends EntityService> {
protected Service service;
// Default constructor private to avoid blank constructor
protected RestResource() {
this.service = null;
}
/**
* Creates an object
* @param object Object to create
* @return The newly created object
*/
@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
@JsonView(BaseEntity.JsonViewContext.Detailed.class)
Entity create(@RequestBody @Valid Entity object) throws UserEmailAlreadyExistsException {
return service.create(object);
}
/**
* Deletes an object from its id
* @param id Object to delete id
* @return The deleted object
* @throws EntityNotFoundException
*/
@RequestMapping(value = "{id}", method = RequestMethod.DELETE)
@JsonView(BaseEntity.JsonViewContext.Detailed.class)
User delete(@PathVariable("id") String id) throws EntityNotFoundException {
service.delete(id);
return new User();
}
/**
* Gets all the objects
* @return All the objects
*/
@RequestMapping(method = RequestMethod.GET)
@JsonView(BaseEntity.JsonViewContext.Summary.class)
List<Entity> findAll() {
return service.findAll();
}
/**
* Finds one object from its id
* @param id The object to find id
* @return The corresponding object
* @throws EntityNotFoundException
*/
@RequestMapping(value = "{id}", method = RequestMethod.GET)
@JsonView(BaseEntity.JsonViewContext.Detailed.class)
Entity findById(@PathVariable("id") String id) throws EntityNotFoundException {
return service.findOneById(id);
}
/**
* Updates an object
* @param object The object to update
* @return The updated object
*/
@RequestMapping(value = "{id}", method = RequestMethod.PUT)
@JsonView(BaseEntity.JsonViewContext.Detailed.class)
Entity update(@PathVariable String id, @RequestBody @Valid Entity object) {
return service.update(id, object);
}
/**
* Handles the EntityNotFound exception to return a pretty 404 error
* @param ex The concerned exception
*/
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_FOUND)
public void handleEntityNotFound(EntityNotFoundException ex) {
}
/**
* Handles the REST input validation exceptions to return a pretty 400 bad request error
* with more info
* @param ex The validation exception
* @return A pretty list of the errors in the form
*/
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public List<ObjectError> handleValidationFailed(MethodArgumentNotValidException ex) {
// TODO : Check and improve the return of this method according to the front
// The concept is to automatically bind the error dans the failed parameter
return ex.getBindingResult().getAllErrors();
}
}