ArchiveFile.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.archive;

import java.io.IOException;
import java.util.Map;

import org.apache.lucene.index.CorruptIndexException;
import org.ctan.site.CtanConfiguration.CtanConfig;
import org.ctan.site.services.DateUtils;
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.google.common.collect.ImmutableMap;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder.Default;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

/**
 * This domain class records the info about a file or directory in the
 * <code>tex-archive</code> tree.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
@Entity(name = "archive_file")
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class ArchiveFile implements Searchable {

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

        /**
         * The field <code>UNKNOWN</code> contains the indicates an unknown file
         * type.
         */
        UNKNOWN('?'),
        /**
         * The field <code>FILE</code> contains the indicates a plain file.
         */
        FILE('f'),
        /**
         * The field <code>DIRECTORY</code> contains the indicator for a
         * directory.
         */
        DIRECTORY('d'),
        /**
         * The field <code>ARCHIVE</code> contains the indicator for an archive.
         */
        ARCHIVE('a'),
        /**
         * The field <code>GENERATED</code> contains the indicator for a
         * generated file.
         */
        GENERATED('g');

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

        /**
         * Creates a new object.
         *
         * @param id the id
         */
        FileType(char id) {

            this.id = id;
        }

    }

    /**
     * The field <code>id</code> contains the numerical id.
     */
    @Id
    @GeneratedValue
    @EqualsAndHashCode.Exclude
    private Long id;

    /**
     * The field <code>path</code> contains the full path, i.e. the directory
     * and the file name.
     */
    @Column(length = 1024)
    @Default
    private String path = null;

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

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

    /**
     * The field <code>type</code> contains the file type.
     */
    @Enumerated(EnumType.STRING)
    @Column
    private FileType type;

    /**
     * The field <code>mtime</code> contains the modification time.
     */
    @Column
    private long mtime;

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

        return switch (type) {
            case FileType.FILE -> "/tex-archive" + path + "#" + name;
            default -> "/tex-archive" + path + "/" + (name != null ? name : "");
        };
    }

    /**
     * This method is a getter for the boolean property of a file being an
     * archive.
     *
     * @return <code>true</code> iff the file is an archive
     */
    public boolean isArchive() {

        return type == FileType.ARCHIVE;
    }

    /**
     * This method is a getter for the boolean property of a file being a
     * directory.
     *
     * @return <code>true</code> iff the file is a directory
     */
    public boolean isDirectory() {

        return type == FileType.DIRECTORY;
    }

    /**
     * This method is a getter for the boolean property of a file being a plain
     * file.
     *
     * @return <code>true</code> iff the file is a plain file
     */
    public boolean isFile() {

        return type == FileType.FILE;
    }

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

        return ImmutableMap.of("id", (Object) getId(),
            "type", type.toString(),
            "path", path,
            "name", name,
            "title", title,
            "mtime", DateUtils.formatDateTime(mtime),
            "isArchive", isArchive(),
            "isFile", isFile(),
            "isDirectory", isDirectory());
    }

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

        IndexArgs args = IndexArgs.builder()
            .type(IndexType.FILES)
            .title(title != null ? title : name)
            .display(path)
            .content(new String[]{name})
            .modified(mtime)
            .build();
        String indexPath = indexPath();
        for (var locale : CtanConfig.LOCALES) {
            args.setLocale(locale);
            session.updateIndex(indexPath, args);
        }
    }

}