Topic.java
/*
* Copyright © 2012-2026 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.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 org.ctan.site.services.util.NullCheck;
import com.google.common.collect.ImmutableMap;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded;
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;
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 class <code>TopicTexts</code> contains the language specific texts
* for a topic.
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Embeddable
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
public static class TopicTexts {
/**
* The field <code>title</code> contains the title in the given
* language.
*/
@Column(length = 512, nullable = false)
private String title;
/**
* The field <code>description</code> contains the description in the
* given language.
*/
@Column(length = 2048, nullable = false)
private String description;
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return title;
}
}
/**
* 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>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>detailsEn</code> contains the English details.
*/
@Embedded
@Default
@AttributeOverrides({
@AttributeOverride(name = "description",
column = @Column(name = "en_description")),
@AttributeOverride(name = "title", column = @Column(name = "en_title"))
})
private TopicTexts textsEn = new TopicTexts();
/**
* The field <code>detailsDe</code> contains the German details.
*/
@Embedded
@Default
@AttributeOverrides({
@AttributeOverride(name = "description",
column = @Column(name = "de_description")),
@AttributeOverride(name = "title", column = @Column(name = "de_title"))
})
private TopicTexts textsDe = new TopicTexts();
/**
* The field <code>detailsFr</code> contains the French details.
*/
@Embedded
@Default
@AttributeOverrides({
@AttributeOverride(name = "description",
column = @Column(name = "fr_description")),
@AttributeOverride(name = "title", column = @Column(name = "fr_title"))
})
private TopicTexts textsFr = new TopicTexts();
/**
* 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) {
TopicTexts it = getTopicTexts(lang);
return it == null ? "" : NullCheck.or(it.getDescription(), "");
}
/**
* 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) {
TopicTexts it = getTopicTexts(lang);
return it == null ? "" : NullCheck.or(it.getTitle(), "");
}
/**
* The method <code>getTopicTexts</code> provides means to retrieve the text
* of a topic in a given language.
*
* @param lang the two-letter language code
* @return the detail
*/
public TopicTexts getTopicTexts(String lang) {
return switch (lang) {
case null -> textsEn;
case "en" -> textsEn;
case "de" -> textsDe;
case "fr" -> textsFr;
default -> textsEn;
};
}
/**
* {@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,
"number", number,
"description", getDescription(locale));
}
/**
* {@inheritDoc}
*
* @see org.ctan.site.services.search.base.Searchable#updateIndex(org.ctan.site.services.search.base.IndexingSession,
* java.lang.String)
*/
@Override
public void updateIndex(IndexingSession session, String locale)
throws CorruptIndexException,
IOException {
session.updateIndex(indexPath(),
IndexArgs.builder()
.type(IndexType.TOPICS)
.locale(locale)
.title(getTitle(locale))
.display(getDescription(locale))
.content(new String[]{
key,
getTitle(locale),
getDescription(locale)})
.build());
}
}