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