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.content; 029 030import org.opencms.file.CmsFile; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsResource; 033import org.opencms.file.CmsResourceFilter; 034import org.opencms.file.types.CmsResourceTypeLocaleIndependentXmlContent; 035import org.opencms.file.types.CmsResourceTypeXmlAdeConfiguration; 036import org.opencms.file.types.CmsResourceTypeXmlContainerPage; 037import org.opencms.file.types.I_CmsResourceType; 038import org.opencms.i18n.CmsEncoder; 039import org.opencms.i18n.CmsLocaleManager; 040import org.opencms.main.CmsException; 041import org.opencms.main.CmsIllegalArgumentException; 042import org.opencms.main.CmsLog; 043import org.opencms.main.CmsRuntimeException; 044import org.opencms.main.OpenCms; 045import org.opencms.staticexport.CmsLinkProcessor; 046import org.opencms.staticexport.CmsLinkTable; 047import org.opencms.util.CmsMacroResolver; 048import org.opencms.util.CmsStringUtil; 049import org.opencms.xml.A_CmsXmlDocument; 050import org.opencms.xml.CmsXmlContentDefinition; 051import org.opencms.xml.CmsXmlException; 052import org.opencms.xml.CmsXmlGenericWrapper; 053import org.opencms.xml.CmsXmlUtils; 054import org.opencms.xml.types.CmsXmlNestedContentDefinition; 055import org.opencms.xml.types.I_CmsXmlContentValue; 056import org.opencms.xml.types.I_CmsXmlSchemaType; 057 058import java.io.IOException; 059import java.util.ArrayList; 060import java.util.Collection; 061import java.util.Collections; 062import java.util.Comparator; 063import java.util.HashMap; 064import java.util.HashSet; 065import java.util.Iterator; 066import java.util.List; 067import java.util.Locale; 068import java.util.Set; 069 070import org.apache.commons.logging.Log; 071 072import org.dom4j.Document; 073import org.dom4j.Element; 074import org.dom4j.Node; 075import org.xml.sax.EntityResolver; 076import org.xml.sax.SAXException; 077 078/** 079 * Implementation of a XML content object, 080 * used to access and manage structured content.<p> 081 * 082 * Use the {@link org.opencms.xml.content.CmsXmlContentFactory} to generate an 083 * instance of this class.<p> 084 * 085 * @since 6.0.0 086 */ 087public class CmsXmlContent extends A_CmsXmlDocument { 088 089 /** The name of the XML content auto correction runtime attribute, this must always be a Boolean. */ 090 public static final String AUTO_CORRECTION_ATTRIBUTE = CmsXmlContent.class.getName() + ".autoCorrectionEnabled"; 091 092 /** The property to set to enable xerces schema validation. */ 093 public static final String XERCES_SCHEMA_PROPERTY = "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation"; 094 095 /** 096 * Comparator to sort values according to the XML element position.<p> 097 */ 098 private static final Comparator<I_CmsXmlContentValue> COMPARE_INDEX = new Comparator<I_CmsXmlContentValue>() { 099 100 public int compare(I_CmsXmlContentValue v1, I_CmsXmlContentValue v2) { 101 102 return v1.getIndex() - v2.getIndex(); 103 } 104 }; 105 106 /** The log object for this class. */ 107 private static final Log LOG = CmsLog.getLog(CmsXmlContent.class); 108 109 /** Flag to control if auto correction is enabled when saving this XML content. */ 110 protected boolean m_autoCorrectionEnabled; 111 112 /** The XML content definition object (i.e. XML schema) used by this content. */ 113 protected CmsXmlContentDefinition m_contentDefinition; 114 115 /** 116 * Hides the public constructor.<p> 117 */ 118 protected CmsXmlContent() { 119 120 // noop 121 } 122 123 /** 124 * Creates a new XML content based on the provided XML document.<p> 125 * 126 * The given encoding is used when marshalling the XML again later.<p> 127 * 128 * @param cms the cms context, if <code>null</code> no link validation is performed 129 * @param document the document to create the xml content from 130 * @param encoding the encoding of the xml content 131 * @param resolver the XML entitiy resolver to use 132 */ 133 protected CmsXmlContent(CmsObject cms, Document document, String encoding, EntityResolver resolver) { 134 135 // must set document first to be able to get the content definition 136 m_document = document; 137 138 // for the next line to work the document must already be available 139 m_contentDefinition = getContentDefinition(resolver); 140 // initialize the XML content structure 141 initDocument(cms, m_document, encoding, m_contentDefinition); 142 } 143 144 /** 145 * Create a new XML content based on the given default content, 146 * that will have all language nodes of the default content and ensures the presence of the given locale.<p> 147 * 148 * The given encoding is used when marshalling the XML again later.<p> 149 * 150 * @param cms the current users OpenCms content 151 * @param locale the locale to generate the default content for 152 * @param modelUri the absolute path to the XML content file acting as model 153 * 154 * @throws CmsException in case the model file is not found or not valid 155 */ 156 protected CmsXmlContent(CmsObject cms, Locale locale, String modelUri) 157 throws CmsException { 158 159 // init model from given modelUri 160 CmsFile modelFile = cms.readFile(modelUri, CmsResourceFilter.ONLY_VISIBLE_NO_DELETED); 161 CmsXmlContent model = CmsXmlContentFactory.unmarshal(cms, modelFile); 162 163 // initialize macro resolver to use on model file values 164 CmsMacroResolver macroResolver = CmsMacroResolver.newInstance().setCmsObject(cms); 165 macroResolver.setKeepEmptyMacros(true); 166 167 // content defition must be set here since it's used during document creation 168 m_contentDefinition = model.getContentDefinition(); 169 // get the document from the default content 170 Document document = (Document)model.m_document.clone(); 171 // initialize the XML content structure 172 initDocument(cms, document, model.getEncoding(), m_contentDefinition); 173 // resolve eventual macros in the nodes 174 visitAllValuesWith(new CmsXmlContentMacroVisitor(cms, macroResolver)); 175 if (!hasLocale(locale)) { 176 // required locale not present, add it 177 try { 178 addLocale(cms, locale); 179 } catch (CmsXmlException e) { 180 // this can not happen since the locale does not exist 181 } 182 } 183 } 184 185 /** 186 * Create a new XML content based on the given content definiton, 187 * that will have one language node for the given locale all initialized with default values.<p> 188 * 189 * The given encoding is used when marshalling the XML again later.<p> 190 * 191 * @param cms the current users OpenCms content 192 * @param locale the locale to generate the default content for 193 * @param encoding the encoding to use when marshalling the XML content later 194 * @param contentDefinition the content definiton to create the content for 195 */ 196 protected CmsXmlContent(CmsObject cms, Locale locale, String encoding, CmsXmlContentDefinition contentDefinition) { 197 198 // content defition must be set here since it's used during document creation 199 m_contentDefinition = contentDefinition; 200 // create the XML document according to the content definition 201 Document document = m_contentDefinition.createDocument(cms, this, locale); 202 // initialize the XML content structure 203 initDocument(cms, document, encoding, m_contentDefinition); 204 } 205 206 /** 207 * @see org.opencms.xml.I_CmsXmlDocument#addLocale(org.opencms.file.CmsObject, java.util.Locale) 208 */ 209 public void addLocale(CmsObject cms, Locale locale) throws CmsXmlException { 210 211 if (hasLocale(locale)) { 212 throw new CmsXmlException( 213 org.opencms.xml.page.Messages.get().container( 214 org.opencms.xml.page.Messages.ERR_XML_PAGE_LOCALE_EXISTS_1, 215 locale)); 216 } 217 // add element node for Locale 218 m_contentDefinition.createLocale(cms, this, m_document.getRootElement(), locale); 219 // re-initialize the bookmarks 220 initDocument(cms, m_document, m_encoding, m_contentDefinition); 221 } 222 223 /** 224 * Adds a new XML content value for the given element name and locale at the given index position 225 * to this XML content document.<p> 226 * 227 * @param cms the current users OpenCms context 228 * @param path the path to the XML content value element 229 * @param locale the locale where to add the new value 230 * @param index the index where to add the value (relative to all other values of this type) 231 * 232 * @return the created XML content value 233 * 234 * @throws CmsIllegalArgumentException if the given path is invalid 235 * @throws CmsRuntimeException if the element identified by the path already occurred {@link I_CmsXmlSchemaType#getMaxOccurs()} 236 * or the given <code>index</code> is invalid (too high). 237 */ 238 public I_CmsXmlContentValue addValue(CmsObject cms, String path, Locale locale, int index) 239 throws CmsIllegalArgumentException, CmsRuntimeException { 240 241 // get the schema type of the requested path 242 I_CmsXmlSchemaType type = m_contentDefinition.getSchemaType(path); 243 if (type == null) { 244 throw new CmsIllegalArgumentException( 245 Messages.get().container(Messages.ERR_XMLCONTENT_UNKNOWN_ELEM_PATH_SCHEMA_1, path)); 246 } 247 248 Element parentElement; 249 String elementName; 250 CmsXmlContentDefinition contentDefinition; 251 if (CmsXmlUtils.isDeepXpath(path)) { 252 // this is a nested content definition, so the parent element must be in the bookmarks 253 String parentPath = CmsXmlUtils.createXpath(CmsXmlUtils.removeLastXpathElement(path), 1); 254 Object o = getBookmark(parentPath, locale); 255 if (o == null) { 256 throw new CmsIllegalArgumentException( 257 Messages.get().container(Messages.ERR_XMLCONTENT_UNKNOWN_ELEM_PATH_1, path)); 258 } 259 CmsXmlNestedContentDefinition parentValue = (CmsXmlNestedContentDefinition)o; 260 parentElement = parentValue.getElement(); 261 elementName = CmsXmlUtils.getLastXpathElement(path); 262 contentDefinition = parentValue.getNestedContentDefinition(); 263 } else { 264 // the parent element is the locale element 265 parentElement = getLocaleNode(locale); 266 elementName = CmsXmlUtils.removeXpathIndex(path); 267 contentDefinition = m_contentDefinition; 268 } 269 270 int insertIndex; 271 272 if (contentDefinition.getChoiceMaxOccurs() > 0) { 273 // for a choice sequence with maxOccurs we do not check the index position, we rather check if maxOccurs has already been hit 274 // additionally we ensure that the insert index is not too big 275 List<?> choiceSiblings = parentElement.content(); 276 int numSiblings = choiceSiblings != null ? choiceSiblings.size() : 0; 277 278 if ((numSiblings >= contentDefinition.getChoiceMaxOccurs()) || (index > numSiblings)) { 279 throw new CmsRuntimeException( 280 Messages.get().container( 281 Messages.ERR_XMLCONTENT_ADD_ELEM_INVALID_IDX_CHOICE_3, 282 new Integer(index), 283 elementName, 284 parentElement.getUniquePath())); 285 } 286 insertIndex = index; 287 288 } else { 289 // read the XML siblings from the parent node 290 List<Element> siblings = CmsXmlGenericWrapper.elements(parentElement, elementName); 291 292 if (siblings.size() > 0) { 293 // we want to add an element to a sequence, and there are elements already of the same type 294 295 if (siblings.size() >= type.getMaxOccurs()) { 296 // must not allow adding an element if max occurs would be violated 297 throw new CmsRuntimeException( 298 Messages.get().container( 299 Messages.ERR_XMLCONTENT_ELEM_MAXOCCURS_2, 300 elementName, 301 new Integer(type.getMaxOccurs()))); 302 } 303 304 if (index > siblings.size()) { 305 // index position behind last element of the list 306 throw new CmsRuntimeException( 307 Messages.get().container( 308 Messages.ERR_XMLCONTENT_ADD_ELEM_INVALID_IDX_3, 309 new Integer(index), 310 new Integer(siblings.size()))); 311 } 312 313 // check for offset required to append beyond last position 314 int offset = (index == siblings.size()) ? 1 : 0; 315 // get the element from the parent at the selected position 316 Element sibling = siblings.get(index - offset); 317 // check position of the node in the parent node content 318 insertIndex = sibling.getParent().content().indexOf(sibling) + offset; 319 } else { 320 // we want to add an element to a sequence, but there are no elements of the same type yet 321 322 if (index > 0) { 323 // since the element does not occur, index must be 0 324 throw new CmsRuntimeException( 325 Messages.get().container( 326 Messages.ERR_XMLCONTENT_ADD_ELEM_INVALID_IDX_2, 327 new Integer(index), 328 elementName)); 329 } 330 331 // check where in the type sequence the type should appear 332 int typeIndex = contentDefinition.getTypeSequence().indexOf(type); 333 if (typeIndex == 0) { 334 // this is the first type, so we just add at the very first position 335 insertIndex = 0; 336 } else { 337 338 // create a list of all element names that should occur before the selected type 339 List<String> previousTypeNames = new ArrayList<String>(); 340 for (int i = 0; i < typeIndex; i++) { 341 I_CmsXmlSchemaType t = contentDefinition.getTypeSequence().get(i); 342 previousTypeNames.add(t.getName()); 343 } 344 345 // iterate all elements of the parent node 346 Iterator<Node> i = CmsXmlGenericWrapper.content(parentElement).iterator(); 347 int pos = 0; 348 while (i.hasNext()) { 349 Node node = i.next(); 350 if (node instanceof Element) { 351 if (!previousTypeNames.contains(node.getName())) { 352 // the element name is NOT in the list of names that occurs before the selected type, 353 // so it must be an element that occurs AFTER the type 354 break; 355 } 356 } 357 pos++; 358 } 359 insertIndex = pos; 360 } 361 } 362 } 363 364 // just append the new element at the calculated position 365 I_CmsXmlContentValue newValue = addValue(cms, parentElement, type, locale, insertIndex); 366 367 // re-initialize this XML content 368 initDocument(m_document, m_encoding, m_contentDefinition); 369 370 // return the value instance that was stored in the bookmarks 371 // just returning "newValue" isn't enough since this instance is NOT stored in the bookmarks 372 return getBookmark(getBookmarkName(newValue.getPath(), locale)); 373 } 374 375 /** 376 * @see java.lang.Object#clone() 377 */ 378 @Override 379 public CmsXmlContent clone() { 380 381 CmsXmlContent clone = new CmsXmlContent(); 382 clone.m_autoCorrectionEnabled = m_autoCorrectionEnabled; 383 clone.m_contentDefinition = m_contentDefinition; 384 clone.m_conversion = m_conversion; 385 clone.m_document = (Document)(m_document.clone()); 386 clone.m_encoding = m_encoding; 387 clone.m_file = m_file; 388 clone.initDocument(); 389 return clone; 390 } 391 392 /** 393 * Copies the content of the given source locale to the given destination locale in this XML document.<p> 394 * 395 * @param source the source locale 396 * @param destination the destination loacle 397 * @param elements the set of elements to copy 398 * @throws CmsXmlException if something goes wrong 399 */ 400 public void copyLocale(Locale source, Locale destination, Set<String> elements) throws CmsXmlException { 401 402 if (!hasLocale(source)) { 403 throw new CmsXmlException( 404 Messages.get().container(org.opencms.xml.Messages.ERR_LOCALE_NOT_AVAILABLE_1, source)); 405 } 406 if (hasLocale(destination)) { 407 throw new CmsXmlException( 408 Messages.get().container(org.opencms.xml.Messages.ERR_LOCALE_ALREADY_EXISTS_1, destination)); 409 } 410 411 Element sourceElement = null; 412 Element rootNode = m_document.getRootElement(); 413 Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(rootNode); 414 String localeStr = source.toString(); 415 while (i.hasNext()) { 416 Element element = i.next(); 417 String language = element.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, null); 418 if ((language != null) && (localeStr.equals(language))) { 419 // detach node with the locale 420 sourceElement = createDeepElementCopy(element, elements); 421 // there can be only one node for the locale 422 break; 423 } 424 } 425 426 if (sourceElement == null) { 427 // should not happen since this was checked already, just to make sure... 428 throw new CmsXmlException( 429 Messages.get().container(org.opencms.xml.Messages.ERR_LOCALE_NOT_AVAILABLE_1, source)); 430 } 431 432 // switch locale value in attribute of copied node 433 sourceElement.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, destination.toString()); 434 // attach the copied node to the root node 435 rootNode.add(sourceElement); 436 437 // re-initialize the document bookmarks 438 initDocument(m_document, m_encoding, getContentDefinition()); 439 } 440 441 /** 442 * Returns all simple type sub values.<p> 443 * 444 * @param value the value 445 * 446 * @return the simple type sub values 447 */ 448 public List<I_CmsXmlContentValue> getAllSimpleSubValues(I_CmsXmlContentValue value) { 449 450 List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>(); 451 for (I_CmsXmlContentValue subValue : getSubValues(value.getPath(), value.getLocale())) { 452 if (subValue.isSimpleType()) { 453 result.add(subValue); 454 } else { 455 result.addAll(getAllSimpleSubValues(subValue)); 456 } 457 } 458 return result; 459 } 460 461 /** 462 * Returns the list of choice options for the given xpath in the selected locale.<p> 463 * 464 * In case the xpath does not select a nested choice content definition, 465 * or in case the xpath does not exist at all, <code>null</code> is returned.<p> 466 * 467 * @param xpath the xpath to check the choice options for 468 * @param locale the locale to check 469 * 470 * @return the list of choice options for the given xpath 471 */ 472 public List<I_CmsXmlSchemaType> getChoiceOptions(String xpath, Locale locale) { 473 474 I_CmsXmlSchemaType type = m_contentDefinition.getSchemaType(xpath); 475 if (type == null) { 476 // the xpath is not valid in the document 477 return null; 478 } 479 if (!type.isChoiceType() && !type.isChoiceOption()) { 480 // type is neither defining a choice nor part of a choice 481 return null; 482 } 483 484 if (type.isChoiceType()) { 485 // the type defines a choice sequence 486 CmsXmlContentDefinition cd = ((CmsXmlNestedContentDefinition)type).getNestedContentDefinition(); 487 return cd.getTypeSequence(); 488 } 489 490 // type must be a choice option 491 I_CmsXmlContentValue value = getValue(xpath, locale); 492 if ((value == null) || (value.getContentDefinition().getChoiceMaxOccurs() > 1)) { 493 // value does not exist in the document or is a multiple choice value 494 return type.getContentDefinition().getTypeSequence(); 495 } 496 497 // value must be a single choice that already exists in the document, so we must return null 498 return null; 499 } 500 501 /** 502 * @see org.opencms.xml.I_CmsXmlDocument#getContentDefinition() 503 */ 504 public CmsXmlContentDefinition getContentDefinition() { 505 506 return m_contentDefinition; 507 } 508 509 /** 510 * @see org.opencms.xml.I_CmsXmlDocument#getHandler() 511 */ 512 public I_CmsXmlContentHandler getHandler() { 513 514 return getContentDefinition().getContentHandler(); 515 } 516 517 /** 518 * @see org.opencms.xml.A_CmsXmlDocument#getLinkProcessor(org.opencms.file.CmsObject, org.opencms.staticexport.CmsLinkTable) 519 */ 520 public CmsLinkProcessor getLinkProcessor(CmsObject cms, CmsLinkTable linkTable) { 521 522 // initialize link processor 523 String relativeRoot = null; 524 if (m_file != null) { 525 relativeRoot = CmsResource.getParentFolder(cms.getSitePath(m_file)); 526 } 527 return new CmsLinkProcessor(cms, linkTable, getEncoding(), relativeRoot); 528 } 529 530 /** 531 * Returns the XML root element node for the given locale.<p> 532 * 533 * @param locale the locale to get the root element for 534 * 535 * @return the XML root element node for the given locale 536 * 537 * @throws CmsRuntimeException if no language element is found in the document 538 */ 539 public Element getLocaleNode(Locale locale) throws CmsRuntimeException { 540 541 String localeStr = locale.toString(); 542 Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(m_document.getRootElement()); 543 while (i.hasNext()) { 544 Element element = i.next(); 545 if (localeStr.equals(element.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE))) { 546 // language element found, return it 547 return element; 548 } 549 } 550 // language element was not found 551 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_LOCALE_1, locale)); 552 } 553 554 /** 555 * Returns all simple type values below a given path.<p> 556 * 557 * @param elementPath the element path 558 * @param locale the content locale 559 * 560 * @return the simple type values 561 */ 562 public List<I_CmsXmlContentValue> getSimpleValuesBelowPath(String elementPath, Locale locale) { 563 564 List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>(); 565 for (I_CmsXmlContentValue value : getValuesByPath(elementPath, locale)) { 566 if (value.isSimpleType()) { 567 result.add(value); 568 } else { 569 result.addAll(getAllSimpleSubValues(value)); 570 } 571 } 572 573 return result; 574 } 575 576 /** 577 * Returns the list of sub-value for the given xpath in the selected locale.<p> 578 * 579 * @param path the xpath to look up the sub-value for 580 * @param locale the locale to use 581 * 582 * @return the list of sub-value for the given xpath in the selected locale 583 */ 584 @Override 585 public List<I_CmsXmlContentValue> getSubValues(String path, Locale locale) { 586 587 List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>(); 588 String bookmark = getBookmarkName(CmsXmlUtils.createXpath(path, 1), locale); 589 int depth = CmsResource.getPathLevel(bookmark) + 1; 590 Iterator<String> i = getBookmarks().iterator(); 591 while (i.hasNext()) { 592 String bm = i.next(); 593 if (bm.startsWith(bookmark) && (CmsResource.getPathLevel(bm) == depth)) { 594 result.add(getBookmark(bm)); 595 } 596 } 597 if (result.size() > 0) { 598 Collections.sort(result, COMPARE_INDEX); 599 } 600 return result; 601 } 602 603 /** 604 * Returns all values of the given element path.<p> 605 * 606 * @param elementPath the element path 607 * @param locale the content locale 608 * 609 * @return the values 610 */ 611 public List<I_CmsXmlContentValue> getValuesByPath(String elementPath, Locale locale) { 612 613 String[] pathElements = elementPath.split("/"); 614 List<I_CmsXmlContentValue> values = getValues(pathElements[0], locale); 615 for (int i = 1; i < pathElements.length; i++) { 616 List<I_CmsXmlContentValue> subValues = new ArrayList<I_CmsXmlContentValue>(); 617 for (I_CmsXmlContentValue value : values) { 618 subValues.addAll(getValues(CmsXmlUtils.concatXpath(value.getPath(), pathElements[i]), locale)); 619 } 620 if (subValues.isEmpty()) { 621 values = Collections.emptyList(); 622 break; 623 } 624 values = subValues; 625 } 626 return values; 627 } 628 629 /** 630 * Returns the value sequence for the selected element xpath in this XML content.<p> 631 * 632 * If the given element xpath is not valid according to the schema of this XML content, 633 * <code>null</code> is returned.<p> 634 * 635 * @param xpath the element xpath to get the value sequence for 636 * @param locale the locale to get the value sequence for 637 * 638 * @return the value sequence for the selected element name in this XML content 639 */ 640 public CmsXmlContentValueSequence getValueSequence(String xpath, Locale locale) { 641 642 I_CmsXmlSchemaType type = m_contentDefinition.getSchemaType(xpath); 643 if (type == null) { 644 return null; 645 } 646 return new CmsXmlContentValueSequence(xpath, locale, this); 647 } 648 649 /** 650 * Returns <code>true</code> if choice options exist for the given xpath in the selected locale.<p> 651 * 652 * In case the xpath does not select a nested choice content definition, 653 * or in case the xpath does not exist at all, <code>false</code> is returned.<p> 654 * 655 * @param xpath the xpath to check the choice options for 656 * @param locale the locale to check 657 * 658 * @return <code>true</code> if choice options exist for the given xpath in the selected locale 659 */ 660 public boolean hasChoiceOptions(String xpath, Locale locale) { 661 662 List<I_CmsXmlSchemaType> options = getChoiceOptions(xpath, locale); 663 if ((options == null) || (options.size() <= 1)) { 664 return false; 665 } 666 return true; 667 } 668 669 /** 670 * @see org.opencms.xml.A_CmsXmlDocument#isAutoCorrectionEnabled() 671 */ 672 @Override 673 public boolean isAutoCorrectionEnabled() { 674 675 return m_autoCorrectionEnabled; 676 } 677 678 /** 679 * Checks if the content is locale independent.<p> 680 * 681 * @return true if the content is locale independent 682 */ 683 public boolean isLocaleIndependent() { 684 685 CmsFile file = getFile(); 686 if (CmsResourceTypeXmlContainerPage.isContainerPage(file) 687 || OpenCms.getResourceManager().matchResourceType( 688 CmsResourceTypeXmlContainerPage.GROUP_CONTAINER_TYPE_NAME, 689 file.getTypeId()) 690 || OpenCms.getResourceManager().matchResourceType( 691 CmsResourceTypeXmlContainerPage.INHERIT_CONTAINER_CONFIG_TYPE_NAME, 692 file.getTypeId())) { 693 return true; 694 } 695 696 try { 697 I_CmsResourceType resourceType = OpenCms.getResourceManager().getResourceType(file); 698 if ((resourceType instanceof CmsResourceTypeLocaleIndependentXmlContent) 699 || (resourceType instanceof CmsResourceTypeXmlAdeConfiguration)) { 700 return true; 701 } 702 } catch (Exception e) { 703 // ignore 704 } 705 return false; 706 707 } 708 709 /** 710 * Removes an existing XML content value of the given element name and locale at the given index position 711 * from this XML content document.<p> 712 * 713 * @param name the name of the XML content value element 714 * @param locale the locale where to remove the value 715 * @param index the index where to remove the value (relative to all other values of this type) 716 */ 717 public void removeValue(String name, Locale locale, int index) { 718 719 // first get the value from the selected locale and index 720 I_CmsXmlContentValue value = getValue(name, locale, index); 721 722 if (!value.isChoiceOption()) { 723 // check for the min / max occurs constrains 724 List<I_CmsXmlContentValue> values = getValues(name, locale); 725 if (values.size() <= value.getMinOccurs()) { 726 // must not allow removing an element if min occurs would be violated 727 throw new CmsRuntimeException( 728 Messages.get().container( 729 Messages.ERR_XMLCONTENT_ELEM_MINOCCURS_2, 730 name, 731 new Integer(value.getMinOccurs()))); 732 } 733 } 734 735 // detach the value node from the XML document 736 value.getElement().detach(); 737 738 // re-initialize this XML content 739 initDocument(m_document, m_encoding, m_contentDefinition); 740 } 741 742 /** 743 * Resolves the mappings for all values of this XML content.<p> 744 * 745 * @param cms the current users OpenCms context 746 */ 747 public void resolveMappings(CmsObject cms) { 748 749 // iterate through all initialized value nodes in this XML content 750 CmsXmlContentMappingVisitor visitor = new CmsXmlContentMappingVisitor(cms, this); 751 visitAllValuesWith(visitor); 752 } 753 754 /** 755 * Sets the flag to control if auto correction is enabled when saving this XML content.<p> 756 * 757 * @param value the flag to control if auto correction is enabled when saving this XML content 758 */ 759 public void setAutoCorrectionEnabled(boolean value) { 760 761 m_autoCorrectionEnabled = value; 762 } 763 764 /** 765 * Synchronizes the locale independent fields for the given locale.<p> 766 * 767 * @param cms the cms context 768 * @param skipPaths the paths to skip 769 * @param sourceLocale the source locale 770 */ 771 public void synchronizeLocaleIndependentValues(CmsObject cms, Collection<String> skipPaths, Locale sourceLocale) { 772 773 if (getContentDefinition().getContentHandler().hasSynchronizedElements() && (getLocales().size() > 1)) { 774 for (String elementPath : getContentDefinition().getContentHandler().getSynchronizations()) { 775 synchronizeElement(cms, elementPath, skipPaths, sourceLocale); 776 } 777 } 778 } 779 780 /** 781 * @see org.opencms.xml.I_CmsXmlDocument#validate(org.opencms.file.CmsObject) 782 */ 783 public CmsXmlContentErrorHandler validate(CmsObject cms) { 784 785 // iterate through all initialized value nodes in this XML content 786 CmsXmlContentValidationVisitor visitor = new CmsXmlContentValidationVisitor(cms); 787 visitAllValuesWith(visitor); 788 789 return visitor.getErrorHandler(); 790 } 791 792 /** 793 * Visits all values of this XML content with the given value visitor.<p> 794 * 795 * Please note that the order in which the values are visited may NOT be the 796 * order they appear in the XML document. It is ensured that the the parent 797 * of a nested value is visited before the element it contains.<p> 798 * 799 * @param visitor the value visitor implementation to visit the values with 800 */ 801 public void visitAllValuesWith(I_CmsXmlContentValueVisitor visitor) { 802 803 List<String> bookmarks = new ArrayList<String>(getBookmarks()); 804 Collections.sort(bookmarks); 805 806 for (int i = 0; i < bookmarks.size(); i++) { 807 808 String key = bookmarks.get(i); 809 I_CmsXmlContentValue value = getBookmark(key); 810 visitor.visit(value); 811 } 812 } 813 814 /** 815 * Creates a new bookmark for the given element.<p> 816 * 817 * @param element the element to create the bookmark for 818 * @param locale the locale 819 * @param parent the parent node of the element 820 * @param parentPath the parent's path 821 * @param parentDef the parent's content definition 822 */ 823 protected void addBookmarkForElement( 824 Element element, 825 Locale locale, 826 Element parent, 827 String parentPath, 828 CmsXmlContentDefinition parentDef) { 829 830 int elemIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(parent)); 831 String elemPath = CmsXmlUtils.concatXpath( 832 parentPath, 833 CmsXmlUtils.createXpathElement(element.getName(), elemIndex)); 834 I_CmsXmlSchemaType elemSchemaType = parentDef.getSchemaType(element.getName()); 835 I_CmsXmlContentValue elemValue = elemSchemaType.createValue(this, element, locale); 836 addBookmark(elemPath, locale, true, elemValue); 837 } 838 839 /** 840 * Adds a bookmark for the given value.<p> 841 * 842 * @param value the value to bookmark 843 * @param path the lookup path to use for the bookmark 844 * @param locale the locale to use for the bookmark 845 * @param enabled if true, the value is enabled, if false it is disabled 846 */ 847 protected void addBookmarkForValue(I_CmsXmlContentValue value, String path, Locale locale, boolean enabled) { 848 849 addBookmark(path, locale, enabled, value); 850 } 851 852 /** 853 * Adds a new XML schema type with the default value to the given parent node.<p> 854 * 855 * @param cms the cms context 856 * @param parent the XML parent element to add the new value to 857 * @param type the type of the value to add 858 * @param locale the locale to add the new value for 859 * @param insertIndex the index in the XML document where to add the XML node 860 * 861 * @return the created XML content value 862 */ 863 protected I_CmsXmlContentValue addValue( 864 CmsObject cms, 865 Element parent, 866 I_CmsXmlSchemaType type, 867 Locale locale, 868 int insertIndex) { 869 870 // first generate the XML element for the new value 871 Element element = type.generateXml(cms, this, parent, locale); 872 // detach the XML element from the appended position in order to insert it at the required position 873 element.detach(); 874 // add the XML element at the required position in the parent XML node 875 CmsXmlGenericWrapper.content(parent).add(insertIndex, element); 876 // create the type and return it 877 I_CmsXmlContentValue value = type.createValue(this, element, locale); 878 // generate the default value again - required for nested mappings because only now the full path is available 879 String defaultValue = m_contentDefinition.getContentHandler().getDefault(cms, value, locale); 880 if (defaultValue != null) { 881 // only if there is a default value available use it to overwrite the initial default 882 value.setStringValue(cms, defaultValue); 883 } 884 // finally return the value 885 return value; 886 } 887 888 /** 889 * @see org.opencms.xml.A_CmsXmlDocument#getBookmark(java.lang.String) 890 */ 891 @Override 892 protected I_CmsXmlContentValue getBookmark(String bookmark) { 893 894 // allows package classes to directly access the bookmark information of the XML content 895 return super.getBookmark(bookmark); 896 } 897 898 /** 899 * @see org.opencms.xml.A_CmsXmlDocument#getBookmarks() 900 */ 901 @Override 902 protected Set<String> getBookmarks() { 903 904 // allows package classes to directly access the bookmark information of the XML content 905 return super.getBookmarks(); 906 } 907 908 /** 909 * Returns the content definition object for this xml content object.<p> 910 * 911 * @param resolver the XML entity resolver to use, required for VFS access 912 * 913 * @return the content definition object for this xml content object 914 * 915 * @throws CmsRuntimeException if the schema location attribute (<code>systemId</code>)cannot be found, 916 * parsing of the schema fails, an underlying IOException occurs or unmarshalling fails 917 * 918 */ 919 protected CmsXmlContentDefinition getContentDefinition(EntityResolver resolver) throws CmsRuntimeException { 920 921 String schemaLocation = m_document.getRootElement().attributeValue( 922 I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION); 923 // Note regarding exception handling: 924 // Since this object already is a valid XML content object, 925 // it must have a valid schema, otherwise it would not exist. 926 // Therefore the exceptions should never be really thrown. 927 if (schemaLocation == null) { 928 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_SCHEMA_0)); 929 } 930 931 try { 932 return CmsXmlContentDefinition.unmarshal(schemaLocation, resolver); 933 } catch (SAXException e) { 934 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XML_SCHEMA_PARSE_1, schemaLocation), e); 935 } catch (IOException e) { 936 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XML_SCHEMA_IO_1, schemaLocation), e); 937 } catch (CmsXmlException e) { 938 throw new CmsRuntimeException( 939 Messages.get().container(Messages.ERR_XMLCONTENT_UNMARSHAL_1, schemaLocation), 940 e); 941 } 942 } 943 944 /** 945 * Initializes an XML document based on the provided document, encoding and content definition.<p> 946 * 947 * Checks the links and removes invalid ones in the initialized document.<p> 948 * 949 * @param cms the current users OpenCms content 950 * @param document the base XML document to use for initializing 951 * @param encoding the encoding to use when marshalling the document later 952 * @param definition the content definition to use 953 */ 954 protected void initDocument(CmsObject cms, Document document, String encoding, CmsXmlContentDefinition definition) { 955 956 initDocument(document, encoding, definition); 957 // check invalid links 958 if (cms != null) { 959 // this will remove all invalid links 960 getHandler().invalidateBrokenLinks(cms, this); 961 } 962 } 963 964 /** 965 * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition) 966 */ 967 @Override 968 protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) { 969 970 m_document = document; 971 m_contentDefinition = definition; 972 m_encoding = CmsEncoder.lookupEncoding(encoding, encoding); 973 m_elementLocales = new HashMap<String, Set<Locale>>(); 974 m_elementNames = new HashMap<Locale, Set<String>>(); 975 m_locales = new HashSet<Locale>(); 976 clearBookmarks(); 977 978 // initialize the bookmarks 979 for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(m_document.getRootElement()); i.hasNext();) { 980 Element node = i.next(); 981 try { 982 Locale locale = CmsLocaleManager.getLocale( 983 node.attribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE).getValue()); 984 985 addLocale(locale); 986 processSchemaNode(node, null, locale, definition); 987 } catch (NullPointerException e) { 988 LOG.error(Messages.get().getBundle().key(Messages.LOG_XMLCONTENT_INIT_BOOKMARKS_0), e); 989 } 990 } 991 992 } 993 994 /** 995 * Processes a document node and extracts the values of the node according to the provided XML 996 * content definition.<p> 997 * 998 * @param root the root node element to process 999 * @param rootPath the Xpath of the root node in the document 1000 * @param locale the locale 1001 * @param definition the XML content definition to use for processing the values 1002 */ 1003 protected void processSchemaNode(Element root, String rootPath, Locale locale, CmsXmlContentDefinition definition) { 1004 1005 // iterate all XML nodes 1006 List<Node> content = CmsXmlGenericWrapper.content(root); 1007 for (int i = content.size() - 1; i >= 0; i--) { 1008 Node node = content.get(i); 1009 if (!(node instanceof Element)) { 1010 // this node is not an element, so it must be a white space text node, remove it 1011 node.detach(); 1012 } else { 1013 // node must be an element 1014 Element element = (Element)node; 1015 String name = element.getName(); 1016 int xpathIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(root)); 1017 1018 // build the Xpath expression for the current node 1019 String path; 1020 if (rootPath != null) { 1021 StringBuffer b = new StringBuffer(rootPath.length() + name.length() + 6); 1022 b.append(rootPath); 1023 b.append('/'); 1024 b.append(CmsXmlUtils.createXpathElement(name, xpathIndex)); 1025 path = b.toString(); 1026 } else { 1027 path = CmsXmlUtils.createXpathElement(name, xpathIndex); 1028 } 1029 1030 // create a XML content value element 1031 I_CmsXmlSchemaType schemaType = definition.getSchemaType(name); 1032 1033 if (schemaType != null) { 1034 // directly add simple type to schema 1035 I_CmsXmlContentValue value = schemaType.createValue(this, element, locale); 1036 addBookmark(path, locale, true, value); 1037 1038 if (!schemaType.isSimpleType()) { 1039 // recurse for nested schema 1040 CmsXmlNestedContentDefinition nestedSchema = (CmsXmlNestedContentDefinition)schemaType; 1041 processSchemaNode(element, path, locale, nestedSchema.getNestedContentDefinition()); 1042 } 1043 } else { 1044 // unknown XML node name according to schema 1045 if (LOG.isWarnEnabled()) { 1046 LOG.warn( 1047 Messages.get().getBundle().key( 1048 Messages.LOG_XMLCONTENT_INVALID_ELEM_2, 1049 name, 1050 definition.getSchemaLocation())); 1051 } 1052 } 1053 } 1054 } 1055 } 1056 1057 /** 1058 * Sets the file this XML content is written to.<p> 1059 * 1060 * @param file the file this XML content content is written to 1061 */ 1062 protected void setFile(CmsFile file) { 1063 1064 m_file = file; 1065 } 1066 1067 /** 1068 * Ensures the parent values to the given path are created.<p> 1069 * 1070 * @param cms the cms context 1071 * @param valuePath the value path 1072 * @param locale the content locale 1073 */ 1074 private void ensureParentValues(CmsObject cms, String valuePath, Locale locale) { 1075 1076 if (valuePath.contains("/")) { 1077 String parentPath = valuePath.substring(0, valuePath.lastIndexOf("/")); 1078 if (!hasValue(parentPath, locale)) { 1079 ensureParentValues(cms, parentPath, locale); 1080 int index = CmsXmlUtils.getXpathIndexInt(parentPath) - 1; 1081 addValue(cms, parentPath, locale, index); 1082 } 1083 } 1084 } 1085 1086 /** 1087 * Removes all surplus values of locale independent fields in the other locales.<p> 1088 * 1089 * @param elementPath the element path 1090 * @param valueCount the value count 1091 * @param sourceLocale the source locale 1092 */ 1093 private void removeSurplusValuesInOtherLocales(String elementPath, int valueCount, Locale sourceLocale) { 1094 1095 for (Locale locale : getLocales()) { 1096 if (locale.equals(sourceLocale)) { 1097 continue; 1098 } 1099 List<I_CmsXmlContentValue> localeValues = getValues(elementPath, locale); 1100 for (int i = valueCount; i < localeValues.size(); i++) { 1101 removeValue(elementPath, locale, 0); 1102 } 1103 } 1104 } 1105 1106 /** 1107 * Removes all values of the given path in the other locales.<p> 1108 * 1109 * @param elementPath the element path 1110 * @param sourceLocale the source locale 1111 */ 1112 private void removeValuesInOtherLocales(String elementPath, Locale sourceLocale) { 1113 1114 for (Locale locale : getLocales()) { 1115 if (locale.equals(sourceLocale)) { 1116 continue; 1117 } 1118 while (hasValue(elementPath, locale)) { 1119 removeValue(elementPath, locale, 0); 1120 } 1121 } 1122 } 1123 1124 /** 1125 * Sets the value in all other locales.<p> 1126 * 1127 * @param cms the cms context 1128 * @param value the value 1129 * @param requiredParent the path to the required parent value 1130 */ 1131 private void setValueForOtherLocales(CmsObject cms, I_CmsXmlContentValue value, String requiredParent) { 1132 1133 if (!value.isSimpleType()) { 1134 throw new IllegalArgumentException(); 1135 } 1136 for (Locale locale : getLocales()) { 1137 if (locale.equals(value.getLocale())) { 1138 continue; 1139 } 1140 String valuePath = value.getPath(); 1141 if (CmsStringUtil.isEmptyOrWhitespaceOnly(requiredParent) || hasValue(requiredParent, locale)) { 1142 ensureParentValues(cms, valuePath, locale); 1143 if (hasValue(valuePath, locale)) { 1144 I_CmsXmlContentValue localeValue = getValue(valuePath, locale); 1145 localeValue.setStringValue(cms, value.getStringValue(cms)); 1146 } else { 1147 int index = CmsXmlUtils.getXpathIndexInt(valuePath) - 1; 1148 I_CmsXmlContentValue localeValue = addValue(cms, valuePath, locale, index); 1149 localeValue.setStringValue(cms, value.getStringValue(cms)); 1150 } 1151 } 1152 } 1153 } 1154 1155 /** 1156 * Synchronizes the values for the given element path.<p> 1157 * 1158 * @param cms the cms context 1159 * @param elementPath the element path 1160 * @param skipPaths the paths to skip 1161 * @param sourceLocale the source locale 1162 */ 1163 private void synchronizeElement( 1164 CmsObject cms, 1165 String elementPath, 1166 Collection<String> skipPaths, 1167 Locale sourceLocale) { 1168 1169 if (elementPath.contains("/")) { 1170 String parentPath = CmsXmlUtils.removeLastXpathElement(elementPath); 1171 List<I_CmsXmlContentValue> parentValues = getValuesByPath(parentPath, sourceLocale); 1172 String elementName = CmsXmlUtils.getLastXpathElement(elementPath); 1173 for (I_CmsXmlContentValue parentValue : parentValues) { 1174 String valuePath = CmsXmlUtils.concatXpath(parentValue.getPath(), elementName); 1175 boolean skip = false; 1176 for (String skipPath : skipPaths) { 1177 if (valuePath.startsWith(skipPath)) { 1178 skip = true; 1179 break; 1180 } 1181 } 1182 if (!skip) { 1183 if (hasValue(valuePath, sourceLocale)) { 1184 List<I_CmsXmlContentValue> subValues = getValues(valuePath, sourceLocale); 1185 removeSurplusValuesInOtherLocales(elementPath, subValues.size(), sourceLocale); 1186 for (I_CmsXmlContentValue value : subValues) { 1187 if (value.isSimpleType()) { 1188 setValueForOtherLocales(cms, value, CmsXmlUtils.removeLastXpathElement(valuePath)); 1189 } else { 1190 List<I_CmsXmlContentValue> simpleValues = getAllSimpleSubValues(value); 1191 for (I_CmsXmlContentValue simpleValue : simpleValues) { 1192 setValueForOtherLocales(cms, simpleValue, parentValue.getPath()); 1193 } 1194 } 1195 } 1196 } else { 1197 removeValuesInOtherLocales(valuePath, sourceLocale); 1198 } 1199 } 1200 } 1201 } else { 1202 if (hasValue(elementPath, sourceLocale)) { 1203 List<I_CmsXmlContentValue> subValues = getValues(elementPath, sourceLocale); 1204 removeSurplusValuesInOtherLocales(elementPath, subValues.size(), sourceLocale); 1205 for (I_CmsXmlContentValue value : subValues) { 1206 if (value.isSimpleType()) { 1207 setValueForOtherLocales(cms, value, null); 1208 } else { 1209 List<I_CmsXmlContentValue> simpleValues = getAllSimpleSubValues(value); 1210 for (I_CmsXmlContentValue simpleValue : simpleValues) { 1211 setValueForOtherLocales(cms, simpleValue, null); 1212 } 1213 } 1214 } 1215 } else { 1216 removeValuesInOtherLocales(elementPath, sourceLocale); 1217 } 1218 } 1219 } 1220}