org.emoseman.aconf.AutoConfig.java Source code

Java tutorial

Introduction

Here is the source code for org.emoseman.aconf.AutoConfig.java

Source

/*
 * Copyright (c) 2013. Evan Moseman
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.emoseman.aconf;

import com.google.common.reflect.Invokable;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * AutoConfig information.
 *
 * @author Evan Moseman (evan.moseman@gmail.com)
 * @version 1.0
 * @since 5/20/13
 */
public class AutoConfig {

    private static Logger log = LoggerFactory.getLogger(AutoConfig.class);

    private static AtomicBoolean shutdown = new AtomicBoolean(false);

    private static ReentrantReadWriteLock configLock = new ReentrantReadWriteLock();

    private static final HashMap<Class<? extends ConfigContainer>, ArrayList<Field>> configContainerFields = new HashMap<>();

    private static final HashMap<Class<? extends ConfigContainer>, ArrayList<String>> configContainerRequiredFields = new HashMap<>();

    private static final HashMap<Class<? extends ConfigContainer>, HashMap<String, Field>> fieldMap = new HashMap<>();

    private static final HashMap<Class<? extends ConfigContainer>, HashMap<String, Class>> fieldTypeMap = new HashMap<>();

    private static Set<Class<? extends ConfigContainer>> configContainerTypes;

    private static final Set<ConfigChangeListener> listeners = new HashSet<>();

    public static final String RELOAD_DELAY = "reload.delay";

    public static final String DATE_FORMAT = "date.format";

    public static final String TIME_FORMAT = "time.format";

    public static final String PACKAGE_SEARCH_NAME = "package.search.name";

    public static final SimpleDateFormat dateFormat;

    public static final SimpleDateFormat timeFormat;

    private static final ScheduledExecutorService timedPool = Executors.newScheduledThreadPool(1);

    static {
        String packageName = "";
        if (System.getProperty(PACKAGE_SEARCH_NAME) != null) {
            packageName = System.getProperty(PACKAGE_SEARCH_NAME);
        } else if (System.getenv(PACKAGE_SEARCH_NAME) != null) {
            packageName = System.getenv(PACKAGE_SEARCH_NAME);
        }

        final Reflections reflections = new Reflections(packageName);

        configContainerTypes = reflections.getSubTypesOf(ConfigContainer.class);

        if (configContainerTypes.size() == 0) {
            System.err.println("No classes found implementing ConfigContainer interface.");
        }

        long reloadDelay = -1;
        if (System.getProperty(RELOAD_DELAY) != null) {
            reloadDelay = Long.valueOf(System.getProperty(RELOAD_DELAY));
        } else if (System.getenv(RELOAD_DELAY) != null) {
            reloadDelay = Long.valueOf(System.getProperty(RELOAD_DELAY));
        }
        if (reloadDelay > 0) {
            timedPool.scheduleWithFixedDelay(new Reloader(), 0, reloadDelay, TimeUnit.MILLISECONDS);
        }

        if (System.getProperty(DATE_FORMAT) != null) {
            dateFormat = new SimpleDateFormat(System.getProperty(DATE_FORMAT));
        } else if (System.getenv(DATE_FORMAT) != null) {
            dateFormat = new SimpleDateFormat(System.getenv(DATE_FORMAT));
        } else {
            dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        }

        if (System.getProperty(TIME_FORMAT) != null) {
            timeFormat = new SimpleDateFormat(System.getProperty(TIME_FORMAT));
        } else if (System.getenv(TIME_FORMAT) != null) {
            timeFormat = new SimpleDateFormat(System.getenv(TIME_FORMAT));
        } else {
            timeFormat = new SimpleDateFormat("hh:mm a");
        }
    }

    private AutoConfig() {

    }

    public static synchronized final void init() {

        processConfigContainers();
    }

    public static synchronized final void reload() {

        processConfigContainers();
    }

    private static void processConfigContainers() {

        if (shutdown.get()) {
            return;
        }

        for (Class<? extends ConfigContainer> containerType : configContainerTypes) {
            log.debug("processing config container: {}", containerType);

            ConfigFile configFileAnnotation = containerType.getAnnotation(ConfigFile.class);
            if (configFileAnnotation == null) {
                throw new RuntimeException("ConfigContainer class " + containerType.getName()
                        + " does not have the required ConfigFile annotation.");
            }
            final String configFilename;
            configFilename = configFileAnnotation.value();

            configContainerRequiredFields.put(containerType, new ArrayList<String>());
            configContainerFields.put(containerType, new ArrayList<Field>());
            fieldTypeMap.put(containerType, new HashMap<String, Class>());
            fieldMap.put(containerType, new HashMap<String, Field>());

            for (Field f : containerType.getDeclaredFields()) {
                ConfigElement configElement = f.getAnnotation(ConfigElement.class);
                if (configElement == null) {
                    continue;
                }

                log.debug("config element: {}", configElement.toString());

                if (!Modifier.isStatic(f.getModifiers())) {
                    throw new RuntimeException("Fields annotated with ConfigElement, like " + f.getName()
                            + " are required to be static!");
                }

                configContainerFields.get(containerType).add(f);

                fieldTypeMap.get(containerType).put(configElement.propertyName(), configElement.propertyType());

                fieldMap.get(containerType).put(configElement.propertyName(), f);

                if (configElement.required()) {
                    configContainerRequiredFields.get(containerType).add(configElement.propertyName());
                }
            }

            log.debug("field map:{}", fieldMap.get(containerType).toString());

            readConfigFromFile(containerType, configFilename);
        }
    }

