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}