Ctan.java

/*
 * Copyright © 2022-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;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.EnumSet;

import org.ctan.site.command.InitSearchCommand;
import org.ctan.site.domain.account.User.CtanPrincipal;
import org.ctan.site.health.AppHealthCheck;
import org.ctan.site.health.ContentHealthCheck;
import org.ctan.site.health.CtanAnnHealthCheck;
import org.ctan.site.health.IncomingHealthCheck;
import org.ctan.site.health.TexarchiveHealthCheck;
import org.ctan.site.resources.Search3Resource;
import org.ctan.site.resources.Sitemap3Resource;
import org.ctan.site.resources.admin.CrudAuthor3Resource;
import org.ctan.site.resources.admin.CrudGuestbook3Resource;
import org.ctan.site.resources.admin.CrudLicense3Resource;
import org.ctan.site.resources.admin.CrudUpload3Resource;
import org.ctan.site.resources.admin.CrudUser3Resource;
import org.ctan.site.resources.admin.CrudVote3Resource;
import org.ctan.site.resources.admin.Stopword3Resource;
import org.ctan.site.resources.admin.TexArchiveNotes3Resource;
import org.ctan.site.resources.admin.Ticket3Resource;
import org.ctan.site.resources.admin.UserStopword3Resource;
import org.ctan.site.resources.catalogue.Author3Resource;
import org.ctan.site.resources.catalogue.Bibtex3Resource;
import org.ctan.site.resources.catalogue.License3Resource;
import org.ctan.site.resources.catalogue.Lug3Resource;
import org.ctan.site.resources.catalogue.Pkg3Resource;
import org.ctan.site.resources.catalogue.Topic3Resource;
import org.ctan.site.resources.catalogue.Version3Resource;
import org.ctan.site.resources.catalogue.Vote3Resource;
import org.ctan.site.resources.catalogue.api.JsonAuthorResource;
import org.ctan.site.resources.catalogue.api.JsonLicenseResource;
import org.ctan.site.resources.catalogue.api.JsonPkgResource;
import org.ctan.site.resources.catalogue.api.JsonTopicResource;
import org.ctan.site.resources.catalogue.api.JsonVersionResource;
import org.ctan.site.resources.catalogue.api.SearchResource;
import org.ctan.site.resources.catalogue.api.SubmitResource;
import org.ctan.site.resources.catalogue.api.XmlAuthorResource;
import org.ctan.site.resources.catalogue.api.XmlDtdResource;
import org.ctan.site.resources.catalogue.api.XmlLicenseResource;
import org.ctan.site.resources.catalogue.api.XmlPkgResource;
import org.ctan.site.resources.catalogue.api.XmlTopicResource;
import org.ctan.site.resources.catalogue.api.XmlVersionResource;
import org.ctan.site.resources.content.Content3Resource;
import org.ctan.site.resources.mirrors.MirrMon3Resource;
import org.ctan.site.resources.mirrors.MirrorRegistration3Resource;
import org.ctan.site.resources.mirrors.Mirrors3Resource;
import org.ctan.site.resources.postings.Atom10Resource;
import org.ctan.site.resources.postings.Postings3Resource;
import org.ctan.site.resources.postings.Rss20Resource;
import org.ctan.site.resources.site.CtanSite3Resource;
import org.ctan.site.resources.site.GuestBook3Resource;
import org.ctan.site.resources.site.Message3Resource;
import org.ctan.site.resources.site.Role3Resource;
import org.ctan.site.resources.site.User3Resource;
import org.ctan.site.resources.texarchive.Texarchive3Resource;
import org.ctan.site.resources.upload.Upload3Resource;
import org.ctan.site.services.SitemapService;
import org.ctan.site.services.account.AccountService;
import org.ctan.site.services.catalogue.BibtexService;
import org.ctan.site.services.catalogue.CatalogueImportService;
import org.ctan.site.services.catalogue.VoteService;
import org.ctan.site.services.content.ContentService;
import org.ctan.site.services.content.LionService;
import org.ctan.site.services.mail.MailService;
import org.ctan.site.services.mirrors.MirrMonService;
import org.ctan.site.services.mirrors.MirrorRegistrationService;
import org.ctan.site.services.mirrors.MirrorService;
import org.ctan.site.services.postings.PostingsService;
import org.ctan.site.services.search.SearchService;
import org.ctan.site.services.search.base.IndexingService;
import org.ctan.site.services.search.base.IndexingSession;
import org.ctan.site.services.texarchive.ArchiveFilesUpdateService;
import org.ctan.site.services.texarchive.PkgService;
import org.ctan.site.services.texarchive.TexArchiveService;
import org.ctan.site.services.upload.Submit11Service;
import org.ctan.site.services.upload.SubmitService;
import org.ctan.site.services.upload.UploadService;
import org.ctan.site.stores.ArchiveFileStore;
import org.ctan.site.stores.AuthorStore;
import org.ctan.site.stores.GuestBookStore;
import org.ctan.site.stores.LicenseStore;
import org.ctan.site.stores.LugStore;
import org.ctan.site.stores.MessageStore;
import org.ctan.site.stores.MirrorRegistrationStore;
import org.ctan.site.stores.MirrorStore;
import org.ctan.site.stores.PkgAliasStore;
import org.ctan.site.stores.PkgStore;
import org.ctan.site.stores.StopwordStore;
import org.ctan.site.stores.TexArchiveNotesStore;
import org.ctan.site.stores.TicketStore;
import org.ctan.site.stores.TopicStore;
import org.ctan.site.stores.UploadStore;
import org.ctan.site.stores.UserStopwordStore;
import org.ctan.site.stores.UserStore;
import org.ctan.site.stores.VoteStore;
import org.ctan.site.tasks.ArchiveFilesUpdateTask;
import org.ctan.site.tasks.CatalogueUpdateTask;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.hibernate.SessionFactory;

import com.codahale.metrics.health.HealthCheckRegistry;

import io.dropwizard.auth.AuthDynamicFeature;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
import io.dropwizard.configuration.SubstitutingSourceProvider;
import io.dropwizard.core.Application;
import io.dropwizard.core.setup.AdminEnvironment;
import io.dropwizard.core.setup.Bootstrap;
import io.dropwizard.core.setup.Environment;
import io.dropwizard.forms.MultiPartBundle;
import io.dropwizard.jersey.setup.JerseyEnvironment;
import jakarta.servlet.DispatcherType;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

/**
 * The class <code>Ctan</code> contains the command line interface to start the
 * Dropwizard server.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
@Slf4j
public class Ctan extends Application<CtanConfiguration> {

    /**
     * The method <code>main</code> provides means to run the Dropwizard
     * application from the command line.
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {

        try {
            new Ctan().run(args);
        } catch (Exception e) {
            log.error("Exception in main()", e);
            System.exit(1);
        }
    }

    /**
     * The method <code>configureAuth</code> provides means to configure the
     * authentication.
     *
     * @param jersey the environment
     * @param userStore the user store
     */
    private void configureAuth(JerseyEnvironment jersey, UserStore userStore) {

        jersey.register(new AuthDynamicFeature(new CtanAuthFilter(userStore)));
        jersey.register(RolesAllowedDynamicFeature.class);
        // If you want to use @Auth to inject a custom Principal type into your
        // resource
        jersey.register(
            new AuthValueFactoryProvider.Binder<>(CtanPrincipal.class));
    }

    /**
     * The method <code>configureCors</code> provides means to configure the
     * treatment of CORS.
     *
     * @param environment the environment
     */
    private void configureCors(Environment environment) {

        var cors = environment
            .servlets()
            .addFilter("CORS", CrossOriginFilter.class);
        cors.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
        cors.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM,
            "X-Requested-With,"
                + "Content-Type,"
                + "Accept,"
                + "Origin");
        // cors.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM,
        // "X-Requested-With,"
        // + "Content-Type,"
        // + "Accept,"
        // + "Origin,"
        // + "Authorization,"
        // + "Authentication");
        cors.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM,
            "OPTIONS,GET,PUT,POST,DELETE,HEAD");
        cors.setInitParameter(CrossOriginFilter.ALLOW_CREDENTIALS_PARAM,
            "true");

        cors.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true,
            "/*");
    }

    /**
     * {@inheritDoc}
     *
     * @see io.dropwizard.core.Application#initialize(io.dropwizard.core.setup.Bootstrap)
     */
    @Override
    public void initialize(Bootstrap<CtanConfiguration> bootstrap) {

        bootstrap.setConfigurationSourceProvider(new SubstitutingSourceProvider(
            bootstrap.getConfigurationSourceProvider(),
            new EnvironmentVariableSubstitutor(false)));

        bootstrap.addBundle(CtanDatabaseBundles.SITE_DB_BUNDLE);
        bootstrap.addBundle(CtanDatabaseBundles.MIRRORS_DB_BUNDLE);
        bootstrap.addBundle(new MultiPartBundle());

        bootstrap.addCommand(new InitSearchCommand());

        // bootstrap.addBundle(new FlywayBundle<CtanConfiguration>() {
        //
        // @Override
        // public DataSourceFactory getDataSourceFactory(
        // CtanConfiguration configuration) {
        //
        // return configuration.getDataSourceFactory();
        // }
        //
        // @Override
        // public FlywayFactory getFlywayFactory(
        // CtanConfiguration configuration) {
        //
        // return configuration.getFlywayFactory();
        // }
        // });
    }

    /**
     * The method <code>registerHealthChecks</code> provides means to add health
     * checks to the registry.
     *
     * @param healthChecks the target registry
     * @param config the configuration
     */
    private void registerHealthChecks(HealthCheckRegistry healthChecks,
        CtanConfiguration config) {

        log.info("Registering health checks");
        healthChecks.register("AppHealthCheck",
            new AppHealthCheck(config));
        healthChecks.register("TexarchiveHealthCheck",
            new TexarchiveHealthCheck(config));
        healthChecks.register("ContentHealthCheck",
            new ContentHealthCheck(config));
        healthChecks.register("CtanAnnHealthCheck",
            new CtanAnnHealthCheck(config));
        healthChecks.register("IncomingHealthCheck",
            new IncomingHealthCheck(config));
        // healthChecks.register("SiteDbHealthCheck",
        // new SiteDbHealthCheck(config));
    }

    /**
     * The method <code>registerPublishedApi</code> provides means to register
     * the published API services to interact with JSON and XML.
     *
     * @param jersey the environment instance
     * @param pkgStore the package store
     * @param authorStore the author store
     * @param topicStore the topic store
     * @param authorRefStore the authorRef store
     * @param licenseStore the license store
     * @param uploadService the upload service
     */
    private void registerPublishedApi(JerseyEnvironment jersey,
        PkgStore pkgStore, AuthorStore authorStore, TopicStore topicStore,
        LicenseStore licenseStore, SubmitService submitService) {

        jersey.register(new JsonAuthorResource(authorStore));
        jersey.register(new XmlAuthorResource(authorStore));

        jersey.register(new JsonLicenseResource(licenseStore));
        jersey.register(new XmlLicenseResource(licenseStore));

        jersey.register(new JsonPkgResource(pkgStore));
        jersey.register(new XmlPkgResource(pkgStore));

        jersey.register(new JsonTopicResource(topicStore));
        jersey.register(new XmlTopicResource(topicStore));

        jersey.register(new JsonVersionResource());
        jersey.register(new XmlVersionResource());

        jersey.register(new XmlDtdResource());

        jersey.register(new SubmitResource(submitService));
    }

    /**
     * The method <code>registerResources</code> provides means to add resources
     * to the registry.
     *
     * @param jersey the target registry
     * @param adminEnvironment the administration environment
     * @param config the configuration
     * @return the user store
     *
     * @throws IOException in case of an I/O error
     * @throws FileNotFoundException in case of an error
     */
    private UserStore registerResources(JerseyEnvironment jersey,
        AdminEnvironment admin,
        CtanConfiguration config)
        throws FileNotFoundException,
            IOException {

        // TODO jersey.register(new
        // AuthValueFactoryProvider.Binder<>(User.class));

        log.info("Registering REST resources");

        IndexingService indexingService =
            new IndexingService(config.getIndex());
        IndexingSession indexingSession = indexingService.indexingSession();
        SessionFactory dbSessionFactory =
            CtanDatabaseBundles.SITE_DB_BUNDLE.getSessionFactory();
        PkgStore pkgStore =
            new PkgStore(dbSessionFactory, indexingSession);
        AuthorStore authorStore =
            new AuthorStore(dbSessionFactory, indexingSession);
        TopicStore topicStore =
            new TopicStore(dbSessionFactory, indexingSession);
        MessageStore messageStore = new MessageStore(dbSessionFactory);
        UserStore userStore = new UserStore(dbSessionFactory);

        jersey.register(new CtanSite3Resource(config,
            messageStore,
            authorStore,
            pkgStore,
            topicStore));

        jersey.register(new Message3Resource(messageStore));

        var ctanConfig = config.getCtan();
        var contentService =
            new ContentService(config.getContent(), ctanConfig);
        var pkgService = new PkgService(config, contentService, pkgStore);

        var texArchiveNotesStore = new TexArchiveNotesStore(dbSessionFactory);
        var texarchiveService = new TexArchiveService(config,
            pkgService,
            texArchiveNotesStore);
        jersey.register(
            new Texarchive3Resource(texarchiveService));

        jersey.register(new TexArchiveNotes3Resource(texArchiveNotesStore));

        jersey.register(new Content3Resource(contentService,
            new LionService(config.getContent())));

        var uploadConfig = config.getUpload();
        var uploadStore = new UploadStore(dbSessionFactory);
        var uploadService = new UploadService(uploadConfig, uploadStore);
        jersey.register(new Upload3Resource(
            uploadService));

        jersey.register(new Author3Resource(ctanConfig,
            authorStore,
            userStore));

        var bibtexService = new BibtexService(pkgStore);
        var postingService =
            new PostingsService(config.getCtanAnnounce().getDirectory());

        jersey.register(new Pkg3Resource(ctanConfig,
            pkgStore,
            contentService,
            bibtexService,
            postingService,
            new PkgAliasStore(dbSessionFactory)));

        var voteStore = new VoteStore(dbSessionFactory);
        jersey.register(new Topic3Resource(ctanConfig,
            topicStore,
            contentService,
            voteStore));
        jersey.register(new Vote3Resource(voteStore, new VoteService(voteStore),
            pkgStore, userStore));

        var mailService = new MailService(config.getMail());
        var licenseStore = new LicenseStore(dbSessionFactory,
            indexingService.indexingSession());
        var submitService = new Submit11Service(config,
            uploadService,
            mailService,
            pkgStore,
            topicStore,
            licenseStore);
        registerPublishedApi(jersey,
            pkgStore,
            authorStore,
            topicStore,
            licenseStore,
            submitService);

        jersey.register(new MirrorRegistration3Resource(
            new MirrorRegistrationService(
                new MirrorRegistrationStore(dbSessionFactory),
                mailService)));

        GuestBookStore guestbookStore =
            new GuestBookStore(dbSessionFactory, indexingSession);
        jersey.register(
            new GuestBook3Resource(guestbookStore));

        jersey.register(new Version3Resource());
        jersey.register(new JsonVersionResource());

        var mirrorsSessions =
            CtanDatabaseBundles.MIRRORS_DB_BUNDLE.getSessionFactory();

        jersey.register(new Mirrors3Resource(
            new MirrorService(
                new MirrorStore(mirrorsSessions, indexingSession))));

        jersey.register(new MirrMon3Resource(
            new MirrMonService(config.getMirrmon())));

        jersey.register(new License3Resource(licenseStore,
            contentService));

        jersey.register(new JsonLicenseResource(licenseStore));

        jersey.register(new Lug3Resource(
            new LugStore(dbSessionFactory)));

        var ticketStore = new TicketStore(dbSessionFactory);
        jersey.register(
            new Ticket3Resource(ticketStore));
        jersey.register(new User3Resource(new AccountService(
            userStore,
            ticketStore,
            authorStore,
            mailService)));

        jersey.register(
            new Role3Resource());

        SearchService searchService = new SearchService(config.getIndex());
        jersey.register(
            new SearchResource(searchService));
        jersey.register(
            new Search3Resource(searchService));

        jersey.register(
            new CrudUser3Resource(userStore));

        jersey.register(
            new CrudAuthor3Resource(authorStore));
        jersey.register(
            new CrudGuestbook3Resource(guestbookStore));

        jersey.register(
            new CrudLicense3Resource(licenseStore));

        jersey.register(
            new CrudUpload3Resource(uploadStore));

        jersey.register(new UserStopword3Resource(
            new UserStopwordStore(dbSessionFactory)));

        jersey.register(new Stopword3Resource(
            new StopwordStore(dbSessionFactory)));

        jersey.register(new CrudVote3Resource(
            voteStore));

        jersey.register(new Sitemap3Resource(new SitemapService()));

        /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

        postingService.update(); // TODO start thread instead
        jersey.register(
            new Postings3Resource(postingService, contentService, pkgStore));
        jersey.register(
            new Rss20Resource(postingService, config));
        jersey.register(
            new Atom10Resource(postingService, config));

        jersey.register(new Bibtex3Resource(
            new BibtexService(pkgStore)));

        /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

        var catalogUpdateService =
            new CatalogueImportService(config.getCatalogue(),
                authorStore,
                topicStore,
                pkgStore,
                licenseStore,
                indexingService);
        admin.addTask(new CatalogueUpdateTask(catalogUpdateService));

        admin.addTask(new ArchiveFilesUpdateTask(
            new ArchiveFilesUpdateService(config,
                new ArchiveFileStore(config.getTexArchive(), dbSessionFactory,
                    indexingSession))));
        return userStore;
    }

    /**
     * {@inheritDoc}
     *
     * @see io.dropwizard.core.Application#run(io.dropwizard.core.Configuration,
     *     io.dropwizard.core.setup.Environment)
     */
    @Override
    public void run(@NonNull CtanConfiguration config, @NonNull Environment env)
        throws Exception {

        UserStore userStore = registerResources(env.jersey(),
            env.admin(),
            config);
        registerHealthChecks(env.healthChecks(), config);
        configureAuth(env.jersey(), userStore);
        configureCors(env);
    }
}