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("&", "&").replaceAll("<", "<")
.replaceAll(">", ">").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));
}
}