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