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