001/*
002 * (C) Copyright 2010-2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Julien Carsique
018 */
019package org.nuxeo.launcher.config;
020
021import java.io.BufferedReader;
022import java.io.BufferedWriter;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FileNotFoundException;
026import java.io.FileReader;
027import java.io.FileWriter;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.StringWriter;
031import java.io.Writer;
032import java.net.Inet6Address;
033import java.net.InetAddress;
034import java.net.MalformedURLException;
035import java.net.ServerSocket;
036import java.net.URL;
037import java.net.URLClassLoader;
038import java.net.UnknownHostException;
039import java.security.MessageDigest;
040import java.sql.Connection;
041import java.sql.Driver;
042import java.sql.DriverManager;
043import java.sql.SQLException;
044import java.util.ArrayList;
045import java.util.Arrays;
046import java.util.Enumeration;
047import java.util.HashMap;
048import java.util.HashSet;
049import java.util.Hashtable;
050import java.util.List;
051import java.util.Map;
052import java.util.Map.Entry;
053import java.util.Properties;
054import java.util.Set;
055import java.util.StringTokenizer;
056import java.util.TreeSet;
057import java.util.UUID;
058import java.util.regex.Matcher;
059import java.util.regex.Pattern;
060import java.util.stream.Collectors;
061import java.util.stream.Stream;
062
063import javax.naming.NamingException;
064import javax.naming.directory.DirContext;
065import javax.naming.directory.InitialDirContext;
066
067import org.apache.commons.codec.binary.Hex;
068import org.apache.commons.codec.digest.DigestUtils;
069import org.apache.commons.lang.ArrayUtils;
070import org.apache.commons.lang.StringUtils;
071import org.apache.commons.lang.SystemUtils;
072import org.apache.commons.lang.text.StrSubstitutor;
073import org.apache.commons.logging.Log;
074import org.apache.commons.logging.LogFactory;
075import org.apache.log4j.Logger;
076import org.apache.log4j.helpers.NullEnumeration;
077import org.nuxeo.common.Environment;
078import org.nuxeo.common.codec.Crypto;
079import org.nuxeo.common.codec.CryptoProperties;
080import org.nuxeo.common.utils.TextTemplate;
081import org.nuxeo.launcher.commons.DatabaseDriverException;
082import org.nuxeo.launcher.config.JVMVersion.UpTo;
083import org.nuxeo.log4j.Log4JHelper;
084
085import freemarker.core.ParseException;
086import freemarker.template.TemplateException;
087
088/**
089 * Builder for server configuration and datasource files from templates and properties.
090 *
091 * @author jcarsique
092 */
093public class ConfigurationGenerator {
094
095    /**
096     * @since 6.0
097     */
098    public static final String TEMPLATE_SEPARATOR = ",";
099
100    /**
101     * Accurate but not used internally. NXP-18023: Java 8 update 40+ required
102     *
103     * @since 5.7
104     */
105    public static final String[] COMPLIANT_JAVA_VERSIONS = new String[] { "1.8.0_40" };
106
107    /**
108     * @since 5.6
109     */
110    protected static final String CONFIGURATION_PROPERTIES = "configuration.properties";
111
112    private static final Log log = LogFactory.getLog(ConfigurationGenerator.class);
113
114    public static final String NUXEO_CONF = "nuxeo.conf";
115
116    public static final String TEMPLATES = "templates";
117
118    public static final String NUXEO_DEFAULT_CONF = "nuxeo.defaults";
119
120    /**
121     * Absolute or relative PATH to the user chosen templates (comma separated list)
122     */
123    public static final String PARAM_TEMPLATES_NAME = "nuxeo.templates";
124
125    public static final String PARAM_TEMPLATE_DBNAME = "nuxeo.dbtemplate";
126
127    /**
128     * @since 8.1
129     */
130    public static final String PARAM_TEMPLATE_DBNOSQL_NAME = "nuxeo.dbnosqltemplate";
131
132    public static final String PARAM_TEMPLATE_DBTYPE = "nuxeo.db.type";
133
134    /**
135     * @since 8.1
136     */
137    public static final String PARAM_TEMPLATE_DBNOSQL_TYPE = "nuxeo.dbnosql.type";
138
139    public static final String OLD_PARAM_TEMPLATES_PARSING_EXTENSIONS = "nuxeo.templates.parsing.extensions";
140
141    public static final String PARAM_TEMPLATES_PARSING_EXTENSIONS = "nuxeo.plaintext_parsing_extensions";
142
143    public static final String PARAM_TEMPLATES_FREEMARKER_EXTENSIONS = "nuxeo.freemarker_parsing_extensions";
144
145    /**
146     * Absolute or relative PATH to the included templates (comma separated list)
147     */
148    protected static final String PARAM_INCLUDED_TEMPLATES = "nuxeo.template.includes";
149
150    public static final String PARAM_FORCE_GENERATION = "nuxeo.force.generation";
151
152    public static final String BOUNDARY_BEGIN = "### BEGIN - DO NOT EDIT BETWEEN BEGIN AND END ###";
153
154    public static final String BOUNDARY_END = "### END - DO NOT EDIT BETWEEN BEGIN AND END ###";
155
156    public static final List<String> DB_LIST = Arrays.asList("default", "postgresql", "oracle", "mysql", "mariadb",
157            "mssql", "db2");
158
159    public static final List<String> DB_NOSQL_LIST = Arrays.asList("none", "mongodb", "marklogic");
160
161    public static final List<String> DB_EXCLUDE_CHECK_LIST = Arrays.asList("default", "none");
162
163    public static final String PARAM_WIZARD_DONE = "nuxeo.wizard.done";
164
165    public static final String PARAM_WIZARD_RESTART_PARAMS = "wizard.restart.params";
166
167    public static final String PARAM_FAKE_WINDOWS = "org.nuxeo.fake.vindoz";
168
169    public static final String PARAM_LOOPBACK_URL = "nuxeo.loopback.url";
170
171    public static final int MIN_PORT = 1;
172
173    public static final int MAX_PORT = 65535;
174
175    public static final int ADDRESS_PING_TIMEOUT = 1000;
176
177    public static final String PARAM_BIND_ADDRESS = "nuxeo.bind.address";
178
179    public static final String PARAM_HTTP_PORT = "nuxeo.server.http.port";
180
181    /**
182     * @deprecated Since 7.4. Use {@link Environment#SERVER_STATUS_KEY} instead
183     */
184    @Deprecated
185    public static final String PARAM_STATUS_KEY = Environment.SERVER_STATUS_KEY;
186
187    public static final String PARAM_CONTEXT_PATH = "org.nuxeo.ecm.contextPath";
188
189    public static final String PARAM_MP_DIR = "nuxeo.distribution.marketplace.dir";
190
191    public static final String DISTRIBUTION_MP_DIR = "setupWizardDownloads";
192
193    public static final String INSTALL_AFTER_RESTART = "installAfterRestart.log";
194
195    public static final String PARAM_DB_DRIVER = "nuxeo.db.driver";
196
197    public static final String PARAM_DB_JDBC_URL = "nuxeo.db.jdbc.url";
198
199    public static final String PARAM_DB_HOST = "nuxeo.db.host";
200
201    public static final String PARAM_DB_PORT = "nuxeo.db.port";
202
203    public static final String PARAM_DB_NAME = "nuxeo.db.name";
204
205    public static final String PARAM_DB_USER = "nuxeo.db.user";
206
207    public static final String PARAM_DB_PWD = "nuxeo.db.password";
208
209    /**
210     * @since 8.1
211     */
212    public static final String PARAM_MONGODB_NAME = "nuxeo.mongodb.dbname";
213
214    /**
215     * @since 8.1
216     */
217    public static final String PARAM_MONGODB_SERVER = "nuxeo.mongodb.server";
218
219    /**
220     * Catch values like ${env:PARAM_KEY:defaultValue}
221     *
222     * @since 9.1
223     */
224    private static final Pattern ENV_VALUE_PATTERN = Pattern.compile(
225            "\\$\\{env(?<boolean>\\?\\?)?:(?<envparam>\\w*)(:?(?<defaultvalue>.*?)?)?\\}");
226
227    /**
228     * Keys which value must be displayed thoughtfully
229     *
230     * @since 8.1
231     */
232    public static final List<String> SECRET_KEYS = Arrays.asList(PARAM_DB_PWD, "mailservice.password",
233            "mail.transport.password", "nuxeo.http.proxy.password", "nuxeo.ldap.bindpassword",
234            "nuxeo.user.emergency.password");
235
236    /**
237     * @deprecated Since 7.10. Use {@link Environment#PRODUCT_NAME}
238     */
239    @Deprecated
240    public static final String PARAM_PRODUCT_NAME = Environment.PRODUCT_NAME;
241
242    /**
243     * @deprecated Since 7.10. Use {@link Environment#PRODUCT_VERSION}
244     */
245    @Deprecated
246    public static final String PARAM_PRODUCT_VERSION = Environment.PRODUCT_VERSION;
247
248    /**
249     * @since 5.6
250     */
251    public static final String PARAM_NUXEO_URL = "nuxeo.url";
252
253    /**
254     * Global dev property, duplicated from runtime framework
255     *
256     * @since 5.6
257     */
258    public static final String NUXEO_DEV_SYSTEM_PROP = "org.nuxeo.dev";
259
260    /**
261     * Seam hot reload property, also controlled by {@link #NUXEO_DEV_SYSTEM_PROP}
262     *
263     * @since 5.6
264     */
265    public static final String SEAM_DEBUG_SYSTEM_PROP = "org.nuxeo.seam.debug";
266
267    /** @since 8.4 */
268    public static final String JVMCHECK_PROP = "jvmcheck";
269
270    /** @since 8.4 */
271    public static final String JVMCHECK_FAIL = "fail";
272
273    /** @since 8.4 */
274    public static final String JVMCHECK_NOFAIL = "nofail";
275
276    private final File nuxeoHome;
277
278    // User configuration file
279    private final File nuxeoConf;
280
281    // Chosen templates
282    private final List<File> includedTemplates = new ArrayList<>();
283
284    // Common default configuration file
285    private File nuxeoDefaultConf;
286
287    public boolean isJBoss;
288
289    public boolean isJetty;
290
291    public boolean isTomcat;
292
293    private ServerConfigurator serverConfigurator;
294
295    private BackingServiceConfigurator backingServicesConfigurator;
296
297    private boolean forceGeneration;
298
299    private Properties defaultConfig;
300
301    private CryptoProperties userConfig;
302
303    private boolean configurable = false;
304
305    private boolean onceGeneration = false;
306
307    private String templates;
308
309    // if PARAM_FORCE_GENERATION=once, set to false; else keep current value
310    private boolean setOnceToFalse = true;
311
312    // if PARAM_FORCE_GENERATION=false, set to once; else keep the current
313    // value
314    private boolean setFalseToOnce = false;
315
316    public boolean isConfigurable() {
317        return configurable;
318    }
319
320    public ConfigurationGenerator() {
321        this(true, false);
322    }
323
324    private boolean quiet = false;
325
326    private static boolean hideDeprecationWarnings = false;
327
328    private Environment env;
329
330    private Properties storedConfig;
331
332    private String currentConfigurationDigest;
333
334    /**
335     * @since 5.7
336     */
337    protected Properties getStoredConfig() {
338        if (storedConfig == null) {
339            updateStoredConfig();
340        }
341        return storedConfig;
342    }
343
344    protected static final Map<String, String> parametersMigration = new HashMap<String, String>() {
345        private static final long serialVersionUID = 1L;
346
347        {
348            put(OLD_PARAM_TEMPLATES_PARSING_EXTENSIONS, PARAM_TEMPLATES_PARSING_EXTENSIONS);
349            put("nuxeo.db.user.separator.key", "nuxeo.db.user_separator_key");
350            put("mail.pop3.host", "mail.store.host");
351            put("mail.pop3.port", "mail.store.port");
352            put("mail.smtp.host", "mail.transport.host");
353            put("mail.smtp.port", "mail.transport.port");
354            put("mail.smtp.username", "mail.transport.username");
355            put("mail.transport.username", "mail.transport.user");
356            put("mail.smtp.password", "mail.transport.password");
357            put("mail.smtp.usetls", "mail.transport.usetls");
358            put("mail.smtp.auth", "mail.transport.auth");
359        }
360    };
361
362    /**
363     * @param quiet Suppress info level messages from the console output
364     * @param debug Activate debug level logging
365     * @since 5.6
366     */
367    public ConfigurationGenerator(boolean quiet, boolean debug) {
368        this.quiet = quiet;
369        File serverHome = Environment.getDefault().getServerHome();
370        if (serverHome != null) {
371            nuxeoHome = serverHome.getAbsoluteFile();
372        } else {
373            File userDir = new File(System.getProperty("user.dir"));
374            if ("bin".equalsIgnoreCase(userDir.getName())) {
375                nuxeoHome = userDir.getParentFile().getAbsoluteFile();
376            } else {
377                nuxeoHome = userDir.getAbsoluteFile();
378            }
379        }
380        String nuxeoConfPath = System.getProperty(NUXEO_CONF);
381        if (nuxeoConfPath != null) {
382            nuxeoConf = new File(nuxeoConfPath).getAbsoluteFile();
383        } else {
384            nuxeoConf = new File(nuxeoHome, "bin" + File.separator + "nuxeo.conf").getAbsoluteFile();
385        }
386        System.setProperty(NUXEO_CONF, nuxeoConf.getPath());
387
388        nuxeoDefaultConf = new File(nuxeoHome, TEMPLATES + File.separator + NUXEO_DEFAULT_CONF);
389
390        // detect server type based on System properties
391        isJetty = System.getProperty(JettyConfigurator.JETTY_HOME) != null;
392        isTomcat = System.getProperty(TomcatConfigurator.TOMCAT_HOME) != null;
393        if (!isJBoss && !isJetty && !isTomcat) {
394            // fallback on jar detection
395            isJBoss = new File(nuxeoHome, "bin/run.jar").exists();
396            isTomcat = new File(nuxeoHome, "bin/bootstrap.jar").exists();
397            String[] files = nuxeoHome.list();
398            for (String file : files) {
399                if (file.startsWith("nuxeo-runtime-launcher")) {
400                    isJetty = true;
401                    break;
402                }
403            }
404        }
405        if (isTomcat) {
406            serverConfigurator = new TomcatConfigurator(this);
407        } else if (isJetty) {
408            serverConfigurator = new JettyConfigurator(this);
409        } else {
410            serverConfigurator = new UnknownServerConfigurator(this);
411        }
412        if (Logger.getRootLogger().getAllAppenders() instanceof NullEnumeration) {
413            serverConfigurator.initLogs();
414        }
415        backingServicesConfigurator = new BackingServiceConfigurator(this);
416        String homeInfo = "Nuxeo home:          " + nuxeoHome.getPath();
417        String confInfo = "Nuxeo configuration: " + nuxeoConf.getPath();
418        if (quiet) {
419            log.debug(homeInfo);
420            log.debug(confInfo);
421        } else {
422            log.info(homeInfo);
423            log.info(confInfo);
424        }
425    }
426
427    public void hideDeprecationWarnings(boolean hide) {
428        hideDeprecationWarnings = hide;
429    }
430
431    /**
432     * @see #PARAM_FORCE_GENERATION
433     */
434    public void setForceGeneration(boolean forceGeneration) {
435        this.forceGeneration = forceGeneration;
436    }
437
438    /**
439     * @see #PARAM_FORCE_GENERATION
440     * @return true if configuration will be generated from templates
441     * @since 5.4.2
442     */
443    public boolean isForceGeneration() {
444        return forceGeneration;
445    }
446
447    public CryptoProperties getUserConfig() {
448        return userConfig;
449    }
450
451    /**
452     * @since 5.4.2
453     */
454    public final ServerConfigurator getServerConfigurator() {
455        return serverConfigurator;
456    }
457
458    /**
459     * Runs the configuration files generation.
460     */
461    public void run() throws ConfigurationException {
462        if (init()) {
463            if (!serverConfigurator.isConfigured()) {
464                log.info("No current configuration, generating files...");
465                generateFiles();
466            } else if (forceGeneration) {
467                log.info("Configuration files generation (nuxeo.force.generation="
468                        + userConfig.getProperty(PARAM_FORCE_GENERATION) + ")...");
469                generateFiles();
470            } else {
471                log.info(
472                        "Server already configured (set nuxeo.force.generation=true to force configuration files generation).");
473            }
474        }
475    }
476
477    /**
478     * Initialize configurator, check requirements and load current configuration
479     *
480     * @return returns true if current install is configurable, else returns false
481     */
482    public boolean init() {
483        return init(false);
484    }
485
486    /**
487     * Initialize configurator, check requirements and load current configuration
488     *
489     * @since 5.6
490     * @param forceReload If true, forces configuration reload.
491     * @return returns true if current install is configurable, else returns false
492     */
493    public boolean init(boolean forceReload) {
494        if (serverConfigurator instanceof UnknownServerConfigurator) {
495            configurable = false;
496            forceGeneration = false;
497            log.warn("Server will be considered as not configurable.");
498        }
499        if (!nuxeoConf.exists()) {
500            log.info("Missing " + nuxeoConf);
501            configurable = false;
502            userConfig = new CryptoProperties();
503            defaultConfig = new Properties();
504        } else if (userConfig == null || userConfig.size() == 0 || forceReload) {
505            try {
506                if (forceReload) {
507                    // force 'templates' reload
508                    templates = null;
509                }
510                setBasicConfiguration();
511                configurable = true;
512            } catch (ConfigurationException e) {
513                log.warn("Error reading basic configuration.", e);
514                configurable = false;
515            }
516        } else {
517            configurable = true;
518        }
519        return configurable;
520    }
521
522    /**
523     * @return Old templates
524     */
525    public String changeTemplates(String newTemplates) {
526        String oldTemplates = templates;
527        templates = newTemplates;
528        try {
529            setBasicConfiguration(false);
530            configurable = true;
531        } catch (ConfigurationException e) {
532            log.warn("Error reading basic configuration.", e);
533            configurable = false;
534        }
535        return oldTemplates;
536    }
537
538    /**
539     * Change templates using given database template
540     *
541     * @param dbTemplate new database template
542     * @since 5.4.2
543     */
544    public void changeDBTemplate(String dbTemplate) {
545        changeTemplates(rebuildTemplatesStr(dbTemplate));
546    }
547
548    private void setBasicConfiguration() throws ConfigurationException {
549        setBasicConfiguration(true);
550    }
551
552    private void setBasicConfiguration(boolean save) throws ConfigurationException {
553        try {
554            // Load default configuration
555            defaultConfig = loadTrimmedProperties(nuxeoDefaultConf);
556            // Add System properties
557            defaultConfig.putAll(System.getProperties());
558            userConfig = new CryptoProperties(defaultConfig);
559
560            // If Windows, replace backslashes in paths in nuxeo.conf
561            if (SystemUtils.IS_OS_WINDOWS) {
562                replaceBackslashes();
563            }
564            // Load user configuration
565            userConfig.putAll(loadTrimmedProperties(nuxeoConf));
566            onceGeneration = "once".equals(userConfig.getProperty(PARAM_FORCE_GENERATION));
567            forceGeneration = onceGeneration
568                    || Boolean.parseBoolean(userConfig.getProperty(PARAM_FORCE_GENERATION, "false"));
569            checkForDeprecatedParameters(userConfig);
570
571            // Synchronize directories between serverConfigurator and
572            // userConfig/defaultConfig
573            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_DATA_DIR);
574            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_LOG_DIR);
575            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_PID_DIR);
576            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_TMP_DIR);
577            setDirectoryWithProperty(org.nuxeo.common.Environment.NUXEO_MP_DIR);
578        } catch (NullPointerException e) {
579            throw new ConfigurationException("Missing file", e);
580        } catch (FileNotFoundException e) {
581            throw new ConfigurationException("Missing file: " + nuxeoDefaultConf + " or " + nuxeoConf, e);
582        } catch (IOException e) {
583            throw new ConfigurationException("Error reading " + nuxeoConf, e);
584        }
585
586        // Override default configuration with specific configuration(s) of
587        // the chosen template(s) which can be outside of server filesystem
588        try {
589            includeTemplates();
590            checkForDeprecatedParameters(defaultConfig);
591            extractDatabaseTemplateName();
592            extractNoSqlDatabaseTemplateName();
593        } catch (FileNotFoundException e) {
594            throw new ConfigurationException("Missing file", e);
595        } catch (IOException e) {
596            throw new ConfigurationException("Error reading " + nuxeoConf, e);
597        }
598
599        Map<String, String> newParametersToSave = evalDynamicProperties();
600        if (save && newParametersToSave != null && !newParametersToSave.isEmpty()) {
601            saveConfiguration(newParametersToSave, false, false);
602        }
603
604        logDebugInformation();
605
606        // Could be useful to initialize DEFAULT env...
607        // initEnv();
608    }
609
610    /**
611     * @since 5.7
612     * @throws IOException
613     */
614    protected void includeTemplates() throws IOException {
615        includedTemplates.clear();
616        List<File> orderedTemplates = includeTemplates(getUserTemplates());
617        includedTemplates.clear();
618        includedTemplates.addAll(orderedTemplates);
619        log.debug(includedTemplates);
620    }
621
622    private void logDebugInformation() {
623        String debugPropValue = userConfig.getProperty(NUXEO_DEV_SYSTEM_PROP);
624        if (Boolean.parseBoolean(debugPropValue)) {
625            log.debug("Nuxeo Dev mode enabled");
626        } else {
627            log.debug("Nuxeo Dev mode is not enabled");
628        }
629
630        // XXX: cannot init seam debug mode when global debug mode is set, as
631        // it needs to be activated at startup, and requires the seam-debug jar
632        // to be in the classpath anyway
633        String seamDebugPropValue = userConfig.getProperty(SEAM_DEBUG_SYSTEM_PROP);
634        if (Boolean.parseBoolean(seamDebugPropValue)) {
635            log.debug("Nuxeo Seam HotReload is enabled");
636        } else {
637            log.debug("Nuxeo Seam HotReload is not enabled");
638        }
639    }
640
641    /**
642     * Generate properties which values are based on others
643     *
644     * @return Map with new parameters to save in {@code nuxeoConf}
645     * @throws ConfigurationException
646     * @since 5.5
647     */
648    protected HashMap<String, String> evalDynamicProperties() throws ConfigurationException {
649        HashMap<String, String> newParametersToSave = new HashMap<>();
650        evalEnvironmentVariables(newParametersToSave);
651        evalLoopbackURL();
652        evalServerStatusKey(newParametersToSave);
653        return newParametersToSave;
654    }
655
656    /**
657     * Expand environment variable for properties values of the form ${env:MY_VAR}.
658     *
659     * @since 9.1
660     */
661    protected void evalEnvironmentVariables(Map<String, String> newParametersToSave) {
662        for (Object keyObject : userConfig.keySet()) {
663            String key = (String) keyObject;
664            String value = userConfig.getProperty(key);
665
666            if (StringUtils.isNotBlank(value)) {
667                String newValue = replaceEnvironmentVariables(value);
668                if (!value.equals(newValue)) {
669                    newParametersToSave.put(key, newValue);
670                }
671            }
672        }
673    }
674
675    private String replaceEnvironmentVariables(String value) {
676        if(StringUtils.isBlank(value)) {
677            return value;
678        }
679
680        Matcher matcher = ENV_VALUE_PATTERN.matcher(value);
681        StringBuffer sb = new StringBuffer();
682        while (matcher.find()) {
683            boolean booleanValue = "??".equals(matcher.group("boolean"));
684            String envVarName = matcher.group("envparam");
685            String defaultValue = matcher.group("defaultvalue");
686
687            String envValue = getEnvironmentVariableValue(envVarName);
688
689            String result;
690            if (booleanValue) {
691                result = StringUtils.isBlank(envValue) ? "false" : "true";
692            } else {
693                result = StringUtils.isBlank(envValue) ? defaultValue : envValue;
694            }
695            matcher.appendReplacement(sb, result);
696        }
697        matcher.appendTail(sb);
698
699        return sb.toString();
700
701    }
702
703    /**
704     * Generate a server status key if not already set
705     *
706     * @throws ConfigurationException
707     * @see Environment#SERVER_STATUS_KEY
708     * @since 5.5
709     */
710    private void evalServerStatusKey(Map<String, String> newParametersToSave) throws ConfigurationException {
711        if (userConfig.getProperty(Environment.SERVER_STATUS_KEY) == null) {
712            newParametersToSave.put(Environment.SERVER_STATUS_KEY, UUID.randomUUID().toString().substring(0, 8));
713        }
714    }
715
716    private void evalLoopbackURL() throws ConfigurationException {
717        String loopbackURL = userConfig.getProperty(PARAM_LOOPBACK_URL);
718        if (loopbackURL != null) {
719            log.debug("Using configured loop back url: " + loopbackURL);
720            return;
721        }
722        InetAddress bindAddress = getBindAddress();
723        // Address and ports already checked by #checkAddressesAndPorts
724        try {
725            if (bindAddress.isAnyLocalAddress()) {
726                boolean preferIPv6 = "false".equals(System.getProperty("java.net.preferIPv4Stack"))
727                        && "true".equals(System.getProperty("java.net.preferIPv6Addresses"));
728                bindAddress = preferIPv6 ? InetAddress.getByName("::1") : InetAddress.getByName("127.0.0.1");
729                log.debug("Bind address is \"ANY\", using local address instead: " + bindAddress);
730            }
731        } catch (UnknownHostException e) {
732            log.debug(e, e);
733            log.error(e.getMessage());
734        }
735
736        String httpPort = userConfig.getProperty(PARAM_HTTP_PORT);
737        String contextPath = userConfig.getProperty(PARAM_CONTEXT_PATH);
738        // Is IPv6 or IPv4 ?
739        if (bindAddress instanceof Inet6Address) {
740            loopbackURL = "http://[" + bindAddress.getHostAddress() + "]:" + httpPort + contextPath;
741        } else {
742            loopbackURL = "http://" + bindAddress.getHostAddress() + ":" + httpPort + contextPath;
743        }
744        log.debug("Set as loop back URL: " + loopbackURL);
745        defaultConfig.setProperty(PARAM_LOOPBACK_URL, loopbackURL);
746    }
747
748    /**
749     * Read nuxeo.conf, replace backslashes in paths and write new nuxeo.conf
750     *
751     * @throws ConfigurationException if any error reading or writing nuxeo.conf
752     * @since 5.4.1
753     */
754    protected void replaceBackslashes() throws ConfigurationException {
755        StringBuilder sb = new StringBuilder();
756        try (BufferedReader reader = new BufferedReader(new FileReader(nuxeoConf))) {
757            String line;
758            while ((line = reader.readLine()) != null) {
759                if (line.matches(".*:\\\\.*")) {
760                    line = line.replaceAll("\\\\", "/");
761                }
762                sb.append(line).append(System.getProperty("line.separator"));
763            }
764        } catch (IOException e) {
765            throw new ConfigurationException("Error reading " + nuxeoConf, e);
766        }
767        try (FileWriter writer = new FileWriter(nuxeoConf, false)) {
768            // Copy back file content
769            writer.append(sb.toString());
770        } catch (IOException e) {
771            throw new ConfigurationException("Error writing in " + nuxeoConf, e);
772        }
773    }
774
775    /**
776     * @since 5.4.2
777     * @param key Directory system key
778     * @see Environment
779     */
780    public void setDirectoryWithProperty(String key) {
781        String directory = userConfig.getProperty(key);
782        if (directory == null) {
783            defaultConfig.setProperty(key, serverConfigurator.getDirectory(key).getPath());
784        } else {
785            serverConfigurator.setDirectory(key, directory);
786        }
787    }
788
789    public String getUserTemplates() {
790        if (templates == null) {
791            templates = userConfig.getProperty(PARAM_TEMPLATES_NAME);
792        }
793        if (templates == null) {
794            log.warn("No template found in configuration! Fallback on 'default'.");
795            templates = "default";
796        }
797        templates = replaceEnvironmentVariables(templates);
798        userConfig.setProperty(PARAM_TEMPLATES_NAME, templates);
799        return templates;
800    }
801
802    protected void generateFiles() throws ConfigurationException {
803        try {
804            serverConfigurator.parseAndCopy(userConfig);
805            serverConfigurator.dumpProperties(userConfig);
806            log.info("Configuration files generated.");
807            // keep true or false, switch once to false
808            if (onceGeneration) {
809                setOnceToFalse = true;
810                writeConfiguration();
811            }
812        } catch (FileNotFoundException e) {
813            throw new ConfigurationException("Missing file: " + e.getMessage(), e);
814        } catch (TemplateException | ParseException e) {
815            throw new ConfigurationException("Could not process FreeMarker template: " + e.getMessage(), e);
816        } catch (IOException e) {
817            throw new ConfigurationException("Configuration failure: " + e.getMessage(), e);
818        }
819    }
820
821    private List<File> includeTemplates(String templatesList) throws IOException {
822        List<File> orderedTemplates = new ArrayList<>();
823        StringTokenizer st = new StringTokenizer(templatesList, TEMPLATE_SEPARATOR);
824        while (st.hasMoreTokens()) {
825            String nextToken = replaceEnvironmentVariables(st.nextToken());
826            File chosenTemplate = new File(nextToken);
827            // is it absolute and existing or relative path ?
828            if (!chosenTemplate.exists() || !chosenTemplate.getPath().equals(chosenTemplate.getAbsolutePath())) {
829                chosenTemplate = new File(nuxeoDefaultConf.getParentFile(), nextToken);
830            }
831            if (includedTemplates.contains(chosenTemplate)) {
832                log.debug("Already included " + nextToken);
833                continue;
834            }
835            if (!chosenTemplate.exists()) {
836                log.error(String.format(
837                        "Template '%s' not found with relative or absolute path (%s). "
838                                + "Check your %s parameter, and %s for included files.",
839                        nextToken, chosenTemplate, PARAM_TEMPLATES_NAME, PARAM_INCLUDED_TEMPLATES));
840                continue;
841            }
842            File chosenTemplateConf = new File(chosenTemplate, NUXEO_DEFAULT_CONF);
843            includedTemplates.add(chosenTemplate);
844            if (!chosenTemplateConf.exists()) {
845                log.warn("Ignore template (no default configuration): " + nextToken);
846                continue;
847            }
848
849            Properties subTemplateConf = loadTrimmedProperties(chosenTemplateConf);
850            String subTemplatesList = replaceEnvironmentVariables(
851                    subTemplateConf.getProperty(PARAM_INCLUDED_TEMPLATES));
852            if (subTemplatesList != null && subTemplatesList.length() > 0) {
853                orderedTemplates.addAll(includeTemplates(subTemplatesList));
854            }
855            // Load configuration from chosen templates
856            defaultConfig.putAll(subTemplateConf);
857            orderedTemplates.add(chosenTemplate);
858            String templateInfo = "Include template: " + chosenTemplate.getPath();
859            if (quiet) {
860                log.debug(templateInfo);
861            } else {
862                log.info(templateInfo);
863            }
864        }
865        return orderedTemplates;
866    }
867
868    /**
869     * Check for deprecated parameters
870     *
871     * @since 5.6
872     */
873    protected void checkForDeprecatedParameters(Properties properties) {
874        serverConfigurator.addServerSpecificParameters(parametersMigration);
875        @SuppressWarnings("rawtypes")
876        Enumeration userEnum = properties.propertyNames();
877        while (userEnum.hasMoreElements()) {
878            String key = (String) userEnum.nextElement();
879            if (parametersMigration.containsKey(key)) {
880                String value = properties.getProperty(key);
881                properties.setProperty(parametersMigration.get(key), value);
882                // Don't remove the deprecated key yet - more
883                // warnings but old things should keep working
884                // properties.remove(key);
885                if (!hideDeprecationWarnings) {
886                    log.warn("Parameter " + key + " is deprecated - please use " + parametersMigration.get(key)
887                            + " instead");
888                }
889            }
890        }
891    }
892
893    public File getNuxeoHome() {
894        return nuxeoHome;
895    }
896
897    public File getNuxeoDefaultConf() {
898        return nuxeoDefaultConf;
899    }
900
901    public List<File> getIncludedTemplates() {
902        return includedTemplates;
903    }
904
905    /**
906     * Save changed parameters in {@code nuxeo.conf}. This method does not check values in map. Use
907     * {@link #saveFilteredConfiguration(Map)} for parameters filtering.
908     *
909     * @param changedParameters Map of modified parameters
910     * @see #saveFilteredConfiguration(Map)
911     */
912    public void saveConfiguration(Map<String, String> changedParameters) throws ConfigurationException {
913        // Keep generation true or once; switch false to once
914        saveConfiguration(changedParameters, false, true);
915    }
916
917    /**
918     * Save changed parameters in {@code nuxeo.conf} calculating templates if changedParameters contains a value for
919     * {@link #PARAM_TEMPLATE_DBNAME}. If a parameter value is empty ("" or null), then the property is unset.
920     * {@link #PARAM_WIZARD_DONE}, {@link #PARAM_TEMPLATES_NAME} and {@link #PARAM_FORCE_GENERATION} cannot be unset,
921     * but their value can be changed.<br/>
922     * This method does not check values in map: use {@link #saveFilteredConfiguration(Map)} for parameters filtering.
923     *
924     * @param changedParameters Map of modified parameters
925     * @param setGenerationOnceToFalse If generation was on (true or once), then set it to false or not?
926     * @param setGenerationFalseToOnce If generation was off (false), then set it to once?
927     * @see #saveFilteredConfiguration(Map)
928     * @since 5.5
929     */
930    public void saveConfiguration(Map<String, String> changedParameters, boolean setGenerationOnceToFalse,
931            boolean setGenerationFalseToOnce) throws ConfigurationException {
932        setOnceToFalse = setGenerationOnceToFalse;
933        setFalseToOnce = setGenerationFalseToOnce;
934        updateStoredConfig();
935        String newDbTemplate = changedParameters.remove(PARAM_TEMPLATE_DBNAME);
936        if (newDbTemplate != null) {
937            changedParameters.put(PARAM_TEMPLATES_NAME, rebuildTemplatesStr(newDbTemplate));
938        }
939        newDbTemplate = changedParameters.remove(PARAM_TEMPLATE_DBNOSQL_NAME);
940        if (newDbTemplate != null) {
941            changedParameters.put(PARAM_TEMPLATES_NAME, rebuildTemplatesStr(newDbTemplate));
942        }
943        if (changedParameters.containsValue(null) || changedParameters.containsValue("")) {
944            // There are properties to unset
945            Set<String> propertiesToUnset = new HashSet<>();
946            for (Entry<String, String> entry : changedParameters.entrySet()) {
947                if (StringUtils.isEmpty(entry.getValue())) {
948                    propertiesToUnset.add(entry.getKey());
949                }
950            }
951            for (String key : propertiesToUnset) {
952                changedParameters.remove(key);
953                userConfig.remove(key);
954            }
955        }
956        userConfig.putAll(changedParameters);
957        writeConfiguration();
958        updateStoredConfig();
959    }
960
961    private void updateStoredConfig() {
962        if (storedConfig == null) {
963            storedConfig = new Properties(defaultConfig);
964        } else {
965            storedConfig.clear();
966        }
967        storedConfig.putAll(userConfig);
968    }
969
970    /**
971     * Save changed parameters in {@code nuxeo.conf}, filtering parameters with {@link #getChangedParameters(Map)}
972     *
973     * @param changedParameters Maps of modified parameters
974     * @since 5.4.2
975     * @see #saveConfiguration(Map)
976     * @see #getChangedParameters(Map)
977     */
978    public void saveFilteredConfiguration(Map<String, String> changedParameters) throws ConfigurationException {
979        Map<String, String> filteredParameters = getChangedParameters(changedParameters);
980        saveConfiguration(filteredParameters);
981    }
982
983    /**
984     * Filters given parameters including them only if (there was no previous value and new value is not empty/null) or
985     * (there was a previous value and it differs from the new value)
986     *
987     * @param changedParameters parameters to be filtered
988     * @return filtered map
989     * @since 5.4.2
990     */
991    public Map<String, String> getChangedParameters(Map<String, String> changedParameters) {
992        Map<String, String> filteredChangedParameters = new HashMap<>();
993        for (String key : changedParameters.keySet()) {
994            String oldParam = getStoredConfig().getProperty(key);
995            String newParam = changedParameters.get(key);
996            if (newParam != null) {
997                newParam = newParam.trim();
998            }
999            if (oldParam == null && StringUtils.isNotEmpty(newParam)
1000                    || oldParam != null && !oldParam.trim().equals(newParam)) {
1001                filteredChangedParameters.put(key, newParam);
1002            }
1003        }
1004        return filteredChangedParameters;
1005    }
1006
1007    private void writeConfiguration() throws ConfigurationException {
1008        final MessageDigest newContentDigest = DigestUtils.getMd5Digest();
1009        StringWriter newContent = new StringWriter() {
1010            @Override
1011            public void write(String str) {
1012                if (str != null) {
1013                    newContentDigest.update(str.getBytes());
1014                }
1015                super.write(str);
1016            }
1017        };
1018        // Copy back file content
1019        newContent.append(readConfiguration());
1020        // Write changed parameters
1021        newContent.write(BOUNDARY_BEGIN + System.getProperty("line.separator"));
1022        for (Object o : new TreeSet<>(userConfig.keySet())) {
1023            String key = (String) o;
1024            // Ignore parameters already stored in newContent
1025            if (PARAM_FORCE_GENERATION.equals(key) || PARAM_WIZARD_DONE.equals(key)
1026                    || PARAM_TEMPLATES_NAME.equals(key)) {
1027                continue;
1028            }
1029            String oldValue = storedConfig.getProperty(key, "");
1030            String newValue = userConfig.getRawProperty(key, "");
1031            if (!newValue.equals(oldValue)) {
1032                newContent.write("#" + key + "=" + oldValue + System.getProperty("line.separator"));
1033                newContent.write(key + "=" + newValue + System.getProperty("line.separator"));
1034            }
1035        }
1036        newContent.write(BOUNDARY_END + System.getProperty("line.separator"));
1037
1038        // Write file only if content has changed
1039        if (!Hex.encodeHexString(newContentDigest.digest()).equals(currentConfigurationDigest)) {
1040            try (Writer writer = new FileWriter(nuxeoConf, false)) {
1041                writer.append(newContent.getBuffer());
1042            } catch (IOException e) {
1043                throw new ConfigurationException("Error writing in " + nuxeoConf, e);
1044            }
1045        }
1046    }
1047
1048    private StringBuffer readConfiguration() throws ConfigurationException {
1049        // Will change wizardParam value instead of appending it
1050        String wizardParam = userConfig.getProperty(PARAM_WIZARD_DONE);
1051
1052        // Will change templatesParam value instead of appending it
1053        String templatesParam = userConfig.getProperty(PARAM_TEMPLATES_NAME);
1054        Integer generationIndex = null, wizardIndex = null, templatesIndex = null;
1055        List<String> newLines = new ArrayList<>();
1056        try (BufferedReader reader = new BufferedReader(new FileReader(nuxeoConf))) {
1057            String line;
1058            MessageDigest digest = DigestUtils.getMd5Digest();
1059            boolean onConfiguratorContent = false;
1060            while ((line = reader.readLine()) != null) {
1061                digest.update(line.getBytes());
1062                if (!onConfiguratorContent) {
1063                    if (!line.startsWith(BOUNDARY_BEGIN)) {
1064                        if (line.startsWith(PARAM_FORCE_GENERATION)) {
1065                            if (setOnceToFalse && onceGeneration) {
1066                                line = PARAM_FORCE_GENERATION + "=false";
1067                            }
1068                            if (setFalseToOnce && !forceGeneration) {
1069                                line = PARAM_FORCE_GENERATION + "=once";
1070                            }
1071                            if (generationIndex == null) {
1072                                newLines.add(line);
1073                                generationIndex = newLines.size() - 1;
1074                            } else {
1075                                newLines.set(generationIndex, line);
1076                            }
1077                        } else if (line.startsWith(PARAM_WIZARD_DONE)) {
1078                            if (wizardParam != null) {
1079                                line = PARAM_WIZARD_DONE + "=" + wizardParam;
1080                            }
1081                            if (wizardIndex == null) {
1082                                newLines.add(line);
1083                                wizardIndex = newLines.size() - 1;
1084                            } else {
1085                                newLines.set(wizardIndex, line);
1086                            }
1087                        } else if (line.startsWith(PARAM_TEMPLATES_NAME)) {
1088                            if (templatesParam != null) {
1089                                line = PARAM_TEMPLATES_NAME + "=" + templatesParam;
1090                            }
1091                            if (templatesIndex == null) {
1092                                newLines.add(line);
1093                                templatesIndex = newLines.size() - 1;
1094                            } else {
1095                                newLines.set(templatesIndex, line);
1096                            }
1097                        } else {
1098                            int equalIdx = line.indexOf("=");
1099                            if (equalIdx < 1 || line.trim().startsWith("#")) {
1100                                newLines.add(line);
1101                            } else {
1102                                String key = line.substring(0, equalIdx).trim();
1103                                if (userConfig.getProperty(key) != null) {
1104                                    newLines.add(line);
1105                                } else {
1106                                    newLines.add("#" + line);
1107                                }
1108                            }
1109                        }
1110                    } else {
1111                        // What must be written just before the BOUNDARY_BEGIN
1112                        if (templatesIndex == null && templatesParam != null) {
1113                            newLines.add(PARAM_TEMPLATES_NAME + "=" + templatesParam);
1114                            templatesIndex = newLines.size() - 1;
1115                        }
1116                        if (wizardIndex == null && wizardParam != null) {
1117                            newLines.add(PARAM_WIZARD_DONE + "=" + wizardParam);
1118                            wizardIndex = newLines.size() - 1;
1119                        }
1120                        onConfiguratorContent = true;
1121                    }
1122                } else {
1123                    if (!line.startsWith(BOUNDARY_END)) {
1124                        int equalIdx = line.indexOf("=");
1125                        if (line.startsWith("#" + PARAM_TEMPLATES_NAME) || line.startsWith(PARAM_TEMPLATES_NAME)) {
1126                            // Backward compliance, it must be ignored
1127                            continue;
1128                        }
1129                        if (equalIdx < 1) { // Ignore non-readable lines
1130                            continue;
1131                        }
1132                        if (line.trim().startsWith("#")) {
1133                            String key = line.substring(1, equalIdx).trim();
1134                            String value = line.substring(equalIdx + 1).trim();
1135                            getStoredConfig().setProperty(key, value);
1136                        } else {
1137                            String key = line.substring(0, equalIdx).trim();
1138                            String value = line.substring(equalIdx + 1).trim();
1139                            if (!value.equals(userConfig.getRawProperty(key))) {
1140                                getStoredConfig().setProperty(key, value);
1141                            }
1142                        }
1143                    } else {
1144                        onConfiguratorContent = false;
1145                    }
1146                }
1147            }
1148            reader.close();
1149            currentConfigurationDigest = Hex.encodeHexString(digest.digest());
1150        } catch (IOException e) {
1151            throw new ConfigurationException("Error reading " + nuxeoConf, e);
1152        }
1153        StringBuffer newContent = new StringBuffer();
1154        for (String newLine : newLines) {
1155            newContent.append(newLine.trim()).append(System.getProperty("line.separator"));
1156        }
1157        return newContent;
1158    }
1159
1160    /**
1161     * Extract a database template from the current list of templates. Return the last one if there are multiples.
1162     *
1163     * @see #rebuildTemplatesStr(String)
1164     */
1165    public String extractDatabaseTemplateName() {
1166        return extractDbTemplateName(DB_LIST, PARAM_TEMPLATE_DBTYPE, PARAM_TEMPLATE_DBNAME, "unknown");
1167    }
1168
1169    /**
1170     * Extract a NoSQL database template from the current list of templates. Return the last one if there are multiples.
1171     *
1172     * @see #rebuildTemplatesStr(String)
1173     * @since 8.1
1174     */
1175    public String extractNoSqlDatabaseTemplateName() {
1176        return extractDbTemplateName(DB_NOSQL_LIST, PARAM_TEMPLATE_DBNOSQL_TYPE, PARAM_TEMPLATE_DBNOSQL_NAME, null);
1177    }
1178
1179    private String extractDbTemplateName(List<String> knownDbList, String paramTemplateDbType,
1180            String paramTemplateDbName, String defaultTemplate) {
1181        String dbTemplate = defaultTemplate;
1182        boolean found = false;
1183        for (File templateFile : includedTemplates) {
1184            String template = templateFile.getName();
1185            if (knownDbList.contains(template)) {
1186                dbTemplate = template;
1187                found = true;
1188            }
1189        }
1190        String dbType = userConfig.getProperty(paramTemplateDbType);
1191        if (!found && dbType != null) {
1192            log.warn(String.format("Didn't find a known database template in the list but "
1193                    + "some template contributed a value for %s.", paramTemplateDbType));
1194            dbTemplate = dbType;
1195        }
1196        if (dbTemplate != null && !dbTemplate.equals(dbType)) {
1197            if (dbType == null) {
1198                log.warn(String.format("Missing value for %s, using %s", paramTemplateDbType, dbTemplate));
1199                userConfig.setProperty(paramTemplateDbType, dbTemplate);
1200            } else {
1201                log.debug(String.format("Different values between %s (%s) and %s (%s)", paramTemplateDbName, dbTemplate,
1202                        paramTemplateDbType, dbType));
1203            }
1204        }
1205        if (dbTemplate == null) {
1206            defaultConfig.remove(paramTemplateDbName);
1207        } else {
1208            defaultConfig.setProperty(paramTemplateDbName, dbTemplate);
1209        }
1210        return dbTemplate;
1211    }
1212
1213    /**
1214     * @return nuxeo.conf file used
1215     */
1216    public File getNuxeoConf() {
1217        return nuxeoConf;
1218    }
1219
1220    /**
1221     * Delegate logs initialization to serverConfigurator instance
1222     *
1223     * @since 5.4.2
1224     */
1225    public void initLogs() {
1226        serverConfigurator.initLogs();
1227    }
1228
1229    /**
1230     * @return log directory
1231     * @since 5.4.2
1232     */
1233    public File getLogDir() {
1234        return serverConfigurator.getLogDir();
1235    }
1236
1237    /**
1238     * @return pid directory
1239     * @since 5.4.2
1240     */
1241    public File getPidDir() {
1242        return serverConfigurator.getPidDir();
1243    }
1244
1245    /**
1246     * @return Data directory
1247     * @since 5.4.2
1248     */
1249    public File getDataDir() {
1250        return serverConfigurator.getDataDir();
1251    }
1252
1253    /**
1254     * Create needed directories. Check existence of old paths. If old paths have been found and they cannot be upgraded
1255     * automatically, then upgrading message is logged and error thrown.
1256     *
1257     * @throws ConfigurationException If a deprecated directory has been detected.
1258     * @since 5.4.2
1259     * @see ServerConfigurator#verifyInstallation()
1260     */
1261    public void verifyInstallation() throws ConfigurationException {
1262        checkJavaVersion();
1263        ifNotExistsAndIsDirectoryThenCreate(getLogDir());
1264        ifNotExistsAndIsDirectoryThenCreate(getPidDir());
1265        ifNotExistsAndIsDirectoryThenCreate(getDataDir());
1266        ifNotExistsAndIsDirectoryThenCreate(getTmpDir());
1267        ifNotExistsAndIsDirectoryThenCreate(getPackagesDir());
1268        checkAddressesAndPorts();
1269        serverConfigurator.verifyInstallation();
1270        backingServicesConfigurator.verifyInstallation();
1271
1272    }
1273
1274    /**
1275     * @return Marketplace packages directory
1276     * @since 5.9.4
1277     */
1278    private File getPackagesDir() {
1279        return serverConfigurator.getPackagesDir();
1280    }
1281
1282    /**
1283     * Check that the process is executed with a supported Java version. See
1284     * <a href="http://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html">J2SE SDK/JRE Version String
1285     * Naming Convention</a>
1286     *
1287     * @throws ConfigurationException
1288     * @since 5.6
1289     */
1290    public void checkJavaVersion() throws ConfigurationException {
1291        String version = System.getProperty("java.version");
1292        checkJavaVersion(version, COMPLIANT_JAVA_VERSIONS);
1293    }
1294
1295    /**
1296     * Check the java version compared to compliant ones.
1297     *
1298     * @param version the java version
1299     * @param compliantVersions the compliant java versions
1300     * @since 9.1
1301     */
1302    protected static void checkJavaVersion(String version, String[] compliantVersions) throws ConfigurationException {
1303        // compliantVersions represents the java versions on which Nuxeo runs perfectly, so:
1304        // - if we run Nuxeo with a major java version present in compliantVersions and compatible with then this
1305        // method exits without error and without logging a warn message about loose compliance
1306        // - if we run Nuxeo with a major java version not present in compliantVersions but greater than once then
1307        // this method exits without error and logs a warn message about loose compliance
1308        // - if we run Nuxeo with a non valid java version then method exits with error
1309        // - if we run Nuxeo with a non valid java version and with jvmcheck=nofail property then method exits without
1310        // error and logs a warn message about loose compliance
1311
1312        // try to retrieve the closest compliant java version
1313        String lastCompliantVersion = null;
1314        for (String compliantVersion : compliantVersions) {
1315            if (checkJavaVersion(version, compliantVersion, false, false)) {
1316                // current compliant version is valid, go to next one
1317                lastCompliantVersion = compliantVersion;
1318            } else if (lastCompliantVersion != null) {
1319                // current compliant version is not valid, but we found a valid one earlier, 1st case
1320                return;
1321            } else if (checkJavaVersion(version, compliantVersion, true, true)) {
1322                // current compliant version is not valid, try to check java version with jvmcheck=nofail, 4th case
1323                // here we will log about loose compliance for the lower compliant java version
1324                return;
1325            }
1326        }
1327        // we might have lastCompliantVersion, unless nothing is valid against the current java version
1328        if (lastCompliantVersion != null) {
1329            // 2nd case: log about loose compliance if current major java version is greater than the greatest
1330            // compliant java version
1331            checkJavaVersion(version, lastCompliantVersion, false, true);
1332            return;
1333        }
1334
1335        // 3th case
1336        String message = String.format("Nuxeo requires Java %s (detected %s).", ArrayUtils.toString(compliantVersions),
1337                version);
1338        throw new ConfigurationException(message + " See '" + JVMCHECK_PROP + "' option to bypass version check.");
1339    }
1340
1341    /**
1342     * Checks the java version compared to the required one.
1343     * <p>
1344     * Loose compliance is assumed if the major version is greater than the required major version or a jvmcheck=nofail
1345     * flag is set.
1346     *
1347     * @param version the java version
1348     * @param requiredVersion the required java version
1349     * @param allowNoFailFlag if {@code true} then check jvmcheck=nofail flag to always have loose compliance
1350     * @param warnIfLooseCompliance if {@code true} then log a WARN if the is loose compliance
1351     * @return true if the java version is compliant (maybe loosely) with the required version
1352     * @since 8.4
1353     */
1354    protected static boolean checkJavaVersion(String version, String requiredVersion, boolean allowNoFailFlag,
1355            boolean warnIfLooseCompliance) {
1356        allowNoFailFlag = allowNoFailFlag
1357                && JVMCHECK_NOFAIL.equalsIgnoreCase(System.getProperty(JVMCHECK_PROP, JVMCHECK_FAIL));
1358        try {
1359            JVMVersion required = JVMVersion.parse(requiredVersion);
1360            JVMVersion actual = JVMVersion.parse(version);
1361            boolean compliant = actual.compareTo(required) >= 0;
1362            if (compliant && actual.compareTo(required, UpTo.MAJOR) == 0) {
1363                return true;
1364            }
1365            if (!compliant && !allowNoFailFlag) {
1366                return false;
1367            }
1368            // greater major version or noFail is present in system property, considered loosely compliant but may warn
1369            if (warnIfLooseCompliance) {
1370                log.warn(String.format("Nuxeo requires Java %s+ (detected %s).", requiredVersion, version));
1371            }
1372            return true;
1373        } catch (java.text.ParseException cause) {
1374            if (allowNoFailFlag) {
1375                log.warn("Cannot check java version", cause);
1376                return true;
1377            }
1378            throw new IllegalArgumentException("Cannot check java version", cause);
1379        }
1380    }
1381
1382    /**
1383     * Checks the java version compared to the required one.
1384     * <p>
1385     * If major version is same as required major version and minor is greater or equal, it is compliant.
1386     * <p>
1387     * If major version is greater than required major version, it is compliant.
1388     *
1389     * @param version the java version
1390     * @param requiredVersion the required java version
1391     * @return true if the java version is compliant with the required version
1392     * @since 8.4
1393     */
1394    public static boolean checkJavaVersion(String version, String requiredVersion) {
1395        return checkJavaVersion(version, requiredVersion, false, false);
1396    }
1397
1398    /**
1399     * Will check the configured addresses are reachable and Nuxeo required ports are available on those addresses.
1400     * Server specific implementations should override this method in order to check for server specific ports.
1401     * {@link #PARAM_BIND_ADDRESS} must be set before.
1402     *
1403     * @throws ConfigurationException
1404     * @since 5.5
1405     * @see ServerConfigurator#verifyInstallation()
1406     */
1407    public void checkAddressesAndPorts() throws ConfigurationException {
1408        InetAddress bindAddress = getBindAddress();
1409        // Sanity check
1410        if (bindAddress.isMulticastAddress()) {
1411            throw new ConfigurationException("Multicast address won't work: " + bindAddress);
1412        }
1413        checkAddressReachable(bindAddress);
1414        checkPortAvailable(bindAddress, Integer.parseInt(userConfig.getProperty(PARAM_HTTP_PORT)));
1415    }
1416
1417    /**
1418     * Checks the userConfig bind address is not 0.0.0.0 and replaces it with 127.0.0.1 if needed
1419     *
1420     * @return the userConfig bind address if not 0.0.0.0 else 127.0.0.1
1421     * @throws ConfigurationException
1422     * @since 5.7
1423     */
1424    public InetAddress getBindAddress() throws ConfigurationException {
1425        return getBindAddress(userConfig.getProperty(PARAM_BIND_ADDRESS));
1426    }
1427
1428    /**
1429     * Checks hostName bind address is not 0.0.0.0 and replaces it with 127.0.0.1 if needed
1430     *
1431     * @param hostName the hostname of Nuxeo server (works also with the IP)
1432     * @return the bind address matching hostName parameter if not 0.0.0.0 else 127.0.0.1
1433     * @throws ConfigurationException
1434     * @since 9.2
1435     */
1436    public static InetAddress getBindAddress(String hostName) throws ConfigurationException {
1437        InetAddress bindAddress;
1438        try {
1439            bindAddress = InetAddress.getByName(hostName);
1440            if (bindAddress.isAnyLocalAddress()) {
1441                boolean preferIPv6 = "false".equals(System.getProperty("java.net.preferIPv4Stack"))
1442                        && "true".equals(System.getProperty("java.net.preferIPv6Addresses"));
1443                bindAddress = preferIPv6 ? InetAddress.getByName("::1") : InetAddress.getByName("127.0.0.1");
1444                log.debug("Bind address is \"ANY\", using local address instead: " + bindAddress);
1445            }
1446            log.debug("Configured bind address: " + bindAddress);
1447        } catch (UnknownHostException e) {
1448            throw new ConfigurationException(e);
1449        }
1450        return bindAddress;
1451    }
1452
1453    /**
1454     * @param address address to check for availability
1455     * @throws ConfigurationException
1456     * @since 5.5
1457     */
1458    public static void checkAddressReachable(InetAddress address) throws ConfigurationException {
1459        try {
1460            log.debug("Checking availability of " + address);
1461            address.isReachable(ADDRESS_PING_TIMEOUT);
1462        } catch (IOException e) {
1463            throw new ConfigurationException("Unreachable bind address " + address);
1464        }
1465    }
1466
1467    /**
1468     * Checks if port is available on given address.
1469     *
1470     * @param port port to check for availability
1471     * @throws ConfigurationException Throws an exception if address is unavailable.
1472     * @since 5.5
1473     */
1474    public static void checkPortAvailable(InetAddress address, int port) throws ConfigurationException {
1475        if ((port == 0) || (port == -1)) {
1476            log.warn("Port is set to " + Integer.toString(port)
1477                    + " - assuming it is disabled - skipping availability check");
1478            return;
1479        }
1480        if (port < MIN_PORT || port > MAX_PORT) {
1481            throw new IllegalArgumentException("Invalid port: " + port);
1482        }
1483        ServerSocket socketTCP = null;
1484        // DatagramSocket socketUDP = null;
1485        try {
1486            log.debug("Checking availability of port " + port + " on address " + address);
1487            socketTCP = new ServerSocket(port, 0, address);
1488            socketTCP.setReuseAddress(true);
1489            // socketUDP = new DatagramSocket(port, address);
1490            // socketUDP.setReuseAddress(true);
1491            // return true;
1492        } catch (IOException e) {
1493            throw new ConfigurationException(e.getMessage() + ": " + address + ":" + port, e);
1494        } finally {
1495            // if (socketUDP != null) {
1496            // socketUDP.close();
1497            // }
1498            if (socketTCP != null) {
1499                try {
1500                    socketTCP.close();
1501                } catch (IOException e) {
1502                    // Do not throw
1503                }
1504            }
1505        }
1506    }
1507
1508    /**
1509     * @return Temporary directory
1510     */
1511    public File getTmpDir() {
1512        return serverConfigurator.getTmpDir();
1513    }
1514
1515    private void ifNotExistsAndIsDirectoryThenCreate(File directory) {
1516        if (!directory.isDirectory()) {
1517            directory.mkdirs();
1518        }
1519    }
1520
1521    /**
1522     * @return Log files produced by Log4J configuration without loading this configuration instead of current active
1523     *         one.
1524     * @since 5.4.2
1525     */
1526    public ArrayList<String> getLogFiles() {
1527        File log4jConfFile = serverConfigurator.getLogConfFile();
1528        System.setProperty(org.nuxeo.common.Environment.NUXEO_LOG_DIR, getLogDir().getPath());
1529        return Log4JHelper.getFileAppendersFiles(log4jConfFile);
1530    }
1531
1532    /**
1533     * Check if wizard must and can be ran
1534     *
1535     * @return true if configuration wizard is required before starting Nuxeo
1536     * @since 5.4.2
1537     */
1538    public boolean isWizardRequired() {
1539        return !"true".equalsIgnoreCase(getUserConfig().getProperty(PARAM_WIZARD_DONE, "true"))
1540                && serverConfigurator.isWizardAvailable();
1541    }
1542
1543    /**
1544     * Rebuild a templates string for use in nuxeo.conf
1545     *
1546     * @param dbTemplate database template to use instead of current one
1547     * @return new templates string using given dbTemplate
1548     * @since 5.4.2
1549     * @see #extractDatabaseTemplateName()
1550     * @see #changeDBTemplate(String)
1551     * @see #changeTemplates(String)
1552     */
1553    public String rebuildTemplatesStr(String dbTemplate) {
1554        List<String> templatesList = new ArrayList<>();
1555        templatesList.addAll(Arrays.asList(templates.split(TEMPLATE_SEPARATOR)));
1556        String currentDBTemplate = null;
1557        if (DB_LIST.contains(dbTemplate)) {
1558            currentDBTemplate = userConfig.getProperty(PARAM_TEMPLATE_DBNAME);
1559            if (currentDBTemplate == null) {
1560                currentDBTemplate = extractDatabaseTemplateName();
1561            }
1562        } else if (DB_NOSQL_LIST.contains(dbTemplate)) {
1563            currentDBTemplate = userConfig.getProperty(PARAM_TEMPLATE_DBNOSQL_NAME);
1564            if (currentDBTemplate == null) {
1565                currentDBTemplate = extractNoSqlDatabaseTemplateName();
1566            }
1567            if ("none".equals(dbTemplate)) {
1568                dbTemplate = null;
1569            }
1570        }
1571        int dbIdx = templatesList.indexOf(currentDBTemplate);
1572        if (dbIdx < 0) {
1573            if (dbTemplate == null) {
1574                return templates;
1575            }
1576            // current db template is implicit => set the new one
1577            templatesList.add(dbTemplate);
1578        } else if (dbTemplate == null) {
1579            // current db template is explicit => remove it
1580            templatesList.remove(dbIdx);
1581        } else {
1582            // current db template is explicit => replace it
1583            templatesList.set(dbIdx, dbTemplate);
1584        }
1585        return replaceEnvironmentVariables(StringUtils.join(templatesList, TEMPLATE_SEPARATOR));
1586    }
1587
1588    /**
1589     * @return Nuxeo config directory
1590     * @since 5.4.2
1591     */
1592    public File getConfigDir() {
1593        return serverConfigurator.getConfigDir();
1594    }
1595
1596    /**
1597     * Ensure the server will start only wizard application, not Nuxeo
1598     *
1599     * @since 5.4.2
1600     */
1601    public void prepareWizardStart() {
1602        serverConfigurator.prepareWizardStart();
1603    }
1604
1605    /**
1606     * Ensure the wizard won't be started and nuxeo is ready for use
1607     *
1608     * @since 5.4.2
1609     */
1610    public void cleanupPostWizard() {
1611        serverConfigurator.cleanupPostWizard();
1612    }
1613
1614    /**
1615     * @return Nuxeo runtime home
1616     */
1617    public File getRuntimeHome() {
1618        return serverConfigurator.getRuntimeHome();
1619    }
1620
1621    /**
1622     * @since 5.4.2
1623     * @return true if there's an install in progress
1624     */
1625    public boolean isInstallInProgress() {
1626        return getInstallFile().exists();
1627    }
1628
1629    /**
1630     * @return File pointing to the directory containing the marketplace packages included in the distribution
1631     * @since 5.6
1632     */
1633    public File getDistributionMPDir() {
1634        String mpDir = userConfig.getProperty(PARAM_MP_DIR, DISTRIBUTION_MP_DIR);
1635        return new File(getNuxeoHome(), mpDir);
1636    }
1637
1638    /**
1639     * @return Install/upgrade file
1640     * @since 5.4.1
1641     */
1642    public File getInstallFile() {
1643        return new File(serverConfigurator.getDataDir(), INSTALL_AFTER_RESTART);
1644    }
1645
1646    /**
1647     * Add template(s) to the {@link #PARAM_TEMPLATES_NAME} list if not already present
1648     *
1649     * @param templatesToAdd Comma separated templates to add
1650     * @throws ConfigurationException
1651     * @since 5.5
1652     */
1653    public void addTemplate(String templatesToAdd) throws ConfigurationException {
1654        List<String> templatesList = getTemplateList();
1655        List<String> templatesToAddList = Arrays.asList(templatesToAdd.split(TEMPLATE_SEPARATOR));
1656        if (templatesList.addAll(templatesToAddList)) {
1657            String newTemplatesStr = StringUtils.join(templatesList, TEMPLATE_SEPARATOR);
1658            HashMap<String, String> parametersToSave = new HashMap<>();
1659            parametersToSave.put(PARAM_TEMPLATES_NAME, newTemplatesStr);
1660            saveFilteredConfiguration(parametersToSave);
1661            changeTemplates(newTemplatesStr);
1662        }
1663    }
1664
1665    /**
1666     * Return the list of templates.
1667     * @return
1668     * @since 9.2
1669     */
1670    public List<String> getTemplateList() {
1671        String currentTemplatesStr = userConfig.getProperty(PARAM_TEMPLATES_NAME);
1672
1673        return Stream.of(replaceEnvironmentVariables(currentTemplatesStr).split(TEMPLATE_SEPARATOR))
1674                .collect(Collectors.toList());
1675
1676    }
1677
1678    /**
1679     * Remove template(s) from the {@link #PARAM_TEMPLATES_NAME} list
1680     *
1681     * @param templatesToRm Comma separated templates to remove
1682     * @throws ConfigurationException
1683     * @since 5.5
1684     */
1685    public void rmTemplate(String templatesToRm) throws ConfigurationException {
1686        List<String> templatesList = getTemplateList();
1687        List<String> templatesToRmList = Arrays.asList(templatesToRm.split(TEMPLATE_SEPARATOR));
1688        if (templatesList.removeAll(templatesToRmList)) {
1689            String newTemplatesStr = StringUtils.join(templatesList, TEMPLATE_SEPARATOR);
1690            HashMap<String, String> parametersToSave = new HashMap<>();
1691            parametersToSave.put(PARAM_TEMPLATES_NAME, newTemplatesStr);
1692            saveFilteredConfiguration(parametersToSave);
1693            changeTemplates(newTemplatesStr);
1694        }
1695    }
1696
1697    /**
1698     * Set a property in nuxeo configuration
1699     *
1700     * @throws ConfigurationException
1701     * @return The old value
1702     * @since 5.5
1703     */
1704    public String setProperty(String key, String value) throws ConfigurationException {
1705        String oldValue = getStoredConfig().getProperty(key);
1706        if (PARAM_TEMPLATES_NAME.equals(key)) {
1707            templates = StringUtils.isBlank(value) ? null : value;
1708        }
1709        HashMap<String, String> newParametersToSave = new HashMap<>();
1710        newParametersToSave.put(key, value);
1711        saveFilteredConfiguration(newParametersToSave);
1712        setBasicConfiguration();
1713        return oldValue;
1714    }
1715
1716    /**
1717     * Set properties in nuxeo configuration
1718     *
1719     * @return The old values
1720     * @throws ConfigurationException
1721     * @since 7.4
1722     */
1723    public Map<String, String> setProperties(Map<String, String> newParametersToSave) throws ConfigurationException {
1724        Map<String, String> oldValues = new HashMap<>();
1725        for (String key : newParametersToSave.keySet()) {
1726            oldValues.put(key, getStoredConfig().getProperty(key));
1727            if (PARAM_TEMPLATES_NAME.equals(key)) {
1728                String value = newParametersToSave.get(key);
1729                templates = StringUtils.isBlank(value) ? null : value;
1730            }
1731        }
1732        saveFilteredConfiguration(newParametersToSave);
1733        setBasicConfiguration();
1734        return oldValues;
1735    }
1736
1737    /**
1738     * Set properties in the given template, if it exists
1739     *
1740     * @return The old values
1741     * @throws ConfigurationException
1742     * @throws IOException
1743     * @since 7.4
1744     */
1745    public Map<String, String> setProperties(String template, Map<String, String> newParametersToSave)
1746            throws ConfigurationException, IOException {
1747        File templateConf = getTemplateConf(template);
1748        Properties templateProperties = loadTrimmedProperties(templateConf);
1749        Map<String, String> oldValues = new HashMap<>();
1750        StringBuilder newContent = new StringBuilder();
1751        try (BufferedReader reader = new BufferedReader(new FileReader(templateConf))) {
1752            String line = reader.readLine();
1753            if (line != null && line.startsWith("## DO NOT EDIT THIS FILE")) {
1754                throw new ConfigurationException("The template states in its header that it must not be modified.");
1755            }
1756            while (line != null) {
1757                int equalIdx = line.indexOf("=");
1758                if (equalIdx < 1 || line.trim().startsWith("#")) {
1759                    newContent.append(line).append(System.getProperty("line.separator"));
1760                } else {
1761                    String key = line.substring(0, equalIdx).trim();
1762                    if (newParametersToSave.containsKey(key)) {
1763                        newContent.append(key).append("=").append(newParametersToSave.get(key)).append(
1764                                System.getProperty("line.separator"));
1765                    } else {
1766                        newContent.append(line).append(System.getProperty("line.separator"));
1767                    }
1768                }
1769                line = reader.readLine();
1770            }
1771        }
1772        for (String key : newParametersToSave.keySet()) {
1773            if (templateProperties.containsKey(key)) {
1774                oldValues.put(key, templateProperties.getProperty(key));
1775            } else {
1776                newContent.append(key).append("=").append(newParametersToSave.get(key)).append(
1777                        System.getProperty("line.separator"));
1778            }
1779        }
1780        try (BufferedWriter writer = new BufferedWriter(new FileWriter(templateConf))) {
1781            writer.append(newContent.toString());
1782        }
1783        setBasicConfiguration();
1784        return oldValues;
1785    }
1786
1787    /**
1788     * Check driver availability and database connection
1789     *
1790     * @param databaseTemplate Nuxeo database template
1791     * @param dbName nuxeo.db.name parameter in nuxeo.conf
1792     * @param dbUser nuxeo.db.user parameter in nuxeo.conf
1793     * @param dbPassword nuxeo.db.password parameter in nuxeo.conf
1794     * @param dbHost nuxeo.db.host parameter in nuxeo.conf
1795     * @param dbPort nuxeo.db.port parameter in nuxeo.conf
1796     * @throws DatabaseDriverException
1797     * @throws IOException
1798     * @throws FileNotFoundException
1799     * @throws SQLException
1800     * @since 5.6
1801     */
1802    public void checkDatabaseConnection(String databaseTemplate, String dbName, String dbUser, String dbPassword,
1803            String dbHost, String dbPort)
1804                    throws FileNotFoundException, IOException, DatabaseDriverException, SQLException {
1805        File databaseTemplateDir = new File(nuxeoHome, TEMPLATES + File.separator + databaseTemplate);
1806        Properties templateProperties = loadTrimmedProperties(new File(databaseTemplateDir, NUXEO_DEFAULT_CONF));
1807        String classname, connectionUrl;
1808        if (userConfig.getProperty(PARAM_TEMPLATE_DBNAME).equals(databaseTemplateDir)) {
1809            // userConfig already includes databaseTemplate
1810            classname = userConfig.getProperty(PARAM_DB_DRIVER);
1811            connectionUrl = userConfig.getProperty(PARAM_DB_JDBC_URL);
1812        } else { // testing a databaseTemplate not included in userConfig
1813            // check if value is set in nuxeo.conf
1814            if (userConfig.containsKey(PARAM_DB_DRIVER)) {
1815                classname = (String) userConfig.get(PARAM_DB_DRIVER);
1816            } else {
1817                classname = templateProperties.getProperty(PARAM_DB_DRIVER);
1818            }
1819            if (userConfig.containsKey(PARAM_DB_JDBC_URL)) {
1820                connectionUrl = (String) userConfig.get(PARAM_DB_JDBC_URL);
1821            } else {
1822                connectionUrl = templateProperties.getProperty(PARAM_DB_JDBC_URL);
1823            }
1824        }
1825        // Load driver class from template or default lib directory
1826        Driver driver = lookupDriver(databaseTemplate, databaseTemplateDir, classname);
1827        // Test db connection
1828        DriverManager.registerDriver(driver);
1829        Properties ttProps = new Properties(userConfig);
1830        ttProps.put(PARAM_DB_HOST, dbHost);
1831        ttProps.put(PARAM_DB_PORT, dbPort);
1832        ttProps.put(PARAM_DB_NAME, dbName);
1833        ttProps.put(PARAM_DB_USER, dbUser);
1834        ttProps.put(PARAM_DB_PWD, dbPassword);
1835        TextTemplate tt = new TextTemplate(ttProps);
1836        String url = tt.processText(connectionUrl);
1837        Properties conProps = new Properties();
1838        conProps.put("user", dbUser);
1839        conProps.put("password", dbPassword);
1840        log.debug("Testing URL " + url + " with " + conProps);
1841        Connection con = driver.connect(url, conProps);
1842        con.close();
1843    }
1844
1845    /**
1846     * Build an {@link URLClassLoader} for the given databaseTemplate looking in the templates directory and in the
1847     * server lib directory, then looks for a driver
1848     *
1849     * @param classname Driver class name, defined by {@link #PARAM_DB_DRIVER}
1850     * @return Driver driver if found, else an Exception must have been raised.
1851     * @throws IOException
1852     * @throws FileNotFoundException
1853     * @throws DatabaseDriverException If there was an error when trying to instantiate the driver.
1854     * @since 5.6
1855     */
1856    private Driver lookupDriver(String databaseTemplate, File databaseTemplateDir, String classname)
1857            throws FileNotFoundException, IOException, DatabaseDriverException {
1858        File[] files = (File[]) ArrayUtils.addAll( //
1859                new File(databaseTemplateDir, "lib").listFiles(), //
1860                serverConfigurator.getServerLibDir().listFiles());
1861        List<URL> urlsList = new ArrayList<>();
1862        if (files != null) {
1863            for (File file : files) {
1864                if (file.getName().endsWith("jar")) {
1865                    try {
1866                        urlsList.add(new URL("jar:file:" + file.getPath() + "!/"));
1867                        log.debug("Added " + file.getPath());
1868                    } catch (MalformedURLException e) {
1869                        log.error(e);
1870                    }
1871                }
1872            }
1873        }
1874        URLClassLoader ucl = new URLClassLoader(urlsList.toArray(new URL[0]));
1875        try {
1876            return (Driver) Class.forName(classname, true, ucl).newInstance();
1877        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
1878            throw new DatabaseDriverException(e);
1879        }
1880    }
1881
1882    /**
1883     * @since 5.6
1884     * @return an {@link Environment} initialized with a few basics
1885     */
1886    public Environment getEnv() {
1887        /*
1888         * It could be useful to initialize DEFAULT env in {@link #setBasicConfiguration()}... For now, the generated
1889         * {@link Environment} is not static.
1890         */
1891        if (env == null) {
1892            env = new Environment(getRuntimeHome());
1893            File distribFile = new File(new File(nuxeoHome, TEMPLATES), "common/config/distribution.properties");
1894            if (distribFile.exists()) {
1895                try {
1896                    env.loadProperties(loadTrimmedProperties(distribFile));
1897                } catch (IOException e) {
1898                    log.error(e);
1899                }
1900            }
1901            env.loadProperties(userConfig);
1902            env.setServerHome(getNuxeoHome());
1903            env.init();
1904            env.setData(userConfig.getProperty(Environment.NUXEO_DATA_DIR, "data"));
1905            env.setLog(userConfig.getProperty(Environment.NUXEO_LOG_DIR, "logs"));
1906            env.setTemp(userConfig.getProperty(Environment.NUXEO_TMP_DIR, "tmp"));
1907            env.setPath(PARAM_MP_DIR, getDistributionMPDir(), env.getServerHome());
1908            env.setPath(Environment.NUXEO_MP_DIR, getPackagesDir(), env.getServerHome());
1909        }
1910        return env;
1911    }
1912
1913    /**
1914     * @since 5.6
1915     * @param propsFile Properties file
1916     * @return new Properties containing trimmed keys and values read in {@code propsFile}
1917     * @throws IOException
1918     */
1919    public static Properties loadTrimmedProperties(File propsFile) throws IOException {
1920        Properties props = new Properties();
1921        try (FileInputStream propsIS = new FileInputStream(propsFile)) {
1922            loadTrimmedProperties(props, propsIS);
1923        }
1924        return props;
1925    }
1926
1927    /**
1928     * @since 5.6
1929     * @param props Properties object to be filled
1930     * @param propsIS Properties InputStream
1931     * @throws IOException
1932     */
1933    public static void loadTrimmedProperties(Properties props, InputStream propsIS) throws IOException {
1934        if (props == null) {
1935            return;
1936        }
1937        Properties p = new Properties();
1938        p.load(propsIS);
1939        @SuppressWarnings("unchecked")
1940        Enumeration<String> pEnum = (Enumeration<String>) p.propertyNames();
1941        while (pEnum.hasMoreElements()) {
1942            String key = pEnum.nextElement();
1943            String value = p.getProperty(key);
1944            props.put(key.trim(), value.trim());
1945        }
1946    }
1947
1948    /**
1949     * @return The generated properties file with dumped configuration.
1950     * @since 5.6
1951     */
1952    public File getDumpedConfig() {
1953        return new File(getConfigDir(), CONFIGURATION_PROPERTIES);
1954    }
1955
1956    /**
1957     * Build a {@link Hashtable} which contains environment properties to instantiate a {@link InitialDirContext}
1958     *
1959     * @since 6.0
1960     */
1961    public Hashtable<Object, Object> getContextEnv(String ldapUrl, String bindDn, String bindPassword,
1962            boolean checkAuthentication) {
1963        Hashtable<Object, Object> contextEnv = new Hashtable<>();
1964        contextEnv.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
1965        contextEnv.put("com.sun.jndi.ldap.connect.timeout", "10000");
1966        contextEnv.put(javax.naming.Context.PROVIDER_URL, ldapUrl);
1967        if (checkAuthentication) {
1968            contextEnv.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple");
1969            contextEnv.put(javax.naming.Context.SECURITY_PRINCIPAL, bindDn);
1970            contextEnv.put(javax.naming.Context.SECURITY_CREDENTIALS, bindPassword);
1971        }
1972        return contextEnv;
1973    }
1974
1975    /**
1976     * Check if the LDAP parameters are correct to bind to a LDAP server. if authenticate argument is true, it will also
1977     * check if the authentication against the LDAP server succeeds
1978     *
1979     * @param authenticate Indicates if authentication against LDAP should be checked.
1980     * @since 6.0
1981     */
1982    public void checkLdapConnection(String ldapUrl, String ldapBindDn, String ldapBindPwd, boolean authenticate)
1983            throws NamingException {
1984        checkLdapConnection(getContextEnv(ldapUrl, ldapBindDn, ldapBindPwd, authenticate));
1985    }
1986
1987    /**
1988     * @param contextEnv Environment properties to build a {@link InitialDirContext}
1989     * @since 6.0
1990     */
1991    public void checkLdapConnection(Hashtable<Object, Object> contextEnv) throws NamingException {
1992        DirContext dirContext = new InitialDirContext(contextEnv);
1993        dirContext.close();
1994    }
1995
1996    /**
1997     * @return a {@link Crypto} instance initialized with the configuration parameters
1998     * @since 7.4
1999     * @see Crypto
2000     */
2001    public Crypto getCrypto() {
2002        return userConfig.getCrypto();
2003    }
2004
2005    /**
2006     * @param template path to configuration template directory
2007     * @return A {@code nuxeo.defaults} file if it exists.
2008     * @throws ConfigurationException if the template file is not found.
2009     * @since 7.4
2010     */
2011    public File getTemplateConf(String template) throws ConfigurationException {
2012        File templateDir = new File(template);
2013        if (!templateDir.isAbsolute()) {
2014            templateDir = new File(System.getProperty("user.dir"), template);
2015            if (!templateDir.exists() || !new File(templateDir, NUXEO_DEFAULT_CONF).exists()) {
2016                templateDir = new File(nuxeoDefaultConf.getParentFile(), template);
2017            }
2018        }
2019        if (!templateDir.exists() || !new File(templateDir, NUXEO_DEFAULT_CONF).exists()) {
2020            throw new ConfigurationException("Template not found: " + template);
2021        }
2022        return new File(templateDir, NUXEO_DEFAULT_CONF);
2023    }
2024
2025    /**
2026     * Gets the Java options with 'nuxeo.*' properties substituted. It enables usage of property like ${nuxeo.log.dir}
2027     * inside JAVA_OPTS.
2028     *
2029     * @return the java options string.
2030     */
2031    protected String getJavaOpts(String key, String value) {
2032        return StrSubstitutor.replace(System.getProperty(key, value), getUserConfig());
2033    }
2034
2035    /**
2036     * @return the value of an environment variable. Overriden for testing.
2037     * @since 9.1
2038     */
2039    protected String getEnvironmentVariableValue(String key) {
2040        return System.getenv(key);
2041    }
2042}