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