AbstractIndexingStore.java

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

import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.util.List;

import org.apache.lucene.index.CorruptIndexException;
import org.ctan.site.CtanConfiguration.CtanConfig;
import org.ctan.site.services.search.base.IndexType;
import org.ctan.site.services.search.base.IndexingSession;
import org.ctan.site.services.search.base.Searchable;
import org.hibernate.SessionFactory;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.dropwizard.util.Generics;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

/**
 * The class <code>AbstractIndexingStore</code> contains the abstract repository
 * for CRUD operations on an indexed entity.
 *
 * @param <T> the type of the underlying entity
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
@Slf4j
public abstract class AbstractIndexingStore<T extends Searchable>
    extends
        AbstractStore<T>
    implements
        IndexingStore {

    /**
     * The field <code>simpleName</code> contains the name of the entity.
     */
    private String simpleName;

    /**
     * The field <code>indexingSession</code> contains the indexing session.
     */
    private IndexingSession indexingSession;

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

        super(sessionFactory);
        this.indexingSession = indexingSession;
        this.simpleName = ((Class<T>) ((ParameterizedType) getClass()
            .getGenericSuperclass()).getActualTypeArguments()[0])
                .getSimpleName();
    }

    /**
     * The method <code>count</code> provides means to count all entities in the
     * database.
     *
     * @return the total number of entities
     */
    public Long count() {

        Class<?> entityClass = Generics.getTypeParameter(getClass());
        CriteriaBuilder qb = currentSession().getCriteriaBuilder();
        CriteriaQuery<Long> cq = qb.createQuery(Long.class);
        cq.select(qb.count(cq.from(entityClass)));
        return currentSession().createQuery(cq).getSingleResult();
    }

    /**
     * The method <code>findAll</code> provides means to find a list of all
     * entries.
     *
     * @return the list of entries
     */
    public List<T> findAll() {

        Class<?> entityClass = Generics.getTypeParameter(getClass());
        var query = criteriaQuery();
        query.from(entityClass);
        return list(query);
    }

    /**
     * The method <code>indexType</code> provides means to retrieve the index
     * type.
     *
     * @return the index type
     */
    protected abstract IndexType indexType();

    /**
     * The method <code>remove</code> provides means to remove an entity. A new
     * session is created and closed at the end.
     *
     * @param id the id
     * @return {@code true} iff something has been removed
     */
    @Override
    public boolean remove(@NonNull Long id) {

        T entity = get(id);
        if (entity == null) {
            return false;
        }
        return remove(entity);
    }

    /**
     * The method <code>remove</code> provides means to remove an entity. A new
     * session is created and closed at the end.
     *
     * @param entity the entity
     * @return {@code true} iff something has been removed
     */
    @Override
    public boolean remove(@NonNull T entity) {

        String indexPath = entity.indexPath();
        for (var it : CtanConfig.LOCALES) {
            try {
                indexingSession.remove(indexType(), indexPath, it);
            } catch (IOException e) {
                log.error(e.toString());
            }
        }
        return super.remove(entity);
    }

    /**
     * The method <code>save</code> provides means to store an entity in the
     * database.
     *
     * @param entity the entity
     * @return the updated entity
     */
    @Override
    public T save(@NonNull T entity) {

        try {
            entity.updateIndex(indexingSession);
        } catch (IOException e) {
            log.error(e.toString());
        }
        return persist(entity);
    }

    /**
     * The method <code>updateIndex</code> provides means to update all entries
     * in the database. Note that entries which are gone are not removed!
     *
     * @throws IOException in case of an I/O error
     * @throws CorruptIndexException in case of an error in an index
     */
    @Override
    public void updateIndex() throws CorruptIndexException, IOException {

        for (var it : findAll()) {
            it.updateIndex(indexingSession);
        }
    }
}