Postings3Resource.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.postings;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.ctan.site.domain.catalogue.PkgCaption;
import org.ctan.site.services.content.ContentService;
import org.ctan.site.services.content.ContentService.TeaserType;
import org.ctan.site.services.postings.Posting;
import org.ctan.site.services.postings.PostingCache;
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.DefaultValue;
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>Postings3Resource</code> contains the controller for the
* postings resource.
*
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Path("/3.0")
@Produces(MediaType.APPLICATION_JSON)
public class Postings3Resource {
/**
* The class <code>PackageTo</code> contains the transport object for a
* package.
*/
@Getter
@Builder
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public static class PackageTo {
private String pkg;
private String info;
private boolean hasTeaser;
}
/**
* The class <code>PostingsPageTo</code> contains the transport object for a
* posting page.
*/
@Getter
@Builder
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public static class PostingsPageTo {
private List<PostingsTo> list;
private int total;
}
/**
* The class <code>PostingsTo</code> contains the transport object for a
* list of postings.
*/
@Getter
@Builder
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public static class PostingsTo {
private String id;
private String date;
private String subject;
@JsonInclude(Include.NON_NULL)
@Default
private List<String> pkg = null;
@JsonInclude(Include.NON_NULL)
@Default
private List<PackageTo> pkgs = null;
}
/**
* The class <code>PostingTo</code> contains the transport object for a
* posting.
*/
@Getter
@Builder
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public static class PostingTo {
private String id;
private String date;
private String subject;
private String body;
private String teaser;
private List<String> pkg;
}
/**
* The field <code>store</code> contains the underlying repository.
*/
private PostingCache service;
/**
* The field <code>contentService</code> contains the content service.
*/
private @NonNull ContentService contentService;
/**
* The field <code>pkgStore</code> contains the package store.
*/
private PkgStore pkgStore;
/**
* This is the constructor for the class <code>Postings3Resource</code>.
*
* @param service the underlying service
* @param contentService the content service
* @param pkgStore the package store
*/
@SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
public Postings3Resource(@NonNull PostingCache service,
@NonNull ContentService contentService,
@NonNull PkgStore pkgStore) {
this.pkgStore = pkgStore;
this.service = service;
this.contentService = contentService;
}
/**
* The method <code>getPkgs</code> provides means to get the associated
* packages of a posting as transport object.
*
* @param posting the posting
* @param lang the language
*
* @return the list of packages
*/
private List<PackageTo> getPkgs(Posting posting, String lang) {
var packs = posting.getPkg();
if (packs == null) {
return List.of();
}
var packageList = new ArrayList<PackageTo>();
for (var it : packs) {
var p = pkgStore.getByKey(it);
if (p == null) {
continue;
}
PkgCaption caption = p.getCaption(lang);
packageList.add(PackageTo.builder()
.pkg(it)
.hasTeaser(contentService.hasTeaser(TeaserType.PKG, lang))
.info(caption == null ? null : caption.getCaption())
.build());
}
return packageList;
}
/**
* The method <code>getPosting</code> provides means to retrieve a single
* mail posting.
*
* @param id the id of the posting
* @return the posting or throw a 404 error
*/
@GET
@Path("/posting/{id}")
@PermitAll
public PostingTo getPosting(
@NonNull @PathParam("id") String id) {
var posting = service.get(id);
if (posting == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
var pkgs = posting.getPkg();
String teaser = null;
if (pkgs != null) {
for (var it : pkgs) {
var t = contentService.hasTeaser(TeaserType.PKG, it);
if (t) {
teaser = it;
break;
}
}
}
return PostingTo.builder()
.id(id)
.teaser(teaser)
.body(posting.getBodyAsHtml())
.pkg(pkgs)
.subject(posting.getSubject())
.date(posting.getDateFormatted())
.build();
}
/**
* The method <code>getPostings</code> provides means to retrieve a paged
* list of email postings. They are sorted in reverse chronological order.
*
* @param page the current page
* @param size the page size
* @param lang the locale
* @param data indicator whether to include more data
* @return the page segment
*/
@GET
@Path("/postings")
@PermitAll
@UnitOfWork(value = "siteDb")
public PostingsPageTo getPostings(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("16") int size,
@QueryParam("lang") @DefaultValue("en") String lang,
@QueryParam("data") @DefaultValue("false") boolean data) {
if (page < 0) {
page = 0;
}
if (size <= 0) {
size = 16;
}
var postings = service.listPaged(page, size);
var list = postings
.stream()
.map(data
? it -> PostingsTo.builder()
.id(it.getId())
.subject(it.getSubject())
.date(it.getDay())
.pkgs(getPkgs(it, lang))
.build()
: it -> PostingsTo.builder()
.id(it.getId())
.subject(it.getSubject())
.date(it.getDay())
.pkg(it.getPkg())
.build())
.collect(Collectors.toList());
return PostingsPageTo.builder()
.list(list)
.total(service.total())
.build();
}
/**
* The method <code>getVersion</code> provides means to retrieve the version
* number of the API.
*
* @return a Map with a single attribute <code>version</code> containing the
* version number as String.
*/
@GET
@Path("/postings/version")
public Map<String, String> getVersion() {
return Map.of("version", "3.0");
}
}