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.page;
029
030import org.opencms.configuration.CmsConfigurationManager;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.i18n.CmsEncoder;
035import org.opencms.i18n.CmsLocaleManager;
036import org.opencms.main.CmsIllegalArgumentException;
037import org.opencms.main.CmsLog;
038import org.opencms.main.CmsRuntimeException;
039import org.opencms.staticexport.CmsLinkProcessor;
040import org.opencms.staticexport.CmsLinkTable;
041import org.opencms.xml.A_CmsXmlDocument;
042import org.opencms.xml.CmsXmlContentDefinition;
043import org.opencms.xml.CmsXmlEntityResolver;
044import org.opencms.xml.CmsXmlException;
045import org.opencms.xml.CmsXmlGenericWrapper;
046import org.opencms.xml.CmsXmlUtils;
047import org.opencms.xml.content.CmsXmlContentErrorHandler;
048import org.opencms.xml.content.I_CmsXmlContentHandler;
049import org.opencms.xml.types.CmsXmlHtmlValue;
050import org.opencms.xml.types.I_CmsXmlContentValue;
051import org.opencms.xml.types.I_CmsXmlSchemaType;
052
053import java.io.IOException;
054import java.util.ArrayList;
055import java.util.Collections;
056import java.util.HashMap;
057import java.util.HashSet;
058import java.util.Iterator;
059import java.util.LinkedHashSet;
060import java.util.List;
061import java.util.Locale;
062import java.util.Map;
063import java.util.Set;
064
065import org.apache.commons.logging.Log;
066
067import org.dom4j.Attribute;
068import org.dom4j.Document;
069import org.dom4j.DocumentHelper;
070import org.dom4j.Element;
071import org.xml.sax.InputSource;
072
073/**
074 * Implementation of a page object used to access and manage xml data.<p>
075 *
076 * This implementation consists of several named elements optionally available for
077 * various languages. The data of each element is accessible via its name and language.
078 *
079 * The content of each element is stored as CDATA, links within the
080 * content are processed and are separately accessible as entries of a CmsLinkTable.
081 *
082 * @since 6.0.0
083 */
084public class CmsXmlPage extends A_CmsXmlDocument {
085
086    /** Name of the name attribute of the elements node. */
087    public static final String ATTRIBUTE_ENABLED = "enabled";
088
089    /** Name of the language attribute of the elements node. */
090    public static final String ATTRIBUTE_LANGUAGE = "language";
091
092    /** Name of the name attribute of the elements node. */
093    public static final String ATTRIBUTE_NAME = "name";
094
095    /** Name of the element node. */
096    public static final String NODE_CONTENT = "content";
097
098    /** Name of the elements node. */
099    public static final String NODE_ELEMENTS = "elements";
100
101    /** Name of the link node. */
102    public static final String NODE_LINK = "link";
103
104    /** Name of the links node. */
105    public static final String NODE_LINKS = "links";
106
107    /** Name of the page node. */
108    public static final String NODE_PAGE = "page";
109
110    /** Name of the page node. */
111    public static final String NODE_PAGES = "pages";
112
113    /** Property to check if relative links are allowed. */
114    public static final String PROPERTY_ALLOW_RELATIVE = "allowRelativeLinks";
115
116    /** The DTD address of the OpenCms xmlpage. */
117    public static final String XMLPAGE_XSD_SYSTEM_ID = CmsConfigurationManager.DEFAULT_DTD_PREFIX + "xmlpage.xsd";
118
119    /** The log object for this class. */
120    private static final Log LOG = CmsLog.getLog(CmsXmlPage.class);
121
122    /** The XML page content definition is static. */
123    private static CmsXmlContentDefinition m_xmlPageContentDefinition;
124
125    /** Name of the element node. */
126    private static final String NODE_ELEMENT = "element";
127
128    /** Indicates if relative Links are allowed. */
129    private boolean m_allowRelativeLinks;
130
131    /**
132     * Creates a new CmsXmlPage based on the provided document and encoding.<p>
133     *
134     * The encoding is used for marshalling the XML document later.<p>
135     *
136     * @param document the document to create the CmsXmlPage from
137     * @param encoding the encoding of the xml page
138     */
139    public CmsXmlPage(Document document, String encoding) {
140
141        initDocument(document, encoding, getContentDefinition());
142    }
143
144    /**
145     * Creates an empty XML page in the provided locale using
146     * the provided encoding.<p>
147     *
148     * The page is initialized according to the minimal necessary xml structure.
149     * The encoding is used for marshalling the XML document later.<p>
150     *
151     * @param locale the initial locale of the XML page
152     * @param encoding the encoding of the XML page
153     */
154    public CmsXmlPage(Locale locale, String encoding) {
155
156        initDocument(CmsXmlPageFactory.createDocument(locale), encoding, getContentDefinition());
157    }
158
159    /**
160     * @see org.opencms.xml.I_CmsXmlDocument#addLocale(org.opencms.file.CmsObject, java.util.Locale)
161     */
162    public void addLocale(CmsObject cms, Locale locale) throws CmsXmlException {
163
164        if (hasLocale(locale)) {
165            throw new CmsXmlException(Messages.get().container(Messages.ERR_XML_PAGE_LOCALE_EXISTS_1, locale));
166        }
167        // add element node for Locale
168        getContentDefinition().createLocale(cms, this, m_document.getRootElement(), locale);
169        // re-initialize the bookmarks
170        initDocument(m_document, m_encoding, getContentDefinition());
171    }
172
173    /**
174     * Adds a new, empty value with the given name and locale
175     * to this XML document.<p>
176     *
177     * @param name the name of the value
178     * @param locale the locale of the value
179     *
180     * @throws CmsIllegalArgumentException if the name contains an index ("[&lt;number&gt;]") or the value for the
181     *         given locale already exists in the xmlpage.
182     *
183     */
184    public void addValue(String name, Locale locale) throws CmsIllegalArgumentException {
185
186        if (name.indexOf('[') >= 0) {
187            throw new CmsIllegalArgumentException(
188                Messages.get().container(Messages.ERR_XML_PAGE_CONTAINS_INDEX_1, name));
189        }
190
191        if (hasValue(name, locale)) {
192            throw new CmsIllegalArgumentException(
193                Messages.get().container(Messages.ERR_XML_PAGE_LANG_ELEM_EXISTS_2, name, locale));
194        }
195
196        Element pages = m_document.getRootElement();
197        String localeStr = locale.toString();
198        Element page = null;
199
200        // search if a page for the selected language is already available
201        for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(pages, NODE_PAGE); i.hasNext();) {
202            Element nextPage = i.next();
203            String language = nextPage.attributeValue(ATTRIBUTE_LANGUAGE);
204            if (localeStr.equals(language)) {
205                // a page for the selected language was found
206                page = nextPage;
207                break;
208            }
209        }
210
211        // create the new element
212        Element element;
213        if (page != null) {
214            // page for selected language already available
215            element = page.addElement(NODE_ELEMENT).addAttribute(ATTRIBUTE_NAME, name);
216        } else {
217            // no page for the selected language was found
218            element = pages.addElement(NODE_PAGE).addAttribute(ATTRIBUTE_LANGUAGE, localeStr);
219            element = element.addElement(NODE_ELEMENT).addAttribute(ATTRIBUTE_NAME, name);
220        }
221
222        // add empty nodes for link table and content to the element
223        element.addElement(NODE_LINKS);
224        element.addElement(NODE_CONTENT);
225
226        CmsXmlHtmlValue value = new CmsXmlHtmlValue(this, element, locale);
227
228        // bookmark the element
229        addBookmark(CmsXmlUtils.createXpathElement(name, 1), locale, true, value);
230    }
231
232    /**
233     * Returns if relative links are accepted (and left unprocessed).<p>
234     *
235     * @return true if relative links are allowed
236     */
237    public boolean getAllowRelativeLinks() {
238
239        return m_allowRelativeLinks;
240    }
241
242    /**
243     * @see org.opencms.xml.I_CmsXmlDocument#getContentDefinition()
244     */
245    public CmsXmlContentDefinition getContentDefinition() throws CmsRuntimeException {
246
247        if (m_xmlPageContentDefinition == null) {
248            // since XML page schema is cached anyway we don't need an CmsObject instance
249            CmsXmlEntityResolver resolver = new CmsXmlEntityResolver(null);
250            InputSource source;
251            try {
252                source = resolver.resolveEntity(null, XMLPAGE_XSD_SYSTEM_ID);
253                // store content definition in static variable
254                m_xmlPageContentDefinition = CmsXmlContentDefinition.unmarshal(source, XMLPAGE_XSD_SYSTEM_ID, resolver);
255            } catch (CmsXmlException e) {
256                throw new CmsRuntimeException(
257                    Messages.get().container(Messages.ERR_XML_PAGE_UNMARSHAL_CONTENDDEF_0),
258                    e);
259            } catch (IOException e) {
260                throw new CmsRuntimeException(
261                    Messages.get().container(Messages.ERR_XML_PAGE_UNMARSHAL_CONTENDDEF_0),
262                    e);
263            }
264        }
265        return m_xmlPageContentDefinition;
266    }
267
268    /**
269     * @see org.opencms.xml.I_CmsXmlDocument#getHandler()
270     */
271    public I_CmsXmlContentHandler getHandler() {
272
273        return getContentDefinition().getContentHandler();
274    }
275
276    /**
277     * @see org.opencms.xml.A_CmsXmlDocument#getLinkProcessor(org.opencms.file.CmsObject, org.opencms.staticexport.CmsLinkTable)
278     */
279    public CmsLinkProcessor getLinkProcessor(CmsObject cms, CmsLinkTable linkTable) {
280
281        // initialize link processor
282        String relativeRoot = null;
283        if ((!m_allowRelativeLinks) && (m_file != null)) {
284            relativeRoot = CmsResource.getParentFolder(cms.getSitePath(m_file));
285        }
286        return new CmsLinkProcessor(cms, linkTable, getEncoding(), relativeRoot);
287    }
288
289    /**
290     * Returns the link table of an element.<p>
291     *
292     * @param name name of the element
293     * @param locale locale of the element
294     * @return the link table
295     */
296    public CmsLinkTable getLinkTable(String name, Locale locale) {
297
298        CmsXmlHtmlValue value = (CmsXmlHtmlValue)getValue(name, locale);
299        if (value != null) {
300            return value.getLinkTable();
301        }
302        return new CmsLinkTable();
303    }
304
305    /**
306     * @see org.opencms.xml.A_CmsXmlDocument#getNames(java.util.Locale)
307     */
308    @Override
309    public List<String> getNames(Locale locale) {
310
311        Set<String> sn = m_elementNames.get(locale);
312        if (sn != null) {
313            List<String> result = new ArrayList<String>();
314            Iterator<String> i = sn.iterator();
315            while (i.hasNext()) {
316                String path = i.next();
317                result.add(CmsXmlUtils.removeXpathIndex(path));
318            }
319            return result;
320        }
321        return Collections.emptyList();
322    }
323
324    /**
325     * Checks if the element of a page object is enabled.<p>
326     *
327     * @param name the name of the element
328     * @param locale the locale of the element
329     * @return true if the element exists and is not disabled
330     */
331    @Override
332    public boolean isEnabled(String name, Locale locale) {
333
334        CmsXmlHtmlValue value = (CmsXmlHtmlValue)getValue(name, locale);
335
336        if (value != null) {
337            Element element = value.getElement();
338            Attribute enabled = element.attribute(ATTRIBUTE_ENABLED);
339            return ((enabled == null) || Boolean.valueOf(enabled.getValue()).booleanValue());
340        }
341
342        return false;
343    }
344
345    /**
346     * Removes an existing value with the given name and locale
347     * from this XML document.<p>
348     *
349     * @param name the name of the value
350     * @param locale the locale of the value
351     */
352    public void removeValue(String name, Locale locale) {
353
354        I_CmsXmlContentValue value = removeBookmark(CmsXmlUtils.createXpath(name, 1), locale);
355        if (value != null) {
356            Element element = value.getElement();
357            element.detach();
358        }
359    }
360
361    /**
362     * Renames the page-element value from the old to the new one.<p>
363     *
364     * @param oldValue the old value
365     * @param newValue the new value
366     * @param locale the locale
367     *
368     * @throws CmsIllegalArgumentException if the name contains an index ("[&lt;number&gt;]"), the new value for the
369     *         given locale already exists in the xmlpage or the the old value does not exist for the locale in the xmlpage.
370     *
371     */
372    public void renameValue(String oldValue, String newValue, Locale locale) throws CmsIllegalArgumentException {
373
374        CmsXmlHtmlValue oldXmlHtmlValue = (CmsXmlHtmlValue)getValue(oldValue, locale);
375        if (oldXmlHtmlValue == null) {
376            throw new CmsIllegalArgumentException(
377                Messages.get().container(Messages.ERR_XML_PAGE_NO_ELEM_FOR_LANG_2, oldValue, locale));
378        }
379
380        if (hasValue(newValue, locale)) {
381            throw new CmsIllegalArgumentException(
382                Messages.get().container(Messages.ERR_XML_PAGE_LANG_ELEM_EXISTS_2, newValue, locale));
383        }
384        if (newValue.indexOf('[') >= 0) {
385            throw new CmsIllegalArgumentException(
386                Messages.get().container(Messages.ERR_XML_PAGE_CONTAINS_INDEX_1, newValue));
387        }
388
389        // get the element
390        Element element = oldXmlHtmlValue.getElement();
391
392        // update value of the element attribute 'NAME'
393        element.addAttribute(ATTRIBUTE_NAME, newValue);
394
395        // re-initialize the document to update the bookmarks
396        initDocument(m_document, m_encoding, getContentDefinition());
397    }
398
399    /**
400     * Sets the enabled flag of an already existing element.<p>
401     *
402     * Note: if isEnabled is set to true, the attribute is removed
403     * since true is the default
404     *
405     * @param name name name of the element
406     * @param locale locale of the element
407     * @param isEnabled enabled flag for the element
408     */
409    public void setEnabled(String name, Locale locale, boolean isEnabled) {
410
411        CmsXmlHtmlValue value = (CmsXmlHtmlValue)getValue(name, locale);
412        Element element = value.getElement();
413        Attribute enabled = element.attribute(ATTRIBUTE_ENABLED);
414
415        if (enabled == null) {
416            if (!isEnabled) {
417                element.addAttribute(ATTRIBUTE_ENABLED, Boolean.toString(isEnabled));
418            }
419        } else if (isEnabled) {
420            element.remove(enabled);
421        } else {
422            enabled.setValue(Boolean.toString(isEnabled));
423        }
424    }
425
426    /**
427     * Sets the data of an already existing value.<p>
428     *
429     * The data will be enclosed as CDATA within the xml page structure.
430     * When setting the element data, the content of this element will be
431     * processed automatically.<p>
432     *
433     * @param cms the cms object
434     * @param name name of the element
435     * @param locale locale of the element
436     * @param content character data (CDATA) of the element
437     *
438     * @throws CmsXmlException if something goes wrong
439     */
440    public void setStringValue(CmsObject cms, String name, Locale locale, String content) throws CmsXmlException {
441
442        CmsXmlHtmlValue value = (CmsXmlHtmlValue)getValue(name, locale);
443
444        if (value != null) {
445            // set the values
446            value.setStringValue(cms, content);
447        } else {
448            throw new CmsXmlException(
449                Messages.get().container(Messages.ERR_XML_PAGE_INVALID_ELEM_SELECT_2, locale, name));
450        }
451    }
452
453    /**
454     * @see org.opencms.xml.I_CmsXmlDocument#validate(org.opencms.file.CmsObject)
455     */
456    public CmsXmlContentErrorHandler validate(CmsObject cms) {
457
458        // XML pages currently do not support validation
459        return new CmsXmlContentErrorHandler();
460    }
461
462    /**
463     * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition)
464     */
465    @Override
466    protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) {
467
468        m_encoding = CmsEncoder.lookupEncoding(encoding, encoding);
469        m_document = document;
470        m_elementLocales = new HashMap<String, Set<Locale>>();
471        m_elementNames = new HashMap<Locale, LinkedHashSet<String>>();
472        m_locales = new HashSet<Locale>();
473
474        // convert pre 5.3.6 XML page documents
475        if (!NODE_PAGES.equals(m_document.getRootElement().getName())) {
476            convertOldDocument();
477        }
478
479        // initialize the bookmarks
480        clearBookmarks();
481        Element pages = m_document.getRootElement();
482        try {
483            for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(pages, NODE_PAGE); i.hasNext();) {
484
485                Element page = i.next();
486                Locale locale = CmsLocaleManager.getLocale(page.attributeValue(ATTRIBUTE_LANGUAGE));
487                for (Iterator<Element> j = CmsXmlGenericWrapper.elementIterator(page, NODE_ELEMENT); j.hasNext();) {
488
489                    Element element = j.next();
490                    String name = element.attributeValue(ATTRIBUTE_NAME);
491
492                    String elementEnabled = element.attributeValue(ATTRIBUTE_ENABLED);
493                    boolean enabled = (elementEnabled == null) ? true : Boolean.valueOf(elementEnabled).booleanValue();
494
495                    // create an element type from the XML node
496                    CmsXmlHtmlValue value = new CmsXmlHtmlValue(this, element, locale);
497                    value.setContentDefinition(definition);
498
499                    // add the element type bookmark
500                    addBookmark(CmsXmlUtils.createXpathElement(name, 1), locale, enabled, value);
501                }
502                addLocale(locale);
503            }
504        } catch (NullPointerException e) {
505            LOG.error(Messages.get().getBundle().key(Messages.ERR_XML_PAGE_INIT_BOOKMARKS_0), e);
506        }
507    }
508
509    /**
510     * Sets the parameter that controls the relative link generation.<p>
511     *
512     * @param value the parameter that controls the relative link generation
513     */
514    protected void setAllowRelativeLinks(boolean value) {
515
516        m_allowRelativeLinks = value;
517    }
518
519    /**
520     * Sets the file this XML page content is written to.<p>
521     *
522     * @param file the file this XML page content is written to
523     */
524    protected void setFile(CmsFile file) {
525
526        m_file = file;
527    }
528
529    /**
530     * Converts the XML structure of the pre 5.5.0 development version of
531     * the XML page to the final 6.0 version.<p>
532     */
533    private void convertOldDocument() {
534
535        Document newDocument = DocumentHelper.createDocument();
536        Element root = newDocument.addElement(NODE_PAGES);
537        root.add(I_CmsXmlSchemaType.XSI_NAMESPACE);
538        root.addAttribute(I_CmsXmlSchemaType.XSI_NAMESPACE_ATTRIBUTE_NO_SCHEMA_LOCATION, XMLPAGE_XSD_SYSTEM_ID);
539
540        Map<String, Element> pages = new HashMap<String, Element>();
541
542        if ((m_document.getRootElement() != null) && (m_document.getRootElement().element(NODE_ELEMENTS) != null)) {
543            for (Iterator<Element> i = CmsXmlGenericWrapper.elementIterator(
544                m_document.getRootElement().element(NODE_ELEMENTS),
545                NODE_ELEMENT); i.hasNext();) {
546
547                Element elem = i.next();
548                try {
549                    String elementName = elem.attributeValue(ATTRIBUTE_NAME);
550                    String elementLang = elem.attributeValue(ATTRIBUTE_LANGUAGE);
551                    String elementEnabled = elem.attributeValue(ATTRIBUTE_ENABLED);
552                    boolean enabled = (elementEnabled == null) ? true : Boolean.valueOf(elementEnabled).booleanValue();
553
554                    Element page = pages.get(elementLang);
555                    if (page == null) {
556                        // no page available for the language, add one
557                        page = root.addElement(NODE_PAGE).addAttribute(ATTRIBUTE_LANGUAGE, elementLang);
558                        pages.put(elementLang, page);
559                    }
560
561                    Element newElement = page.addElement(NODE_ELEMENT).addAttribute(ATTRIBUTE_NAME, elementName);
562                    if (!enabled) {
563                        newElement.addAttribute(ATTRIBUTE_ENABLED, String.valueOf(enabled));
564                    }
565                    Element links = elem.element(NODE_LINKS);
566                    if (links != null) {
567                        newElement.add(links.createCopy());
568                    }
569                    Element content = elem.element(NODE_CONTENT);
570                    if (content != null) {
571                        newElement.add(content.createCopy());
572                    }
573
574                } catch (NullPointerException e) {
575                    LOG.error(Messages.get().getBundle().key(Messages.ERR_XML_PAGE_CONVERT_CONTENT_0), e);
576                }
577            }
578        }
579
580        // now replace the old with the new document
581        m_document = newDocument;
582    }
583}