PostingsService.java

/*
 * Copyright © 2016-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.postings;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

/**
 * This class provides a service to read and watch the mailings on a local
 * mailman mailing list (like ctan-ann).
 *
 * @author <a href="gene@ctan.org">Gerd Neugebauer</a>
 */
@Slf4j
public class PostingsService extends PostingCache {

    /**
     * The class <code>PostingsThread</code> contains the internal thread.
     */
    class PostingsThread implements Runnable {

        /**
         * {@inheritDoc}
         *
         * @see java.lang.Runnable#run()
         */
        @Override
        public void run() {

            WatchService watcher;
            try {
                watcher = FileSystems.getDefault().newWatchService();
                getBase().toPath().register(watcher,
                    ENTRY_CREATE,
                    ENTRY_MODIFY);
                // java.nio.file.StandardWatchEventKinds.ENTRY_DELETE,
            } catch (IOException ex) {
                throw new IllegalArgumentException(
                    "watcher failed: " + ex.getMessage());
            }
            try {
                for (;;) {
                    WatchKey key = watcher.take();
                    var needUpdate = false;
                    for (WatchEvent<?> event : key.pollEvents()) {
                        if (event.kind() == OVERFLOW) {
                            continue;
                        }
                        var filename = ((WatchEvent<Path>) event).context();
                        if (!filename.getName(filename.getNameCount() - 1)
                            .toString().endsWith(".txt.gz")) {
                            continue;
                        }
                        needUpdate = true;
                        break;
                    }
                    if (!key.reset()) {
                        break;
                    }
                    if (needUpdate) {
                        Thread.sleep(1000L);
                        update();
                    }
                }
            } catch (InterruptedException ex) {
                log.error("watcher interrupted", ex);
            } catch (IOException e) {
                log.error("watcher update failed", e);
            }
        }

        /**
         * The method <code>start</code> provides means to run the watcher.
         */
        public void start() {

            new Thread(this).start();
        }
    }

    /**
     * This is the constructor for the class <code>PostingsService</code>.
     *
     * @param base the base directory to scan
     * @throws IOException in case of an I/O error
     * @throws FileNotFoundException in case of an error
     */
    @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW")
    public PostingsService(@NonNull String base)
        throws FileNotFoundException,
            IOException {

        super(new File(base));
    }

    /**
     * This method starts the processing right after the service has been
     * constructed.
     *
     * @return the thread
     */
    public PostingsThread start() {

        var postingsThread = new PostingsThread();
        postingsThread.start();
        return postingsThread;
    }
}