org.nuxeo.launcher.config.ConfigurationGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.launcher.config.ConfigurationGenerator.java

Source

/*
 * (C) Copyright 2010-2016 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * 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.
 *
 * Contributors:
 *     Julien Carsique
 *
 */

package org.nuxeo.launcher.config;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.UUID;

import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;
import org.apache.log4j.helpers.NullEnumeration;

import org.nuxeo.common.Environment;
import org.nuxeo.common.codec.Crypto;
import org.nuxeo.common.codec.CryptoProperties;
import org.nuxeo.common.utils.TextTemplate;
import org.nuxeo.launcher.commons.DatabaseDriverException;
import org.nuxeo.launcher.config.JVMVersion.UpTo;
import org.nuxeo.log4j.Log4JHelper;

import freemarker.core.ParseException;
import freemarker.template.TemplateException;

/**
 * Builder for server configuration and datasource files from templates and properties.
 *
 * @author jcarsique
 */
public class ConfigurationGenerator {

    /**
     * @since 6.0
     */
    public static final String TEMPLATE_SEPARATOR = ",";

    /**
     * Accurate but not used internally. NXP-18023: Java 8 update 40+ required
     *
     * @since 5.7
     */
    public static final String[] COMPLIANT_JAVA_VERSIONS = new String[] { "1.8.0_40" };

    /**
     * @since 5.6
     */
    protected static final String CONFIGURATION_PROPERTIES = "configuration.properties";

    private static final Log log = LogFactory.getLog(ConfigurationGenerator.class);

    /**
     * @deprecated Since 5.6, use {@link Environment#NUXEO_HOME} instead
     */
    @Deprecated
    public static final String NUXEO_HOME = Environment.NUXEO_HOME;

    public static final String NUXEO_CONF = "nuxeo.conf";

    public static final String TEMPLATES = "templates";

    public static final String NUXEO_DEFAULT_CONF = "nuxeo.defaults";

    /**
     * Absolute or relative PATH to the user chosen template
     *
     * @deprecated use {@link #PARAM_TEMPLATES_NAME} instead
     */
    @Deprecated
    public static final String PARAM_TEMPLATE_NAME = "nuxeo.template";

    /**
     * Absolute or relative PATH to the user chosen templates (comma separated list)
     */
    public static final String PARAM_TEMPLATES_NAME = "nuxeo.templates";

    public static final String PARAM_TEMPLATE_DBNAME = "nuxeo.dbtemplate";

    /**
     * @since 8.1
     */
    public static final String PARAM_TEMPLATE_DBNOSQL_NAME = "nuxeo.dbnosqltemplate";

    public static final String PARAM_TEMPLATE_DBTYPE = "nuxeo.db.type";

    /**
     * @since 8.1
     */
    public static final String PARAM_TEMPLATE_DBNOSQL_TYPE = "nuxeo.dbnosql.type";

    /**
     * @deprecated since 5.7
     */
    @Deprecated
    public static final String PARAM_TEMPLATES_NODB = "nuxeo.nodbtemplates";

    public static final String OLD_PARAM_TEMPLATES_PARSING_EXTENSIONS = "nuxeo.templates.parsing.extensions";

    public static final String PARAM_TEMPLATES_PARSING_EXTENSIONS = "nuxeo.plaintext_parsing_extensions";

    public static final String PARAM_TEMPLATES_FREEMARKER_EXTENSIONS = "nuxeo.freemarker_parsing_extensions";

    /**
     * Absolute or relative PATH to the included templates (comma separated list)
     */
    protected static final String PARAM_INCLUDED_TEMPLATES = "nuxeo.template.includes";

    public static final String PARAM_FORCE_GENERATION = "nuxeo.force.generation";

    public static final String BOUNDARY_BEGIN = "### BEGIN - DO NOT EDIT BETWEEN BEGIN AND END ###";

    public static final String BOUNDARY_END = "### END - DO NOT EDIT BETWEEN BEGIN AND END ###";

    public static final List<String> DB_LIST = Arrays.asList("default", "postgresql", "oracle", "mysql", "mssql",
            "db2");

    public static final List<String> DB_NOSQL_LIST = Arrays.asList("none", "mongodb", "marklogic");

    public static final List<String> DB_EXCLUDE_CHECK_LIST = Arrays.asList("default", "none");

    public static final String PARAM_WIZARD_DONE = "nuxeo.wizard.done";

    public static final String PARAM_WIZARD_RESTART_PARAMS = "wizard.restart.params";

    public static final String PARAM_FAKE_WINDOWS = "org.nuxeo.fake.vindoz";

    public static final String PARAM_LOOPBACK_URL = "nuxeo.loopback.url";

    public static final int MIN_PORT = 1;

    public static final int MAX_PORT = 65535;

    public static final int ADDRESS_PING_TIMEOUT = 1000;

    public static final String PARAM_BIND_ADDRESS = "nuxeo.bind.address";

    public static final String PARAM_HTTP_PORT = "nuxeo.server.http.port";

    /**
     * @deprecated Since 7.4. Use {@link Environment#SERVER_STATUS_KEY} instead
     */
    @Deprecated
    public static final String PARAM_STATUS_KEY = Environment.SERVER_STATUS_KEY;

    public static final String PARAM_CONTEXT_PATH = "org.nuxeo.ecm.contextPath";

    public static final String PARAM_MP_DIR = "nuxeo.distribution.marketplace.dir";

    public static final String DISTRIBUTION_MP_DIR = "setupWizardDownloads";

    public static final String INSTALL_AFTER_RESTART = "installAfterRestart.log";

    public static final String PARAM_DB_DRIVER = "nuxeo.db.driver";

    public static final String PARAM_DB_JDBC_URL = "nuxeo.db.jdbc.url";

    public static final String PARAM_DB_HOST = "nuxeo.db.host";

    public static final String PARAM_DB_PORT = "nuxeo.db.port";

    public static final String PARAM_DB_NAME = "nuxeo.db.name";

    public static final String PARAM_DB_USER = "nuxeo.db.user";

    public static final String PARAM_DB_PWD = "nuxeo.db.password";

    /**
     * @since 8.1
     */
    public static final String PARAM_MONGODB_NAME = "nuxeo.mongodb.dbname";

    /**
     * @since 8.1
     */
    public static final String PARAM_MONGODB_SERVER = "nuxeo.mongodb.server";

    /**
     * Keys which value must be displayed thoughtfully
     *
     * @since 8.1
     */
    public static final List<String> SECRET_KEYS = Arrays.asList(PARAM_DB_PWD, "mailservice.password",
            "mail.transport.password", "nuxeo.http.proxy.password", "nuxeo.ldap.bindpassword",
            "nuxeo.user.emergency.password");

    /**
     * @deprecated Since 7.10. Use {@link Environment#PRODUCT_NAME}
     */
    @Deprecated
    public static final String PARAM_PRODUCT_NAME = Environment.PRODUCT_NAME;

    /**
     * @deprecated Since 7.10. Use {@link Environment#PRODUCT_VERSION}
     */
    @Deprecated
    public static final String PARAM_PRODUCT_VERSION = Environment.PRODUCT_VERSION;

    /**
     * @since 5.6
     */
    public static final String PARAM_NUXEO_URL = "nuxeo.url";

    /**
     * Global dev property, duplicated from runtime framework
     *
     * @since 5.6
     */
    public static final String NUXEO_DEV_SYSTEM_PROP = "org.nuxeo.dev";

    /**
     * Seam hot reload property, also controlled by {@link #NUXEO_DEV_SYSTEM_PROP}
     *
     * @since 5.6
     */
    public static final String SEAM_DEBUG_SYSTEM_PROP = "org.nuxeo.seam.debug";

    /**
     * Old way of detecting if seam debug should be enabled, by looking for the presence of this file. Setting property
     * {@link #SEAM_DEBUG_SYSTEM_PROP} in nuxeo.conf is enough now.
     *
     * @deprecated since 5.6
     * @since 5.6
     */
    @Deprecated
    public static final String SEAM_HOT_RELOAD_GLOBAL_CONFIG_FILE = "seam-debug.properties";

    /** @since 8.4 */
    public static final String JVMCHECK_PROP = "jvmcheck";

    /** @since 8.4 */
    public static final String JVMCHECK_FAIL = "fail";

    /** @since 8.4 */
    public static final String JVMCHECK_NOFAIL = "nofail";

    private final File nuxeoHome;

    // User configuration file
    private final File nuxeoConf;

    // Chosen templates
    private final List<File> includedTemplates = new ArrayList<>();

    // Common default configuration file
    private File nuxeoDefaultConf;

    public boolean isJBoss;

    public boolean isJetty;

    public boolean isTomcat;

    private ServerConfigurator serverConfigurator;

    private boolean forceGeneration;

