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