org.ngrinder.infra.config.Config.java Source code

Java tutorial

Introduction

Here is the source code for org.ngrinder.infra.config.Config.java

Source

/*
* Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
*  You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ngrinder.infra.config;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import net.grinder.util.ListenerSupport;
import net.grinder.util.ListenerSupport.Informer;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.ngrinder.common.constant.ClusterConstants;
import org.ngrinder.common.constant.ControllerConstants;
import org.ngrinder.common.constants.InternalConstants;
import org.ngrinder.common.exception.ConfigurationException;
import org.ngrinder.common.model.Home;
import org.ngrinder.common.util.FileWatchdog;
import org.ngrinder.common.util.PropertiesKeyMapper;
import org.ngrinder.common.util.PropertiesWrapper;
import org.ngrinder.infra.logger.CoreLogger;
import org.ngrinder.infra.spring.SpringContext;
import org.ngrinder.service.AbstractConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Properties;

import static net.grinder.util.NoOp.noOp;
import static org.ngrinder.common.constant.DatabaseConstants.PROP_DATABASE_UNIT_TEST;
import static org.ngrinder.common.util.Preconditions.checkNotNull;

/**
 * Spring component which is responsible to get the nGrinder configurations which is stored ${NGRINDER_HOME}.
 *
 * @author JunHo Yoon
 * @since 3.0
 */
