SubmitResource.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.api;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.ctan.site.domain.account.User;
import org.ctan.site.services.upload.SubmitService;
import org.ctan.site.services.upload.SubmitService.Fields;
import org.ctan.site.services.upload.SubmitService.UploadData;
import org.ctan.site.services.upload.util.Messages;
import org.ctan.site.services.upload.util.archive.Archive;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.dropwizard.hibernate.UnitOfWork;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
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;
import jakarta.ws.rs.core.Response.Status;
import lombok.NonNull;
/**
* The class <code>SubmitResource</code> contains the controller for the submit
* resource.
*
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Path("/submit")
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
public class SubmitResource {
/**
* The field <code>service</code> contains the underlying service.
*/
private SubmitService service;
/**
* This is the constructor for the class <code>SubmitResource</code>.
*
* @param service the underlying service
*/
@SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
public SubmitResource(@NonNull SubmitService service) {
this.service = service;
}
/**
* The method <code>fields</code> provides means to retrieve the supported
* fields.
*
* @param api the API version
* @return a Map with the fields and their attributes
*/
@GET
@Path("/fields")
public Fields fields(
@QueryParam("api") String api) {
if (api == null) {
api = service.getVersion();
}
return switch (api) {
case "1.0" -> throw new WebApplicationException(Status.GONE);
case "1.1" -> service.getFields(api);
default -> throw new WebApplicationException(Status.NOT_FOUND);
};
}
/**
* The method <code>getVersions</code> provides means to retrieve the
* supported versions number of the API.
*
* @return a Map with version number mapped to a list of supported end-
* points.
*/
@GET
@Path("/versions")
public Map<String, List<String>> getVersions() {
return Map.of("1.1",
List.of("upload", "validate", "version", "versions", "fields"));
}
/**
* The method <code>isKnownArchive</code> provides means to check whether
* the file name argument has a known extension.
*
* @param filename the file name
* @return if the file name is known
*/
private boolean isKnownArchive(String filename) {
if (filename == null) {
return false;
}
filename = filename.toLowerCase();
return filename.endsWith(".zip")
|| filename.endsWith(".tgz")
|| filename.endsWith(".tar.gz");
}
/**
* The method <code>upload</code> provides means to upload a package to
* CTAN.
*
* @param api the API version or <code>null</code>
* @param stream the uploaded file
* @param formData the meta data for the file
* @param announce the announce parameter
* @param announcement the announcement parameter
* @param authors the authors parameter
* @param bugs the bugs parameter
* @param caption the caption parameter
* @param confirm the confirm parameter
* @param ctanPath the CTAN path parameter
* @param description the description parameter
* @param development the development channel parameter
* @param email the uploader's email
* @param home the home page
* @param licenses the licenses parameter
* @param mailinglist the mailing list
* @param name the printable name of the package
* @param notes the note for the upload managers
* @param pkg the package name in lower-case
* @param repository the repository
* @param support the support channel
* @param topics the topics parameter
* @param uploader the name of the uploader
* @param update the indicator for update vs. new
* @param vers the version number
*
* @return the list of messages
*/
@POST
@Path("/upload")
@Consumes({
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.MULTIPART_FORM_DATA,
MediaType.APPLICATION_JSON})
@Produces(MediaType.APPLICATION_JSON)
@UnitOfWork(value = "siteDb")
public Response upload(
// @Auth Optional<User> user,
@QueryParam("api") String api,
@FormDataParam("file") InputStream stream,
@FormDataParam("file") FormDataContentDisposition formData,
@FormDataParam("announce") String announce,
@FormDataParam("announcement") String announcement,
@FormDataParam("authors") String authors,
@FormDataParam("bugs") String bugs,
@FormDataParam("caption") String caption,
@FormDataParam("confirm") String confirm,
@FormDataParam("ctanPath") String ctanPath,
@FormDataParam("description") String description,
@FormDataParam("development") String development,
@FormDataParam("email") String email,
@FormDataParam("home") String home,
@FormDataParam("licenses") String licenses,
@FormDataParam("mailinglist") String mailinglist,
@FormDataParam("name") String name,
@FormDataParam("notes") String notes,
@FormDataParam("pkg") String pkg,
@FormDataParam("repository") String repository,
@FormDataParam("support") String support,
@FormDataParam("topics") String topics,
@FormDataParam("uploader") String uploader,
@FormDataParam("update") String update,
@FormDataParam("vers") String vers) {
Optional<User> user = Optional.empty();
if (api == null) {
api = service.getVersion();
}
if (!"1.1".equals(api)) {
throw new WebApplicationException("1.0".equals(api)
? Status.GONE
: Status.NOT_FOUND);
}
UploadData data = UploadData.builder()
.announce(announce)
.announcement(announcement)
.authors(authors)
.bugs(bugs)
.caption(caption)
.confirm("true".equals(confirm))
.ctanPath(ctanPath)
.description(description)
.development(development)
.email(email)
.licenses(licenses != null ? licenses.split("[,;: ]+") : null)
.mailinglist(mailinglist)
.name(name)
.notes(notes)
.pkg(pkg)
.repository(repository)
.support(support)
.topics(topics != null ? topics.split("[,;: ]+") : null)
.update(update)
.uploader(uploader)
.vers(vers)
.build();
Messages messages = new Messages();
if (stream == null || formData == null) {
messages.error("Missing archive file");
} else {
String filename = formData.getFileName();
Archive archive = Archive.of(filename, stream);
if (archive == null) {
messages.error("Unknown archive type", filename);
} else {
messages = service.upload(api,
data,
filename,
stream,
user.orElse(null));
}
}
return Response
.status(messages.getStatus())
.encoding("UTF-8")
.type(MediaType.APPLICATION_JSON)
.entity(messages.getValues())
.build();
}
/**
* The method <code>validate</code> provides means to upload a package to
* CTAN for validation.
*
* @param api the API version
* @param stream the uploaded file
* @param contentDispo the meta data for the file
* @param announce the announce parameter
* @param announcement the announcement parameter
* @param authors the authors parameter
* @param bugs the bugs parameter
* @param caption the caption parameter
* @param confirm the confirm parameter
* @param ctanPath the CTAN path parameter
* @param description the description parameter
* @param development the development channel parameter
* @param email the uploader's email
* @param home the home page
* @param licenses the licenses parameter
* @param mailinglist the mailing list
* @param name the printable name of the package
* @param notes the note for the upload managers
* @param pkg the package name in lower-case
* @param repository the repository
* @param support the support channel
* @param topics the topics parameter
* @param uploader the name of the uploader
* @param update the indicator for update vs. new
* @param vers the version number
*
* @return the response
*/
@POST
@Path("/validate")
@Consumes({
MediaType.APPLICATION_FORM_URLENCODED,
MediaType.MULTIPART_FORM_DATA,
MediaType.APPLICATION_JSON})
@Produces(MediaType.APPLICATION_JSON)
@UnitOfWork(value = "siteDb")
public Response validate(
@FormDataParam("api") String api,
@FormDataParam("file") InputStream stream,
@FormDataParam("file") FormDataContentDisposition contentDispo,
@FormDataParam("announce") String announce,
@FormDataParam("announcement") String announcement,
@FormDataParam("authors") String authors,
@FormDataParam("bugs") String bugs,
@FormDataParam("caption") String caption,
@FormDataParam("confirm") String confirm,
@FormDataParam("ctanPath") String ctanPath,
@FormDataParam("description") String description,
@FormDataParam("development") String development,
@FormDataParam("email") String email,
@FormDataParam("home") String home,
@FormDataParam("licenses") String licenses,
@FormDataParam("mailinglist") String mailinglist,
@FormDataParam("name") String name,
@FormDataParam("notes") String notes,
@FormDataParam("pkg") String pkg,
@FormDataParam("repository") String repository,
@FormDataParam("support") String support,
@FormDataParam("topics") String topics,
@FormDataParam("uploader") String uploader,
@FormDataParam("update") String update,
@FormDataParam("vers") String vers) {
if (api == null) {
api = service.getVersion();
}
if (!"1.1".equals(api)) {
throw new WebApplicationException("1.0".equals(api)
? Status.GONE
: Status.NOT_FOUND);
}
UploadData data = UploadData.builder()
.announce(announce)
.announcement(announcement)
.authors(authors)
.bugs(bugs)
.caption(caption)
.confirm("true".equals(confirm))
.ctanPath(ctanPath)
.description(description)
.development(development)
.email(email)
.licenses(licenses != null ? licenses.split("[,;: ]+") : null)
.mailinglist(mailinglist)
.name(name)
.notes(notes)
.pkg(pkg)
.repository(repository)
.support(support)
.topics(topics != null ? topics.split("[,;: ]+") : null)
.update(update)
.uploader(uploader)
.vers(vers)
.build();
Messages messages;
if (stream == null || contentDispo == null) {
messages = Messages.errorMessage("Missing archive file");
} else {
String filename = contentDispo.getFileName();
if (!isKnownArchive(filename)) {
messages =
Messages.errorMessage("Unknown archive type", filename);
} else {
messages = service.validate(api, data, filename, stream);
}
}
return Response
.status(messages.getStatus())
.encoding("UTF-8")
.type(MediaType.APPLICATION_JSON)
.entity(messages.getValues())
.build();
}
/**
* The method <code>version</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("/version")
public Map<String, String> version() {
return Map.of("version", service.getVersion());
}
}