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, 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.containerpage;
029
030import org.opencms.ade.containerpage.shared.CmsContainer;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
036import org.opencms.i18n.CmsEncoder;
037import org.opencms.i18n.CmsLocaleManager;
038import org.opencms.main.CmsException;
039import org.opencms.main.CmsLog;
040import org.opencms.main.OpenCms;
041import org.opencms.relations.CmsLink;
042import org.opencms.relations.CmsRelationType;
043import org.opencms.util.CmsMacroResolver;
044import org.opencms.util.CmsStringUtil;
045import org.opencms.util.CmsUUID;
046import org.opencms.xml.CmsXmlContentDefinition;
047import org.opencms.xml.CmsXmlException;
048import org.opencms.xml.CmsXmlGenericWrapper;
049import org.opencms.xml.CmsXmlUtils;
050import org.opencms.xml.content.CmsXmlContent;
051import org.opencms.xml.content.CmsXmlContentMacroVisitor;
052import org.opencms.xml.content.CmsXmlContentProperty;
053import org.opencms.xml.content.CmsXmlContentPropertyHelper;
054import org.opencms.xml.page.CmsXmlPage;
055import org.opencms.xml.types.CmsXmlNestedContentDefinition;
056import org.opencms.xml.types.CmsXmlVfsFileValue;
057import org.opencms.xml.types.I_CmsXmlContentValue;
058import org.opencms.xml.types.I_CmsXmlSchemaType;
059
060import java.util.ArrayList;
061import java.util.HashMap;
062import java.util.HashSet;
063import java.util.Iterator;
064import java.util.LinkedHashSet;
065import java.util.List;
066import java.util.Locale;
067import java.util.Map;
068import java.util.Set;
069
070import org.apache.commons.logging.Log;
071
072import org.dom4j.Document;
073import org.dom4j.Element;
074import org.xml.sax.EntityResolver;
075
076/**
077 * Implementation of a object used to access and manage the xml data of a group container.<p>
078 *
079 * In addition to the XML content interface. It also provides access to more comfortable beans.<p>
080 *
081 * @since 8.0.0
082 */
083public class CmsXmlGroupContainer extends CmsXmlContent {
084
085    /** XML node name constants. */
086    public enum XmlNode {
087
088        /** Container description node name. */
089        Description,
090        /** Container elements node name. */
091        Element,
092        /** Main node name. */
093        GroupContainers,
094        /** Container title node name. */
095        Title,
096        /** Container type node name. */
097        Type,
098        /** File list URI node name. */
099        Uri;
100    }
101
102    /** The log object for this class. */
103    private static final Log LOG = CmsLog.getLog(CmsXmlGroupContainer.class);
104
105    /** The group container objects. */
106    private Map<Locale, CmsGroupContainerBean> m_groupContainers;
107
108    /**
109     * Hides the public constructor.<p>
110     */
111    protected CmsXmlGroupContainer() {
112
113        // do nothing
114    }
115
116    /**
117     * Creates a new group container based on the provided XML document.<p>
118     *
119     * The given encoding is used when marshalling the XML again later.<p>
120     *
121     * @param cms the cms context, if <code>null</code> no link validation is performed
122     * @param document the document to create the container page from
123     * @param encoding the encoding of the container page
124     * @param resolver the XML entity resolver to use
125     */
126    protected CmsXmlGroupContainer(CmsObject cms, Document document, String encoding, EntityResolver resolver) {
127
128        // must set document first to be able to get the content definition
129        m_document = document;
130        // for the next line to work the document must already be available
131        m_contentDefinition = getContentDefinition(resolver);
132        // initialize the XML content structure
133        initDocument(cms, m_document, encoding, m_contentDefinition);
134    }
135
136    /**
137     * Create a new group container based on the given default content,
138     * that will have all language nodes of the default content and ensures the presence of the given locale.<p>
139     *
140     * The given encoding is used when marshalling the XML again later.<p>
141     *
142     * @param cms the current users OpenCms content
143     * @param locale the locale to generate the default content for
144     * @param modelUri the absolute path to the container page file acting as model
145     *
146     * @throws CmsException in case the model file is not found or not valid
147     */
148    protected CmsXmlGroupContainer(CmsObject cms, Locale locale, String modelUri)
149    throws CmsException {
150
151        // init model from given modelUri
152        CmsFile modelFile = cms.readFile(modelUri, CmsResourceFilter.ONLY_VISIBLE_NO_DELETED);
153        CmsXmlGroupContainer model = CmsXmlGroupContainerFactory.unmarshal(cms, modelFile);
154
155        // initialize macro resolver to use on model file values
156        CmsMacroResolver macroResolver = CmsMacroResolver.newInstance().setCmsObject(cms);
157
158        // content definition must be set here since it's used during document creation
159        m_contentDefinition = model.getContentDefinition();
160        // get the document from the default content
161        Document document = (Document)model.m_document.clone();
162        // initialize the XML content structure
163        initDocument(cms, document, model.getEncoding(), m_contentDefinition);
164        // resolve eventual macros in the nodes
165        visitAllValuesWith(new CmsXmlContentMacroVisitor(cms, macroResolver));
166        if (!hasLocale(locale)) {
167            // required locale not present, add it
168            try {
169                addLocale(cms, locale);
170            } catch (CmsXmlException e) {
171                // this can not happen since the locale does not exist
172            }
173        }
174    }
175
176    /**
177     * Create a new container page based on the given content definition,
178     * that will have one language node for the given locale all initialized with default values.<p>
179     *
180     * The given encoding is used when marshalling the XML again later.<p>
181     *
182     * @param cms the current users OpenCms content
183     * @param locale the locale to generate the default content for
184     * @param encoding the encoding to use when marshalling the container page later
185     * @param contentDefinition the content definition to create the content for
186     */
187    protected CmsXmlGroupContainer(
188        CmsObject cms,
189        Locale locale,
190        String encoding,
191        CmsXmlContentDefinition contentDefinition) {
192
193        // content definition must be set here since it's used during document creation
194        m_contentDefinition = contentDefinition;
195        // create the XML document according to the content definition
196        Document document = m_contentDefinition.createDocument(cms, this, locale);
197        // initialize the XML content structure
198        initDocument(cms, document, encoding, m_contentDefinition);
199    }
200
201    /**
202     * Removes all locales from the element group XML.<p>
203     *
204     * @throws CmsXmlException if something goes wrong
205     */
206    public void clearLocales() throws CmsXmlException {
207
208        for (Locale locale : getLocales()) {
209            removeLocale(locale);
210        }
211    }
212
213    /**
214     * Returns the group container bean for the given locale.<p>
215     *
216     * @param cms the cms context
217     *
218     * @return the group container bean
219     */
220    public CmsGroupContainerBean getGroupContainer(CmsObject cms) {
221
222        if (m_groupContainers.containsKey(CmsLocaleManager.MASTER_LOCALE)) {
223            return m_groupContainers.get(CmsLocaleManager.MASTER_LOCALE);
224        } else if (!m_groupContainers.isEmpty()) {
225            return m_groupContainers.get(m_groupContainers.keySet().iterator().next());
226        } else {
227            return null;
228        }
229    }
230
231    /**
232     * @see org.opencms.xml.content.CmsXmlContent#isAutoCorrectionEnabled()
233     */
234    @Override
235    public boolean isAutoCorrectionEnabled() {
236
237        return true;
238    }
239
240    /**
241     * Saves given container page in the current locale, and not only in memory but also to VFS.<p>
242     *
243     * @param cms the current cms context
244     * @param groupContainer the group-container page to save
245     * @param locale the locale to save
246     *
247     * @throws CmsException if something goes wrong
248     */
249    public void save(CmsObject cms, CmsGroupContainerBean groupContainer, Locale locale) throws CmsException {
250
251        CmsFile file = getFile();
252
253        // lock the file
254        cms.lockResourceTemporary(cms.getSitePath(file));
255
256        // wipe the locale
257        if (hasLocale(locale)) {
258            removeLocale(locale);
259        }
260
261        addLocale(cms, locale);
262
263        // add the nodes to the raw XML structure
264        Element parent = getLocaleNode(locale);
265        saveGroupContainer(cms, parent, groupContainer);
266
267        // generate bookmarks
268        initDocument(m_document, m_encoding, m_contentDefinition);
269
270        // write to VFS
271        file.setContents(marshal());
272        cms.writeFile(file);
273    }
274
275    /**
276     * Fills a {@link CmsXmlVfsFileValue} with the resource identified by the given id.<p>
277     *
278     * @param cms the current CMS context
279     * @param element the XML element to fill
280     * @param res the resource to use
281     */
282    protected void fillResource(CmsObject cms, Element element, CmsResource res) {
283
284        String xpath = element.getPath();
285        int pos = xpath.lastIndexOf("/" + XmlNode.GroupContainers.name() + "/");
286        if (pos > 0) {
287            xpath = xpath.substring(pos + 1);
288        }
289        CmsRelationType type = getHandler().getRelationType(xpath);
290        CmsXmlVfsFileValue.fillEntry(element, res.getStructureId(), res.getRootPath(), type);
291    }
292
293    /**
294     * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition)
295     */
296    @Override
297    protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) {
298
299        m_document = document;
300        m_contentDefinition = definition;
301        m_encoding = CmsEncoder.lookupEncoding(encoding, encoding);
302        m_elementLocales = new HashMap<String, Set<Locale>>();
303        m_elementNames = new HashMap<Locale, LinkedHashSet<String>>();
304        m_locales = new HashSet<Locale>();
305        m_groupContainers = new HashMap<Locale, CmsGroupContainerBean>();
306        clearBookmarks();
307
308        // initialize the bookmarks
309        for (Iterator<Element> itGroupContainers = CmsXmlGenericWrapper.elementIterator(
310            m_document.getRootElement()); itGroupContainers.hasNext();) {
311            Element cntPage = itGroupContainers.next();
312
313            try {
314                Locale locale = CmsLocaleManager.getLocale(
315                    cntPage.attribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE).getValue());
316
317                addLocale(locale);
318                Element groupContainer = cntPage.element(XmlNode.GroupContainers.name());
319
320                // container itself
321                int cntIndex = CmsXmlUtils.getXpathIndexInt(groupContainer.getUniquePath(cntPage));
322                String cntPath = CmsXmlUtils.createXpathElement(groupContainer.getName(), cntIndex);
323                I_CmsXmlSchemaType cntSchemaType = definition.getSchemaType(groupContainer.getName());
324                I_CmsXmlContentValue cntValue = cntSchemaType.createValue(this, groupContainer, locale);
325                addBookmark(cntPath, locale, true, cntValue);
326                CmsXmlContentDefinition cntDef = ((CmsXmlNestedContentDefinition)cntSchemaType).getNestedContentDefinition();
327
328                //title
329                Element title = groupContainer.element(XmlNode.Title.name());
330                addBookmarkForElement(title, locale, groupContainer, cntPath, cntDef);
331
332                //description
333                Element description = groupContainer.element(XmlNode.Description.name());
334                addBookmarkForElement(description, locale, groupContainer, cntPath, cntDef);
335
336                // types
337                Set<String> types = new HashSet<String>();
338                for (Iterator<Element> itTypes = CmsXmlGenericWrapper.elementIterator(
339                    groupContainer,
340                    XmlNode.Type.name()); itTypes.hasNext();) {
341                    Element type = itTypes.next();
342                    addBookmarkForElement(type, locale, groupContainer, cntPath, cntDef);
343                    String typeName = type.getTextTrim();
344                    if (!CmsStringUtil.isEmptyOrWhitespaceOnly(typeName)) {
345                        types.addAll(CmsContainer.splitType(typeName));
346                    }
347                }
348
349                List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
350                // Elements
351                for (Element element : CmsXmlGenericWrapper.elementIterable(groupContainer, XmlNode.Element.name())) {
352                    // element itself
353                    int elemIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(groupContainer));
354                    String elemPath = CmsXmlUtils.concatXpath(
355                        cntPath,
356                        CmsXmlUtils.createXpathElement(element.getName(), elemIndex));
357                    I_CmsXmlSchemaType elemSchemaType = cntDef.getSchemaType(element.getName());
358                    I_CmsXmlContentValue elemValue = elemSchemaType.createValue(this, element, locale);
359                    addBookmark(elemPath, locale, true, elemValue);
360                    CmsXmlContentDefinition elemDef = ((CmsXmlNestedContentDefinition)elemSchemaType).getNestedContentDefinition();
361
362                    // uri
363                    Element uri = element.element(XmlNode.Uri.name());
364                    addBookmarkForElement(uri, locale, element, elemPath, elemDef);
365                    Element uriLink = uri.element(CmsXmlPage.NODE_LINK);
366                    CmsUUID elementId = null;
367                    if (uriLink == null) {
368                        // this can happen when adding the elements node to the xml content
369                        // it is not dangerous since the link has to be set before saving
370                    } else {
371                        elementId = new CmsLink(uriLink).getStructureId();
372                    }
373
374                    //                    Element createNewElement = element.element(CmsXmlContainerPage.XmlNode.CreateNew.name());
375                    //                    boolean createNew = (createNewElement != null)
376                    //                        && Boolean.parseBoolean(createNewElement.getStringValue());
377
378                    // propeties
379                    Map<String, String> propertiesMap = CmsXmlContentPropertyHelper.readProperties(
380                        this,
381                        locale,
382                        element,
383                        elemPath,
384                        elemDef);
385
386                    if (elementId != null) {
387                        elements.add(new CmsContainerElementBean(elementId, null, propertiesMap, false));
388                    }
389                }
390                m_groupContainers.put(
391                    locale,
392                    new CmsGroupContainerBean(title.getText(), description.getText(), elements, types));
393            } catch (NullPointerException e) {
394                LOG.error(
395                    org.opencms.xml.content.Messages.get().getBundle().key(
396                        org.opencms.xml.content.Messages.LOG_XMLCONTENT_INIT_BOOKMARKS_0),
397                    e);
398            }
399        }
400    }
401
402    /**
403     * Adds the given container page to the given element.<p>
404     *
405     * @param cms the current CMS object
406     * @param parent the element to add it
407     * @param groupContainer the container page to add
408     *
409     * @throws CmsException if something goes wrong
410     */
411    protected void saveGroupContainer(CmsObject cms, Element parent, CmsGroupContainerBean groupContainer)
412    throws CmsException {
413
414        parent.clearContent();
415        Element groupContainerElem = parent.addElement(XmlNode.GroupContainers.name());
416
417        groupContainerElem.addElement(XmlNode.Title.name()).addCDATA(groupContainer.getTitle());
418        groupContainerElem.addElement(XmlNode.Description.name()).addCDATA(groupContainer.getDescription());
419
420        for (String type : groupContainer.getTypes()) {
421            groupContainerElem.addElement(XmlNode.Type.name()).addCDATA(type);
422        }
423
424        // the elements
425        for (CmsContainerElementBean element : groupContainer.getElements()) {
426            CmsResource res = cms.readResource(element.getId(), CmsResourceFilter.IGNORE_EXPIRATION);
427            if (OpenCms.getResourceManager().getResourceType(res.getTypeId()).getTypeName().equals(
428                CmsResourceTypeXmlContainerPage.GROUP_CONTAINER_TYPE_NAME)) {
429                LOG.warn(Messages.get().container(Messages.LOG_WARN_ELEMENT_GROUP_INSIDE_ELEMENT_GROUP_0));
430                continue;
431            }
432            Element elemElement = groupContainerElem.addElement(XmlNode.Element.name());
433
434            // the element
435            Element uriElem = elemElement.addElement(XmlNode.Uri.name());
436            fillResource(cms, uriElem, res);
437
438            // the properties
439            Map<String, String> properties = element.getIndividualSettings();
440            Map<String, CmsXmlContentProperty> propertiesConf = OpenCms.getADEManager().getElementSettings(cms, res);
441
442            CmsXmlContentPropertyHelper.saveProperties(cms, elemElement, properties, propertiesConf, true);
443        }
444    }
445
446    /**
447     * @see org.opencms.xml.content.CmsXmlContent#setFile(org.opencms.file.CmsFile)
448     */
449    @Override
450    protected void setFile(CmsFile file) {
451
452        // just for visibility from the factory
453        super.setFile(file);
454    }
455}