Upload.java

/*
 * Copyright © 2012-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.domain.catalogue;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.regex.Pattern;

import org.ctan.site.domain.AbstractEntity;
import org.ctan.site.stores.UploadStore.StringsMap;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import lombok.AllArgsConstructor;
import lombok.Builder.Default;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

/**
 * This domain class represents the log entry for a successful upload.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public class Upload extends AbstractEntity {

    /**
     * This enumeration lists known file types.
     */
    public enum TerminationStatus {

        /**
         * The field <code>UNKNOWN</code> contains the indicates an unknown
         * status.
         */
        UNKNOWN(0),
        /**
         * The field <code>UPLOAD</code> contains the indicator for a successful
         * upload.
         */
        UPLOAD(2),
        /**
         * The field <code>UPLOAD_FAILURE</code> contains the indicator for a
         * failed upload.
         */
        UPLOAD_FAILURE(6), UPLOAD_OK(5),
        /**
         * The field <code>VALIDATE</code> contains the status of a validation
         * request.
         */
        VALIDATE(1),
        /**
         * The field <code>VALIDATE_FAILURE</code> contains the status of a
         * validation request which failed.
         */
        VALIDATE_FAILURE(4),
        /**
         * The field <code>VALIDATE_FAILURE</code> contains the status of a
         * validation request which succeeded.
         */
        VALIDATE_OK(3);

        /**
         * Finder for the message type by its numerical id.
         *
         * @param id the id
         * @return the associated element or {@code null}
         */
        static TerminationStatus of(int id) {

            for (TerminationStatus val : values()) {
                if (val.id == id) {
                    return val;
                }
            }
            return null;
        }

        /**
         * The field <code>id</code> contains the numerical representation.
         */
        private int id;

        /**
         * Creates a new object.
         *
         * @param id the id
         */
        TerminationStatus(int id) {

            this.id = id;
        }

        /**
         * Getter for the value.
         *
         * @return the value
         */
        public int value() {

            return id;
        }
    }

    /**
     * The class <code>UpWriter</code> contains a specialised file writer.
     */
    private static class UpWriter extends BufferedWriter {

        /**
         * This is the constructor for <code>UpWriter</code>.
         *
         * @param file the file
         * @throws IOException in case of an I/O error
         */
        public UpWriter(File file) throws IOException {

            super(new FileWriter(file, StandardCharsets.UTF_8));
        }

        /**
         * The method <code>append</code> provides means to write a line with
         * key and value to the output.
         *
         * @param key the key
         * @param value the value
         * @return this
         * @throws IOException in case of an I/O error
         */
        public UpWriter append(String key, String value) throws IOException {

            this.append(key)
                .append(":\t")
                .append(value == null ? "" : value)
                .append("\n");
            return this;
        }
    }

    // enum UploadType {
    // NEW, SILENT, ANNOUNCE
    // }

    /**
     * The field <code>SOFT_HYPHEN</code> contains the singleton string
     * containing the soft hyphen character.
     */
    private static final String SOFT_HYPHEN = "\uc2ad";

    /**
     * The constant <code>DATE_PATTERN</code> contains the pattern for
     * recognising a date.
     */
    static final Pattern DATE_PATTERN =
        Pattern.compile("(.*)"
            + "((19|20)\\d\\d[-/.][0-3]\\d[-/.][0-3]\\d|[0-3]\\d/[0-3]"
            + "\\d/(19|20)\\d\\d)"
            + "(.*)");

    /**
     * The field <code>announce</code> contains the URL for announce messages.
     */
    @Column(length = 255)
    private String announce;

    /**
     * The field <code>announcement</code> contains the text for the CTAN-ann
     * mailing list.
     */
    @Column(length = 8192)
    private String announcement;

    /**
     * The field <code>authors</code> contains the list of authors,
     * comma-separated.
     */
    @Column(length = 255)
    private String authors;

    /**
     * The field <code>bugs</code> contains the URL for the bug tracker.
     */
    @Column(length = 255)
    private String bugs;

    /**
     * The field <code>confirm</code> contains the indicator whether a
     * confirmation email has been requested.
     */
    @Column
    private Boolean confirm;

    /**
     * The field <code>ctanPath</code> contains the optional path in TeX
     * archive.
     */
    @Column(length = 255, name = "ctan_path")
    private String ctanPath;

    /**
     * The field <code>dateCreated</code> contains the creation date.
     */
    @Column(name = "date_created")
    @Default
    private LocalDateTime dateCreated = LocalDateTime.now();

    /**
     * The field <code>description</code> contains the description of the
     * package.
     */
    @Column(length = 4096)
    private String description;

    /**
     * The field <code>development</code> contains the URL for contact to the
     * developers.
     */
    @Column(length = 255)
    private String development;

    /**
     * The field <code>email</code> contains the email address of the uploader.
     */
    @Column(length = 255)
    private String email;

    /**
     * The field <code>home</code> contains the URL of the package's home page.
     */
    @Column(length = 255)
    private String home;

    /**
     * The field <code>lastUpdated</code> contains the time stamp of the last
     * update.
     */
    @Column(name = "last_updated")
    @Default
    private LocalDateTime lastUpdated = LocalDateTime.now();

    /**
     * The field <code>license</code> contains the licenses, comma-separated.
     */
    @Column(length = 2048)
    private String license;

    /**
     * The field <code>mailinglist</code> contains the URL of the package's
     * mailing list.
     */
    @Column(length = 255)
    private String mailinglist;

    /**
     * The field <code>note</code> contains the notes to the CTAN team.
     */
    @Column(length = 2048)
    private String note;

    /**
     * The field <code>pkg</code> contains the CTAN key of the package.
     */
    @Column(length = 32)
    private String pkg;

    /**
     * The field <code>repository</code> contains the URL of the package
     * repository.
     */
    @Column(length = 255)
    private String repository;

    /**
     * The field <code>serverUrl</code> contains the name of the server to which
     * the upload has been performed. This is there for historical reasons only.
     */
    @Column(length = 255, name = "server_url")
    private String serverUrl;

    /**
     * The field <code>status</code> contains the current status of this upload.
     */
    @Column
    @Default
    private TerminationStatus status = TerminationStatus.UNKNOWN;

    /**
     * The field <code>summary</code> contains the short summary in English of
     * the package.
     */
    @Column(length = 128)
    private String summary;

    /**
     * The field <code>support</code> contains the URL for support requests.
     */
    @Column(length = 255)
    private String support;

    /**
     * The field <code>topics</code> contains the comma-separated list of
     * topics.
     */
    @Column(length = 2048)
    private String topics;

    // type validator: { val, obj -> val in ['new', 'silent' , 'announce']}
    @Column(length = 16)
    private String type;

    /**
     * The field <code>uploader</code> contains the author key of the uploader.
     */
    @Column(length = 255)
    private String uploader;

    /**
     * The field <code>vers</code> contains the version.
     */
    @Column(length = 32)
    private String vers;

    /**
     * This method is the getter for the announcement in HTML format.
     *
     * @return the announcement
     */
    public String getAnnouncementAsHtml() {

        return announcement == null
            ? ""
            : announcement.replaceAll("\n", "<br>\n");
    }

    /**
     * This method is the getter for the description in HTML format.
     *
     * @return the description as HTML
     */
    public String getDescriptionAsHtml() {

        return description == null
            ? ""
            : description.replaceAll("\n", "<br>\n");
    }

    /**
     * This is the getter for <code>license</code>.
     *
     * @return the license
     */
    public String[] getLicenses() {

        return license == null ? new String[]{} : license.split("[ \t\r\n,]+");
    }

    /**
     * This method is the getter for the note in HTML format.
     *
     * @return the note
     */
    public String getNoteAsHtml() {

        return note == null ? "" : note.replaceAll("\n", "<br>\n");
    }

    /**
     * This method is the getter for the summary in HTML format.
     *
     * @return the summary as HTML
     */
    public String getSummaryAsHtml() {

        return summary == null ? "" : summary.replaceAll("\n", "<br>\n");
    }

    /**
     * This method is the getter for the version date.
     *
     * @return the version date
     */
    public String getVersionDate() {

        return separateVersion()[1];
    }

    /**
     * This method is the getter for the version number.
     *
     * @return the version number
     */
    public String getVersionNumber() {

        return separateVersion()[0];
    }

    /**
     * This method separates the version date and version number from one field.
     *
     * @return a list of the two elements version number and version date.
     */
    private String[] separateVersion() {

        if (vers == null) {
            return new String[]{"", ""};
        }
        var m = DATE_PATTERN.matcher(vers);
        if (m.matches()) {
            return new String[]{
                (m.group(1) + " " + m.group(5)).replaceAll("[()\\[\\]]", "")
                    .trim(),
                m.group(2)};
        }
        return new String[]{vers, ""};
    }

    /**
     * This method is the setter for the description.
     *
     * @param s the description
     */
    public void setDescription(String s) {

        description = s == null ? "" : s.replaceAll(SOFT_HYPHEN, "");
    }

    /**
     * This is the setter for <code>license</code>.
     *
     * @param licenses the list of licenses
     */
    public void setLicenses(String[] licenses) {

        this.license = String.join("\t", licenses);
    }

    /**
     * This method is the setter for the summary.
     *
     * @param s the summary
     */
    public void setSummary(String s) {

        summary = s == null ? "" : s.replaceAll(SOFT_HYPHEN, "");
    }

    /**
     * This method saves the upload to a file.
     *
     * @param file the target file
     * @throws IOException in case of an error
     */
    public void toFile(File file) throws IOException {

        try (var writer = new UpWriter(file)) {
            writer.append("Upload target", serverUrl)
                .append("Package name", pkg)
                .append("Version", vers)
                .append("Authors", authors)
                .append("Uploader", uploader)
                .append("Uploader email", email)
                .append("Upload type", type)
                .append("Location", ctanPath)
                .append("License", license)
                .append("Home", home)
                .append("Announce", announce)
                .append("Repository", repository)
                .append("Support", support)
                .append("Bugs", bugs)
                .append("Development", development)
                .append("Topics", topics)
                .append("Summary", summary)
                .append("Description", description)
                .append("Announcement", announcement)
                .append("Note", note)
                .append("\n");
        }
    }

    /**
     * The method <code>toMap</code> provides means to get the instance as an
     * Map.
     *
     * @return the Map
     */
    public Map<String, Object> toMap() {

        return new StringsMap()
            .add("id", getId())
            .add("announce", announce)
            .add("announcement", announcement)
            .add("authors", authors)
            .add("bugs", bugs)
            .add("confirm", confirm)
            .add("ctanPath", ctanPath)
            .add("dateCreated", dateCreated)
            .add("description", description)
            .add("development", development)
            .add("email", email)
            .add("home", home)
            .add("lastUpdated", lastUpdated)
            .add("license", license)
            .add("mailinglist", mailinglist)
            .add("note", note)
            .add("pkg", pkg)
            .add("repository", repository)
            .add("serverUrl", serverUrl)
            .add("status", status)
            .add("summary", summary)
            .add("support", support)
            .add("topics", topics)
            .add("type", type)
            .add("uploader", uploader)
            .add("vers", vers);
    }

    /**
     * {@inheritDoc}
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {

        return String.format("%s %tF", pkg, dateCreated);
    }
}