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);
}
}