    private static void readConfigFromFile(Class<? extends ConfigContainer> configContainer,
            String configFilename) {

        configLock.writeLock().lock();

        URL fileURL = null;
        if (configFilename.startsWith("file:///")) {
            try {
                fileURL = new URL(configFilename);
            } catch (MalformedURLException e) {
                throw new RuntimeException("Failed to create URL from " + configFilename, e);
            }
        } else {
            ClassLoader cl = ClassLoader.getSystemClassLoader();
            fileURL = cl.getResource(configFilename);
        }
        if (fileURL == null) {
            throw new RuntimeException("Failed to load config file " + configFilename);
        }

        Gson gson = new Gson();
        String val;
        Field field;
        Class type;
        try {
            // read json from config file
            // TODO enforce extension?
            HashMap<String, String> tmp = gson.fromJson(new InputStreamReader(fileURL.openStream()), HashMap.class);

            // search for required keys
            ArrayList<String> missingKeys = new ArrayList<>();
            for (String k : configContainerRequiredFields.get(configContainer)) {
                if (!tmp.containsKey(k)) {
                    missingKeys.add(k);
                }
            }
            if (missingKeys.size() > 0) {
                throw new RuntimeException(
                        "Properties " + StringUtils.join(missingKeys, ", ") + " are required, and missing");
            }

            // assign defaults
            for (Field f : configContainerFields.get(configContainer)) {
                ConfigElement configElement = f.getAnnotation(ConfigElement.class);
                if (configElement == null) {
                    continue;
                }
                if (StringUtils.isEmpty(configElement.defaultValue())) {
                    continue;
                }
                if (!tmp.containsKey(configElement.propertyName())) {
                    tmp.put(configElement.propertyName(), configElement.defaultValue());
                }
            }

            // populate fields
            for (String k : tmp.keySet()) {
                if (fieldMap.get(configContainer).containsKey(k)) {
                    val = tmp.get(k);
                    if (val.equalsIgnoreCase("${hostname}")) {
                        val = populateMacro(val);
                    }
                    field = fieldMap.get(configContainer).get(k);
                    field.setAccessible(true);

                    type = fieldTypeMap.get(configContainer).get(k);

                    if (type == String.class) {
                        field.set(configContainer, String.valueOf(val));
                    } else if (type == BigInteger.class) {
                        field.set(configContainer, new BigInteger(val));
                    } else if (type == Float.class || type == float.class) {
                        field.set(configContainer, Float.valueOf(val));
                    } else if (type == Long.class || type == long.class) {
                        field.set(configContainer, Long.valueOf(val));
                    } else if (type == Double.class || type == double.class) {
                        field.set(configContainer, Double.valueOf(val));
                    } else if (type == Integer.class || type == int.class) {
                        field.set(configContainer, Integer.valueOf(val));
                    } else if (type == Boolean.class || type == boolean.class) {
                        field.set(configContainer, Boolean.valueOf(val));
                    } else {
                        // try to execute the method reflectively
                        try {
                            Method m = type.getDeclaredMethod("valueOf", String.class);
                            Invokable invokable = new TypeToken() {
                            }.method(m);
                            if (invokable.isStatic() && !invokable.isPrivate()) {
                                field.set(configContainer, m.invoke(type, val));
                                continue;
                            }
                        } catch (Throwable ignored) {
                            // ignored
                        }
                        try {
                            Constructor typeConstructor = type.getConstructor(String.class);
                            Invokable invokable = new TypeToken() {
                            }.constructor(typeConstructor);
                            if (!invokable.isPrivate()) {
                                field.set(configContainer, typeConstructor.newInstance(val));
                                continue;
                            }
                        } catch (Throwable ignored) {
                            // ignored
                        }

                        throw new RuntimeException(
                                "Not coded for type " + type + " and could not find a method to execute!");
                    }
                }
            }
            for (ConfigChangeListener listener : listeners) {
                listener.configReloaded();
            }
        } catch (Exception e) {
            log.error(e.getClass() + "-" + e.getMessage());
            if (log.isDebugEnabled()) {
                e.printStackTrace(System.err);
            }

            shutdown.set(true);
            timedPool.shutdownNow();
            throw new RuntimeException(e);
        }

        configLock.writeLock().unlock();
    }

    public static final String HOSTNAME_MACRO = "${hostname}";

    public static final String DATE_MACRO = "${date}";

    public static final String TIME_MACRO = "${time}";

    private static String populateMacro(final String val) {

        switch (val) {
        case HOSTNAME_MACRO:
            return gethostname();
        case DATE_MACRO:
            return getDate();
        case TIME_MACRO:
            return getTime();
        default:
            return val;
        }
    }

    public static final boolean addListener(ConfigChangeListener listener) {

        return listeners.add(listener);
    }

    public static final boolean removeListener(ConfigChangeListener listener) {

        return listeners.remove(listener);
    }

    private static String getDate() {

        return dateFormat.format(new Date());
    }

    private static String getTime() {

        return timeFormat.format(new Date());
    }

    private static String gethostname() {

        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            log.error(e.getMessage());
        }
        return "failed to resolve hostname";
    }

    private static class Reloader implements Runnable {

        @Override
        public void run() {

            reload();
        }
    }
}