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