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 GmbH & Co. KG, 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.i18n.CmsEncoder; 031import org.opencms.i18n.CmsMessageContainer; 032import org.opencms.main.CmsLog; 033import org.opencms.util.CmsCollectionsGenericWrapper; 034import org.opencms.util.CmsStringUtil; 035import org.opencms.xml.CmsXmlEntityResolver; 036import org.opencms.xml.CmsXmlException; 037import org.opencms.xml.CmsXmlUtils; 038 039import java.io.File; 040import java.io.FileNotFoundException; 041import java.io.FileOutputStream; 042import java.io.FileReader; 043import java.io.IOException; 044import java.io.OutputStream; 045import java.io.StringReader; 046import java.util.ArrayList; 047import java.util.HashMap; 048import java.util.Iterator; 049import java.util.List; 050import java.util.Map; 051 052import org.apache.commons.logging.Log; 053 054import org.dom4j.Attribute; 055import org.dom4j.Document; 056import org.dom4j.Element; 057import org.dom4j.Node; 058import org.xml.sax.EntityResolver; 059import org.xml.sax.InputSource; 060 061/** 062 * Helper class to modify xml files.<p> 063 * 064 * For more info about xpath see: <br> 065 * <ul> 066 * <li>http://www.w3.org/TR/xpath.html</li> 067 * <li>http://www.zvon.org/xxl/XPathTutorial/General/examples.html</li> 068 * </ul><p> 069 * 070 * @since 6.1.8 071 */ 072public class CmsSetupXmlHelper { 073 074 /** The log object for this class. */ 075 private static final Log LOG = CmsLog.getLog(CmsSetupXmlHelper.class); 076 077 /** Entity resolver to skip dtd validation. */ 078 private static final EntityResolver NO_ENTITY_RESOLVER = new EntityResolver() { 079 080 /** 081 * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String, java.lang.String) 082 */ 083 public InputSource resolveEntity(String publicId, String systemId) { 084 085 return new InputSource(new StringReader("")); 086 } 087 }; 088 089 /** Optional base path. */ 090 private String m_basePath; 091 092 /** Document cache. */ 093 private Map<String, Document> m_cache = new HashMap<String, Document>(); 094 095 /** 096 * Default constructor.<p> 097 * 098 * Uses no base path.<p> 099 */ 100 public CmsSetupXmlHelper() { 101 102 // ignore 103 } 104 105 /** 106 * Uses an optional base file path.<p> 107 * 108 * @param basePath the base file path to use; 109 */ 110 public CmsSetupXmlHelper(String basePath) { 111 112 m_basePath = basePath; 113 } 114 115 /** 116 * Unmarshals (reads) an XML string into a new document.<p> 117 * 118 * @param xml the XML code to unmarshal 119 * 120 * @return the generated document 121 * 122 * @throws CmsXmlException if something goes wrong 123 */ 124 public static String format(String xml) throws CmsXmlException { 125 126 return CmsXmlUtils.marshal((Node)CmsXmlUtils.unmarshalHelper(xml, null), CmsEncoder.ENCODING_UTF_8); 127 } 128 129 /** 130 * Returns the value in the given xpath of the given xml file.<p> 131 * 132 * @param document the xml document 133 * @param xPath the xpath to read (should select a single node or attribute) 134 * 135 * @return the value in the given xpath of the given xml file, or <code>null</code> if no matching node 136 */ 137 public static String getValue(Document document, String xPath) { 138 139 Node node = document.selectSingleNode(xPath); 140 if (node != null) { 141 // return the value 142 return node.getText(); 143 } else { 144 return null; 145 } 146 } 147 148 /** 149 * Replaces a attibute's value in the given node addressed by the xPath.<p> 150 * 151 * @param document the document to replace the node attribute 152 * @param xPath the xPath to the node 153 * @param attribute the attribute to replace the value of 154 * @param value the new value to set 155 * 156 * @return <code>true</code> if successful <code>false</code> otherwise 157 */ 158 public static boolean setAttribute(Document document, String xPath, String attribute, String value) { 159 160 Node node = document.selectSingleNode(xPath); 161 Element e = (Element)node; 162 @SuppressWarnings("unchecked") 163 List<Attribute> attributes = e.attributes(); 164 for (Attribute a : attributes) { 165 if (a.getName().equals(attribute)) { 166 a.setValue(value); 167 return true; 168 } 169 } 170 return false; 171 } 172 173 /** 174 * Sets the given value in all nodes identified by the given xpath of the given xml file.<p> 175 * 176 * If value is <code>null</code>, all nodes identified by the given xpath will be deleted.<p> 177 * 178 * If the node identified by the given xpath does not exists, the missing nodes will be created 179 * (if <code>value</code> not <code>null</code>).<p> 180 * 181 * @param document the xml document 182 * @param xPath the xpath to set 183 * @param value the value to set (can be <code>null</code> for deletion) 184 * 185 * @return the number of successful changed or deleted nodes 186 */ 187 public static int setValue(Document document, String xPath, String value) { 188 189 return setValue(document, xPath, value, null); 190 } 191 192 /** 193 * Sets the given value in all nodes identified by the given xpath of the given xml file.<p> 194 * 195 * If value is <code>null</code>, all nodes identified by the given xpath will be deleted.<p> 196 * 197 * If the node identified by the given xpath does not exists, the missing nodes will be created 198 * (if <code>value</code> not <code>null</code>).<p> 199 * 200 * @param document the xml document 201 * @param xPath the xpath to set 202 * @param value the value to set (can be <code>null</code> for deletion) 203 * @param nodeToInsert optional, if given it will be inserted after xPath with the given value 204 * 205 * @return the number of successful changed or deleted nodes 206 */ 207 public static int setValue(Document document, String xPath, String value, String nodeToInsert) { 208 209 int changes = 0; 210 // be naive and try to find the node 211 Iterator<Node> itNodes = CmsCollectionsGenericWrapper.<Node> list(document.selectNodes(xPath)).iterator(); 212 213 // if not found 214 if (!itNodes.hasNext()) { 215 if (value == null) { 216 // if no node found for deletion 217 return 0; 218 } 219 // find the node creating missing nodes in the way 220 Iterator<String> it = CmsStringUtil.splitAsList(xPath, "/", false).iterator(); 221 Node currentNode = document; 222 while (it.hasNext()) { 223 String nodeName = it.next(); 224 // if a string condition contains '/' 225 while ((nodeName.indexOf("='") > 0) && (nodeName.indexOf("']") < 0)) { 226 nodeName += "/" + it.next(); 227 } 228 Node node = currentNode.selectSingleNode(nodeName); 229 if (node != null) { 230 // node found 231 currentNode = node; 232 if (!it.hasNext()) { 233 currentNode.setText(value); 234 } 235 } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) { 236 Element elem = (Element)currentNode; 237 if (!nodeName.startsWith("@")) { 238 elem = handleNode(elem, nodeName); 239 if (!it.hasNext() && CmsStringUtil.isNotEmptyOrWhitespaceOnly(value)) { 240 elem.setText(value); 241 } 242 } else { 243 // if node is attribute create it with given value 244 elem.addAttribute(nodeName.substring(1), value); 245 } 246 currentNode = elem; 247 } else { 248 // should never happen 249 if (LOG.isDebugEnabled()) { 250 LOG.debug(Messages.get().getBundle().key(Messages.ERR_XML_SET_VALUE_2, xPath, value)); 251 } 252 break; 253 } 254 } 255 if (nodeToInsert == null) { 256 // if not inserting we are done 257 return 1; 258 } 259 // if inserting, we just created the insertion point, so continue 260 itNodes = CmsCollectionsGenericWrapper.<Node> list(document.selectNodes(xPath)).iterator(); 261 } 262 263 // if found 264 while (itNodes.hasNext()) { 265 Node node = itNodes.next(); 266 if (nodeToInsert == null) { 267 // if not inserting 268 if (value != null) { 269 // if found, change the value 270 node.setText(value); 271 } else { 272 // if node for deletion is found 273 node.getParent().remove(node); 274 } 275 } else { 276 // first create the node to insert 277 Element parent = node.getParent(); 278 Element elem = handleNode(parent, nodeToInsert); 279 if (value != null) { 280 elem.setText(value); 281 } 282 // get the parent element list 283 List<Node> list = CmsCollectionsGenericWrapper.<Node> list(parent.content()); 284 // remove the just created element 285 list.remove(list.size() - 1); 286 // insert it back to the right position 287 int pos = list.indexOf(node); 288 list.add(pos + 1, elem); // insert after 289 } 290 changes++; 291 } 292 return changes; 293 } 294 295 /** 296 * Handles the xpath name, by creating the given node and its children.<p> 297 * 298 * @param parent the parent node to use 299 * @param xpathName the xpathName, ie <code>a[@b='c'][d='e'][text()='f']</code> 300 * 301 * @return the new created element 302 */ 303 private static Element handleNode(Element parent, String xpathName) { 304 305 // if node is no attribute, create a new node 306 String childrenPart = null; 307 String nodeName; 308 int pos = xpathName.indexOf("["); 309 if (pos > 0) { 310 childrenPart = xpathName.substring(pos + 1, xpathName.length() - 1); 311 nodeName = xpathName.substring(0, pos); 312 } else { 313 nodeName = xpathName; 314 } 315 // create node 316 Element elem = parent.addElement(nodeName); 317 if (childrenPart != null) { 318 pos = childrenPart.indexOf("["); 319 if ((pos > 0) && (childrenPart.indexOf("]") > pos)) { 320 handleNode(elem, childrenPart); 321 return elem; 322 } 323 if (childrenPart.contains("=")) { 324 Map<String, String> children = CmsStringUtil.splitAsMap(childrenPart, "][", "="); 325 // handle child nodes 326 for (Map.Entry<String, String> child : children.entrySet()) { 327 String childName = child.getKey(); 328 String childValue = child.getValue(); 329 if (childValue.startsWith("'")) { 330 childValue = childValue.substring(1); 331 } 332 if (childValue.endsWith("'")) { 333 childValue = childValue.substring(0, childValue.length() - 1); 334 } 335 if (childName.startsWith("@")) { 336 elem.addAttribute(childName.substring(1), childValue); 337 } else if (childName.equals("text()")) { 338 elem.setText(childValue); 339 } else if (!childName.contains("(")) { 340 Element childElem = elem.addElement(childName); 341 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(childValue)) { 342 childElem.addText(childValue); 343 } 344 } 345 } 346 } 347 } 348 return elem; 349 } 350 351 /** 352 * Discards the changes in the given file.<p> 353 * 354 * @param xmlFilename the xml config file (could be relative to the base path) 355 */ 356 public void flush(String xmlFilename) { 357 358 m_cache.remove(xmlFilename); 359 } 360 361 /** 362 * Discards the changes in all files.<p> 363 */ 364 public void flushAll() { 365 366 m_cache.clear(); 367 } 368 369 /** 370 * Returns the base file Path.<p> 371 * 372 * @return the base file Path 373 */ 374 public String getBasePath() { 375 376 return m_basePath; 377 } 378 379 /** 380 * Returns the document for the given filename.<p> 381 * It can be new read or come from the document cache.<p> 382 * 383 * @param xmlFilename the filename to read 384 * 385 * @return the document for the given filename 386 * 387 * @throws CmsXmlException if something goes wrong while reading 388 */ 389 public Document getDocument(String xmlFilename) throws CmsXmlException { 390 391 // try to get it from the cache 392 Document document = m_cache.get(xmlFilename); 393 394 if (document == null) { 395 try { 396 document = CmsXmlUtils.unmarshalHelper( 397 new InputSource(new FileReader(getFile(xmlFilename))), 398 NO_ENTITY_RESOLVER); 399 } catch (FileNotFoundException e) { 400 LOG.error("Could not read file " + xmlFilename, e); 401 throw new CmsXmlException(new CmsMessageContainer(null, e.toString())); 402 403 } catch (Exception e) { 404 LOG.error("Could not parse file " + xmlFilename, e); 405 throw new CmsXmlException( 406 Messages.get().container(Messages.ERR_XML_COULD_NOT_PARSE_FILE_1, xmlFilename), 407 e); 408 } 409 // cache the doc 410 m_cache.put(xmlFilename, document); 411 } 412 return document; 413 } 414 415 /** 416 * Returns the value in the given xpath of the given xml file.<p> 417 * 418 * @param xmlFilename the xml config file (could be relative to the base path) 419 * @param xPath the xpath to read (should select a single node or attribute) 420 * 421 * @return the value in the given xpath of the given xml file, or <code>null</code> if no matching node 422 * 423 * @throws CmsXmlException if something goes wrong while reading 424 */ 425 public String getValue(String xmlFilename, String xPath) throws CmsXmlException { 426 427 return getValue(getDocument(xmlFilename), xPath); 428 } 429 430 /** 431 * Replaces a attibute's value in the given node addressed by the xPath.<p> 432 * 433 * @param xmlFilename the xml file name to get the document from 434 * @param xPath the xPath to the node 435 * @param attribute the attribute to replace the value of 436 * @param value the new value to set 437 * 438 * @return <code>true</code> if successful <code>false</code> otherwise 439 * 440 * @throws CmsXmlException if the xml document coudn't be read 441 */ 442 public boolean setAttribute(String xmlFilename, String xPath, String attribute, String value) 443 throws CmsXmlException { 444 445 return setAttribute(getDocument(xmlFilename), xPath, attribute, value); 446 } 447 448 /** 449 * Sets the given value in all nodes identified by the given xpath of the given xml file.<p> 450 * 451 * If value is <code>null</code>, all nodes identified by the given xpath will be deleted.<p> 452 * 453 * If the node identified by the given xpath does not exists, the missing nodes will be created 454 * (if <code>value</code> not <code>null</code>).<p> 455 * 456 * @param xmlFilename the xml config file (could be relative to the base path) 457 * @param xPath the xpath to set 458 * @param value the value to set (can be <code>null</code> for deletion) 459 * 460 * @return the number of successful changed or deleted nodes 461 * 462 * @throws CmsXmlException if something goes wrong 463 */ 464 public int setValue(String xmlFilename, String xPath, String value) throws CmsXmlException { 465 466 return setValue(getDocument(xmlFilename), xPath, value, null); 467 } 468 469 /** 470 * Sets the given value in all nodes identified by the given xpath of the given xml file.<p> 471 * 472 * If value is <code>null</code>, all nodes identified by the given xpath will be deleted.<p> 473 * 474 * If the node identified by the given xpath does not exists, the missing nodes will be created 475 * (if <code>value</code> not <code>null</code>).<p> 476 * 477 * @param xmlFilename the xml config file (could be relative to the base path) 478 * @param xPath the xpath to set 479 * @param value the value to set (can be <code>null</code> for deletion) 480 * @param nodeToInsert optional, if given it will be inserted after xPath with the given value 481 * 482 * @return the number of successful changed or deleted nodes 483 * 484 * @throws CmsXmlException if something goes wrong 485 */ 486 public int setValue(String xmlFilename, String xPath, String value, String nodeToInsert) throws CmsXmlException { 487 488 return setValue(getDocument(xmlFilename), xPath, value, nodeToInsert); 489 } 490 491 /** 492 * Writes the given file back to disk.<p> 493 * 494 * @param xmlFilename the xml config file (could be relative to the base path) 495 * 496 * @throws CmsXmlException if something wrong while writing 497 */ 498 public void write(String xmlFilename) throws CmsXmlException { 499 500 // try to get it from the cache 501 Document document = m_cache.get(xmlFilename); 502 503 if (document != null) { 504 try { 505 CmsXmlUtils.validateXmlStructure(document, CmsEncoder.ENCODING_UTF_8, new CmsXmlEntityResolver(null)); 506 OutputStream out = null; 507 out = new FileOutputStream(getFile(xmlFilename)); 508 CmsXmlUtils.marshal(document, out, CmsEncoder.ENCODING_UTF_8); 509 } catch (FileNotFoundException e) { 510 throw new CmsXmlException(new CmsMessageContainer(null, e.toString())); 511 } catch (CmsXmlException e) { 512 // write invalid config files to the file system with a prefix of "invalid-" so they can be inspected for errors 513 try { 514 OutputStream invalidOut = new FileOutputStream(getFile("invalid-" + xmlFilename)); 515 CmsXmlUtils.marshal(document, invalidOut, CmsEncoder.ENCODING_UTF_8); 516 } catch (IOException e2) { 517 // ignore 518 519 } 520 throw e; 521 } 522 } 523 } 524 525 /** 526 * Flushes all cached documents.<p> 527 * 528 * @throws CmsXmlException if something wrong while writing 529 */ 530 public void writeAll() throws CmsXmlException { 531 532 Iterator<String> it = new ArrayList<String>(m_cache.keySet()).iterator(); 533 while (it.hasNext()) { 534 String filename = it.next(); 535 write(filename); 536 } 537 } 538 539 /** 540 * Returns a file from a given filename.<p> 541 * 542 * @param xmlFilename the file name 543 * 544 * @return the file 545 */ 546 private File getFile(String xmlFilename) { 547 548 File file = new File(m_basePath + xmlFilename); 549 if (!file.exists() || !file.canRead()) { 550 file = new File(xmlFilename); 551 } 552 return file; 553 } 554}