    private Properties defaultConfig;

    private CryptoProperties userConfig;

    private boolean configurable = false;

    private boolean onceGeneration = false;

    private String templates;

    // if PARAM_FORCE_GENERATION=once, set to false; else keep current value
    private boolean setOnceToFalse = true;

    // if PARAM_FORCE_GENERATION=false, set to once; else keep the current
    // value
    private boolean setFalseToOnce = false;

    public boolean isConfigurable() {
        return configurable;
    }

    public ConfigurationGenerator() {
        this(true, false);
    }

    private boolean quiet = false;

    @SuppressWarnings("unused")
    private boolean debug = false;

    private static boolean hideDeprecationWarnings = false;

    private Environment env;

    private Properties storedConfig;

    private String currentConfigurationDigest;

    /**
     * @since 5.7
     */
    protected Properties getStoredConfig() {
        if (storedConfig == null) {
            updateStoredConfig();
        }
        return storedConfig;
    }

    protected static final Map<String, String> parametersMigration = new HashMap<String, String>() {
        private static final long serialVersionUID = 1L;
        {
            put(OLD_PARAM_TEMPLATES_PARSING_EXTENSIONS, PARAM_TEMPLATES_PARSING_EXTENSIONS);
            put("nuxeo.db.user.separator.key", "nuxeo.db.user_separator_key");
            put("mail.pop3.host", "mail.store.host");
            put("mail.pop3.port", "mail.store.port");
            put("mail.smtp.host", "mail.transport.host");
            put("mail.smtp.port", "mail.transport.port");
            put("mail.smtp.username", "mail.transport.username");
            put("mail.transport.username", "mail.transport.user");
            put("mail.smtp.password", "mail.transport.password");
            put("mail.smtp.usetls", "mail.transport.usetls");
            put("mail.smtp.auth", "mail.transport.auth");
        }
    };

    /**
     * @param quiet Suppress info level messages from the console output
     * @param debug Activate debug level logging
     * @since 5.6
     */
    public ConfigurationGenerator(boolean quiet, boolean debug) {
        this.quiet = quiet;
        this.debug = debug;
        File serverHome = Environment.getDefault().getServerHome();
        if (serverHome != null) {
            nuxeoHome = serverHome.getAbsoluteFile();
        } else {
            File userDir = new File(System.getProperty("user.dir"));
            if ("bin".equalsIgnoreCase(userDir.getName())) {
                nuxeoHome = userDir.getParentFile().getAbsoluteFile();
            } else {
                nuxeoHome = userDir.getAbsoluteFile();
            }
        }
        String nuxeoConfPath = System.getProperty(NUXEO_CONF);
        if (nuxeoConfPath != null) {
            nuxeoConf = new File(nuxeoConfPath).getAbsoluteFile();
        } else {
            nuxeoConf = new File(nuxeoHome, "bin" + File.separator + "nuxeo.conf").getAbsoluteFile();
        }
        System.setProperty(NUXEO_CONF, nuxeoConf.getPath());

        nuxeoDefaultConf = new File(nuxeoHome, TEMPLATES + File.separator + NUXEO_DEFAULT_CONF);

        // detect server type based on System properties
        isJetty = System.getProperty(JettyConfigurator.JETTY_HOME) != null;
        isTomcat = System.getProperty(TomcatConfigurator.TOMCAT_HOME) != null;
        if (!isJBoss && !isJetty && !isTomcat) {
            // fallback on jar detection
            isJBoss = new File(nuxeoHome, "bin/run.jar").exists();
            isTomcat = new File(nuxeoHome, "bin/bootstrap.jar").exists();
            String[] files = nuxeoHome.list();
            for (String file : files) {
                if (file.startsWith("nuxeo-runtime-launcher")) {
                    isJetty = true;
                    break;
                }
            }
        }
        if (isTomcat) {
            serverConfigurator = new TomcatConfigurator(this);
        } else if (isJetty) {
            serverConfigurator = new JettyConfigurator(this);
        } else {
            serverConfigurator = new UnknownServerConfigurator(this);
        }
        if (Logger.getRootLogger().getAllAppenders() instanceof NullEnumeration) {
            serverConfigurator.initLogs();
        }
        String homeInfo = "Nuxeo home:          " + nuxeoHome.getPath();
        String confInfo = "Nuxeo configuration: " + nuxeoConf.getPath();
        if (quiet) {
            log.debug(homeInfo);
            log.debug(confInfo);
        } else {
            log.info(homeInfo);
            log.info(confInfo);
        }
    }

    public void hideDeprecationWarnings(boolean hide) {
        hideDeprecationWarnings = hide;
    }

    /**
     * @see #PARAM_FORCE_GENERATION
     * @param forceGeneration
     */
    public void setForceGeneration(boolean forceGeneration) {
        this.forceGeneration = forceGeneration;
    }

    /**
     * @see #PARAM_FORCE_GENERATION
     * @return true if configuration will be generated from templates
     * @since 5.4.2
     */
    public boolean isForceGeneration() {
        return forceGeneration;
    }

    public CryptoProperties getUserConfig() {
        return userConfig;
    }

    /**
     * @since 5.4.2
     */
    public final ServerConfigurator getServerConfigurator() {
        return serverConfigurator;
    }

    /**
     * Runs the configuration files generation.
     */
    public void run() throws ConfigurationException {
        if (init()) {
            if (!serverConfigurator.isConfigured()) {
                log.info("No current configuration, generating files...");
                generateFiles();
            } else if (forceGeneration) {
                log.info("Configuration files generation (nuxeo.force.generation="
                        + userConfig.getProperty(PARAM_FORCE_GENERATION) + ")...");
                generateFiles();
            } else {
                log.info(
                        "Server already configured (set nuxeo.force.generation=true to force configuration files generation).");
            }
        }
    }

    /**
     * Initialize configurator, check requirements and load current configuration
     *
     * @return returns true if current install is configurable, else returns false
     */
    public boolean init() {
        return init(false);
    }

    /**
     * Initialize configurator, check requirements and load current configuration
     *
     * @since 5.6
     * @param forceReload If true, forces configuration reload.
     * @return returns true if current install is configurable, else returns false
     */
    public boolean init(boolean forceReload) {
        if (serverConfigurator instanceof UnknownServerConfigurator) {
            configurable = false;
            forceGeneration = false;
            log.warn("Server will be considered as not configurable.");
        }
        if (!nuxeoConf.exists()) {
            log.info("Missing " + nuxeoConf);
            configurable = false;
            userConfig = new CryptoProperties();
            defaultConfig = new Properties();
        } else if (userConfig == null || userConfig.size() == 0 || forceReload) {
            try {
                setBasicConfiguration();
                configurable = true;
            } catch (ConfigurationException e) {
                log.warn("Error reading basic configuration.", e);
                configurable = false;
            }
        } else {
            configurable = true;
        }
        return configurable;
    }

    /**
     * @param newTemplates
     * @return Old templates
     */
    public String changeTemplates(String newTemplates) {
        String oldTemplates = templates;
        templates = newTemplates;
        try {
            setBasicConfiguration(false);
            configurable = true;
        } catch (ConfigurationException e) {
            log.warn("Error reading basic configuration.", e);
            configurable = false;
        }
        return oldTemplates;
    }

    /**
     * Change templates using given database template
     *
     * @param dbTemplate new database template
     * @since 5.4.2
     */
    public void changeDBTemplate(String dbTemplate) {
        changeTemplates(rebuildTemplatesStr(dbTemplate));
    }

    private void setBasicConfiguration() throws ConfigurationException {
        setBasicConfiguration(true);
    }

