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