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