001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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, 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.setup.xml;
029
030import org.opencms.json.JSONException;
031import org.opencms.json.JSONObject;
032import org.opencms.util.CmsFileUtil;
033import org.opencms.util.CmsStringUtil;
034import org.opencms.xml.CmsXmlEntityResolver;
035import org.opencms.xml.CmsXmlException;
036import org.opencms.xml.CmsXmlUtils;
037
038import java.io.ByteArrayInputStream;
039import java.io.ByteArrayOutputStream;
040import java.io.File;
041import java.io.FileInputStream;
042import java.io.FileOutputStream;
043import java.io.IOException;
044import java.io.InputStream;
045import java.io.StringReader;
046import java.util.ArrayList;
047import java.util.List;
048
049import javax.xml.parsers.ParserConfigurationException;
050import javax.xml.parsers.SAXParserFactory;
051import javax.xml.transform.OutputKeys;
052import javax.xml.transform.Result;
053import javax.xml.transform.Source;
054import javax.xml.transform.Transformer;
055import javax.xml.transform.TransformerConfigurationException;
056import javax.xml.transform.TransformerException;
057import javax.xml.transform.TransformerFactory;
058import javax.xml.transform.URIResolver;
059import javax.xml.transform.sax.SAXSource;
060import javax.xml.transform.stream.StreamResult;
061import javax.xml.transform.stream.StreamSource;
062
063import org.apache.xml.utils.SystemIDResolver;
064
065import org.dom4j.Document;
066import org.dom4j.Element;
067import org.dom4j.Node;
068import org.xml.sax.EntityResolver;
069import org.xml.sax.InputSource;
070import org.xml.sax.SAXException;
071import org.xml.sax.XMLReader;
072
073/**
074 * Class for updating the XML configuration files using a set of XSLT transforms.
075 *
076 * The XSLT transforms are stored in the directory update/xmlupdate, together with a file transforms.xml
077 * that contains the list of transformation files and the configuration files to which they should be applied to.
078 *
079 */
080public class CmsXmlConfigUpdater {
081
082    /**
083     * Need this so that 'dummy' entity resolver is also used for documents read with the document() function.
084     */
085    public class EntityIgnoringUriResolver implements URIResolver {
086
087        public Source resolve(String href, String base) throws TransformerException {
088
089            try {
090                String uri = SystemIDResolver.getAbsoluteURI(href, base);
091                XMLReader reader = m_parserFactory.newSAXParser().getXMLReader();
092                reader.setEntityResolver(NO_ENTITY_RESOLVER);
093                Source source;
094                source = new SAXSource(reader, new InputSource(uri));
095                return source;
096            } catch (Exception e) {
097                throw new TransformerException(e);
098            }
099        }
100    }
101
102    /**
103     * Single entry from transforms.xml.
104     */
105    private class TransformEntry {
106
107        /** Name of the XSLT file. */
108        private String m_xslt;
109
110        /** Name of the config file. */
111        private String m_configFile;
112
113        /**
114         * Creates a new entry.
115         *
116         * @param configFile the name of the config file
117         * @param xslt the name of the XSLT file
118         */
119        public TransformEntry(String configFile, String xslt) {
120
121            super();
122            m_xslt = xslt;
123            m_configFile = configFile;
124        }
125
126        /**
127         * Gets the name of the config file.
128         *
129         * @return the name of the config file
130         */
131        public String getConfigFile() {
132
133            return m_configFile;
134        }
135
136        /**
137         * Gets the name of the XSLT file.
138         *
139         * @return the name of the XSLT file
140         */
141        public String getXslt() {
142
143            return m_xslt;
144        }
145
146    }
147
148    /**
149     * Default XML for new config files.
150     */
151    public static final String DEFAULT_XML = "<opencms/>";
152
153    /** Entity resolver to skip dtd validation. */
154    private static final EntityResolver NO_ENTITY_RESOLVER = new EntityResolver() {
155
156        /**
157         * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String, java.lang.String)
158         */
159        public InputSource resolveEntity(String publicId, String systemId) {
160
161            // return new InputSource(new StringReader("<!ELEMENT opencms ANY>"));
162            return new InputSource(new StringReader(""));
163        }
164    };
165
166    /**Flag to indicate if transformation was done.*/
167    private boolean m_isDone = false;
168
169    /** Directory for the config files. */
170    private File m_configDir;
171
172    /** The transformer factory. */
173    private TransformerFactory m_transformerFactory = TransformerFactory.newInstance();
174
175    /** The parser factory. */
176    private SAXParserFactory m_parserFactory = SAXParserFactory.newInstance();
177
178    /** The directory containing the XSLT transforms. */
179    private File m_xsltDir;
180
181    /**
182     * Creates a new instance.
183     *
184     * @param xsltDir the directory containing the XSLT files
185     * @param configDir the configuration directory
186     */
187    public CmsXmlConfigUpdater(File xsltDir, File configDir) {
188
189        m_configDir = configDir;
190        m_xsltDir = xsltDir;
191        m_parserFactory.setNamespaceAware(true);
192        m_parserFactory.setValidating(false);
193        m_transformerFactory.setURIResolver(new EntityIgnoringUriResolver());
194    }
195
196    /**
197     * Checks if updater has tried to transform.<p>
198     *
199     * @return boolean
200     */
201    public boolean isDone() {
202
203        return m_isDone;
204    }
205
206    /**
207     * Transforms a config file with an XSLT transform.
208     *
209     * @param name file name of the config file
210     * @param transform file name of the XSLT file
211     *
212     * @throws Exception if something goes wrong
213     */
214    public void transform(String name, String transform) throws Exception {
215
216        File configFile = new File(m_configDir, name);
217        File transformFile = new File(m_xsltDir, transform);
218        try (InputStream stream = new FileInputStream(transformFile)) {
219            StreamSource source = new StreamSource(stream);
220            transform(configFile, source);
221        }
222    }
223
224    /**
225     * Transforms the configuration.
226     *
227     * @throws Exception if something goes wrong
228     */
229    public void transformConfig() throws Exception {
230
231        List<TransformEntry> entries = readTransformEntries(new File(m_xsltDir, "transforms.xml"));
232        for (TransformEntry entry : entries) {
233            transform(entry.getConfigFile(), entry.getXslt());
234        }
235        m_isDone = true;
236    }
237
238    /**
239     * Gets validation errors either as a JSON string, or null if there are no validation errors.
240     *
241     * @return the validation error JSON
242     */
243    public String validationErrors() {
244
245        List<String> errors = new ArrayList<>();
246        for (File config : getConfigFiles()) {
247            String filename = config.getName();
248            try (FileInputStream stream = new FileInputStream(config)) {
249                CmsXmlUtils.unmarshalHelper(CmsFileUtil.readFully(stream, false), new CmsXmlEntityResolver(null), true);
250            } catch (CmsXmlException e) {
251                errors.add(filename + ":" + e.getCause().getMessage());
252            } catch (Exception e) {
253                errors.add(filename + ":" + e.getMessage());
254            }
255        }
256        if (errors.size() == 0) {
257            return null;
258        }
259        String errString = CmsStringUtil.listAsString(errors, "\n");
260        JSONObject obj = new JSONObject();
261        try {
262            obj.put("err", errString);
263        } catch (JSONException e) {
264
265        }
266        return obj.toString();
267    }
268
269    /**
270     * Gets existing config files.
271     *
272     * @return the existing config files
273     */
274    private List<File> getConfigFiles() {
275
276        String[] filenames = {
277            "opencms-modules.xml",
278            "opencms-system.xml",
279            "opencms-vfs.xml",
280            "opencms-importexport.xml",
281            "opencms-sites.xml",
282            "opencms-variables.xml",
283            "opencms-scheduler.xml",
284            "opencms-workplace.xml",
285            "opencms-search.xml"};
286        List<File> result = new ArrayList<>();
287        for (String fn : filenames) {
288            File file = new File(m_configDir, fn);
289            if (file.exists()) {
290                result.add(file);
291            }
292        }
293        return result;
294    }
295
296    /**
297     * Reads entries from transforms.xml.
298     *
299     * @param file the XML file
300     * @return the transform entries read from the file
301     *
302     * @throws Exception if something goes wrong
303     */
304    private List<TransformEntry> readTransformEntries(File file) throws Exception {
305
306        List<TransformEntry> result = new ArrayList<>();
307        try (FileInputStream fis = new FileInputStream(file)) {
308            byte[] data = CmsFileUtil.readFully(fis, false);
309            Document doc = CmsXmlUtils.unmarshalHelper(data, null, false);
310            for (Node node : doc.selectNodes("//transform")) {
311                Element elem = ((Element)node);
312                String xslt = elem.attributeValue("xslt");
313                String conf = elem.attributeValue("config");
314                TransformEntry entry = new TransformEntry(conf, xslt);
315                result.add(entry);
316            }
317        }
318        return result;
319    }
320
321    /**
322     * Transforms a single configuration file using the given transformation source.
323     *
324     * @param file the configuration file
325     * @param transformSource the transform soruce
326     *
327     * @throws TransformerConfigurationException -
328     * @throws IOException -
329     * @throws SAXException -
330     * @throws TransformerException -
331     * @throws ParserConfigurationException -
332     */
333    private void transform(File file, Source transformSource)
334    throws TransformerConfigurationException, IOException, SAXException, TransformerException,
335    ParserConfigurationException {
336
337        Transformer transformer = m_transformerFactory.newTransformer(transformSource);
338        transformer.setOutputProperty(OutputKeys.ENCODING, "us-ascii");
339        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
340        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
341        String configDirPath = m_configDir.getAbsolutePath();
342        configDirPath = configDirPath.replaceFirst("[/\\\\]$", "");
343        transformer.setParameter("configDir", configDirPath);
344        XMLReader reader = m_parserFactory.newSAXParser().getXMLReader();
345        reader.setEntityResolver(NO_ENTITY_RESOLVER);
346
347        Source source;
348
349        if (file.exists()) {
350            source = new SAXSource(reader, new InputSource(file.getCanonicalPath()));
351        } else {
352            source = new SAXSource(reader, new InputSource(new ByteArrayInputStream(DEFAULT_XML.getBytes("UTF-8"))));
353        }
354
355        ByteArrayOutputStream baos = new ByteArrayOutputStream();
356        Result target = new StreamResult(baos);
357        transformer.transform(source, target);
358        byte[] transformedConfig = baos.toByteArray();
359        try (FileOutputStream output = new FileOutputStream(file)) {
360            output.write(transformedConfig);
361        }
362    }
363
364}