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