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