BibtexService.java

/*
 * Copyright © 2018-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.services.catalogue;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.ctan.markup.html2latex.Html2Latex;
import org.ctan.markup.html2latex.LinkManager;
import org.ctan.site.domain.catalogue.Pkg;
import org.ctan.site.services.DateUtils;
import org.ctan.site.stores.PkgStore;

import com.google.common.base.Strings;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.NonNull;

/**
 * This service provides special methods for dealing with BibTeX files.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
public class BibtexService {

    /**
     * The configuration for the BibTeX service.
     */
    @Data
    @Builder
    @AllArgsConstructor
    public static class BibtexConfig {

        /**
         * This parameter contains the BibTeX type to produce. The default is
         * 'online'.
         */
        private String type;

        /**
         * If set to 'true' then the abstracts of the entries are included. The
         * default is 'true'.
         */
        @Default
        private boolean addAbstract = true;

        /**
         * If set to 'true' then the transitive closure is computed. The default
         * is 'true'.
         */
        @Default
        private boolean transitiveClosure = true;

        /**
         * If set to 'true' then the used string definitions are included. The
         * default is 'true'.
         */
        @Default
        private boolean useString = true;

        /**
         * This parameter is added before any key.
         */
        @Default
        private String keyPrefix = "pkg:";
    }

    /**
     * The link manager implementation for the BibTeX service.
     */
    private class BibtexLinkManager implements LinkManager {

        /**
         * The field <code>references</code> contains the keys of the already
         * processed entries.
         */
        private Map<String, Boolean> references = new HashMap<>();

        /**
         * The field <code>todo</code> contains the list of unprocessed package
         * keys.
         */
        private List<Package> todo = new ArrayList<>();

        /**
         * The field <code>cfg</code> contains the configuration.
         */
        private BibtexConfig cfg;

        /**
         * The field <code>count</code> contains the number of items processed.
         */
        private int count = 0;

        /**
         * The field <code>locked</code> contains the indicator whether the
         * processing is locked.
         */
        private boolean locked = false;

        /**
         * The field <code>buffer</code> contains the output target.
         */
        private StringBuilder buffer;

        /**
         * The field <code>html2Latex</code> contains the transformer.
         */
        private Html2Latex html2Latex;

        /**
         * Creates a new object.
         *
         * @param cfg the configuration
         * @param buffer the target buffer
         */
        public BibtexLinkManager(BibtexConfig cfg, StringBuilder buffer) {

            this.cfg = cfg;
            this.buffer = buffer;
            this.html2Latex = new Html2Latex(this);
        }

        /**
         * Add some links.
         *
         * @param links the links to add
         *
         * @return the current instance
         */
        public BibtexLinkManager add(Pkg... links) {

            if (locked) {
                return this;
            }
            for (var it : links) {
                todo.add(new Package(it.getKey(), it));
            }
            return this;
        }

        /**
         * Add some links.
         *
         * @param links the links to add
         *
         * @return the current instance
         */
        public BibtexLinkManager add(String... links) {

            if (locked) {
                return this;
            }
            for (var it : links) {
                todo.add(new Package(it, null));
            }
            return this;
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markup.html2latex.LinkManager#lock(boolean)
         */
        @Override
        public LinkManager lock(boolean locked) {

            this.locked = locked;
            return this;
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markup.html2latex.LinkManager#see(java.lang.String)
         */
        @Override
        public String see(String link) {

            if (locked) {
                return null;
            }
            link = link.replaceFirst("^ctan:pkg:", "");
            var lnk = references.get(link);
            if (lnk != null && lnk) {
                return link;
            }
            for (var it : todo) {
                if (link.equals(it.key)) {
                    return link;
                }
            }
            todo.add(new Package(link, null));
            return null;
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markup.html2latex.LinkManager#seen(StringBuilder)
         */
        @Override
        public String seen() {

            while (!todo.isEmpty()) {
                var pkgName = todo.remove(0);
                var pkg = pkgName.pkg != null
                    ? pkgName.pkg
                    : store.getByKey(pkgName.key);
                if (pkg != null) {
                    references.put(pkg.getKey(), Boolean.TRUE);
                    toBibtex(pkg, buffer);
                    count++;
                    // if (count % 100 == 1) {
                    // log.warn("--- " + count);
                    // }
                }
            }
            return count == 0 ? "" : buffer.toString();
        }

        /**
         * Write one package as BibTeX.
         *
         * @param pkg the package
         * @param buffer the target buffer
         */
        private void toBibtex(Pkg pkg, StringBuilder buffer) {

            var type = cfg.getType();
            buffer.append('@')
                .append(type != null ? type : "online")
                .append("{ ")
                .append(cfg.getKeyPrefix())
                .append(pkg.getKey())
                .append(",\n");
            var authors = pkg.getAuthors();
            if (authors != null && !authors.isEmpty()) {
                buffer.append("  author =\t  {")
                    .append(authors.stream()
                        .map(
                            it -> html2Latex.convert(it.getAuthor().toString()))
                        .collect(Collectors.joining(" and ")))
                    .append("},\n");
            }
            var name = pkg.getName();
            buffer.append("  title =\t  {")
                .append(html2Latex.convert(name != null ? name : pkg.getKey()));
            var caption = pkg.getCaption("en");
            if (caption != null
                && !Strings.isNullOrEmpty(caption.getCaption())) {
                buffer.append(" -- ")
                    .append(html2Latex.convert(caption.getCaption()));
            }
            buffer.append("},\n");
            var versionNumber = pkg.getVersionNumber();
            if (!Strings.isNullOrEmpty(versionNumber)) {
                buffer.append("  version =\t  {")
                    .append(versionNumber)
                    .append("},\n");
            }
            var versionDate = pkg.getVersionDate();
            if (!Strings.isNullOrEmpty(versionDate)) {
                buffer.append("  date =\t  {")
                    .append(versionDate)
                    .append("},\n");
            }
            if (cfg.isAddAbstract() && !pkg.getDescriptions().isEmpty()) {
                buffer.append("  abstract =  {")
                    .append(
                        html2Latex.convert(
                            pkg.getDescriptions().get(0).getDescription())
                            .replaceAll("\\\\cite\\{ctan:pkg:",
                                "\\\\cite{" + cfg.keyPrefix))
                    .append("},\n");
            }
            buffer.append("  url = \t  {https://ctan.org/pkg/")
                .append(pkg.getKey())
                .append("},\n")
                .append("  urldate =\t  {")
                .append(
                    DateUtils.LOCAL_DATE_FORMATTER.format(LocalDateTime.now()))
                .append("},\n")
                .append("  organization = ")
                .append(cfg.useString
                    ? "CTAN\n"
                    : "{Comprehensive \\TeX{} Archive Network}\n")
                .append("}\n\n");
        }
    }

    /**
     * The class <code>Package</code> contains the intermediate object for a
     * package to be processed further.
     */
    @Data
    @AllArgsConstructor
    private class Package {

        /**
         * The field <code>key</code> contains the reference key for the
         * package.
         */
        String key;

        /**
         * The field <code>pkg</code> contains the optional package data.
         */
        Pkg pkg;
    }

    /**
     * The field <code>store</code> contains the package store.
     */
    private PkgStore store;

    /**
     * This is the constructor for <code>BibtexService</code>.
     *
     * @param store the package store
     */
    @SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
    public BibtexService(@NonNull PkgStore store) {

        this.store = store;
    }

    /**
     * Generate a BibTeX file for all packages.
     *
     * @param cfg the configuration
     *
     * @return the content of the BibTeX file
     */
    public String toBibtex(BibtexConfig cfg) {

        return toBibtex(cfg, store.findAll().toArray(new Pkg[]{}));
    }

    /**
     * Generate a BibTeX file for a list of packages.
     *
     * @param cfg the configuration
     * @param packages the array of package names to include
     *
     * @return the content of the BibTeX file
     */
    public String toBibtex(BibtexConfig cfg, Pkg... packages) {

        if (packages == null) {
            return "";
        }
        if (cfg == null) {
            cfg = BibtexConfig.builder().build();
        }
        var buffer = new StringBuilder();
        if (cfg.isUseString()) {
            buffer.append(
                "@STRING{CTAN=\"Comprehensive \\TeX{} Archive Network\"}\n\n");
        }
        var manager = new BibtexLinkManager(cfg, buffer)
            .add(packages)
            .lock(cfg.isTransitiveClosure());
        return manager.seen();
    }

    /**
     * Generate a BibTeX file for a list of packages.
     *
     * @param cfg the configuration
     * @param pkgNames the array of package names to include
     *
     * @return the content of the BibTeX file
     */
    public String toBibtex(BibtexConfig cfg, String... pkgNames) {

        if (pkgNames == null) {
            return "";
        }
        if (cfg == null) {
            cfg = BibtexConfig.builder().build();
        }
        var buffer = new StringBuilder();
        if (cfg.isUseString()) {
            buffer.append(
                "@STRING{CTAN=\"Comprehensive \\TeX{} Archive Network\"}\n\n");
        }
        var manager = new BibtexLinkManager(cfg, buffer)
            .add(pkgNames)
            .lock(cfg.isTransitiveClosure());
        return manager.seen();
    }
}