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