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;
029
030import org.opencms.file.CmsFile;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsResource;
033import org.opencms.i18n.CmsLocaleManager;
034import org.opencms.main.CmsIllegalArgumentException;
035import org.opencms.main.CmsRuntimeException;
036import org.opencms.main.OpenCms;
037import org.opencms.xml.types.CmsXmlAccessRestrictionValue;
038import org.opencms.xml.types.CmsXmlCategoryValue;
039import org.opencms.xml.types.CmsXmlDynamicCategoryValue;
040import org.opencms.xml.types.CmsXmlNestedContentDefinition;
041import org.opencms.xml.types.I_CmsXmlContentValue;
042import org.opencms.xml.types.I_CmsXmlSchemaType;
043
044import java.io.ByteArrayOutputStream;
045import java.io.OutputStream;
046import java.util.ArrayList;
047import java.util.Collections;
048import java.util.Comparator;
049import java.util.HashMap;
050import java.util.HashSet;
051import java.util.Iterator;
052import java.util.List;
053import java.util.Locale;
054import java.util.Map;
055import java.util.Set;
056import java.util.concurrent.ConcurrentHashMap;
057
058import org.dom4j.Attribute;
059import org.dom4j.Document;
060import org.dom4j.Element;
061import org.dom4j.Node;
062import org.xml.sax.EntityResolver;
063
064/**
065 * Provides basic XML document handling functions useful when dealing
066 * with XML documents that are stored in the OpenCms VFS.<p>
067 *
068 * @since 6.0.0
069 */
070public abstract class A_CmsXmlDocument implements I_CmsXmlDocument {
071
072    /** The content conversion to use for this XML document. */
073    protected String m_conversion;
074
075    /** The document object of the document. */
076    protected Document m_document;
077
078    /** Maps element names to available locales. */
079    protected Map<String, Set<Locale>> m_elementLocales;
080
081    /** Maps locales to available element names. */
082    protected Map<Locale, Set<String>> m_elementNames;
083
084    /** The encoding to use for this XML document. */
085    protected String m_encoding;
086
087    /** The file that contains the document data (note: is not set when creating an empty or document based document). */
088    protected CmsFile m_file;
089
090    /** Set of locales contained in this document. */
091    protected Set<Locale> m_locales;
092
093    /** Reference for named elements in the document. */
094    private Map<String, I_CmsXmlContentValue> m_bookmarks;
095
096    /** Cache for temporary data associated with the content. */
097    private Map<String, Object> m_tempDataCache = new ConcurrentHashMap<>();
098
099    /**
100     * Default constructor for a XML document
101     * that initializes some internal values.<p>
102     */
103    protected A_CmsXmlDocument() {
104
105        m_bookmarks = new HashMap<String, I_CmsXmlContentValue>();
106        m_locales = new HashSet<Locale>();
107    }
108
109    /**
110     * Creates the bookmark name for a localized element to be used in the bookmark lookup table.<p>
111     *
112     * @param name the element name
113     * @param locale the element locale
114     * @return the bookmark name for a localized element
115     */
116    protected static final String getBookmarkName(String name, Locale locale) {
117
118        StringBuffer result = new StringBuffer(64);
119        result.append('/');
120        result.append(locale.toString());
121        result.append('/');
122        result.append(name);
123        return result.toString();
124    }
125
126    /**
127     * @see org.opencms.xml.I_CmsXmlDocument#copyLocale(java.util.List, java.util.Locale)
128     */
129    public void copyLocale(List<Locale> possibleSources, Locale destination) throws CmsXmlException {
130
131        if (hasLocale(destination)) {
132            throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_ALREADY_EXISTS_1, destination));
133        }
134        Iterator<Locale> i = possibleSources.iterator();
135        Locale source = null;
136        while (i.hasNext() && (source == null)) {
137            // check all locales and try to find the first match
138            Locale candidate = i.next();
139            if (hasLocale(candidate)) {
140                // locale has been found
141                source = candidate;
142            }
143        }
144        if (source != null) {
145            // found a locale, copy this to the destination
146            copyLocale(source, destination);
147        } else {
148            // no matching locale has been found
149            throw new CmsXmlException(
150                Messages.get().container(
151                    Messages.ERR_LOCALE_NOT_AVAILABLE_1,
152                    CmsLocaleManager.getLocaleNames(possibleSources)));
153        }
154    }
155
156    /**
157     * @see org.opencms.xml.I_CmsXmlDocument#copyLocale(java.util.Locale, java.util.Locale)
158     */
159    public void copyLocale(Locale source, Locale destination) throws CmsXmlException {
160
161        if (!hasLocale(source)) {
162            throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_NOT_AVAILABLE_1, source));
163        }
164        if (hasLocale(destination)) {
165            throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_ALREADY_EXISTS_1, destination));
166        }
167
168        Element sourceElement = null;
169        Element rootNode = m_document.getRootElement();
170        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(rootNode);
171        String localeStr = source.toString();
172        while (i.hasNext()) {
173            Element element = i.next();
174            String language = element.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, null);
175            if ((language != null) && (localeStr.equals(language))) {
176                // detach node with the locale
177                sourceElement = element.createCopy();
178                // there can be only one node for the locale
179                break;
180            }
181        }
182
183        if (sourceElement == null) {
184            // should not happen since this was checked already, just to make sure...
185            throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_NOT_AVAILABLE_1, source));
186        }
187
188        // switch locale value in attribute of copied node
189        sourceElement.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, destination.toString());
190        // attach the copied node to the root node
191        rootNode.add(sourceElement);
192
193        // re-initialize the document bookmarks
194        initDocument(m_document, m_encoding, getContentDefinition());
195    }
196
197    /**
198     * Corrects the structure of this XML document.<p>
199     *
200     * @param cms the current OpenCms user context
201     *
202     * @return the file that contains the corrected XML structure
203     *
204     * @throws CmsXmlException if something goes wrong
205     */
206    public CmsFile correctXmlStructure(CmsObject cms) throws CmsXmlException {
207
208        // apply XSD schema translation
209        Attribute schema = m_document.getRootElement().attribute(
210            I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION);
211        if (schema != null) {
212            String schemaLocation = schema.getValue();
213            String translatedSchema = OpenCms.getResourceManager().getXsdTranslator().translateResource(schemaLocation);
214            if (!schemaLocation.equals(translatedSchema)) {
215                schema.setValue(translatedSchema);
216            }
217        }
218        updateLocaleNodeSorting();
219
220        // iterate over all locales
221        Iterator<Locale> i = m_locales.iterator();
222        while (i.hasNext()) {
223            Locale locale = i.next();
224            List<String> names = getNames(locale);
225            List<I_CmsXmlContentValue> validValues = new ArrayList<I_CmsXmlContentValue>();
226
227            // iterate over all nodes per language
228            Iterator<String> j = names.iterator();
229            while (j.hasNext()) {
230
231                // this step is required for values that need a processing of their content
232                // an example for this is the HTML value that does link replacement
233                String name = j.next();
234                I_CmsXmlContentValue value = getValue(name, locale);
235                if (value.isSimpleType()) {
236                    String content = value.getStringValue(cms);
237                    value.setStringValue(cms, content);
238                }
239
240                // save valid elements for later check
241                validValues.add(value);
242            }
243
244            if (isAutoCorrectionEnabled()) {
245                // full correction of XML
246                if (validValues.size() < 1) {
247                    // no valid element was in the content
248                    if (hasLocale(locale)) {
249                        // remove the old locale entirely, as there was no valid element
250                        removeLocale(locale);
251                    }
252                    // add a new default locale, this will also generate the default XML as required
253                    addLocale(cms, locale);
254                } else {
255                    // there is at least one valid element in the content
256
257                    List<Element> roots = new ArrayList<Element>();
258                    List<CmsXmlContentDefinition> rootCds = new ArrayList<CmsXmlContentDefinition>();
259
260                    // gather all XML content definitions and their parent nodes
261                    Iterator<I_CmsXmlContentValue> it = validValues.iterator();
262                    while (it.hasNext()) {
263                        // collect all root elements, also for the nested content definitions
264                        I_CmsXmlContentValue value = it.next();
265                        Element element = value.getElement();
266                        if (element.supportsParent()) {
267                            // get the parent XML node
268                            Element root = element.getParent();
269                            if ((root != null) && !roots.contains(root)) {
270                                // this is a parent node we do not have already in our storage
271                                CmsXmlContentDefinition rcd = value.getContentDefinition();
272                                if (rcd != null) {
273                                    // this value has a valid XML content definition
274                                    roots.add(root);
275                                    rootCds.add(rcd);
276                                } else {
277                                    // no valid content definition for the XML value
278                                    throw new CmsXmlException(
279                                        Messages.get().container(
280                                            Messages.ERR_CORRECT_NO_CONTENT_DEF_3,
281                                            value.getName(),
282                                            value.getTypeName(),
283                                            value.getPath()));
284                                }
285                            }
286                        }
287                        // the following also adds empty nested contents
288                        if (value instanceof CmsXmlNestedContentDefinition) {
289                            if (!roots.contains(element)) {
290                                CmsXmlContentDefinition contentDef = ((CmsXmlNestedContentDefinition)value).getNestedContentDefinition();
291                                roots.add(element);
292                                rootCds.add(contentDef);
293                            }
294                        }
295                    }
296
297                    for (int le = 0; le < roots.size(); le++) {
298                        // iterate all XML content root nodes and correct each XML subtree
299
300                        Element root = roots.get(le);
301                        CmsXmlContentDefinition cd = rootCds.get(le);
302
303                        // step 1: first sort the nodes according to the schema, this takes care of re-ordered elements
304                        List<List<Element>> nodeLists = new ArrayList<List<Element>>();
305                        boolean isMultipleChoice = cd.getSequenceType() == CmsXmlContentDefinition.SequenceType.MULTIPLE_CHOICE;
306
307                        // if it's a multiple choice element, the child elements must not be sorted into their types,
308                        // but must keep their original order
309                        if (isMultipleChoice) {
310                            List<Element> nodeList = new ArrayList<Element>();
311                            List<Element> elements = CmsXmlGenericWrapper.elements(root);
312                            Set<String> typeNames = cd.getSchemaTypes();
313                            for (Element element : elements) {
314                                // check if the node type is still in the definition
315                                if (typeNames.contains(element.getName())) {
316                                    nodeList.add(element);
317                                }
318                            }
319                            checkMaxOccurs(nodeList, cd.getChoiceMaxOccurs(), cd.getTypeName());
320                            nodeLists.add(nodeList);
321                        }
322                        // if it's a sequence, the children are sorted according to the sequence type definition
323                        else {
324                            for (I_CmsXmlSchemaType type : cd.getTypeSequence()) {
325                                List<Element> elements = CmsXmlGenericWrapper.elements(root, type.getName());
326                                checkMaxOccurs(elements, type.getMaxOccurs(), type.getTypeName());
327                                nodeLists.add(elements);
328                            }
329                        }
330
331                        // step 2: clear the list of nodes (this will remove all invalid nodes)
332                        List<Element> nodeList = CmsXmlGenericWrapper.elements(root);
333                        nodeList.clear();
334                        Iterator<List<Element>> in = nodeLists.iterator();
335                        while (in.hasNext()) {
336                            // now add all valid nodes in the right order
337                            List<Element> elements = in.next();
338                            nodeList.addAll(elements);
339                        }
340
341                        // step 3: now append the missing elements according to the XML content definition
342                        cd.addDefaultXml(cms, this, root, locale);
343                    }
344                }
345            }
346            initDocument();
347        }
348
349        boolean removedCategoryField = false;
350        for (Locale locale : getLocales()) {
351            for (I_CmsXmlContentValue value : getValues(locale)) {
352                if ((value instanceof CmsXmlDynamicCategoryValue) && (value.getMinOccurs() == 0)) {
353                    value.getElement().detach();
354                    removedCategoryField = true;
355                }
356            }
357        }
358        if (removedCategoryField) {
359            initDocument();
360        }
361
362        for (Node node : m_document.selectNodes("//" + CmsXmlDynamicCategoryValue.N_CATEGORY_STRING)) {
363            node.detach();
364        }
365        for (Node node : m_document.selectNodes("//" + CmsXmlAccessRestrictionValue.N_ACCESS_RESTRICTION_VALUE)) {
366            node.detach();
367        }
368
369        // write the modified XML back to the VFS file
370        if (m_file != null) {
371            // make sure the file object is available
372            m_file.setContents(marshal());
373        }
374        return m_file;
375    }
376
377    /**
378     * @see org.opencms.xml.I_CmsXmlDocument#getBestMatchingLocale(java.util.Locale)
379     */
380    public Locale getBestMatchingLocale(Locale locale) {
381
382        // the requested locale is the match we want to find most
383        if (hasLocale(locale)) {
384            // check if the requested locale is directly available
385            return locale;
386        }
387        if (locale.getVariant().length() > 0) {
388            // locale has a variant like "en_EN_whatever", try only with language and country
389            Locale check = new Locale(locale.getLanguage(), locale.getCountry(), "");
390            if (hasLocale(check)) {
391                return check;
392            }
393        }
394        if (locale.getCountry().length() > 0) {
395            // locale has a country like "en_EN", try only with language
396            Locale check = new Locale(locale.getLanguage(), "", "");
397            if (hasLocale(check)) {
398                return check;
399            }
400        }
401        return null;
402    }
403
404    /**
405     * @see org.opencms.xml.I_CmsXmlDocument#getConversion()
406     */
407    public String getConversion() {
408
409        return m_conversion;
410    }
411
412    /**
413     * @see org.opencms.xml.I_CmsXmlDocument#getEncoding()
414     */
415    public String getEncoding() {
416
417        return m_encoding;
418    }
419
420    /**
421     * @see org.opencms.xml.I_CmsXmlDocument#getFile()
422     */
423    public CmsFile getFile() {
424
425        return m_file;
426    }
427
428    /**
429     * @see org.opencms.xml.I_CmsXmlDocument#getIndexCount(java.lang.String, java.util.Locale)
430     */
431    public int getIndexCount(String path, Locale locale) {
432
433        List<I_CmsXmlContentValue> elements = getValues(path, locale);
434        if (elements == null) {
435            return 0;
436        } else {
437            return elements.size();
438        }
439    }
440
441    /**
442     * @see org.opencms.xml.I_CmsXmlDocument#getLocales()
443     */
444    public List<Locale> getLocales() {
445
446        return new ArrayList<Locale>(m_locales);
447    }
448
449    /**
450     * Returns a List of all locales that have the named element set in this document.<p>
451     *
452     * If no locale for the given element name is available, an empty list is returned.<p>
453     *
454     * @param path the element to look up the locale List for
455     * @return a List of all Locales that have the named element set in this document
456     */
457    public List<Locale> getLocales(String path) {
458
459        Set<Locale> locales = m_elementLocales.get(CmsXmlUtils.createXpath(path, 1));
460        if (locales != null) {
461            return new ArrayList<Locale>(locales);
462        }
463        return Collections.emptyList();
464    }
465
466    /**
467     * @see org.opencms.xml.I_CmsXmlDocument#getNames(java.util.Locale)
468     */
469    public List<String> getNames(Locale locale) {
470
471        Set<String> names = m_elementNames.get(locale);
472        if (names != null) {
473            return new ArrayList<String>(names);
474        }
475        return Collections.emptyList();
476    }
477
478    /**
479     * @see org.opencms.xml.I_CmsXmlDocument#getStringValue(org.opencms.file.CmsObject, java.lang.String, java.util.Locale)
480     */
481    public String getStringValue(CmsObject cms, String path, Locale locale) {
482
483        I_CmsXmlContentValue value = getValueInternal(CmsXmlUtils.createXpath(path, 1), locale);
484        if (value != null) {
485            return value.getStringValue(cms);
486        }
487        return null;
488    }
489
490    /**
491     * @see org.opencms.xml.I_CmsXmlDocument#getStringValue(CmsObject, java.lang.String, Locale, int)
492     */
493    public String getStringValue(CmsObject cms, String path, Locale locale, int index) {
494
495        // directly calling getValueInternal() is more efficient then calling getStringValue(CmsObject, String, Locale)
496        // since the most costs are generated in resolving the xpath name
497        I_CmsXmlContentValue value = getValueInternal(CmsXmlUtils.createXpath(path, index + 1), locale);
498        if (value != null) {
499            return value.getStringValue(cms);
500        }
501        return null;
502    }
503
504    /**
505     * @see org.opencms.xml.I_CmsXmlDocument#getSubValues(java.lang.String, java.util.Locale)
506     */
507    public List<I_CmsXmlContentValue> getSubValues(String path, Locale locale) {
508
509        List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>();
510        String bookmark = getBookmarkName(CmsXmlUtils.createXpath(path, 1), locale);
511        I_CmsXmlContentValue value = getBookmark(bookmark);
512        if ((value != null) && !value.isSimpleType()) {
513            // calculate level of current bookmark
514            int depth = CmsResource.getPathLevel(bookmark) + 1;
515            Iterator<String> i = getBookmarks().iterator();
516            while (i.hasNext()) {
517                String bm = i.next();
518                if (bm.startsWith(bookmark) && (CmsResource.getPathLevel(bm) == depth)) {
519                    // add only values directly below the value
520                    result.add(getBookmark(bm));
521                }
522            }
523        }
524        return result;
525    }
526
527    /**
528     * Gets the temporary data cache.
529     *
530     * @return the temporary data cache
531     */
532    public Map<String, Object> getTempDataCache() {
533
534        return m_tempDataCache;
535    }
536
537    /**
538     * @see org.opencms.xml.I_CmsXmlDocument#getValue(java.lang.String, java.util.Locale)
539     */
540    public I_CmsXmlContentValue getValue(String path, Locale locale) {
541
542        return getValueInternal(CmsXmlUtils.createXpath(path, 1), locale);
543    }
544
545    /**
546     * @see org.opencms.xml.I_CmsXmlDocument#getValue(java.lang.String, java.util.Locale, int)
547     */
548    public I_CmsXmlContentValue getValue(String path, Locale locale, int index) {
549
550        return getValueInternal(CmsXmlUtils.createXpath(path, index + 1), locale);
551    }
552
553    /**
554     * @see org.opencms.xml.I_CmsXmlDocument#getValues(java.util.Locale)
555     */
556    public List<I_CmsXmlContentValue> getValues(Locale locale) {
557
558        List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>();
559
560        // bookmarks are stored with the locale as first prefix
561        String prefix = '/' + locale.toString() + '/';
562
563        // it's better for performance to iterate through the list of bookmarks directly
564        Iterator<Map.Entry<String, I_CmsXmlContentValue>> i = m_bookmarks.entrySet().iterator();
565        while (i.hasNext()) {
566            Map.Entry<String, I_CmsXmlContentValue> entry = i.next();
567            if (entry.getKey().startsWith(prefix)) {
568                result.add(entry.getValue());
569            }
570        }
571
572        // sort the result
573        Collections.sort(result);
574
575        return result;
576    }
577
578    /**
579     * @see org.opencms.xml.I_CmsXmlDocument#getValues(java.lang.String, java.util.Locale)
580     */
581    public List<I_CmsXmlContentValue> getValues(String path, Locale locale) {
582
583        List<I_CmsXmlContentValue> result = new ArrayList<I_CmsXmlContentValue>();
584        String bookmark = getBookmarkName(CmsXmlUtils.createXpath(CmsXmlUtils.removeXpathIndex(path), 1), locale);
585        I_CmsXmlContentValue value = getBookmark(bookmark);
586        if (value != null) {
587            if (value.getContentDefinition().getChoiceMaxOccurs() > 1) {
588                // selected value belongs to a xsd:choice
589                String parent = CmsXmlUtils.removeLastXpathElement(bookmark);
590                int depth = CmsResource.getPathLevel(bookmark);
591                Iterator<String> i = getBookmarks().iterator();
592                while (i.hasNext()) {
593                    String bm = i.next();
594                    if (bm.startsWith(parent) && (CmsResource.getPathLevel(bm) == depth)) {
595                        result.add(getBookmark(bm));
596                    }
597                }
598            } else {
599                // selected value belongs to a xsd:sequence
600                int index = 1;
601                String bm = CmsXmlUtils.removeXpathIndex(bookmark);
602                while (value != null) {
603                    result.add(value);
604                    index++;
605                    String subpath = CmsXmlUtils.createXpathElement(bm, index);
606                    value = getBookmark(subpath);
607                }
608            }
609        }
610        return result;
611    }
612
613    /**
614     * @see org.opencms.xml.I_CmsXmlDocument#hasLocale(java.util.Locale)
615     */
616    public boolean hasLocale(Locale locale) {
617
618        if (locale == null) {
619            throw new CmsIllegalArgumentException(Messages.get().container(Messages.ERR_NULL_LOCALE_0));
620        }
621
622        return m_locales.contains(locale);
623    }
624
625    /**
626     * @see org.opencms.xml.I_CmsXmlDocument#hasValue(java.lang.String, java.util.Locale)
627     */
628    public boolean hasValue(String path, Locale locale) {
629
630        return null != getBookmark(CmsXmlUtils.createXpath(path, 1), locale);
631    }
632
633    /**
634     * @see org.opencms.xml.I_CmsXmlDocument#hasValue(java.lang.String, java.util.Locale, int)
635     */
636    public boolean hasValue(String path, Locale locale, int index) {
637
638        return null != getBookmark(CmsXmlUtils.createXpath(path, index + 1), locale);
639    }
640
641    /**
642     * @see org.opencms.xml.I_CmsXmlDocument#initDocument()
643     */
644    public void initDocument() {
645
646        initDocument(m_document, m_encoding, getContentDefinition());
647    }
648
649    /**
650     * @see org.opencms.xml.I_CmsXmlDocument#isEnabled(java.lang.String, java.util.Locale)
651     */
652    public boolean isEnabled(String path, Locale locale) {
653
654        return hasValue(path, locale);
655    }
656
657    /**
658     * @see org.opencms.xml.I_CmsXmlDocument#isEnabled(java.lang.String, java.util.Locale, int)
659     */
660    public boolean isEnabled(String path, Locale locale, int index) {
661
662        return hasValue(path, locale, index);
663    }
664
665    /**
666     * Marshals (writes) the content of the current XML document
667     * into a byte array using the selected encoding.<p>
668     *
669     * @return the content of the current XML document written into a byte array
670     * @throws CmsXmlException if something goes wrong
671     */
672    public byte[] marshal() throws CmsXmlException {
673
674        return ((ByteArrayOutputStream)marshal(new ByteArrayOutputStream(), m_encoding)).toByteArray();
675    }
676
677    /**
678     * @see org.opencms.xml.I_CmsXmlDocument#moveLocale(java.util.Locale, java.util.Locale)
679     */
680    public void moveLocale(Locale source, Locale destination) throws CmsXmlException {
681
682        copyLocale(source, destination);
683        removeLocale(source);
684    }
685
686    /**
687     * @see org.opencms.xml.I_CmsXmlDocument#removeLocale(java.util.Locale)
688     */
689    public void removeLocale(Locale locale) throws CmsXmlException {
690
691        if (!hasLocale(locale)) {
692            throw new CmsXmlException(Messages.get().container(Messages.ERR_LOCALE_NOT_AVAILABLE_1, locale));
693        }
694
695        Element rootNode = m_document.getRootElement();
696        Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(rootNode);
697        String localeStr = locale.toString();
698        while (i.hasNext()) {
699            Element element = i.next();
700            String language = element.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE, null);
701            if ((language != null) && (localeStr.equals(language))) {
702                // detach node with the locale
703                element.detach();
704                // there can be only one node for the locale
705                break;
706            }
707        }
708
709        // re-initialize the document bookmarks
710        initDocument(m_document, m_encoding, getContentDefinition());
711    }
712
713    /**
714     * Sets the content conversion mode for this document.<p>
715     *
716     * @param conversion the conversion mode to set for this document
717     */
718    public void setConversion(String conversion) {
719
720        m_conversion = conversion;
721    }
722
723    /**
724     * @see java.lang.Object#toString()
725     */
726    @Override
727    public String toString() {
728
729        try {
730            return CmsXmlUtils.marshal(m_document, m_encoding);
731        } catch (CmsXmlException e) {
732            throw new CmsRuntimeException(Messages.get().container(Messages.ERR_WRITE_XML_DOC_TO_STRING_0), e);
733        }
734    }
735
736    /**
737     * Validates the XML structure of the document with the DTD or XML schema used by the document.<p>
738     *
739     * This is required in case someone modifies the XML structure of a
740     * document using the "edit control code" option.<p>
741     *
742     * @param resolver the XML entity resolver to use
743     * @throws CmsXmlException if the validation fails
744     */
745    public void validateXmlStructure(EntityResolver resolver) throws CmsXmlException {
746
747        if (m_file != null) {
748            // file is set, use bytes from file directly
749            CmsXmlUtils.validateXmlStructure(m_file.getContents(), resolver);
750        } else {
751            // use XML document - note that this will be copied in a byte[] array first
752            CmsXmlUtils.validateXmlStructure(m_document, m_encoding, resolver);
753        }
754    }
755
756    /**
757     * Adds a bookmark for the given value.<p>
758     *
759     * @param path the lookup path to use for the bookmark
760     * @param locale the locale to use for the bookmark
761     * @param enabled if true, the value is enabled, if false it is disabled
762     * @param value the value to bookmark
763     */
764    protected void addBookmark(String path, Locale locale, boolean enabled, I_CmsXmlContentValue value) {
765
766        // add the locale (since the locales are a set adding them more then once does not matter)
767        addLocale(locale);
768
769        // add a bookmark to the provided value
770        m_bookmarks.put(getBookmarkName(path, locale), value);
771
772        Set<Locale> sl;
773        // update mapping of element name to locale
774        if (enabled) {
775            // only include enabled elements
776            sl = m_elementLocales.get(path);
777            if (sl != null) {
778                sl.add(locale);
779            } else {
780                Set<Locale> set = new HashSet<Locale>();
781                set.add(locale);
782                m_elementLocales.put(path, set);
783            }
784        }
785        // update mapping of locales to element names
786        Set<String> sn = m_elementNames.get(locale);
787        if (sn == null) {
788            sn = new HashSet<String>();
789            m_elementNames.put(locale, sn);
790        }
791        sn.add(path);
792    }
793
794    /**
795     * Adds a locale to the set of locales of the XML document.<p>
796     *
797     * @param locale the locale to add
798     */
799    protected void addLocale(Locale locale) {
800
801        // add the locale to all locales in this dcoument
802        m_locales.add(locale);
803    }
804
805    /**
806     * Clears the XML document bookmarks.<p>
807     */
808    protected void clearBookmarks() {
809
810        m_bookmarks.clear();
811    }
812
813    /**
814     * Creates a partial deep element copy according to the set of element paths.<p>
815     * Only elements contained in that set will be copied.
816     *
817     * @param element the element to copy
818     * @param copyElements the set of paths for elements to copy
819     *
820     * @return a partial deep copy of <code>element</code>
821     */
822    protected Element createDeepElementCopy(Element element, Set<String> copyElements) {
823
824        return createDeepElementCopyInternal(null, null, element, copyElements);
825    }
826
827    /**
828     * Returns the bookmarked value for the given bookmark,
829     * which must be a valid bookmark name.
830     *
831     * Use {@link #getBookmarks()} to get the list of all valid bookmark names.<p>
832     *
833     * @param bookmark the bookmark name to look up
834     * @return the bookmarked value for the given bookmark
835     */
836    protected I_CmsXmlContentValue getBookmark(String bookmark) {
837
838        return m_bookmarks.get(bookmark);
839    }
840
841    /**
842     * Returns the bookmarked value for the given name.<p>
843     *
844     * @param path the lookup path to use for the bookmark
845     * @param locale the locale to get the bookmark for
846     * @return the bookmarked value
847     */
848    protected I_CmsXmlContentValue getBookmark(String path, Locale locale) {
849
850        return m_bookmarks.get(getBookmarkName(path, locale));
851    }
852
853    /**
854     * Returns the names of all bookmarked elements.<p>
855     *
856     * @return the names of all bookmarked elements
857     */
858    protected Set<String> getBookmarks() {
859
860        return m_bookmarks.keySet();
861    }
862
863    /**
864     * Internal method to look up a value, requires that the name already has been
865     * "normalized" for the bookmark lookup.
866     *
867     * This is required to find names like "title/subtitle" which are stored
868     * internally as "title[0]/subtitle[0]" in the bookmarks.
869     *
870     * @param path the path to look up
871     * @param locale the locale to look up
872     *
873     * @return the value found in the bookmarks
874     */
875    protected I_CmsXmlContentValue getValueInternal(String path, Locale locale) {
876
877        return getBookmark(path, locale);
878    }
879
880    /**
881     * Initializes an XML document based on the provided document, encoding and content definition.<p>
882     *
883     * @param document the base XML document to use for initializing
884     * @param encoding the encoding to use when marshalling the document later
885     * @param contentDefinition the content definition to use
886     */
887    protected abstract void initDocument(Document document, String encoding, CmsXmlContentDefinition contentDefinition);
888
889    /**
890     * Returns <code>true</code> if the auto correction feature is enabled for saving this XML content.<p>
891     *
892     * @return <code>true</code> if the auto correction feature is enabled for saving this XML content
893     */
894    protected boolean isAutoCorrectionEnabled() {
895
896        // by default, this method always returns false
897        return false;
898    }
899
900    /**
901     * Marshals (writes) the content of the current XML document
902     * into an output stream.<p>
903     *
904     * @param out the output stream to write to
905     * @param encoding the encoding to use
906     * @return the output stream with the XML content
907     * @throws CmsXmlException if something goes wrong
908     */
909    protected OutputStream marshal(OutputStream out, String encoding) throws CmsXmlException {
910
911        return CmsXmlUtils.marshal(m_document, out, encoding);
912    }
913
914    /**
915     * Removes the bookmark for an element with the given name and locale.<p>
916     *
917     * @param path the lookup path to use for the bookmark
918     * @param locale the locale of the element
919     * @return the element removed from the bookmarks or null
920     */
921    protected I_CmsXmlContentValue removeBookmark(String path, Locale locale) {
922
923        // remove mapping of element name to locale
924        Set<Locale> sl;
925        sl = m_elementLocales.get(path);
926        if (sl != null) {
927            sl.remove(locale);
928        }
929        // remove mapping of locale to element name
930        Set<String> sn = m_elementNames.get(locale);
931        if (sn != null) {
932            sn.remove(path);
933        }
934        // remove the bookmark and return the removed element
935        return m_bookmarks.remove(getBookmarkName(path, locale));
936    }
937
938    /**
939     * Updates the order of the locale nodes if required.<p>
940     */
941    protected void updateLocaleNodeSorting() {
942
943        // check if the locale nodes require sorting
944        List<Locale> locales = new ArrayList<Locale>(m_locales);
945        Collections.sort(locales, new Comparator<Locale>() {
946
947            public int compare(Locale o1, Locale o2) {
948
949                return o1.toString().compareTo(o2.toString());
950            }
951        });
952        List<Element> localeNodes = new ArrayList<Element>(m_document.getRootElement().elements());
953        boolean sortRequired = false;
954        if (localeNodes.size() != locales.size()) {
955            sortRequired = true;
956        } else {
957            int i = 0;
958            for (Element el : localeNodes) {
959                if (!locales.get(i).toString().equals(el.attributeValue("language"))) {
960                    sortRequired = true;
961                    break;
962                }
963                i++;
964            }
965        }
966
967        if (sortRequired) {
968            // do the actual node sorting, by removing the nodes first
969            for (Element el : localeNodes) {
970                m_document.getRootElement().remove(el);
971            }
972
973            Collections.sort(localeNodes, new Comparator<Object>() {
974
975                public int compare(Object o1, Object o2) {
976
977                    String locale1 = ((Element)o1).attributeValue("language");
978                    String locale2 = ((Element)o2).attributeValue("language");
979                    return locale1.compareTo(locale2);
980                }
981            });
982            // re-adding the nodes in alphabetical order
983            for (Element el : localeNodes) {
984                m_document.getRootElement().add(el);
985            }
986        }
987    }
988
989    /**
990     * Removes all nodes that exceed newly defined maxOccurs rules from the list of elements.<p>
991     *
992     * @param elements the list of elements to check
993     * @param maxOccurs maximum number of elements allowed
994     * @param typeName name of the element type
995     */
996    private void checkMaxOccurs(List<Element> elements, int maxOccurs, String typeName) {
997
998        if (elements.size() > maxOccurs) {
999            if (typeName.equals(CmsXmlCategoryValue.TYPE_NAME)) {
1000                if (maxOccurs == 1) {
1001                    Element category = elements.get(0);
1002                    List<Element> categories = new ArrayList<Element>();
1003                    for (Element value : elements) {
1004                        Iterator<Element> itLink = value.elementIterator();
1005                        while (itLink.hasNext()) {
1006                            Element link = itLink.next();
1007                            categories.add((Element)link.clone());
1008                        }
1009                    }
1010                    category.clearContent();
1011                    for (Element value : categories) {
1012                        category.add(value);
1013                    }
1014                }
1015            }
1016
1017            // too many nodes of this type appear according to the current schema definition
1018            for (int lo = (elements.size() - 1); lo >= maxOccurs; lo--) {
1019                elements.remove(lo);
1020            }
1021        }
1022    }
1023
1024    /**
1025     * Creates a partial deep element copy according to the set of element paths.<p>
1026     * Only elements contained in that set will be copied.
1027     *
1028     * @param parentPath the path of the parent element or <code>null</code>, initially
1029     * @param parent the parent element
1030     * @param element the element to copy
1031     * @param copyElements the set of paths for elements to copy
1032     *
1033     * @return a partial deep copy of <code>element</code>
1034     */
1035    private Element createDeepElementCopyInternal(
1036        String parentPath,
1037        Element parent,
1038        Element element,
1039        Set<String> copyElements) {
1040
1041        String elName = element.getName();
1042        if (parentPath != null) {
1043            Element first = element.getParent().element(elName);
1044            int elIndex = (element.getParent().indexOf(element) - first.getParent().indexOf(first)) + 1;
1045            elName = parentPath + (parentPath.length() > 0 ? "/" : "") + elName.concat("[" + elIndex + "]");
1046        }
1047
1048        if ((parentPath == null) || copyElements.contains(elName)) {
1049            // this is a content element we want to copy
1050            Element copy = element.createCopy();
1051            // copy.detach();
1052            if (parentPath != null) {
1053                parent.add(copy);
1054            }
1055
1056            // check if we need to copy subelements, too
1057            boolean copyNested = (parentPath == null);
1058            for (Iterator<String> i = copyElements.iterator(); !copyNested && i.hasNext();) {
1059                String path = i.next();
1060                copyNested = !elName.equals(path) && path.startsWith(elName);
1061            }
1062
1063            if (copyNested) {
1064                copy.clearContent();
1065                for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(element); i.hasNext();) {
1066                    Element el = i.next();
1067                    createDeepElementCopyInternal((parentPath == null) ? "" : elName, copy, el, copyElements);
1068                }
1069            }
1070
1071            return copy;
1072        } else {
1073            return null;
1074        }
1075    }
1076}