package svanimpe.reminders.resources;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Set;
import javax.annotation.Resource;
import javax.enterprise.context.RequestScoped;
import javax.imageio.ImageIO;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.transaction.Transactional;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import svanimpe.reminders.domain.List;
import svanimpe.reminders.domain.Role;
import svanimpe.reminders.domain.User;
import svanimpe.reminders.validation.OnPasswordUpdate;
import static svanimpe.reminders.util.Utilities.IMAGES_BASE_DIR;
import static svanimpe.reminders.util.Utilities.MAX_PROFILE_PICTURE_SIZE_IN_MB;
import static svanimpe.reminders.util.Utilities.mergeMessages;
@Path("users")
@Transactional(dontRollbackOn = {BadRequestException.class, ForbiddenException.class, NotFoundException.class})
@RequestScoped
public class Users
{
@PersistenceContext
private EntityManager em;
@Resource
private Validator validator;
@Context
private SecurityContext context;
@GET
@Produces(MediaType.APPLICATION_JSON)
public java.util.List<User> getAllUsers(@QueryParam("from") @DefaultValue("0") int from, @QueryParam("results") @DefaultValue("20") int results)
{
TypedQuery<User> q = em.createNamedQuery("User.findAll", User.class);
q.setFirstResult(from);
q.setMaxResults(results);
return q.getResultList();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addUser(User user)
{
Set<ConstraintViolation<User>> violations = validator.validate(user, OnPasswordUpdate.class);
if (!violations.isEmpty()) {
throw new BadRequestException(mergeMessages(violations));
}
if (em.find(User.class, user.getUsername()) != null) {
throw new BadRequestException("USER_USERNAME");
}
// Because adding users currently does not require authentication, we set the roles to
// Role.USER and do not allow new admins to be created.
user.getRoles().clear();
user.getRoles().add(Role.USER);
em.persist(user);
return Response.created(URI.create("/users/" + user.getUsername())).build();
}
@GET
@Path("{username}")
@Produces(MediaType.APPLICATION_JSON)
public User getUser(@PathParam("username") String username)
{
User user = em.find(User.class, username);
if (user == null) {
throw new NotFoundException();
}
return user;
}
@PUT
@Path("{username}")
@Consumes(MediaType.APPLICATION_JSON)
public void updateUser(@PathParam("username") String username, InputStream in)
{
User user = em.find(User.class, username);
if (user == null) {
throw new NotFoundException();
}
if (!context.getUserPrincipal().getName().equals(username) && !context.isUserInRole(Role.ADMINISTRATOR.name())) {
throw new ForbiddenException();
}
em.detach(user);
boolean passwordChanged = false;
try {
JsonObject userUpdate = Json.createReader(in).readObject();
if (userUpdate.containsKey("fullName")) {
if (userUpdate.isNull("fullName")) {
user.setFullName(null);
} else {
user.setFullName(userUpdate.getString("fullName"));
}
}
JsonArray roles = userUpdate.getJsonArray("roles");
if (roles != null) {
// Only admins can change roles.
if (!context.isUserInRole(Role.ADMINISTRATOR.name())) {
throw new ForbiddenException();
}
user.getRoles().clear();
for (int i = 0; i < roles.size(); i++) {
try {
Role role = Role.valueOf(roles.getString(i).toUpperCase());
user.getRoles().add(role);
} catch (IllegalArgumentException ex) {
// Invalid role name.
throw new BadRequestException("USER_ROLES");
}
}
}
if (userUpdate.containsKey("password")) {
user.setPassword(userUpdate.getString("password"));
passwordChanged = true;
}
} catch (JsonException | ClassCastException ex) {
// Invalid JSON or type mismatch.
throw new BadRequestException("JSON");
}
Set<ConstraintViolation<User>> violations;
if (passwordChanged) {
violations = validator.validate(user, OnPasswordUpdate.class);
} else {
violations = validator.validate(user);
}
if (!violations.isEmpty()) {
throw new BadRequestException(mergeMessages(violations));
}
em.merge(user);
}
@Inject
private Lists listsResource;
@DELETE
@Path("{username}")
public void removeUser(@PathParam("username") String username) throws IOException
{
User user = em.find(User.class, username);
if (user == null) {
throw new NotFoundException();
}
if (!context.getUserPrincipal().getName().equals(username) && !context.isUserInRole(Role.ADMINISTRATOR.name())) {
throw new ForbiddenException();
}
TypedQuery<List> q = em.createNamedQuery("List.findByOwner", List.class).setParameter("owner", user);
for (List list : q.getResultList()) {
listsResource.removeList(list.getId());
}
Files.deleteIfExists(IMAGES_BASE_DIR.resolve(username + ".png"));
em.remove(user);
}
@GET
@Path("{username}/picture")
@Produces("image/png")
public InputStream getProfilePicture(@PathParam("username") String username) throws IOException
{
User user = em.find(User.class, username);
if (user == null) {
throw new NotFoundException();
}
java.nio.file.Path path = IMAGES_BASE_DIR.resolve(user.getProfilePicture());
if (!Files.exists(path)) {
throw new InternalServerErrorException("Could not load profile picture " + user.getProfilePicture());
}
return Files.newInputStream(path);
}
@PUT
@Path("{username}/picture")
@Consumes({"image/jpeg", "image/png"})
public void setProfilePicture(@PathParam("username") String username, @HeaderParam("Content-Length") long fileSize, InputStream in) throws IOException
{
User user = em.find(User.class, username);
if (user == null) {
throw new NotFoundException();
}
if (!context.getUserPrincipal().getName().equals(username) && !context.isUserInRole(Role.ADMINISTRATOR.name())) {
throw new ForbiddenException();
}
// Make sure the file is not larger than the maximum allowed size.
if (fileSize > 1024 * 1024 * MAX_PROFILE_PICTURE_SIZE_IN_MB) {
throw new BadRequestException("USER_PICTURE");
}
BufferedImage image = ImageIO.read(in);
// Scale the image to 200px x 200px.
BufferedImage scaledImage = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = scaledImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(image, 0, 0, 200, 200, 0, 0, image.getWidth(), image.getHeight(), null);
g.dispose();
// Save the image. By default, {username}.png is used as the filename.
OutputStream out = Files.newOutputStream(IMAGES_BASE_DIR.resolve(username + ".png"), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
ImageIO.write(scaledImage, "png", out);
// Don't forget to set it.
user.setProfilePicture(username + ".png");
}
@DELETE
@Path("{username}/picture")
public void removeProfilePicture(@PathParam("username") String username) throws IOException
{
User user = em.find(User.class, username);
if (user == null) {
throw new NotFoundException();
}
if (!context.getUserPrincipal().getName().equals(username) && !context.isUserInRole(Role.ADMINISTRATOR.name())) {
throw new ForbiddenException();
}
Files.deleteIfExists(IMAGES_BASE_DIR.resolve(username + ".png"));
// Clearing the profile picture will reset it to the default profile picture.
user.setProfilePicture(null);
}
}