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