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        // clear formerly mapped values
804        try {
805            getHandler().clearMappings(cms, this);
806        } catch (CmsException e) {
807            String message = "Failed to clean mappings for content "
808                + (this.getFile() == null ? "<unknown>" : this.getFile().getRootPath());
809            if (LOG.isDebugEnabled()) {
810                LOG.debug(message, e);
811            } else {
812                LOG.error(message);
813            }
814        }
815        // iterate through all initialized value nodes in this XML content
816        CmsXmlContentMappingVisitor visitor = new CmsXmlContentMappingVisitor(cms, this);
817        visitAllValuesWith(visitor);
818    }
819
820    /**
821     * Sets the flag to control if auto correction is enabled when saving this XML content.<p>
822     *
823     * @param value the flag to control if auto correction is enabled when saving this XML content
824     */
825    public void setAutoCorrectionEnabled(boolean value) {
826
827        m_autoCorrectionEnabled = value;
828    }
829
830    /**
831     * Synchronizes the locale independent fields for the given locale.<p>
832     *
833     * @param cms the cms context
834     * @param skipPaths the paths to skip
835     * @param sourceLocale the source locale
836     */
837    public void synchronizeLocaleIndependentValues(CmsObject cms, Collection<String> skipPaths, Locale sourceLocale) {
838
839        if (getContentDefinition().getContentHandler().hasSynchronizedElements() && (getLocales().size() > 1)) {
840            for (Map.Entry<String, SynchronizationMode> syncEntry : getContentDefinition().getContentHandler().getSynchronizations(
841                true).asMap().entrySet()) {
842                String elementPath = syncEntry.getKey();
843                SynchronizationMode syncMode = syncEntry.getValue();
844                if (syncMode == SynchronizationMode.none) {
845                    continue;
846                }
847                synchronizeElement(cms, elementPath, skipPaths, sourceLocale, syncMode);
848            }
849        }
850    }
851
852    /**
853     * @see org.opencms.xml.I_CmsXmlDocument#validate(org.opencms.file.CmsObject)
854     */
855    public CmsXmlContentErrorHandler validate(CmsObject cms) {
856
857        // iterate through all initialized value nodes in this XML content
858        CmsXmlContentValidationVisitor visitor = new CmsXmlContentValidationVisitor(cms);
859        visitAllValuesWith(visitor);
860
861        return visitor.getErrorHandler();
862    }
863
864    /**
865     * Visits all values of this XML content with the given value visitor.<p>
866     *
867     * Please note that the order in which the values are visited may NOT be the
868     * order they appear in the XML document. It is ensured that the parent
869     * of a nested value is visited before the element it contains.<p>
870     *
871     * @param visitor the value visitor implementation to visit the values with
872     */
873    public void visitAllValuesWith(I_CmsXmlContentValueVisitor visitor) {
874
875        List<String> bookmarks = new ArrayList<String>(getBookmarks());
876        Collections.sort(bookmarks);
877
878        for (int i = 0; i < bookmarks.size(); i++) {
879
880            String key = bookmarks.get(i);
881            I_CmsXmlContentValue value = getBookmark(key);
882            visitor.visit(value);
883        }
884    }
885
886    /**
887     * Creates a new bookmark for the given element.<p>
888     *
889     * @param element the element to create the bookmark for
890     * @param locale the locale
891     * @param parent the parent node of the element
892     * @param parentPath the parent's path
893     * @param parentDef the parent's content definition
894     */
895    protected void addBookmarkForElement(
896        Element element,
897        Locale locale,
898        Element parent,
899        String parentPath,
900        CmsXmlContentDefinition parentDef) {
901
902        int elemIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(parent));
903        String elemPath = CmsXmlUtils.concatXpath(
904            parentPath,
905            CmsXmlUtils.createXpathElement(element.getName(), elemIndex));
906        I_CmsXmlSchemaType elemSchemaType = parentDef.getSchemaType(element.getName());
907        I_CmsXmlContentValue elemValue = elemSchemaType.createValue(this, element, locale);
908        addBookmark(elemPath, locale, true, elemValue);
909    }
910
911    /**
912     * Adds a bookmark for the given value.<p>
913     *
914     * @param value the value to bookmark
915     * @param path the lookup path to use for the bookmark
916     * @param locale the locale to use for the bookmark
917     * @param enabled if true, the value is enabled, if false it is disabled
918     */
919    protected void addBookmarkForValue(I_CmsXmlContentValue value, String path, Locale locale, boolean enabled) {
920
921        addBookmark(path, locale, enabled, value);
922    }
923
924    /**
925     * Adds a new XML schema type with the default value to the given parent node.<p>
926     *
927     * @param cms the cms context
928     * @param parent the XML parent element to add the new value to
929     * @param type the type of the value to add
930     * @param locale the locale to add the new value for
931     * @param insertIndex the index in the XML document where to add the XML node
932     *
933     * @return the created XML content value
934     */
935    protected I_CmsXmlContentValue addValue(
936        CmsObject cms,
937        Element parent,
938        I_CmsXmlSchemaType type,
939        Locale locale,
940        int insertIndex) {
941
942        // first generate the XML element for the new value
943        Element element = type.generateXml(cms, this, parent, locale);
944        // detach the XML element from the appended position in order to insert it at the required position
945        element.detach();
946        // add the XML element at the required position in the parent XML node
947        CmsXmlGenericWrapper.content(parent).add(insertIndex, element);
948        // create the type and return it
949        I_CmsXmlContentValue value = type.createValue(this, element, locale);
950        // generate the default value again - required for nested mappings because only now the full path is available
951        String defaultValue = m_contentDefinition.getContentHandler().getDefault(cms, value, locale);
952        if (defaultValue != null) {
953            // only if there is a default value available use it to overwrite the initial default
954            value.setStringValue(cms, defaultValue);
955        }
956        // finally return the value
957        return value;
958    }
959
960    /**
961     * @see org.opencms.xml.A_CmsXmlDocument#getBookmark(java.lang.String)
962     */
963    @Override
964    protected I_CmsXmlContentValue getBookmark(String bookmark) {
965
966        // allows package classes to directly access the bookmark information of the XML content
967        return super.getBookmark(bookmark);
968    }
969
970    /**
971     * @see org.opencms.xml.A_CmsXmlDocument#getBookmarks()
972     */
973    @Override
974    protected Set<String> getBookmarks() {
975
976        // allows package classes to directly access the bookmark information of the XML content
977        return super.getBookmarks();
978    }
979
980    /**
981     * Returns the content definition object for this xml content object.<p>
982     *
983     * @param resolver the XML entity resolver to use, required for VFS access
984     *
985     * @return the content definition object for this xml content object
986     *
987     * @throws CmsRuntimeException if the schema location attribute (<code>systemId</code>)cannot be found,
988     *         parsing of the schema fails, an underlying IOException occurs or unmarshalling fails
989     *
990     */
991    protected CmsXmlContentDefinition getContentDefinition(EntityResolver resolver) throws CmsRuntimeException {
992
993        String schemaLocation = m_document.getRootElement().attributeValue(
994            I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION);
995        // Note regarding exception handling:
996        // Since this object already is a valid XML content object,
997        // it must have a valid schema, otherwise it would not exist.
998        // Therefore the exceptions should never be really thrown.
999        if (schemaLocation == null) {
1000            throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XMLCONTENT_MISSING_SCHEMA_0));
1001        }
1002
1003        try {
1004            return CmsXmlContentDefinition.unmarshal(schemaLocation, resolver);
1005        } catch (SAXException e) {
1006            throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XML_SCHEMA_PARSE_1, schemaLocation), e);
1007        } catch (IOException e) {
1008            throw new CmsRuntimeException(Messages.get().container(Messages.ERR_XML_SCHEMA_IO_1, schemaLocation), e);
1009        } catch (CmsXmlException e) {
1010            throw new CmsRuntimeException(
1011                Messages.get().container(Messages.ERR_XMLCONTENT_UNMARSHAL_1, schemaLocation),
1012                e);
1013        }
1014    }
1015
1016    /**
1017     * Initializes an XML document based on the provided document, encoding and content definition.<p>
1018     *
1019     * Checks the links and removes invalid ones in the initialized document.<p>
1020     *
1021     * @param cms the current users OpenCms content
1022     * @param document the base XML document to use for initializing
1023     * @param encoding the encoding to use when marshalling the document later
1024     * @param definition the content definition to use
1025     */
1026    protected void initDocument(CmsObject cms, Document document, String encoding, CmsXmlContentDefinition definition) {
1027
1028        initDocument(document, encoding, definition);
1029        // check invalid links
1030        if (cms != null) {
1031            // this will remove all invalid links
1032            getHandler().invalidateBrokenLinks(cms, this);
1033        }
1034    }
1035
1036    /**
1037     * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition)
1038     */
1039    @Override
1040    protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) {
1041
1042        m_document = document;
1043        m_contentDefinition = definition;
1044        m_encoding = CmsEncoder.lookupEncoding(encoding, encoding);
1045        m_elementLocales = new HashMap<String, Set<Locale>>();
1046        m_elementNames = new HashMap<Locale, Set<String>>();
1047        m_locales = new HashSet<Locale>();
1048        clearBookmarks();
1049
1050        // initialize the bookmarks
1051        for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(m_document.getRootElement()); i.hasNext();) {
1052            Element node = i.next();
1053            try {
1054                Locale locale = CmsLocaleManager.getLocale(
1055                    node.attribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE).getValue());
1056
1057                addLocale(locale);
1058                processSchemaNode(node, null, locale, definition);
1059            } catch (NullPointerException e) {
1060                LOG.error(Messages.get().getBundle().key(Messages.LOG_XMLCONTENT_INIT_BOOKMARKS_0), e);
1061            }
1062        }
1063
1064    }
1065
1066    /**
1067     * Processes a document node and extracts the values of the node according to the provided XML
1068     * content definition.<p>
1069     *
1070     * @param root the root node element to process
1071     * @param rootPath the Xpath of the root node in the document
1072     * @param locale the locale
1073     * @param definition the XML content definition to use for processing the values
1074     */
1075    protected void processSchemaNode(Element root, String rootPath, Locale locale, CmsXmlContentDefinition definition) {
1076
1077        // iterate all XML nodes
1078        List<Node> content = CmsXmlGenericWrapper.content(root);
1079        for (int i = content.size() - 1; i >= 0; i--) {
1080            Node node = content.get(i);
1081            if (!(node instanceof Element)) {
1082                // this node is not an element, so it must be a white space text node, remove it
1083                node.detach();
1084            } else {
1085                // node must be an element
1086                Element element = (Element)node;
1087                String name = element.getName();
1088                int xpathIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(root));
1089
1090                // build the Xpath expression for the current node
1091                String path;
1092                if (rootPath != null) {
1093                    StringBuffer b = new StringBuffer(rootPath.length() + name.length() + 6);
1094                    b.append(rootPath);
1095                    b.append('/');
1096                    b.append(CmsXmlUtils.createXpathElement(name, xpathIndex));
1097                    path = b.toString();
1098                } else {
1099                    path = CmsXmlUtils.createXpathElement(name, xpathIndex);
1100                }
1101
1102                // create a XML content value element
1103                I_CmsXmlSchemaType schemaType = definition.getSchemaType(name);
1104
1105                if (schemaType != null) {
1106                    // directly add simple type to schema
1107                    I_CmsXmlContentValue value = schemaType.createValue(this, element, locale);
1108                    addBookmark(path, locale, true, value);
1109
1110                    if (!schemaType.isSimpleType()) {
1111                        // recurse for nested schema
1112                        CmsXmlNestedContentDefinition nestedSchema = (CmsXmlNestedContentDefinition)schemaType;
1113                        processSchemaNode(element, path, locale, nestedSchema.getNestedContentDefinition());
1114                    }
1115                } else {
1116                    // unknown XML node name according to schema
1117                    if (LOG.isWarnEnabled()) {
1118                        LOG.warn(
1119                            Messages.get().getBundle().key(
1120                                Messages.LOG_XMLCONTENT_INVALID_ELEM_2,
1121                                name,
1122                                definition.getSchemaLocation()));
1123                    }
1124                }
1125            }
1126        }
1127    }
1128
1129    /**
1130     * Sets the file this XML content is written to.<p>
1131     *
1132     * @param file the file this XML content content is written to
1133     */
1134    protected void setFile(CmsFile file) {
1135
1136        m_file = file;
1137    }
1138
1139    /**
1140     * Ensures the parent values to the given path are created.<p>
1141     *
1142     * @param cms the cms context
1143     * @param valuePath the value path
1144     * @param locale the content locale
1145     */
1146    private void ensureParentValues(CmsObject cms, String valuePath, Locale locale) {
1147
1148        if (valuePath.contains("/")) {
1149            String parentPath = valuePath.substring(0, valuePath.lastIndexOf("/"));
1150            if (!hasValue(parentPath, locale)) {
1151                ensureParentValues(cms, parentPath, locale);
1152                int index = CmsXmlUtils.getXpathIndexInt(parentPath) - 1;
1153                addValue(cms, parentPath, locale, index);
1154            }
1155        }
1156    }
1157
1158    private void removePreviousPropertyMappings(CmsObject cms) {
1159
1160        Set<String> mappings = new HashSet<>();
1161        getHandler().getMappings().values().stream().forEach(mps -> mappings.addAll(mps));
1162        Set<String> properties = new HashSet<>();
1163        for (String mapping : mappings) {
1164
1165        }
1166    }
1167
1168    /**
1169     * Removes all surplus values of locale independent fields in the other locales.<p>
1170     *
1171     * @param elementPath the element path
1172     * @param valueCount the value count
1173     * @param sourceLocale the source locale
1174     */
1175    private void removeSurplusValuesInOtherLocales(String elementPath, int valueCount, Locale sourceLocale) {
1176
1177        for (Locale locale : getLocales()) {
1178            if (locale.equals(sourceLocale)) {
1179                continue;
1180            }
1181            List<I_CmsXmlContentValue> localeValues = getValues(elementPath, locale);
1182            for (int i = valueCount; i < localeValues.size(); i++) {
1183                removeValue(elementPath, locale, 0);
1184            }
1185        }
1186    }
1187
1188    /**
1189     * Removes all values of the given path in the other locales.<p>
1190     *
1191     * @param elementPath the element path
1192     * @param sourceLocale the source locale
1193     */
1194    private void removeValuesInOtherLocales(String elementPath, Locale sourceLocale) {
1195
1196        for (Locale locale : getLocales()) {
1197            if (locale.equals(sourceLocale)) {
1198                continue;
1199            }
1200            while (hasValue(elementPath, locale)) {
1201                removeValue(elementPath, locale, 0);
1202            }
1203        }
1204    }
1205
1206    /**
1207     * Sets the value in all other locales.<p>
1208     *
1209     * @param cms the cms context
1210     * @param value the value
1211     * @param requiredParent the path to the required parent value
1212     */
1213    private void setValueForOtherLocales(CmsObject cms, I_CmsXmlContentValue value, String requiredParent) {
1214
1215        if (!value.isSimpleType()) {
1216            throw new IllegalArgumentException();
1217        }
1218        for (Locale locale : getLocales()) {
1219            if (locale.equals(value.getLocale())) {
1220                continue;
1221            }
1222            String valuePath = value.getPath();
1223            if (CmsStringUtil.isEmptyOrWhitespaceOnly(requiredParent) || hasValue(requiredParent, locale)) {
1224                ensureParentValues(cms, valuePath, locale);
1225                if (hasValue(valuePath, locale)) {
1226                    I_CmsXmlContentValue localeValue = getValue(valuePath, locale);
1227                    localeValue.setStringValue(cms, value.getStringValue(cms));
1228                } else {
1229                    int index = CmsXmlUtils.getXpathIndexInt(valuePath) - 1;
1230                    I_CmsXmlContentValue localeValue = addValue(cms, valuePath, locale, index);
1231                    localeValue.setStringValue(cms, value.getStringValue(cms));
1232                }
1233            }
1234        }
1235    }
1236
1237    /**
1238     * Synchronizes the values for the given element path.<p>
1239     *
1240     * @param cms the cms context
1241     * @param elementPath the element path
1242     * @param skipPaths the paths to skip
1243     * @param sourceLocale the source locale
1244     * @param syncMode the synchronization mode
1245     */
1246    private void synchronizeElement(
1247        CmsObject cms,
1248        String elementPath,
1249        Collection<String> skipPaths,
1250        Locale sourceLocale,
1251        SynchronizationMode syncMode) {
1252
1253        if (syncMode == SynchronizationMode.none) {
1254            return;
1255        }
1256
1257        if (elementPath.contains("/")) {
1258            String parentPath = CmsXmlUtils.removeLastXpathElement(elementPath);
1259            List<I_CmsXmlContentValue> parentValues = getValuesByPath(parentPath, sourceLocale);
1260            String elementName = CmsXmlUtils.getLastXpathElement(elementPath);
1261            for (I_CmsXmlContentValue parentValue : parentValues) {
1262                String valuePath = CmsXmlUtils.concatXpath(parentValue.getPath(), elementName);
1263                boolean skip = false;
1264                for (String skipPath : skipPaths) {
1265                    if (valuePath.startsWith(skipPath)) {
1266                        skip = true;
1267                        break;
1268                    }
1269                }
1270                if (!skip) {
1271                    if (hasValue(valuePath, sourceLocale)) {
1272                        List<I_CmsXmlContentValue> subValues = getValues(valuePath, sourceLocale);
1273                        removeSurplusValuesInOtherLocales(elementPath, subValues.size(), sourceLocale);
1274                        for (I_CmsXmlContentValue value : subValues) {
1275                            if (value.isSimpleType()) {
1276                                setValueForOtherLocales(
1277                                    cms,
1278                                    value,
1279                                    syncMode == SynchronizationMode.strong
1280                                    ? null // strong -> auto-create parent values
1281                                    : CmsXmlUtils.removeLastXpathElement(valuePath));
1282                            } else {
1283                                List<I_CmsXmlContentValue> simpleValues = getAllSimpleSubValues(value);
1284                                for (I_CmsXmlContentValue simpleValue : simpleValues) {
1285                                    setValueForOtherLocales(cms, simpleValue, parentValue.getPath());
1286                                }
1287                            }
1288                        }
1289                    } else {
1290                        removeValuesInOtherLocales(valuePath, sourceLocale);
1291                    }
1292                }
1293            }
1294        } else {
1295            if (hasValue(elementPath, sourceLocale)) {
1296                List<I_CmsXmlContentValue> subValues = getValues(elementPath, sourceLocale);
1297                removeSurplusValuesInOtherLocales(elementPath, subValues.size(), sourceLocale);
1298                for (I_CmsXmlContentValue value : subValues) {
1299                    if (value.isSimpleType()) {
1300                        setValueForOtherLocales(cms, value, null);
1301                    } else {
1302                        List<I_CmsXmlContentValue> simpleValues = getAllSimpleSubValues(value);
1303                        for (I_CmsXmlContentValue simpleValue : simpleValues) {
1304                            setValueForOtherLocales(cms, simpleValue, null);
1305                        }
1306                    }
1307                }
1308            } else {
1309                removeValuesInOtherLocales(elementPath, sourceLocale);
1310            }
1311        }
1312
1313        // this handles the case where a elementPath is missing in the source locale because its parent value is missing
1314        if (syncMode == SynchronizationMode.strong) {
1315            if (getValuesByPath(elementPath, sourceLocale).size() == 0) {
1316                boolean minOccursWarning = false;
1317                boolean changed = false;
1318                for (Locale locale : getLocales()) {
1319                    if (!locale.equals(sourceLocale)) {
1320                        List<I_CmsXmlContentValue> candidatesForRemoval = getValuesByPath(elementPath, locale);
1321                        for (I_CmsXmlContentValue candidate : candidatesForRemoval) {
1322                            if (candidate.getMinOccurs() > 0) {
1323                                // it makes no sense to remove only part of the values
1324                                minOccursWarning = true;
1325                                break;
1326                            } else {
1327                                candidate.getElement().detach();
1328                                changed = true;
1329                            }
1330                        }
1331                    }
1332                }
1333                if (changed) {
1334                    initDocument(m_document, m_encoding, m_contentDefinition);
1335                }
1336                if (minOccursWarning) {
1337                    String schema = getContentDefinition().getSchemaLocation();
1338                    LOG.warn(
1339                        " synchronization setting 'strong' for '"
1340                            + elementPath
1341                            + "' in '"
1342                            + schema
1343                            + "' is incorrect because it is a required value in an optional nested content.");
1344                }
1345            }
1346        }
1347    }
1348
1349}