All Downloads are FREE. Search and download functionalities are using the official Maven repository.

me.legrange.service.Service Maven / Gradle / Ivy

package me.legrange.service;

import me.legrange.config.ConfigurationException;
import me.legrange.config.YamlLoader;
import me.legrange.service.RuntimeEnvironment.Type;
import sun.misc.Signal;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

import static java.lang.String.format;
import static me.legrange.log.Log.critical;
import static me.legrange.log.Log.error;
import static me.legrange.log.Log.info;
import static me.legrange.log.Log.warning;

/**
 * @param  The type of the configuration class for this service.
 * @author gideon
 */
public abstract class Service {

    private Conf conf;
    private final Map, Component> components = new HashMap<>();
    private final ExecutorService threadPool = new ForkJoinPool(32);
    private boolean running;

    public static void main(String... args) {
        try {
            InputStream config = null;
            switch (args.length) {
                case 1:
                    config = new FileInputStream(args[0]);
                    break;
                case 2:
                    switch (args[0]) {
                        case "-file":
                            config = new FileInputStream(args[1]);
                            break;
                        case "-resource":
                            config = Service.class.getClassLoader().getResourceAsStream(args[1]);
                            break;
                        default:
                    }
                    break;
            }
            if (config == null) {
                failedStartup("Usage: Server [-file|-resource]  ");
            }
            Class serviceClass = determineServiceClass();
            Service service = serviceClass.getDeclaredConstructor().newInstance();
            try {
                var conf = YamlLoader.readConfiguration(config, service.getConfigClass());
                service.start(conf);
            } catch (ServiceException | ConfigurationException ex) {
                ex.printStackTrace();
                failedStartup(String.format("Error configuring server: %s", ex.getMessage()));
            }
            while (service.isRunning()) {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException ignored) {
                }
            }
            service.stop();
            System.exit(0);
        } catch (ServiceException | InstantiationException | NoSuchMethodException | SecurityException |
                 IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            error(ex, "Fatal error: %s", ex.getMessage());
            System.exit(1);
        } catch (FileNotFoundException ex) {
            error(ex, "Fatal error: %s", ex.getMessage());
        }
    }

    public final void start(Conf config) throws ServiceException {
        this.conf = config;
        startComponents();
        setupShutdownSignals();
        info("Platform is %s on %s %s", RuntimeEnvironment.getOsType(),
                RuntimeEnvironment.getArch(),
                RuntimeEnvironment.isInContainer() ? "running in a container" : "");
        start();
        running = true;
    }

    public final void stop() throws ServiceException {
        running = false;
        onStop();
        stopComponents();
    }

    /**
     * Get the running component for the given component class.
     *
     * @param    The type of the component
     * @param clazz The class representing the component
     * @return The component
     * @throws me.legrange.service.ComponentNotFoundException Thrown if the component requested cannot be found
     */
    public final  C getComponent(Class clazz) throws ComponentNotFoundException {
        if (components.containsKey(clazz)) {
            return clazz.cast(components.get(clazz));
        }
        throw new ComponentNotFoundException(format("No component registered of type '%s'. BUG!", clazz.getSimpleName()));
    }

    /**
     * Start the service components
     */
    private void startComponents() throws ServiceException {
        for (Class clazz : getRequiredComponents()) {
            if (!components.containsKey(clazz)) {
                startComponent(clazz);
            }
        }
    }

    /**
     * Stop the service components
     */
    private void stopComponents() throws ServiceException {
        for (Class clazz : getRequiredComponents()) {
            if (components.containsKey(clazz)) {
                try {
                    stopComponent(clazz);
                } catch (ComponentException ex) {
                    error(ex, "Error stopping component %s (%s)", clazz.getSimpleName(), ex.getMessage());
                }
            }
        }
    }

    /**
     * Start a specific component
     *
     * @param clazz The class of the component to start.
     */
    private  C startComponent(Class clazz) throws ServiceException {
        try {
            Constructor cons = clazz.getConstructor(Service.class);
            C comp = (C) cons.newInstance(this);
            if (comp.requiresConfig()) {
                Object compConf = getConfigFor(comp);
                comp.start(compConf);
            } else {
                comp.start(null);
            }
            components.put(clazz, comp);
            return comp;
        } catch (InstantiationException | IllegalAccessException | SecurityException | IllegalArgumentException |
                 InvocationTargetException ex) {
            throw new ServiceException(format("Error creating new component of type '%s': %s", clazz.getSimpleName(), ex.getMessage()), ex);
        } catch (NoSuchMethodException ex) {
            throw new ServiceException(format("Could not find constructor with Service as parameter on component of type '%s': %s", clazz.getSimpleName(), ex.getMessage()), ex);
        }
    }

    /**
     * Stop a specific component
     *
     * @param clazz The class of the component to stop.
     */
    private  void stopComponent(Class clazz) throws ServiceException {
        C comp = (C) components.get(clazz);
        comp.stop();
    }

    /**
     * Get the class for the configuration of this service.
     *
     * @return The configuration class
     */
    private Class getConfigClass() throws ServiceException {
        String name = getClass().getName();
        int idx = name.lastIndexOf("Service");
        if (idx > 0) {
            name = name.substring(0, idx);
            name = name + "Config";
        } else {
            if (!name.endsWith("Config")) {
                name = name + "Config";
            }
        }
        try {
            return (Class) Class.forName(name);
        } catch (ClassNotFoundException ex) {
            throw new ServiceException(format("Could not find config class '%s' for service class '%s'. BUG!", name, getClass().getName()), ex);
        }
    }

    /**
     * Find the components we need to activate for this service. The components
     * are supplied in the order in which they need to be activated.
     *
     * @return The set of components required.
     */
    private List> getRequiredComponents() throws ServiceException {
        return getRequiredComponents(getClass());
    }

    /**
     * Find the components we need to activate for this class (component or
     * service).
     *
     * @return The list of components required.
     */
    private List> getRequiredComponents(Class clazz) throws ServiceException {
        List> res = new LinkedList<>();
        Set> interfaces = getWithInterfaces(clazz);
        for (Class iface : interfaces) {
            String name = iface.getName().replace(".With", ".").concat("Component");
            try {
                Class compClass = Class.forName(name);
                if (Component.class.isAssignableFrom(compClass)) {
                    res.addAll(0, getRequiredComponents(compClass));
                    if (!res.contains(compClass)) {
                        res.add((Class) compClass);
                    }
                } else {
                    throw new ServiceException(format("Class '%s' associated with interface '%s' does not extend '%s'", name, iface.getName(), Component.class.getName()));
                }
            } catch (ClassNotFoundException ex) {
                throw new ServiceException(format("Cannot find component class '%s' associated with interface '%s'", name, iface.getName()), ex);
            }
        }
        return res;
    }

    public final  C requireComponent(Class clazz) throws ServiceException {
        if (components.containsKey(clazz)) {
            return (C) components.get(clazz);
        }
        return startComponent(clazz);
    }

    /**
     * Find all WithComponent interfaces applied to a service or component class and it's
     * super classes.
     *
     * @param type The service or component class
     * @return The set of interface classes.
     */
    private Set> getWithInterfaces(Class type) {
        Set> res = new HashSet<>();
        for (Class iface : type.getInterfaces()) {
            if (WithComponent.class.isAssignableFrom(iface)) {
                res.add((Class) iface);
            }
        }
        if (Service.class.isAssignableFrom(type.getSuperclass())) {
            res.addAll(getWithInterfaces(type.getSuperclass()));
        } else if (Component.class.isAssignableFrom(type.getSuperclass())) {
            res.addAll(getWithInterfaces(type.getSuperclass()));
        }
        return res;
    }

    public final boolean isRunning() {
        return running;
    }

    /**
     * Start the service. This needs to be implemented by the implementation
     * subclass to get work done.
     *
     * @throws ServiceException Thrown if the service cannot be started
     */
    protected abstract void start() throws ServiceException;

    /**
     * Stop the service. This can be implemented by the implementation subclass to
     * release resources, do cleanup or accomplish other shut down tasks.
     *
     * @throws ServiceException Thrown if there is a problem stopping the service
     */
    protected void onStop() throws ServiceException {

    }


    /**
     * Return the service configuration.
     *
     * @return The configuration object
     */
    protected Conf getConfig() {
        return conf;
    }

    /**
     * Submit a task for running to the thread pool.
     *
     * @param task The task to run.
     */
    public void submit(final Runnable task) {
        threadPool.submit(() -> {
            try {
                task.run();
            } catch (Throwable ex) {
                critical(ex, "Uncaught exception in task '%s': %s", task.getClass().getSimpleName(), ex.getMessage());
            }
        });
    }

    public final void sleep(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException ignored) {
        }
    }

    /**
     * Configure the server.
     */
    private void configure(InputStream config) throws ServiceException {
        try {
            conf = YamlLoader.readConfiguration(config, getConfigClass());
        } catch (ConfigurationException ex) {
            throw new ServiceException(ex.getMessage(), ex);
        }
    }

    /**
     * Output text to the console (used for startup errors
     *
     * @param fmt  format string
     * @param args arguments
     */
    private static void say(String fmt, Object... args) {
        System.out.printf(fmt, args);
        if (!fmt.endsWith("\n")) {
            System.out.println();
        }
    }


    private Object getConfigFor(Component com) throws ServiceException {
        String name = com.getName();
        String getName = "get" + name;
        for (Method meth : conf.getClass().getMethods()) {
            if (meth.getName().equalsIgnoreCase(getName)) {
                try {
                    return meth.invoke(conf);
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                    throw new ServiceException(format("Cannot find configuration for component '%s' of type '%s' in config object of type '%s'. BUG!",
                            com.getName(), com.getClass().getSimpleName(), conf.getClass().getSimpleName()), ex);
                }
            }
        }
        throw new ServiceException(format("Cannot find configuration for component '%s' of type '%s' in config object of type '%s'",
                com.getName(), com.getClass().getSimpleName(), conf.getClass().getSimpleName()));
    }

    /**
     * Determine what sub-class of Service is the actual service we want to run.
     */
    private static Class determineServiceClass() throws ServiceException {
        Class[] classContext = new SecurityManager() {
            @Override
            public Class[] getClassContext() {
                return super.getClassContext();
            }
        }.getClassContext();
        int idx = 0;
        Class clazz = null;
        while (idx < classContext.length) {
            Class next = classContext[idx];
            if (Service.class.isAssignableFrom(next)) {
                clazz = next;
            }
            idx++;
        }
        if (clazz == null) {
            throw new ServiceException("Could not find the service class to instantiate. Does your application extend Service?");
        }
        if (clazz == Service.class) {
            throw new ServiceException("Could not find the service class to instantiate. Did you implement the main method in your class?");
        }
        return clazz;
    }

    private void setupShutdownSignals() {
        String[] signals = RuntimeEnvironment.getOsType() == Type.WINDOWS ? new String[]{"TERM", "INT"} : new String[]{"TERM", "INT", "HUP"};
        for (String name : signals) {
            try {
                Signal.handle(new Signal(name), sig -> {
                    warning("%s signal (%d) received; shutting down", sig.getName(), sig.getNumber());
                    running = false;
                });
            } catch (IllegalArgumentException ex) {
                warning("Cannot setup signal (%s)", ex.getMessage());
            }
        }
    }

    private static void failedStartup(String reason) {
        say(reason);
        System.exit(1);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy