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