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.FileNotFoundException;
025import java.io.FileOutputStream;
026import java.io.FileReader;
027import java.io.FileWriter;
028import java.io.FilenameFilter;
029import java.io.IOException;
030import java.io.OutputStream;
031import java.net.MalformedURLException;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Enumeration;
035import java.util.List;
036import java.util.Map;
037import java.util.Properties;
038
039import org.apache.commons.io.FileUtils;
040import org.apache.commons.io.IOUtils;
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.apache.log4j.BasicConfigurator;
044import org.apache.log4j.xml.DOMConfigurator;
045
046import org.nuxeo.common.codec.CryptoProperties;
047import org.nuxeo.common.utils.TextTemplate;
048
049import freemarker.template.TemplateException;
050
051/**
052 * @author jcarsique
053 */
054public abstract class ServerConfigurator {
055
056    protected static final Log log = LogFactory.getLog(ServerConfigurator.class);
057
058    protected final ConfigurationGenerator generator;
059
060    protected File dataDir = null;
061
062    protected File logDir = null;
063
064    protected File pidDir = null;
065
066    protected File libDir = null;
067
068    protected File tmpDir = null;
069
070    protected File packagesDir = null;
071
072    /**
073     * @since 5.4.2
074     */
075    public static final List<String> NUXEO_SYSTEM_PROPERTIES = Arrays.asList(new String[] { "nuxeo.conf", "nuxeo.home",
076            "log.id" });
077
078    protected static final String DEFAULT_CONTEXT_NAME = "/nuxeo";
079
080    private static final String NEW_FILES = ConfigurationGenerator.TEMPLATES + File.separator + "files.list";
081
082    /**
083     * @since 5.4.2
084     * @deprecated Since 5.9.4. Use {@link org.nuxeo.common.Environment#DEFAULT_LOG_DIR} instead.
085     */
086    @Deprecated
087    public static final String DEFAULT_LOG_DIR = org.nuxeo.common.Environment.DEFAULT_LOG_DIR;
088
089    /**
090     * @deprecated Since 5.9.4. Use {@link org.nuxeo.common.Environment#DEFAULT_DATA_DIR} instead.
091     */
092    @Deprecated
093    public static final String DEFAULT_DATA_DIR = org.nuxeo.common.Environment.DEFAULT_DATA_DIR;
094
095    /**
096     * @since 5.4.2
097     * @deprecated Since 5.9.4. Use {@link org.nuxeo.common.Environment#DEFAULT_TMP_DIR} instead.
098     */
099    @Deprecated
100    public static final String DEFAULT_TMP_DIR = org.nuxeo.common.Environment.DEFAULT_TMP_DIR;
101
102    public ServerConfigurator(ConfigurationGenerator configurationGenerator) {
103        generator = configurationGenerator;
104    }
105
106    /**
107     * @return true if server configuration files already exist
108     */
109    abstract boolean isConfigured();
110
111    /**
112     * Generate configuration files from templates and given configuration parameters
113     *
114     * @param config Properties with configuration parameters for template replacement
115     * @throws ConfigurationException
116     */
117    protected void parseAndCopy(Properties config) throws IOException, TemplateException, ConfigurationException {
118        // FilenameFilter for excluding "nuxeo.defaults" files from copy
119        final FilenameFilter filter = new FilenameFilter() {
120            @Override
121            public boolean accept(File dir, String name) {
122                return !ConfigurationGenerator.NUXEO_DEFAULT_CONF.equals(name);
123            }
124        };
125        final TextTemplate templateParser = new TextTemplate(config);
126        templateParser.setKeepEncryptedAsVar(true);
127        templateParser.setTrim(true);
128        templateParser.setTextParsingExtensions(config.getProperty(
129                ConfigurationGenerator.PARAM_TEMPLATES_PARSING_EXTENSIONS, "xml,properties,nx"));
130        templateParser.setFreemarkerParsingExtensions(config.getProperty(
131                ConfigurationGenerator.PARAM_TEMPLATES_FREEMARKER_EXTENSIONS, "nxftl"));
132
133        deleteTemplateFiles();
134        // add included templates directories
135        List<String> newFilesList = new ArrayList<>();
136        for (File includedTemplate : generator.getIncludedTemplates()) {
137            File[] listFiles = includedTemplate.listFiles(filter);
138            if (listFiles != null) {
139                String templateName = includedTemplate.getName();
140                log.debug(String.format("Parsing %s... %s", templateName, Arrays.toString(listFiles)));
141                // Check for deprecation
142                Boolean isDeprecated = Boolean.valueOf(config.getProperty(templateName + ".deprecated"));
143                if (isDeprecated) {
144                    log.warn("WARNING: Template " + templateName + " is deprecated.");
145                    String deprecationMessage = config.getProperty(templateName + ".deprecation");
146                    if (deprecationMessage != null) {
147                        log.warn(deprecationMessage);
148                    }
149                }
150                // Retrieve optional target directory if defined
151                String outputDirectoryStr = config.getProperty(templateName + ".target");
152                File outputDirectory = (outputDirectoryStr != null) ? new File(generator.getNuxeoHome(),
153                        outputDirectoryStr) : getOutputDirectory();
154                for (File in : listFiles) {
155                    // copy template(s) directories parsing properties
156                    newFilesList.addAll(templateParser.processDirectory(in, new File(outputDirectory, in.getName())));
157                }
158            }
159        }
160        storeNewFilesList(newFilesList);
161    }
162
163    /**
164     * Delete files previously deployed by templates. If a file had been overwritten by a template, it will be restored.
165     * Helps the server returning to the state before any template was applied.
166     *
167     * @throws IOException
168     * @throws ConfigurationException
169     */
170    private void deleteTemplateFiles() throws IOException, ConfigurationException {
171        File newFiles = new File(generator.getNuxeoHome(), NEW_FILES);
172        if (!newFiles.exists()) {
173            return;
174        }
175        BufferedReader reader = null;
176        try {
177            reader = new BufferedReader(new FileReader(newFiles));
178            String line;
179            while ((line = reader.readLine()) != null) {
180                if (line.endsWith(".bak")) {
181                    log.debug("Restore " + line);
182                    try {
183                        File backup = new File(generator.getNuxeoHome(), line);
184                        File original = new File(generator.getNuxeoHome(), line.substring(0, line.length() - 4));
185                        FileUtils.copyFile(backup, original);
186                        backup.delete();
187                    } catch (IOException e) {
188                        throw new ConfigurationException(String.format("Failed to restore %s from %s\nEdit or "
189                                + "delete %s to bypass that error.", line.substring(0, line.length() - 4), line,
190                                newFiles), e);
191                    }
192                } else {
193                    log.debug("Remove " + line);
194                    new File(generator.getNuxeoHome(), line).delete();
195                }
196            }
197        } finally {
198            IOUtils.closeQuietly(reader);
199        }
200        newFiles.delete();
201    }
202
203    /**
204     * Store into {@link #NEW_FILES} the list of new files deployed by the templates. For later use by
205     * {@link #deleteTemplateFiles()}
206     *
207     * @param newFilesList
208     * @throws IOException
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 FileWriter(newFiles, false));
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     * @param absoluteDirectory
412     * @since 5.9.4
413     */
414    private void setPackagesDir(String packagesDirStr) {
415        packagesDir = new File(packagesDirStr);
416        packagesDir.mkdirs();
417    }
418
419    /**
420     * Make absolute the directory passed in parameter. If it was relative, then store absolute path in user config
421     * instead of relative and return value
422     *
423     * @param key Directory system key
424     * @param directory absolute or relative directory path
425     * @return absolute directory path
426     * @since 5.4.2
427     */
428    private String setAbsolutePath(String key, String directory) {
429        if (!new File(directory).isAbsolute()) {
430            directory = new File(generator.getNuxeoHome(), directory).getPath();
431            generator.getUserConfig().setProperty(key, directory);
432        }
433        return directory;
434    }
435
436    /**
437     * @see Environment
438     * @param key directory system key
439     * @return Directory denoted by key
440     * @since 5.4.2
441     */
442    public File getDirectory(String key) {
443        if (org.nuxeo.common.Environment.NUXEO_DATA_DIR.equals(key)) {
444            return getDataDir();
445        } else if (org.nuxeo.common.Environment.NUXEO_LOG_DIR.equals(key)) {
446            return getLogDir();
447        } else if (org.nuxeo.common.Environment.NUXEO_PID_DIR.equals(key)) {
448            return getPidDir();
449        } else if (org.nuxeo.common.Environment.NUXEO_TMP_DIR.equals(key)) {
450            return getTmpDir();
451        } else if (org.nuxeo.common.Environment.NUXEO_MP_DIR.equals(key)) {
452            return getPackagesDir();
453        } else {
454            log.error("Unknown directory key: " + key);
455            return null;
456        }
457    }
458
459    /**
460     * Check if oldPath exist; if so, then raise a ConfigurationException with information for fixing issue
461     *
462     * @param oldPath Path that must NOT exist
463     * @param message Error message thrown with exception
464     * @throws ConfigurationException If an old path has been discovered
465     */
466    protected void checkPath(File oldPath, String message) throws ConfigurationException {
467        if (oldPath.exists()) {
468            log.error("Deprecated paths used.");
469            throw new ConfigurationException(message);
470        }
471    }
472
473    /**
474     * @return Log4J configuration file
475     * @since 5.4.2
476     */
477    public abstract File getLogConfFile();
478
479    /**
480     * @return Nuxeo config directory
481     * @since 5.4.2
482     */
483    public abstract File getConfigDir();
484
485    /**
486     * @since 5.4.2
487     */
488    public void prepareWizardStart() {
489        // Nothing to do by default
490    }
491
492    /**
493     * @since 5.4.2
494     */
495    public void cleanupPostWizard() {
496        // Nothing to do by default
497    }
498
499    /**
500     * Override it to make the wizard available for a given server.
501     *
502     * @return true if configuration wizard is required before starting Nuxeo
503     * @since 5.4.2
504     * @see #prepareWizardStart()
505     * @see #cleanupPostWizard()
506     */
507    public boolean isWizardAvailable() {
508        return false;
509    }
510
511    /**
512     * @param userConfig Properties to dump into config directory
513     * @since 5.4.2
514     */
515    public void dumpProperties(CryptoProperties userConfig) {
516        Properties dumpedProperties = filterSystemProperties(userConfig);
517        File dumpedFile = generator.getDumpedConfig();
518        OutputStream os = null;
519        try {
520            os = new FileOutputStream(dumpedFile, false);
521            dumpedProperties.store(os, "Generated by " + getClass());
522        } catch (FileNotFoundException e) {
523            log.error(e);
524        } catch (IOException e) {
525            log.error("Could not dump properties to " + dumpedFile, e);
526        } finally {
527            IOUtils.closeQuietly(os);
528        }
529    }
530
531    /**
532     * Extract Nuxeo properties from given Properties (System properties are removed, except those set by Nuxeo)
533     *
534     * @param properties Properties to be filtered
535     * @return copy of given properties filtered out of System properties
536     * @since 5.4.2
537     */
538    public Properties filterSystemProperties(CryptoProperties properties) {
539        Properties dumpedProperties = new Properties();
540        for (@SuppressWarnings("unchecked")
541        Enumeration<String> propertyNames = (Enumeration<String>) properties.propertyNames(); propertyNames.hasMoreElements();) {
542            String key = propertyNames.nextElement();
543            // Exclude System properties except Nuxeo's System properties
544            if (!System.getProperties().containsKey(key) || NUXEO_SYSTEM_PROPERTIES.contains(key)) {
545                dumpedProperties.setProperty(key, properties.getRawProperty(key));
546            }
547        }
548        return dumpedProperties;
549    }
550
551    /**
552     * @return Nuxeo's third party libraries directory
553     * @since 5.4.1
554     */
555    public File getNuxeoLibDir() {
556        return new File(getRuntimeHome(), "lib");
557    }
558
559    /**
560     * @return Server's third party libraries directory
561     * @since 5.4.1
562     */
563    public abstract File getServerLibDir();
564
565    /**
566     * @throws ConfigurationException
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     * @throws ConfigurationException
578     * @since 5.7
579     * @see ConfigurationGenerator#checkAddressesAndPorts()
580     */
581    protected void checkNetwork() throws ConfigurationException {
582    }
583
584    /**
585     * Override to add server specific parameters to the list of parameters to migrate
586     *
587     * @param parametersmigration
588     * @since 5.7
589     */
590    protected void addServerSpecificParameters(Map<String, String> parametersmigration) {
591        // Nothing to do
592    }
593
594    /**
595     * @return Marketplace Packages directory
596     * @since 5.9.4
597     */
598    public File getPackagesDir() {
599        if (packagesDir == null) {
600            packagesDir = new File(generator.getNuxeoHome(), getDefaultPackagesDir());
601        }
602        return packagesDir;
603    }
604
605    /**
606     * @return Default MP directory path relative to Nuxeo Home
607     * @since 5.9.4
608     */
609    public String getDefaultPackagesDir() {
610        return org.nuxeo.common.Environment.DEFAULT_MP_DIR;
611    }
612
613}