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