Vote3Resource.java

/*
 * Copyright © 2023-2025 The CTAN Team and individual authors
 *
 * This file is distributed under the 3-clause BSD license.
 * See file LICENSE for details.
 */
package org.ctan.site.resources.catalogue;

import java.time.LocalDateTime;

import org.ctan.site.domain.account.User;
import org.ctan.site.domain.account.User.PrintableUser;
import org.ctan.site.domain.catalogue.Pkg;
import org.ctan.site.domain.site.Vote;
import org.ctan.site.services.catalogue.VoteService;
import org.ctan.site.services.catalogue.VoteService.VoteSummaryTo;
import org.ctan.site.services.catalogue.VoteService.VoteTo;
import org.ctan.site.stores.PkgStore;
import org.ctan.site.stores.UserStore;
import org.ctan.site.stores.VoteStore;
import org.glassfish.jersey.media.multipart.FormDataParam;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.dropwizard.hibernate.UnitOfWork;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response.Status;
import lombok.NonNull;

/**
 * The class <code>Vote3Resource</code> contains the controller for the vote
 * resource.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
@Path("/3.0")
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class Vote3Resource {

    /**
     * The field <code>voteStore</code> contains the vote store.
     */
    private VoteStore voteStore;

    /**
     * The field <code>pkgStore</code> contains the package store.
     */
    private PkgStore pkgStore;

    /**
     * The field <code>userStore</code> contains the user store.
     */
    private UserStore userStore;

    /**
     * The field <code>voteService</code> contains the vote service.
     */
    private @NonNull VoteService voteService;

    /**
     * This is the constructor for <code>Vote3Resource</code>.
     *
     * @param voteStore the vote store
     * @param voteService the vote service
     * @param pkgStore the package store
     * @param userStore the user store
     */
    @SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
    public Vote3Resource(@NonNull VoteStore voteStore,
        @NonNull VoteService voteService, @NonNull PkgStore pkgStore,
        @NonNull UserStore userStore) {

        this.voteStore = voteStore;
        this.voteService = voteService;
        this.pkgStore = pkgStore;
        this.userStore = userStore;
    }

    /**
     * The method <code>deleteVote</code> provides means to delete a vote.
     *
     * @param pkg the name of the package
     * @param account the account name
     * @return <code>true</code> iff the deletion was successful
     */
    @DELETE
    @Path("/vote")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public boolean deleteVote(@FormDataParam("pkg") String pkg,
        @FormDataParam("account") String account) {

        Pkg p = pkgStore.getByKey(pkg);
        if (p == null) {
            throw new WebApplicationException("unknown package",
                Status.BAD_REQUEST);
        }
        if (account == null) {
            throw new WebApplicationException("undefined user",
                Status.BAD_REQUEST);
        }
        User user = userStore.getByAccount(account); // TODO a&a
        if (user == null) {
            throw new WebApplicationException("unknown user",
                Status.BAD_REQUEST);
        }
        Vote vote = voteStore.getByAccountAndPkg(user, p);
        if (vote == null) {
            throw new WebApplicationException("unknown vote",
                Status.BAD_REQUEST);
        }
        return voteStore.remove(vote);
    }

    /**
     * The method <code>listVotesByAccount</code> provides means to retrieve a
     * list of votes for a user.
     *
     * @param uid the user id
     * @param page the page
     * @param size the page size
     * @return a list of matching author summaries
     */
    @GET
    @Path("/votes/by/{user}")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public VoteSummaryTo listVotesByAccount(
        @PathParam("user") String uid,
        @DefaultValue("0") @QueryParam("page") long page,
        @DefaultValue("16") @QueryParam("size") long size) {

        if (page < 0L) {
            throw new WebApplicationException("negative page",
                Status.BAD_REQUEST);
        }
        if (size <= 0L) {
            throw new WebApplicationException("non-positive size",
                Status.BAD_REQUEST);
        }

        User user = userStore.getByAccount(uid);
        if (user == null) {
            throw new WebApplicationException(Status.NOT_FOUND);
        }
        return voteService.find(user, page, size);
    }

    /**
     * The method <code>listVotesByPkg</code> provides means to retrieve a list
     * of votes for a package.
     *
     * @param pkgKey the package name
     * @param page the page
     * @param size the page size
     * @return a list of matching author summaries
     */
    @GET
    @Path("/votes/{pkg}")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public VoteSummaryTo listVotesByPkg(
        @PathParam("pkg") String pkgKey,
        @DefaultValue("0") @QueryParam("page") long page,
        @DefaultValue("16") @QueryParam("size") long size) {

        if (page < 0L) {
            throw new WebApplicationException("negative page",
                Status.BAD_REQUEST);
        }
        if (size <= 0L) {
            throw new WebApplicationException("non-positive size",
                Status.BAD_REQUEST);
        }

        Pkg pkg = pkgStore.getByKey(pkgKey);
        if (pkg == null) {
            throw new WebApplicationException(Status.NOT_FOUND);
        }
        return voteService.find(pkg, page, size);
    }

    /**
     * The method <code>vote</code> provides means to update a vote.
     *
     * @param pkg the package name
     * @param rating the rating in the range [1, 5]
     * @param expertise the expertise in the range [0, 4]
     * @param comment the comment
     * @param account the account name of the user
     * @return the updated vote record
     */
    @POST
    @Path("/vote")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public VoteTo vote(@FormDataParam("pkg") String pkg,
        @FormDataParam("rating") int rating,
        @FormDataParam("expertise") int expertise,
        @FormDataParam("comment") String comment,
        @FormDataParam("account") String account) {

        Pkg p = pkgStore.getByKey(pkg);
        if (p == null) {
            throw new WebApplicationException("unknown package",
                Status.BAD_REQUEST);
        }
        User user = userStore.getByAccount(account); // TODO a&a
        if (user == null) {
            throw new WebApplicationException("unknown user",
                Status.BAD_REQUEST);
        }

        if (rating < 1 || rating > 5) {
            throw new WebApplicationException("invalid rating",
                Status.BAD_REQUEST);
        }
        if (expertise < 0 || expertise > 4) {
            throw new WebApplicationException("invalid expertise",
                Status.BAD_REQUEST);
        }

        Vote vote = voteStore.getByAccountAndPkg(user, p);
        if (vote == null) {
            vote = Vote.builder()
                .comment(comment)
                .pkg(p)
                .rating(rating)
                .expertise(expertise)
                .lastModified(LocalDateTime.now())
                .user(user)
                .build();
        } else {
            vote.setComment(comment);
            vote.setExpertise(expertise);
            vote.setRating(rating);
            vote.setLastModified(LocalDateTime.now());
        }

        vote = voteStore.save(vote);

        PrintableUser printable = user.getPrintable();
        return VoteTo.builder()
            .account(printable.getAccount())
            .comment(vote.getComment())
            .date(vote.getDate())
            .expertise(vote.getExpertise())
            .rating(vote.getRating())
            .user(printable.getName())
            .build();
    }
}