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.containerpage;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.ade.configuration.CmsFormatterUtils;
032import org.opencms.ade.containerpage.CmsContainerpageService;
033import org.opencms.ade.containerpage.CmsModelGroupHelper;
034import org.opencms.ade.containerpage.CmsSettingTranslator;
035import org.opencms.ade.containerpage.shared.CmsContainerElement;
036import org.opencms.ade.containerpage.shared.CmsFormatterConfig;
037import org.opencms.file.CmsFile;
038import org.opencms.file.CmsObject;
039import org.opencms.file.CmsResource;
040import org.opencms.file.CmsResourceFilter;
041import org.opencms.i18n.CmsEncoder;
042import org.opencms.i18n.CmsLocaleManager;
043import org.opencms.main.CmsException;
044import org.opencms.main.CmsLog;
045import org.opencms.main.OpenCms;
046import org.opencms.relations.CmsLink;
047import org.opencms.relations.CmsRelationType;
048import org.opencms.util.CmsMacroResolver;
049import org.opencms.util.CmsUUID;
050import org.opencms.xml.CmsXmlContentDefinition;
051import org.opencms.xml.CmsXmlException;
052import org.opencms.xml.CmsXmlGenericWrapper;
053import org.opencms.xml.CmsXmlUtils;
054import org.opencms.xml.content.CmsXmlContent;
055import org.opencms.xml.content.CmsXmlContentMacroVisitor;
056import org.opencms.xml.content.CmsXmlContentProperty;
057import org.opencms.xml.content.CmsXmlContentPropertyHelper;
058import org.opencms.xml.page.CmsXmlPage;
059import org.opencms.xml.types.CmsXmlNestedContentDefinition;
060import org.opencms.xml.types.CmsXmlVfsFileValue;
061import org.opencms.xml.types.I_CmsXmlContentValue;
062import org.opencms.xml.types.I_CmsXmlSchemaType;
063
064import java.util.ArrayList;
065import java.util.Arrays;
066import java.util.Collections;
067import java.util.HashMap;
068import java.util.HashSet;
069import java.util.Iterator;
070import java.util.LinkedHashMap;
071import java.util.List;
072import java.util.Locale;
073import java.util.Map;
074import java.util.Set;
075import java.util.function.Function;
076
077import org.apache.commons.logging.Log;
078
079import org.dom4j.Document;
080import org.dom4j.Element;
081import org.xml.sax.EntityResolver;
082
083import com.google.common.collect.ArrayListMultimap;
084import com.google.common.collect.ComparisonChain;
085import com.google.common.collect.Multimap;
086import com.google.common.collect.Ordering;
087
088/**
089 * Implementation of a object used to access and manage the xml data of a container page.<p>
090 *
091 * In addition to the XML content interface. It also provides access to more comfortable beans.
092 *
093 * @since 7.5.2
094 *
095 * @see #getContainerPage(CmsObject)
096 */
097public class CmsXmlContainerPage extends CmsXmlContent {
098
099    /** XML node name constants. */
100    public enum XmlNode {
101
102        /** Container attribute node name. */
103        Attribute,
104        /** Main node name. */
105        Containers,
106        /** The create new element node name. */
107        CreateNew,
108
109        /** Element instance id node name. */
110        ElementInstanceId,
111        /** Container elements node name. */
112        Elements,
113        /** Element formatter node name. */
114        Formatter,
115
116        /** Formatter key node name.*/
117        FormatterKey,
118        /** The is root container node name. */
119        IsRootContainer,
120        /** Container attribute key node name. */
121        Key,
122        /** Container name node name. */
123        Name,
124        /** Parent element instance id node name. */
125        ParentInstanceId,
126        /** Container type node name. */
127        Type,
128        /** Element URI node name. */
129        Uri,
130        /** Container attribute value node name. */
131        Value;
132    }
133
134    /** Name for old internal setting names that are not used with the SYSTEM:: prefix in code. */
135    public static final Set<String> LEGACY_SYSTEM_SETTING_NAMES = Collections.unmodifiableSet(
136        new HashSet<>(
137            Arrays.asList(
138                CmsContainerElement.USE_AS_COPY_MODEL,
139                CmsContainerElement.MODEL_GROUP_ID,
140                CmsContainerElement.MODEL_GROUP_STATE,
141                CmsContainerElement.USE_AS_COPY_MODEL,
142                CmsContainerpageService.SOURCE_CONTAINERPAGE_ID_SETTING)));
143
144    /** Prefix for system element settings. */
145    public static final String SYSTEM_SETTING_PREFIX = "SYSTEM::";
146
147    /** The log object for this class. */
148    private static final Log LOG = CmsLog.getLog(CmsXmlContainerPage.class);
149
150    /** The container page objects. */
151    private Map<Locale, CmsContainerPageBean> m_cntPages;
152
153    /**
154     * Hides the public constructor.<p>
155     */
156    protected CmsXmlContainerPage() {
157
158        // noop
159    }
160
161    /**
162     * Creates a new container page based on the provided XML document.<p>
163     *
164     * The given encoding is used when marshalling the XML again later.<p>
165     *
166     * @param cms the cms context, if <code>null</code> no link validation is performed
167     * @param document the document to create the container page from
168     * @param encoding the encoding of the container page
169     * @param resolver the XML entity resolver to use
170     */
171    protected CmsXmlContainerPage(CmsObject cms, Document document, String encoding, EntityResolver resolver) {
172
173        // must set document first to be able to get the content definition
174        m_document = document;
175        // for the next line to work the document must already be available
176        m_contentDefinition = getContentDefinition(resolver);
177        // initialize the XML content structure
178        initDocument(cms, m_document, encoding, m_contentDefinition);
179    }
180
181    /**
182     * Create a new container page based on the given default content,
183     * that will have all language nodes of the default content and ensures the presence of the given locale.<p>
184     *
185     * The given encoding is used when marshalling the XML again later.<p>
186     *
187     * @param cms the current users OpenCms content
188     * @param locale the locale to generate the default content for
189     * @param modelUri the absolute path to the container page file acting as model
190     *
191     * @throws CmsException in case the model file is not found or not valid
192     */
193    protected CmsXmlContainerPage(CmsObject cms, Locale locale, String modelUri)
194    throws CmsException {
195
196        // init model from given modelUri
197        CmsFile modelFile = cms.readFile(modelUri, CmsResourceFilter.ONLY_VISIBLE_NO_DELETED);
198        CmsXmlContainerPage model = CmsXmlContainerPageFactory.unmarshal(cms, modelFile);
199
200        // initialize macro resolver to use on model file values
201        CmsMacroResolver macroResolver = CmsMacroResolver.newInstance().setCmsObject(cms);
202
203        // content definition must be set here since it's used during document creation
204        m_contentDefinition = model.getContentDefinition();
205        // get the document from the default content
206        Document document = (Document)model.m_document.clone();
207        // initialize the XML content structure
208        initDocument(cms, document, model.getEncoding(), m_contentDefinition);
209        // resolve eventual macros in the nodes
210        visitAllValuesWith(new CmsXmlContentMacroVisitor(cms, macroResolver));
211        if (!hasLocale(locale)) {
212            // required locale not present, add it
213            try {
214                addLocale(cms, locale);
215            } catch (CmsXmlException e) {
216                // this can not happen since the locale does not exist
217                LOG.error(e.getMessage(), e);
218            }
219        }
220    }
221
222    /**
223     * Create a new container page based on the given content definition,
224     * that will have one language node for the given locale all initialized with default values.<p>
225     *
226     * The given encoding is used when marshalling the XML again later.<p>
227     *
228     * @param cms the current users OpenCms content
229     * @param locale the locale to generate the default content for
230     * @param encoding the encoding to use when marshalling the container page later
231     * @param contentDefinition the content definition to create the content for
232     */
233    protected CmsXmlContainerPage(
234        CmsObject cms,
235        Locale locale,
236        String encoding,
237        CmsXmlContentDefinition contentDefinition) {
238
239        // content definition must be set here since it's used during document creation
240        m_contentDefinition = contentDefinition;
241        // create the XML document according to the content definition
242        Document document = m_contentDefinition.createDocument(cms, this, CmsLocaleManager.MASTER_LOCALE);
243        // initialize the XML content structure
244        initDocument(cms, document, encoding, m_contentDefinition);
245    }
246
247    /**
248     * Saves a container page bean to the in-memory XML structure and returns the changed content.<p>
249     *
250     * @param cms the current CMS context
251     * @param cntPage the container page bean
252     * @return the new content for the container page
253     * @throws CmsException if something goes wrong
254     */
255    public byte[] createContainerPageXml(CmsObject cms, CmsContainerPageBean cntPage) throws CmsException {
256
257        // make sure all links are validated
258        writeContainerPage(cms, cntPage);
259        checkLinkConcistency(cms);
260        return marshal();
261
262    }
263
264    /**
265     * Gets the container page content as a bean.<p>
266     *
267     * @param cms the current CMS context
268     * @return the bean containing the container page data
269     */
270    public CmsContainerPageBean getContainerPage(CmsObject cms) {
271
272        Locale masterLocale = CmsLocaleManager.MASTER_LOCALE;
273        Locale localeToLoad = null;
274        // always use master locale if possible, otherwise use the first locale.
275        // this is important for 'legacy' container pages which were created before container pages became locale independent
276        if (m_cntPages.containsKey(masterLocale)) {
277            localeToLoad = masterLocale;
278        } else if (!m_cntPages.isEmpty()) {
279            localeToLoad = m_cntPages.keySet().iterator().next();
280        }
281        if (localeToLoad == null) {
282            return null;
283        } else {
284            CmsContainerPageBean result = m_cntPages.get(localeToLoad);
285            return result;
286        }
287    }
288
289    /**
290     * Calls initDocument, but with a different CmsObject
291     *
292     * @param cms the CmsObject to use
293     */
294    public void initDocument(CmsObject cms) {
295
296        initDocument(cms, m_document, m_encoding, getContentDefinition());
297    }
298
299    /**
300     * @see org.opencms.xml.content.CmsXmlContent#isAutoCorrectionEnabled()
301     */
302    @Override
303    public boolean isAutoCorrectionEnabled() {
304
305        return true;
306    }
307
308    /**
309     * Saves given container page in the current locale, and not only in memory but also to VFS.<p>
310     *
311     * @param cms the current cms context
312     * @param cntPage the container page to save
313     *
314     * @throws CmsException if something goes wrong
315     */
316    public void save(CmsObject cms, CmsContainerPageBean cntPage) throws CmsException {
317
318        save(cms, cntPage, false);
319    }
320
321    /**
322     * Saves given container page in the current locale, and not only in memory but also to VFS.<p>
323     *
324     * @param cms the current cms context
325     * @param cntPage the container page to save
326     * @param ifChangedOnly <code>true</code> to only write the file if the content has changed
327     *
328     * @throws CmsException if something goes wrong
329     */
330    public void save(CmsObject cms, CmsContainerPageBean cntPage, boolean ifChangedOnly) throws CmsException {
331
332        CmsFile file = getFile();
333        byte[] data = createContainerPageXml(cms, cntPage);
334        if (ifChangedOnly && Arrays.equals(file.getContents(), data)) {
335            return;
336        }
337        // lock the file
338        cms.lockResourceTemporary(file);
339        file.setContents(data);
340        cms.writeFile(file);
341    }
342
343    /**
344     * Saves a container page in in-memory XML structure.<p>
345     *
346     * @param cms the current CMS context
347     * @param cntPage the container page bean to save
348     *
349     * @throws CmsException if something goes wrong
350     */
351    public void writeContainerPage(CmsObject cms, CmsContainerPageBean cntPage) throws CmsException {
352
353        // keep unused containers
354        CmsContainerPageBean savePage = cleanupContainersContainers(cms, cntPage);
355        savePage = removeEmptyContainers(cntPage);
356        // Replace existing locales with master locale
357        for (Locale locale : getLocales()) {
358            removeLocale(locale);
359        }
360        Locale masterLocale = CmsLocaleManager.MASTER_LOCALE;
361        addLocale(cms, masterLocale);
362
363        // add the nodes to the raw XML structure
364        Element parent = getLocaleNode(masterLocale);
365        saveContainerPage(cms, parent, savePage);
366        initDocument(m_document, m_encoding, m_contentDefinition);
367    }
368
369    /**
370     * Checks the link consistency for a given locale and reinitializes the document afterwards.<p>
371     *
372     * @param cms the cms context
373     */
374    protected void checkLinkConcistency(CmsObject cms) {
375
376        Locale masterLocale = CmsLocaleManager.MASTER_LOCALE;
377
378        for (I_CmsXmlContentValue contentValue : getValues(masterLocale)) {
379            if (contentValue instanceof CmsXmlVfsFileValue) {
380                CmsLink link = ((CmsXmlVfsFileValue)contentValue).getLink(cms);
381                link.checkConsistency(cms);
382            }
383        }
384        initDocument();
385    }
386
387    /**
388     * Removes all empty containers and merges the containers of the current document that are not used in the given container page with it.<p>
389     *
390     * @param cms the current CMS context
391     * @param cntPage the container page to merge
392     *
393     * @return a new container page with the additional unused containers
394     */
395    protected CmsContainerPageBean cleanupContainersContainers(CmsObject cms, CmsContainerPageBean cntPage) {
396
397        CmsADEConfigData config = OpenCms.getADEManager().lookupConfiguration(cms, getFile().getRootPath());
398        // get the used containers first
399        Map<String, CmsContainerBean> currentContainers = cntPage.getContainers();
400        List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>();
401        for (String cntName : cntPage.getNames()) {
402            CmsContainerBean container = currentContainers.get(cntName);
403            if (!container.getElements().isEmpty()) {
404                containers.add(container);
405            }
406        }
407
408        // now get the unused containers
409        CmsContainerPageBean currentContainerPage = getContainerPage(cms);
410        if (currentContainerPage != null) {
411            for (String cntName : currentContainerPage.getNames()) {
412                if (!currentContainers.containsKey(cntName)) {
413                    CmsContainerBean container = currentContainerPage.getContainers().get(cntName);
414                    if (!container.getElements().isEmpty()) {
415                        containers.add(container);
416                    }
417                }
418            }
419        }
420
421        // check if any nested containers have lost their parent element
422
423        // first collect all present elements
424        Map<String, CmsContainerElementBean> pageElements = new HashMap<String, CmsContainerElementBean>();
425        Map<String, String> parentContainers = new HashMap<String, String>();
426        for (CmsContainerBean container : containers) {
427            for (CmsContainerElementBean element : container.getElements()) {
428                try {
429                    element.initResource(cms);
430
431                    if (!CmsModelGroupHelper.isModelGroupResource(element.getResource())) {
432                        pageElements.put(element.getInstanceId(), element);
433                        parentContainers.put(element.getInstanceId(), container.getName());
434                    }
435                } catch (CmsException e) {
436                    LOG.warn(e.getLocalizedMessage(), e);
437                }
438            }
439        }
440        Iterator<CmsContainerBean> cntIt = containers.iterator();
441        while (cntIt.hasNext()) {
442            CmsContainerBean container = cntIt.next();
443            // check all unused nested containers if their parent element is still part of the page
444            if (!currentContainers.containsKey(container.getName())
445                && (container.isNestedContainer() && !container.isRootContainer())) {
446                boolean remove = !pageElements.containsKey(container.getParentInstanceId())
447                    || container.getElements().isEmpty();
448                if (!remove) {
449                    // check if the parent element formatter is set to strictly render all nested containers
450                    CmsContainerElementBean element = pageElements.get(container.getParentInstanceId());
451                    String settingsKey = CmsFormatterConfig.getSettingsKeyForContainer(
452                        parentContainers.get(element.getInstanceId()));
453                    String formatterId = element.getIndividualSettings().get(settingsKey);
454                    I_CmsFormatterBean bean = config.findFormatter(formatterId);
455                    if (bean != null) {
456                        remove = (bean instanceof CmsFormatterBean) && ((CmsFormatterBean)bean).isStrictContainers();
457                    }
458                }
459                if (remove) {
460                    // remove the sub elements from the page list
461                    for (CmsContainerElementBean element : container.getElements()) {
462                        pageElements.remove(element.getInstanceId());
463                    }
464                    // remove the container
465                    cntIt.remove();
466                }
467            }
468        }
469
470        return new CmsContainerPageBean(containers);
471    }
472
473    /**
474     * Fills a {@link CmsXmlVfsFileValue} with the resource identified by the given id.<p>
475     *
476     * @param cms the current CMS context
477     * @param element the XML element to fill
478     * @param resourceId the ID identifying the resource to use
479     *
480     * @return the resource
481     *
482     * @throws CmsException if the resource can not be read
483     */
484    protected CmsResource fillResource(CmsObject cms, Element element, CmsUUID resourceId) throws CmsException {
485
486        String xpath = element.getPath();
487        int pos = xpath.lastIndexOf("/" + XmlNode.Containers.name() + "/");
488        if (pos > 0) {
489            xpath = xpath.substring(pos + 1);
490        }
491        CmsRelationType type = getHandler().getRelationType(xpath);
492        CmsResource res = cms.readResource(resourceId, CmsResourceFilter.IGNORE_EXPIRATION);
493        CmsXmlVfsFileValue.fillEntry(element, res.getStructureId(), res.getRootPath(), type);
494        return res;
495    }
496
497    /**
498     * @see org.opencms.xml.content.CmsXmlContent#initDocument(org.opencms.file.CmsObject, org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition)
499     */
500    @Override
501    protected void initDocument(CmsObject cms, Document document, String encoding, CmsXmlContentDefinition definition) {
502
503        m_document = document;
504        m_contentDefinition = definition;
505        m_encoding = CmsEncoder.lookupEncoding(encoding, encoding);
506        m_elementLocales = new HashMap<String, Set<Locale>>();
507        m_elementNames = new HashMap<Locale, Set<String>>();
508        m_locales = new HashSet<Locale>();
509        m_cntPages = new LinkedHashMap<Locale, CmsContainerPageBean>();
510        clearBookmarks();
511        CmsADEConfigData config = null;
512        CmsSettingTranslator settingTranslator = null;
513        if ((getFile() != null) && (cms != null)) {
514            config = OpenCms.getADEManager().lookupConfiguration(cms, getFile().getRootPath());
515            settingTranslator = new CmsSettingTranslator(config);
516        }
517
518        // initialize the bookmarks
519        for (Iterator<Element> itCntPages = CmsXmlGenericWrapper.elementIterator(
520            m_document.getRootElement()); itCntPages.hasNext();) {
521            Element cntPage = itCntPages.next();
522
523            try {
524                Locale locale = CmsLocaleManager.getLocale(
525                    cntPage.attribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_LANGUAGE).getValue());
526
527                addLocale(locale);
528
529                List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>();
530                for (Iterator<Element> itCnts = CmsXmlGenericWrapper.elementIterator(
531                    cntPage,
532                    XmlNode.Containers.name()); itCnts.hasNext();) {
533                    Element container = itCnts.next();
534
535                    // container itself
536                    int cntIndex = CmsXmlUtils.getXpathIndexInt(container.getUniquePath(cntPage));
537                    String cntPath = CmsXmlUtils.createXpathElement(container.getName(), cntIndex);
538                    I_CmsXmlSchemaType cntSchemaType = definition.getSchemaType(container.getName());
539                    I_CmsXmlContentValue cntValue = cntSchemaType.createValue(this, container, locale);
540                    addBookmark(cntPath, locale, true, cntValue);
541                    CmsXmlContentDefinition cntDef = ((CmsXmlNestedContentDefinition)cntSchemaType).getNestedContentDefinition();
542
543                    // name
544                    Element name = container.element(XmlNode.Name.name());
545                    String containerName = name.getText();
546                    addBookmarkForElement(name, locale, container, cntPath, cntDef);
547
548                    // type
549                    Element type = container.element(XmlNode.Type.name());
550                    addBookmarkForElement(type, locale, container, cntPath, cntDef);
551
552                    // parent instance id
553                    Element parentInstance = container.element(XmlNode.ParentInstanceId.name());
554                    if (parentInstance != null) {
555                        addBookmarkForElement(parentInstance, locale, container, cntPath, cntDef);
556                    }
557
558                    Element isRootContainer = container.element(XmlNode.IsRootContainer.name());
559                    if (isRootContainer != null) {
560                        addBookmarkForElement(isRootContainer, locale, container, cntPath, cntDef);
561                    }
562
563                    List<CmsContainerElementBean> elements = new ArrayList<CmsContainerElementBean>();
564                    // Elements
565                    for (Iterator<Element> itElems = CmsXmlGenericWrapper.elementIterator(
566                        container,
567                        XmlNode.Elements.name()); itElems.hasNext();) {
568                        Element element = itElems.next();
569
570                        // element itself
571                        int elemIndex = CmsXmlUtils.getXpathIndexInt(element.getUniquePath(container));
572                        String elemPath = CmsXmlUtils.concatXpath(
573                            cntPath,
574                            CmsXmlUtils.createXpathElement(element.getName(), elemIndex));
575                        I_CmsXmlSchemaType elemSchemaType = cntDef.getSchemaType(element.getName());
576                        I_CmsXmlContentValue elemValue = elemSchemaType.createValue(this, element, locale);
577                        addBookmark(elemPath, locale, true, elemValue);
578                        CmsXmlContentDefinition elemDef = ((CmsXmlNestedContentDefinition)elemSchemaType).getNestedContentDefinition();
579
580                        Element instanceIdElem = element.element(XmlNode.ElementInstanceId.name());
581                        String elementInstanceId = null;
582                        if (instanceIdElem != null) {
583                            elementInstanceId = instanceIdElem.getTextTrim();
584                        }
585
586                        Element formatterKeyElem = element.element(XmlNode.FormatterKey.name());
587                        String formatterKey = null;
588                        if (formatterKeyElem != null) {
589                            formatterKey = formatterKeyElem.getTextTrim();
590                        }
591
592                        // uri
593                        Element uri = element.element(XmlNode.Uri.name());
594                        CmsUUID elementId = null;
595                        if (uri != null) {
596                            addBookmarkForElement(uri, locale, element, elemPath, elemDef);
597                            Element uriLink = uri.element(CmsXmlPage.NODE_LINK);
598                            if (uriLink == null) {
599                                // this can happen when adding the elements node to the xml content
600                                // it is not dangerous since the link has to be set before saving
601                            } else {
602                                CmsLink link = new CmsLink(uriLink);
603                                if (cms != null) {
604                                    link.checkConsistency(cms);
605                                }
606                                elementId = link.getStructureId();
607                            }
608                        }
609                        // uri may be null for dynamic functions, try find the element id from the settings later
610
611                        Element createNewElement = element.element(XmlNode.CreateNew.name());
612                        boolean createNew = (createNewElement != null)
613                            && Boolean.parseBoolean(createNewElement.getStringValue());
614
615                        // formatter
616                        Element formatter = element.element(XmlNode.Formatter.name());
617                        CmsUUID formatterId = null;
618                        if (formatter != null) {
619                            addBookmarkForElement(formatter, locale, element, elemPath, elemDef);
620                            Element formatterLink = formatter.element(CmsXmlPage.NODE_LINK);
621
622                            if (formatterLink == null) {
623                                // this can happen when adding the elements node to the xml content
624                                // it is not dangerous since the link has to be set before saving
625                            } else {
626                                CmsLink link = new CmsLink(formatterLink);
627                                if (cms != null) {
628                                    link.checkConsistency(cms);
629                                }
630                                formatterId = link.getStructureId();
631                            }
632                        }
633
634                        // the properties
635                        Map<String, String> propertiesMap = CmsXmlContentPropertyHelper.readProperties(
636                            this,
637                            locale,
638                            element,
639                            elemPath,
640                            elemDef);
641                        propertiesMap = translateMapKeys(propertiesMap, this::translateSettingNameForLoad);
642                        if ((config != null) && (getFile() != null)) {
643                            propertiesMap = fixNestedFormatterSettings(cms, config, propertiesMap);
644                        }
645                        if (formatterKey != null) {
646                            propertiesMap.put(CmsFormatterConfig.FORMATTER_SETTINGS_KEY + containerName, formatterKey);
647                        }
648
649                        I_CmsFormatterBean dynamicFormatter = null;
650                        if (config != null) {
651                            // make sure alias keys are replaced with main keys in the settings
652                            String key1 = CmsFormatterConfig.FORMATTER_SETTINGS_KEY + containerName;
653                            String key2 = CmsFormatterConfig.FORMATTER_SETTINGS_KEY;
654                            for (String key : new String[] {key1, key2}) {
655                                String value = propertiesMap.get(key);
656                                if (value != null) {
657                                    I_CmsFormatterBean temp = config.findFormatter(value);
658                                    if (temp != null) {
659                                        dynamicFormatter = temp;
660                                        propertiesMap.put(key, dynamicFormatter.getKeyOrId());
661                                        break;
662                                    }
663                                }
664                            }
665                        }
666                        if ((config != null) && (dynamicFormatter != null) && (settingTranslator != null)) {
667                            propertiesMap = settingTranslator.translateSettings(dynamicFormatter, propertiesMap);
668                        }
669
670                        if (elementInstanceId != null) {
671                            propertiesMap.put(CmsContainerElement.ELEMENT_INSTANCE_ID, elementInstanceId);
672                        }
673
674                        CmsUUID pageId;
675                        if (getFile() != null) {
676                            pageId = getFile().getStructureId();
677                        } else {
678                            pageId = CmsUUID.getNullUUID();
679                        }
680                        propertiesMap.put(CmsContainerElement.SETTING_PAGE_ID, "" + pageId);
681
682                        boolean createNewFromSetting = Boolean.parseBoolean(
683                            propertiesMap.remove(CmsContainerElement.SETTING_CREATE_NEW));
684                        createNew |= createNewFromSetting;
685
686                        if (config != null) {
687                            // in the new container page format, new dynamic functions are not stored with their URIs in the page
688                            String key = CmsFormatterUtils.getFormatterKey(containerName, propertiesMap);
689                            I_CmsFormatterBean maybeFunction = config.findFormatter(key);
690                            if (maybeFunction instanceof CmsFunctionFormatterBean) {
691                                elementId = new CmsUUID(maybeFunction.getId());
692                            }
693                        }
694
695                        if (elementId != null) {
696                            elements.add(new CmsContainerElementBean(elementId, formatterId, propertiesMap, createNew));
697                        }
698                    }
699                    CmsContainerBean newContainerBean = new CmsContainerBean(
700                        name.getText(),
701                        type.getText(),
702                        parentInstance != null ? parentInstance.getText() : null,
703                        (isRootContainer != null) && Boolean.valueOf(isRootContainer.getText()).booleanValue(),
704                        elements);
705                    containers.add(newContainerBean);
706                }
707
708                m_cntPages.put(locale, new CmsContainerPageBean(containers));
709            } catch (NullPointerException e) {
710                LOG.error(
711                    org.opencms.xml.content.Messages.get().getBundle().key(
712                        org.opencms.xml.content.Messages.LOG_XMLCONTENT_INIT_BOOKMARKS_0),
713                    e);
714            }
715        }
716
717        if (cms != null) {
718            // this will remove all invalid links
719            getHandler().invalidateBrokenLinks(cms, this);
720        }
721    }
722
723    /**
724     * @see org.opencms.xml.A_CmsXmlDocument#initDocument(org.dom4j.Document, java.lang.String, org.opencms.xml.CmsXmlContentDefinition)
725     */
726    @Override
727    protected void initDocument(Document document, String encoding, CmsXmlContentDefinition definition) {
728
729        initDocument(null, document, encoding, definition);
730    }
731
732    /**
733     * Removes all empty containers to clean up container page XML.<p>
734     *
735     * @param cntPage the container page bean
736     *
737     * @return the newly generated result
738     */
739    protected CmsContainerPageBean removeEmptyContainers(CmsContainerPageBean cntPage) {
740
741        List<CmsContainerBean> containers = new ArrayList<CmsContainerBean>();
742        for (CmsContainerBean container : cntPage.getContainers().values()) {
743            if (container.getElements().size() > 0) {
744                containers.add(container);
745            }
746        }
747        return new CmsContainerPageBean(containers);
748    }
749
750    /**
751     * Adds the given container page to the given element.<p>
752     *
753     * @param cms the current CMS object
754     * @param parent the element to add it
755     * @param cntPage the container page to add
756     *
757     * @throws CmsException if something goes wrong
758     */
759    protected void saveContainerPage(CmsObject cms, Element parent, CmsContainerPageBean cntPage) throws CmsException {
760
761        parent.clearContent();
762
763        CmsADEConfigData adeConfig = OpenCms.getADEManager().lookupConfiguration(cms, getFile().getRootPath());
764        if (adeConfig.isUseFormatterKeys()) {
765            saveContainerPageV2(cms, parent, cntPage, adeConfig);
766        } else {
767            saveContainerPageV1(cms, parent, cntPage, adeConfig);
768        }
769    }
770
771    /**
772     * @see org.opencms.xml.content.CmsXmlContent#setFile(org.opencms.file.CmsFile)
773     */
774    @Override
775    protected void setFile(CmsFile file) {
776
777        // just for visibility from the factory
778        super.setFile(file);
779    }
780
781    /**
782     * Replaces formatter id prefixes for nested settings with corresponding formatter keys, if possible.<p>
783     *
784     * Also handles replacement of alias keys with main keys in nested settings.
785     *
786     * @param cms the CMS Context
787     * @param config the sitemap configuration
788      *@param propertiesMap the map of setting s
789     * @return the modified settings
790     */
791    private Map<String, String> fixNestedFormatterSettings(
792        CmsObject cms,
793        CmsADEConfigData config,
794        Map<String, String> propertiesMap) {
795
796        Map<String, String> result = new HashMap<>();
797        for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
798            String key = entry.getKey();
799
800            // replace structure ids, fallback keys or alias keys with the main key if possible
801
802            int underscorePos = key.indexOf("_");
803            if (underscorePos >= 0) {
804                String prefix = key.substring(0, underscorePos);
805                I_CmsFormatterBean formatter = config.findFormatter(prefix, /* noWarn = */true);
806                if (formatter != null) {
807                    key = formatter.getKeyOrId() + key.substring(underscorePos);
808                }
809            }
810
811            result.put(key, entry.getValue());
812        }
813        return result;
814    }
815
816    /**
817     * Do some processing for the element settings before saving them.
818     *
819     * @param config the ADE configuration
820     * @param settings the element settings
821     * @return the modified element settings
822     */
823    private Map<String, String> processSettingsForSaveV1(CmsADEConfigData config, Map<String, String> settings) {
824
825        Map<String, String> result = new LinkedHashMap<>();
826        for (Map.Entry<String, String> entry : settings.entrySet()) {
827            String key = entry.getKey();
828            String value = entry.getValue();
829            if (key.startsWith(CmsFormatterConfig.FORMATTER_SETTINGS_KEY)) {
830                if (!CmsUUID.isValidUUID(value)) {
831                    I_CmsFormatterBean dynamicFmt = config.findFormatter(value);
832                    if ((dynamicFmt != null) && (dynamicFmt.getId() != null)) {
833                        value = dynamicFmt.getId();
834                    }
835                }
836            } else {
837                // nested formatters
838                int underscorePos = key.indexOf("_");
839                if (underscorePos != -1) {
840                    String partBeforeUnderscore = key.substring(0, underscorePos);
841                    String partAfterUnderscore = key.substring(underscorePos + 1);
842                    I_CmsFormatterBean dynamicFmt = config.findFormatter(partBeforeUnderscore);
843                    if ((dynamicFmt != null) && dynamicFmt.getSettings(config).containsKey(partAfterUnderscore)) {
844                        String id = dynamicFmt.getId();
845                        if (id != null) {
846                            key = id + "_" + partAfterUnderscore;
847                        }
848                    }
849                }
850            }
851            result.put(key, value);
852        }
853        result.remove(CmsContainerElement.SETTING_PAGE_ID);
854        return result;
855    }
856
857    /**
858     * Do some processing for the element settings before saving them.
859     *
860     * @param config the ADE configuration
861     * @param settings the element settings
862     * @return the modified element settings
863     */
864    private Map<String, String> processSettingsForSaveV2(CmsADEConfigData config, Map<String, String> settings) {
865
866        Map<String, String> result = new LinkedHashMap<>();
867
868        for (Map.Entry<String, String> entry : settings.entrySet()) {
869            String key = entry.getKey();
870            String value = entry.getValue();
871            if (key.startsWith(CmsFormatterConfig.FORMATTER_SETTINGS_KEY)) {
872                if (CmsUUID.isValidUUID(value)) {
873                    I_CmsFormatterBean dynamicFmt = config.findFormatter(value);
874                    if ((dynamicFmt != null) && (dynamicFmt.getKey() != null)) {
875                        value = dynamicFmt.getKey();
876                    }
877                }
878            }
879            result.put(key, value);
880        }
881        result.remove(CmsContainerElement.SETTING_PAGE_ID);
882        result = sortSettingsForSave(translateMapKeys(result, this::translateSettingNameForSave));
883        return result;
884    }
885
886    /**
887     * Adds the given container page to the given element.<p>
888     *
889     * @param cms the current CMS object
890     * @param parent the element to add it
891     * @param cntPage the container page to add
892     * @param adeConfig the current sitemap configuration
893     *
894     * @throws CmsException if something goes wrong
895     */
896    private void saveContainerPageV1(
897        CmsObject cms,
898        Element parent,
899        CmsContainerPageBean cntPage,
900        CmsADEConfigData adeConfig)
901    throws CmsException {
902
903        // save containers in a defined order
904        List<String> containerNames = new ArrayList<String>(cntPage.getNames());
905        Collections.sort(containerNames);
906
907        for (String containerName : containerNames) {
908            CmsContainerBean container = cntPage.getContainers().get(containerName);
909
910            // the container
911            Element cntElement = parent.addElement(XmlNode.Containers.name());
912            cntElement.addElement(XmlNode.Name.name()).addCDATA(container.getName());
913            cntElement.addElement(XmlNode.Type.name()).addCDATA(container.getType());
914            if (container.isNestedContainer()) {
915                cntElement.addElement(XmlNode.ParentInstanceId.name()).addCDATA(container.getParentInstanceId());
916            }
917            if (container.isRootContainer()) {
918                cntElement.addElement(XmlNode.IsRootContainer.name()).addText(Boolean.TRUE.toString());
919            }
920
921            // the elements
922            for (CmsContainerElementBean element : container.getElements()) {
923                Element elemElement = cntElement.addElement(XmlNode.Elements.name());
924
925                // the element
926                Element uriElem = elemElement.addElement(XmlNode.Uri.name());
927                CmsResource uriRes = fillResource(cms, uriElem, element.getId());
928                if (element.getFormatterId() != null) {
929                    Element formatterElem = elemElement.addElement(XmlNode.Formatter.name());
930                    fillResource(cms, formatterElem, element.getFormatterId());
931                }
932                if (element.isCreateNew()) {
933                    Element createNewElem = elemElement.addElement(XmlNode.CreateNew.name());
934                    createNewElem.addText(Boolean.TRUE.toString());
935                }
936                // the properties
937                Map<String, String> properties = element.getIndividualSettings();
938                Map<String, String> processedSettings = processSettingsForSaveV1(adeConfig, properties);
939                Map<String, CmsXmlContentProperty> propertiesConf = OpenCms.getADEManager().getElementSettings(
940                    cms,
941                    uriRes);
942
943                CmsXmlContentPropertyHelper.saveProperties(cms, elemElement, processedSettings, propertiesConf, true);
944            }
945        }
946    }
947
948    /**
949     * Adds the given container page to the given element.<p>
950     *
951     * @param cms the current CMS object
952     * @param parent the element to add it
953     * @param cntPage the container page to add
954     * @param adeConfig the current sitemap configuration
955     *
956     * @throws CmsException if something goes wrong
957     */
958    private void saveContainerPageV2(
959        CmsObject cms,
960        Element parent,
961        CmsContainerPageBean cntPage,
962        CmsADEConfigData adeConfig)
963    throws CmsException {
964
965        // save containers in a defined order
966        List<String> containerNames = sortContainerNames(cntPage);
967
968        for (String containerName : containerNames) {
969            CmsContainerBean container = cntPage.getContainers().get(containerName);
970
971            // the container
972            Element cntElement = parent.addElement(XmlNode.Containers.name());
973            cntElement.addElement(XmlNode.Name.name()).addCDATA(container.getName());
974            cntElement.addElement(XmlNode.Type.name()).addCDATA(container.getType());
975            if (container.isNestedContainer()) {
976                cntElement.addElement(XmlNode.ParentInstanceId.name()).addCDATA(container.getParentInstanceId());
977            }
978            if (container.isRootContainer()) {
979                cntElement.addElement(XmlNode.IsRootContainer.name()).addText(Boolean.TRUE.toString());
980            }
981
982            // the elements
983            for (CmsContainerElementBean element : container.getElements()) {
984                Element elemElement = cntElement.addElement(XmlNode.Elements.name());
985
986                Map<String, String> properties = new HashMap<>(element.getIndividualSettings());
987
988                String instanceId = properties.remove(CmsContainerElement.ELEMENT_INSTANCE_ID);
989                if (instanceId != null) {
990                    Element instanceIdElem = elemElement.addElement(XmlNode.ElementInstanceId.name());
991                    instanceIdElem.addText(instanceId);
992                }
993
994                String formatterKey = CmsFormatterUtils.removeFormatterKey(containerName, properties);
995                I_CmsFormatterBean formatter = null;
996                if (formatterKey != null) {
997                    Element formatterKeyElem = elemElement.addElement(XmlNode.FormatterKey.name());
998
999                    formatter = adeConfig.findFormatter(formatterKey);
1000                    if ((formatter != null) && (formatter.getKeyOrId() != null)) {
1001                        formatterKey = formatter.getKeyOrId();
1002                    }
1003                    formatterKeyElem.addText(formatterKey);
1004                }
1005
1006                CmsResource elementRes;
1007                if (!(formatter instanceof CmsFunctionFormatterBean)) {
1008                    // the element
1009                    Element uriElem = elemElement.addElement(XmlNode.Uri.name());
1010                    elementRes = fillResource(cms, uriElem, element.getId());
1011                    if ((element.getFormatterId() != null) && (formatterKey == null)) {
1012                        Element formatterElem = elemElement.addElement(XmlNode.Formatter.name());
1013                        fillResource(cms, formatterElem, element.getFormatterId());
1014                    }
1015                } else {
1016                    elementRes = cms.readResource(element.getId(), CmsResourceFilter.IGNORE_EXPIRATION);
1017                }
1018                if (element.isCreateNew()) {
1019                    properties.put(CmsContainerElement.SETTING_CREATE_NEW, "true");
1020                }
1021                // the properties
1022
1023                Map<String, String> processedSettings = processSettingsForSaveV2(adeConfig, properties);
1024                Map<String, CmsXmlContentProperty> propertiesConf = OpenCms.getADEManager().getElementSettings(
1025                    cms,
1026                    elementRes);
1027                CmsXmlContentPropertyHelper.saveProperties(cms, elemElement, processedSettings, propertiesConf, false);
1028            }
1029        }
1030    }
1031
1032    /**
1033     * Computes a container sort ordering for saving the containers of a container page bean.<p>
1034     *
1035     * @param page the container page bean
1036     * @return the sorted list of container names
1037     */
1038    private List<String> sortContainerNames(CmsContainerPageBean page) {
1039
1040        Multimap<String, CmsContainerBean> containersByParentId = ArrayListMultimap.create();
1041        Map<String, CmsContainerElementBean> elementsById = new HashMap<>();
1042        List<CmsContainerBean> rootContainers = new ArrayList<>();
1043
1044        //  make table of container elements by instance id
1045
1046        for (CmsContainerBean container : page.getContainers().values()) {
1047            for (CmsContainerElementBean element : container.getElements()) {
1048                if (element.getInstanceId() != null) {
1049                    elementsById.put(element.getInstanceId(), element);
1050                }
1051            }
1052        }
1053
1054        // make table of containers by their parent instance id
1055
1056        for (CmsContainerBean container : page.getContainers().values()) {
1057            String parentInstanceId = container.getParentInstanceId();
1058            if (parentInstanceId != null) {
1059                containersByParentId.put(parentInstanceId, container);
1060            }
1061            if ((parentInstanceId == null) || !elementsById.containsKey(parentInstanceId)) {
1062                rootContainers.add(container);
1063            }
1064        }
1065
1066        // Visit all containers via depth-first traversal, using the previously constructed tables and a stack.
1067        // Record their names in the order they were encountered.
1068        // For children of the same container, they are ordered by name.
1069
1070        rootContainers.sort((a, b) -> b.getName().compareTo(a.getName())); // we put them on a stack, so the last element should be the smallest one
1071        ArrayList<CmsContainerBean> stack = new ArrayList<>();
1072        stack.addAll(rootContainers);
1073        Map<String, Integer> order = new HashMap<>();
1074        int counter = 0;
1075        while (stack.size() > 0) {
1076            CmsContainerBean container = stack.remove(stack.size() - 1);
1077
1078            // avoid already visited containers, in case there are cycles (possible in principle, if you change the container page manually)
1079            if (order.containsKey(container.getName())) {
1080                continue;
1081            }
1082            order.put(container.getName(), Integer.valueOf(counter));
1083            counter += 1;
1084
1085            for (CmsContainerElementBean element : container.getElements()) {
1086                String instanceId = element.getInstanceId();
1087                if (instanceId != null) {
1088                    List<CmsContainerBean> childContainers = new ArrayList<>(containersByParentId.get(instanceId));
1089                    childContainers.sort((a, b) -> b.getName().compareTo(a.getName()));
1090                    stack.addAll(childContainers);
1091                }
1092            }
1093        }
1094        List<String> result = new ArrayList<>(page.getContainers().keySet());
1095
1096        result.sort(
1097            (
1098                a,
1099                b) -> ComparisonChain.start().compare(
1100                    order.get(a),
1101                    order.get(b),
1102                    Ordering.natural().nullsLast()).compare(a, b).result());
1103        return result;
1104    }
1105
1106    /**
1107     * Sort element settings such that system settings come first and normal element settings after that, with each group alphabetically sorted.
1108     *
1109     * @param settings the map of settings
1110     * @return the sorted settings map
1111     */
1112    private LinkedHashMap<String, String> sortSettingsForSave(Map<String, String> settings) {
1113
1114        LinkedHashMap<String, String> result = new LinkedHashMap<>();
1115        List<String> keys = new ArrayList<>(settings.keySet());
1116        keys.sort(
1117            (
1118                a,
1119                b) -> ComparisonChain.start().compareTrueFirst(
1120                    a.startsWith(SYSTEM_SETTING_PREFIX),
1121                    b.startsWith(SYSTEM_SETTING_PREFIX)).compare(a, b).result());
1122        for (String key : keys) {
1123            result.put(key, settings.get(key));
1124        }
1125        return result;
1126    }
1127
1128    /**
1129     * Converts a string map to a new map by applying a translation function to the map keys.
1130     *
1131     * @param settings the original map
1132     * @param translation the translation function
1133     * @return the new map with the translated keys
1134     */
1135    private Map<String, String> translateMapKeys(Map<String, String> settings, Function<String, String> translation) {
1136
1137        LinkedHashMap<String, String> result = new LinkedHashMap<>();
1138        settings.entrySet().forEach(e -> result.put(translation.apply(e.getKey()), e.getValue()));
1139        return result;
1140
1141    }
1142
1143    /**
1144     * Translates new SYSTEM:: prefixed names for legacy system element settings to their non-prefixed form.
1145     *
1146     * @param name  the setting name
1147     * @return the translated setting name
1148     */
1149    private String translateSettingNameForLoad(String name) {
1150
1151        if (name.startsWith(SYSTEM_SETTING_PREFIX)) {
1152            String remainder = name.substring(SYSTEM_SETTING_PREFIX.length());
1153            if (LEGACY_SYSTEM_SETTING_NAMES.contains(remainder)) {
1154                return remainder;
1155            }
1156        }
1157        return name;
1158    }
1159
1160    /**
1161     * Translates legacy non-prefixed system settings to the form prefixed with SYSTEM:: .
1162     *
1163     * @param name a setting name
1164     * @return the translated setting name
1165     */
1166    private String translateSettingNameForSave(String name) {
1167
1168        if (LEGACY_SYSTEM_SETTING_NAMES.contains(name)) {
1169            return SYSTEM_SETTING_PREFIX + name;
1170        }
1171        return name;
1172    }
1173
1174}