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