001/*
002 * (C) Copyright 2010-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Julien Carsique
018 */
019package org.nuxeo.launcher.config;
020
021import java.io.BufferedReader;
022import java.io.BufferedWriter;
023import java.io.File;
024import java.io.FileNotFoundException;
025import java.io.FileOutputStream;
026import java.io.FileReader;
027import java.io.FilenameFilter;
028import java.io.IOException;
029import java.io.OutputStreamWriter;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Enumeration;
033import java.util.HashSet;
034import java.util.List;
035import java.util.Map;
036import java.util.Properties;
037import java.util.Set;
038import java.util.StringTokenizer;
039import java.util.TreeSet;
040
041import org.apache.commons.io.FileUtils;
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.apache.logging.log4j.core.config.Configurator;
045import org.apache.logging.log4j.core.config.DefaultConfiguration;
046import org.nuxeo.common.Environment;
047import org.nuxeo.common.codec.Crypto;
048import org.nuxeo.common.codec.CryptoProperties;
049import org.nuxeo.common.utils.TextTemplate;
050import org.nuxeo.connect.update.LocalPackage;
051import org.nuxeo.launcher.info.ConfigurationInfo;
052import org.nuxeo.launcher.info.DistributionInfo;
053import org.nuxeo.launcher.info.InstanceInfo;
054import org.nuxeo.launcher.info.KeyValueInfo;
055import org.nuxeo.launcher.info.PackageInfo;
056import org.nuxeo.log4j.Log4JHelper;
057
058import freemarker.template.TemplateException;
059
060/**
061 * @author jcarsique
062 */
063public abstract class ServerConfigurator {
064
065    protected static final Log log = LogFactory.getLog(ServerConfigurator.class);
066
067    protected final ConfigurationGenerator generator;
068
069    protected File dataDir = null;
070
071    protected File logDir = null;
072
073    protected File pidDir = null;
074
075    protected File tmpDir = null;
076
077    protected File packagesDir = null;
078
079    /**
080     * @since 5.4.2
081     */
082    public static final List<String> NUXEO_SYSTEM_PROPERTIES = Arrays.asList("nuxeo.conf", "nuxeo.home", "log.id");
083
084    protected static final String DEFAULT_CONTEXT_NAME = "/nuxeo";
085
086    /** @since 9.3 */
087    public static final String JAVA_OPTS = "JAVA_OPTS";
088
089    private static final String NEW_FILES = ConfigurationGenerator.TEMPLATES + File.separator + "files.list";
090
091    /**
092     * @since 5.4.2
093     * @deprecated Since 5.9.4. Use {@link org.nuxeo.common.Environment#DEFAULT_LOG_DIR} instead.
094     */
095    @Deprecated
096    public static final String DEFAULT_LOG_DIR = org.nuxeo.common.Environment.DEFAULT_LOG_DIR;
097
098    /**
099     * @deprecated Since 5.9.4. Use {@link org.nuxeo.common.Environment#DEFAULT_DATA_DIR} instead.
100     */
101    @Deprecated
102    public static final String DEFAULT_DATA_DIR = org.nuxeo.common.Environment.DEFAULT_DATA_DIR;
103
104    /**
105     * @since 5.4.2
106     * @deprecated Since 5.9.4. Use {@link org.nuxeo.common.Environment#DEFAULT_TMP_DIR} instead.
107     */
108    @Deprecated
109    public static final String DEFAULT_TMP_DIR = org.nuxeo.common.Environment.DEFAULT_TMP_DIR;
110
111    public ServerConfigurator(ConfigurationGenerator configurationGenerator) {
112        generator = configurationGenerator;
113    }
114
115    /**
116     * @return true if server configuration files already exist
117     */
118    abstract boolean isConfigured();
119
120    /**
121     * Generate configuration files from templates and given configuration parameters
122     *
123     * @param config Properties with configuration parameters for template replacement
124     */
125    protected void parseAndCopy(Properties config) throws IOException, TemplateException, ConfigurationException {
126        // FilenameFilter for excluding "nuxeo.defaults" files from copy
127        final FilenameFilter filter = (dir, name) -> !ConfigurationGenerator.NUXEO_DEFAULT_CONF.equals(name);
128        final TextTemplate templateParser = new TextTemplate(config);
129        templateParser.setKeepEncryptedAsVar(true);
130        templateParser.setTrim(true);
131        templateParser.setTextParsingExtensions(
132                config.getProperty(ConfigurationGenerator.PARAM_TEMPLATES_PARSING_EXTENSIONS, "xml,properties,nx"));
133        templateParser.setFreemarkerParsingExtensions(
134                config.getProperty(ConfigurationGenerator.PARAM_TEMPLATES_FREEMARKER_EXTENSIONS, "nxftl"));
135
136        deleteTemplateFiles();
137        // add included templates directories
138        List<String> newFilesList = new ArrayList<>();
139        for (File includedTemplate : generator.getIncludedTemplates()) {
140            File[] listFiles = includedTemplate.listFiles(filter);
141            if (listFiles != null) {
142                String templateName = includedTemplate.getName();
143                log.debug(String.format("Parsing %s... %s", templateName, Arrays.toString(listFiles)));
144                // Check for deprecation
145                boolean isDeprecated = Boolean.parseBoolean(config.getProperty(templateName + ".deprecated"));
146                if (isDeprecated) {
147                    log.warn("WARNING: Template " + templateName + " is deprecated.");
148                    String deprecationMessage = config.getProperty(templateName + ".deprecation");
149                    if (deprecationMessage != null) {
150                        log.warn(deprecationMessage);
151                    }
152                }
153                // Retrieve optional target directory if defined
154                String outputDirectoryStr = config.getProperty(templateName + ".target");
155                File outputDirectory = (outputDirectoryStr != null)
156                        ? new File(generator.getNuxeoHome(), outputDirectoryStr)
157                        : getOutputDirectory();
158                for (File in : listFiles) {
159                    // copy template(s) directories parsing properties
160                    newFilesList.addAll(templateParser.processDirectory(in, new File(outputDirectory, in.getName())));
161                }
162            }
163        }
164        storeNewFilesList(newFilesList);
165    }
166
167    /**
168     * Delete files previously deployed by templates. If a file had been overwritten by a template, it will be restored.
169     * Helps the server returning to the state before any template was applied.
170     */
171    private void deleteTemplateFiles() throws IOException, ConfigurationException {
172        File newFiles = new File(generator.getNuxeoHome(), NEW_FILES);
173        if (!newFiles.exists()) {
174            return;
175        }
176        try (BufferedReader reader = new BufferedReader(new FileReader(newFiles))) {
177            String line;
178            while ((line = reader.readLine()) != null) {
179                if (line.endsWith(".bak")) {
180                    log.debug("Restore " + line);
181                    try {
182                        File backup = new File(generator.getNuxeoHome(), line);
183                        File original = new File(generator.getNuxeoHome(), line.substring(0, line.length() - 4));
184                        FileUtils.copyFile(backup, original);
185                        backup.delete();
186                    } catch (IOException e) {
187                        throw new ConfigurationException(String.format(
188                                "Failed to restore %s from %s\nEdit or " + "delete %s to bypass that error.",
189                                line.substring(0, line.length() - 4), line, newFiles), e);
190                    }
191                } else {
192                    log.debug("Remove " + line);
193                    new File(generator.getNuxeoHome(), line).delete();
194                }
195            }
196        }
197        newFiles.delete();
198    }
199
200    /**
201     * Store into {@link #NEW_FILES} the list of new files deployed by the templates. For later use by
202     * {@link #deleteTemplateFiles()}
203     */
204    private void storeNewFilesList(List<String> newFilesList) throws IOException {
205        File newFiles = new File(generator.getNuxeoHome(), NEW_FILES);
206        try (BufferedWriter writer = new BufferedWriter(
207                new OutputStreamWriter(new FileOutputStream(newFiles, false), "UTF-8"))) {
208            // Store new files listing
209            int index = generator.getNuxeoHome().getCanonicalPath().length() + 1;
210            for (String filepath : newFilesList) {
211                writer.write(new File(filepath).getCanonicalPath().substring(index));
212                writer.newLine();
213            }
214        }
215    }
216
217    /**
218     * @return output directory for files generation
219     */
220    protected File getOutputDirectory() {
221        return getRuntimeHome();
222    }
223
224    /**
225     * @return Default data directory path relative to Nuxeo Home
226     * @since 5.4.2
227     */
228    protected String getDefaultDataDir() {
229        return org.nuxeo.common.Environment.DEFAULT_DATA_DIR;
230    }
231
232    /**
233     * Returns the Home of NuxeoRuntime (same as Framework.getRuntime().getHome().getAbsolutePath())
234     */
235    protected abstract File getRuntimeHome();
236
237    /**
238     * @return Data directory
239     * @since 5.4.2
240     */
241    public File getDataDir() {
242        if (dataDir == null) {
243            dataDir = new File(generator.getNuxeoHome(), getDefaultDataDir());
244        }
245        return dataDir;
246    }
247
248    /**
249     * @return Log directory
250     * @since 5.4.2
251     */
252    public File getLogDir() {
253        if (logDir == null) {
254            logDir = new File(generator.getNuxeoHome(), org.nuxeo.common.Environment.DEFAULT_LOG_DIR);
255        }
256        return logDir;
257    }
258
259    /**
260     * @param dataDirStr Data directory path to set
261     * @since 5.4.2
262     */
263    public void setDataDir(String dataDirStr) {
264        dataDir = new File(dataDirStr);
265        dataDir.mkdirs();
266    }
267
268    /**
269     * @param logDirStr Log directory path to set
270     * @since 5.4.2
271     */
272    public void setLogDir(String logDirStr) {
273        logDir = new File(logDirStr);
274        logDir.mkdirs();
275    }
276
277    /**
278     * Initialize logs. This is called before {@link ConfigurationGenerator#init()} so the {@code logDir} field is not
279     * yet initialized
280     *
281     * @since 5.4.2
282     */
283    public void initLogs() {
284        File logFile = getLogConfFile();
285        String logDirectory = System.getProperty(org.nuxeo.common.Environment.NUXEO_LOG_DIR);
286        if (logDirectory == null) {
287            System.setProperty(org.nuxeo.common.Environment.NUXEO_LOG_DIR, getLogDir().getPath());
288        }
289        if (logFile == null || !logFile.exists()) {
290            System.out.println("No logs configuration, will setup a basic one.");
291            Configurator.initialize(new DefaultConfiguration());
292        } else {
293            System.out.println("Try to configure logs with " + logFile);
294            Configurator.initialize(Log4JHelper.newConfiguration(logFile));
295        }
296        log.info("Logs successfully configured.");
297    }
298
299    /**
300     * @return Pid directory (usually known as "run directory"); Returns log directory if not set by configuration.
301     * @since 5.4.2
302     */
303    public File getPidDir() {
304        if (pidDir == null) {
305            pidDir = getLogDir();
306        }
307        return pidDir;
308    }
309
310    /**
311     * @param pidDirStr Pid directory path to set
312     * @since 5.4.2
313     */
314    public void setPidDir(String pidDirStr) {
315        pidDir = new File(pidDirStr);
316        pidDir.mkdirs();
317    }
318
319    /**
320     * Check server paths; warn if existing deprecated paths. Override this method to perform server specific checks.
321     *
322     * @throws ConfigurationException If deprecated paths have been detected
323     * @since 5.4.2
324     */
325    public void checkPaths() throws ConfigurationException {
326        File badInstanceClid = new File(generator.getNuxeoHome(),
327                getDefaultDataDir() + File.separator + "instance.clid");
328        if (badInstanceClid.exists() && !getDataDir().equals(badInstanceClid.getParentFile())) {
329            log.warn(String.format("Moving %s to %s.", badInstanceClid, getDataDir()));
330            try {
331                FileUtils.moveFileToDirectory(badInstanceClid, getDataDir(), true);
332            } catch (IOException e) {
333                throw new ConfigurationException("NXP-6722 move failed: " + e.getMessage(), e);
334            }
335        }
336
337        File oldPackagesPath = new File(getDataDir(), getDefaultPackagesDir());
338        if (oldPackagesPath.exists() && !oldPackagesPath.equals(getPackagesDir())) {
339            log.warn(String.format(
340                    "NXP-8014 Packages cache location changed. You can safely delete %s or move its content to %s",
341                    oldPackagesPath, getPackagesDir()));
342        }
343
344    }
345
346    /**
347     * @return Temporary directory
348     * @since 5.4.2
349     */
350    public File getTmpDir() {
351        if (tmpDir == null) {
352            tmpDir = new File(generator.getNuxeoHome(), getDefaultTmpDir());
353        }
354        return tmpDir;
355    }
356
357    /**
358     * @return Default temporary directory path relative to Nuxeo Home
359     * @since 5.4.2
360     */
361    public String getDefaultTmpDir() {
362        return org.nuxeo.common.Environment.DEFAULT_TMP_DIR;
363    }
364
365    /**
366     * @param tmpDirStr Temporary directory path to set
367     * @since 5.4.2
368     */
369    public void setTmpDir(String tmpDirStr) {
370        tmpDir = new File(tmpDirStr);
371        tmpDir.mkdirs();
372    }
373
374    /**
375     * @see Environment
376     * @param key directory system key
377     * @param directory absolute or relative directory path
378     * @since 5.4.2
379     */
380    public void setDirectory(String key, String directory) {
381        String absoluteDirectory = setAbsolutePath(key, directory);
382        if (org.nuxeo.common.Environment.NUXEO_DATA_DIR.equals(key)) {
383            setDataDir(absoluteDirectory);
384        } else if (org.nuxeo.common.Environment.NUXEO_LOG_DIR.equals(key)) {
385            setLogDir(absoluteDirectory);
386        } else if (org.nuxeo.common.Environment.NUXEO_PID_DIR.equals(key)) {
387            setPidDir(absoluteDirectory);
388        } else if (org.nuxeo.common.Environment.NUXEO_TMP_DIR.equals(key)) {
389            setTmpDir(absoluteDirectory);
390        } else if (org.nuxeo.common.Environment.NUXEO_MP_DIR.equals(key)) {
391            setPackagesDir(absoluteDirectory);
392        } else {
393            log.error("Unknown directory key: " + key);
394        }
395    }
396
397    /**
398     * @since 5.9.4
399     */
400    private void setPackagesDir(String packagesDirStr) {
401        packagesDir = new File(packagesDirStr);
402        packagesDir.mkdirs();
403    }
404
405    /**
406     * Make absolute the directory passed in parameter. If it was relative, then store absolute path in user config
407     * instead of relative and return value
408     *
409     * @param key Directory system key
410     * @param directory absolute or relative directory path
411     * @return absolute directory path
412     * @since 5.4.2
413     */
414    private String setAbsolutePath(String key, String directory) {
415        if (!new File(directory).isAbsolute()) {
416            directory = new File(generator.getNuxeoHome(), directory).getPath();
417            generator.getUserConfig().setProperty(key, directory);
418        }
419        return directory;
420    }
421
422    /**
423     * @see Environment
424     * @param key directory system key
425     * @return Directory denoted by key
426     * @since 5.4.2
427     */
428    public File getDirectory(String key) {
429        if (org.nuxeo.common.Environment.NUXEO_DATA_DIR.equals(key)) {
430            return getDataDir();
431        } else if (org.nuxeo.common.Environment.NUXEO_LOG_DIR.equals(key)) {
432            return getLogDir();
433        } else if (org.nuxeo.common.Environment.NUXEO_PID_DIR.equals(key)) {
434            return getPidDir();
435        } else if (org.nuxeo.common.Environment.NUXEO_TMP_DIR.equals(key)) {
436            return getTmpDir();
437        } else if (org.nuxeo.common.Environment.NUXEO_MP_DIR.equals(key)) {
438            return getPackagesDir();
439        } else {
440            log.error("Unknown directory key: " + key);
441            return null;
442        }
443    }
444
445    /**
446     * Check if oldPath exist; if so, then raise a ConfigurationException with information for fixing issue
447     *
448     * @param oldPath Path that must NOT exist
449     * @param message Error message thrown with exception
450     * @throws ConfigurationException If an old path has been discovered
451     */
452    protected void checkPath(File oldPath, String message) throws ConfigurationException {
453        if (oldPath.exists()) {
454            log.error("Deprecated paths used.");
455            throw new ConfigurationException(message);
456        }
457    }
458
459    /**
460     * @return Log4J configuration file
461     * @since 5.4.2
462     */
463    public abstract File getLogConfFile();
464
465    /**
466     * @return Nuxeo config directory
467     * @since 5.4.2
468     */
469    public abstract File getConfigDir();
470
471    /**
472     * @since 5.4.2
473     */
474    public void prepareWizardStart() {
475        // Nothing to do by default
476    }
477
478    /**
479     * @since 5.4.2
480     */
481    public void cleanupPostWizard() {
482        // Nothing to do by default
483    }
484
485    /**
486     * Override it to make the wizard available for a given server.
487     *
488     * @return true if configuration wizard is required before starting Nuxeo
489     * @since 5.4.2
490     * @see #prepareWizardStart()
491     * @see #cleanupPostWizard()
492     */
493    public boolean isWizardAvailable() {
494        return false;
495    }
496
497    /**
498     * @param userConfig Properties to dump into config directory
499     * @since 5.4.2
500     */
501    public void dumpProperties(CryptoProperties userConfig) {
502        Properties dumpedProperties = filterSystemProperties(userConfig);
503        File dumpedFile = generator.getDumpedConfig();
504        try (OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(dumpedFile, false), "UTF-8")) {
505            dumpedProperties.store(os, "Generated by " + getClass());
506        } catch (FileNotFoundException e) {
507            log.error(e);
508        } catch (IOException e) {
509            log.error("Could not dump properties to " + dumpedFile, e);
510        }
511    }
512
513    /**
514     * Extract Nuxeo properties from given Properties (System properties are removed, except those set by Nuxeo)
515     *
516     * @param properties Properties to be filtered
517     * @return copy of given properties filtered out of System properties
518     * @since 5.4.2
519     */
520    public Properties filterSystemProperties(CryptoProperties properties) {
521        Properties dumpedProperties = new Properties();
522        for (@SuppressWarnings("unchecked")
523        Enumeration<String> propertyNames = (Enumeration<String>) properties.propertyNames(); propertyNames.hasMoreElements();) {
524            String key = propertyNames.nextElement();
525            // Exclude System properties except Nuxeo's System properties
526            if (!System.getProperties().containsKey(key) || NUXEO_SYSTEM_PROPERTIES.contains(key)) {
527                dumpedProperties.setProperty(key, properties.getRawProperty(key));
528            }
529        }
530        return dumpedProperties;
531    }
532
533    /**
534     * @return Nuxeo's third party libraries directory
535     * @since 5.4.1
536     */
537    public File getNuxeoLibDir() {
538        return new File(getRuntimeHome(), "lib");
539    }
540
541    /**
542     * @return Server's third party libraries directory
543     * @since 5.4.1
544     */
545    public abstract File getServerLibDir();
546
547    /**
548     * @since 5.7
549     */
550    public void verifyInstallation() throws ConfigurationException {
551        checkPaths();
552        checkNetwork();
553    }
554
555    /**
556     * Perform server specific checks, not already done by {@link ConfigurationGenerator#checkAddressesAndPorts()}
557     *
558     * @since 5.7
559     * @see ConfigurationGenerator#checkAddressesAndPorts()
560     */
561    protected void checkNetwork() throws ConfigurationException {
562    }
563
564    /**
565     * Override to add server specific parameters to the list of parameters to migrate
566     *
567     * @since 5.7
568     */
569    protected void addServerSpecificParameters(Map<String, String> parametersMigration) {
570        // Nothing to do
571    }
572
573    /**
574     * @return Marketplace Packages directory
575     * @since 5.9.4
576     */
577    public File getPackagesDir() {
578        if (packagesDir == null) {
579            packagesDir = new File(generator.getNuxeoHome(), getDefaultPackagesDir());
580        }
581        return packagesDir;
582    }
583
584    /**
585     * @return Default MP directory path relative to Nuxeo Home
586     * @since 5.9.4
587     */
588    public String getDefaultPackagesDir() {
589        return org.nuxeo.common.Environment.DEFAULT_MP_DIR;
590    }
591
592    /**
593     * Introspect the server and builds the instance info
594     *
595     * @since 8.3
596     */
597    public InstanceInfo getInfo(String clid, List<LocalPackage> pkgs) {
598        InstanceInfo nxInstance = new InstanceInfo();
599        nxInstance.NUXEO_CONF = generator.getNuxeoConf().getPath();
600        nxInstance.NUXEO_HOME = generator.getNuxeoHome().getPath();
601        // distribution
602        File distFile = new File(generator.getConfigDir(), "distribution.properties");
603        if (!distFile.exists()) {
604            // fallback in the file in templates
605            distFile = new File(generator.getNuxeoHome(), "templates");
606            distFile = new File(distFile, "common");
607            distFile = new File(distFile, "config");
608            distFile = new File(distFile, "distribution.properties");
609        }
610        try {
611            nxInstance.distribution = new DistributionInfo(distFile);
612        } catch (IOException e) {
613            nxInstance.distribution = new DistributionInfo();
614        }
615        // packages
616        nxInstance.clid = clid;
617        Set<String> pkgTemplates = new HashSet<>();
618        for (LocalPackage pkg : pkgs) {
619            final PackageInfo info = new PackageInfo(pkg);
620            nxInstance.packages.add(info);
621            pkgTemplates.addAll(info.templates);
622        }
623        // templates
624        nxInstance.config = new ConfigurationInfo();
625        nxInstance.config.dbtemplate = generator.extractDatabaseTemplateName();
626        String userTemplates = generator.getUserTemplates();
627        StringTokenizer st = new StringTokenizer(userTemplates, ",");
628        while (st.hasMoreTokens()) {
629            String template = st.nextToken();
630            if (template.equals(nxInstance.config.dbtemplate)) {
631                continue;
632            }
633            if (pkgTemplates.contains(template)) {
634                nxInstance.config.pkgtemplates.add(template);
635            } else {
636                File testBase = new File(generator.getNuxeoHome(),
637                        ConfigurationGenerator.TEMPLATES + File.separator + template);
638                if (testBase.exists()) {
639                    nxInstance.config.basetemplates.add(template);
640                } else {
641                    nxInstance.config.usertemplates.add(template);
642                }
643            }
644        }
645        // Settings from nuxeo.conf
646        CryptoProperties userConfig = generator.getUserConfig();
647        for (Object item : new TreeSet<>(userConfig.keySet())) {
648            String key = (String) item;
649            String value = userConfig.getRawProperty(key);
650            if (JAVA_OPTS.equals(key)) {
651                value = generator.getJavaOptsString();
652            }
653            if (ConfigurationGenerator.SECRET_KEYS.contains(key) || key.contains("password")
654                    || key.equals(Environment.SERVER_STATUS_KEY) || Crypto.isEncrypted(value)) {
655                value = "********";
656            }
657            nxInstance.config.keyvals.add(new KeyValueInfo(key, value));
658        }
659        return nxInstance;
660    }
661}