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.xml.page; 029 030import org.opencms.configuration.CmsConfigurationManager; 031import org.opencms.file.CmsFile; 032import org.opencms.file.CmsObject; 033import org.opencms.file.CmsResource; 034import org.opencms.i18n.CmsEncoder; 035import org.opencms.i18n.CmsLocaleManager; 036import org.opencms.main.CmsIllegalArgumentException; 037import org.opencms.main.CmsLog; 038import org.opencms.main.CmsRuntimeException; 039import org.opencms.staticexport.CmsLinkProcessor; 040import org.opencms.staticexport.CmsLinkTable; 041import org.opencms.xml.A_CmsXmlDocument; 042import org.opencms.xml.CmsXmlContentDefinition; 043import org.opencms.xml.CmsXmlEntityResolver; 044import org.opencms.xml.CmsXmlException; 045import org.opencms.xml.CmsXmlGenericWrapper; 046import org.opencms.xml.CmsXmlUtils; 047import org.opencms.xml.content.CmsXmlContentErrorHandler; 048import org.opencms.xml.content.I_CmsXmlContentHandler; 049import org.opencms.xml.types.CmsXmlHtmlValue; 050import org.opencms.xml.types.I_CmsXmlContentValue; 051import org.opencms.xml.types.I_CmsXmlSchemaType; 052 053import java.io.IOException; 054import java.util.ArrayList; 055import java.util.Collections; 056import java.util.HashMap; 057import java.util.HashSet; 058import java.util.Iterator; 059import java.util.List; 060import java.util.Locale; 061import java.util.Map; 062import java.util.Set; 063 064import org.apache.commons.logging.Log; 065 066import org.dom4j.Attribute; 067import org.dom4j.Document; 068import org.dom4j.DocumentHelper; 069import org.dom4j.Element; 070import org.xml.sax.InputSource; 071 072/** 073 * Implementation of a page object used to access and manage xml data.<p> 074 * 075 * This implementation consists of several named elements optionally available for 076 * various languages. The data of each element is accessible via its name and language. 077 * 078 * The content of each element is stored as CDATA, links within the 079 * content are processed and are separately accessible as entries of a CmsLinkTable. 080 * 081 * @since 6.0.0 082 */ 083public class CmsXmlPage extends A_CmsXmlDocument { 084 085 /** Name of the name attribute of the elements node. */ 086 public static final String ATTRIBUTE_ENABLED = "enabled"; 087 088 /** Name of the language attribute of the elements node. */ 089 public static final String ATTRIBUTE_LANGUAGE = "language"; 090 091 /** Name of the name attribute of the elements node. */ 092 public static final String ATTRIBUTE_NAME = "name"; 093 094 /** Name of the element node. */ 095 public static final String NODE_CONTENT = "content"; 096 097 /** Name of the elements node. */ 098 public static final String NODE_ELEMENTS = "elements"; 099 100 /** Name of the link node. */ 101 public static final String NODE_LINK = "link"; 102 103 /** Name of the links node. */ 104 public static final String NODE_LINKS = "links"; 105 106 /** Name of the page node. */ 107 public static final String NODE_PAGE = "page"; 108 109 /** Name of the page node. */ 110 public static final String NODE_PAGES = "pages"; 111 112 /** Property to check if relative links are allowed. */ 113 public static final String PROPERTY_ALLOW_RELATIVE = "allowRelativeLinks"; 114 115 /** The DTD address of the OpenCms xmlpage. */ 116 public static final String XMLPAGE_XSD_SYSTEM_ID = CmsConfigurationManager.DEFAULT_DTD_PREFIX + "xmlpage.xsd"; 117 118 /** The log object for this class. */ 119 private static final Log LOG = CmsLog.getLog(CmsXmlPage.class); 120 121 /** The XML page content definition is static. */ 122 private static CmsXmlContentDefinition m_xmlPageContentDefinition; 123 124 /** Name of the element node. */ 125 private static final String NODE_ELEMENT = "element"; 126 127 /** Indicates if relative Links are allowed. */ 128 private boolean m_allowRelativeLinks; 129 130 /** 131 * Creates a new CmsXmlPage based on the provided document and encoding.<p> 132 * 133 * The encoding is used for marshalling the XML document later.<p> 134 * 135 * @param document the document to create the CmsXmlPage from 136 * @param encoding the encoding of the xml page 137 */ 138 public CmsXmlPage(Document document, String encoding) { 139 140 initDocument(document, encoding, getContentDefinition()); 141 } 142 143 /** 144 * Creates an empty XML page in the provided locale using 145 * the provided encoding.<p> 146 * 147 * The page is initialized according to the minimal necessary xml structure. 148 * The encoding is used for marshalling the XML document later.<p> 149 * 150 * @param locale the initial locale of the XML page 151 * @param encoding the encoding of the XML page 152 */ 153 public CmsXmlPage(Locale locale, String encoding) { 154 155 initDocument(CmsXmlPageFactory.createDocument(locale), encoding, getContentDefinition()); 156 } 157 158 /** 159 * @see org.opencms.xml.I_CmsXmlDocument#addLocale(org.opencms.file.CmsObject, java.util.Locale) 160 */ 161 public void addLocale(CmsObject cms, Locale locale) throws CmsXmlException { 162 163 if (hasLocale(locale)) { 164 throw new CmsXmlException(Messages.get().container(Messages.ERR_XML_PAGE_LOCALE_EXISTS_1, locale)); 165 } 166 // add element node for Locale 167 getContentDefinition().createLocale(cms, this, m_document.getRootElement(), locale); 168 // re-initialize the bookmarks 169 initDocument(m_document, m_encoding, getContentDefinition()); 170 } 171 172 /** 173 * Adds a new, empty value with the given name and locale 174 * to this XML document.<p> 175 * 176 * @param name the name of the value 177 * @param locale the locale of the value 178 * 179 * @throws CmsIllegalArgumentException if the name contains an index ("[<number>]") or the value for the 180 * given locale already exists in the xmlpage. 181 * 182 */ 183 public void addValue(String name, Locale locale) throws CmsIllegalArgumentException { 184 185 if (name.indexOf('[') >= 0) { 186 throw new CmsIllegalArgumentException( 187 Messages.get().container(Messages.ERR_XML_PAGE_CONTAINS_INDEX_1, name)); 188 } 189 190 if (hasValue(name, locale)) { 191 throw new CmsIllegalArgumentException( 192 Messages.get().container(Messages.ERR_XML_PAGE_LANG_ELEM_EXISTS_2, name, locale)); 193 } 194 195 Element pages = m_document.getRootElement(); 196 String localeStr = locale.toString(); 197 Element page = null; 198 199 // search if a page for the selected language is already available 200 for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(pages, NODE_PAGE); i.hasNext();) { 201 Element nextPage = i.next(); 202 String language = nextPage.attributeValue(ATTRIBUTE_LANGUAGE); 203 if (localeStr.equals(language)) { 204 // a page for the selected language was found 205 page = nextPage; 206 break; 207 } 208 } 209 210 // create the new element 211 Element element; 212 if (page != null) { 213 // page for selected language already available 214 element = page.addElement(NODE_ELEMENT).addAttribute(ATTRIBUTE_NAME, name); 215 } else { 216 // no page for the selected language was found 217 element = pages.addElement(NODE_PAGE).addAttribute(ATTRIBUTE_LANGUAGE, localeStr); 218 element = element.addElement(NODE_ELEMENT).addAttribute(ATTRIBUTE_NAME, name); 219 } 220 221 // add empty nodes for link table and content to the element 222 element.addElement(NODE_LINKS); 223 element.addElement(NODE_CONTENT); 224 225 CmsXmlHtmlValue value = new CmsXmlHtmlValue(this, element, locale); 226 227 // bookmark the element 228 addBookmark(CmsXmlUtils.createXpathElement(name, 1), locale, true, value); 229 } 230 231 /** 232 * Returns if relative links are accepted (and left unprocessed).<p> 233 * 234 * @return true if relative links are allowed 235 */ 236 public boolean getAllowRelativeLinks() { 237 238 return m_allowRelativeLinks; 239 } 240 241 /** 242 * @see org.opencms.xml.I_CmsXmlDocument#getContentDefinition() 243 */ 244 public CmsXmlContentDefinition getContentDefinition() throws CmsRuntimeException { 245 246 if (m_xmlPageContentDefinition == null) { 247 // since XML page schema is cached anyway we don't need an CmsObject instance 248 CmsXmlEntityResolver resolver = new CmsXmlEntityResolver(null); 249 InputSource source; 250 try { 251 source = resolver.resolveEntity(null, XMLPAGE_XSD_SYSTEM_ID); 252 // store content definition in static variable 253 m_xmlPageContentDefinition = CmsXmlContentDefinition.unmarshal(source, XMLPAGE_XSD_SYSTEM_ID, resolver); 254 } catch (CmsXmlException e) { 255 throw new CmsRuntimeException( 256 Messages.get().container(Messages.ERR_XML_PAGE_UNMARSHAL_CONTENDDEF_0), 257 e); 258 } catch (IOException e) { 259 throw new CmsRuntimeException( 260 Messages.get().container(Messages.ERR_XML_PAGE_UNMARSHAL_CONTENDDEF_0), 261 e); 262 } 263 } 264 return m_xmlPageContentDefinition; 265 } 266 267 /** 268 * @see org.opencms.xml.I_CmsXmlDocument#getHandler() 269 */ 270 public I_CmsXmlContentHandler getHandler() { 271 272 return getContentDefinition().getContentHandler(); 273 } 274 275 /** 276 * @see org.opencms.xml.A_CmsXmlDocument#getLinkProcessor(org.opencms.file.CmsObject, org.opencms.staticexport.CmsLinkTable) 277 */ 278 public CmsLinkProcessor getLinkProcessor(CmsObject cms, CmsLinkTable linkTable) { 279 280 // initialize link processor 281 String relativeRoot = null; 282 if ((!m_allowRelativeLinks) && (m_file != null)) { 283 relativeRoot = CmsResource.getParentFolder(cms.getSitePath(m_file)); 284 } 285 return new CmsLinkProcessor(cms, linkTable, getEncoding(), relativeRoot); 286 } 287 288 /** 289 * Returns the link table of an element.<p> 290 * 291 * @param name name of the element 292 * @param locale locale of the element 293 * @return the link table 294 */ 295 public CmsLinkTable getLinkTable(String name, Locale locale) { 296 297 CmsXmlHtmlValue value = (CmsXmlHtmlValue)getValue(name, locale); 298 if (value != null) { 299 return value.getLinkTable(); 300 } 301 return new CmsLinkTable(); 302 } 303 304 /** 305 * @see org.opencms.xml.A_CmsXmlDocument#getNames(java.util.Locale) 306 */ 307 @Override 308 public List<String> getNames(Locale locale) { 309 310 Set<String> sn = m_elementNames.get(locale); 311 if (sn != null) { 312 List<String> result = new ArrayList<String>(); 313 Iterator<String> i = sn.iterator(); 314 while (i.hasNext()) { 315 String path = i.next(); 316 result.add(CmsXmlUtils.removeXpathIndex(path)); 317 } 318 return result; 319 } 320 return Collections.emptyList(); 321 } 322 323 /** 324 * Checks if the element of a page object is enabled.<p> 325 * 326 * @param name the name of the element 327 * @param locale the locale of the element 328 * @return true if the element exists and is not disabled 329 */ 330 @Override 331 public boolean isEnabled(String name, Locale locale) { 332 333 CmsXmlHtmlValue value = (CmsXmlHtmlValue)getValue(name, locale); 334 335 if (value != null) { 336 Element element = value.getElement(); 337 Attribute enabled = element.attribute(ATTRIBUTE_ENABLED); 338 return ((enabled == null) || Boolean.valueOf(enabled.getValue()).booleanValue()); 339 } 340 341 return false; 342 } 343 344 /** 345 * Removes an existing value with the given name and locale 346 * from this XML document.<p> 347 * 348 * @param name the name of the value 349 * @param locale the locale of the value 350 */ 351 public void removeValue(String name, Locale locale) { 352 353 I_CmsXmlContentValue value = removeBookmark(CmsXmlUtils.createXpath(name, 1), locale); 354 if (value != null) { 355 Element element = value.getElement(); 356 element.detach(); 357 } 358 } 359 360 /** 361 * Renames the page-element value from the old to the new one.<p> 362 * 363 * @param oldValue the old value 364 * @param newValue the new value 365 * @param locale the locale 366 * 367 * @throws CmsIllegalArgumentException if the name contains an index ("[<number>]"), the new value for the 368 * given locale already exists in the xmlpage or the the old value does not exist for the locale in the xmlpage. 369 * 370 */ 371 public void renameValue(String oldValue, String newValue, Locale locale) throws CmsIllegalArgumentException { 372 373 CmsXmlHtmlValue oldXmlHtmlValue = (CmsXmlHtmlValue)getValue(oldValue, locale); 374 if (oldXmlHtmlValue == null) { 375 throw new CmsIllegalArgumentException( 376 Messages.get().container(Messages.ERR_XML_PAGE_NO_ELEM_FOR_LANG_2, oldValue, locale)); 377 } 378 379 if (hasValue(newValue, locale)) { 380 throw new CmsIllegalArgumentException( 381 Messages.get().container(Messages.ERR_XML_PAGE_LANG_ELEM_EXISTS_2, newValue, locale)); 382 } 383 if (newValue.indexOf('[') >= 0) { 384 throw new CmsIllegalArgumentException( 385 Messages.get().container(Messages.ERR_XML_PAGE_CONTAINS_INDEX_1, newValue)); 386 } 387 388 // get the element 389 Element element = oldXmlHtmlValue.getElement(); 390 391 // update value of the element attribute 'NAME' 392 element.addAttribute(ATTRIBUTE_NAME, newValue); 393 394 // re-initialize the document to update the bookmarks 395 initDocument(m_document, m_encoding, getContentDefinition()); 396 } 397 398 /** 399 * Sets the enabled flag of an already existing element.<p> 400 * 401 * Note: if isEnabled is set to true, the attribute is removed 402 * since true is the default 403 * 404 * @param name name name of the element 405 * @param locale locale of the element 406 * @param isEnabled enabled flag for the element 407 */ 408 public void setEnabled(String name, Locale locale, boolean isEnabled) { 409 410 CmsXmlHtmlValue value = (CmsXmlHtmlValue)getValue(name, locale); 411 Element element = value.getElement(); 412 Attribute enabled = element.attribute(ATTRIBUTE_ENABLED); 413 414 if (enabled == null) { 415 if (!isEnabled) { 416 element.addAttribute(ATTRIBUTE_ENABLED, Boolean.toString(isEnabled)); 417 } 418 } else if (isEnabled) { 419 element.remove(enabled); 420 } else { 421 enabled.setValue(Boolean.toString(isEnabled)); 422 } 423 } 424 425 /** 426 * Sets the data of an already existing value.<p> 427 * 428 * The data will be enclosed as CDATA within the xml page structure. 429 * When setting the element data, the content of this element will be 430 * processed automatically.<p> 431 * 432 * @param cms the cms object 433 * @param name name of the element 434 * @param locale locale of the element 435 * @param content character data (CDATA) of the element 436 * 437 * @throws CmsXmlException if something goes wrong 438 */ 439 public void setStringValue(CmsObject cms, String name, Locale locale, String content) throws CmsXmlException { 440 441 CmsXmlHtmlValue value = (CmsXmlHtmlValue)getValue(name, locale); 442 443 if (value != null) { 444 // set the values 445 value.setStringValue(cms, content); 446 } else { 447 throw new CmsXmlException( 448 Messages.get().container(Messages.ERR_XML_PAGE_INVALID_ELEM_SELECT_2, locale, name)); 449 } 450 } 451 452 /** 453 * @see org.opencms.xml.I_CmsXmlDocument#validate(org.opencms.file.CmsObject) 454 */ 455 public CmsXmlContentErrorHandler validate(CmsObject cms) { 456 457 // XML pages currently do not support validation 458 return new CmsXmlContentErrorHandler(); 459 } 460 461 /** 462 * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition) 463 */ 464 @Override 465 protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) { 466 467 m_encoding = CmsEncoder.lookupEncoding(encoding, encoding); 468 m_document = document; 469 m_elementLocales = new HashMap<String, Set<Locale>>(); 470 m_elementNames = new HashMap<Locale, Set<String>>(); 471 m_locales = new HashSet<Locale>(); 472 473 // convert pre 5.3.6 XML page documents 474 if (!NODE_PAGES.equals(m_document.getRootElement().getName())) { 475 convertOldDocument(); 476 } 477 478 // initialize the bookmarks 479 clearBookmarks(); 480 Element pages = m_document.getRootElement(); 481 try { 482 for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(pages, NODE_PAGE); i.hasNext();) { 483 484 Element page = i.next(); 485 Locale locale = CmsLocaleManager.getLocale(page.attributeValue(ATTRIBUTE_LANGUAGE)); 486 for (Iterator<Element> j = CmsXmlGenericWrapper.elementIterator(page, NODE_ELEMENT); j.hasNext();) { 487 488 Element element = j.next(); 489 String name = element.attributeValue(ATTRIBUTE_NAME); 490 491 String elementEnabled = element.attributeValue(ATTRIBUTE_ENABLED); 492 boolean enabled = (elementEnabled == null) ? true : Boolean.valueOf(elementEnabled).booleanValue(); 493 494 // create an element type from the XML node 495 CmsXmlHtmlValue value = new CmsXmlHtmlValue(this, element, locale); 496 value.setContentDefinition(definition); 497 498 // add the element type bookmark 499 addBookmark(CmsXmlUtils.createXpathElement(name, 1), locale, enabled, value); 500 } 501 addLocale(locale); 502 } 503 } catch (NullPointerException e) { 504 LOG.error(Messages.get().getBundle().key(Messages.ERR_XML_PAGE_INIT_BOOKMARKS_0), e); 505 } 506 } 507 508 /** 509 * Sets the parameter that controls the relative link generation.<p> 510 * 511 * @param value the parameter that controls the relative link generation 512 */ 513 protected void setAllowRelativeLinks(boolean value) { 514 515 m_allowRelativeLinks = value; 516 } 517 518 /** 519 * Sets the file this XML page content is written to.<p> 520 * 521 * @param file the file this XML page content is written to 522 */ 523 protected void setFile(CmsFile file) { 524 525 m_file = file; 526 } 527 528 /** 529 * Converts the XML structure of the pre 5.5.0 development version of 530 * the XML page to the final 6.0 version.<p> 531 */ 532 private void convertOldDocument() { 533 534 Document newDocument = DocumentHelper.createDocument(); 535 Element root = newDocument.addElement(NODE_PAGES); 536 root.add(I_CmsXmlSchemaType.XSI_NAMESPACE); 537 root.addAttribute(I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION, XMLPAGE_XSD_SYSTEM_ID); 538 539 Map<String, Element> pages = new HashMap<String, Element>(); 540 541 if ((m_document.getRootElement() != null) && (m_document.getRootElement().element(NODE_ELEMENTS) != null)) { 542 for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator( 543 m_document.getRootElement().element(NODE_ELEMENTS), 544 NODE_ELEMENT); i.hasNext();) { 545 546 Element elem = i.next(); 547 try { 548 String elementName = elem.attributeValue(ATTRIBUTE_NAME); 549 String elementLang = elem.attributeValue(ATTRIBUTE_LANGUAGE); 550 String elementEnabled = elem.attributeValue(ATTRIBUTE_ENABLED); 551 boolean enabled = (elementEnabled == null) ? true : Boolean.valueOf(elementEnabled).booleanValue(); 552 553 Element page = pages.get(elementLang); 554 if (page == null) { 555 // no page available for the language, add one 556 page = root.addElement(NODE_PAGE).addAttribute(ATTRIBUTE_LANGUAGE, elementLang); 557 pages.put(elementLang, page); 558 } 559 560 Element newElement = page.addElement(NODE_ELEMENT).addAttribute(ATTRIBUTE_NAME, elementName); 561 if (!enabled) { 562 newElement.addAttribute(ATTRIBUTE_ENABLED, String.valueOf(enabled)); 563 } 564 Element links = elem.element(NODE_LINKS); 565 if (links != null) { 566 newElement.add(links.createCopy()); 567 } 568 Element content = elem.element(NODE_CONTENT); 569 if (content != null) { 570 newElement.add(content.createCopy()); 571 } 572 573 } catch (NullPointerException e) { 574 LOG.error(Messages.get().getBundle().key(Messages.ERR_XML_PAGE_CONVERT_CONTENT_0), e); 575 } 576 } 577 } 578 579 // now replace the old with the new document 580 m_document = newDocument; 581 } 582}