JsonPkgResource.java
/*
* Copyright © 2024-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.api;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.ctan.site.domain.catalogue.AuthorRef;
import org.ctan.site.domain.catalogue.Pkg;
import org.ctan.site.domain.catalogue.PkgCaption;
import org.ctan.site.stores.PkgStore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
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.Builder.Default;
import lombok.Getter;
import lombok.NonNull;
/**
* The class <code>AuthorResource.java</code> contains the controller for the
* author resource.
*
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public class JsonPkgResource {
/**
* The class <code>AuthorShortTo</code> contains the transport object for
* authors references.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
protected static class AuthorShortTo {
private String id;
private Boolean active;
}
/**
* The class <code>AuthorTo</code> contains the transport object for
* authors.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
protected static class AuthorTo {
private String id;
private boolean active;
private String familyname;
private String givenname;
private String von;
private String junior;
private String pseudonym;
@Default
private String gender = "m";
@Default
private Boolean died = Boolean.FALSE;
}
/**
* The class <code>CopyrightTo</code> contains the transport object for
* copyright information.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
protected static class CopyrightTo {
private String year;
private String owner;
}
/**
* The class <code>CtanTo</code> contains the transport object for
* references to the tex-archive.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
protected static class CtanTo {
private String path;
private Boolean file;
}
/**
* The class <code>DescriptionTo</code> contains the transport object for a
* package description.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
protected static class DescriptionTo {
private String lang;
private String description;
}
/**
* The class <code>DocTo</code> contains the transport object for package
* documentation.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
protected static class DocTo {
private String lang;
private String details;
private String href;
}
/**
* The class <code>ListItemTo</code> contains the transport object for list
* items.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
protected static class ListItemTo {
private String key;
private String name;
private String details;
}
/**
* The class <code>PackageTo</code> contains the transport object for a
* package.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
protected static class PackageTo {
private String id;
private String name;
private VersionTo version;
private String caption;
private List<DescriptionTo> descriptions;
private List<DocTo> documentations;
private List<AuthorTo> authors;
private List<CopyrightTo> copyright;
private CtanTo ctan;
private String miktex;
private String texlive;
private List<String> license;
private String install;
private String home;
private String support;
private String announce;
private String bugs;
private String repository;
private String development;
private List<String> index;
private List<String> also;
private List<String> topics;
}
/**
* The class <code>VersionTo</code> contains the transport object for the
* version number.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
protected static class VersionTo {
private String number;
private String date;
}
/**
* The constant <code>values</code> contains the versions which support this
* end-point.
*/
static List<String> VERSIONS =
List.of("1.0", "1.1", "1.2", "1.3", "2.0", "2.1");
/**
* The field <code>store</code> contains the underlying repository.
*/
private PkgStore store;
/**
* This is the constructor for the class <code>PkgResource</code>.
*
* @param store the underlying store
*/
@SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
public JsonPkgResource(@NonNull PkgStore store) {
this.store = store;
}
/**
* The method <code>authorFamilyname</code> provides means to extract the
* family name of an author. If the author has a pseudonym then this is
* returned.
*
* @param ar the author reference
* @return the family name
*/
private String authorFamilyname(AuthorRef ar) {
var author = ar.getAuthor();
return author.getPseudonym() != null
? author.getPseudonym()
: author.getFamilyname();
}
/**
* The method <code>authorGivenname</code> provides means to extract the
* given name of an author. If the author has a pseudonym then {@code null}
* is returned.
*
* @param ar the author reference
* @return the given name
*/
private String authorGivenname(AuthorRef ar) {
var author = ar.getAuthor();
return author.getPseudonym() != null
? null
: author.getGivenname();
}
/**
* The method <code>collectAuthors</code> provides means to retrieve the
* authors.
*
* @param pkg the package
* @param vers the version of the API
* @param expand expand the authors and return more attributes
* @return the list of authors or {@code null}
*/
private List<AuthorTo> collectAuthors(Pkg pkg, String vers,
boolean expand) {
var authors = pkg.getAuthors();
if (authors == null) {
return null;
}
if (!expand) {
return authors.stream()
.map(a -> AuthorTo.builder()
.id(a.getAuthor().getKey())
.active(a.isActive())
.build())
.collect(Collectors.toList());
}
return authors.stream()
.map(a -> AuthorTo.builder()
.id(a.getAuthor().getKey())
.active(a.isActive())
.familyname(authorFamilyname(a))
.givenname(authorGivenname(a))
.build())
.collect(Collectors.toList());
}
/**
* The method <code>collectCopyright</code> provides means to retrieve the
* copyright items.
*
* @param pkg the package
* @return the list of copyright items
*/
private List<CopyrightTo> collectCopyright(Pkg pkg) {
var copy = pkg.getCopy();
if (copy == null || copy.isEmpty()) {
return null;
}
return copy.stream()
.map(c -> CopyrightTo.builder()
.owner(c.getOwner())
.year(c.getYear())
.build())
.collect(Collectors.toList());
}
/**
* The method <code>collectTopics</code> provides means to retrieve the
* descriptions.
*
* @param pkg the package
* @return the associated descriptions or {@code null} for none
*/
private List<DescriptionTo> collectDescriptions(Pkg pkg) {
var descs = pkg.getDescriptions();
if (descs == null || descs.isEmpty()) {
return null;
}
return descs.stream()
.map(d -> DescriptionTo.builder()
.lang(d.getLang())
.description(d.getDescription())
.build())
.collect(Collectors.toList());
}
/**
* The method <code>collectDocumentations</code> provides means to retrieve
* the documentations.
*
* @param pkg the package
* @return the list of docs
*/
private List<DocTo> collectDocumentations(Pkg pkg) {
var docs = pkg.getDocs();
if (docs == null || docs.isEmpty()) {
return null;
}
return docs.stream()
.map(doc -> DocTo.builder()
.lang(doc.getLang())
.href(doc.getHref())
.details(doc.getTitle())
.build())
.collect(Collectors.toList());
}
/**
* The method <code>collectLicenses</code> provides means to retrieve a
* single license. Since the underlying package may have several licenses
* one is selected randomly.
*
* @param pkg the package
* @return the license or {@code null}
*/
private List<String> collectLicenses(Pkg pkg) {
var lics = pkg.getLicenses();
if (lics == null || lics.isEmpty()) {
return null;
}
return lics.stream()
.map(t -> t.getKey())
.collect(Collectors.toList());
}
/**
* The method <code>collectTopics</code> provides means to retrieve the ids
* of the associated topics.
*
* @param pkg the package
* @return the associated topics or {@code null} for none
*/
private List<String> collectTopics(Pkg pkg) {
var tops = pkg.getTopics();
if (tops == null || tops.isEmpty()) {
return null;
}
return tops.stream()
.map(t -> t.getKey())
.collect(Collectors.toList());
}
/**
* The method <code>getPackages</code> provides means to retrieve a list of
* packages starting with a given pattern.
*
* @param vers the version number
* @param key the pattern for the key
* @return a list of matching package summaries
*/
@GET
@Path("/json/{vers}/packages")
@PermitAll
@UnitOfWork(value = "siteDb")
public List<ListItemTo> getPackages(
@PathParam("vers") String vers,
@QueryParam("key") String key) {
if (!VERSIONS.contains(vers)) {
throw new WebApplicationException(Status.NOT_FOUND);
}
return store.findAllByKeyStartingWith(key != null ? key : "")
.stream()
.map(a -> {
return ListItemTo.builder()
.key(a.getKey())
.name(a.getName())
.details(a.getCaption("en").getCaption())
.build();
}).collect(Collectors.toList());
}
/**
* The method <code>getPkgByKey</code> provides means to retrieve a package.
*
* @param vers the version
* @param id the key of the package
* @param authorNames indicator to include author names
* @param drop comma separated list of fields to omit
* @return an package or {@code null}
*/
@GET
@Path("/json/{vers}/pkg/{id}")
@PermitAll
@UnitOfWork(value = "siteDb")
public PackageTo getPkgByKey(
@PathParam("vers") String vers,
@PathParam("id") String id,
@QueryParam("author-names") Boolean authorNames,
@QueryParam("drop") String drop) {
if (!VERSIONS.contains(vers)) {
throw new WebApplicationException(Status.NOT_FOUND);
}
var drops =
drop == null ? List.of() : Arrays.asList(drop.split(",\\s*"));
var pkg = store.getByKey(id);
if (pkg == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
if (authorNames == null) {
authorNames = Boolean.FALSE;
}
var builder = PackageTo.builder()
.id(pkg.getKey())
.license(collectLicenses(pkg));
if (!drops.contains("also")) {
var also = pkg.getAlso();
if (also != null) {
builder.also(also.stream()
.map(x -> x.getKey())
.collect(Collectors.toList()));
}
}
if (!drops.contains("author")) {
builder.authors(collectAuthors(pkg, vers, authorNames));
}
if (!drops.contains("caption")) {
PkgCaption caption = pkg.getCaption("en");
builder.caption(caption == null ? null : caption.getCaption());
}
if (!drops.contains("copyright")) {
builder.copyright(collectCopyright(pkg));
}
if (!drops.contains("ctan")) {
builder.ctan(CtanTo.builder()
.path(pkg.getCtanPath())
.file(pkg.isCtanFile() ? Boolean.TRUE : null)
.build());
}
if (!drops.contains("description")) {
builder.descriptions(collectDescriptions(pkg));
}
if (!drops.contains("documentation")) {
builder.documentations(collectDocumentations(pkg));
}
if (!drops.contains("home")) {
builder.home(pkg.getHome());
}
if (!drops.contains("index")) {
var extraIndex = pkg.getExtraIndex();
builder.index(extraIndex == null
? List.of()
: Arrays.asList(extraIndex.split("\\s+")));
}
if (!drops.contains("install")) {
builder.install(pkg.getInstallPath());
}
if (!drops.contains("miktex")) {
builder.miktex(pkg.getMiktexLocation());
}
if (!drops.contains("name")) {
builder.name(pkg.getName());
}
if (!drops.contains("texlive")) {
builder.texlive(pkg.getTexliveLocation());
}
if (!drops.contains("topics")) {
builder.topics(collectTopics(pkg));
}
if (!drops.contains("version")) {
builder.version(VersionTo.builder()
.number(pkg.getVersionNumber())
.date(pkg.getVersionDate())
.build());
}
if (!"1.0".equals(vers) && !"1.1".equals(vers)) {
if (!drops.contains("announce")) {
builder.announce(pkg.getAnnounce());
}
if (!drops.contains("bugs")) {
builder.bugs(pkg.getBugs());
}
if (!drops.contains("development")) {
builder.development(pkg.getDevelopment());
}
if (!drops.contains("repository")) {
builder.repository(pkg.getRepository());
}
if (!drops.contains("support")) {
builder.support(pkg.getSupport());
}
}
if ("3.0".equals(vers)) {
List<Pkg> also = pkg.getAlso();
if (also != null && !drops.contains("also")) {
builder.also(also.stream()
.map(it -> it.getKey())
.toList());
}
}
return builder.build();
}
}