
org.opencms.setup.xml.CmsXmlConfigUpdater Maven / Gradle / Ivy
/*
* This library is part of OpenCms -
* the Open Source Content Management System
*
* Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* For further information about Alkacon Software, please see the
* company website: http://www.alkacon.com
*
* For further information about OpenCms, please see the
* project website: http://www.opencms.org
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.opencms.setup.xml;
import org.opencms.configuration.CmsConfigurationManager;
import org.opencms.json.JSONException;
import org.opencms.json.JSONObject;
import org.opencms.util.CmsFileUtil;
import org.opencms.util.CmsStringUtil;
import org.opencms.xml.CmsXmlEntityResolver;
import org.opencms.xml.CmsXmlException;
import org.opencms.xml.CmsXmlUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.xml.utils.SystemIDResolver;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
/**
* Class for updating the XML configuration files using a set of XSLT transforms.
*
* The XSLT transforms are stored in the directory update/xmlupdate, together with a file transforms.xml
* that contains the list of transformation files and the configuration files to which they should be applied to.
*
*/
public class CmsXmlConfigUpdater {
/**
* Need this so that 'dummy' entity resolver is also used for documents read with the document() function.
*/
public class EntityIgnoringUriResolver implements URIResolver {
public Source resolve(String href, String base) throws TransformerException {
try {
String uri = SystemIDResolver.getAbsoluteURI(href, base);
XMLReader reader = m_parserFactory.newSAXParser().getXMLReader();
reader.setEntityResolver(NO_ENTITY_RESOLVER);
Source source;
source = new SAXSource(reader, new InputSource(uri));
return source;
} catch (Exception e) {
throw new TransformerException(e);
}
}
}
/**
* Single entry from transforms.xml.
*/
private class TransformEntry {
/** Name of the config file. */
private String m_configFile;
/** Name of the XSLT file. */
private String m_xslt;
/**
* Creates a new entry.
*
* @param configFile the name of the config file
* @param xslt the name of the XSLT file
*/
public TransformEntry(String configFile, String xslt) {
super();
m_xslt = xslt;
m_configFile = configFile;
}
/**
* Gets the name of the config file.
*
* @return the name of the config file
*/
public String getConfigFile() {
return m_configFile;
}
/**
* Gets the name of the XSLT file.
*
* @return the name of the XSLT file
*/
public String getXslt() {
return m_xslt;
}
}
/**
* Default XML for new config files.
*/
public static final String DEFAULT_XML = " ";
/** Entity resolver to skip dtd validation. */
private static final EntityResolver NO_ENTITY_RESOLVER = new EntityResolver() {
/**
* @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String, java.lang.String)
*/
public InputSource resolveEntity(String publicId, String systemId) {
// return new InputSource(new StringReader(""));
return new InputSource(new StringReader(""));
}
};
/** Directory for the config files. */
private File m_configDir;
/**Flag to indicate if transformation was done.*/
private boolean m_isDone = false;
/** The parser factory. */
private SAXParserFactory m_parserFactory = SAXParserFactory.newInstance();
/** The transformer factory. */
private TransformerFactory m_transformerFactory = new org.apache.xalan.processor.TransformerFactoryImpl();
/** The directory containing the XSLT transforms. */
private File m_xsltDir;
/**
* Creates a new instance.
*
* @param xsltDir the directory containing the XSLT files
* @param configDir the configuration directory
*/
public CmsXmlConfigUpdater(File xsltDir, File configDir) {
m_configDir = configDir;
m_xsltDir = xsltDir;
m_parserFactory.setNamespaceAware(true);
m_parserFactory.setValidating(false);
m_transformerFactory.setURIResolver(new EntityIgnoringUriResolver());
}
/**
* Helper method for determining the position for a top-level configuration element in opencms-system.xml.
*
* This can be used by XSL transformations to insert optional nodes for new features on the top level.
* @param name the element name
* @return the position for the element name, or -1 if the position could not be determined
*
* @throws Exception if something goes wrong
*/
public static int getSystemConfigPosition(String name) throws Exception {
byte[] fileData = CmsFileUtil.readFully(
CmsConfigurationManager.class.getResourceAsStream("opencms-system.dtd"),
true);
String dtdText = new String(fileData, StandardCharsets.UTF_8);
// Assumption: declaration of 'system' in the DTD is just a list of elements (with +/*/? suffixes), and doesn't have nested expressions
String regex = "(?s)";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(dtdText);
List elementNames = new ArrayList<>();
if (m.find()) {
String items = m.group(1);
for (String token : items.split("(?:\\s|,)+")) {
token = token.trim();
if (token.length() == 0) {
continue;
}
if (token.endsWith("*") || token.endsWith("?") || token.endsWith("+")) {
token = token.substring(0, token.length() - 1);
}
elementNames.add(token);
}
return elementNames.indexOf(name);
}
return -1;
}
/**
* Checks if updater has tried to transform.
*
* @return boolean
*/
public boolean isDone() {
return m_isDone;
}
/**
* Transforms a config file with an XSLT transform.
*
* @param name file name of the config file
* @param transform file name of the XSLT file
*
* @throws Exception if something goes wrong
*/
public void transform(String name, String transform) throws Exception {
File configFile = new File(m_configDir, name);
File transformFile = new File(m_xsltDir, transform);
try (InputStream stream = new FileInputStream(transformFile)) {
StreamSource source = new StreamSource(stream);
transform(configFile, source);
}
}
/**
* Transforms the configuration.
*
* @throws Exception if something goes wrong
*/
public void transformConfig() throws Exception {
List entries = readTransformEntries(new File(m_xsltDir, "transforms.xml"));
for (TransformEntry entry : entries) {
transform(entry.getConfigFile(), entry.getXslt());
}
m_isDone = true;
}
/**
* Gets validation errors either as a JSON string, or null if there are no validation errors.
*
* @return the validation error JSON
*/
public String validationErrors() {
List errors = new ArrayList<>();
for (File config : getConfigFiles()) {
String filename = config.getName();
try (FileInputStream stream = new FileInputStream(config)) {
CmsXmlUtils.unmarshalHelper(CmsFileUtil.readFully(stream, false), new CmsXmlEntityResolver(null), true);
} catch (CmsXmlException e) {
errors.add(filename + ":" + e.getCause().getMessage());
} catch (Exception e) {
errors.add(filename + ":" + e.getMessage());
}
}
if (errors.size() == 0) {
return null;
}
String errString = CmsStringUtil.listAsString(errors, "\n");
JSONObject obj = new JSONObject();
try {
obj.put("err", errString);
} catch (JSONException e) {
}
return obj.toString();
}
/**
* Gets existing config files.
*
* @return the existing config files
*/
private List getConfigFiles() {
String[] filenames = {
"opencms-modules.xml",
"opencms-system.xml",
"opencms-vfs.xml",
"opencms-importexport.xml",
"opencms-sites.xml",
"opencms-variables.xml",
"opencms-scheduler.xml",
"opencms-workplace.xml",
"opencms-search.xml"};
List result = new ArrayList<>();
for (String fn : filenames) {
File file = new File(m_configDir, fn);
if (file.exists()) {
result.add(file);
}
}
return result;
}
/**
* Reads entries from transforms.xml.
*
* @param file the XML file
* @return the transform entries read from the file
*
* @throws Exception if something goes wrong
*/
private List readTransformEntries(File file) throws Exception {
List result = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(file)) {
byte[] data = CmsFileUtil.readFully(fis, false);
Document doc = CmsXmlUtils.unmarshalHelper(data, null, false);
for (Node node : doc.selectNodes("//transform")) {
Element elem = ((Element)node);
String xslt = elem.attributeValue("xslt");
String conf = elem.attributeValue("config");
TransformEntry entry = new TransformEntry(conf, xslt);
result.add(entry);
}
}
return result;
}
/**
* Transforms a single configuration file using the given transformation source.
*
* @param file the configuration file
* @param transformSource the transform soruce
*
* @throws TransformerConfigurationException -
* @throws IOException -
* @throws SAXException -
* @throws TransformerException -
* @throws ParserConfigurationException -
*/
private void transform(File file, Source transformSource)
throws TransformerConfigurationException, IOException, SAXException, TransformerException,
ParserConfigurationException {
Transformer transformer = m_transformerFactory.newTransformer(transformSource);
transformer.setOutputProperty(OutputKeys.ENCODING, "us-ascii");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
String configDirPath = m_configDir.getAbsolutePath();
configDirPath = configDirPath.replaceFirst("[/\\\\]$", "");
transformer.setParameter("configDir", configDirPath);
XMLReader reader = m_parserFactory.newSAXParser().getXMLReader();
reader.setEntityResolver(NO_ENTITY_RESOLVER);
Source source;
if (file.exists()) {
source = new SAXSource(reader, new InputSource(file.getCanonicalPath()));
} else {
source = new SAXSource(reader, new InputSource(new ByteArrayInputStream(DEFAULT_XML.getBytes("UTF-8"))));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Result target = new StreamResult(baos);
transformer.transform(source, target);
byte[] transformedConfig = baos.toByteArray();
try (FileOutputStream output = new FileOutputStream(file)) {
output.write(transformedConfig);
}
}
}