AuthorStore.java

/*
 * Copyright © 2021-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.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.ctan.site.domain.Gender;
import org.ctan.site.domain.catalogue.Author;
import org.ctan.site.domain.catalogue.Author.NameFormat;
import org.ctan.site.domain.catalogue.AuthorRef;
import org.ctan.site.services.search.base.IndexType;
import org.ctan.site.services.search.base.IndexingSession;
import org.ctan.site.stores.base.AbstractIndexingStore;
import org.ctan.site.stores.base.GeneralPage;
import org.hibernate.SessionFactory;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Root;
import lombok.NonNull;

/**
 * The class <code>AuthorStore</code> contains the repository for authors.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
public class AuthorStore extends AbstractIndexingStore<Author> {

    /**
     * This is the constructor for the <code>AuthorStore</code>.
     *
     * @param sessionFactory the session factory
     * @param indexingSession the indexing session
     */
    public AuthorStore(SessionFactory sessionFactory,
        IndexingSession indexingSession) {

        super(sessionFactory, indexingSession);
    }

    /**
     * The method <code>findAll</code> provides means to retrieve authors.
     *
     * @return the list of authors sorted by key
     */
    @Override
    public List<Author> findAll() {

        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        var query = criteriaQuery();
        Root<Author> author = query.from(Author.class);
        query.orderBy(cb.asc(author.get("key")));
        return list(query);
    }

    /**
     * The method <code>findAllByKeyStartingWith</code> provides means to
     * retrieve authors where the key is starting with a given string. The
     * comparison is done case-insensitive.
     *
     * @param s the initial segment
     * @return the list of authors ordered by the key
     */
    public List<Author> findAllByKeyStartingWith(String s) {

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<Author> author = query.from(Author.class);
        query.where(cb.like(author.get("key"), s.toLowerCase() + "%"))
            .orderBy(cb.asc(author.get("key")));
        return list(query);
    }

    /**
     * The method <code>findAllByNameContaining</code> provides means to search
     * for authors.
     *
     * @param s the string contained in the name
     * @return the list of authors ordered by the key
     */
    public List<Author> findAllByNameContaining(String s) {

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<Author> author = query.from(Author.class);
        query.where(cb.like(author.get("sortKey"), "%" + s.toLowerCase() + "%"))
            .orderBy(cb.asc(author.get("sortKey")));
        return list(query);
    }

    /**
     * The method <code>findAllByNameStartingWith</code> provides means to
     * retrieve authors where the sort text is starting with a given string. The
     * comparison is done case-insensitive.
     *
     * @param s the initial segment
     * @return the list of authors ordered by the sort text
     */
    public List<Author> findAllByNameStartingWith(String s) {

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<Author> author = query.from(Author.class);
        if (s != null && !"".equals(s)) {
            query.where(cb.like(author.get("sortText"), s.toLowerCase() + "%"));
        }
        query.orderBy(cb.asc(author.get("sortText")));
        return list(query);
    }

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

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<Author> author = query.from(Author.class);
        query.where(cb.equal(author.get("email"), email.toLowerCase()));
        return uniqueResult(query);
    }

    /**
     * The method <code>getByKey</code> provides means to find an author by its
     * key.
     *
     * @param key the key
     * @return the author or {@code null}
     */
    public Author getByKey(String key) {

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<Author> author = query.from(Author.class);
        Join<Author, AuthorRef> refs = author.join("refs", JoinType.LEFT);
        refs.join("pkg", JoinType.LEFT);
        // conditions.add(cb.equal(pkg.get("id"), associateId));
        // conditions.add(cb.isNull(refs.get("ack_date")));
        query.where(cb.equal(author.get("key"), key));
        return uniqueResult(query);
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.stores.base.AbstractIndexingStore#indexType()
     */
    @Override
    protected IndexType indexType() {

        return IndexType.AUTHORS;
    }

    /**
     * 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
     * @return the paged results
     */
    @Override
    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<Author> author = query.from(Author.class);
        if (term != null && !term.isBlank()) {
            var t = "%" + term.toLowerCase() + "%";
            query.where(
                cb.or(
                    cb.like(cb.lower(author.get("key")), t),
                    cb.like(cb.lower(author.get("name")), t),
                    cb.like(cb.lower(author.get("email")), t)));
        }
        if (orderBy != null && !orderBy.isBlank()) {
            if (asc) {
                query.orderBy(cb.asc(author.get(orderBy)));
            } else {
                query.orderBy(cb.desc(author.get(orderBy)));
            }
        }
        var hits = list(query);
        var hitCount = hits.size();
        List<Map<String, Object>> list = hits
            .subList(Math.min(page * pageSize, hitCount),
                Math.min((page + 1) * pageSize, hitCount))
            .stream()
            .map(a -> a.toMap())
            .collect(Collectors.toList());
        return GeneralPage.builder()
            .size(1)
            .list(list)
            .build();
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.stores.base.AbstractStore#listQuery(java.lang.String,
     *     jakarta.persistence.criteria.CriteriaBuilder,
     *     jakarta.persistence.criteria.CriteriaQuery)
     */
    @Override
    protected Root<Author> listQuery(String term, CriteriaBuilder cb,
        CriteriaQuery<Author> query) {

        // TODO unimplemented
        throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.stores.base.AbstractStore#map(java.util.List)
     */
    @Override
    protected List<Map<String, Object>> map(List<Author> list) {

        return list
            .stream()
            .map(it -> it.toMap())
            .collect(Collectors.toList());
    }

    /**
     * The method <code>set</code> provides means to set a single attribute to a
     * new value.
     *
     * @param key the key
     * @param attribute the attribute name
     * @param value the attribute value
     * @return {@code true} iff the change has been saved
     */
    public Author set(@NonNull String key, String attribute, String value) {

        var author = getByKey(key);
        if (author == null) {
            throw new IllegalArgumentException();
        }
        switch (attribute) {
            case "familyname":
                author.setFamilyname(value);
                break;
            case "key":
                author.setKey(value);
                break;
            case "format":
                author.setFormat(NameFormat.valueOf(value.toUpperCase()));
                break;
            case "givenname":
                author.setGivenname(value);
                break;
            case "von":
                author.setVon(value);
                break;
            case "junior":
                author.setJunior(value);
                break;
            case "title":
                author.setTitle(value);
                break;
            case "pseudonym":
                author.setPseudonym(value);
                break;
            case "died":
                author.setDied("true".equals(value));
                break;
            case "gender":
                author.setGender(Gender.of(value));
                break;
            // case "refs":
            // break;
            // case "emails":
            // author.setEmails(value.split("[,; ]+"));
            // break;
            default:
                throw new IllegalArgumentException(attribute);
        }
        return save(author);
    }
}