Pkg.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.domain.catalogue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.lucene.index.CorruptIndexException;
import org.ctan.site.CtanConfiguration.CtanConfig;
import org.ctan.site.domain.AbstractEntity;
import org.ctan.site.services.search.base.IndexType;
import org.ctan.site.services.search.base.IndexingSession;
import org.ctan.site.services.search.base.IndexingSession.IndexArgs;
import org.ctan.site.services.search.base.Searchable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import lombok.AllArgsConstructor;
import lombok.Builder.Default;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* The domain class <code>Pkg</code> contains the description of a package in
* the Catalogue.
*
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public class Pkg extends AbstractEntity implements Searchable {
/**
* The field <code>key</code> contains the unique reference key for the
* package.
*
* <p>
* By convention the key starts with a lower-case letter followed by
* lower-case letters, digits or the minus sign.
*/
@Column(length = 64, unique = true, nullable = false)
private String key;
/**
* The field <code>name</code> contains the printable name of the package.
* As a fallback the key should be used.
*/
@Column(length = 64, nullable = false)
private String name;
/**
* The field <code>home</code> contains the optional link for the home page
* of the package.
*/
@Column(length = 255)
private String home;
/**
* The field <code>bugs</code> contains the optional link for the bugs
* address.
*/
@Column(length = 255)
private String bugs;
/**
* The field <code>support</code> contains the optional link for the support
* address.
*/
@Column(length = 255)
private String support;
/**
* The field <code>repository</code> contains the optional link to the
* source code repository.
*/
@Column(length = 255)
private String repository;
/**
* The field <code>announce</code> contains the optional link to the
* announcement mailing list.
*/
@Column(length = 255)
private String announce;
/**
* The field <code>development</code> contains the optional link to the
* developer's mailing list.
*/
@Column(length = 255)
private String development;
/**
* The field <code>versionNumber</code> contains the version number.
*/
@Column(length = 32, name = "version_number")
private String versionNumber;
/**
* The field <code>versionDate</code> contains the version date.
*/
@Column(length = 32, name = "version_date")
private String versionDate;
/**
* The field <code>ctanPath</code> contains the location in the CTAN archive
* of the package.
*/
@Column(length = 256, name = "ctan_path")
@Default
private String ctanPath = null;
/**
* The field <code>ctanFile</code> contains the indicator that the package
* consists of a single file only.
*
* <p>
* This practice is deprecated and supported for backward-compatibility
* only.
*/
@Column(name = "ctan_file")
@Default
@EqualsAndHashCode.Exclude
private boolean ctanFile = false;
/**
* The field <code>ctanZip</code> contains the indicator that a zip file is
* present.
*/
@Column(name = "ctan_zip")
@Default
@EqualsAndHashCode.Exclude
private boolean ctanZip = false;
/**
* The field <code>miktexLocation</code> contains key of the package in the
* MiK<span>T<span style=
* "text-transform:uppercase;font-size:90%;vertical-align:-0.4ex;
* margin-left:-0.2em;margin-right:-0.1em;line-height: 0;" >e</span>
* X</span> distribution.
*/
@Column(length = 64, name = "miktex_location")
@Default
private String miktexLocation = null;
/**
* The field <code>texliveLocation</code> contains key of the package in the
* TeXlive distribution.
*/
@Column(length = 64, name = "texlive_location")
@Default
private String texliveLocation = null;
/**
* The field <code>tlContribLocation</code> contains key of the package in
* the distribution TeXlive contrib.
*/
@Column(length = 64, name = "tl_contrib_location")
@Default
private String tlContribLocation = null;
/**
* The field <code>extraIndex</code> contains extra words to be used for
* indexing.
*/
@Column(length = 4196, name = "extra_index")
@EqualsAndHashCode.Exclude
private String extraIndex;
/**
* The field <code>installPath</code> contains the install path.
*/
@Column(length = 4196, name = "install_path")
@Default
@EqualsAndHashCode.Exclude
private String installPath = null;
/**
* The field <code>authors</code> contains the references to the authors.
*/
@OneToMany(mappedBy = "pkg",
cascade = CascadeType.ALL,
orphanRemoval = true)
@Default
private List<AuthorRef> authors = new ArrayList();
/**
* The field <code>uploaders</code> contains the references to the
* uploaders.
*/
@OneToMany(mappedBy = "pkg",
cascade = CascadeType.ALL,
orphanRemoval = true)
@Default
@EqualsAndHashCode.Exclude
private List<UploaderRef> uploaders = new ArrayList();
// /**
// * The field <code>tdsPath</code> contains the TDS path.
// */
// @Default
// private String tdsPath = null;
// static transients = ['tdsPath']
/**
* The field <code>captions</code> contains the captions for different
* languages.
*/
@OneToMany(mappedBy = "pkg",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.EAGER)
@Default
@EqualsAndHashCode.Exclude
private List<PkgCaption> captions = new ArrayList();
/**
* The field <code>descriptions</code> contains the descriptions for
* different languages.
*/
@OneToMany(mappedBy = "pkg", cascade = CascadeType.ALL,
orphanRemoval = true)
@Default
@EqualsAndHashCode.Exclude
private List<PkgDescription> descriptions = new ArrayList();
/**
* The field <code>copy</code> contains the copyright infos. There might be
* several of them. For instance when the authors have changed over time.
*/
@OneToMany(mappedBy = "pkg", cascade = CascadeType.ALL,
orphanRemoval = true)
@Default
@EqualsAndHashCode.Exclude
private List<PkgCopyright> copy = new ArrayList();
/**
* The field <code>docs</code> contains the list of documents associated
* with the packages.
*/
@OneToMany(mappedBy = "pkg", cascade = CascadeType.ALL,
orphanRemoval = true)
@Default
@EqualsAndHashCode.Exclude
private List<PkgDoc> docs = new ArrayList();
/**
* The field <code>topics</code> contains the set of associated topics.
*/
@ManyToMany
@JoinTable(name = "pkg_topic", //
joinColumns = {@JoinColumn(name = "pkg_topics_id")}, //
inverseJoinColumns = {@JoinColumn(name = "topic_id")})
@Default
@EqualsAndHashCode.Exclude
private List<Topic> topics = new ArrayList<>();
/**
* The field <code>also</code> contains the associated references to other
* packages.
*/
@ManyToMany // (mappedBy = "pkg_also_id", cascade = CascadeType.ALL)
@JoinTable(name = "pkg_pkg")
@Default
@EqualsAndHashCode.Exclude
private List<Pkg> also = new ArrayList();
/**
* The field <code>licenses</code> contains the set of associated licenses.
*/
@ManyToMany
@JoinTable(name = "pkg_license", //
joinColumns = {@JoinColumn(name = "pkg_licenses_id")}, //
inverseJoinColumns = {@JoinColumn(name = "license_id")})
@Default
@EqualsAndHashCode.Exclude
private List<License> licenses = new ArrayList<>();
/**
* The field <code>aliases</code> contains the set of associated aliases.
*/
@OneToMany(mappedBy = "pkg")
// (mappedBy = "pkg_alias_id", cascade = CascadeType.ALL)
// @JoinTable(name = "pkg_pkg")
@Default
@EqualsAndHashCode.Exclude
private List<PkgAlias> aliases = new ArrayList<>();
/**
* Getter for a caption in a given language.
*
* @param languages the accepted languages in descending order
* @return the caption for the given language or <code>null</code>
*/
public PkgCaption getCaption(String... languages) {
if (captions == null) {
return null;
}
for (String lang : languages) {
for (PkgCaption cap : captions) {
if (lang.equals(cap.getLang())) {
return cap;
}
}
if ("en".equals(lang)) {
for (PkgCaption cap : captions) {
if (cap.getLang() == null) {
return cap;
}
}
}
}
return null;
}
/**
* Getter for a description in a given language.
*
* @param languages the languages
* @return the description for the given language or <code>null</code>
*/
public PkgDescription getDescription(String... languages) {
for (String lang : languages) {
for (PkgDescription desc : descriptions) {
if (lang.equals(desc.getLang())) {
return desc;
}
}
if ("en".equals(lang)) {
for (PkgDescription desc : descriptions) {
if (desc.getLang() == null) {
return desc;
}
}
}
}
return null;
}
/**
* The method <code>getVers</code> provides means to retrieve the combined
* fields versionNumber and versionDate. The fields are combined with a
* separating space if both are defined. Otherwise only one field is
* returned.
*
* @return the joined fields
*/
public String getVers() {
if (isEmpty(versionNumber)) {
return versionDate;
} else if (isEmpty(versionDate)) {
return versionNumber;
} else {
return versionNumber + " " + versionDate;
}
}
/**
* {@inheritDoc}
*
* @see org.ctan.site.services.search.base.Searchable#indexPath()
*/
@Override
public String indexPath() {
return "/pkg/" + key;
}
/**
* The method <code>isEmpty</code> provides means to check the argument for
* {@code null} or the empty string.
*
* @param s the string to check
* @return {@code true} if the string is {@code null} or empty
*/
private boolean isEmpty(String s) {
return s == null || "".equals(s);
}
/**
* The method <code>isObsolete</code> provides means to retrieve the
* information about the obsolete state.
*
* @return {@code true} iff the package is obsolete
*/
public boolean isObsolete() {
return ctanPath == null || ctanPath.startsWith("/obsolete");
}
/**
* The method <code>isOrphanted</code> provides means to determine whether
* the package is orphaned. This is the case if there is no author which is
* marked as active and who is not marked as dead.
*
* @return the indicator for orphaned packages
*/
public boolean isOrphaned() {
for (var it : authors) {
if (it.isActive() && !it.getAuthor().getDied()) {
return false;
}
}
return true;
}
/**
* The method <code>topicsAsString</code> provides means to retrieve the
* topic keys.
*
* @return the topic keys concatenated with "; "
*/
public String topicsAsString() {
return topics == null
? ""
: topics.stream()
.map(it -> it.getKey())
.collect(Collectors.joining("; "));
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return key;
}
/**
* {@inheritDoc}
*
* @see org.ctan.site.services.search.base.Searchable#updateIndex(IndexingSession)
*/
@Override
public void updateIndex(IndexingSession session)
throws CorruptIndexException,
IOException {
for (var locale : CtanConfig.LOCALES) {
PkgCaption display = getCaption(locale);
session.updateIndex(indexPath(),
IndexArgs.builder()
.type(IndexType.PKG)
.locale(locale)
.title(name != null ? key : name)
.display(display.getCaption())
.content(new String[]{
key,
name,
display.getCaption(),
getDescription(locale).getDescription()})
.build()); // TODO provide other args
}
}
}