TipsService.java

/*
 * Copyright © 2022-2026 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.services.site;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.ctan.site.domain.catalogue.Author;
import org.ctan.site.domain.catalogue.Pkg;
import org.ctan.site.domain.catalogue.PkgCaption;
import org.ctan.site.domain.catalogue.Topic;
import org.ctan.site.services.content.ContentService;
import org.ctan.site.services.content.ContentService.TeaserType;
import org.ctan.site.stores.AuthorStore;
import org.ctan.site.stores.PkgStore;
import org.ctan.site.stores.TopicStore;

import com.fasterxml.jackson.annotation.JsonInclude;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

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

    /**
     * The class <code>TipTo</code> contains the transport object for tips.
     */
    @Getter
    @Builder
    @AllArgsConstructor
    @SuppressFBWarnings(value = "EI_EXPOSE_REP")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public static class TipTo {

        private String details;

        private boolean hasTeaser;

        private String key;

        private long size;

        private String title;

        private String type;
    }

    /**
     * The enumeration <code>TipType</code> contains the possible typed of tips
     * which can be retrieved..
     *
     * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
     */
    public enum TipType {

        /**
         * The value <code>A</code> is the tip type for an author.
         */
        A {

            /**
             * {@inheritDoc}
             *
             * @see org.ctan.site.services.site.TipsService.TipType#selectItem(java.lang.String,
             *     java.util.List, java.util.List, java.util.List,
             *     org.ctan.site.services.content.ContentService)
             */
            @Override
            TipTo selectItem(String locale,
                List<Topic> topics,
                List<Pkg> packages,
                List<Author> authors, ContentService contentService) {

                Author author = authors.get(rnd.nextInt(authors.size()));
                String key = author.getKey();
                return TipTo.builder()
                    .details(null) // TODO
                    .hasTeaser(
                        contentService.hasTeaser(TeaserType.PKG, key))
                    .key(key)
                    // .size(author.getRefs().size())
                    .title(author.toString())
                    .type("A")
                    .build();
            }
        },
        /**
         * The value <code>P</code> is the tip type for a package.
         */
        P {

            /**
             * {@inheritDoc}
             *
             * @see org.ctan.site.services.site.TipsService.TipType#selectItem(java.lang.String,
             *     java.util.List, java.util.List, java.util.List,
             *     org.ctan.site.services.content.ContentService)
             */
            @Override
            TipTo selectItem(String locale,
                List<Topic> topics,
                List<Pkg> packages,
                List<Author> authors, ContentService contentService) {

                Pkg pkg =
                    packages.get(TipType.rnd.nextInt(packages.size()));
                String key = pkg.getKey();
                PkgCaption caption = pkg.getCaption(locale, "en");
                return TipTo.builder()
                    .details(caption == null ? null : caption.getCaption())
                    .hasTeaser(
                        contentService.hasTeaser(TeaserType.PKG, key))
                    .key(key)
                    .size(1)
                    .title(pkg.getName())
                    .type("P")
                    .build();
            }
        },
        /**
         * The value <code>T</code> is the tip type for a topic.
         */
        T {

            /**
             * {@inheritDoc}
             *
             * @see org.ctan.site.services.site.TipsService.TipType#selectItem(java.lang.String,
             *     java.util.List, java.util.List, java.util.List,
             *     org.ctan.site.services.content.ContentService)
             */
            @Override
            TipTo selectItem(String locale,
                List<Topic> topics,
                List<Pkg> packages,
                List<Author> authors, ContentService contentService) {

                Topic topic = topics.get(rnd.nextInt(topics.size()));
                String key = topic.getKey();
                return TipTo.builder()
                    .details(topic.getDetails(locale))
                    .hasTeaser(
                        contentService.hasTeaser(TeaserType.TOPIC, key))
                    .key(key)
                    .size(topic.getNumber())
                    .title(topic.getTitle(locale))
                    .type("T")
                    .build();
            }
        };

        /**
         * The field <code>rnd</code> contains the random number generator.
         */
        static Random rnd = new Random();

        /**
         * The method <code>of</code> provides means to translate a single
         * letter to a tip type.
         *
         * @param c the single character for the type
         * @return the tip type
         * @throws IllegalArgumentException in case the argument letter is not
         *     supported
         */
        static TipType of(char c) {

            return switch (c) {
                case 'A' -> A;
                case 'T' -> T;
                case 'P' -> P;
                default -> throw new IllegalArgumentException();
            };
        }

        /**
         * The method <code>of</code> provides means to translate a string into
         * an array of tip types.
         *
         * @param s the string containing the letters for the tip type
         * @return the array of the types
         * @throws IllegalArgumentException in case the argument letter is not
         *     supported
         */
        public static List<TipType> of(String s) {

            List<TipType> list = new ArrayList<TipsService.TipType>();
            for (char it : s.toCharArray()) {
                list.add(of(it));
            }

            return list;
        }

        /**
         * The method <code>selectItem</code> provides means to construct a
         * random item of the current type.
         *
         * @param locale the current language
         * @param topics the cached topics
         * @param packages the cached packages
         * @param authors the cached authors
         * @param contentService the content service
         * @return a proper item
         */
        abstract TipTo selectItem(String locale, List<Topic> topics,
            List<Pkg> packages, List<Author> authors,
            ContentService contentService);
    }

    /**
     * The field <code>contentService</code> contains the content service.
     */
    private ContentService contentService;

    /**
     * The field <code>authorStore</code> contains the author store.
     */
    private @NonNull AuthorStore authorStore;

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

    /**
     * The field <code>topicStore</code> contains the topic store.
     */
    private @NonNull TopicStore topicStore;

    /**
     * The field <code>topics</code> contains the cached list of all topics.
     */
    private List<Topic> topics = null;

    /**
     * The field <code>packages</code> contains the cached list of all packages.
     */
    private List<Pkg> packages = null;

    /**
     * The field <code>authors</code> contains the cache list of all authors.
     */
    private List<Author> authors = null;

    /**
     * This is the constructor for the class <code>TipsService</code>.
     *
     * @param authorStore the author store
     * @param pkgStore the package store
     * @param topicStore the topic store
     * @param contentService the content service
     */
    @SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
    public TipsService(AuthorStore authorStore,
        PkgStore pkgStore,
        TopicStore topicStore,
        ContentService contentService) {

        this.pkgStore = pkgStore;
        this.topicStore = topicStore;
        this.authorStore = authorStore;
        this.contentService = contentService;
    }

    /**
     * The method <code>clear</code> provides means to remove all instances of a
     * TipType from the list of TipTypes.
     *
     * @param types the types
     * @param it the type to be cleared
     * @return the adjusted types
     */
    private List<TipType> clear(List<TipType> types, TipType it) {

        List<TipType> list = new ArrayList<>();

        for (var t : types) {
            if (!t.equals(it)) {
                list.add(t);
            }
        }
        if (list.isEmpty()) {
            throw new IllegalStateException("types reduced to none");
        }
        return list;
    }

    /**
     * The method <code>initAuthorsCache</code> provides means to initialise the
     * cache if required and clear the types list if no entry is present.
     *
     * @param types the types
     * @return the adjusted types
     */
    private List<TipType> initAuthorsCache(List<TipType> types) {

        if (!types.contains(TipType.A)) {
            return types;
        }
        if (authors == null) {
            authors = authorStore.findAll();
        }
        if (authors.size() == 0) {
            return clear(types, TipType.A);
        }
        return types;
    }

    /**
     * The method <code>initPkgsCache</code> provides means to initialise the
     * cache if required and clear the types list if no entry is present.
     *
     * @param types the types
     * @return adjusted types
     */
    private List<TipType> initPkgsCache(List<TipType> types) {

        if (!types.contains(TipType.P)) {
            return types;
        }
        if (packages == null) {
            packages = pkgStore.findAll();
        }
        if (packages.size() == 0) {
            return clear(types, TipType.P);
        }
        return types;
    }

    /**
     * The method <code>initTopicsCache</code> provides means to initialise the
     * cache if required and clear the types list if no entry is present.
     *
     * @param types the types
     * @return the adjusted types
     */
    private List<TipType> initTopicsCache(List<TipType> types) {

        if (!types.contains(TipType.T)) {
            return types;
        }
        if (topics == null) {
            topics = topicStore.findAll();
        }
        if (topics.size() == 0) {
            return clear(types, TipType.T);
        }
        return types;
    }

    /**
     * The method <code>randomInfo</code> provides means to retrieve a list of
     * random items.
     *
     * @param count the number of items requested
     * @param locale the language
     * @param types the array of types requested
     * @return the items
     */
    public synchronized List<TipTo> randomInfo(
        int count,
        String locale,
        List<TipType> types) {

        if (locale == null) {
            locale = "en";
        }
        types = initTopicsCache(types);
        types = initPkgsCache(types);
        types = initAuthorsCache(types);

        List<TipTo> result = new ArrayList<TipTo>();

        for (int i = 0; i < count; i++) {
            TipType t = types.get(TipType.rnd.nextInt(types.size()));
            result.add(t.selectItem(locale, topics, packages, authors,
                contentService));
        }
        return result;
    }

}