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;
}
}