ArchiveFileStore.java

/*
 * Copyright © 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.stores;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.lucene.index.CorruptIndexException;
import org.ctan.site.CtanConfiguration.TexArchiveConfig;
import org.ctan.site.domain.archive.ArchiveFile;
import org.ctan.site.domain.archive.ArchiveFile.FileType;
import org.ctan.site.services.search.base.IndexType;
import org.ctan.site.services.search.base.IndexingSession;
import org.ctan.site.stores.base.AbstractIndexingStore;
import org.hibernate.SessionFactory;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import lombok.NonNull;

/**
 * The class <code>ArchiveFileStore</code> contains the repository for archive
 * files.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
@SuppressFBWarnings(value = "EI_EXPOSE_REP2")
public class ArchiveFileStore extends AbstractIndexingStore<ArchiveFile> {

    /**
     * The field <code>strip</code> contains the length of the initial segment
     * to strip off.
     */
    private int strip;

    /**
     * The field <code>archive</code> contains the archive.
     */
    private File archive;

    /**
     * The field <code>n</code> contains the counter.
     */
    private Long counter = 1L;

    /**
     * This is the constructor for the <code>ArchiveFileStore</code>.
     *
     * @param config the configuration
     * @param sessionFactory the session factory
     * @param indexingSession the indexing session
     */
    @SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
    public ArchiveFileStore(@NonNull TexArchiveConfig config,
        SessionFactory sessionFactory,
        IndexingSession indexingSession) {

        super(sessionFactory, indexingSession);

        String directory = config.getDirectory();
        this.strip = directory.length();
        this.archive = new File(directory);
    }

    /**
     * The method <code>fileType</code> provides means to determine the file
     * type.
     *
     * @param fileName the name of the file
     * @return the file type
     */
    private FileType fileType(String fileName) {

        if (fileName.endsWith(".zip")
            || fileName.endsWith(".tgz")
            || fileName.endsWith(".tar.gz")) {
            return FileType.ARCHIVE;
        }
        return FileType.FILE;
    }

    /**
     * The method <code>findAllByPath</code> provides means to retrieve archive
     * files where the path is a given string. The comparison is done
     * case-sensitive.
     *
     * @param path the path
     * @return the list of archive files ordered by the name
     */
    public List<ArchiveFile> findAllByPath(String path) {

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<ArchiveFile> af = query.from(ArchiveFile.class);
        query.where(cb.like(af.get("path"), path))
            .orderBy(cb.asc(af.get("name")));
        return list(query);
    }

    /**
     * The method <code>findAllByType</code> provides means to retrieve all
     * items with a given type.
     *
     * @param type the type
     * @return the list of archive files
     */
    public List<ArchiveFile> findAllByType(FileType type) {

        var query = criteriaQuery();
        CriteriaBuilder cb = currentSession().getCriteriaBuilder();
        Root<ArchiveFile> af = query.from(ArchiveFile.class);
        query.where(cb.equal(af.get("type"), type));
        return list(query);
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.stores.base.AbstractIndexingStore#indexType()
     */
    @Override
    protected IndexType indexType() {

        return IndexType.SITE;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.stores.base.AbstractStore#listQuery(java.lang.String,
     *     jakarta.persistence.criteria.CriteriaBuilder,
     *     jakarta.persistence.criteria.CriteriaQuery)
     */
    @Override
    protected Root<ArchiveFile> listQuery(String term, CriteriaBuilder cb,
        CriteriaQuery<ArchiveFile> query) {

        Root<ArchiveFile> root = query.from(ArchiveFile.class);
        if (term != null && !term.isBlank()) {
            var t = "%" + term.toLowerCase() + "%";
            query.where(cb.like(cb.lower(root.get("name")), t));
        }
        return root;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.stores.base.AbstractStore#map(java.util.List)
     */
    @Override
    protected List<Map<String, Object>> map(List<ArchiveFile> list) {

        return list.stream()
            .map(it -> it.toMap())
            .collect(Collectors.toList());
    }

    /**
     * The method <code>updateArchive</code> provides means to recursively
     * update the files of a directory tree.
     *
     * @param dir the current directory
     * @throws IOException in case of an I/O error
     * @throws CorruptIndexException in case of an exception
     */
    private void updateArchive(File dir)
        throws CorruptIndexException,
            IOException {

        ArchiveFile archiveFile;
        File[] files = dir.listFiles();
        if (files == null) {
            throw new FileNotFoundException(dir.toString());
        }
        for (var f : files) {
            if (f.isDirectory()) {
                archiveFile = ArchiveFile.builder()
                    .id(counter++)
                    .name(f.getName())
                    .path(dir.getPath().substring(strip))
                    .type(FileType.DIRECTORY)
                    .mtime(f.lastModified())
                    .build();
                save(archiveFile);
                updateArchive(f);
            } else if (f.isFile()) {
                String fileName = f.getName();
                archiveFile = ArchiveFile.builder()
                    .id(counter++)
                    .name(fileName)
                    .path(dir.getPath().substring(strip))
                    .type(fileType(fileName))
                    .mtime(f.lastModified())
                    .build();
                save(archiveFile);
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * @see org.ctan.site.stores.base.IndexingStore#updateIndex()
     */
    @Override
    public void updateIndex() throws CorruptIndexException, IOException {

        updateArchive(archive);
    }
}