Atom10Resource.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 java.util.UUID;
import org.ctan.site.CtanConfiguration;
import org.ctan.site.services.DateUtils;
import org.ctan.site.services.postings.PostingCache;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
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>Atom10Resource</code> contains the controller for the ATOM
* feed resource.
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4287">The Atom
* Syndication Format</a>
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Path("/ctan-ann")
@Produces("application/atom+xml")
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public class Atom10Resource {
/**
* The class <code>AtomEntry</code> contains the item node of Atom 1.0.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
public static class AtomEntry {
@JacksonXmlProperty
private String title;
@JacksonXmlProperty(localName = "link")
@JacksonXmlElementWrapper(useWrapping = false)
private List<AtomLink> links;
private String id;
private String updated;
private String summary;
private String content;
}
/**
* The class <code>Atom</code> contains the top node of Atom 1.0.
*/
@Getter
@Builder
@JacksonXmlRootElement(localName = "feed")
@JsonInclude(Include.NON_NULL)
public static class AtomFeed {
@JacksonXmlProperty(isAttribute = true)
@Default
private String xmlns = "http://www.w3.org/2005/Atom";
private String title;
private String subtitle;
@JacksonXmlProperty(localName = "link")
@JacksonXmlElementWrapper(useWrapping = false)
private List<AtomLink> links;
private String icon;
private String id;
private String updated;
private AtomGenerator generator;
@JacksonXmlProperty(localName = "entry")
@JacksonXmlElementWrapper(useWrapping = false)
private List<AtomEntry> entries;
}
/**
* The class <code>AtomGenerator</code> contains the Generator node of Atom
* 1.0.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
public static class AtomGenerator {
@JacksonXmlProperty(isAttribute = true)
String uri;
@JacksonXmlProperty(isAttribute = true)
String version;
@JacksonXmlText
String value;
}
/**
* The class <code>AtomLink</code> contains the Link node of Atom 1.0.
*/
@Getter
@Builder
@JsonInclude(Include.NON_NULL)
public static class AtomLink {
@JacksonXmlProperty(isAttribute = true)
private String href;
@JacksonXmlProperty(isAttribute = true)
private String rel;
@JacksonXmlProperty(isAttribute = true)
private String type;
}
/**
* The field <code>URL_PREFIX</code> contains the initial part of the URL.
*/
private static final String URL_PREFIX = "https://ctan.org/ctan-ann";
/**
* 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 Atom10Resource(@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>getAtom10</code> provides means to retrieve the Atom 1.0
* of the latest postings.
*
* @param length the maximal number of items returned
* @return the Atom feed
*/
@GET
@Path("/atom/")
public AtomFeed getAtom10(
@QueryParam("length") @DefaultValue("64") int length) {
List<AtomEntry> entries = new ArrayList<>();
for (var it : service.listNewest(length)) {
entries.add(AtomEntry.builder()
.title(it.getSubject())
.summary(clip(it.getBody(), 70))
.content(clip(it.getBody(), 150))
.links(List.of(
AtomLink.builder()
.href(URL_PREFIX + "/id/" + it.getId())
.build()))
.updated(
DateUtils.formatRfc3339(it.getDate()))
.build());
}
return AtomFeed.builder()
.title("ctan-ann")
.subtitle("Announcements of the Comprehensive TeX Archive Network")
.links(List.of(AtomLink.builder()
.rel("self")
.href("https://ctan.org/ctan-ann/rss")
.build(),
AtomLink.builder()
.href("https://ctan.org")
.build()))
.updated(DateUtils.formatRfc3339(Instant.now()))
.id("urn:uuid:" + UUID.randomUUID().toString())
.icon("https://ctan.org/assets/favicon/android-chrome-48x48.png")
.generator(AtomGenerator.builder()
.uri(
"https://gitlab.com/comprehensive-tex-archive-network/ctan-site")
.version(version)
.value("CTAN site")
.build())
.entries(entries)
.build();
}
/**
* The method <code>getAtom10ByPkg</code> provides means to retrieve the
* Atom 1.0 of the latest postings.
*
* @param pkg the CTAN name of the package
* @param length the maximal number of items returned
* @return the Atom feed
*/
@GET
@Path("/atom/{pkg}.xml")
public AtomFeed getAtom10ByPkg(
@NonNull @PathParam("pkg") String pkg,
@QueryParam("length") @DefaultValue("64") int length) {
List<AtomEntry> entries = new ArrayList<>();
for (var it : service.listNewest(pkg, length)) {
entries.add(AtomEntry.builder()
.title(it.getSubject())
.summary(clip(it.getBody(), 70))
.content(clip(it.getBody(), 150))
.links(List.of(
AtomLink.builder()
.href(URL_PREFIX + "/id/" + it.getId())
.build()))
.updated(
DateUtils.formatRfc3339(it.getDate()))
.build());
}
return AtomFeed.builder()
.title("ctan-ann")
.subtitle("Announcements of the Comprehensive TeX Archive Network")
.links(List.of(AtomLink.builder()
.rel("self")
.href("https://ctan.org/ctan-ann/atom/" + pkg + ".xml")
.build(),
AtomLink.builder()
.href("https://ctan.org")
.build()))
.updated(DateUtils.formatRfc3339(Instant.now()))
.id("urn:uuid:" + UUID.randomUUID().toString())
.icon("https://ctan.org/assets/favicon/android-chrome-48x48.png")
.generator(AtomGenerator.builder()
.uri(
"https://gitlab.com/comprehensive-tex-archive-network/ctan-site")
.version(version)
.value("CTAN site")
.build())
.entries(entries)
.build();
}
}