Rss20Resource.java
/*
* Copyright © 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.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.ctan.site.CtanConfiguration;
import org.ctan.site.services.DateUtils;
import org.ctan.site.services.postings.PostingCache;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
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 lombok.Builder;
import lombok.Builder.Default;
import lombok.Getter;
import lombok.NonNull;
/**
* The class <code>Rss20Resource</code> contains the controller for the RSS feed
* resource.
*
* @see <a href="https://www.rssboard.org/rss-specification">RSS 2.0
* Specification</a>
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Path("/ctan-ann")
@Produces("application/rss+xml")
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public class Rss20Resource {
/**
* The class <code>Rss</code> contains the top node of RSS 2.0.
*/
@Getter
@Builder
@JacksonXmlRootElement(localName = "rss")
public static class Rss {
private RssChannel channel;
@Default
@JacksonXmlProperty(isAttribute = true)
private String version = "2.0";
}
/**
* The class <code>RssChannel</code> contains the channel node of RSS 2.0.
*/
@Getter
@Builder
public static class RssChannel {
private String title;
private String link;
private String description;
private String pubDate;
private String language;
private String ttl;
private String webMaster;
@Default
private String generator = "CTAN site 3.0.0";
private RssImage image;
@JacksonXmlProperty(localName = "item")
@JacksonXmlElementWrapper(useWrapping = false)
private List<RssItem> items;
}
/**
* The class <code>RssGuid</code> contains the guid node of RSS 2.0.
*/
@Getter
@Builder
public static class RssGuid {
@JacksonXmlText
private String value;
@Default
@JacksonXmlProperty(isAttribute = true)
private String isPermaLink = "true";
}
/**
* The class <code>RssImage</code> contains the image node of RSS 2.0.
*/
@Getter
@Builder
public static class RssImage {
private String title;
private String url;
private String link;
}
/**
* The class <code>RssItem</code> contains the item node of RSS 2.0.
*/
@Getter
@Builder
public static class RssItem {
private String title;
private String link;
private String description;
private String pubDate;
private RssGuid guid;
}
/**
* The field <code>URL_PREFIX</code> contains the initial part of the URL.
*/
private static final String URL_PREFIX = "https://ctan.org/ctan-ann/rss";
/**
* The field <code>store</code> contains the underlying repository.
*/
private PostingCache service;
/**
* The field <code>version</code> contains the current version number.
*/
private String version;
/**
* This is the constructor for the class <code>RssResource</code>.
*
* @param service the underlying service
* @param config the configuration
*/
@SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
public Rss20Resource(@NonNull PostingCache service,
@NonNull CtanConfiguration config) {
this.service = service;
this.version = config.getVersion();
}
/**
* The method <code>clip</code> provides means to restrict the length of a
* string.
*
* @param body the text
* @param len the length
* @return the clipped text
*/
String clip(String body, int len) {
if (body == null) {
return null;
}
if (body.length() > len) {
var i = body.indexOf('.', len);
if (i >= 0) {
body = body.substring(0, i) + "...";
}
}
return body;
}
/**
* The method <code>getRss20</code> provides means to retrieve the RSS 2.0
* of the latest postings.
*
* @param length the maximal number of items returned
* @return the RSS feed
*/
@GET
@Path("/rss")
public Rss getRss20(
@QueryParam("length") @DefaultValue("64") int length) {
List<RssItem> items = new ArrayList<>();
for (var it : service.listNewest(length)) {
items.add(RssItem.builder()
.title(it.getSubject())
.link(URL_PREFIX + "/id/" + it.getId())
.guid(RssGuid.builder()
.value(URL_PREFIX + "/id/" + it.getId())
.build())
.pubDate(DateUtils.formatFullDateTime(it.getDate()))
.description(clip(it.getBody(), 150))
.build());
}
return Rss.builder()
.channel(RssChannel.builder()
.title("ctan-ann")
.link(URL_PREFIX)
.description("CTAN announcements mailing list - recent")
.pubDate(DateUtils.formatFullDateTime(Instant.now()))
.language("en-US")
.ttl("60")
.webMaster(
"webmaster@ctan.org (Comprehensive TeX Archive Network)")
.generator("CTAN site " + version)
.image(RssImage.builder()
.title("Comprehensive TeX Archive Network")
.link(URL_PREFIX)
.url(
"https://ctan.org/assets/favicon/android-chrome-48x48.png")
.build())
.items(items)
.build())
.build();
}
/**
* The method <code>getRss20ByPk</code> provides means to retrieve the RSS
* 2.0 of the latest postings.
*
* @param pkg the CTAN name of the package
* @param length the maximal number of items returned
* @return the RSS feed
*/
@GET
@Path("/rss/{pkg}.xml")
public Rss getRss20ByPkg(
@NonNull @PathParam("pkg") String pkg,
@QueryParam("length") @DefaultValue("64") int length) {
List<RssItem> items = new ArrayList<>();
for (var it : service.listNewest(pkg, length)) {
items.add(RssItem.builder()
.title(it.getSubject())
.link(URL_PREFIX + "/id/" + it.getId())
.guid(RssGuid.builder()
.value(URL_PREFIX + "/id/" + it.getId())
.build())
.pubDate(DateUtils.formatFullDateTime(it.getDate()))
.description(clip(it.getBody(), 150))
.build());
}
return Rss.builder()
.channel(RssChannel.builder()
.title("ctan-ann - " + pkg)
.link(URL_PREFIX + "/" + pkg + ".xml")
.description(
"CTAN announcements mailing list - for package " + pkg)
.pubDate(DateUtils.formatFullDateTime(Instant.now()))
.language("en-US")
.ttl("60")
.webMaster(
"webmaster@ctan.org (Comprehensive TeX Archive Network)")
.generator("CTAN site " + version)
.image(RssImage.builder()
.title("Comprehensive TeX Archive Network")
.link(URL_PREFIX)
.url(
"https://ctan.org/assets/favicon/android-chrome-48x48.png")
.build())
.items(items)
.build())
.build();
}
}