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