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