XmlPkgResource.java

/*
 * Copyright © 2024-2025 The CTAN Team and individual pkgs
 *
 * This file is distributed under the 3-clause BSD license.
 * See file LICENSE for details.
 */
package org.ctan.site.resources.catalogue.api;

import java.util.List;

import org.ctan.site.domain.catalogue.Pkg;
import org.ctan.site.domain.catalogue.PkgCaption;
import org.ctan.site.domain.catalogue.PkgDescription;
import org.ctan.site.stores.PkgStore;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.dropwizard.hibernate.UnitOfWork;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response.Status;
import lombok.NonNull;

/**
 * The class <code>XmlPkgResource</code> contains the controller for the pkg
 * resource.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
@Path("/")
@Produces(MediaType.APPLICATION_XML)
public class XmlPkgResource {

    /**
     * The field <code>store</code> contains the underlying repository.
     */
    private PkgStore store;

    /**
     * This is the constructor for the class <code>XmlPkgResource</code>.
     *
     * @param store the underlying store
     */
    @SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
    public XmlPkgResource(@NonNull PkgStore store) {

        this.store = store;
    }

    /**
     * The method <code>getPkgByKey</code> provides means to retrieve an pkg.
     *
     * @param vers the version
     * @param id the key of the pkg
     * @param ref the indicator whether or not to return the references to the
     *     packages pkged by the pkg
     * @return an pkg or {@code null}
     */
    @GET
    @Path("/xml/{vers}/pkg/{id}")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public String getPkgByKey(
        @NonNull @PathParam("vers") String vers,
        @NonNull @PathParam("id") String id,
        @QueryParam("ref") @DefaultValue("false") Boolean ref,
        @QueryParam("no-dtd") @DefaultValue("false") Boolean noDtd,
        @QueryParam("no-xml") @DefaultValue("false") Boolean noXml) {

        switch (vers) {
            case "1.0", "1.1", "1.2", "1.3", "2.0", "2.1", "3.0":
                break;
            default:
                throw new WebApplicationException(Status.NOT_FOUND);
        }
        var pkg = store.getByKey(id);
        if (pkg == null) {
            throw new WebApplicationException(Status.NOT_FOUND);
        }
        var xml = new XmlWriter();
        if (!noXml) {
            xml.outNl("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        }
        if (!noDtd) {
            xml.outNl("<!DOCTYPE entry SYSTEM 'http://www.ctan.org/xml/" + vers
                + "/catalogue.dtd'>");
        }
        xml.outNl("<entry id=\"", id, "\">");
        xml.outFullTag("name", pkg.getName());
        PkgCaption caption = pkg.getCaption("en");
        xml.outFullTag("caption",
            caption == null ? null : caption.getCaption());
        var authors = pkg.getAuthors();
        if (authors != null) {
            for (var a : authors) {
                var author = a.getAuthor();
                xml.out("  <authorref id=\"", author.getKey(), "\"");
                // TODO
                // xml.outIf("familyname=", author.getFamilyname());
                // xml.outIf("givenname=", author.getGivenname());
                xml.outNl(" />");
            }
        }
        var copy = pkg.getCopy();
        if (copy != null) {
            for (var cpy : pkg.getCopy()) {
                xml.outNl("  <copyright owner=\"", cpy.getOwner(), "\" year=\"",
                    cpy.getYear(), "\" />");
            }
        }
        var licenses = pkg.getLicenses();
        if (licenses != null) {
            for (var lic : licenses) {
                xml.outNl("  <license type=\"", lic.getKey(), "\" >");
            }
        }
        if (pkg.getVersionNumber() != null || pkg.getVersionDate() != null) {
            xml.out("  <version");
            xml.outIf("number", pkg.getVersionNumber());
            xml.outIf("date", pkg.getVersionDate());
            xml.outNl(" >");
        }
        PkgDescription description = pkg.getDescription("en");
        xml.outFullTag("description",
            description == null
                ? null
                : description.getDescription());
        var docs = pkg.getDocs();
        if (docs != null) {
            for (var doc : docs) {
                xml.out("  <documentation");
                xml.outIf("details", doc.getDetails());
                xml.outIf("href", doc.getHref());
                xml.outIf("lang", doc.getLang());
                xml.outNl(" >");
            }
        }
        if (isIn(vers, "1.2", "1.3", "2.0", "2.1")) {
            xml.outFullTag("home", pkg.getHome());
            xml.out("  <contact type=\"support\" href=\"", pkg.getSupport(),
                "\" />\n");
            xml.out("  <contact type=\"bugs\" href=\"", pkg.getBugs(),
                "\" />\n");
            xml.out("  <contact type=\"announce\" href=\"", pkg.getAnnounce(),
                "\" />\n");
            xml.out("  <contact type=\"repository\" href=\"",
                pkg.getRepository(),
                "\" />\n");
            xml.out("  <contact type=\"development\" href=\"",
                pkg.getDevelopment(),
                "\" />\n");
        }
        var ctanPath = pkg.getCtanPath();
        if (ctanPath != null) {
            xml.out("  <ctan path=\"", ctanPath, "\"");
            xml.outNl(">");
        }
        var installPath = pkg.getInstallPath();
        if (installPath != null) {
            xml.outNl("  <install path=\"", installPath, "\">");
        }
        xml.outTagWithAttribute("miktex", "location", pkg.getMiktexLocation());
        xml.outTagWithAttribute("texlive", "location",
            pkg.getTexliveLocation());
        var topics = pkg.getTopics();
        if (topics != null) {
            for (var top : topics) {
                xml.outNl("  <keyval key=\"topic\" value=\"", top.getKey(),
                    "\" />");
            }
        }
        // if (isIn(vers, "2.0")) { // TODO alias
        // }
        List<Pkg> also = pkg.getAlso();
        if (also != null && isIn(vers, "3.0")) {
            for (var it : also) {
                xml.outNl("  <also id=\"", it.getKey(), "\" />");
            }
        }
        xml.outNl("</entry>");
        return xml.toString();
    }

    /**
     * The method <code>getPkgs</code> provides means to retrieve a list of pkgs
     * starting with a given pattern.
     *
     * @param vers the version number
     * @param key the key
     * @return a list of matching pkg summaries
     */
    @GET
    @Path("/xml/{vers}/pkgs")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public String getPkgs(
        @NonNull @PathParam("vers") String vers,
        @QueryParam("key") String key,
        @QueryParam("no-dtd") @DefaultValue("false") Boolean noDtd,
        @QueryParam("no-xml") @DefaultValue("false") Boolean noXml) {

        switch (vers) {
            case "1.0", "1.1", "1.2", "1.3", "2.0", "2.1":
                break;
            default:
                throw new WebApplicationException(Status.NOT_FOUND);
        }
        var xml = new XmlWriter();
        if (!noXml) {
            xml.outNl("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        }
        if (!noDtd) {
            xml.outNl(
                "<!DOCTYPE packages SYSTEM 'http://www.ctan.org/xml/" + vers
                    + "/catalogue.dtd'>");
        }
        var list = store.findAllByKeyStartingWith(key != null ? key : "");
        xml.outNl("<packages>");
        for (var p : list) {
            xml.out("  <package key=\"", p.getKey(), "\"");
            xml.outIf("name", p.getName());
            xml.outIf("caption", p.getCaption("en").getCaption());
            xml.outNl(" />");
        }
        xml.outNl("</packages>");
        return xml.toString();
    }

    /**
     * The method <code>isIn</code> provides means to check whether a string is
     * contained in a list of candidates.
     *
     * @param a the reference
     * @param args array of varargs
     * @return {@code true} iff the reference is found
     */
    private boolean isIn(String a, String... args) {

        for (var x : args) {
            if (a.equals(x)) {
                return true;
            }
        }
        return false;
    }
}