001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 * 
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.configuration;
029
030import org.opencms.i18n.CmsEncoder;
031import org.opencms.main.CmsLog;
032import org.opencms.util.CmsFileUtil;
033import org.opencms.xml.CmsXmlEntityResolver;
034import org.opencms.xml.CmsXmlErrorHandler;
035
036import java.io.File;
037import java.io.FileOutputStream;
038import java.io.IOException;
039import java.io.OutputStream;
040import java.net.URL;
041import java.text.SimpleDateFormat;
042import java.util.ArrayList;
043import java.util.Date;
044import java.util.Iterator;
045import java.util.List;
046
047import org.apache.commons.digester.Digester;
048import org.apache.commons.logging.Log;
049
050import org.dom4j.Document;
051import org.dom4j.DocumentHelper;
052import org.dom4j.Element;
053import org.dom4j.dom.DOMDocumentType;
054import org.dom4j.io.OutputFormat;
055import org.dom4j.io.XMLWriter;
056import org.xml.sax.SAXException;
057
058/**
059 * Configuration manager for digesting the OpenCms XML configuration.<p>
060 * 
061 * Reads the individual configuration class nodes first and creaes new 
062 * instances of the "base" configuration classes.<p>
063 * 
064 * @since 6.0.0
065 */
066public class CmsConfigurationManager implements I_CmsXmlConfiguration {
067
068    /** The location of the OpenCms configuration DTD if the default prefix is the system ID. */
069    public static final String DEFAULT_DTD_LOCATION = "org/opencms/configuration/";
070
071    /** The default prefix for the OpenCms configuration DTD. */
072    public static final String DEFAULT_DTD_PREFIX = "http://www.opencms.org/dtd/6.0/";
073
074    /** The name of the default XML file for this configuration. */
075    public static final String DEFAULT_XML_FILE_NAME = "opencms.xml";
076
077    /** The name of the DTD file for this configuration. */
078    public static final String DTD_FILE_NAME = "opencms-configuration.dtd";
079
080    /** The "opencms" root node of the XML configuration. */
081    public static final String N_ROOT = "opencms";
082
083    /** Postfix for original configuration files. */
084    public static final String POSTFIX_ORI = ".ori";
085
086    /** The config node. */
087    protected static final String N_CONFIG = "config";
088
089    /** The configurations node. */
090    protected static final String N_CONFIGURATION = "configuration";
091
092    /** Date format for the backup file time prefix. */
093    private static final SimpleDateFormat BACKUP_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_");
094
095    /** The log object for this class. */
096    private static final Log LOG = CmsLog.getLog(CmsConfigurationManager.class);
097
098    /** The number of days to keep old backups for. */
099    private static final long MAX_BACKUP_DAYS = 15;
100
101    /** The folder where to store the backup files of the configuration. */
102    private File m_backupFolder;
103
104    /** The base folder where the configuration files are located. */
105    private File m_baseFolder;
106
107    /** The initialized configuration classes. */
108    private List<I_CmsXmlConfiguration> m_configurations;
109
110    /** The digester for reading the XML configuration. */
111    private Digester m_digester;
112
113    /** The configuration based on <code>opencms.properties</code>. */
114    private CmsParameterConfiguration m_propertyConfiguration;
115
116    /**
117     * Creates a new OpenCms configuration manager.<p>
118     * 
119     * @param baseFolder base folder where XML configurations to load are located
120     */
121    public CmsConfigurationManager(String baseFolder) {
122
123        m_baseFolder = new File(baseFolder);
124        if (!m_baseFolder.exists()) {
125            if (LOG.isErrorEnabled()) {
126                LOG.error(Messages.get().getBundle().key(
127                    Messages.LOG_INVALID_CONFIG_BASE_FOLDER_1,
128                    m_baseFolder.getAbsolutePath()));
129            }
130        }
131        m_backupFolder = new File(m_baseFolder.getAbsolutePath() + File.separatorChar + "backup");
132        if (!m_backupFolder.exists()) {
133            if (LOG.isDebugEnabled()) {
134                LOG.debug(Messages.get().getBundle().key(
135                    Messages.LOG_CREATE_CONFIG_BKP_FOLDER_1,
136                    m_backupFolder.getAbsolutePath()));
137            }
138            m_backupFolder.mkdirs();
139        }
140        if (LOG.isDebugEnabled()) {
141            LOG.debug(Messages.get().getBundle().key(Messages.LOG_CONFIG_BASE_FOLDER_1, m_baseFolder.getAbsolutePath()));
142            LOG.debug(Messages.get().getBundle().key(Messages.LOG_CONFIG_BKP_FOLDER_1, m_backupFolder.getAbsolutePath()));
143        }
144        cacheDtdSystemId(this);
145        m_configurations = new ArrayList<I_CmsXmlConfiguration>();
146    }
147
148    /**
149     * Adds a configuration object to the configuration manager.<p>
150     * 
151     * @param configuration the configuration to add
152     */
153    public void addConfiguration(I_CmsXmlConfiguration configuration) {
154
155        if (LOG.isDebugEnabled()) {
156            LOG.debug(Messages.get().getBundle().key(Messages.LOG_ADD_CONFIG_1, configuration));
157        }
158        m_configurations.add(configuration);
159        cacheDtdSystemId(configuration);
160    }
161
162    /**
163     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String)
164     */
165    public void addConfigurationParameter(String paramName, String paramValue) {
166
167        // noop, this configuration has no additional parameters
168    }
169
170    /**
171     * @see org.opencms.configuration.I_CmsXmlConfiguration#addXmlDigesterRules(org.apache.commons.digester.Digester)
172     */
173    public void addXmlDigesterRules(Digester digester) {
174
175        // add rule for <configuration> node        
176        digester.addObjectCreate(
177            "*/" + N_CONFIGURATION + "/" + N_CONFIG,
178            I_CmsXmlConfiguration.A_CLASS,
179            CmsConfigurationException.class);
180        digester.addSetNext("*/" + N_CONFIGURATION + "/" + N_CONFIG, "addConfiguration");
181    }
182
183    /**
184     * @see org.opencms.configuration.I_CmsXmlConfiguration#generateXml(org.dom4j.Element)
185     */
186    public Element generateXml(Element parent) {
187
188        // add the <configuration> node
189        Element configurationElement = parent.addElement(N_CONFIGURATION);
190        for (int i = 0; i < m_configurations.size(); i++) {
191            // append the individual configuration 
192            I_CmsXmlConfiguration configuration = m_configurations.get(i);
193            configurationElement.addElement(N_CONFIG).addAttribute(
194                I_CmsXmlConfiguration.A_CLASS,
195                configuration.getClass().getName());
196        }
197        return parent;
198    }
199
200    /**
201     * Creates the XML document build from the provided configuration.<p>
202     * 
203     * @param configuration the configuration to build the XML for
204     * @return the XML document build from the provided configuration
205     */
206    public Document generateXml(I_CmsXmlConfiguration configuration) {
207
208        // create a new document
209        Document result = DocumentHelper.createDocument();
210
211        // set the document type        
212        DOMDocumentType docType = new DOMDocumentType();
213        docType.setElementName(N_ROOT);
214        docType.setSystemID(configuration.getDtdUrlPrefix() + configuration.getDtdFilename());
215        result.setDocType(docType);
216
217        Element root = result.addElement(N_ROOT);
218        // start the XML generation
219        configuration.generateXml(root);
220
221        // return the resulting document
222        return result;
223    }
224
225    /**
226     * Returns the backup folder.<p>
227     *
228     * @return the backup folder
229     */
230    public File getBackupFolder() {
231
232        return m_backupFolder;
233    }
234
235    /**
236     * Returns the properties read from <code>opencms.properties</code>.<p> 
237     * 
238     * @see #setConfiguration(CmsParameterConfiguration)
239     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration()
240     */
241    public CmsParameterConfiguration getConfiguration() {
242
243        return m_propertyConfiguration;
244    }
245
246    /**
247     * Returns a specific configuration from the list of initialized configurations.<p>
248     * 
249     * @param clazz the configuration class that should be returned
250     * @return the initialized configuration class instance, or <code>null</code> if this is not found
251     */
252    public I_CmsXmlConfiguration getConfiguration(Class<?> clazz) {
253
254        for (int i = 0; i < m_configurations.size(); i++) {
255            I_CmsXmlConfiguration configuration = m_configurations.get(i);
256            if (clazz.equals(configuration.getClass())) {
257                return configuration;
258            }
259        }
260        return null;
261    }
262
263    /**
264     * Returns the list of all initialized configurations.<p>
265     * 
266     * @return the list of all initialized configurations
267     */
268    public List<I_CmsXmlConfiguration> getConfigurations() {
269
270        return m_configurations;
271    }
272
273    /**
274     * @see org.opencms.configuration.I_CmsXmlConfiguration#getDtdFilename()
275     */
276    public String getDtdFilename() {
277
278        return DTD_FILE_NAME;
279    }
280
281    /**
282     * @see org.opencms.configuration.I_CmsXmlConfiguration#getDtdSystemLocation()
283     */
284    public String getDtdSystemLocation() {
285
286        return DEFAULT_DTD_LOCATION;
287    }
288
289    /**
290     * @see org.opencms.configuration.I_CmsXmlConfiguration#getDtdUrlPrefix()
291     */
292    public String getDtdUrlPrefix() {
293
294        return DEFAULT_DTD_PREFIX;
295    }
296
297    /**
298     * @see org.opencms.configuration.I_CmsXmlConfiguration#getXmlFileName()
299     */
300    public String getXmlFileName() {
301
302        return DEFAULT_XML_FILE_NAME;
303    }
304
305    /**
306     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration()
307     */
308    public void initConfiguration() {
309
310        // does not need to be initialized
311        if (LOG.isDebugEnabled()) {
312            LOG.debug(Messages.get().getBundle().key(Messages.LOG_INIT_CONFIGURATION_1, this));
313        }
314    }
315
316    /**
317     * Loads the OpenCms configuration from the given XML file.<p>
318     * 
319     * @throws SAXException in case of XML parse errors
320     * @throws IOException in case of file IO errors
321     */
322    public void loadXmlConfiguration() throws SAXException, IOException {
323
324        URL baseUrl = m_baseFolder.toURI().toURL();
325        if (LOG.isDebugEnabled()) {
326            LOG.debug(Messages.get().getBundle().key(Messages.LOG_BASE_URL_1, baseUrl));
327        }
328
329        // first load the base configuration
330        loadXmlConfiguration(baseUrl, this);
331
332        // now iterate all sub-configurations
333        Iterator<I_CmsXmlConfiguration> i = m_configurations.iterator();
334        while (i.hasNext()) {
335            loadXmlConfiguration(baseUrl, i.next());
336        }
337
338        // remove the old backups
339        removeOldBackups(MAX_BACKUP_DAYS);
340    }
341
342    /**
343     * Sets the configuration read from the <code>opencms.properties</code>.<p>
344     * 
345     * @param propertyConfiguration the configuration read from the <code>opencms.properties</code>
346     * 
347     * @see #getConfiguration()
348     */
349    public void setConfiguration(CmsParameterConfiguration propertyConfiguration) {
350
351        m_propertyConfiguration = propertyConfiguration;
352    }
353
354    /**
355     * Writes the XML configuration for the provided configuration instance.<p>
356     * 
357     * @param clazz the configuration class to write the XML for
358     * @throws IOException in case of I/O errors while writing
359     * @throws CmsConfigurationException if the given class is not a valid configuration class
360     */
361    public void writeConfiguration(Class<?> clazz) throws IOException, CmsConfigurationException {
362
363        I_CmsXmlConfiguration configuration = getConfiguration(clazz);
364        if (configuration == null) {
365            throw new CmsConfigurationException(Messages.get().container(
366                Messages.ERR_CONFIG_WITH_UNKNOWN_CLASS_1,
367                clazz.getName()));
368        }
369
370        // generate the file URL for the XML input
371        File file = new File(m_baseFolder, configuration.getXmlFileName());
372        if (LOG.isDebugEnabled()) {
373            LOG.debug(Messages.get().getBundle().key(Messages.LOG_WRITE_CONFIG_XMLFILE_1, file.getAbsolutePath()));
374        }
375
376        // generate the XML document 
377        Document config = generateXml(configuration);
378
379        // output the document
380        XMLWriter writer = null;
381        OutputFormat format = OutputFormat.createPrettyPrint();
382        format.setIndentSize(4);
383        format.setTrimText(false);
384        format.setEncoding(CmsEncoder.ENCODING_UTF_8);
385
386        try {
387            OutputStream out = new FileOutputStream(file);
388            writer = new XMLWriter(out, format);
389            writer.write(config);
390            writer.flush();
391        } finally {
392            if (writer != null) {
393                writer.close();
394            }
395        }
396
397        if (LOG.isInfoEnabled()) {
398            LOG.info(Messages.get().getBundle().key(
399                Messages.LOG_WRITE_CONFIG_SUCCESS_2,
400                file.getAbsolutePath(),
401                configuration.getClass().getName()));
402        }
403    }
404
405    /**
406     * Creates a backup of the given XML configurations input file.<p>
407     * 
408     * @param configuration the configuration for which the input file should be backed up
409     */
410    private void backupXmlConfiguration(I_CmsXmlConfiguration configuration) {
411
412        String fromName = m_baseFolder.getAbsolutePath() + File.separatorChar + configuration.getXmlFileName();
413        String toDatePrefix = BACKUP_DATE_FORMAT.format(new Date());
414        String toName = m_backupFolder.getAbsolutePath()
415            + File.separatorChar
416            + toDatePrefix
417            + configuration.getXmlFileName();
418
419        if (LOG.isDebugEnabled()) {
420            LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_CONFIG_BKP_2, fromName, toName));
421        }
422
423        try {
424            CmsFileUtil.copy(fromName, toName);
425        } catch (IOException e) {
426            LOG.error(Messages.get().getBundle().key(Messages.LOG_CREATE_CONFIG_BKP_FAILURE_1, toName), e);
427        }
428    }
429
430    /**
431     * Adds a new DTD system id prefix mapping for internal resolution of external URLs.<p>
432     * 
433     * @param configuration the configuration to add the mapping from
434     */
435    private void cacheDtdSystemId(I_CmsXmlConfiguration configuration) {
436
437        if (configuration.getDtdSystemLocation() != null) {
438            try {
439                String file = CmsFileUtil.readFile(
440                    configuration.getDtdSystemLocation() + configuration.getDtdFilename(),
441                    CmsEncoder.ENCODING_UTF_8);
442                CmsXmlEntityResolver.cacheSystemId(
443                    configuration.getDtdUrlPrefix() + configuration.getDtdFilename(),
444                    file.getBytes(CmsEncoder.ENCODING_UTF_8));
445                if (LOG.isDebugEnabled()) {
446                    LOG.debug(Messages.get().getBundle().key(
447                        Messages.LOG_CACHE_DTD_SYSTEM_ID_1,
448                        configuration.getDtdUrlPrefix()
449                            + configuration.getDtdFilename()
450                            + " --> "
451                            + configuration.getDtdSystemLocation()
452                            + configuration.getDtdFilename()));
453                }
454            } catch (IOException e) {
455                LOG.error(
456                    Messages.get().getBundle().key(
457                        Messages.LOG_CACHE_DTD_SYSTEM_ID_FAILURE_1,
458                        configuration.getDtdSystemLocation() + configuration.getDtdFilename()),
459                    e);
460            }
461        }
462    }
463
464    /**
465     * Loads the OpenCms configuration from the given XML URL.<p>
466     * 
467     * @param url the base URL of the XML configuration to load
468     * @param configuration the configuration to load
469     * @throws SAXException in case of XML parse errors
470     * @throws IOException in case of file IO errors
471     */
472    private void loadXmlConfiguration(URL url, I_CmsXmlConfiguration configuration) throws SAXException, IOException {
473
474        // generate the file URL for the XML input
475        URL fileUrl = new URL(url, configuration.getXmlFileName());
476        if (LOG.isDebugEnabled()) {
477            LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOAD_CONFIG_XMLFILE_1, fileUrl));
478        }
479
480        // create a backup of the configuration
481        backupXmlConfiguration(configuration);
482
483        // instantiate Digester and enable XML validation
484        m_digester = new Digester();
485        m_digester.setUseContextClassLoader(true);
486        m_digester.setValidating(true);
487        m_digester.setEntityResolver(new CmsXmlEntityResolver(null));
488        m_digester.setRuleNamespaceURI(null);
489        m_digester.setErrorHandler(new CmsXmlErrorHandler(fileUrl.getFile()));
490
491        // add this class to the Digester
492        m_digester.push(configuration);
493
494        configuration.addXmlDigesterRules(m_digester);
495
496        // start the parsing process        
497        m_digester.parse(fileUrl.openStream());
498    }
499
500    /**
501     * Removes all backups that are older then the given number of days.<p>
502     * 
503     * @param daysToKeep the days to keep the backups for
504     */
505    private void removeOldBackups(long daysToKeep) {
506
507        long maxAge = (System.currentTimeMillis() - (daysToKeep * 24 * 60 * 60 * 1000));
508        File[] files = m_backupFolder.listFiles();
509        for (int i = 0; i < files.length; i++) {
510            File file = files[i];
511            long lastMod = file.lastModified();
512            if ((lastMod < maxAge) & (!file.getAbsolutePath().endsWith(CmsConfigurationManager.POSTFIX_ORI))) {
513                file.delete();
514                if (LOG.isDebugEnabled()) {
515                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_REMOVE_CONFIG_FILE_1, file.getAbsolutePath()));
516                }
517            }
518        }
519    }
520}