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