001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.xml.content; 029 030import org.opencms.file.CmsFile; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsResource; 033import org.opencms.file.CmsResourceFilter; 034import org.opencms.file.types.CmsResourceTypeLocaleIndependentXmlContent; 035import org.opencms.file.types.CmsResourceTypeXmlAdeConfiguration; 036import org.opencms.file.types.CmsResourceTypeXmlContainerPage; 037import org.opencms.file.types.I_CmsResourceType; 038import org.opencms.i18n.CmsEncoder; 039import org.opencms.i18n.CmsLocaleManager; 040import org.opencms.main.CmsException; 041import org.opencms.main.CmsIllegalArgumentException; 042import org.opencms.main.CmsLog; 043import org.opencms.main.CmsRuntimeException; 044import org.opencms.main.OpenCms; 045import org.opencms.staticexport.CmsLinkProcessor; 046import org.opencms.staticexport.CmsLinkTable; 047import org.opencms.util.CmsMacroResolver; 048import org.opencms.util.CmsStringUtil; 049import org.opencms.xml.A_CmsXmlDocument; 050import org.opencms.xml.CmsXmlContentDefinition; 051import org.opencms.xml.CmsXmlException; 052import org.opencms.xml.CmsXmlGenericWrapper; 053import org.opencms.xml.CmsXmlUtils; 054import org.opencms.xml.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 * Returns <code>true</code> if choice options exist for the given xpath in the selected locale.<p> 685 * 686 * In case the xpath does not select a nested choice content definition, 687 * or in case the xpath does not exist at all, <code>false</code> is returned.<p> 688 * 689 * @param xpath the xpath to check the choice options for 690 * @param locale the locale to check 691 * 692 * @return <code>true</code> if choice options exist for the given xpath in the selected locale 693 */ 694 public boolean hasChoiceOptions(String xpath, Locale locale) { 695 696 List<I_CmsXmlSchemaType> options = getChoiceOptions(xpath, locale); 697 if ((options == null) || (options.size() <= 1)) { 698 return false; 699 } 700 return true; 701 } 702 703 /** 704 * Checks if any broken links have been invalidated in this content. 705 * 706 * @return true if broken links have been invalidated 707 */ 708 public boolean hasInvalidatedBrokenLinks() { 709 710 return m_hasInvalidatedBrokenLinks; 711 } 712 713 /** 714 * @see org.opencms.xml.A_CmsXmlDocument#isAutoCorrectionEnabled() 715 */ 716 @Override 717 public boolean isAutoCorrectionEnabled() { 718 719 return m_autoCorrectionEnabled; 720 } 721 722 /** 723 * Checks if the content is locale independent.<p> 724 * 725 * @return true if the content is locale independent 726 */ 727 public boolean isLocaleIndependent() { 728 729 CmsFile file = getFile(); 730 if (CmsResourceTypeXmlContainerPage.isContainerPage(file) 731 || OpenCms.getResourceManager().matchResourceType( 732 CmsResourceTypeXmlContainerPage.GROUP_CONTAINER_TYPE_NAME, 733 file.getTypeId()) 734 || OpenCms.getResourceManager().matchResourceType( 735 CmsResourceTypeXmlContainerPage.INHERIT_CONTAINER_CONFIG_TYPE_NAME, 736 file.getTypeId())) { 737 return true; 738 } 739 740 try { 741 I_CmsResourceType resourceType = OpenCms.getResourceManager().getResourceType(file); 742 if ((resourceType instanceof CmsResourceTypeLocaleIndependentXmlContent) 743 || (resourceType instanceof CmsResourceTypeXmlAdeConfiguration)) { 744 return true; 745 } 746 } catch (Exception e) { 747 // ignore 748 } 749 return false; 750 751 } 752 753 /** 754 * Checks if a version transformation was used when creating this content object. 755 * 756 * @return true if a version transformation was used when creating this content object 757 */ 758 public boolean isTransformedVersion() { 759 760 return m_isTransformedVersion; 761 } 762 763 /** 764 * Removes an existing XML content value of the given element name and locale at the given index position 765 * from this XML content document.<p> 766 * 767 * @param name the name of the XML content value element 768 * @param locale the locale where to remove the value 769 * @param index the index where to remove the value (relative to all other values of this type) 770 */ 771 public void removeValue(String name, Locale locale, int index) { 772 773 // first get the value from the selected locale and index 774 I_CmsXmlContentValue value = getValue(name, locale, index); 775 776 if (!value.isChoiceOption()) { 777 // check for the min / max occurs constrains 778 List<I_CmsXmlContentValue> values = getValues(name, locale); 779 if (values.size() <= value.getMinOccurs()) { 780 // must not allow removing an element if min occurs would be violated 781 throw new CmsRuntimeException( 782 Messages.get().container( 783 Messages.ERR_XMLCONTENT_ELEM_MINOCCURS_2, 784 name, 785 Integer.valueOf(value.getMinOccurs()))); 786 } 787 } 788 789 // detach the value node from the XML document 790 value.getElement().detach(); 791 792 // re-initialize this XML content 793 initDocument(m_document, m_encoding, m_contentDefinition); 794 } 795 796 /** 797 * Resolves the mappings for all values of this XML content.<p> 798 * 799 * @param cms the current users OpenCms context 800 */ 801 public void resolveMappings(CmsObject cms) { 802 803 // iterate through all initialized value nodes in this XML content 804 CmsXmlContentMappingVisitor visitor = new CmsXmlContentMappingVisitor(cms, this); 805 visitAllValuesWith(visitor); 806 } 807 808 /** 809 * Sets the flag to control if auto correction is enabled when saving this XML content.<p> 810 * 811 * @param value the flag to control if auto correction is enabled when saving this XML content 812 */ 813 public void setAutoCorrectionEnabled(boolean value) { 814 815 m_autoCorrectionEnabled = value; 816 } 817 818 /** 819 * Synchronizes the locale independent fields for the given locale.<p> 820 * 821 * @param cms the cms context 822 * @param skipPaths the paths to skip 823 * @param sourceLocale the source locale 824 */ 825 public void synchronizeLocaleIndependentValues(CmsObject cms, Collection<String> skipPaths, Locale sourceLocale) { 826 827 if (getContentDefinition().getContentHandler().hasSynchronizedElements() && (getLocales().size() > 1)) { 828 for (Map.Entry<String, SynchronizationMode> syncEntry : getContentDefinition().getContentHandler().getSynchronizations( 829 true).asMap().entrySet()) { 830 String elementPath = syncEntry.getKey(); 831 SynchronizationMode syncMode = syncEntry.getValue(); 832 if (syncMode == SynchronizationMode.none) { 833 continue; 834 } 835 synchronizeElement(cms, elementPath, skipPaths, sourceLocale, syncMode); 836 } 837 } 838 } 839 840 /** 841 * @see org.opencms.xml.I_CmsXmlDocument#validate(org.opencms.file.CmsObject) 842 */ 843 public CmsXmlContentErrorHandler validate(CmsObject cms) { 844 845 // iterate through all initialized value nodes in this XML content 846 CmsXmlContentValidationVisitor visitor = new CmsXmlContentValidationVisitor(cms); 847 visitAllValuesWith(visitor); 848 849 return visitor.getErrorHandler(); 850 } 851 852 /** 853 * Visits all values of this XML content with the given value visitor.<p> 854 * 855 * Please note that the order in which the values are visited may NOT be the 856 * order they appear in the XML document. It is ensured that the parent 857 * of a nested value is visited before the element it contains.<p> 858 * 859 * @param visitor the value visitor implementation to visit the values with 860 */ 861 public void visitAllValuesWith(I_CmsXmlContentValueVisitor visitor) { 862 863 List<String> bookmarks = new ArrayList<String>(getBookmarks()); 864 Collections.sort(bookmarks); 865 866 for (int i = 0; i < bookmarks.size(); i++) { 867 868 String key = bookmarks.get(i); 869 I_CmsXmlContentValue value = getBookmark(key); 870 visitor.visit(value); 871 } 872 } 873 874 /** 875 * Creates a new bookmark for the given element.<p> 876 * 877 * @param element the element to create the bookmark for 878 * @param locale the locale 879 * @param parent the parent node of the element 880 * @param parentPath the parent's path 881 * @param parentDef the parent's content definition 882 */ 883 protected void addBookmarkForElement( 884 Element element, 885 Locale locale, 886 Element parent, 887 String parentPath, 888 CmsXmlContentDefinition parentDef) { 889 890 int elemIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(parent)); 891 String elemPath = CmsXmlUtils.concatXpath( 892 parentPath, 893 CmsXmlUtils.createXpathElement(element.getName(), elemIndex)); 894 I_CmsXmlSchemaType elemSchemaType = parentDef.getSchemaType(element.getName()); 895 I_CmsXmlContentValue elemValue = elemSchemaType.createValue(this, element, locale); 896 addBookmark(elemPath, locale, true, elemValue); 897 } 898 899 /** 900 * Adds a bookmark for the given value.<p> 901 * 902 * @param value the value to bookmark 903 * @param path the lookup path to use for the bookmark 904 * @param locale the locale to use for the bookmark 905 * @param enabled if true, the value is enabled, if false it is disabled 906 */ 907 protected void addBookmarkForValue(I_CmsXmlContentValue value, String path, Locale locale, boolean enabled) { 908 909 addBookmark(path, locale, enabled, value); 910 } 911 912 /** 913 * Adds a new XML schema type with the default value to the given parent node.<p> 914 * 915 * @param cms the cms context 916 * @param parent the XML parent element to add the new value to 917 * @param type the type of the value to add 918 * @param locale the locale to add the new value for 919 * @param insertIndex the index in the XML document where to add the XML node 920 * 921 * @return the created XML content value 922 */ 923 protected I_CmsXmlContentValue addValue( 924 CmsObject cms, 925 Element parent, 926 I_CmsXmlSchemaType type, 927 Locale locale, 928 int insertIndex) { 929 930 // first generate the XML element for the new value 931 Element element = type.generateXml(cms, this, parent, locale); 932 // detach the XML element from the appended position in order to insert it at the required position 933 element.detach(); 934 // add the XML element at the required position in the parent XML node 935 CmsXmlGenericWrapper.content(parent).add(insertIndex, element); 936 // create the type and return it 937 I_CmsXmlContentValue value = type.createValue(this, element, locale); 938 // generate the default value again - required for nested mappings because only now the full path is available 939 String defaultValue = m_contentDefinition.getContentHandler().getDefault(cms, value, locale); 940 if (defaultValue != null) { 941 // only if there is a default value available use it to overwrite the initial default 942 value.setStringValue(cms, defaultValue); 943 } 944 // finally return the value 945 return value; 946 } 947 948 /** 949 * @see org.opencms.xml.A_CmsXmlDocument#getBookmark(java.lang.String) 950 */ 951 @Override 952 protected I_CmsXmlContentValue getBookmark(String bookmark) { 953 954 // allows package classes to directly access the bookmark information of the XML content 955 return super.getBookmark(bookmark); 956 } 957 958 /** 959 * @see org.opencms.xml.A_CmsXmlDocument#getBookmarks() 960 */ 961 @Override 962 protected Set<String> getBookmarks() { 963 964 // allows package classes to directly access the bookmark information of the XML content 965 return super.getBookmarks(); 966 } 967 968 /** 969 * Returns the content definition object for this xml content object.<p> 970 * 971 * @param resolver the XML entity resolver to use, required for VFS access 972 * 973 * @return the content definition object for this xml content object 974 * 975 * @throws CmsRuntimeException if the schema location attribute (<code>systemId</code>)cannot be found, 976 * parsing of the schema fails, an underlying IOException occurs or unmarshalling fails 977 * 978 */ 979 protected CmsXmlContentDefinition getContentDefinition(EntityResolver resolver) throws CmsRuntimeException { 980 981 String schemaLocation = m_document.getRootElement().attributeValue( 982 I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION); 983 // Note regarding exception handling: 984 // Since this object already is a valid XML content object, 985 // it must have a valid schema, otherwise it would not exist. 986 // Therefore the exceptions should never be really thrown. 987 if (schemaLocation == null) { 988 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_SCHEMA_0)); 989 } 990 991 try { 992 return CmsXmlContentDefinition.unmarshal(schemaLocation, resolver); 993 } catch (SAXException e) { 994 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XML_SCHEMA_PARSE_1, schemaLocation), e); 995 } catch (IOException e) { 996 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XML_SCHEMA_IO_1, schemaLocation), e); 997 } catch (CmsXmlException e) { 998 throw new CmsRuntimeException( 999 Messages.get().container(Messages.ERR_XMLCONTENT_UNMARSHAL_1, schemaLocation), 1000 e); 1001 } 1002 } 1003 1004 /** 1005 * Initializes an XML document based on the provided document, encoding and content definition.<p> 1006 * 1007 * Checks the links and removes invalid ones in the initialized document.<p> 1008 * 1009 * @param cms the current users OpenCms content 1010 * @param document the base XML document to use for initializing 1011 * @param encoding the encoding to use when marshalling the document later 1012 * @param definition the content definition to use 1013 */ 1014 protected void initDocument(CmsObject cms, Document document, String encoding, CmsXmlContentDefinition definition) { 1015 1016 initDocument(document, encoding, definition); 1017 // check invalid links 1018 if (cms != null) { 1019 // this will remove all invalid links 1020 getHandler().invalidateBrokenLinks(cms, this); 1021 } 1022 } 1023 1024 /** 1025 * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition) 1026 */ 1027 @Override 1028 protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) { 1029 1030 m_document = document; 1031 m_contentDefinition = definition; 1032 m_encoding = CmsEncoder.lookupEncoding(encoding, encoding); 1033 m_elementLocales = new HashMap<String, Set<Locale>>(); 1034 m_elementNames = new HashMap<Locale, Set<String>>(); 1035 m_locales = new HashSet<Locale>(); 1036 clearBookmarks(); 1037 1038 // initialize the bookmarks 1039 for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(m_document.getRootElement()); i.hasNext();) { 1040 Element node = i.next(); 1041 try { 1042 Locale locale = CmsLocaleManager.getLocale( 1043 node.attribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE).getValue()); 1044 1045 addLocale(locale); 1046 processSchemaNode(node, null, locale, definition); 1047 } catch (NullPointerException e) { 1048 LOG.error(Messages.get().getBundle().key(Messages.LOG_XMLCONTENT_INIT_BOOKMARKS_0), e); 1049 } 1050 } 1051 1052 } 1053 1054 /** 1055 * Processes a document node and extracts the values of the node according to the provided XML 1056 * content definition.<p> 1057 * 1058 * @param root the root node element to process 1059 * @param rootPath the Xpath of the root node in the document 1060 * @param locale the locale 1061 * @param definition the XML content definition to use for processing the values 1062 */ 1063 protected void processSchemaNode(Element root, String rootPath, Locale locale, CmsXmlContentDefinition definition) { 1064 1065 // iterate all XML nodes 1066 List<Node> content = CmsXmlGenericWrapper.content(root); 1067 for (int i = content.size() - 1; i >= 0; i--) { 1068 Node node = content.get(i); 1069 if (!(node instanceof Element)) { 1070 // this node is not an element, so it must be a white space text node, remove it 1071 node.detach(); 1072 } else { 1073 // node must be an element 1074 Element element = (Element)node; 1075 String name = element.getName(); 1076 int xpathIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(root)); 1077 1078 // build the Xpath expression for the current node 1079 String path; 1080 if (rootPath != null) { 1081 StringBuffer b = new StringBuffer(rootPath.length() + name.length() + 6); 1082 b.append(rootPath); 1083 b.append('/'); 1084 b.append(CmsXmlUtils.createXpathElement(name, xpathIndex)); 1085 path = b.toString(); 1086 } else { 1087 path = CmsXmlUtils.createXpathElement(name, xpathIndex); 1088 } 1089 1090 // create a XML content value element 1091 I_CmsXmlSchemaType schemaType = definition.getSchemaType(name); 1092 1093 if (schemaType != null) { 1094 // directly add simple type to schema 1095 I_CmsXmlContentValue value = schemaType.createValue(this, element, locale); 1096 addBookmark(path, locale, true, value); 1097 1098 if (!schemaType.isSimpleType()) { 1099 // recurse for nested schema 1100 CmsXmlNestedContentDefinition nestedSchema = (CmsXmlNestedContentDefinition)schemaType; 1101 processSchemaNode(element, path, locale, nestedSchema.getNestedContentDefinition()); 1102 } 1103 } else { 1104 // unknown XML node name according to schema 1105 if (LOG.isWarnEnabled()) { 1106 LOG.warn( 1107 Messages.get().getBundle().key( 1108 Messages.LOG_XMLCONTENT_INVALID_ELEM_2, 1109 name, 1110 definition.getSchemaLocation())); 1111 } 1112 } 1113 } 1114 } 1115 } 1116 1117 /** 1118 * Sets the file this XML content is written to.<p> 1119 * 1120 * @param file the file this XML content content is written to 1121 */ 1122 protected void setFile(CmsFile file) { 1123 1124 m_file = file; 1125 } 1126 1127 /** 1128 * Ensures the parent values to the given path are created.<p> 1129 * 1130 * @param cms the cms context 1131 * @param valuePath the value path 1132 * @param locale the content locale 1133 */ 1134 private void ensureParentValues(CmsObject cms, String valuePath, Locale locale) { 1135 1136 if (valuePath.contains("/")) { 1137 String parentPath = valuePath.substring(0, valuePath.lastIndexOf("/")); 1138 if (!hasValue(parentPath, locale)) { 1139 ensureParentValues(cms, parentPath, locale); 1140 int index = CmsXmlUtils.getXpathIndexInt(parentPath) - 1; 1141 addValue(cms, parentPath, locale, index); 1142 } 1143 } 1144 } 1145 1146 /** 1147 * Removes all surplus values of locale independent fields in the other locales.<p> 1148 * 1149 * @param elementPath the element path 1150 * @param valueCount the value count 1151 * @param sourceLocale the source locale 1152 */ 1153 private void removeSurplusValuesInOtherLocales(String elementPath, int valueCount, Locale sourceLocale) { 1154 1155 for (Locale locale : getLocales()) { 1156 if (locale.equals(sourceLocale)) { 1157 continue; 1158 } 1159 List<I_CmsXmlContentValue> localeValues = getValues(elementPath, locale); 1160 for (int i = valueCount; i < localeValues.size(); i++) { 1161 removeValue(elementPath, locale, 0); 1162 } 1163 } 1164 } 1165 1166 /** 1167 * Removes all values of the given path in the other locales.<p> 1168 * 1169 * @param elementPath the element path 1170 * @param sourceLocale the source locale 1171 */ 1172 private void removeValuesInOtherLocales(String elementPath, Locale sourceLocale) { 1173 1174 for (Locale locale : getLocales()) { 1175 if (locale.equals(sourceLocale)) { 1176 continue; 1177 } 1178 while (hasValue(elementPath, locale)) { 1179 removeValue(elementPath, locale, 0); 1180 } 1181 } 1182 } 1183 1184 /** 1185 * Sets the value in all other locales.<p> 1186 * 1187 * @param cms the cms context 1188 * @param value the value 1189 * @param requiredParent the path to the required parent value 1190 */ 1191 private void setValueForOtherLocales(CmsObject cms, I_CmsXmlContentValue value, String requiredParent) { 1192 1193 if (!value.isSimpleType()) { 1194 throw new IllegalArgumentException(); 1195 } 1196 for (Locale locale : getLocales()) { 1197 if (locale.equals(value.getLocale())) { 1198 continue; 1199 } 1200 String valuePath = value.getPath(); 1201 if (CmsStringUtil.isEmptyOrWhitespaceOnly(requiredParent) || hasValue(requiredParent, locale)) { 1202 ensureParentValues(cms, valuePath, locale); 1203 if (hasValue(valuePath, locale)) { 1204 I_CmsXmlContentValue localeValue = getValue(valuePath, locale); 1205 localeValue.setStringValue(cms, value.getStringValue(cms)); 1206 } else { 1207 int index = CmsXmlUtils.getXpathIndexInt(valuePath) - 1; 1208 I_CmsXmlContentValue localeValue = addValue(cms, valuePath, locale, index); 1209 localeValue.setStringValue(cms, value.getStringValue(cms)); 1210 } 1211 } 1212 } 1213 } 1214 1215 /** 1216 * Synchronizes the values for the given element path.<p> 1217 * 1218 * @param cms the cms context 1219 * @param elementPath the element path 1220 * @param skipPaths the paths to skip 1221 * @param sourceLocale the source locale 1222 * @param syncMode the synchronization mode 1223 */ 1224 private void synchronizeElement( 1225 CmsObject cms, 1226 String elementPath, 1227 Collection<String> skipPaths, 1228 Locale sourceLocale, 1229 SynchronizationMode syncMode) { 1230 1231 if (syncMode == SynchronizationMode.none) { 1232 return; 1233 } 1234 1235 if (elementPath.contains("/")) { 1236 String parentPath = CmsXmlUtils.removeLastXpathElement(elementPath); 1237 List<I_CmsXmlContentValue> parentValues = getValuesByPath(parentPath, sourceLocale); 1238 String elementName = CmsXmlUtils.getLastXpathElement(elementPath); 1239 for (I_CmsXmlContentValue parentValue : parentValues) { 1240 String valuePath = CmsXmlUtils.concatXpath(parentValue.getPath(), elementName); 1241 boolean skip = false; 1242 for (String skipPath : skipPaths) { 1243 if (valuePath.startsWith(skipPath)) { 1244 skip = true; 1245 break; 1246 } 1247 } 1248 if (!skip) { 1249 if (hasValue(valuePath, sourceLocale)) { 1250 List<I_CmsXmlContentValue> subValues = getValues(valuePath, sourceLocale); 1251 removeSurplusValuesInOtherLocales(elementPath, subValues.size(), sourceLocale); 1252 for (I_CmsXmlContentValue value : subValues) { 1253 if (value.isSimpleType()) { 1254 setValueForOtherLocales( 1255 cms, 1256 value, 1257 syncMode == SynchronizationMode.strong 1258 ? null // strong -> auto-create parent values 1259 : CmsXmlUtils.removeLastXpathElement(valuePath)); 1260 } else { 1261 List<I_CmsXmlContentValue> simpleValues = getAllSimpleSubValues(value); 1262 for (I_CmsXmlContentValue simpleValue : simpleValues) { 1263 setValueForOtherLocales(cms, simpleValue, parentValue.getPath()); 1264 } 1265 } 1266 } 1267 } else { 1268 removeValuesInOtherLocales(valuePath, sourceLocale); 1269 } 1270 } 1271 } 1272 } else { 1273 if (hasValue(elementPath, sourceLocale)) { 1274 List<I_CmsXmlContentValue> subValues = getValues(elementPath, sourceLocale); 1275 removeSurplusValuesInOtherLocales(elementPath, subValues.size(), sourceLocale); 1276 for (I_CmsXmlContentValue value : subValues) { 1277 if (value.isSimpleType()) { 1278 setValueForOtherLocales(cms, value, null); 1279 } else { 1280 List<I_CmsXmlContentValue> simpleValues = getAllSimpleSubValues(value); 1281 for (I_CmsXmlContentValue simpleValue : simpleValues) { 1282 setValueForOtherLocales(cms, simpleValue, null); 1283 } 1284 } 1285 } 1286 } else { 1287 removeValuesInOtherLocales(elementPath, sourceLocale); 1288 } 1289 } 1290 1291 // this handles the case where a elementPath is missing in the source locale because its parent value is missing 1292 if (syncMode == SynchronizationMode.strong) { 1293 if (getValuesByPath(elementPath, sourceLocale).size() == 0) { 1294 boolean minOccursWarning = false; 1295 boolean changed = false; 1296 for (Locale locale : getLocales()) { 1297 if (!locale.equals(sourceLocale)) { 1298 List<I_CmsXmlContentValue> candidatesForRemoval = getValuesByPath(elementPath, locale); 1299 for (I_CmsXmlContentValue candidate : candidatesForRemoval) { 1300 if (candidate.getMinOccurs() > 0) { 1301 // it makes no sense to remove only part of the values 1302 minOccursWarning = true; 1303 break; 1304 } else { 1305 candidate.getElement().detach(); 1306 changed = true; 1307 } 1308 } 1309 } 1310 } 1311 if (changed) { 1312 initDocument(m_document, m_encoding, m_contentDefinition); 1313 } 1314 if (minOccursWarning) { 1315 String schema = getContentDefinition().getSchemaLocation(); 1316 LOG.warn( 1317 " synchronization setting 'strong' for '" 1318 + elementPath 1319 + "' in '" 1320 + schema 1321 + "' is incorrect because it is a required value in an optional nested content."); 1322 } 1323 } 1324 } 1325 } 1326 1327}