PkgService.java

/*
 * Copyright © 2024-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.texarchive;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.ctan.site.CtanConfiguration;
import org.ctan.site.domain.catalogue.AuthorRef;
import org.ctan.site.domain.catalogue.License;
import org.ctan.site.domain.catalogue.Pkg;
import org.ctan.site.domain.catalogue.PkgCaption;
import org.ctan.site.domain.catalogue.PkgCopyright;
import org.ctan.site.domain.catalogue.PkgDescription;
import org.ctan.site.domain.catalogue.PkgDoc;
import org.ctan.site.domain.catalogue.Topic;
import org.ctan.site.services.content.ContentService;
import org.ctan.site.services.content.ContentService.TeaserType;
import org.ctan.site.services.util.ConfigUtils;
import org.ctan.site.stores.PkgStore;

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

/**
 * The class <code>TexarchiveService</code> contains the service to access the
 * <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>
 * archive directory.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
public class PkgService {

    /**
     * The class <code>PkgTo</code> contains a transport object for the package.
     */
    @Getter
    @Builder
    @AllArgsConstructor
    @SuppressFBWarnings(value = "EI_EXPOSE_REP")
    public static class PkgTo {

        private String key;

        private String name;

        private String caption;

        private String captionLang;

        private String description;

        private String descriptionLang;

        private String versionDate;

        private String versionNumber;

        private String miktexLocation;

        private String texliveLocation;

        private String tlContribLocation;

        private List<Map<String, String>> authors;

        private List<Map<String, String>> licenses;

        private List<Map<String, String>> copyrights;

        private List<Map<String, String>> docs;

        private boolean zip;

        private String install;

        private boolean hasTeaser;

        private List<Map<String, String>> topics;
    }

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

    /**
     * The field <code>defaultLang</code> contains the default language.
     */
    private String defaultLang;

    /**
     * The field <code>contentService</code> contains the content service.
     */
    private ContentService contentService;

    /**
     * This is the constructor for the class <code>PkgService</code>.
     *
     * @param config the CTAN configuration
     * @param contentService the content service
     * @param pkgStore the package store
     */
    @SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
    public PkgService(@NonNull CtanConfiguration config,
        @NonNull ContentService contentService,
        @NonNull PkgStore pkgStore) {

        this.contentService = contentService;
        this.pkgStore = pkgStore;
        this.defaultLang = ConfigUtils.defaultLanguage(config);
    }

    /**
     * The method <code>failsave</code> provides means to savely access a sting.
     *
     * @param s the string
     * @return s or the empty string if s is {@code null}
     */
    private String failsave(String s) {

        return s == null ? "" : s;
    }

    /**
     * The method <code>getPkgByKey</code> provides means to retrieve a package
     * by its key.
     *
     * @param key the package key
     * @param lang the language for the notes
     * @return the transport object or {@code null}
     */
    public PkgTo getPkgByKey(String key, String lang) {

        return ("".equals(key) || key == null
            ? null
            : mapPkg(pkgStore.getByKey(key), lang));
    }

    /**
     * The method <code>getPkgByPath</code> provides means to retrieve a package
     * by its CTAN path.
     *
     * @param path the CTAN path of the package
     * @param lang the language for the notes
     * @return the transport object or {@code null}
     */
    public PkgTo getPkgByPath(String path, String lang) {

        return ("".equals(path) || path == null
            ? null
            : mapPkg(pkgStore.getByCtanPath(path), lang));
    }

    /**
     * The method <code>mapAuthors</code> provides means to map persisted
     * authors to transport objects.
     *
     * @param authors the persisted author data
     * @return a list of transport objects for authors
     */
    private List<Map<String, String>> mapAuthors(List<AuthorRef> authors) {

        if (authors == null) {
            return List.of();
        }
        return authors.stream()
            .map(a -> Map.of("key", a.getAuthor().getKey(),
                "name", a.getAuthor().toString(),
                "isActive", a.isActive() ? "true" : "false"))
            .collect(Collectors.toList());
    }

    /**
     * The method <code>mapCopyright</code> provides means to map persisted
     * copyright infos to transport objects.
     *
     * @param copy the list of copyrights
     * @return the list of transport objects
     */
    private List<Map<String, String>> mapCopyright(List<PkgCopyright> copy) {

        if (copy == null) {
            return List.of();
        }
        return copy.stream()
            .map(a -> Map.of(
                "year", a.getYear(),
                "owner", a.getOwner()))
            .collect(Collectors.toList());
    }

    /**
     * The method <code>mapDocs</code> provides means to map persisted
     * documentation references to transport objects.
     *
     * @param docs the persisted documentation references
     * @return a list of transport objects for docs
     */
    private List<Map<String, String>> mapDocs(List<PkgDoc> docs) {

        if (docs == null) {
            return List.of();
        }
        return docs.stream()
            .map(doc -> Map.of(
                "title", failsave(doc.getTitle()),
                "lang", failsave(doc.getLang()),
                "href", failsave(doc.getHref()),
                "author", failsave(doc.getAuthor())))
            .collect(Collectors.toList());
    }

    /**
     * The method <code>mapLicenses</code> provides means to map persisted
     * licenses to transport objects.
     *
     * @param licenses the persisted license data
     * @return a list of transport objects for licenses
     */
    private List<Map<String, String>> mapLicenses(List<License> licenses) {

        if (licenses == null) {
            return List.of();
        }
        return licenses.stream()
            .map(a -> Map.of(
                "key", a.getKey(),
                "name", failsave(a.getName())))
            .collect(Collectors.toList());
    }

    /**
     * The method <code>mapPkg</code> provides means to map a persisted package
     * to a transport object.
     *
     * @param pkg the package or {@code null}
     * @param lang the language for the notes
     * @return the transport object
     */
    private PkgTo mapPkg(Pkg pkg, String lang) {

        if (pkg == null) {
            return null;
        }
        if (lang == null) {
            lang = defaultLang;
        }
        var key = pkg.getKey();
        PkgCaption caption = pkg.getCaption(lang);
        PkgDescription description = pkg.getDescription(lang);
        return PkgTo.builder()
            .key(key)
            .authors(mapAuthors(pkg.getAuthors()))
            .caption(caption == null ? null : caption.getCaption())
            .captionLang(caption == null ? null : caption.getLang())
            .copyrights(mapCopyright(pkg.getCopy()))
            .description(description == null
                ? null
                : description.getDescription())
            .descriptionLang(description == null
                ? null
                : description.getLang())
            .docs(mapDocs(pkg.getDocs()))
            .hasTeaser(contentService.hasTeaser(TeaserType.PKG, key))
            .licenses(mapLicenses(pkg.getLicenses()))
            .miktexLocation(pkg.getMiktexLocation())
            .name(pkg.getName() != null ? pkg.getName() : key)
            .texliveLocation(pkg.getTexliveLocation())
            .tlContribLocation(pkg.getTlContribLocation())
            .topics(mapTopics(pkg.getTopics(), lang))
            .versionDate(pkg.getVersionDate())
            .versionNumber(pkg.getVersionNumber())
            .zip(pkg.isCtanZip())
            .build();
    }

    /**
     * The method <code>mapTopics</code> provides means to map persisted topics
     * to transport objects.
     *
     * @param topics the list of topics or {@code null}
     * @param lang the locale
     * @return the list of transport objects
     */
    private List<Map<String, String>> mapTopics(List<Topic> topics,
        String lang) {

        if (topics == null) {
            return List.of();
        }
        return topics.stream()
            .map(t -> Map.of("key", t.getKey(),
                "title", failsave(t.getTitle(lang))))
            .collect(Collectors.toList());
    }
}