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