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