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