    private void setBasicConfiguration(boolean save) throws ConfigurationException {
        try {
            // Load default configuration
            defaultConfig = loadTrimmedProperties(nuxeoDefaultConf);
            // Add System properties
            defaultConfig.putAll(System.getProperties());
            userConfig = new CryptoProperties(defaultConfig);

            // If Windows, replace backslashes in paths in nuxeo.conf
            if (SystemUtils.IS_OS_WINDOWS) {
                replaceBackslashes();
            }
            // Load user configuration
            userConfig.putAll(loadTrimmedProperties(nuxeoConf));
            onceGeneration = "once".equals(userConfig.getProperty(PARAM_FORCE_GENERATION));
            forceGeneration = onceGeneration
                    || Boolean.parseBoolean(userConfig.getProperty(PARAM_FORCE_GENERATION, "false"));
            checkForDeprecatedParameters(userConfig);

            // Synchronize directories between serverConfigurator and
            // userConfig/defaultConfig
            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_DATA_DIR);
            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_LOG_DIR);
            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_PID_DIR);
            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_TMP_DIR);
            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_MP_DIR);
        } catch (NullPointerException e) {
            throw new ConfigurationException("Missing file", e);
        } catch (FileNotFoundException e) {
            throw new ConfigurationException("Missing file: " + nuxeoDefaultConf + " or " + nuxeoConf, e);
        } catch (IOException e) {
            throw new ConfigurationException("Error reading " + nuxeoConf, e);
        }

        // Override default configuration with specific configuration(s) of
        // the chosen template(s) which can be outside of server filesystem
        try {
            includeTemplates();
            checkForDeprecatedParameters(defaultConfig);
            extractDatabaseTemplateName();
            extractNoSqlDatabaseTemplateName();
        } catch (FileNotFoundException e) {
            throw new ConfigurationException("Missing file", e);
        } catch (IOException e) {
            throw new ConfigurationException("Error reading " + nuxeoConf, e);
        }

        Map<String, String> newParametersToSave = evalDynamicProperties();
        if (save && newParametersToSave != null && !newParametersToSave.isEmpty()) {
            saveConfiguration(newParametersToSave, false, false);
        }

        logDebugInformation();

        // Could be useful to initialize DEFAULT env...
        // initEnv();
    }

    /**
     * @since 5.7
     * @throws IOException
     */
    protected void includeTemplates() throws IOException {
        includedTemplates.clear();
        List<File> orderedTemplates = includeTemplates(getUserTemplates());
        includedTemplates.clear();
        includedTemplates.addAll(orderedTemplates);
        log.debug(includedTemplates);
    }

    /**
     * Old way of detecting if seam debug is set, by checking for the presence of a file.
     * <p>
     * On 5.6, using the config generator to get the info from the nuxeo.conf file makes it possible to get the property
     * value this early, so adding an empty file at {@link #SEAM_HOT_RELOAD_GLOBAL_CONFIG_FILE} is no longer needed.
     *
     * @deprecated since 5.6
     */
    @Deprecated
    protected boolean hasSeamDebugFile() {
        File f = new File(getServerConfigurator().getConfigDir(), SEAM_HOT_RELOAD_GLOBAL_CONFIG_FILE);
        if (!f.exists()) {
            return false;
        }
        return true;
    }

    private void logDebugInformation() {
        String debugPropValue = userConfig.getProperty(NUXEO_DEV_SYSTEM_PROP);
        if (Boolean.parseBoolean(debugPropValue)) {
            log.debug("Nuxeo Dev mode enabled");
        } else {
            log.debug("Nuxeo Dev mode is not enabled");
        }

        // XXX: cannot init seam debug mode when global debug mode is set, as
        // it needs to be activated at startup, and requires the seam-debug jar
        // to be in the classpath anyway
        String seamDebugPropValue = userConfig.getProperty(SEAM_DEBUG_SYSTEM_PROP);
        if (Boolean.parseBoolean(seamDebugPropValue) || hasSeamDebugFile()) {
            log.debug("Nuxeo Seam HotReload is enabled");
            // add it to the system props for compat, in case this mode was
            // detected because of presence of the file in the config dir, and
            // because it's checked there on code that relies on it
            System.setProperty(SEAM_DEBUG_SYSTEM_PROP, "true");
        } else {
            log.debug("Nuxeo Seam HotReload is not enabled");
        }
    }

    /**
     * Generate properties which values are based on others
     *
     * @return Map with new parameters to save in {@code nuxeoConf}
     * @throws ConfigurationException
     * @since 5.5
     */
    protected HashMap<String, String> evalDynamicProperties() throws ConfigurationException {
        HashMap<String, String> newParametersToSave = new HashMap<>();
        evalLoopbackURL();
        evalServerStatusKey(newParametersToSave);
        return newParametersToSave;
    }

    /**
     * Generate a server status key if not already set
     *
     * @param newParametersToSave
     * @throws ConfigurationException
     * @see Environment#SERVER_STATUS_KEY
     * @since 5.5
     */
    private void evalServerStatusKey(Map<String, String> newParametersToSave) throws ConfigurationException {
        if (userConfig.getProperty(Environment.SERVER_STATUS_KEY) == null) {
            newParametersToSave.put(Environment.SERVER_STATUS_KEY, UUID.randomUUID().toString().substring(0, 8));
        }
    }

    private void evalLoopbackURL() throws ConfigurationException {
        String loopbackURL = userConfig.getProperty(PARAM_LOOPBACK_URL);
        if (loopbackURL != null) {
            log.debug("Using configured loop back url: " + loopbackURL);
            return;
        }
        InetAddress bindAddress = getBindAddress();
        // Address and ports already checked by #checkAddressesAndPorts
        try {
            if (bindAddress.isAnyLocalAddress()) {
                boolean preferIPv6 = "false".equals(System.getProperty("java.net.preferIPv4Stack"))
                        && "true".equals(System.getProperty("java.net.preferIPv6Addresses"));
                bindAddress = preferIPv6 ? InetAddress.getByName("::1") : InetAddress.getByName("127.0.0.1");
                log.debug("Bind address is \"ANY\", using local address instead: " + bindAddress);
            }
        } catch (UnknownHostException e) {
            log.debug(e, e);
            log.error(e.getMessage());
        }

        String httpPort = userConfig.getProperty(PARAM_HTTP_PORT);
        String contextPath = userConfig.getProperty(PARAM_CONTEXT_PATH);
        // Is IPv6 or IPv4 ?
        if (bindAddress instanceof Inet6Address) {
            loopbackURL = "http://[" + bindAddress.getHostAddress() + "]:" + httpPort + contextPath;
        } else {
            loopbackURL = "http://" + bindAddress.getHostAddress() + ":" + httpPort + contextPath;
        }
        log.debug("Set as loop back URL: " + loopbackURL);
        defaultConfig.setProperty(PARAM_LOOPBACK_URL, loopbackURL);
    }

    /**
     * Read nuxeo.conf, replace backslashes in paths and write new nuxeo.conf
     *
     * @throws ConfigurationException if any error reading or writing nuxeo.conf
     * @since 5.4.1
     */
    protected void replaceBackslashes() throws ConfigurationException {
        StringBuffer sb = new StringBuffer();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(nuxeoConf));
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.matches(".*:\\\\.*")) {
                    line = line.replaceAll("\\\\", "/");
                }
                sb.append(line + System.getProperty("line.separator"));
            }
            reader.close();
        } catch (IOException e) {
            throw new ConfigurationException("Error reading " + nuxeoConf, e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    throw new ConfigurationException(e);
                }
            }
        }
        FileWriter writer = null;
        try {
            writer = new FileWriter(nuxeoConf, false);
            // Copy back file content
            writer.append(sb.toString());
        } catch (IOException e) {
            throw new ConfigurationException("Error writing in " + nuxeoConf, e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    throw new ConfigurationException(e);
                }
            }
        }
    }

    /**
     * @since 5.4.2
     * @param key Directory system key
     * @see Environment
     */
    public void setDirectoryWithProperty(String key) {
        String directory = userConfig.getProperty(key);
        if (directory == null) {
            defaultConfig.setProperty(key, serverConfigurator.getDirectory(key).getPath());
        } else {
            serverConfigurator.setDirectory(key, directory);
        }
    }

    public String getUserTemplates() {
        if (templates == null) {
            templates = userConfig.getProperty(PARAM_TEMPLATES_NAME);
        }
        if (templates == null) {
            // backward compliance: manage parameter for a single template
            templates = userConfig.getProperty(PARAM_TEMPLATE_NAME);
        }
        if (templates == null) {
            log.warn("No template found in configuration! Fallback on 'default'.");
            templates = "default";
        }
        userConfig.setProperty(PARAM_TEMPLATES_NAME, templates);
        return templates;
    }

    protected void generateFiles() throws ConfigurationException {
        try {
            serverConfigurator.parseAndCopy(userConfig);
            serverConfigurator.dumpProperties(userConfig);
            log.info("Configuration files generated.");
            // keep true or false, switch once to false
            if (onceGeneration) {
                setOnceToFalse = true;
                writeConfiguration();
            }
        } catch (FileNotFoundException e) {
            throw new ConfigurationException("Missing file: " + e.getMessage(), e);
        } catch (TemplateException | ParseException e) {
            throw new ConfigurationException("Could not process FreeMarker template: " + e.getMessage(), e);
        } catch (IOException e) {
            throw new ConfigurationException("Configuration failure: " + e.getMessage(), e);
        }
    }

    private List<File> includeTemplates(String templatesList) throws IOException {
        List<File> orderedTemplates = new ArrayList<>();
        StringTokenizer st = new StringTokenizer(templatesList, TEMPLATE_SEPARATOR);
        while (st.hasMoreTokens()) {
            String nextToken = st.nextToken();
            File chosenTemplate = new File(nextToken);
            // is it absolute and existing or relative path ?
            if (!chosenTemplate.exists() || !chosenTemplate.getPath().equals(chosenTemplate.getAbsolutePath())) {
                chosenTemplate = new File(nuxeoDefaultConf.getParentFile(), nextToken);
            }
            if (includedTemplates.contains(chosenTemplate)) {
                log.debug("Already included " + nextToken);
                continue;
            }
            if (!chosenTemplate.exists()) {
                log.error(String.format(
                        "Template '%s' not found with relative or absolute path (%s). "
                                + "Check your %s parameter, and %s for included files.",
                        nextToken, chosenTemplate, PARAM_TEMPLATES_NAME, PARAM_INCLUDED_TEMPLATES));
                continue;
            }
            File chosenTemplateConf = new File(chosenTemplate, NUXEO_DEFAULT_CONF);
            includedTemplates.add(chosenTemplate);
            if (!chosenTemplateConf.exists()) {
                log.warn("Ignore template (no default configuration): " + nextToken);
                continue;
            }

            Properties subTemplateConf = loadTrimmedProperties(chosenTemplateConf);
            String subTemplatesList = subTemplateConf.getProperty(PARAM_INCLUDED_TEMPLATES);
            if (subTemplatesList != null && subTemplatesList.length() > 0) {
                orderedTemplates.addAll(includeTemplates(subTemplatesList));
            }
            // Load configuration from chosen templates
            defaultConfig.putAll(subTemplateConf);
            orderedTemplates.add(chosenTemplate);
            String templateInfo = "Include template: " + chosenTemplate.getPath();
            if (quiet) {
                log.debug(templateInfo);
            } else {
                log.info(templateInfo);
            }
        }
        return orderedTemplates;
    }

    /**
     * Check for deprecated parameters
     *
     * @param properties
     * @since 5.6
     */
    protected void checkForDeprecatedParameters(Properties properties) {
        serverConfigurator.addServerSpecificParameters(parametersMigration);
        @SuppressWarnings("rawtypes")
        Enumeration userEnum = properties.propertyNames();
        while (userEnum.hasMoreElements()) {
            String key = (String) userEnum.nextElement();
            if (parametersMigration.containsKey(key)) {
                String value = properties.getProperty(key);
                properties.setProperty(parametersMigration.get(key), value);
                // Don't remove the deprecated key yet - more
                // warnings but old things should keep working
                // properties.remove(key);
                if (!hideDeprecationWarnings) {
                    log.warn("Parameter " + key + " is deprecated - please use " + parametersMigration.get(key)
                            + " instead");
                }
            }
        }
    }

    public File getNuxeoHome() {
        return nuxeoHome;
    }

    public File getNuxeoDefaultConf() {
        return nuxeoDefaultConf;
    }

    public List<File> getIncludedTemplates() {
        return includedTemplates;
    }

    public static void main(String[] args) throws ConfigurationException {
        new ConfigurationGenerator().run();
    }

    /**
     * Save changed parameters in {@code nuxeo.conf}. This method does not check values in map. Use
     * {@link #saveFilteredConfiguration(Map)} for parameters filtering.
     *
     * @param changedParameters Map of modified parameters
     * @see #saveFilteredConfiguration(Map)
     */
    public void saveConfiguration(Map<String, String> changedParameters) throws ConfigurationException {
        // Keep generation true or once; switch false to once
        saveConfiguration(changedParameters, false, true);
    }

    /**
     * Save changed parameters in {@code nuxeo.conf} calculating templates if changedParameters contains a value for
     * {@link #PARAM_TEMPLATE_DBNAME}. If a parameter value is empty ("" or null), then the property is unset.
     * {@link #PARAM_WIZARD_DONE}, {@link #PARAM_TEMPLATES_NAME} and {@link #PARAM_FORCE_GENERATION} cannot be unset,
     * but their value can be changed.<br/>
     * This method does not check values in map: use {@link #saveFilteredConfiguration(Map)} for parameters filtering.
     *
     * @param changedParameters Map of modified parameters
     * @param setGenerationOnceToFalse If generation was on (true or once), then set it to false or not?
     * @param setGenerationFalseToOnce If generation was off (false), then set it to once?
     * @see #saveFilteredConfiguration(Map)
     * @since 5.5
     */
    public void saveConfiguration(Map<String, String> changedParameters, boolean setGenerationOnceToFalse,
            boolean setGenerationFalseToOnce) throws ConfigurationException {
        setOnceToFalse = setGenerationOnceToFalse;
        setFalseToOnce = setGenerationFalseToOnce;
        updateStoredConfig();
        String newDbTemplate = changedParameters.remove(PARAM_TEMPLATE_DBNAME);
        if (newDbTemplate != null) {
            changedParameters.put(PARAM_TEMPLATES_NAME, rebuildTemplatesStr(newDbTemplate));
        }
        newDbTemplate = changedParameters.remove(PARAM_TEMPLATE_DBNOSQL_NAME);
        if (newDbTemplate != null) {
            changedParameters.put(PARAM_TEMPLATES_NAME, rebuildTemplatesStr(newDbTemplate));
        }
        if (changedParameters.containsValue(null) || changedParameters.containsValue("")) {
            // There are properties to unset
            Set<String> propertiesToUnset = new HashSet<>();
            for (String key : changedParameters.keySet()) {
                if (StringUtils.isEmpty(changedParameters.get(key))) {
                    propertiesToUnset.add(key);
                }
            }
            for (String key : propertiesToUnset) {
                changedParameters.remove(key);
                userConfig.remove(key);
            }
        }
        userConfig.putAll(changedParameters);
        writeConfiguration();
        updateStoredConfig();
    }

    private void updateStoredConfig() {
        if (storedConfig == null) {
            storedConfig = new Properties(defaultConfig);
        } else {
            storedConfig.clear();
        }
        storedConfig.putAll(userConfig);
    }

    /**
     * Save changed parameters in {@code nuxeo.conf}, filtering parameters with {@link #getChangedParameters(Map)}
     *
     * @param changedParameters Maps of modified parameters
     * @since 5.4.2
     * @see #saveConfiguration(Map)
     * @see #getChangedParameters(Map)
     */
    public void saveFilteredConfiguration(Map<String, String> changedParameters) throws ConfigurationException {
        Map<String, String> filteredParameters = getChangedParameters(changedParameters);
        saveConfiguration(filteredParameters);
    }

    /**
     * Filters given parameters including them only if (there was no previous value and new value is not empty/null) or
     * (there was a previous value and it differs from the new value)
     *
     * @param changedParameters parameters to be filtered
     * @return filtered map
     * @since 5.4.2
     */
    public Map<String, String> getChangedParameters(Map<String, String> changedParameters) {
        Map<String, String> filteredChangedParameters = new HashMap<>();
        for (String key : changedParameters.keySet()) {
            String oldParam = getStoredConfig().getProperty(key);
            String newParam = changedParameters.get(key);
            if (newParam != null) {
                newParam = newParam.trim();
            }
            if (oldParam == null && StringUtils.isNotEmpty(newParam)
                    || oldParam != null && !oldParam.trim().equals(newParam)) {
                filteredChangedParameters.put(key, newParam);
            }
        }
        return filteredChangedParameters;
    }

    private void writeConfiguration() throws ConfigurationException {
        final MessageDigest newContentDigest = DigestUtils.getMd5Digest();
        StringWriter newContent = new StringWriter() {
            @Override
            public void write(String str) {
                if (str != null) {
                    newContentDigest.update(str.getBytes());
                }
                super.write(str);
            }
        };
        // Copy back file content
        newContent.append(readConfiguration());
        // Write changed parameters
        newContent.write(BOUNDARY_BEGIN + System.getProperty("line.separator"));
        for (Object o : new TreeSet<>(userConfig.keySet())) {
            String key = (String) o;
            // Ignore parameters already stored in newContent
            if (PARAM_FORCE_GENERATION.equals(key) || PARAM_WIZARD_DONE.equals(key)
                    || PARAM_TEMPLATES_NAME.equals(key)) {
                continue;
            }
            String oldValue = storedConfig.getProperty(key, "");
            String newValue = userConfig.getRawProperty(key, "");
            if (!newValue.equals(oldValue)) {
                newContent.write("#" + key + "=" + oldValue + System.getProperty("line.separator"));
                newContent.write(key + "=" + newValue + System.getProperty("line.separator"));
            }
        }
        newContent.write(BOUNDARY_END + System.getProperty("line.separator"));

        // Write file only if content has changed
        if (!Hex.encodeHexString(newContentDigest.digest()).equals(currentConfigurationDigest)) {
            try (Writer writer = new FileWriter(nuxeoConf, false)) {
                writer.append(newContent.getBuffer());
            } catch (IOException e) {
                throw new ConfigurationException("Error writing in " + nuxeoConf, e);
            }
        }
    }

    private StringBuffer readConfiguration() throws ConfigurationException {
        String wizardParam = null, templatesParam = null;
        Integer generationIndex = null, wizardIndex = null, templatesIndex = null;
        // Will change wizardParam value instead of appending it
        wizardParam = userConfig.getProperty(PARAM_WIZARD_DONE);
        // Will change templatesParam value instead of appending it
        templatesParam = userConfig.getProperty(PARAM_TEMPLATES_NAME);
        ArrayList<String> newLines = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(nuxeoConf))) {
            String line;
            MessageDigest digest = DigestUtils.getMd5Digest();
            boolean onConfiguratorContent = false;
            while ((line = reader.readLine()) != null) {
                digest.update(line.getBytes());
                if (!onConfiguratorContent) {
                    if (!line.startsWith(BOUNDARY_BEGIN)) {
                        if (line.startsWith(PARAM_FORCE_GENERATION)) {
                            if (setOnceToFalse && onceGeneration) {
                                line = PARAM_FORCE_GENERATION + "=false";
                            }
                            if (setFalseToOnce && !forceGeneration) {
                                line = PARAM_FORCE_GENERATION + "=once";
                            }
                            if (generationIndex == null) {
                                newLines.add(line);
                                generationIndex = newLines.size() - 1;
                            } else {
                                newLines.set(generationIndex, line);
                            }
                        } else if (line.startsWith(PARAM_WIZARD_DONE)) {
                            if (wizardParam != null) {
                                line = PARAM_WIZARD_DONE + "=" + wizardParam;
                            }
                            if (wizardIndex == null) {
                                newLines.add(line);
                                wizardIndex = newLines.size() - 1;
                            } else {
                                newLines.set(wizardIndex, line);
                            }
                        } else if (line.startsWith(PARAM_TEMPLATES_NAME)) {
                            if (templatesParam != null) {
                                line = PARAM_TEMPLATES_NAME + "=" + templatesParam;
                            }
                            if (templatesIndex == null) {
                                newLines.add(line);
                                templatesIndex = newLines.size() - 1;
                            } else {
                                newLines.set(templatesIndex, line);
                            }
                        } else {
                            int equalIdx = line.indexOf("=");
                            if (equalIdx < 1 || line.trim().startsWith("#")) {
                                newLines.add(line);
                            } else {
                                String key = line.substring(0, equalIdx).trim();
                                if (userConfig.getProperty(key) != null) {
                                    newLines.add(line);
                                } else {
                                    newLines.add("#" + line);
                                }
                            }
                        }
                    } else {
                        // What must be written just before the BOUNDARY_BEGIN
                        if (templatesIndex == null && templatesParam != null) {
                            newLines.add(PARAM_TEMPLATES_NAME + "=" + templatesParam);
                            templatesIndex = newLines.size() - 1;
                        }
                        if (wizardIndex == null && wizardParam != null) {
                            newLines.add(PARAM_WIZARD_DONE + "=" + wizardParam);
                            wizardIndex = newLines.size() - 1;
                        }
                        onConfiguratorContent = true;
                    }
                } else {
                    if (!line.startsWith(BOUNDARY_END)) {
                        int equalIdx = line.indexOf("=");
                        if (line.startsWith("#" + PARAM_TEMPLATES_NAME) || line.startsWith(PARAM_TEMPLATES_NAME)) {
                            // Backward compliance, it must be ignored
                            continue;
                        }
                        if (equalIdx < 1) { // Ignore non-readable lines
                            continue;
                        }
                        if (line.trim().startsWith("#")) {
                            String key = line.substring(1, equalIdx).trim();
                            String value = line.substring(equalIdx + 1).trim();
                            getStoredConfig().setProperty(key, value);
                        } else {
                            String key = line.substring(0, equalIdx).trim();
                            String value = line.substring(equalIdx + 1).trim();
                            if (!value.equals(userConfig.getRawProperty(key))) {
                                getStoredConfig().setProperty(key, value);
                            }
                        }
                    } else {
                        onConfiguratorContent = false;
                    }
                }
            }
            reader.close();
            currentConfigurationDigest = Hex.encodeHexString(digest.digest());
        } catch (IOException e) {
            throw new ConfigurationException("Error reading " + nuxeoConf, e);
        }
        StringBuffer newContent = new StringBuffer();
        for (int i = 0; i < newLines.size(); i++) {
            newContent.append(newLines.get(i).trim() + System.getProperty("line.separator"));
        }
        return newContent;
    }

    /**
     * Extract a database template from the current list of templates. Return the last one if there are multiples.
     *
     * @see #rebuildTemplatesStr(String)
     */
    public String extractDatabaseTemplateName() {
        return extractDbTemplateName(DB_LIST, PARAM_TEMPLATE_DBTYPE, PARAM_TEMPLATE_DBNAME, "unknown");
    }

    /**
     * Extract a NoSQL database template from the current list of templates. Return the last one if there are multiples.
     *
     * @see #rebuildTemplatesStr(String)
     * @since 8.1
     */
    public String extractNoSqlDatabaseTemplateName() {
        return extractDbTemplateName(DB_NOSQL_LIST, PARAM_TEMPLATE_DBNOSQL_TYPE, PARAM_TEMPLATE_DBNOSQL_NAME, null);
    }

    private String extractDbTemplateName(List<String> knownDbList, String paramTemplateDbType,
            String paramTemplateDbName, String defaultTemplate) {
        String dbTemplate = defaultTemplate;
        boolean found = false;
        for (File templateFile : includedTemplates) {
            String template = templateFile.getName();
            if (knownDbList.contains(template)) {
                dbTemplate = template;
                found = true;
            }
        }
        String dbType = userConfig.getProperty(paramTemplateDbType);
        if (!found && dbType != null) {
            log.warn(String.format("Didn't find a known database template in the list but "
                    + "some template contributed a value for %s.", paramTemplateDbType));
            dbTemplate = dbType;
        }
        if (dbTemplate != null && !dbTemplate.equals(dbType)) {
            if (dbType == null) {
                log.warn(String.format("Missing value for %s, using %s", paramTemplateDbType, dbTemplate));
                userConfig.setProperty(paramTemplateDbType, dbTemplate);
            } else {
                log.error(String.format("Inconsistent values between %s (%s) and %s (%s)", paramTemplateDbName,
                        dbTemplate, paramTemplateDbType, dbType));
            }
        }
        if (dbTemplate == null) {
            defaultConfig.remove(paramTemplateDbName);
        } else {
            defaultConfig.setProperty(paramTemplateDbName, dbTemplate);
        }
        return dbTemplate;
    }

    /**
     * @return nuxeo.conf file used
     */
    public File getNuxeoConf() {
        return nuxeoConf;
    }

    /**
     * Delegate logs initialization to serverConfigurator instance
     *
     * @since 5.4.2
     */
    public void initLogs() {
        serverConfigurator.initLogs();
    }

    /**
     * @return log directory
     * @since 5.4.2
     */
    public File getLogDir() {
        return serverConfigurator.getLogDir();
    }

    /**
     * @return pid directory
     * @since 5.4.2
     */
    public File getPidDir() {
        return serverConfigurator.getPidDir();
    }

    /**
     * @return Data directory
     * @since 5.4.2
     */
    public File getDataDir() {
        return serverConfigurator.getDataDir();
    }

    /**
     * Create needed directories. Check existence of old paths. If old paths have been found and they cannot be upgraded
     * automatically, then upgrading message is logged and error thrown.
     *
     * @throws ConfigurationException If a deprecated directory has been detected.
     * @since 5.4.2
     * @see ServerConfigurator#verifyInstallation()
     */
    public void verifyInstallation() throws ConfigurationException {
        checkJavaVersion();
        ifNotExistsAndIsDirectoryThenCreate(getLogDir());
        ifNotExistsAndIsDirectoryThenCreate(getPidDir());
        ifNotExistsAndIsDirectoryThenCreate(getDataDir());
        ifNotExistsAndIsDirectoryThenCreate(getTmpDir());
        ifNotExistsAndIsDirectoryThenCreate(getPackagesDir());
        checkAddressesAndPorts();
        serverConfigurator.verifyInstallation();
        if (!DB_EXCLUDE_CHECK_LIST.contains(userConfig.getProperty(PARAM_TEMPLATE_DBTYPE))) {
            try {
                checkDatabaseConnection(userConfig.getProperty(PARAM_TEMPLATE_DBNAME),
                        userConfig.getProperty(PARAM_DB_NAME), userConfig.getProperty(PARAM_DB_USER),
                        userConfig.getProperty(PARAM_DB_PWD), userConfig.getProperty(PARAM_DB_HOST),
                        userConfig.getProperty(PARAM_DB_PORT));
            } catch (IOException e) {
                throw new ConfigurationException(e);
            } catch (DatabaseDriverException e) {
                log.debug(e, e);
                log.error(e.getMessage());
                throw new ConfigurationException("Could not find database driver: " + e.getMessage());
            } catch (SQLException e) {
                log.debug(e, e);
                log.error(e.getMessage());
                throw new ConfigurationException("Failed to connect on database: " + e.getMessage());
            }
        }
        // TODO NXP-18773: check NoSQL database
    }

    /**
     * @return Marketplace packages directory
     * @since 5.9.4
     */
    private File getPackagesDir() {
        return serverConfigurator.getPackagesDir();
    }

    /**
     * Check that the process is executed with a supported Java version. See <a
     * href="http://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html">J2SE SDK/JRE Version String
     * Naming Convention</a>
     *
     * @throws ConfigurationException
     * @since 5.6
     */
    public void checkJavaVersion() throws ConfigurationException {
        String requiredVersion = COMPLIANT_JAVA_VERSIONS[0];
        String version = System.getProperty("java.version");
        if (!checkJavaVersion(version, requiredVersion, true, true)) {
            String message = String.format("Nuxeo requires Java %s+ (detected %s).", requiredVersion, version);
            throw new ConfigurationException(
                    message + " See '" + JVMCHECK_PROP + "' option to bypass version check.");
        }
    }

    /**
     * Checks the java version compared to the required one.
     * <p>
     * Loose compliance is assumed if the major version is greater than the required major version or a jvmcheck=nofail
     * flag is set.
     *
     * @param version the java version
     * @param requiredVersion the required java version
     * @param allowNoFailFlag if {@code true} then check jvmcheck=nofail flag to always have loose compliance
     * @param warnIfLooseCompliance if {@code true} then log a WARN if the is loose compliance
     * @return true if the java version is compliant (maybe loosely) with the required version
     * @since 8.4
     */
    protected static boolean checkJavaVersion(String version, String requiredVersion, boolean allowNoFailFlag,
            boolean warnIfLooseCompliance) {
        allowNoFailFlag = allowNoFailFlag
                && JVMCHECK_NOFAIL.equalsIgnoreCase(System.getProperty(JVMCHECK_PROP, JVMCHECK_FAIL));
        try {
            JVMVersion required = JVMVersion.parse(requiredVersion);
            JVMVersion actual = JVMVersion.parse(version);
            boolean compliant = actual.compareTo(required) >= 0;
            if (compliant && actual.compareTo(required, UpTo.MAJOR) == 0) {
                return true;
            }
            if (!compliant && !allowNoFailFlag) {
                return false;
            }
            log.warn(String.format("Nuxeo requires Java %s+ (detected %s).", requiredVersion, version));
            return true;
        } catch (java.text.ParseException cause) {
            if (allowNoFailFlag) {
                log.warn("Cannot check java version", cause);
                return true;
            }
            throw new IllegalArgumentException("Cannot check java version", cause);
        }
    }

    /**
     * Checks the java version compared to the required one.
     * <p>
     * If major version is same as required major version and minor is greater or equal, it is compliant.
     * <p>
     * If major version is greater than required major version, it is compliant.
     *
     * @param version the java version
     * @param requiredVersion the required java version
     * @return true if the java version is compliant with the required version
     * @since 8.4
     */
    public static boolean checkJavaVersion(String version, String requiredVersion) {
        return checkJavaVersion(version, requiredVersion, false, false);
    }

    /**
     * Will check the configured addresses are reachable and Nuxeo required ports are available on those addresses.
     * Server specific implementations should override this method in order to check for server specific ports.
     * {@link #PARAM_BIND_ADDRESS} must be set before.
     *
     * @throws ConfigurationException
     * @since 5.5
     * @see ServerConfigurator#verifyInstallation()
     */
    public void checkAddressesAndPorts() throws ConfigurationException {
        InetAddress bindAddress = getBindAddress();
        // Sanity check
        if (bindAddress.isMulticastAddress()) {
            throw new ConfigurationException("Multicast address won't work: " + bindAddress);
        }
        checkAddressReachable(bindAddress);
        checkPortAvailable(bindAddress, Integer.parseInt(userConfig.getProperty(PARAM_HTTP_PORT)));
    }

    public InetAddress getBindAddress() throws ConfigurationException {
        InetAddress bindAddress;
        try {
            bindAddress = InetAddress.getByName(userConfig.getProperty(PARAM_BIND_ADDRESS));
            log.debug("Configured bind address: " + bindAddress);
        } catch (UnknownHostException e) {
            throw new ConfigurationException(e);
        }
        return bindAddress;
    }

    /**
     * @param address address to check for availability
     * @throws ConfigurationException
     * @since 5.5
     */
    public static void checkAddressReachable(InetAddress address) throws ConfigurationException {
        try {
            log.debug("Checking availability of " + address);
            address.isReachable(ADDRESS_PING_TIMEOUT);
        } catch (IOException e) {
            throw new ConfigurationException("Unreachable bind address " + address);
        }
    }

    /**
     * Checks if port is available on given address.
     *
     * @param port port to check for availability
     * @throws ConfigurationException Throws an exception if address is unavailable.
     * @since 5.5
     */
    public static void checkPortAvailable(InetAddress address, int port) throws ConfigurationException {
        if ((port == 0) || (port == -1)) {
            log.warn("Port is set to " + Integer.toString(port)
                    + " - assuming it is disabled - skipping availability check");
            return;
        }
        if (port < MIN_PORT || port > MAX_PORT) {
            throw new IllegalArgumentException("Invalid port: " + port);
        }
        ServerSocket socketTCP = null;
        // DatagramSocket socketUDP = null;
        try {
            log.debug("Checking availability of port " + port + " on address " + address);
            socketTCP = new ServerSocket(port, 0, address);
            socketTCP.setReuseAddress(true);
            // socketUDP = new DatagramSocket(port, address);
            // socketUDP.setReuseAddress(true);
            // return true;
        } catch (IOException e) {
            throw new ConfigurationException(e.getMessage() + ": " + address + ":" + port, e);
        } finally {
            // if (socketUDP != null) {
            // socketUDP.close();
            // }
            if (socketTCP != null) {
                try {
                    socketTCP.close();
                } catch (IOException e) {
                    // Do not throw
                }
            }
        }
    }

    /**
     * @return Temporary directory
     */
    public File getTmpDir() {
        return serverConfigurator.getTmpDir();
    }

    private void ifNotExistsAndIsDirectoryThenCreate(File directory) {
        if (!directory.isDirectory()) {
            directory.mkdirs();
        }
    }

    /**
     * @return Log files produced by Log4J configuration without loading this configuration instead of current active
     *         one.
     * @since 5.4.2
     */
    public ArrayList<String> getLogFiles() {
        File log4jConfFile = serverConfigurator.getLogConfFile();
        System.setProperty(org.nuxeo.common.Environment.NUXEO_LOG_DIR, getLogDir().getPath());
        return Log4JHelper.getFileAppendersFiles(log4jConfFile);
    }

    /**
     * Check if wizard must and can be ran
     *
     * @return true if configuration wizard is required before starting Nuxeo
     * @since 5.4.2
     */
    public boolean isWizardRequired() {
        return !"true".equalsIgnoreCase(getUserConfig().getProperty(PARAM_WIZARD_DONE, "true"))
                && serverConfigurator.isWizardAvailable();
    }

    /**
     * Rebuild a templates string for use in nuxeo.conf
     *
     * @param dbTemplate database template to use instead of current one
     * @return new templates string using given dbTemplate
     * @since 5.4.2
     * @see #extractDatabaseTemplateName()
     * @see #changeDBTemplate(String)
     * @see #changeTemplates(String)
     */
    public String rebuildTemplatesStr(String dbTemplate) {
        List<String> templatesList = new ArrayList<>();
        templatesList.addAll(Arrays.asList(templates.split(TEMPLATE_SEPARATOR)));
        String currentDBTemplate = null;
        if (DB_LIST.contains(dbTemplate)) {
            currentDBTemplate = userConfig.getProperty(PARAM_TEMPLATE_DBNAME);
            if (currentDBTemplate == null) {
                currentDBTemplate = extractDatabaseTemplateName();
            }
        } else if (DB_NOSQL_LIST.contains(dbTemplate)) {
            currentDBTemplate = userConfig.getProperty(PARAM_TEMPLATE_DBNOSQL_NAME);
            if (currentDBTemplate == null) {
                currentDBTemplate = extractNoSqlDatabaseTemplateName();
            }
            if ("none".equals(dbTemplate)) {
                dbTemplate = null;
            }
        }
        int dbIdx = templatesList.indexOf(currentDBTemplate);
        if (dbIdx < 0) {
            if (dbTemplate == null) {
                return templates;
            }
            // current db template is implicit => set the new one
            templatesList.add(dbTemplate);
        } else if (dbTemplate == null) {
            // current db template is explicit => remove it
            templatesList.remove(dbIdx);
        } else {
            // current db template is explicit => replace it
            templatesList.set(dbIdx, dbTemplate);
        }
        return StringUtils.join(templatesList, TEMPLATE_SEPARATOR);
    }

    /**
     * @return Nuxeo config directory
     * @since 5.4.2
     */
    public File getConfigDir() {
        return serverConfigurator.getConfigDir();
    }

    /**
     * Ensure the server will start only wizard application, not Nuxeo
     *
     * @since 5.4.2
     */
    public void prepareWizardStart() {
        serverConfigurator.prepareWizardStart();
    }

    /**
     * Ensure the wizard won't be started and nuxeo is ready for use
     *
     * @since 5.4.2
     */
    public void cleanupPostWizard() {
        serverConfigurator.cleanupPostWizard();
    }

    /**
     * @return Nuxeo runtime home
     */
    public File getRuntimeHome() {
        return serverConfigurator.getRuntimeHome();
    }

    /**
     * @since 5.4.2
     * @return true if there's an install in progress
     */
    public boolean isInstallInProgress() {
        return getInstallFile().exists();
    }

    /**
     * @return File pointing to the directory containing the marketplace packages included in the distribution
     * @since 5.6
     */
    public File getDistributionMPDir() {
        String mpDir = userConfig.getProperty(PARAM_MP_DIR, DISTRIBUTION_MP_DIR);
        return new File(getNuxeoHome(), mpDir);
    }

    /**
     * @return Install/upgrade file
     * @since 5.4.1
     */
    public File getInstallFile() {
        return new File(serverConfigurator.getDataDir(), INSTALL_AFTER_RESTART);
    }

    /**
     * Add template(s) to the {@link #PARAM_TEMPLATES_NAME} list if not already present
     *
     * @param templatesToAdd Comma separated templates to add
     * @throws ConfigurationException
     * @since 5.5
     */
    public void addTemplate(String templatesToAdd) throws ConfigurationException {
        String currentTemplatesStr = userConfig.getProperty(PARAM_TEMPLATES_NAME);
        List<String> templatesList = new ArrayList<>();
        templatesList.addAll(Arrays.asList(currentTemplatesStr.split(TEMPLATE_SEPARATOR)));
        List<String> templatesToAddList = Arrays.asList(templatesToAdd.split(TEMPLATE_SEPARATOR));
        if (templatesList.addAll(templatesToAddList)) {
            String newTemplatesStr = StringUtils.join(templatesList, TEMPLATE_SEPARATOR);
            HashMap<String, String> parametersToSave = new HashMap<>();
            parametersToSave.put(PARAM_TEMPLATES_NAME, newTemplatesStr);
            saveFilteredConfiguration(parametersToSave);
            changeTemplates(newTemplatesStr);
        }
    }

    /**
     * Remove template(s) from the {@link #PARAM_TEMPLATES_NAME} list
     *
     * @param templatesToRm Comma separated templates to remove
     * @throws ConfigurationException
     * @since 5.5
     */
    public void rmTemplate(String templatesToRm) throws ConfigurationException {
        String currentTemplatesStr = userConfig.getProperty(PARAM_TEMPLATES_NAME);
        List<String> templatesList = new ArrayList<>();
        templatesList.addAll(Arrays.asList(currentTemplatesStr.split(TEMPLATE_SEPARATOR)));
        List<String> templatesToRmList = Arrays.asList(templatesToRm.split(TEMPLATE_SEPARATOR));
        if (templatesList.removeAll(templatesToRmList)) {
            String newTemplatesStr = StringUtils.join(templatesList, TEMPLATE_SEPARATOR);
            HashMap<String, String> parametersToSave = new HashMap<>();
            parametersToSave.put(PARAM_TEMPLATES_NAME, newTemplatesStr);
            saveFilteredConfiguration(parametersToSave);
            changeTemplates(newTemplatesStr);
        }
    }

    /**
     * Set a property in nuxeo configuration
     *
     * @param key
     * @param value
     * @throws ConfigurationException
     * @return The old value
     * @since 5.5
     */
    public String setProperty(String key, String value) throws ConfigurationException {
        String oldValue = getStoredConfig().getProperty(key);
        if (PARAM_TEMPLATES_NAME.equals(key)) {
            templates = StringUtils.isBlank(value) ? null : value;
        }
        HashMap<String, String> newParametersToSave = new HashMap<>();
        newParametersToSave.put(key, value);
        saveFilteredConfiguration(newParametersToSave);
        setBasicConfiguration();
        return oldValue;
    }

    /**
     * Set properties in nuxeo configuration
     *
     * @param newParametersToSave
     * @return The old values
     * @throws ConfigurationException
     * @since 7.4
     */
    public Map<String, String> setProperties(Map<String, String> newParametersToSave)
            throws ConfigurationException {
        Map<String, String> oldValues = new HashMap<>();
        for (String key : newParametersToSave.keySet()) {
            oldValues.put(key, getStoredConfig().getProperty(key));
            if (PARAM_TEMPLATES_NAME.equals(key)) {
                String value = newParametersToSave.get(key);
                templates = StringUtils.isBlank(value) ? null : value;
            }
        }
        saveFilteredConfiguration(newParametersToSave);
        setBasicConfiguration();
        return oldValues;
    }

    /**
     * Set properties in the given template, if it exists
     *
     * @param newParametersToSave
     * @return The old values
     * @throws ConfigurationException
     * @throws IOException
     * @since 7.4
     */
    public Map<String, String> setProperties(String template, Map<String, String> newParametersToSave)
            throws ConfigurationException, IOException {
        File templateConf = getTemplateConf(template);
        Properties templateProperties = loadTrimmedProperties(templateConf);
        Map<String, String> oldValues = new HashMap<>();
        StringBuffer newContent = new StringBuffer();
        try (BufferedReader reader = new BufferedReader(new FileReader(templateConf))) {
            String line = reader.readLine();
            if (line != null && line.startsWith("## DO NOT EDIT THIS FILE")) {
                throw new ConfigurationException("The template states in its header that it must not be modified.");
            }
            while (line != null) {
                int equalIdx = line.indexOf("=");
                if (equalIdx < 1 || line.trim().startsWith("#")) {
                    newContent.append(line + System.getProperty("line.separator"));
                } else {
                    String key = line.substring(0, equalIdx).trim();
                    if (newParametersToSave.containsKey(key)) {
                        newContent.append(
                                key + "=" + newParametersToSave.get(key) + System.getProperty("line.separator"));
                    } else {
                        newContent.append(line + System.getProperty("line.separator"));
                    }
                }
                line = reader.readLine();
            }
        }
        for (String key : newParametersToSave.keySet()) {
            if (templateProperties.containsKey(key)) {
                oldValues.put(key, templateProperties.getProperty(key));
            } else {
                newContent.append(key + "=" + newParametersToSave.get(key) + System.getProperty("line.separator"));
            }
        }
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(templateConf))) {
            writer.append(newContent.toString());
        }
        setBasicConfiguration();
        return oldValues;
    }

    /**
     * Check driver availability and database connection
     *
     * @param databaseTemplate Nuxeo database template
     * @param dbName nuxeo.db.name parameter in nuxeo.conf
     * @param dbUser nuxeo.db.user parameter in nuxeo.conf
     * @param dbPassword nuxeo.db.password parameter in nuxeo.conf
     * @param dbHost nuxeo.db.host parameter in nuxeo.conf
     * @param dbPort nuxeo.db.port parameter in nuxeo.conf
     * @throws DatabaseDriverException
     * @throws IOException
     * @throws FileNotFoundException
     * @throws SQLException
     * @since 5.6
     */
    public void checkDatabaseConnection(String databaseTemplate, String dbName, String dbUser, String dbPassword,
            String dbHost, String dbPort)
            throws FileNotFoundException, IOException, DatabaseDriverException, SQLException {
        File databaseTemplateDir = new File(nuxeoHome, TEMPLATES + File.separator + databaseTemplate);
        Properties templateProperties = loadTrimmedProperties(new File(databaseTemplateDir, NUXEO_DEFAULT_CONF));
        String classname, connectionUrl;
        if (userConfig.getProperty(PARAM_TEMPLATE_DBNAME).equals(databaseTemplateDir)) {
            // userConfig already includes databaseTemplate
            classname = userConfig.getProperty(PARAM_DB_DRIVER);
            connectionUrl = userConfig.getProperty(PARAM_DB_JDBC_URL);
        } else { // testing a databaseTemplate not included in userConfig
            // check if value is set in nuxeo.conf
            if (userConfig.containsKey(PARAM_DB_DRIVER)) {
                classname = (String) userConfig.get(PARAM_DB_DRIVER);
            } else {
                classname = templateProperties.getProperty(PARAM_DB_DRIVER);
            }
            if (userConfig.containsKey(PARAM_DB_JDBC_URL)) {
                connectionUrl = (String) userConfig.get(PARAM_DB_JDBC_URL);
            } else {
                connectionUrl = templateProperties.getProperty(PARAM_DB_JDBC_URL);
            }
        }
        // Load driver class from template or default lib directory
        Driver driver = lookupDriver(databaseTemplate, databaseTemplateDir, classname);
        // Test db connection
        DriverManager.registerDriver(driver);
        Properties ttProps = new Properties(userConfig);
        ttProps.put(PARAM_DB_HOST, dbHost);
        ttProps.put(PARAM_DB_PORT, dbPort);
        ttProps.put(PARAM_DB_NAME, dbName);
        ttProps.put(PARAM_DB_USER, dbUser);
        ttProps.put(PARAM_DB_PWD, dbPassword);
        TextTemplate tt = new TextTemplate(ttProps);
        String url = tt.processText(connectionUrl);
        Properties conProps = new Properties();
        conProps.put("user", dbUser);
        conProps.put("password", dbPassword);
        log.debug("Testing URL " + url + " with " + conProps);
        Connection con = driver.connect(url, conProps);
        con.close();
    }

    /**
     * Build an {@link URLClassLoader} for the given databaseTemplate looking in the templates directory and in the
     * server lib directory, then looks for a driver
     *
     * @param databaseTemplate
     * @param databaseTemplateDir
     * @param classname Driver class name, defined by {@link #PARAM_DB_DRIVER}
     * @return Driver driver if found, else an Exception must have been raised.
     * @throws IOException
     * @throws FileNotFoundException
     * @throws DatabaseDriverException If there was an error when trying to instantiate the driver.
     * @since 5.6
     */
    private Driver lookupDriver(String databaseTemplate, File databaseTemplateDir, String classname)
            throws FileNotFoundException, IOException, DatabaseDriverException {
        File[] files = (File[]) ArrayUtils.addAll( //
                new File(databaseTemplateDir, "lib").listFiles(), //
                serverConfigurator.getServerLibDir().listFiles());
        List<URL> urlsList = new ArrayList<>();
        if (files != null) {
            for (File file : files) {
                if (file.getName().endsWith("jar")) {
                    try {
                        urlsList.add(new URL("jar:file:" + file.getPath() + "!/"));
                        log.debug("Added " + file.getPath());
                    } catch (MalformedURLException e) {
                        log.error(e);
                    }
                }
            }
        }
        URLClassLoader ucl = new URLClassLoader(urlsList.toArray(new URL[0]));
        try {
            return (Driver) Class.forName(classname, true, ucl).newInstance();
        } catch (InstantiationException e) {
            throw new DatabaseDriverException(e);
        } catch (IllegalAccessException e) {
            throw new DatabaseDriverException(e);
        } catch (ClassNotFoundException e) {
            throw new DatabaseDriverException(e);
        }
    }

    /**
     * @since 5.6
     * @return an {@link Environment} initialized with a few basics
     */
    public Environment getEnv() {
        /*
         * It could be useful to initialize DEFAULT env in {@link #setBasicConfiguration()}... For now, the generated
         * {@link Environment} is not static.
         */
        if (env == null) {
            env = new Environment(getRuntimeHome());
            File distribFile = new File(new File(nuxeoHome, TEMPLATES), "common/config/distribution.properties");
            if (distribFile.exists()) {
                try {
                    env.loadProperties(loadTrimmedProperties(distribFile));
                } catch (FileNotFoundException e) {
                    log.error(e);
                } catch (IOException e) {
                    log.error(e);
                }
            }
            env.loadProperties(userConfig);
            env.setServerHome(getNuxeoHome());
            env.init();
            env.setData(userConfig.getProperty(Environment.NUXEO_DATA_DIR, "data"));
            env.setLog(userConfig.getProperty(Environment.NUXEO_LOG_DIR, "logs"));
            env.setTemp(userConfig.getProperty(Environment.NUXEO_TMP_DIR, "tmp"));
            env.setPath(PARAM_MP_DIR, getDistributionMPDir(), env.getServerHome());
            env.setPath(Environment.NUXEO_MP_DIR, getPackagesDir(), env.getServerHome());
        }
        return env;
    }

    /**
     * @since 5.6
     * @param propsFile Properties file
     * @return new Properties containing trimmed keys and values read in {@code propsFile}
     * @throws IOException
     */
    public static Properties loadTrimmedProperties(File propsFile) throws IOException {
        Properties props = new Properties();
        FileInputStream propsIS = new FileInputStream(propsFile);
        try {
            loadTrimmedProperties(props, propsIS);
        } finally {
            propsIS.close();
        }
        return props;
    }

    /**
     * @since 5.6
     * @param props Properties object to be filled
     * @param propsIS Properties InputStream
     * @throws IOException
     */
    public static void loadTrimmedProperties(Properties props, InputStream propsIS) throws IOException {
        if (props == null) {
            return;
        }
        Properties p = new Properties();
        p.load(propsIS);
        @SuppressWarnings("unchecked")
        Enumeration<String> pEnum = (Enumeration<String>) p.propertyNames();
        while (pEnum.hasMoreElements()) {
            String key = pEnum.nextElement();
            String value = p.getProperty(key);
            props.put(key.trim(), value.trim());
        }
    }

    /**
     * @return The generated properties file with dumped configuration.
     * @since 5.6
     */
    public File getDumpedConfig() {
        return new File(getConfigDir(), CONFIGURATION_PROPERTIES);
    }

    /**
     * Build a {@link Hashtable} which contains environment properties to instantiate a {@link InitialDirContext}
     *
     * @since 6.0
     */
    public Hashtable<Object, Object> getContextEnv(String ldapUrl, String bindDn, String bindPassword,
            boolean checkAuthentication) {
        Hashtable<Object, Object> contextEnv = new Hashtable<>();
        contextEnv.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        contextEnv.put("com.sun.jndi.ldap.connect.timeout", "10000");
        contextEnv.put(javax.naming.Context.PROVIDER_URL, ldapUrl);
        if (checkAuthentication) {
            contextEnv.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple");
            contextEnv.put(javax.naming.Context.SECURITY_PRINCIPAL, bindDn);
            contextEnv.put(javax.naming.Context.SECURITY_CREDENTIALS, bindPassword);
        }
        return contextEnv;
    }

    /**
     * Check if the LDAP parameters are correct to bind to a LDAP server. if authenticate argument is true, it will also
     * check if the authentication against the LDAP server succeeds
     *
     * @param ldapUrl
     * @param ldapBindDn
     * @param ldapBindPwd
     * @param authenticate Indicates if authentication against LDAP should be checked.
     * @since 6.0
     */
    public void checkLdapConnection(String ldapUrl, String ldapBindDn, String ldapBindPwd, boolean authenticate)
            throws NamingException {
        checkLdapConnection(getContextEnv(ldapUrl, ldapBindDn, ldapBindPwd, authenticate));
    }

    /**
     * @param contextEnv Environment properties to build a {@link InitialDirContext}
     * @since 6.0
     */
    public void checkLdapConnection(Hashtable<Object, Object> contextEnv) throws NamingException {
        DirContext dirContext = new InitialDirContext(contextEnv);
        dirContext.close();
    }

    /**
     * @return a {@link Crypto} instance initialized with the configuration parameters
     * @since 7.4
     * @see Crypto
     */
    public Crypto getCrypto() {
        return userConfig.getCrypto();
    }

    /**
     * @param template path to configuration template directory
     * @return A {@code nuxeo.defaults} file if it exists.
     * @throws ConfigurationException if the template file is not found.
     * @since 7.4
     */
    public File getTemplateConf(String template) throws ConfigurationException {
        File templateDir = new File(template);
        if (!templateDir.isAbsolute()) {
            templateDir = new File(System.getProperty("user.dir"), template);
            if (!templateDir.exists() || !new File(templateDir, NUXEO_DEFAULT_CONF).exists()) {
                templateDir = new File(nuxeoDefaultConf.getParentFile(), template);
            }
        }
        if (!templateDir.exists() || !new File(templateDir, NUXEO_DEFAULT_CONF).exists()) {
            throw new ConfigurationException("Template not found: " + template);
        }
        return new File(templateDir, NUXEO_DEFAULT_CONF);
    }

    /**
     * Gets the Java options with 'nuxeo.*' properties substituted. It enables
     * usage of property like ${nuxeo.log.dir} inside JAVA_OPTS.
     *
     * @return the java options string.
     */
    protected String getJavaOpts(String key, String value) {
        return StrSubstitutor.replace(System.getProperty(key, value), getUserConfig());
    }

}