Posting.java

/*
 * Copyright (C) 2016-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.services.postings;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.ctan.site.services.DateUtils;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * This class represents a mail from a mailing list. This consists of some
 * header fields and the mail body.
 *
 * @author <a href="gene@ctan.org">Gerd Neugebauer</a>
 */
@Slf4j
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public class Posting implements Comparable<Posting> {

    /**
     * The field <code>header</code> contains the header fields of the mail.
     */
    @Default
    private Map<String, String> header = new HashMap<>();

    /**
     * The field <code>from</code> contains the primary from line of the mail.
     */
    private String from;

    /**
     * The field <code>body</code> contains the body of the mail.
     */
    @Default
    private String body = "";

    /**
     * The field <code>date</code> contains the date of the mail.
     */
    @Default
    private Instant date = Instant.ofEpochSecond(0L);

    /**
     * The field <code>pkg</code> contains the list of packages associated with
     * this posting.
     */
    @Default
    private List<String> pkg = new ArrayList<>();

    /**
     * This is the constructor for <code>Posting</code>.
     *
     * @param from the from line
     */
    public Posting(String from) {

        this.from = from;
        this.date = Instant.ofEpochSecond(0L);
    }

    /**
     * This method adds a package to the list of associated packages.
     *
     * @param p the package key
     */
    public void addPkg(String p) {

        if (pkg == null) {
            pkg = new ArrayList<String>();
        }
        pkg.add(p);
    }

    /**
     * {@inheritDoc}
     *
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    @SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS",
        justification = "unclear how to deal with this; false positive?")
    public int compareTo(Posting other) {

        if (equals(other)) {
            return 0;
        }
        return other == null || other.date == null
            ? -1
            : date.compareTo(other.date);
    }

    /**
     * Getter for a header field.
     *
     * @param key the name of the header
     * @return the value of the header or {@code null} for none
     */
    public String get(Object key) {

        return header.get(key);
    }

    /**
     * This is the getter for <code>body</code> in a HTMLified form.
     *
     * @return the body
     */
    public String getBodyAsHtml() {

        return body.replaceAll("&", "&amp;").replaceAll("<", "&lt;")
            .replaceAll(">", "&gt;").replaceAll("R[?]be", "Rübe")
            .replaceAll("Sch[?]pf", "Schöpf")
            .replaceAll("package[?]s", "package's")
            .replaceAll("https?://[a-zA-Z0-9_./-]+",
                "<a href=\"$0\">$0</a>")
            .replaceAll("\n----*\n+", "<hr>");
    }

    /**
     * The method <code>getDateFormatted</code> returns the date in the form of
     * a day.
     *
     * @return the date as day
     */
    public String getDateFormatted() {

        return DateUtils.formatDateTime(date);
    }

    /**
     * The method <code>getDay</code> returns the date in the form of a day.
     *
     * @return the date as day
     */
    public String getDay() {

        return DateUtils.formatDate(date);
    }

    /**
     * The method <code>getFromEmail</code> returns the from email.
     *
     * @return the from email
     */
    public String getFromEmail() {

        var f = get("From");
        return (f == null ? "" : f.replaceAll("\\w*[(].*", ""));
    }

    /**
     * The method <code>getFromName</code> returns the from name.
     *
     * @return the from name
     */
    public String getFromName() {

        var f = get("From");
        return (f == null
            ? ""
            : f.replaceAll(".*[(]", "").replaceAll("[)].*",
                ""));
    }

    /**
     * This is the getter for <code>id</code>.
     *
     * @return the body
     */
    public String getId() {

        var id = header.get("Message-ID");
        return id == null ? "" : id.replaceAll("[<>]", "");
    }

    /**
     * This is the getter for <code>subject</code>.
     *
     * @return the subject
     */
    public String getSubject() {

        return header.get("Subject");
    }

    /**
     * Setter for a header field.
     *
     * @param key the key
     * @param value the value
     * @return the value
     */
    public String put(String key, String value) {

        if ("Date".equals(key)) {
            putDate(value);
        }
        if (header == null) {
            header = new HashMap<String, String>();
        }
        return header.put(key, value);
    }

    /**
     * The method <code>parseDate</code> provides means to set the date as
     * Instant.
     *
     * @param value the value
     */
    private void putDate(String value) {

        String shortened = value.replaceAll(
            "[^0-9]*(\\d+ [A-Za-z0-9]+ \\d+ \\d+:\\d+:\\d+).*", "$1");
        try {
            date = LocalDateTime.parse(shortened,
                DateTimeFormatter.ofPattern("d MMM yyyy HH:mm:ss")
                    .localizedBy(Locale.US))
                .toInstant(ZoneOffset.UTC);
            return;
        } catch (DateTimeParseException e) {
            // fall-through
        }
        try {
            date = LocalDateTime.parse(value,
                DateTimeFormatter.ofPattern("d MM yyyy"))
                .toInstant(ZoneOffset.UTC);
        } catch (DateTimeParseException e2) {
            log.warn("Date parsing failed: " + value);
            // ignored on purpose
        }
    }

    /**
     * This is the setter for <code>body</code>.
     *
     * @param body the new value for body
     */
    public void setBody(String body) {

        this.body = body.trim();
    }

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

        return getDay() + (pkg.isEmpty() ? "" : " " + pkg.get(0));
    }
}