UserStore.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.stores;

import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.ctan.site.domain.Gender;
import org.ctan.site.domain.account.Role;
import org.ctan.site.domain.account.User;
import org.ctan.site.stores.base.GeneralPage;
import org.hibernate.SessionFactory;

import io.dropwizard.hibernate.AbstractDAO;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Root;

/**
 * The class <code>UserStore</code> contains the repository for users.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
public class UserStore extends AbstractDAO<User> {

    /**
     * This is the constructor for the <code>UserStore</code>.
     *
     * @param sessionFactory the session factory
     */
    public UserStore(SessionFactory sessionFactory) {

        super(sessionFactory);
    }

    /**
     * The method <code>getByAccount</code> provides means to find an user by
     * its account name.
     *
     * @param account the account name
     * @return the user or {@code null}
     */
    public User getByAccount(String account) {

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<User> user = query.from(User.class);
        query.where(cb.equal(user.get("account"), account));

        return uniqueResult(query);
    }

    /**
     * The method <code>getByEmail</code> provides means to find an user by its
     * email.
     *
     * @param email the email
     * @return the user or {@code null}
     */
    public User getByEmail(String email) {

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<User> user = query.from(User.class);
        query.where(cb.equal(user.get("email"), email.toLowerCase()));

        return uniqueResult(query);
    }

    /**
     * The method <code>get</code> provides means to find an user by its id.
     *
     * @param id the is
     * @return the user or {@code null}
     */
    public User getById(Long id) {

        return get(id);
    }

    /**
     * The method <code>list</code> provides means to extract a page of items.
     *
     * @param term the search term
     * @param page the current page
     * @param pageSize the page size
     * @param orderBy the name or the column to sort by
     * @param asc the indicator for ascending/descending sort order
     * @return a page with the results
     */
    public GeneralPage list(String term, int page, int pageSize,
            String orderBy, boolean asc) {

        if (page < 0 || pageSize < 1) {
            return null;
        }
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        var query = criteriaQuery();
        Root<User> user = query.from(User.class);
        if (term != null && !term.isBlank()) {
            var t = "%" + term.toLowerCase() + "%";
            query.where(
                cb.or(
                    cb.like(cb.lower(user.get("account")), t),
                    cb.like(cb.lower(user.get("name")), t),
                    cb.like(cb.lower(user.get("email")), t)));
        }
        if (orderBy != null && !orderBy.isBlank()) {
            if (asc) {
                query.orderBy(cb.asc(user.get(orderBy)));
            } else {
                query.orderBy(cb.desc(user.get(orderBy)));
            }
        }

        var hits = list(query);
        var hitCount = hits.size();
        var list = hits
            .subList(Math.min(page * pageSize, hitCount),
                Math.min((page + 1) * pageSize, hitCount))
            .stream()
            .map(u -> Map.of("account", u.getAccount(),
                "name", u.getName(),
                "email", u.getEmail(),
                "enabled", u.getEnabled(),
                "locked", u.getAccountLocked(),
                "authorKey", u.getAuthorKey() != null ? u.getAuthorKey() : "",
                "roles", u.getRoles().stream()
                    .map(r -> r.toString())
                    .collect(Collectors.toList())))
            .collect(Collectors.toList());
        return GeneralPage.builder()
            .size(1)
            .list(list)
            .build();
    }

    /**
     * The method <code>remove</code> provides means to delete an account.
     *
     * @param user the account instance
     */
    public void remove(User user) {

        currentSession().remove(user);
    }

    /**
     * The method <code>removeByAccount</code> provides means to delete an
     * account by account name.
     *
     * @param account the account name
     * @return {@code true} iff the account has been found
     */
    public boolean removeByAccount(String account) {

        var user = getByAccount(account);
        if (user == null) {
            return false;
        }
        currentSession().remove(user);
        return true;
    }

    /**
     * The method <code>save</code> provides means to store a user in the
     * database.
     *
     * @param user the user
     * @return the user
     */
    public User save(User user) {

        currentSession().persist(user);
        return user;
    }

    /**
     * The method <code>set</code> provides means to alter a single attribute
     * and store the user instance.
     *
     * @param account the account
     * @param key the name of the attribute
     * @param value the string representation of the value
     * @return {@code true} iff the attribute has been changed
     */
    public boolean set(String account, String key, String value) {

        var user = getByAccount(account);
        if (user == null) {
            return false;
        }
        String setter;
        switch (key) {
            case "accountExpired":
            case "accountLocked":
            case "enabled":
            case "htmlEmail":
            case "hyphenate":
            case "passwordExpired":
            case "showEmail":
            case "showName":
                setter = "set" + Character.toUpperCase(key.charAt(0))
                        + key.substring(1);
                try {
                    User.class
                        .getMethod(setter, String.class)
                        .invoke(user, Boolean.valueOf(value));
                } catch (NoSuchMethodException | SecurityException
                        | IllegalAccessException
                        | InvocationTargetException e) {
                    return false;
                }
                break;
            case "avatar":
                return false;
            case "dateCreated":
                return false;
            case "gender":
                user.setGender(Gender.valueOf(value.toUpperCase()));
                break;
            case "roles":
                Set<Role> roles = new HashSet<>();
                for (var r : value.split("[,:;]")) {
                    roles.add(Role.valueOf(r));
                }
                user.setRoles(roles);
                break;
            case "account":
            case "authorKey":
            case "avatarType":
            case "email":
            case "location":
            case "name":
            case "selfDescription":
            case "skin":
                setter = "set" + Character.toUpperCase(key.charAt(0))
                        + key.substring(1);
                try {
                    User.class
                        .getMethod(setter, String.class)
                        .invoke(user, value);
                } catch (NoSuchMethodException | SecurityException
                        | IllegalAccessException
                        | InvocationTargetException e) {
                    return false;
                }
                break;
            // case "password":
            default:
                return false;
        }
        save(user);
        return true;
    }

}