Author3Resource.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.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.ctan.site.CtanConfiguration.CtanConfig;
import org.ctan.site.domain.catalogue.Pkg;
import org.ctan.site.domain.catalogue.PkgCaption;
import org.ctan.site.services.util.ConfigUtils;
import org.ctan.site.stores.AuthorStore;
import org.ctan.site.stores.UserStore;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.dropwizard.hibernate.UnitOfWork;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.GET;
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.Builder;
import lombok.Getter;
import lombok.NonNull;

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

    /**
     * The class <code>AuthorListTo</code> contains the transport object for the
     * author list.
     */
    @Getter
    @Builder
    protected static class AuthorListTo {

        private String name;

        private String key;

        private String gender;

        private String text;
    }

    /**
     * The class <code>AuthorSummaryTo</code> contains the transport object for
     * the author resource in the summary list.
     */
    @Getter
    @Builder
    protected static class AuthorSummaryTo {

        private String name;

        private String nameLex;

        private String key;

        private String gender;

        private boolean died;

        private String text;

        private int packages;
    }

    /**
     * The class <code>AuthorTo</code> contains the transport object for the
     * author resource.
     */
    @Getter
    @Builder
    @SuppressFBWarnings(value = "EI_EXPOSE_REP")
    protected static class AuthorTo {

        private String name;

        private String key;

        private String gender;

        private boolean died;

        private String account;

        private List<PkgSummaryTo> packages;
    }

    /**
     * The class <code>PkgSummaryTo</code> contains the transport object for the
     * package of the author resource.
     */
    @Getter
    @Builder
    @SuppressFBWarnings(value = "EI_EXPOSE_REP")
    protected static class PkgSummaryTo {

        private boolean active;

        private String key;

        private String name;

        private boolean obsolete;

        private String summary;

        private List<Map<String, String>> topics;
    }

    /**
     * The constant <code>GENDER_ORG</code> contains the String representation
     * for the gender x. This is used for groups or organisations.
     */
    protected static final String GENDER_ORG = "x";

    /**
     * The field <code>GENDER_MALE</code> contains the String representation for
     * the male gender.
     */
    protected static final String GENDER_MALE = "m";

    /**
     * The field <code>GENDER_FEMALE</code> contains the String representation
     * for the female gender.
     */
    protected static final String GENDER_FEMALE = "f";

    /**
     * The field <code>store</code> contains the underlying repository.
     */
    private AuthorStore store;

    /**
     * The field <code>config</code> contains the configuration.
     */
    private CtanConfig config;

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

    /**
     * This is the constructor for the class <code>Author3Resource</code>.
     *
     * @param config the configuration
     * @param store the underlying store
     * @param userStore the underlying user store
     */
    @SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
    public Author3Resource(@NonNull CtanConfig config,
        @NonNull AuthorStore store, @NonNull UserStore userStore) {

        this.config = config;
        this.store = store;
        this.userStore = userStore;
    }

    /**
     * The method <code>captionOrNull</code> provides means to check whether the
     * caption is present.
     *
     * @param pkg the package
     * @param locale the locale
     * @return the caption or <code>null</code>
     */
    private String captionOrNull(Pkg pkg, String locale) {

        PkgCaption caption = pkg.getCaption(locale, "en");
        return caption == null ? null : caption.getCaption();
    }

    /**
     * The method <code>getAuthorByKey</code> provides means to retrieve an
     * author.
     *
     * @param id the key of the author
     * @param lang the language
     * @return an author or {@code null}
     */
    @GET
    @Path("/author/{id}")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public AuthorTo getAuthorByKey(@NonNull @PathParam("id") String id,
        @QueryParam("lang") String lang) {

        try {
            lang = ConfigUtils.fallbackLanguage(config, lang);
        } catch (IllegalArgumentException e) {
            throw new WebApplicationException(e.getMessage(),
                Status.BAD_REQUEST);
        }
        var locale = lang;
        var author = store.getByKey(id);
        if (author == null) {
            throw new WebApplicationException(Status.NOT_FOUND);
        }
        var refs = author.getRefs();
        List<PkgSummaryTo> packages = refs.stream()
            .map(r -> PkgSummaryTo.builder()
                .active(r.isActive())
                .key(r.getPkg().getKey())
                .name(r.getPkg().getName())
                .summary(captionOrNull(r.getPkg(), locale))
                .obsolete(pathStartsWithObsolete(r.getPkg().getCtanPath()))
                .topics(r.getPkg().getTopics()
                    .stream()
                    .map(t -> Map.of("key", t.getKey(),
                        "name", t.getTitle(locale)))
                    .collect(Collectors.toList()))
                .build())
            .collect(Collectors.toList());
        var email = author.getEmail();
        var user = email == null ? null : userStore.getByEmail(email);
        return AuthorTo.builder()
            .account(user == null ? null : user.getAccount())
            .name(author.toString())
            .key(author.getKey())
            .died(author.getDied())
            .gender(author.getGender().getValue())
            .packages(packages)
            .build();
    }

    /**
     * The method <code>getAuthors</code> provides means to retrieve a list of
     * authors starting with a given pattern.
     *
     * @param pattern the query string as part of the name -- case-insensitive
     * @param lang the language code
     * @return a list of matching author summaries
     */
    @GET
    @Path("/authors/list/{pattern}")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public List<AuthorSummaryTo> getAuthors(
        @PathParam("pattern") String pattern,
        @QueryParam("lang") String lang) {

        // try {
        // lang = ConfigUtils.fallbackLanguage(config, lang);
        // } catch (IllegalArgumentException e) {
        // throw new WebApplicationException(e.getMessage(),
        // Status.BAD_REQUEST);
        // }
        var authors =
            pattern == null || pattern.length() == 0
                ? store.findAll()
                : store.findAllByNameStartingWith(pattern);
        return authors
            .stream()
            .map(it -> {
                return AuthorSummaryTo.builder()
                    .name(it.toString())
                    .nameLex(it.toLexString())
                    .key(it.getKey())
                    .died(it.getDied())
                    .gender(it.getGender().getValue())
                    .text(it.getSortText())
                    .packages(it.getRefs().size())
                    .build();
            })
            .collect(Collectors.toList());
    }

    /**
     * The method <code>getAuthorsCount</code> provides means to retrieve the
     * total number of authors.
     *
     * @return the number of authors
     */
    @GET
    @Path("/authors/count")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public Long getAuthorsCount() {

        return store.count();
    }

    /**
     * The method <code>pathStartsWithObsolete</code> provides means to check
     * whether a string starts with the obsolete directory.
     *
     * @param path the path
     * @return {@code true} iff the path is not {@code null} and starts with
     *     "/obsolete"
     */
    private boolean pathStartsWithObsolete(String path) {

        return path == null ? false : path.startsWith("/obsolete");
    }
}