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