GuestBook.java

/*
 * Copyright (C) 2012-2025 Gerd Neugebauer
 *
 * This file is distributed under the 3-clause BSD license.
 * See file LICENSE for details.
 */
package org.ctan.site.domain.site;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import org.apache.lucene.index.CorruptIndexException;
import org.ctan.site.CtanConfiguration.CtanConfig;
import org.ctan.site.domain.AbstractEntity;
import org.ctan.site.domain.account.User;
import org.ctan.site.services.search.base.IndexType;
import org.ctan.site.services.search.base.IndexingSession;
import org.ctan.site.services.search.base.IndexingSession.IndexArgs;
import org.ctan.site.services.search.base.Searchable;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.google.common.collect.ImmutableMap;

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

/**
 * This domain class represents an entry in the guest book.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
@Entity
@Table(name = "guestbook")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public class GuestBook extends AbstractEntity implements Searchable {

    /**
     * The field <code>title</code> contains the title of the guest book item.
     */
    @Column(length = 128, nullable = false)
    private String title;

    /**
     * The field <code>text</code> contains the text body of the guest book
     * item.
     */
    @Column(length = 2048, nullable = false)
    private String text;

    /**
     * The field <code>name</code> contains the optional name of the author.
     */
    @Column(length = 128, nullable = true)
    private String name;

    /**
     * The field <code>email</code> contains the optional email address of the
     * guest book item.
     */
    @Column(length = 255, nullable = true)
    private String email;

    /**
     * The field <code>hideEmail</code> contains the indicator to hide the email
     * address.
     */
    @Column(name = "hide_email")
    private boolean hideEmail;

    /**
     * The field <code>hidden</code> contains the indicator to hide the complete
     * item. This is used for spurious items.
     */
    @Column
    @Default
    private boolean hidden = false;

    /**
     * The field <code>user</code> contains the optional user who has created
     * the item.
     */
    @ManyToOne
    private User user;

    /**
     * The field <code>dateCreated</code> contains the time stamp when the item
     * has been created.
     */
    @Column(name = "date_created")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm")
    @Default
    private LocalDateTime dateCreated = LocalDateTime.now();

    /**
     * The field <code>lastUpdated</code> contains the time stamp when the item
     * has been updated.
     */
    @Column(name = "last_updated")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm")
    @Default
    private LocalDateTime lastUpdated = LocalDateTime.now();

    /**
     * The field <code>parent</code> contains the reference to the parent item.
     */
    @ManyToOne
    @Default
    private GuestBook parent = null;

    // static hasMany = [ followup: Guestbook ]
    // static constraints = {
    // lastUpdated()
    // hidden()
    // title maxSize: 128
    // text blank: false, maxSize: 2048
    // name nullable: true, maxSize: 128
    // email nullable: true, maxSize: 255
    // hideEmail()
    // user nullable: true
    // parent nullable: true
    // dateCreated()
    // }

    /**
     * This method <code>getCleanedText</code> returns the text property after
     * it has applied some cleanup to it. Currently HTML links are removed.
     *
     * @return the cleaned text
     */
    public String getCleanedText() {

        return text.replaceAll("&lt;a .*&lt;/a&gt;", "");
    }

    /**
     * The method <code>getIndexType</code> provides means to name the
     * indexType.
     *
     * @return the index type
     */
    public IndexType getIndexType() {

        return IndexType.GUESTBOOK;
    }

    /**
     * Getter for the list of parents of this instance.
     *
     * @return the list of parents
     */
    public List<GuestBook> getParents() {

        List<GuestBook> parents = new ArrayList<>();
        // for (var p = this.parent; p != null; p = p.parent) {
        // parents.add(p);
        // }

        // TODO
        return parents; // .reverse();
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.services.search.base.Searchable#indexPath()
     */
    @Override
    public String indexPath() {

        return "/guestbook/" + getId();
    }

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

        return ImmutableMap.of("id", (Object) getId(),
            "title", title,
            "text", text,
            "hidden", hidden,
            "hideEmail", hideEmail,
            "created", String.format("%tF", dateCreated));
    }

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

        return text.length() < 24 ? text : text.substring(0, 24) + "...";
    }

    /**
     * This method <code>toUrl</code> returns the URL for the search index.
     *
     * @return the URL
     */
    public String toUrl() {

        return "/guestbook/item/" + getId();
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.services.search.base.Searchable#updateIndex(IndexingSession)
     */
    @Override
    public void updateIndex(IndexingSession session)
        throws CorruptIndexException,
            IOException {

        IndexArgs args = IndexArgs.builder()
            .type(IndexType.GUESTBOOK)
            .title(title)
            .display(title)
            .content(new String[]{
                title,
                text
            })
            .build();
        for (var locale : CtanConfig.LOCALES) {
            args.setLocale(locale);
            session.updateIndex(indexPath(), args);
        }
    }
}