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