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