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