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