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