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