@Component
public class Config extends AbstractConfig
        implements ControllerConstants, ClusterConstants, ApplicationListener<ContextRefreshedEvent> {
    private static final String NGRINDER_DEFAULT_FOLDER = ".ngrinder";
    private static final String NGRINDER_EX_FOLDER = ".ngrinder_ex";
    private static final Logger LOG = LoggerFactory.getLogger(Config.class);
    private Home home = null;
    private Home exHome = null;
    private PropertiesWrapper internalProperties;
    private PropertiesWrapper controllerProperties;
    private PropertiesWrapper databaseProperties;
    private PropertiesWrapper clusterProperties;
    private String announcement = "";
    private Date announcementDate;
    private boolean verbose;

    public static final String NONE_REGION = "NONE";
    private boolean cluster;
    private ListenerSupport<PropertyChangeListener> systemConfListeners = new ListenerSupport<PropertyChangeListener>();

    protected PropertiesKeyMapper internalPropertiesKeyMapper = PropertiesKeyMapper
            .create("internal-properties.map");
    protected PropertiesKeyMapper databasePropertiesKeyMapper = PropertiesKeyMapper
            .create("database-properties.map");
    protected PropertiesKeyMapper controllerPropertiesKeyMapper = PropertiesKeyMapper
            .create("controller-properties.map");
    protected PropertiesKeyMapper clusterPropertiesKeyMapper = PropertiesKeyMapper.create("cluster-properties.map");

    @SuppressWarnings("SpringJavaAutowiringInspection")
    @Autowired
    private SpringContext context;

    @Autowired
    private ApplicationContext appContext;

    /**
     * Make it singleton.
     */
    Config() {
    }

    /**
     * Add the system configuration change listener.
     *
     * @param listener listener
     */
    @SuppressWarnings("UnusedDeclaration")
    public void addSystemConfListener(PropertyChangeListener listener) {
        systemConfListeners.add(listener);
    }

    /**
     * Initialize the {@link Config} object.
     * <p/>
     * This method mainly resolves ${NGRINDER_HOME} and loads system properties. In addition, the logger is initialized
     * and the default configuration files are copied into ${NGRINDER_HOME} if they do not exists.
     */
    @PostConstruct
    public void init() {
        try {
            CoreLogger.LOGGER.info("nGrinder is starting...");
            home = resolveHome();
            home.init();
            exHome = resolveExHome();
            copyDefaultConfigurationFiles();
            loadInternalProperties();
            loadProperties();
            initHomeMonitor();
            // Load cluster in advance. cluster mode is not dynamically
            // reloadable.
            cluster = resolveClusterMode();
            initDevModeProperties();
            addChangeConfigListenerForStatistics();
            loadAnnouncement();
            loadDatabaseProperties();
        } catch (IOException e) {
            throw new ConfigurationException("Error while init nGrinder", e);
        }
    }

    /**
     * In order to initialize the cache statistics supports, we have to use this application event.
     * Because we should update the cache statistics supports after the CacheManager has been initialized.
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        updateCacheStatisticsSupports();
    }

    protected void initDevModeProperties() {
        if (!isDevMode()) {
            initLogger(false);
        } else {
            final PropertiesWrapper controllerProperties = getControllerProperties();
            controllerProperties.addProperty(PROP_CONTROLLER_AGENT_FORCE_UPDATE, "true");
            controllerProperties.addProperty(PROP_CONTROLLER_ENABLE_AGENT_AUTO_APPROVAL, "true");
            controllerProperties.addProperty(PROP_CONTROLLER_ENABLE_SCRIPT_CONSOLE, "true");
        }
    }

    private void addChangeConfigListenerForStatistics() {
        addSystemConfListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                updateCacheStatisticsSupports();
            }
        });
    }

    private void updateCacheStatisticsSupports() {
        CacheManager cacheManager = appContext.getBean("cacheManager", CacheManager.class);
        boolean enableStatistics = isEnableStatistics();
        for (String cacheName : cacheManager.getCacheNames()) {
            Cache cache = cacheManager.getCache(cacheName);
            ((net.sf.ehcache.Cache) cache.getNativeCache()).setStatisticsEnabled(enableStatistics);
        }
    }

    private boolean resolveClusterMode() {
        String mode = getClusterProperties().getProperty(PROP_CLUSTER_MODE, "none");
        return !"none".equals(mode) || getClusterProperties().getPropertyBoolean(PROP_CLUSTER_ENABLED);
    }

    /**
     * Destroy bean.
     */
    @PreDestroy
    public void destroy() {
        // Stop all the non-daemon thread.
        announcementWatchDog.interrupt();
        systemConfWatchDog.interrupt();
        policyJsWatchDog.interrupt();
    }

    /**
     * Get if the cluster mode is enable or not.
     *
     * @return true if the cluster mode is enabled.
     * @since 3.1
     */
    public boolean isClustered() {
        return cluster;
    }

    public int getControllerPort() {
        return getControllerProperties().getPropertyInt(ControllerConstants.PROP_CONTROLLER_CONTROLLER_PORT);
    }

    /**
     * Get the ngrinder instance IPs consisting of the current cluster from the configuration.
     *
     * @return ngrinder instance IPs
     */
    public String[] getClusterURIs() {
        String clusterUri = getClusterProperties().getProperty(PROP_CLUSTER_MEMBERS);
        return StringUtils.split(StringUtils.trimToEmpty(clusterUri), ",;");
    }

    /**
     * Get the current region from the configuration.
     *
     * @return region. If it's not clustered mode, return "NONE"
     */
    public String getRegion() {
        return isClustered() ? getClusterProperties().getProperty(PROP_CLUSTER_REGION) : NONE_REGION;
    }

    /**
     * Get the monitor listener port from the configuration.
     *
     * @return monitor port
     */
    public int getMonitorPort() {
        return getControllerProperties().getPropertyInt(PROP_CONTROLLER_MONITOR_PORT);
    }

    /**
     * Check if the periodic usage report is enabled.
     *
     * @return true if enabled.
     */
    public boolean isUsageReportEnabled() {
        return getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_USAGE_REPORT);
    }

    /**
     * Check if user self-registration is enabled.
     *
     * @return true if enabled.
     */
    public boolean isSignUpEnabled() {
        return getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_ALLOW_SIGN_UP);
    }

    /**
     * Initialize Logger.
     *
     * @param forceToVerbose true to force verbose logging.
     */
    public synchronized void initLogger(boolean forceToVerbose) {
        setupLogger((forceToVerbose) || getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_VERBOSE));
    }

    /**
     * Set up the logger.
     *
     * @param verbose verbose mode?
     */
    protected void setupLogger(boolean verbose) {
        this.verbose = verbose;
        final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        final JoranConfigurator configurator = new JoranConfigurator();
        configurator.setContext(context);
        context.reset();
        context.putProperty("LOG_LEVEL", verbose ? "DEBUG" : "INFO");
        File logbackConf = home.getSubFile("logback.xml");
        try {
            if (!logbackConf.exists()) {
                logbackConf = new ClassPathResource("/logback/logback-ngrinder.xml").getFile();
                if (exHome.exists() && isClustered()) {
                    context.putProperty("LOG_DIRECTORY", exHome.getGlobalLogFile().getAbsolutePath());
                    context.putProperty("SUFFIX", "_" + getRegion());
                } else {
                    context.putProperty("SUFFIX", "");
                    context.putProperty("LOG_DIRECTORY", home.getGlobalLogFile().getAbsolutePath());
                }
            }
            configurator.doConfigure(logbackConf);
        } catch (JoranException e) {
            CoreLogger.LOGGER.error(e.getMessage(), e);
        } catch (IOException e) {
            CoreLogger.LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * Copy the default files and create default directories to ${NGRINDER_HOME}.
     *
     * @throws IOException occurs when there is no such a files.
     */
    protected void copyDefaultConfigurationFiles() throws IOException {
        checkNotNull(home);
        home.copyFrom(new ClassPathResource("ngrinder_home_template").getFile());
    }

    /**
     * Resolve nGrinder home path.
     *
     * @return resolved home
     */
    protected Home resolveHome() {
        if (StringUtils.isNotBlank(System.getProperty("unit-test"))) {
            final String tempDir = System.getProperty("java.io.tmpdir");
            final File tmpHome = new File(tempDir, ".ngrinder");
            if (tmpHome.mkdirs()) {
                LOG.info("{} is created", tmpHome.getPath());
            }
            try {
                FileUtils.forceDeleteOnExit(tmpHome);
            } catch (IOException e) {
                LOG.error("Error while setting forceDeleteOnExit on {}", tmpHome);
            }
            return new Home(tmpHome);
        }
        String userHomeFromEnv = System.getenv("NGRINDER_HOME");
        String userHomeFromProperty = System.getProperty("ngrinder.home");
        if (!StringUtils.equals(userHomeFromEnv, userHomeFromProperty)) {
            CoreLogger.LOGGER.warn("The path to ngrinder-home is ambiguous:");
            CoreLogger.LOGGER.warn("    System Environment:  NGRINDER_HOME=" + userHomeFromEnv);
            CoreLogger.LOGGER.warn("    Java System Property:  ngrinder.home=" + userHomeFromProperty);
            CoreLogger.LOGGER.warn("    '" + userHomeFromProperty + "' is accepted.");
        }
        String userHome = StringUtils.defaultIfEmpty(userHomeFromProperty, userHomeFromEnv);
        if (StringUtils.isEmpty(userHome)) {
            userHome = System.getProperty("user.home") + File.separator + NGRINDER_DEFAULT_FOLDER;
        } else if (StringUtils.startsWith(userHome, "~" + File.separator)) {
            userHome = System.getProperty("user.home") + File.separator + userHome.substring(2);
        } else if (StringUtils.startsWith(userHome, "." + File.separator)) {
            userHome = System.getProperty("user.dir") + File.separator + userHome.substring(2);
        }

        userHome = FilenameUtils.normalize(userHome);
        File homeDirectory = new File(userHome);
        CoreLogger.LOGGER.info("nGrinder home directory:{}.", homeDirectory.getPath());

        return new Home(homeDirectory);
    }

    /**
     * Resolve nGrinder extended home path.
     *
     * @return resolved home
     */
    protected Home resolveExHome() {
        String exHomeFromEnv = System.getenv("NGRINDER_EX_HOME");
        String exHomeFromProperty = System.getProperty("ngrinder.ex_home");
        if (!StringUtils.equals(exHomeFromEnv, exHomeFromProperty)) {
            CoreLogger.LOGGER.warn("The path to ngrinder ex home is ambiguous:");
            CoreLogger.LOGGER.warn("    System Environment:  NGRINDER_EX_HOME=" + exHomeFromEnv);
            CoreLogger.LOGGER.warn("    Java System Property:  ngrinder.ex_home=" + exHomeFromProperty);
            CoreLogger.LOGGER.warn("    '" + exHomeFromProperty + "' is accepted.");
        }
        String userHome = StringUtils.defaultIfEmpty(exHomeFromProperty, exHomeFromEnv);

        if (StringUtils.isEmpty(userHome)) {
            userHome = System.getProperty("user.home") + File.separator + NGRINDER_EX_FOLDER;
        } else if (StringUtils.startsWith(userHome, "~" + File.separator)) {
            userHome = System.getProperty("user.home") + File.separator + userHome.substring(2);
        } else if (StringUtils.startsWith(userHome, "." + File.separator)) {
            userHome = System.getProperty("user.dir") + File.separator + userHome.substring(2);
        }

        userHome = FilenameUtils.normalize(userHome);
        File exHomeDirectory = new File(userHome);
        CoreLogger.LOGGER.info("nGrinder ex home directory:{}.", exHomeDirectory);
        try {
            //noinspection ResultOfMethodCallIgnored
            exHomeDirectory.mkdirs();
        } catch (Exception e) {
            // If it's not possible.. do it... without real directory.
            noOp();
        }
        return new Home(exHomeDirectory, false);
    }

    /**
     * Load internal properties which is not modifiable by user.
     */
    protected void loadInternalProperties() {
        InputStream inputStream = null;
        Properties properties = new Properties();
        try {
            inputStream = new ClassPathResource("/internal.properties").getInputStream();
            properties.load(inputStream);
            internalProperties = new PropertiesWrapper(properties, internalPropertiesKeyMapper);
        } catch (IOException e) {
            CoreLogger.LOGGER.error("Error while load internal.properties", e);
            internalProperties = new PropertiesWrapper(properties, internalPropertiesKeyMapper);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
    }

    /**
     * Load database related properties. (database.conf)
     */
    protected void loadDatabaseProperties() {
        checkNotNull(home);
        Properties properties = home.getProperties("database.conf");
        properties.put("NGRINDER_HOME", home.getDirectory().getAbsolutePath());
        properties.putAll(System.getProperties());
        databaseProperties = new PropertiesWrapper(properties, databasePropertiesKeyMapper);
    }

    /**
     * Load system related properties. (system.conf)
     */
    public synchronized void loadProperties() {
        Properties properties = checkNotNull(home).getProperties("system.conf");
        properties.put("NGRINDER_HOME", home.getDirectory().getAbsolutePath());
        if (exHome.exists()) {
            Properties exProperties = exHome.getProperties("system-ex.conf");
            if (exProperties.isEmpty()) {
                exProperties = exHome.getProperties("system.conf");
            }
            properties.putAll(exProperties);
        }
        properties.putAll(System.getProperties());
        // Override if exists
        controllerProperties = new PropertiesWrapper(properties, controllerPropertiesKeyMapper);
        clusterProperties = new PropertiesWrapper(properties, clusterPropertiesKeyMapper);
    }

    /**
     * Load the announcement content.
     */
    @SuppressWarnings("SynchronizeOnNonFinalField")
    public void loadAnnouncement() {
        checkNotNull(home);
        synchronized (announcement) {
            File sysFile = home.getSubFile("announcement.conf");
            try {
                announcement = FileUtils.readFileToString(sysFile, "UTF-8");
                if (sysFile.exists()) {
                    announcementDate = new Date(sysFile.lastModified());
                } else {
                    announcementDate = null;
                }
            } catch (IOException e) {
                CoreLogger.LOGGER.error("Error while reading announcement file.", e);
                announcement = "";
            }
        }
    }

    /**
     * watch docs.
     */
    private FileWatchdog announcementWatchDog;
    private FileWatchdog systemConfWatchDog;
    private FileWatchdog policyJsWatchDog;

    protected void initHomeMonitor() {
        checkNotNull(home);
        this.announcementWatchDog = new FileWatchdog(getHome().getSubFile("announcement.conf").getAbsolutePath()) {
            @Override
            protected void doOnChange() {
                CoreLogger.LOGGER.info("Announcement file is changed.");
                loadAnnouncement();
            }
        };
        announcementWatchDog.setName("WatchDog - announcement.conf");
        announcementWatchDog.setDelay(2000);
        announcementWatchDog.start();
        this.systemConfWatchDog = new FileWatchdog(getHome().getSubFile("system.conf").getAbsolutePath()) {
            @Override
            protected void doOnChange() {
                try {
                    CoreLogger.LOGGER.info("System configuration(system.conf) is changed.");
                    loadProperties();
                    systemConfListeners.apply(new Informer<PropertyChangeListener>() {
                        @Override
                        public void inform(PropertyChangeListener listener) {
                            listener.propertyChange(null);
                        }
                    });
                    CoreLogger.LOGGER.info("New system configuration is applied.");
                } catch (Exception e) {
                    CoreLogger.LOGGER.error("Error occurs while applying new system configuration", e);
                }

            }
        };
        systemConfWatchDog.setName("WatchDoc - system.conf");
        systemConfWatchDog.setDelay(2000);
        systemConfWatchDog.start();

        String processThreadPolicyPath = getHome().getSubFile("process_and_thread_policy.js").getAbsolutePath();
        this.policyJsWatchDog = new FileWatchdog(processThreadPolicyPath) {
            @Override
            protected void doOnChange() {
                CoreLogger.LOGGER.info("process_and_thread_policy file is changed.");
                policyScript = "";
            }
        };
        policyJsWatchDog.setName("WatchDoc - process_and_thread_policy.js");
        policyJsWatchDog.setDelay(2000);
        policyJsWatchDog.start();
    }

    /**
     * Get the database properties.
     *
     * @return database properties
     */
    public PropertiesWrapper getDatabaseProperties() {
        checkNotNull(databaseProperties);
        if (context.isUnitTestContext() && !databaseProperties.exist(PROP_DATABASE_UNIT_TEST)) {
            databaseProperties.addProperty(PROP_DATABASE_UNIT_TEST, "true");
        }
        return databaseProperties;
    }

    /**
     * Check if it's test mode.
     *
     * @return true if test mode
     */
    public boolean isDevMode() {
        return getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_DEV_MODE);
    }

    /**
     * Check if the user security is enabled.
     *
     * @return true if user security is enabled.
     */
    public boolean isUserSecurityEnabled() {
        return getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_USER_SECURITY);
    }

    /**
     * Check if it's the security enabled mode.
     *
     * @return true if security is enabled.
     */
    public boolean isSecurityEnabled() {
        return !isDevMode() && getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_SECURITY);
    }

    /**
     * Check if it is the demo mode.
     *
     * @return true if demo mode is enabled.
     */
    public boolean isDemo() {
        return getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_DEMO_MODE);
    }

    /**
     * Check if the plugin support is enabled.
     * <p/>
     * The reason why we need this configuration is that it takes time to initialize plugin system in unit test context.
     *
     * @return true if the plugin is supported.
     */
    public boolean isPluginSupported() {
        return (getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_PLUGIN_SUPPORT));
    }

    /**
     * Get the resolved home folder.
     *
     * @return home
     */
    public Home getHome() {
        return this.home;
    }

    /**
     * Get the resolved extended home folder.
     *
     * @return home
     * @since 3.1
     */
    public Home getExHome() {
        return this.exHome;
    }

    /**
     * Get the system properties.
     *
     * @return {@link PropertiesWrapper} which is loaded from system.conf.
     */
    public PropertiesWrapper getControllerProperties() {
        return checkNotNull(controllerProperties);
    }

    /**
     * Get the announcement content.
     *
     * @return loaded from announcement.conf.
     */
    public String getAnnouncement() {
        return announcement;
    }

    /**
     * Get the nGrinder version number.
     *
     * @return version number
     */
    public String getVersion() {
        return getInternalProperties().getProperty(InternalConstants.PROP_INTERNAL_NGRINDER_VERSION);
    }

    /**
     * Policy file which is used to determine the count of processes and threads.
     */
    private String policyScript = "";

    /**
     * Get the content of "process_and_thread_policy.js" file.
     *
     * @return loaded file content.
     */
    public String getProcessAndThreadPolicyScript() {
        if (StringUtils.isEmpty(policyScript)) {
            try {
                policyScript = FileUtils.readFileToString(getHome().getSubFile("process_and_thread_policy.js"));
                return policyScript;
            } catch (IOException e) {
                LOG.error("Error while load process_and_thread_policy.js", e);
            }
        }
        return policyScript;
    }

    /**
     * Get the internal properties.
     *
     * @return internal properties
     */
    public PropertiesWrapper getInternalProperties() {
        return internalProperties;
    }

    /**
     * Check if it's verbose logging mode.
     *
     * @return true if verbose
     */
    public boolean isVerbose() {
        return verbose;
    }

    /**
     * Get the currently configured controller IP.
     *
     * @return current IP.
     */
    public String getCurrentIP() {
        if (cluster) {
            return StringUtils.trimToEmpty(getClusterProperties().getProperty(PROP_CLUSTER_HOST));
        } else {
            return StringUtils.trimToEmpty(getControllerProperties().getProperty(PROP_CONTROLLER_IP));
        }
    }

    /**
     * Check if the current ngrinder instance is hidden instance from the cluster.
     *
     * @return true if hidden.
     */
    public boolean isInvisibleRegion() {
        return getClusterProperties().getPropertyBoolean(PROP_CLUSTER_HIDDEN_REGION);
    }

    /**
     * Check if no_more_test.lock to block further test executions exists.
     *
     * @return true if it exists
     */
    public boolean hasNoMoreTestLock() {
        return exHome.exists() && exHome.getSubFile("no_more_test.lock").exists();
    }

    /**
     * Check if shutdown.lock exists.
     *
     * @return true if it exists
     */
    public boolean hasShutdownLock() {
        return exHome.exists() && exHome.getSubFile("shutdown.lock").exists();
    }

    /**
     * Get the date of the recent announcement modification.
     *
     * @return the date of the recent announcement modification.
     */
    public Date getAnnouncementDate() {
        return announcementDate;
    }

    /**
     * Get ngrinder help URL.
     *
     * @return help URL
     */
    public String getHelpUrl() {
        return getControllerProperties().getProperty(PROP_CONTROLLER_HELP_URL);
    }

    public PropertiesWrapper getClusterProperties() {
        return clusterProperties;
    }

    /**
     * Get the time out milliseconds which would be used between the console and the agent while preparing to test.
     *
     * @return
     */
    public long getInactiveClientTimeOut() {
        return getControllerProperties().getPropertyLong(PROP_CONTROLLER_INACTIVE_CLIENT_TIME_OUT);
    }

    public boolean isEnableStatistics() {
        return getControllerProperties().getPropertyBoolean(PROP_CONTROLLER_ENABLE_STATISTICS);
    }
}