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