Topic.java
/*
* Copyright © 2012-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 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 com.google.common.collect.ImmutableMap;
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.ManyToOne;
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>Topic</code> represents a topic in the Catalogue.
*
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public class Topic extends AbstractEntity implements Searchable {
/**
* The field <code>key</code> contains the unique reference key for the
* topic. It is used to construct the URL for the topic.
*
* <p>
* By convention the topic key are made up of lower case letters, digits and
* the minus sign.
* </p>
*/
@Column(length = 64, unique = true, nullable = false)
private String key;
/**
* The field <code>details</code> contains the short abstract text in
* English for the topic.
*
* @deprecated use topicDetails instead
*/
@Deprecated
@Column(length = 255, name = "details")
private String oldDetails;
// /**
// * The field <code>title</code> contains the title text in English of the
// * topic.
// *
// * @deprecated use topicDetails instead
// */
// @Deprecated
// @Column(length = 255)
// private String title;
/**
* The field <code>number</code> contains the number of packages contained
* tagged with the topic. It is a cached value for performance.
*/
@Column
@Default
@EqualsAndHashCode.Exclude
private long number = 0;
/**
* The field <code>parent</code> contains the parent topic in the
* inheritance hierarchy. It can be {@code null} for the top-level node.
*/
@ManyToOne
@EqualsAndHashCode.Exclude
private Topic parent;
/**
* The field <code>alias</code> contains an optional alias for the topic.
*/
@ManyToOne
@EqualsAndHashCode.Exclude
private Topic alias;
/**
* The field <code>details</code> contains detail descriptions for the topic
* in different languages.
*/
@OneToMany(cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.EAGER)
@JoinColumn(name = "topic_id")
@Default
@EqualsAndHashCode.Exclude
private List<TopicDetail> details = new ArrayList<TopicDetail>();
/**
* The field <code>children</code> contains the children of the topic in the
* topics tree.
*/
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "parent_id")
@Default
@EqualsAndHashCode.Exclude
private List<Topic> children = new ArrayList<Topic>();
/**
* The field <code>aliases</code> contains the list of aliases.
*/
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "alias_id")
@Default
@EqualsAndHashCode.Exclude
private List<Topic> aliases = new ArrayList<Topic>();
/**
* The field <code>packages</code> contains the set of associated packages.
*/
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "pkg_topic", //
joinColumns = {@JoinColumn(name = "topic_id")}, //
inverseJoinColumns = {@JoinColumn(name = "pkg_topics_id")})
@Default
@EqualsAndHashCode.Exclude
private List<Pkg> packages = new ArrayList<>();
/**
* The method <code>getDescription</code> provides means to retrieve the
* description of a topic in a given language.
*
* @param lang the two-letter language code
* @return the description or the empty string
*/
public String getDescription(String lang) {
TopicDetail it = getTopicDetail(lang);
return it == null ? "" : it.getDescription();
}
/**
* The method <code>getDetails</code> provides means to retrieve the details
* text of a topic in a given language.
*
* @param lang the two-letter language code
* @return the details text or the empty string
*/
public String getDetails(String lang) {
TopicDetail it = getTopicDetail(lang);
if (it == null) {
return oldDetails != null ? oldDetails : "";
}
return it.getDetail();
}
/**
* The method <code>getTeaser</code> provides means to retrieve the teaser
* in a given language.
*
* @param lang the two-letter language code
* @return the teaser or the empty string
*/
public String getTeaser(String lang) {
TopicDetail it = getTopicDetail(lang);
return it == null ? "" : it.getTeaser();
}
/**
* The method <code>getTitle</code> provides means to retrieve the title in
* a given language.
*
* @param lang the two-letter language code
* @return the title or the empty string
*/
public String getTitle(String lang) {
TopicDetail it = getTopicDetail(lang);
return it == null ? "" : it.getTitle();
}
/**
* The method <code>getDetail</code> provides means to retrieve the detail
* text of a topic in a given language.
*
* @param lang the two-letter language code
* @return the detail
*/
public TopicDetail getTopicDetail(String lang) {
TopicDetail en = null;
for (TopicDetail it : details) {
if (it.getLang().equals(lang)) {
return it;
}
if (it.getLang().equals("en")) {
en = it;
}
}
return en != null ? en : null;
}
/**
* {@inheritDoc}
*
* @see org.ctan.site.services.search.base.Searchable#indexPath()
*/
@Override
public String indexPath() {
return "/topic/" + key;
}
/**
* The method <code>toMap</code> provides means to get the instance as an
* immutable Map.
*
* @return the Map
*/
public ImmutableMap<String, Object> toMap() {
return toMap("en");
}
/**
* The method <code>toMap</code> provides means to get the instance as an
* immutable Map.
*
* @param locale the language code
* @return the Map
*/
public ImmutableMap<String, Object> toMap(String locale) {
return ImmutableMap.of("id", (Object) getId(),
"lang", locale,
"key", key,
"details", getDetails(locale));
}
/**
* {@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) {
session.updateIndex(indexPath(),
IndexArgs.builder()
.type(IndexType.TOPICS)
.locale(locale)
.title(getTitle(locale))
.display(getTeaser(locale))
.content(new String[]{
key,
getTitle(locale),
getTeaser(locale),
getDescription(locale),
getDetails(locale)})
.build());
}
}
// static belongsTo = [ parent: Topic, alias: Topic ]
// static hasMany = [ topicDetails: TopicDetail,
// children: Topic,
// aliases: Topic ]
// static mappedBy = [ children: 'parent',
// aliases: 'alias' ]
}