License3Resource.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.resources.catalogue;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

import org.ctan.site.domain.catalogue.License;
import org.ctan.site.services.content.ContentService;
import org.ctan.site.services.content.ContentService.ContentPageTo;
import org.ctan.site.stores.LicenseStore;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.dropwizard.hibernate.UnitOfWork;
import jakarta.annotation.security.PermitAll;
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.Builder;
import lombok.Getter;
import lombok.NonNull;

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

    /**
     * The class <code>LicenseTo</code> contains the transport object for the
     * license resource in the licenses list.
     */
    @Getter
    @Builder
    protected static class LicenseTo {

        private String key;

        private String name;

        private String intro;

        private String text;

        private String type;

        private Boolean selectable;

        private String url;
    }

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

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

    /**
     * This is the constructor for the class <code>Pkg3Resource</code>.
     *
     * @param store the underlying store
     * @param contentService the underlying content service
     */
    @SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
    public License3Resource(@NonNull LicenseStore store,
        @NonNull ContentService contentService) {

        this.store = store;
        this.contentService = contentService;
    }

    /**
     * The method <code>getAllLicenses</code> provides means to retrieve a list
     * of all licenses.
     *
     * @return a list of matching license summaries
     */
    @GET
    @Path("/licenses")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public List<LicenseTo> getAllLicenses() {

        return getLicenses("");
    }

    /**
     * The method <code>getLicenseByKey</code> contains the endpoint for
     * /license/key. It returns the license object.
     *
     * @param key the key
     * @param lang the locale
     * @return the license object
     */
    @GET
    @Path("/license/{key}")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public LicenseTo getLicenseByKey(@NonNull @PathParam("key") String key,
        @QueryParam("lang") String lang) {

        var license = store.getByKey(key);
        if (license == null) {
            throw new WebApplicationException(Status.NOT_FOUND);
        }
        ContentPageTo intro;
        ContentPageTo text;
        try {
            intro = contentService.getText("terms/" + key, lang);
            text = contentService.getText("terms/" + key + "-text", lang);
        } catch (IOException e) {
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }
        return LicenseTo.builder()
            .key(license.getKey())
            .name(license.getName())
            .selectable(license.getSelectable())
            .intro(intro != null ? intro.getContent() : null)
            .text(text != null ? text.getContent() : null)
            .type(licenseType(license))
            .url(license.getUrl())
            .build();
    }

    /**
     * The method <code>getLicenses</code> provides means to retrieve a list of
     * licenses starting with a given pattern.
     *
     * @param pattern the initial string of the key
     * @return a list of matching license summaries
     */
    @GET
    @Path("/licenses/{pattern}")
    @PermitAll
    @UnitOfWork(value = "siteDb")
    public List<LicenseTo> getLicenses(@PathParam("pattern") String pattern) {

        return store.findAllByKeyStartingWith(pattern != null ? pattern : "")
            .stream()
            .map(license -> {
                return LicenseTo.builder()
                    .key(license.getKey())
                    .name(license.getName())
                    .url(license.getUrl())
                    .type(licenseType(license))
                    .selectable(license.getSelectable())
                    .build();
            })
            .collect(Collectors.toList());
    }

    /**
     * The method <code>licenseType</code> provides means to determine the
     * license type.
     *
     * @param license the license
     * @return the type: <br/>
     *     <code>free</code> <br/>
     *     <code>nonfree</code> <br/>
     *     <code>unknown</code>
     */
    private String licenseType(License license) {

        var f = license.getFree();
        return f == null
            ? "unknown"
            : f ? "free" : "nonfree";
    }
}