001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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, 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.ade.configuration;
029
030import org.opencms.ade.configuration.CmsADEConfigDataInternal.AttributeValue;
031import org.opencms.ade.configuration.CmsADEConfigDataInternal.ConfigReferenceMeta;
032import org.opencms.ade.configuration.formatters.CmsFormatterBeanParser;
033import org.opencms.ade.configuration.formatters.CmsFormatterChangeSet;
034import org.opencms.ade.configuration.formatters.CmsFormatterConfigurationCacheState;
035import org.opencms.ade.configuration.formatters.CmsFormatterIndex;
036import org.opencms.ade.configuration.plugins.CmsSitePlugin;
037import org.opencms.ade.containerpage.shared.CmsContainer;
038import org.opencms.ade.containerpage.shared.CmsContainerElement;
039import org.opencms.ade.containerpage.shared.CmsFormatterConfig;
040import org.opencms.ade.detailpage.CmsDetailPageInfo;
041import org.opencms.ade.galleries.CmsAddContentRestriction;
042import org.opencms.file.CmsObject;
043import org.opencms.file.CmsResource;
044import org.opencms.file.CmsResourceFilter;
045import org.opencms.file.types.CmsResourceTypeFolder;
046import org.opencms.file.types.CmsResourceTypeFunctionConfig;
047import org.opencms.file.types.CmsResourceTypeXmlContent;
048import org.opencms.file.types.I_CmsResourceType;
049import org.opencms.gwt.CmsIconUtil;
050import org.opencms.gwt.shared.CmsGwtConstants;
051import org.opencms.jsp.util.CmsFunctionRenderer;
052import org.opencms.loader.CmsLoaderException;
053import org.opencms.main.CmsException;
054import org.opencms.main.CmsLog;
055import org.opencms.main.OpenCms;
056import org.opencms.main.OpenCmsServlet;
057import org.opencms.util.CmsStringUtil;
058import org.opencms.util.CmsUUID;
059import org.opencms.workplace.editors.directedit.CmsAdvancedDirectEditProvider.SitemapDirectEditPermissions;
060import org.opencms.xml.CmsXmlContentDefinition;
061import org.opencms.xml.containerpage.CmsFormatterConfiguration;
062import org.opencms.xml.containerpage.CmsXmlDynamicFunctionHandler;
063import org.opencms.xml.containerpage.I_CmsFormatterBean;
064import org.opencms.xml.content.CmsXmlContentFactory;
065import org.opencms.xml.content.CmsXmlContentProperty;
066
067import java.util.ArrayList;
068import java.util.Arrays;
069import java.util.Collection;
070import java.util.Collections;
071import java.util.HashMap;
072import java.util.HashSet;
073import java.util.Iterator;
074import java.util.LinkedHashMap;
075import java.util.List;
076import java.util.Map;
077import java.util.Objects;
078import java.util.Set;
079import java.util.concurrent.ExecutionException;
080import java.util.stream.Collectors;
081
082import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
083import org.apache.commons.logging.Log;
084
085import com.google.common.base.Optional;
086import com.google.common.cache.CacheBuilder;
087import com.google.common.cache.CacheLoader;
088import com.google.common.cache.LoadingCache;
089import com.google.common.collect.ArrayListMultimap;
090import com.google.common.collect.ImmutableList;
091import com.google.common.collect.Lists;
092import com.google.common.collect.Maps;
093import com.google.common.collect.Multimap;
094import com.google.common.collect.Sets;
095
096/**
097 * A class which represents the accessible configuration data at a given point in a sitemap.<p>
098 */
099public class CmsADEConfigData {
100
101    /**
102     * Bean which contains the detail information for a single sub-sitemap and resource type.<p>
103     *
104     * This includes both  information about the detail page itself, as well as the path of the folder
105     * which is used to store that content type in this subsitemap.<p>
106     *
107     */
108    public class DetailInfo {
109
110        /** The base path of the sitemap configuration where this information originates from. */
111        private String m_basePath;
112
113        /** The information about the detail page info itself. */
114        private CmsDetailPageInfo m_detailPageInfo;
115
116        /** The content folder path. */
117        private String m_folderPath;
118
119        /** The detail type. */
120        private String m_type;
121
122        /**
123         * Creates a new instance.<p>
124         *
125         * @param folderPath the content folder path
126         * @param detailPageInfo the detail page information
127         * @param type the detail type
128         * @param basePath the base path of the sitemap configuration
129         */
130        public DetailInfo(String folderPath, CmsDetailPageInfo detailPageInfo, String type, String basePath) {
131
132            m_folderPath = folderPath;
133            m_detailPageInfo = detailPageInfo;
134            m_type = type;
135            m_basePath = basePath;
136
137        }
138
139        /**
140         * Gets the base path of the sitemap configuration from which this information is coming.<p>
141         *
142         * @return the base path
143         */
144        public String getBasePath() {
145
146            return m_basePath;
147        }
148
149        /**
150         * Gets the detail page information.<p>
151         *
152         * @return the detail page information
153         */
154        public CmsDetailPageInfo getDetailPageInfo() {
155
156            return m_detailPageInfo;
157        }
158
159        /**
160         * Gets the content folder path.<p>
161         *
162         * @return the content folder path
163         */
164        public String getFolderPath() {
165
166            return m_folderPath;
167        }
168
169        /**
170         * Gets the detail type.<p>
171         *
172         * @return the detail type
173         */
174        public String getType() {
175
176            return m_type;
177        }
178
179        /**
180         * Sets the base path.<p>
181         *
182         * @param basePath the new base path value
183         */
184        public void setBasePath(String basePath) {
185
186            m_basePath = basePath;
187        }
188
189        /**
190         * @see java.lang.Object#toString()
191         */
192        @Override
193        public String toString() {
194
195            return ReflectionToStringBuilder.toString(this);
196        }
197    }
198
199    /** Sitemap attribute for the upload folder. */
200    public static final String ATTR_BINARY_UPLOAD_TARGET = "binary.upload.target";
201
202    /** Prefix for logging special request log messages. */
203    public static final String REQ_LOG_PREFIX = "[CmsADEConfigData] ";
204
205    /** Channel for logging special request log messages. */
206    public static final String REQUEST_LOG_CHANNEL = "org.opencms.ade.configuration.CmsADEConfigData.request";
207
208    /** The log instance for this class. */
209    private static final Log LOG = CmsLog.getLog(CmsADEConfigData.class);
210
211    /** Prefixes for internal settings which might be passed as formatter keys to findFormatter(). */
212    private static final HashSet<String> systemSettingPrefixes = new HashSet<>(
213        Arrays.asList("element", "model", "source", "use", "cms", "is"));
214
215    /** The wrapped configuration bean containing the actual data. */
216    protected CmsADEConfigDataInternal m_data;
217
218    /** Lazily initialized map of formatters. */
219    private Map<CmsUUID, I_CmsFormatterBean> m_activeFormatters;
220
221    /** Lazily initialized cache for active formatters by formatter key. */
222    private Multimap<String, I_CmsFormatterBean> m_activeFormattersByKey;
223
224    /** The sitemap attributes (may be null if not yet computed). */
225    private Map<String, AttributeValue> m_attributes;
226
227    /** The cache state to which the wrapped configuration bean belongs. */
228    private CmsADEConfigCacheState m_cache;
229
230    /** Current formatter configuration. */
231    private CmsFormatterConfigurationCacheState m_cachedFormatters;
232
233    /** The configuration sequence (contains the list of all sitemap configuration data beans to be used for inheritance). */
234    private CmsADEConfigurationSequence m_configSequence;
235
236    /** Cache for formatters by container type. */
237    private Map<String, List<I_CmsFormatterBean>> m_formattersByContainerType = new HashMap<>();
238
239    /** Cache for formatters by display type. */
240    private Map<String, List<I_CmsFormatterBean>> m_formattersByDisplayType = new HashMap<>();
241
242    /** Lazily initialized cache for formatters by JSP id. */
243    private Multimap<CmsUUID, I_CmsFormatterBean> m_formattersByJspId;
244
245    /** Lazily initialized cache for formatters by formatter key. */
246    private Multimap<String, I_CmsFormatterBean> m_formattersByKey;
247
248    /** Loading cache for for formatters grouped by type. */
249    private LoadingCache<String, List<I_CmsFormatterBean>> m_formattersByTypeCache = CacheBuilder.newBuilder().build(
250        new CacheLoader<String, List<I_CmsFormatterBean>>() {
251
252            @Override
253            public List<I_CmsFormatterBean> load(String typeName) throws Exception {
254
255                List<I_CmsFormatterBean> result = new ArrayList<>();
256                for (I_CmsFormatterBean formatter : getActiveFormatters().values()) {
257                    if (formatter.getResourceTypeNames().contains(typeName)) {
258                        result.add(formatter);
259                    }
260                }
261                return result;
262            }
263        });
264
265    /** Cached shared setting overrides. */
266    private volatile ImmutableList<CmsUUID> m_sharedSettingOverrides;
267
268    /** Set of names of active types.*/
269    private Set<String> m_typesAddable;
270
271    /** Cache of (active) resource type configurations by name. */
272    private Map<String, CmsResourceTypeConfig> m_typesByName;
273
274    /** Type names configured in this or ancestor sitemap configurations. */
275    private Set<String> m_typesInAncestors;
276
277    /**
278     * Creates a new configuration data object, based on an internal configuration data bean and a
279     * configuration cache state.<p>
280     *
281     * @param data the internal configuration data bean
282     * @param cache the configuration cache state
283     * @param configSequence the configuration sequence
284     */
285    public CmsADEConfigData(
286        CmsADEConfigDataInternal data,
287        CmsADEConfigCacheState cache,
288        CmsADEConfigurationSequence configSequence) {
289
290        m_data = data;
291        m_cache = cache;
292        m_configSequence = configSequence;
293    }
294
295    /**
296     * Generic method to merge lists of named configuration objects.<p>
297     *
298     * The lists are merged such that the configuration objects from the child list rise to the front of the result list,
299     * and two configuration objects will be merged themselves if they share the same name.<p>
300     *
301     * For example, if we have two lists of configuration objects:<p>
302     *
303     * parent: A1, B1, C1<p>
304     * child: D2, B2<p>
305     *
306     * then the resulting list will look like:<p>
307     *
308     * D2, B3, A1, C1<p>
309     *
310     * where B3 is the result of merging B1 and B2.<p>
311     *
312     * @param <C> the type of configuration object
313     * @param parentConfigs the parent configurations
314     * @param childConfigs the child configurations
315     * @param preserveDisabled if true, try to merge parents with disabled children instead of discarding them
316     *
317     * @return the merged configuration object list
318     */
319    public static <C extends I_CmsConfigurationObject<C>> List<C> combineConfigurationElements(
320        List<C> parentConfigs,
321        List<C> childConfigs,
322        boolean preserveDisabled) {
323
324        List<C> result = new ArrayList<C>();
325        Map<String, C> map = new LinkedHashMap<String, C>();
326        if (parentConfigs != null) {
327            for (C parent : Lists.reverse(parentConfigs)) {
328                map.put(parent.getKey(), parent);
329            }
330        }
331        if (childConfigs == null) {
332            childConfigs = Collections.emptyList();
333        }
334        for (C child : Lists.reverse(childConfigs)) {
335            String childKey = child.getKey();
336            if (child.isDisabled() && !preserveDisabled) {
337                map.remove(childKey);
338            } else {
339                C parent = map.get(childKey);
340                map.remove(childKey);
341                C newValue;
342                if (parent != null) {
343                    newValue = parent.merge(child);
344                } else {
345                    newValue = child;
346                }
347                map.put(childKey, newValue);
348            }
349        }
350        result = new ArrayList<C>(map.values());
351        Collections.reverse(result);
352        // those multiple "reverse" calls may a bit confusing. They are there because on the one hand we want to keep the
353        // configuration items from one configuration in the same order as they are defined, on the other hand we want
354        // configuration items from a child configuration to rise to the top of the configuration items.
355
356        // so for example, if the parent configuration has items with the keys A,B,C,E
357        // and the child configuration has items  with the keys C,B,D
358        // we want the items of the combined configuration in the order C,B,D,A,E
359
360        return result;
361    }
362
363    /**
364     * If the given formatter key has a sub-formatter suffix, returns the part before it,
365     * otherwise returns null.
366     *
367     * @param key the formatter key
368     * @return the parent formatter key
369     */
370    public static final String getParentFormatterKey(String key) {
371
372        if (key == null) {
373            return null;
374        }
375        int separatorPos = key.lastIndexOf(CmsGwtConstants.FORMATTER_SUBKEY_SEPARATOR);
376        if (separatorPos == -1) {
377            return null;
378        }
379        return key.substring(0, separatorPos);
380
381    }
382
383    /**
384     * Applies the formatter change sets of this and all parent configurations to a formatter index
385     *
386     * @param formatterIndex the collection of formatters to apply the changes to
387     *
388     * @param formatterCacheState the formatter cache state from which new external formatters should be fetched
389     */
390    public void applyAllFormatterChanges(
391        CmsFormatterIndex formatterIndex,
392        CmsFormatterConfigurationCacheState formatterCacheState) {
393
394        for (CmsFormatterChangeSet changeSet : getFormatterChangeSets()) {
395            changeSet.applyToFormatters(formatterIndex, formatterCacheState);
396        }
397    }
398
399    /**
400     * Gets the 'best' formatter for the given ID.<p>
401     *
402     * If the formatter with the ID has a key, then the active formatter with the same key is returned.  Otherwise, the
403     * formatter matching the ID is returned. So being active and having the same key is prioritized over an exact ID match.
404     *
405     * @param id the formatter ID
406     * @return the best formatter the given ID
407     */
408    public I_CmsFormatterBean findFormatter(CmsUUID id) {
409
410        return findFormatter(id, false);
411    }
412
413    /**
414     * Gets the 'best' formatter for the given ID.<p>
415     *
416     * If the formatter with the ID has a key, then the active formatter with the same key is returned.  Otherwise, the
417     * formatter matching the ID is returned. So being active and having the same key is prioritized over an exact ID match.
418     *
419     * @param id the formatter ID
420     * @param noWarn if true, disables warnings
421     * @return the best formatter the given ID
422     */
423    public I_CmsFormatterBean findFormatter(CmsUUID id, boolean noWarn) {
424
425        if (id == null) {
426            return null;
427        }
428
429        CmsFormatterConfigurationCacheState formatterState = getCachedFormatters();
430        I_CmsFormatterBean originalResult = formatterState.getFormatters().get(id);
431        I_CmsFormatterBean result = originalResult;
432        if ((result != null) && (result.getKey() != null)) {
433            String key = result.getKey();
434            I_CmsFormatterBean resultForKey = getFormatterAndWarnIfAmbiguous(getActiveFormattersByKey(), key, noWarn);
435            if (resultForKey != null) {
436                result = resultForKey;
437            } else {
438                String parentKey = getParentFormatterKey(key);
439                if (parentKey != null) {
440                    resultForKey = getFormatterAndWarnIfAmbiguous(getActiveFormattersByKey(), parentKey, noWarn);
441                    if (resultForKey != null) {
442                        result = resultForKey;
443                    }
444                }
445            }
446        }
447
448        if (result != originalResult) {
449            String message = "Using substitute formatter "
450                + getFormatterLabel(result)
451                + " instead of "
452                + getFormatterLabel(originalResult)
453                + " because of matching key.";
454            LOG.debug(message);
455            OpenCmsServlet.withRequestCache(
456                reqCache -> reqCache.addLog(REQUEST_LOG_CHANNEL, "debug", REQ_LOG_PREFIX + message));
457        }
458        return result;
459    }
460
461    /**
462     * Gets the 'best' formatter for the given name.<p>
463     *
464     * The name can be either a formatter key, or a formatter UUID. If it's a key, an active formatter with that key is returned.
465     * If it's a UUID, and the formatter with that UUID has no key, it will be returned. If it does have a key, the active formatter
466     * with that key is returned (so being active and having the same key is prioritized over an exact ID match).
467     *
468     * @param name a formatter name (key or ID)
469     * @return the best formatter for that name, or null if no formatter could be found
470     */
471    public I_CmsFormatterBean findFormatter(String name) {
472
473        return findFormatter(name, false);
474    }
475
476    /**
477     * Gets the 'best' formatter for the given name.<p>
478     *
479     * The name can be either a formatter key, or a formatter UUID. If it's a key, an active formatter with that key is returned.
480     * If it's a UUID, and the formatter with that UUID has no key, it will be returned. If it does have a key, the active formatter
481     * with that key is returned (so being active and having the same key is prioritized over an exact ID match).
482     *
483     * @param name a formatter name (key or ID)
484     * @param noWarn if true, disables warnings
485     * @return the best formatter for that name, or null if no formatter could be found
486     */
487    public I_CmsFormatterBean findFormatter(String name, boolean noWarn) {
488
489        if (name == null) {
490            return null;
491        }
492
493        if (systemSettingPrefixes.contains(name) || name.startsWith(CmsContainerElement.SYSTEM_SETTING_PREFIX)) {
494            if (LOG.isDebugEnabled()) {
495                LOG.debug("System setting prefix used: " + name, new Exception());
496            }
497            return null;
498        }
499
500        if (CmsUUID.isValidUUID(name)) {
501            return findFormatter(new CmsUUID(name), noWarn);
502        }
503
504        if (name.startsWith(CmsFormatterConfig.SCHEMA_FORMATTER_ID)) {
505            return null;
506        }
507
508        Multimap<String, I_CmsFormatterBean> active = getActiveFormattersByKey();
509        I_CmsFormatterBean result = getFormatterAndWarnIfAmbiguous(active, name, noWarn);
510        if (result != null) {
511            return result;
512        }
513
514        String parentName = getParentFormatterKey(name);
515        if (parentName != null) {
516            result = getFormatterAndWarnIfAmbiguous(active, parentName, noWarn);
517            if (result != null) {
518                return result;
519            }
520        }
521
522        if (!noWarn) {
523            String message1 = "No local formatter found for key '"
524                + name
525                + "' at '"
526                + getBasePath()
527                + "', trying inactive formatters";
528            LOG.warn(message1);
529            OpenCmsServlet.withRequestCache(rc -> rc.addLog(REQUEST_LOG_CHANNEL, "warn", REQ_LOG_PREFIX + message1));
530        }
531
532        Multimap<String, I_CmsFormatterBean> all = getFormattersByKey();
533        result = getFormatterAndWarnIfAmbiguous(all, name, noWarn);
534        if (result != null) {
535            return result;
536        }
537
538        if (parentName != null) {
539            result = getFormatterAndWarnIfAmbiguous(all, parentName, noWarn);
540            if (result != null) {
541                return result;
542            }
543        }
544
545        if (!noWarn) {
546            OpenCmsServlet.withRequestCache(
547                rc -> rc.addLog(
548                    REQUEST_LOG_CHANNEL,
549                    "warn",
550                    REQ_LOG_PREFIX + "No formatter found for key '" + name + "' at '" + getBasePath() + "'"));
551        }
552        return null;
553    }
554
555    /**
556     * Gets the active external (non-schema) formatters for this sub-sitemap.<p>
557     *
558     * @return the map of active external formatters by structure id
559     */
560    public Map<CmsUUID, I_CmsFormatterBean> getActiveFormatters() {
561
562        if (m_activeFormatters == null) {
563            CmsFormatterIndex formatterIndex = new CmsFormatterIndex();
564            for (I_CmsFormatterBean formatter : getCachedFormatters().getAutoEnabledFormatters().values()) {
565                formatterIndex.addFormatter(formatter);
566            }
567            applyAllFormatterChanges(formatterIndex, getCachedFormatters());
568            m_activeFormatters = Collections.unmodifiableMap(formatterIndex.getFormattersWithAdditionalKeys());
569        }
570        return m_activeFormatters;
571    }
572
573    /**
574     * Gets the active formatters for a given container type.
575     *
576     * @param containerType a container type
577     *
578     * @return the active formatters for the container type
579     */
580    public List<I_CmsFormatterBean> getActiveFormattersWithContainerType(String containerType) {
581
582        return m_formattersByContainerType.computeIfAbsent(
583            containerType,
584            type -> Collections.unmodifiableList(
585                getActiveFormatters().values().stream().filter(
586                    formatter -> formatter.getContainerTypes().contains(type)).collect(Collectors.toList())));
587    }
588
589    /**
590     * Gets the active formatters for a given display type.
591     *
592     * @param displayType a display type
593     * @return the active formatters for the display type
594     */
595    public List<I_CmsFormatterBean> getActiveFormattersWithDisplayType(String displayType) {
596
597        return m_formattersByDisplayType.computeIfAbsent(
598            displayType,
599            type -> Collections.unmodifiableList(
600                getActiveFormatters().values().stream().filter(
601                    formatter -> Objects.equals(type, formatter.getDisplayType())).collect(Collectors.toList()))
602
603        );
604    }
605
606    /**
607     * Gets the set of names of types active in this sitemap configuration.
608     *
609     * @return the set of type names of active types
610     */
611    public Set<String> getAddableTypeNames() {
612
613        Set<String> result = m_typesAddable;
614        if (result != null) {
615            return result;
616        } else {
617            Set<String> mutableResult = new HashSet<>();
618            for (CmsResourceTypeConfig typeConfig : internalGetResourceTypes(true)) {
619                if (!typeConfig.isAddDisabled()) {
620                    mutableResult.add(typeConfig.getTypeName());
621                }
622            }
623            result = Collections.unmodifiableSet(mutableResult);
624            m_typesAddable = result;
625            return result;
626        }
627    }
628
629    /**
630     * Gets the 'add content' restriction for this configuration.
631     *
632     * @return the 'add content' restriction
633     */
634    public CmsAddContentRestriction getAddContentRestriction() {
635
636        getAncestorTypeNames();
637
638        CmsADEConfigData parentConfig = parent();
639        if (parentConfig == null) {
640            return m_data.getAddContentRestriction();
641        } else {
642            return parentConfig.getAddContentRestriction().merge(m_data.getAddContentRestriction());
643        }
644    }
645
646    /**
647     * Gets the list of all detail pages.<p>
648     *
649     * @return the list of all detail pages
650     */
651    public List<CmsDetailPageInfo> getAllDetailPages() {
652
653        return getAllDetailPages(true);
654    }
655
656    /**
657     * Gets a list of all detail pages.<p>
658     *
659     * @param update if true, this method will try to correct the root paths in the returned objects if the corresponding resources have been moved
660     *
661     * @return the list of all detail pages
662     */
663    public List<CmsDetailPageInfo> getAllDetailPages(boolean update) {
664
665        CmsADEConfigData parentData = parent();
666        List<CmsDetailPageInfo> parentDetailPages;
667        if (parentData != null) {
668            parentDetailPages = parentData.getAllDetailPages(false);
669        } else {
670            parentDetailPages = Collections.emptyList();
671        }
672        List<CmsDetailPageInfo> result = mergeDetailPages(parentDetailPages, m_data.getOwnDetailPages());
673        if (update) {
674            result = updateUris(result);
675        }
676        return result;
677    }
678
679    /**
680     * Gets the set of names of types configured in this or any ancestor sitemap configurations.
681     *
682     * @return the set of type names from all ancestor configurations
683     */
684    public Set<String> getAncestorTypeNames() {
685
686        Set<String> result = m_typesInAncestors;
687        if (result != null) {
688            return result;
689        } else {
690            Set<String> mutableResult = new HashSet<>();
691            for (CmsResourceTypeConfig typeConfig : internalGetResourceTypes(false)) {
692                mutableResult.add(typeConfig.getTypeName());
693            }
694            result = Collections.unmodifiableSet(mutableResult);
695            m_typesInAncestors = result;
696            return result;
697        }
698    }
699
700    /**
701     * Gets the value of an attribute, or a default value
702     *
703     * @param key the attribute key
704     * @param defaultValue the value to return if no attribute with the given name is found
705     *
706     * @return the attribute value
707     */
708    public String getAttribute(String key, String defaultValue) {
709
710        AttributeValue value = getAttributes().get(key);
711        if (value != null) {
712            return value.getValue();
713        } else {
714            return defaultValue;
715        }
716
717    }
718
719    /**
720     * Gets the active sitemap attribute editor configuration.
721     *
722     * @return the active sitemap attribute editor configuration
723     */
724    public CmsSitemapAttributeEditorConfiguration getAttributeEditorConfiguration() {
725
726        CmsUUID id = getAttributeEditorConfigurationId();
727        CmsSitemapAttributeEditorConfiguration result = m_cache.getAttributeEditorConfiguration(id);
728        if (result == null) {
729            result = CmsSitemapAttributeEditorConfiguration.EMPTY;
730        }
731        return result;
732
733    }
734
735    /**
736     * Gets the structure id of the configured sitemap attribute editor configuration.
737     *
738     * @return the structure id of the configured sitemap attribute editor configuration
739     */
740    public CmsUUID getAttributeEditorConfigurationId() {
741
742        CmsADEConfigData parent = parent();
743        CmsUUID result = m_data.getAttributeEditorConfigId();
744        if ((result == null) && (parent != null)) {
745            result = parent.getAttributeEditorConfigurationId();
746        }
747        return result;
748
749    }
750
751    /**
752     * Gets the map of attributes configured for this sitemap, including values inherited from parent sitemaps.
753     *
754     * @return the map of attributes
755     */
756    public Map<String, AttributeValue> getAttributes() {
757
758        if (m_attributes != null) {
759            return m_attributes;
760        }
761        CmsADEConfigData parentConfig = parent();
762        Map<String, AttributeValue> result = new HashMap<>();
763        if (parentConfig != null) {
764            result.putAll(parentConfig.getAttributes());
765        }
766
767        for (Map.Entry<String, AttributeValue> entry : m_data.getAttributes().entrySet()) {
768            result.put(entry.getKey(), entry.getValue());
769        }
770        Map<String, AttributeValue> immutableResult = Collections.unmodifiableMap(result);
771        m_attributes = immutableResult;
772        return immutableResult;
773    }
774
775    /**
776     * Gets the configuration base path.<p>
777     *
778     * For example, if the configuration file is located at /sites/default/.content/.config, the base path is /sites/default.<p>
779     *
780     * @return the base path of the configuration
781     */
782    public String getBasePath() {
783
784        return m_data.getBasePath();
785    }
786
787    /**
788     * Gets the cached formatters.<p>
789     *
790     * @return the cached formatters
791     */
792    public CmsFormatterConfigurationCacheState getCachedFormatters() {
793
794        if (m_cachedFormatters == null) {
795            m_cachedFormatters = OpenCms.getADEManager().getCachedFormatters(
796                getCms().getRequestContext().getCurrentProject().isOnlineProject());
797        }
798        return m_cachedFormatters;
799    }
800
801    /**
802     * Gets an (immutable) list of paths of configuration files in inheritance order.
803     *
804     * @return the list of configuration files
805     */
806    public List<String> getConfigPaths() {
807
808        return m_configSequence.getConfigPaths();
809
810    }
811
812    /**
813     * Returns the names of the bundles configured as workplace bundles in any module configuration.<p>
814     *
815     * @return the names of the bundles configured as workplace bundles in any module configuration.
816     */
817    public Set<String> getConfiguredWorkplaceBundles() {
818
819        Set<String> result = new HashSet<String>();
820        for (CmsResourceTypeConfig config : internalGetResourceTypes(false)) {
821            String bundlename = config.getConfiguredWorkplaceBundle();
822            if (null != bundlename) {
823                result.add(bundlename);
824            }
825        }
826        return result;
827    }
828
829    /**
830     * Gets the content folder path.<p>
831     *
832     * For example, if the configuration file is located at /sites/default/.content/.config, the content folder path is /sites/default/.content
833     *
834     * @return the content folder path
835     */
836    public String getContentFolderPath() {
837
838        return CmsStringUtil.joinPaths(m_data.getBasePath(), CmsADEManager.CONTENT_FOLDER_NAME);
839
840    }
841
842    /**
843     * Returns a list of the creatable resource types.<p>
844     *
845     * @param cms the CMS context used to check whether the resource types are creatable
846     * @param pageFolderRootPath the root path of the current container page
847     * @return the list of creatable resource type
848     *
849     * @throws CmsException if something goes wrong
850     */
851    public List<CmsResourceTypeConfig> getCreatableTypes(CmsObject cms, String pageFolderRootPath) throws CmsException {
852
853        List<CmsResourceTypeConfig> result = new ArrayList<CmsResourceTypeConfig>();
854        for (CmsResourceTypeConfig typeConfig : getResourceTypes()) {
855            if (typeConfig.checkCreatable(cms, pageFolderRootPath)) {
856                result.add(typeConfig);
857            }
858        }
859        return result;
860    }
861
862    /**
863     * Returns the default detail page.<p>
864     *
865     * @return the default detail page
866     */
867    public CmsDetailPageInfo getDefaultDetailPage() {
868
869        for (CmsDetailPageInfo detailpage : getAllDetailPages(true)) {
870            if (CmsADEManager.DEFAULT_DETAILPAGE_TYPE.equals(detailpage.getType())) {
871                return detailpage;
872            }
873        }
874        return null;
875    }
876
877    /**
878     * Returns the default model page.<p>
879     *
880     * @return the default model page
881     */
882    public CmsModelPageConfig getDefaultModelPage() {
883
884        List<CmsModelPageConfig> modelPages = getModelPages();
885        for (CmsModelPageConfig modelPageConfig : getModelPages()) {
886            if (modelPageConfig.isDefault()) {
887                return modelPageConfig;
888            }
889        }
890        if (modelPages.isEmpty()) {
891            return null;
892        }
893        return modelPages.get(0);
894    }
895
896    /**
897     * Gets the detail information for this sitemap config data object.<p>
898     *
899     * @param cms the CMS context
900     * @return the list of detail information
901     */
902    public List<DetailInfo> getDetailInfos(CmsObject cms) {
903
904        List<DetailInfo> result = Lists.newArrayList();
905        List<CmsDetailPageInfo> detailPages = getAllDetailPages(true);
906        Collections.reverse(detailPages); // make sure primary detail pages come later in the list and override other detail pages for the same type
907        Map<String, CmsDetailPageInfo> primaryDetailPageMapByType = Maps.newHashMap();
908        for (CmsDetailPageInfo pageInfo : detailPages) {
909            primaryDetailPageMapByType.put(pageInfo.getType(), pageInfo);
910        }
911        for (CmsResourceTypeConfig typeConfig : getResourceTypes()) {
912            String typeName = typeConfig.getTypeName();
913            if (((typeConfig.getFolderOrName() == null) || !typeConfig.getFolderOrName().isPageRelative())
914                && primaryDetailPageMapByType.containsKey(typeName)) {
915                String folderPath = typeConfig.getFolderPath(cms, null);
916                CmsDetailPageInfo pageInfo = primaryDetailPageMapByType.get(typeName);
917                result.add(new DetailInfo(folderPath, pageInfo, typeName, getBasePath()));
918            }
919        }
920        return result;
921    }
922
923    /**
924     * Gets the detail pages for a specific type.<p>
925     *
926     * @param type the type name
927     *
928     * @return the list of detail pages for that type
929     */
930    public List<CmsDetailPageInfo> getDetailPagesForType(String type) {
931
932        List<CmsDetailPageInfo> result = new ArrayList<CmsDetailPageInfo>();
933        CmsResourceTypeConfig typeConfig = getResourceType(type);
934        if (type.startsWith(CmsDetailPageInfo.FUNCTION_PREFIX)
935            || ((typeConfig != null) && !typeConfig.isDetailPagesDisabled())) {
936
937            List<CmsDetailPageInfo> defaultPages = new ArrayList<>();
938            for (CmsDetailPageInfo detailpage : getAllDetailPages(true)) {
939                if (detailpage.getType().equals(type)) {
940                    result.add(detailpage);
941                } else if (CmsADEManager.DEFAULT_DETAILPAGE_TYPE.equals(detailpage.getType())) {
942                    defaultPages.add(detailpage);
943                }
944            }
945            result.addAll(defaultPages);
946        }
947        return result;
948    }
949
950    /**
951     * Returns the direct edit permissions for e.g. list elements with the given type.
952     *
953     * @param type the resource type name
954     * @return the permissions
955     */
956    public SitemapDirectEditPermissions getDirectEditPermissions(String type) {
957
958        if (type == null) {
959            LOG.error("Null type in checkListEdit");
960            return SitemapDirectEditPermissions.all;
961        }
962
963        if (!getAncestorTypeNames().contains(type)) {
964            // not configured anywhere for ADE
965            return SitemapDirectEditPermissions.editAndCreate;
966        }
967
968        CmsResourceTypeConfig typeConfig = getResourceType(type);
969        if (typeConfig == null) {
970            return SitemapDirectEditPermissions.none;
971        }
972
973        if (typeConfig.isEnabledInLists()) {
974            return SitemapDirectEditPermissions.editAndCreate;
975        }
976
977        if (typeConfig.isCreateDisabled() || typeConfig.isAddDisabled()) {
978            return SitemapDirectEditPermissions.editOnly;
979        }
980
981        return SitemapDirectEditPermissions.all;
982    }
983
984    /**
985     * Gets the display mode for deactivated functions in the gallery dialog.
986     *
987     * @param defaultValue the default value to return if it's not set
988     * @return the display mode for deactivated types
989     */
990    public CmsGalleryDisabledTypesMode getDisabledFunctionsMode(CmsGalleryDisabledTypesMode defaultValue) {
991
992        CmsADEConfigData parentData = parent();
993        if (m_data.getGalleryDisabledFunctionsMode() != null) {
994            return m_data.getGalleryDisabledFunctionsMode();
995        } else if (parentData != null) {
996            return parentData.getDisabledFunctionsMode(defaultValue);
997        } else {
998            return defaultValue;
999        }
1000    }
1001
1002    /**
1003     * Gets the display mode for deactivated types in the gallery dialog.
1004     *
1005     * @param defaultValue the default value to return if it's not set
1006     * @return the display mode for deactivated types
1007     */
1008    public CmsGalleryDisabledTypesMode getDisabledTypeMode(CmsGalleryDisabledTypesMode defaultValue) {
1009
1010        CmsADEConfigData parentData = parent();
1011        if (m_data.getDisabledTypeMode() != null) {
1012            return m_data.getDisabledTypeMode();
1013        } else if (parentData != null) {
1014            return parentData.getDisabledTypeMode(defaultValue);
1015        } else {
1016            return defaultValue;
1017        }
1018    }
1019
1020    /**
1021     * Returns all available display formatters.<p>
1022     *
1023     * @param cms the cms context
1024     *
1025     * @return the available display formatters
1026     */
1027    public List<I_CmsFormatterBean> getDisplayFormatters(CmsObject cms) {
1028
1029        List<I_CmsFormatterBean> result = new ArrayList<I_CmsFormatterBean>();
1030        for (I_CmsFormatterBean formatter : getCachedFormatters().getFormatters().values()) {
1031            if (formatter.isDisplayFormatter()) {
1032                result.add(formatter);
1033            }
1034        }
1035        return result;
1036    }
1037
1038    /**
1039     * Gets the bean that represents the dynamic function availability.
1040     *
1041     * @param formatterConfig the formatter configuration state
1042     *
1043     * @return the dynamic function availability
1044     */
1045    public CmsFunctionAvailability getDynamicFunctionAvailability(CmsFormatterConfigurationCacheState formatterConfig) {
1046
1047        CmsADEConfigData parentData = parent();
1048        CmsFunctionAvailability result;
1049        if (parentData == null) {
1050            result = new CmsFunctionAvailability(formatterConfig);
1051        } else {
1052            result = parentData.getDynamicFunctionAvailability(formatterConfig);
1053        }
1054        Collection<CmsUUID> enabledIds = m_data.getDynamicFunctions();
1055        Collection<CmsUUID> disabledIds = m_data.getFunctionsToRemove();
1056        if (m_data.isRemoveAllFunctions() && !m_configSequence.getMeta().isSkipRemovals()) {
1057            result.removeAll();
1058        }
1059        if (enabledIds != null) {
1060            result.addAll(enabledIds);
1061        }
1062        if (disabledIds != null) {
1063            for (CmsUUID id : disabledIds) {
1064                result.remove(id);
1065            }
1066        }
1067        return result;
1068    }
1069
1070    /**
1071     * Gets the root path of the closest subsite going up the tree which has the 'exclude external detail contents' option enabled, or '/' if no such subsite exists.
1072     *
1073     * @return the root path of the closest subsite with 'external detail contents excluded'
1074     */
1075    public String getExternalDetailContentExclusionFolder() {
1076
1077        if (m_data.isExcludeExternalDetailContents()) {
1078            String basePath = m_data.getBasePath();
1079            if (basePath == null) {
1080                return "/";
1081            } else {
1082                return basePath;
1083            }
1084        } else {
1085            CmsADEConfigData parent = parent();
1086            if (parent != null) {
1087                return parent.getExternalDetailContentExclusionFolder();
1088            } else {
1089                return "/";
1090            }
1091        }
1092    }
1093
1094    /**
1095     * Returns the formatter change sets for this and all parent sitemaps, ordered by increasing folder depth of the sitemap.<p>
1096     *
1097     * @return the formatter change sets for all ancestor sitemaps
1098     */
1099    public List<CmsFormatterChangeSet> getFormatterChangeSets() {
1100
1101        CmsADEConfigData currentConfig = this;
1102        List<CmsFormatterChangeSet> result = Lists.newArrayList();
1103        while (currentConfig != null) {
1104            CmsFormatterChangeSet changes = currentConfig.getOwnFormatterChangeSet();
1105            if (changes != null) {
1106                if (currentConfig.getMeta().isSkipRemovals()) {
1107                    changes = changes.cloneWithNoRemovals();
1108                }
1109                result.add(changes);
1110            }
1111            currentConfig = currentConfig.parent();
1112        }
1113        Collections.reverse(result);
1114        return result;
1115    }
1116
1117    /**
1118     * Gets the formatter configuration for a resource.<p>
1119     *
1120     * @param cms the current CMS context
1121     * @param res the resource for which the formatter configuration should be retrieved
1122     *
1123     * @return the configuration of formatters for the resource
1124     */
1125    public CmsFormatterConfiguration getFormatters(CmsObject cms, CmsResource res) {
1126
1127        if (CmsResourceTypeFunctionConfig.isFunction(res)) {
1128
1129            CmsFormatterConfigurationCacheState formatters = getCachedFormatters();
1130            I_CmsFormatterBean function = findFormatter(res.getStructureId());
1131            if (function != null) {
1132                return CmsFormatterConfiguration.create(cms, Collections.singletonList(function));
1133            } else {
1134                if ((!res.getStructureId().isNullUUID())
1135                    && cms.existsResource(res.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION)) {
1136                    // usually if it's just been created, but not added to the configuration cache yet
1137                    CmsFormatterBeanParser parser = new CmsFormatterBeanParser(cms, new HashMap<>());
1138                    try {
1139                        function = parser.parse(
1140                            CmsXmlContentFactory.unmarshal(cms, cms.readFile(res)),
1141                            res.getRootPath(),
1142                            "" + res.getStructureId());
1143                        return CmsFormatterConfiguration.create(cms, Collections.singletonList(function));
1144                    } catch (Exception e) {
1145                        LOG.warn(e.getLocalizedMessage(), e);
1146                        return CmsFormatterConfiguration.EMPTY_CONFIGURATION;
1147                    }
1148
1149                } else {
1150                    // if a new function has been dragged on the page, it doesn't exist in the VFS yet, so we need a different
1151                    // instance as a replacement
1152                    CmsResource defaultFormatter = CmsFunctionRenderer.getDefaultFunctionInstance(cms);
1153                    if (defaultFormatter != null) {
1154                        I_CmsFormatterBean defaultFormatterBean = formatters.getFormatters().get(
1155                            defaultFormatter.getStructureId());
1156                        return CmsFormatterConfiguration.create(cms, Collections.singletonList(defaultFormatterBean));
1157                    } else {
1158                        LOG.warn("Could not read default formatter for functions.");
1159                        return CmsFormatterConfiguration.EMPTY_CONFIGURATION;
1160                    }
1161                }
1162            }
1163        } else {
1164            try {
1165                int resTypeId = res.getTypeId();
1166                return getFormatters(
1167                    cms,
1168                    OpenCms.getResourceManager().getResourceType(resTypeId),
1169                    getFormattersFromSchema(cms, res));
1170            } catch (CmsLoaderException e) {
1171                LOG.warn(e.getLocalizedMessage(), e);
1172                return CmsFormatterConfiguration.EMPTY_CONFIGURATION;
1173            }
1174        }
1175    }
1176
1177    /**
1178     * Gets a named function reference.<p>
1179     *
1180     * @param name the name of the function reference
1181     *
1182     * @return the function reference for the given name
1183     */
1184    public CmsFunctionReference getFunctionReference(String name) {
1185
1186        List<CmsFunctionReference> functionReferences = getFunctionReferences();
1187        for (CmsFunctionReference functionRef : functionReferences) {
1188            if (functionRef.getName().equals(name)) {
1189                return functionRef;
1190            }
1191        }
1192        return null;
1193    }
1194
1195    /**
1196     * Gets the list of configured function references.<p>
1197     *
1198     * @return the list of configured function references
1199     */
1200    public List<CmsFunctionReference> getFunctionReferences() {
1201
1202        return internalGetFunctionReferences();
1203    }
1204
1205    /**
1206     * Gets the map of external (non-schema) formatters which are inactive in this sub-sitemap.<p>
1207     *
1208     * @return the map inactive external formatters
1209     */
1210    public Map<CmsUUID, I_CmsFormatterBean> getInactiveFormatters() {
1211
1212        CmsFormatterConfigurationCacheState cacheState = getCachedFormatters();
1213        Map<CmsUUID, I_CmsFormatterBean> result = Maps.newHashMap(cacheState.getFormatters());
1214        result.keySet().removeAll(getActiveFormatters().keySet());
1215        return result;
1216    }
1217
1218    /**
1219     * Gets the list of available model pages.<p>
1220     *
1221     * @return the list of available model pages
1222     */
1223    public List<CmsModelPageConfig> getModelPages() {
1224
1225        return getModelPages(false);
1226    }
1227
1228    /**
1229     * Gets the list of available model pages.<p>
1230     *
1231     * @param includeDisable <code>true</code> to include disabled model pages
1232     *
1233     * @return the list of available model pages
1234     */
1235    public List<CmsModelPageConfig> getModelPages(boolean includeDisable) {
1236
1237        CmsADEConfigData parentData = parent();
1238        List<CmsModelPageConfig> parentModelPages;
1239        if ((parentData != null) && !m_data.isDiscardInheritedModelPages()) {
1240            parentModelPages = parentData.getModelPages();
1241        } else {
1242            parentModelPages = Collections.emptyList();
1243        }
1244
1245        List<CmsModelPageConfig> result = combineConfigurationElements(
1246            parentModelPages,
1247            m_data.getOwnModelPageConfig(),
1248            includeDisable);
1249        return result;
1250    }
1251
1252    /**
1253     * Gets the formatter changes for this sitemap configuration.<p>
1254     *
1255     * @return the formatter change set
1256     */
1257    public CmsFormatterChangeSet getOwnFormatterChangeSet() {
1258
1259        return m_data.getFormatterChangeSet();
1260    }
1261
1262    /**
1263     * Gets the configuration for the available properties.<p>
1264     *
1265     * @return the configuration for the available properties
1266     */
1267    public List<CmsPropertyConfig> getPropertyConfiguration() {
1268
1269        CmsADEConfigData parentData = parent();
1270        List<CmsPropertyConfig> parentProperties;
1271        boolean removeInherited = m_data.isDiscardInheritedProperties() && !getMeta().isSkipRemovals();
1272        if ((parentData != null) && !removeInherited) {
1273            parentProperties = parentData.getPropertyConfiguration();
1274        } else {
1275            parentProperties = Collections.emptyList();
1276        }
1277        LinkedHashMap<String, CmsPropertyConfig> propMap = new LinkedHashMap<>();
1278        for (CmsPropertyConfig conf : parentProperties) {
1279            if (conf.isDisabled()) {
1280                continue;
1281            }
1282            propMap.put(conf.getName(), conf);
1283        }
1284        for (CmsPropertyConfig conf : m_data.getOwnPropertyConfigurations()) {
1285            if (conf.isDisabled()) {
1286                propMap.remove(conf.getName());
1287            } else if (propMap.containsKey(conf.getName())) {
1288                propMap.put(conf.getName(), propMap.get(conf.getName()).merge(conf));
1289            } else {
1290                propMap.put(conf.getName(), conf);
1291            }
1292        }
1293        List<CmsPropertyConfig> result = new ArrayList<>(propMap.values());
1294        return result;
1295    }
1296
1297    /**
1298     * Computes the ordered map of properties to display in the property dialog, given the map of default property configurations passed as a parameter.
1299     *
1300     * @param defaultProperties the default property configurations
1301     * @return the ordered map of property configurations for the property dialog
1302     */
1303    public Map<String, CmsXmlContentProperty> getPropertyConfiguration(
1304        Map<String, CmsXmlContentProperty> defaultProperties) {
1305
1306        List<CmsPropertyConfig> myPropConfigs = getPropertyConfiguration();
1307        Map<String, CmsXmlContentProperty> allProps = new LinkedHashMap<>(defaultProperties);
1308        Map<String, CmsXmlContentProperty> result = new LinkedHashMap<>();
1309        for (CmsPropertyConfig prop : myPropConfigs) {
1310            allProps.put(prop.getName(), prop.getPropertyData());
1311            if (prop.isTop()) {
1312                result.put(prop.getName(), prop.getPropertyData());
1313            }
1314        }
1315        for (Map.Entry<String, CmsXmlContentProperty> entry : allProps.entrySet()) {
1316            if (!result.containsKey(entry.getKey())) {
1317                result.put(entry.getKey(), entry.getValue());
1318            }
1319        }
1320        return result;
1321
1322    }
1323
1324    /**
1325     * Gets the property configuration as a map of CmsXmlContentProperty instances.<p>
1326     *
1327     * @return the map of property configurations
1328     */
1329    public Map<String, CmsXmlContentProperty> getPropertyConfigurationAsMap() {
1330
1331        Map<String, CmsXmlContentProperty> result = new LinkedHashMap<String, CmsXmlContentProperty>();
1332        for (CmsPropertyConfig propConf : getPropertyConfiguration()) {
1333            result.put(propConf.getName(), propConf.getPropertyData());
1334        }
1335        return result;
1336    }
1337
1338    /**
1339     * Returns the resource from which this configuration was read.<p>
1340     *
1341     * @return the resource from which this configuration was read
1342     */
1343    public CmsResource getResource() {
1344
1345        return m_data.getResource();
1346    }
1347
1348    /**
1349     * Returns the configuration for a specific resource type.<p>
1350     *
1351     * @param typeName the name of the type
1352     *
1353     * @return the resource type configuration for that type
1354     */
1355    public CmsResourceTypeConfig getResourceType(String typeName) {
1356
1357        for (CmsResourceTypeConfig type : getResourceTypes()) {
1358            if (typeName.equals(type.getTypeName())) {
1359                return type;
1360            }
1361        }
1362        return null;
1363    }
1364
1365    /**
1366     * Gets a list of all available resource type configurations.<p>
1367     *
1368     * @return the available resource type configurations
1369     */
1370    public List<CmsResourceTypeConfig> getResourceTypes() {
1371
1372        List<CmsResourceTypeConfig> result = internalGetResourceTypes(true);
1373        for (CmsResourceTypeConfig config : result) {
1374            config.initialize(getCms());
1375        }
1376        return result;
1377    }
1378
1379    /**
1380     * Gets the searchable resource type configurations.<p>
1381     *
1382     * @param cms the current CMS context
1383     * @return the searchable resource type configurations
1384     */
1385    public Collection<CmsResourceTypeConfig> getSearchableTypes(CmsObject cms) {
1386
1387        return getResourceTypes();
1388    }
1389
1390    /**
1391     * Gets the list of structure ids of the shared setting overrides, ordered by increasing specificity.
1392     *
1393     * @return the list of structure ids of shared setting overrides
1394     */
1395    public ImmutableList<CmsUUID> getSharedSettingOverrides() {
1396
1397        if (m_sharedSettingOverrides != null) {
1398            return m_sharedSettingOverrides;
1399        }
1400
1401        CmsADEConfigData currentConfig = this;
1402        List<CmsADEConfigData> relevantConfigurations = new ArrayList<>();
1403        while (currentConfig != null) {
1404            relevantConfigurations.add(currentConfig);
1405            if (currentConfig.m_data.isRemoveSharedSettingOverrides()
1406                && !currentConfig.m_configSequence.getMeta().isSkipRemovals()) {
1407                // once we find a configuration where 'remove all shared setting overrides' is enabled,
1408                // all parent configurations become irrelevant
1409                break;
1410            }
1411            currentConfig = currentConfig.parent();
1412        }
1413
1414        // order by ascending specificity
1415        Collections.reverse(relevantConfigurations);
1416
1417        List<CmsUUID> ids = new ArrayList<>();
1418        for (CmsADEConfigData config : relevantConfigurations) {
1419            CmsUUID id = config.m_data.getSharedSettingOverride();
1420            if (id != null) {
1421                ids.add(id);
1422            }
1423        }
1424        ImmutableList<CmsUUID> result = ImmutableList.copyOf(ids);
1425        m_sharedSettingOverrides = result;
1426        return result;
1427    }
1428
1429    /**
1430     * Gets the ids of site plugins which are active in this sitemap configuration.
1431     *
1432     * @return the ids of active site plugins
1433     */
1434    public Set<CmsUUID> getSitePluginIds() {
1435
1436        CmsADEConfigData parent = parent();
1437        Set<CmsUUID> result;
1438        if ((parent == null) || (m_data.isRemoveAllPlugins() && !getMeta().isSkipRemovals())) {
1439            result = new HashSet<>();
1440        } else {
1441            result = parent.getSitePluginIds();
1442        }
1443        result.removeAll(m_data.getRemovedPlugins());
1444        result.addAll(m_data.getAddedPlugins());
1445        return result;
1446    }
1447
1448    /**
1449     * Gets the list of site plugins active in this sitemap configuration.
1450     *
1451     * @return the list of active site plugins
1452     */
1453    public List<CmsSitePlugin> getSitePlugins() {
1454
1455        Set<CmsUUID> pluginIds = getSitePluginIds();
1456        List<CmsSitePlugin> result = new ArrayList<>();
1457        Map<CmsUUID, CmsSitePlugin> plugins = m_cache.getSitePlugins();
1458        for (CmsUUID id : pluginIds) {
1459            CmsSitePlugin sitePlugin = plugins.get(id);
1460            if (sitePlugin != null) {
1461                result.add(sitePlugin);
1462            }
1463        }
1464        return result;
1465    }
1466
1467    /**
1468     * Gets the type ordering mode.
1469     *
1470     * @return the type ordering mode
1471     */
1472    public CmsTypeOrderingMode getTypeOrderingMode() {
1473
1474        CmsTypeOrderingMode ownOrderingMode = m_data.getTypeOrderingMode();
1475        if (ownOrderingMode != null) {
1476            return ownOrderingMode;
1477        } else {
1478            CmsADEConfigData parentConfig = parent();
1479            CmsTypeOrderingMode parentMode = null;
1480            if (parentConfig == null) {
1481                parentMode = CmsTypeOrderingMode.latestOnTop;
1482            } else {
1483                parentMode = parentConfig.getTypeOrderingMode();
1484            }
1485            return parentMode;
1486        }
1487
1488    }
1489
1490    /**
1491     * Gets a map of the active resource type configurations, with type names as keys.
1492     *
1493     * @return the map of active types
1494     */
1495    public Map<String, CmsResourceTypeConfig> getTypesByName() {
1496
1497        if (m_typesByName != null) {
1498            return m_typesByName;
1499        }
1500        Map<String, CmsResourceTypeConfig> result = new HashMap<>();
1501        for (CmsResourceTypeConfig type : getResourceTypes()) {
1502            result.put(type.getTypeName(), type);
1503        }
1504        result = Collections.unmodifiableMap(result);
1505        m_typesByName = result;
1506        return result;
1507    }
1508
1509    /**
1510     * Gets the set of resource type names for which schema formatters can be enabled or disabled and which are not disabled in this sub-sitemap.<p>
1511     *
1512     * @return the set of types for which schema formatters are active
1513     */
1514    public Set<String> getTypesWithActiveSchemaFormatters() {
1515
1516        Set<String> result = Sets.newHashSet(getTypesWithModifiableFormatters());
1517        for (CmsFormatterChangeSet changeSet : getFormatterChangeSets()) {
1518            changeSet.applyToTypes(result);
1519        }
1520        return result;
1521    }
1522
1523    /**
1524     * Gets the set of names of resource types which have schema-based formatters that can be enabled or disabled.<p>
1525     *
1526     * @return the set of names of resource types which have schema-based formatters that can be enabled or disabled
1527     */
1528    public Set<String> getTypesWithModifiableFormatters() {
1529
1530        Set<String> result = new HashSet<String>();
1531        for (I_CmsResourceType type : OpenCms.getResourceManager().getResourceTypes()) {
1532            if (type instanceof CmsResourceTypeXmlContent) {
1533                CmsXmlContentDefinition contentDef = null;
1534                try {
1535                    contentDef = CmsXmlContentDefinition.getContentDefinitionForType(getCms(), type.getTypeName());
1536                    if ((contentDef != null) && contentDef.getContentHandler().hasModifiableFormatters()) {
1537                        result.add(type.getTypeName());
1538                    }
1539                } catch (Exception e) {
1540                    LOG.error(e.getLocalizedMessage(), e);
1541                }
1542            }
1543        }
1544        return result;
1545
1546    }
1547
1548    /**
1549     * Checks if there are any matching formatters for the given set of containers.<p>
1550     *
1551     * @param cms the current CMS context
1552     * @param resType the resource type for which the formatter configuration should be retrieved
1553     * @param containers the page containers
1554     *
1555     * @return if there are any matching formatters
1556     */
1557    public boolean hasFormatters(CmsObject cms, I_CmsResourceType resType, Collection<CmsContainer> containers) {
1558
1559        try {
1560            if (CmsXmlDynamicFunctionHandler.TYPE_FUNCTION.equals(resType.getTypeName())
1561                || CmsResourceTypeFunctionConfig.TYPE_NAME.equals(resType.getTypeName())) {
1562                // dynamic function may match any container
1563                return true;
1564            }
1565            CmsXmlContentDefinition def = CmsXmlContentDefinition.getContentDefinitionForType(
1566                cms,
1567                resType.getTypeName());
1568            CmsFormatterConfiguration schemaFormatters = def.getContentHandler().getFormatterConfiguration(cms, null);
1569            CmsFormatterConfiguration formatters = getFormatters(cms, resType, schemaFormatters);
1570            for (CmsContainer cont : containers) {
1571                if (cont.isEditable()
1572                    && (formatters.getAllMatchingFormatters(cont.getType(), cont.getWidth()).size() > 0)) {
1573                    return true;
1574                }
1575            }
1576        } catch (CmsException e) {
1577            LOG.warn(e.getLocalizedMessage(), e);
1578
1579        }
1580        return false;
1581    }
1582
1583    /**
1584     * Returns the value of the "create contents locally" flag.<p>
1585     *
1586     * If this flag is set, contents of types configured in a super-sitemap will be created in the sub-sitemap (if the user
1587     * creates them from the sub-sitemap).
1588     *
1589     * @return the "create contents locally" flag
1590     */
1591    public boolean isCreateContentsLocally() {
1592
1593        return m_data.isCreateContentsLocally();
1594    }
1595
1596    /**
1597     * Returns the value of the "discard inherited model pages" flag.<p>
1598     *
1599     * If this flag is set, inherited model pages will be discarded for this sitemap.<p>
1600     *
1601     * @return the "discard inherited model pages" flag
1602     */
1603    public boolean isDiscardInheritedModelPages() {
1604
1605        return m_data.isDiscardInheritedModelPages();
1606    }
1607
1608    /**
1609     * Returns the value of the "discard inherited properties" flag.<p>
1610     *
1611     * If this is flag is set, inherited property definitions will be discarded for this sitemap.<p>
1612     *
1613     * @return the "discard inherited properties" flag.<p>
1614     */
1615    public boolean isDiscardInheritedProperties() {
1616
1617        return m_data.isDiscardInheritedProperties();
1618    }
1619
1620    /**
1621     * Returns the value of the "discard inherited types" flag.<p>
1622     *
1623     * If this flag is set, inherited resource types from a super-sitemap will be discarded for this sitemap.<p>
1624     *
1625     * @return the "discard inherited types" flag
1626     */
1627    public boolean isDiscardInheritedTypes() {
1628
1629        return m_data.isDiscardInheritedTypes();
1630    }
1631
1632    /**
1633     * True if detail contents outside this sitemap should not be rendered in detail pages from this sitemap.
1634     *
1635     * @return true if detail contents outside this sitemap should not be rendered in detail pages from this sitemap.
1636     */
1637    public boolean isExcludeExternalDetailContents() {
1638
1639        return m_data.isExcludeExternalDetailContents();
1640    }
1641
1642    /**
1643     * Checks if dynamic functions not matching any containers should be hidden.
1644     *
1645     * @return true if dynamic functions not matching any containers should be hidden
1646     */
1647    public boolean isHideNonMatchingFunctions() {
1648
1649        return getDisabledFunctionsMode(CmsGalleryDisabledTypesMode.hide) == CmsGalleryDisabledTypesMode.hide;
1650    }
1651
1652    /**
1653     * Returns true if the subsite should be included in the site selector.
1654     *
1655     * @return true if the subsite should be included in the site selector
1656     */
1657    public boolean isIncludeInSiteSelector() {
1658
1659        return m_configSequence.getConfig().isIncludeInSiteSelector();
1660    }
1661
1662    /**
1663     * Returns true if this is a module configuration instead of a normal sitemap configuration.<p>
1664     *
1665     * @return true if this is a module configuration
1666     */
1667    public boolean isModuleConfiguration() {
1668
1669        return m_data.isModuleConfig();
1670    }
1671
1672    /**
1673     * Returns true if detail pages from this sitemap should be preferred for links to contents in this sitemap.<p>
1674     *
1675     * @return true if detail pages from this sitemap should be preferred for links to contents in this sitemap
1676     */
1677    public boolean isPreferDetailPagesForLocalContents() {
1678
1679        return m_data.isPreferDetailPagesForLocalContents();
1680    }
1681
1682    /**
1683     * Checks if any formatter with the given JSP id has the 'search content' option set to true.
1684     *
1685     * @param jspId the structure id of a formatter JSP
1686     * @return true if any of the formatters
1687     */
1688    public boolean isSearchContentFormatter(CmsUUID jspId) {
1689
1690        for (I_CmsFormatterBean formatter : getFormattersByJspId().get(jspId)) {
1691            if (formatter.isSearchContent()) {
1692                return true;
1693            }
1694        }
1695        return false;
1696    }
1697
1698    /**
1699     * Returns true if the new container page format, which uses formatter keys (but also is different in other ways from the new format
1700     *
1701     * @return true if formatter keys should be used
1702     */
1703    public boolean isUseFormatterKeys() {
1704
1705        Boolean result = m_data.getUseFormatterKeys();
1706        if (result != null) {
1707            LOG.debug("isUseFormatterKeys - found value " + result + " at " + getBasePath());
1708            return result.booleanValue();
1709        }
1710        CmsADEConfigData parent = parent();
1711        if (parent != null) {
1712            return parent.isUseFormatterKeys();
1713        }
1714        boolean defaultValue = true;
1715        LOG.debug("isUseFormatterKeys - using defaultValue " + defaultValue);
1716        return defaultValue;
1717    }
1718
1719    /**
1720     * Fetches the parent configuration of this configuration.<p>
1721     *
1722     * If this configuration is a sitemap configuration with no direct parent configuration,
1723     * the module configuration will be returned. If this configuration already is a module configuration,
1724     * null will be returned.<p>
1725     *
1726     * @return the parent configuration
1727     */
1728    public CmsADEConfigData parent() {
1729
1730        Optional<CmsADEConfigurationSequence> parentPath = m_configSequence.getParent();
1731        if (parentPath.isPresent()) {
1732            CmsADEConfigDataInternal internalData = parentPath.get().getConfig();
1733            return new CmsADEConfigData(internalData, m_cache, parentPath.get());
1734        } else {
1735            return null;
1736        }
1737    }
1738
1739    /**
1740     * Returns true if the sitemap attribute editor should be available in this subsite.
1741     *
1742     * @return true if the sitemap attribute editor dialog should be available
1743     */
1744    public boolean shouldShowSitemapAttributeDialog() {
1745
1746        return getAttributeEditorConfiguration().getAttributeDefinitions().size() > 0;
1747    }
1748
1749    /**
1750     * Clears the internal formatter caches.
1751     *
1752     * <p>This should only be used for test cases.
1753     */
1754    protected void clearCaches() {
1755
1756        m_activeFormatters = null;
1757        m_activeFormattersByKey = null;
1758        m_formattersByKey = null;
1759        m_formattersByJspId = null;
1760        m_formattersByTypeCache.invalidateAll();
1761    }
1762
1763    /**
1764     * Creates the content directory for this configuration node if possible.<p>
1765     *
1766     * @throws CmsException if something goes wrong
1767     */
1768    protected void createContentDirectory() throws CmsException {
1769
1770        if (!isModuleConfiguration()) {
1771            String contentFolder = getContentFolderPath();
1772            if (!getCms().existsResource(contentFolder)) {
1773                getCms().createResource(
1774                    contentFolder,
1775                    OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.getStaticTypeName()));
1776            }
1777        }
1778    }
1779
1780    /**
1781     * Gets the CMS object used for VFS operations.<p>
1782     *
1783     * @return the CMS object used for VFS operations
1784     */
1785    protected CmsObject getCms() {
1786
1787        return m_cache.getCms();
1788    }
1789
1790    /**
1791     * Gets the CMS object used for VFS operations.<p>
1792     *
1793     * @return the CMS object
1794     */
1795    protected CmsObject getCmsObject() {
1796
1797        return getCms();
1798    }
1799
1800    /**
1801     * Helper method to converts a list of detail pages to a map from type names to lists of detail pages for each type.<p>
1802     *
1803     * @param detailPages the list of detail pages
1804     *
1805     * @return the map of detail pages
1806     */
1807    protected Map<String, List<CmsDetailPageInfo>> getDetailPagesMap(List<CmsDetailPageInfo> detailPages) {
1808
1809        Map<String, List<CmsDetailPageInfo>> result = Maps.newHashMap();
1810        for (CmsDetailPageInfo detailpage : detailPages) {
1811            String type = detailpage.getType();
1812            if (!result.containsKey(type)) {
1813                result.put(type, new ArrayList<CmsDetailPageInfo>());
1814            }
1815            result.get(type).add(detailpage);
1816        }
1817        return result;
1818    }
1819
1820    /**
1821     * Collects the folder types in a map.<p>
1822     *
1823     * @return the map of folder types
1824     *
1825     * @throws CmsException if something goes wrong
1826     */
1827    protected Map<String, String> getFolderTypes() throws CmsException {
1828
1829        Map<String, String> result = new HashMap<String, String>();
1830        CmsObject cms = OpenCms.initCmsObject(getCms());
1831        if (m_data.isModuleConfig()) {
1832            Set<String> siteRoots = OpenCms.getSiteManager().getSiteRoots();
1833            for (String siteRoot : siteRoots) {
1834                cms.getRequestContext().setSiteRoot(siteRoot);
1835                for (CmsResourceTypeConfig config : getResourceTypes()) {
1836                    if (!config.isDetailPagesDisabled()) {
1837                        String typeName = config.getTypeName();
1838                        if (!config.isPageRelative()) { // elements stored with container pages can not be used as detail contents
1839                            String folderPath = config.getFolderPath(cms, null);
1840                            result.put(CmsStringUtil.joinPaths(folderPath, "/"), typeName);
1841                        }
1842                    }
1843                }
1844            }
1845        } else {
1846            for (CmsResourceTypeConfig config : getResourceTypes()) {
1847                if (!config.isDetailPagesDisabled()) {
1848                    String typeName = config.getTypeName();
1849                    if (!config.isPageRelative()) { // elements stored with container pages can not be used as detail contents
1850                        String folderPath = config.getFolderPath(getCms(), null);
1851                        result.put(CmsStringUtil.joinPaths(folderPath, "/"), typeName);
1852                    }
1853                }
1854            }
1855        }
1856        return result;
1857    }
1858
1859    /**
1860     * Gets the formatter configuration for a resource type.<p>
1861     *
1862     * @param cms the current CMS context
1863     * @param resType the resource type
1864     * @param schemaFormatters the resource schema formatters
1865     *
1866     * @return the configuration of formatters for the resource type
1867     */
1868    protected CmsFormatterConfiguration getFormatters(
1869        CmsObject cms,
1870        I_CmsResourceType resType,
1871        CmsFormatterConfiguration schemaFormatters) {
1872
1873        String typeName = resType.getTypeName();
1874        List<I_CmsFormatterBean> formatters = new ArrayList<I_CmsFormatterBean>();
1875        Set<String> types = new HashSet<String>();
1876        types.add(typeName);
1877        for (CmsFormatterChangeSet changeSet : getFormatterChangeSets()) {
1878            if (changeSet != null) {
1879                changeSet.applyToTypes(types);
1880            }
1881        }
1882
1883        if ((schemaFormatters != null) && types.contains(typeName)) {
1884            for (I_CmsFormatterBean formatter : schemaFormatters.getAllFormatters()) {
1885                formatters.add(formatter);
1886            }
1887        }
1888
1889        try {
1890            List<I_CmsFormatterBean> formattersForType = m_formattersByTypeCache.get(typeName);
1891            formatters.addAll(formattersForType);
1892        } catch (ExecutionException e) {
1893            LOG.error(e.getLocalizedMessage(), e);
1894
1895        }
1896        return CmsFormatterConfiguration.create(cms, formatters);
1897    }
1898
1899    /**
1900     * Gets the formatters from the schema.<p>
1901     *
1902     * @param cms the current CMS context
1903     * @param res the resource for which the formatters should be retrieved
1904     *
1905     * @return the formatters from the schema
1906     */
1907    protected CmsFormatterConfiguration getFormattersFromSchema(CmsObject cms, CmsResource res) {
1908
1909        try {
1910            return OpenCms.getResourceManager().getResourceType(res.getTypeId()).getFormattersForResource(cms, res);
1911        } catch (CmsException e) {
1912            LOG.error(e.getLocalizedMessage(), e);
1913            return CmsFormatterConfiguration.EMPTY_CONFIGURATION;
1914        }
1915    }
1916
1917    /**
1918     * Gets the metadata about how this configuration was referenced.
1919     *
1920     * @return the metadata
1921     */
1922    protected ConfigReferenceMeta getMeta() {
1923
1924        return m_configSequence.getMeta();
1925    }
1926
1927    /**
1928     * Internal method for getting the function references.<p>
1929     *
1930     * @return the function references
1931     */
1932    protected List<CmsFunctionReference> internalGetFunctionReferences() {
1933
1934        CmsADEConfigData parentData = parent();
1935        if ((parentData == null)) {
1936            if (m_data.isModuleConfig()) {
1937                return Collections.unmodifiableList(m_data.getFunctionReferences());
1938            } else {
1939                return Lists.newArrayList();
1940            }
1941        } else {
1942            return parentData.internalGetFunctionReferences();
1943
1944        }
1945    }
1946
1947    /**
1948     * Helper method for getting the list of resource types.<p>
1949     *
1950     * @param filterDisabled true if disabled types should be filtered from the result
1951     *
1952     * @return the list of resource types
1953     */
1954    protected List<CmsResourceTypeConfig> internalGetResourceTypes(boolean filterDisabled) {
1955
1956        CmsADEConfigData parentData = parent();
1957        List<CmsResourceTypeConfig> parentResourceTypes = null;
1958        if (parentData == null) {
1959            parentResourceTypes = Lists.newArrayList();
1960        } else {
1961            parentResourceTypes = Lists.newArrayList();
1962            for (CmsResourceTypeConfig typeConfig : parentData.internalGetResourceTypes(false)) {
1963                CmsResourceTypeConfig copiedType = typeConfig.copy(
1964                    m_data.isDiscardInheritedTypes() && !getMeta().isSkipRemovals());
1965                parentResourceTypes.add(copiedType);
1966            }
1967        }
1968        String template = getMeta().getTemplate();
1969        List<CmsResourceTypeConfig> result = combineConfigurationElements(
1970            parentResourceTypes,
1971            m_data.getOwnResourceTypes().stream().map(type -> type.markWithTemplate(template)).collect(
1972                Collectors.toList()),
1973            true);
1974        if (m_data.isCreateContentsLocally()) {
1975            for (CmsResourceTypeConfig typeConfig : result) {
1976                typeConfig.updateBasePath(
1977                    CmsStringUtil.joinPaths(m_data.getBasePath(), CmsADEManager.CONTENT_FOLDER_NAME));
1978            }
1979        }
1980        if (filterDisabled) {
1981            Iterator<CmsResourceTypeConfig> iter = result.iterator();
1982            while (iter.hasNext()) {
1983                CmsResourceTypeConfig typeConfig = iter.next();
1984                if (typeConfig.isDisabled()) {
1985                    iter.remove();
1986                }
1987            }
1988        }
1989        if (getTypeOrderingMode() == CmsTypeOrderingMode.byDisplayOrder) {
1990            Collections.sort(result, (a, b) -> Integer.compare(a.getOrder(), b.getOrder()));
1991        }
1992        return result;
1993    }
1994
1995    /**
1996     * Merges two lists of detail pages, one from a parent configuration and one from a child configuration.<p>
1997     *
1998     * @param parentDetailPages the parent's detail pages
1999     * @param ownDetailPages the child's detail pages
2000     *
2001     * @return the merged detail pages
2002     */
2003    protected List<CmsDetailPageInfo> mergeDetailPages(
2004        List<CmsDetailPageInfo> parentDetailPages,
2005        List<CmsDetailPageInfo> ownDetailPages) {
2006
2007        List<CmsDetailPageInfo> parentDetailPageCopies = Lists.newArrayList();
2008        for (CmsDetailPageInfo info : parentDetailPages) {
2009            parentDetailPageCopies.add(info.copyAsInherited());
2010        }
2011
2012        List<CmsDetailPageInfo> result = new ArrayList<CmsDetailPageInfo>();
2013        Map<String, List<CmsDetailPageInfo>> resultDetailPageMap = Maps.newHashMap();
2014        Map<String, List<CmsDetailPageInfo>> parentPagesGroupedByType = getDetailPagesMap(parentDetailPageCopies);
2015        Map<String, List<CmsDetailPageInfo>> childPagesGroupedByType = getDetailPagesMap(ownDetailPages);
2016        Set<String> allTypes = new HashSet<>();
2017        allTypes.addAll(parentPagesGroupedByType.keySet());
2018        allTypes.addAll(childPagesGroupedByType.keySet());
2019        for (String type : allTypes) {
2020
2021            List<CmsDetailPageInfo> parentPages = parentPagesGroupedByType.get(type);
2022            List<CmsDetailPageInfo> childPages = childPagesGroupedByType.get(type);
2023            List<CmsDetailPageInfo> merged = mergeDetailPagesForType(parentPages, childPages);
2024            resultDetailPageMap.put(type, merged);
2025        }
2026        result = new ArrayList<CmsDetailPageInfo>();
2027        for (List<CmsDetailPageInfo> pages : resultDetailPageMap.values()) {
2028            result.addAll(pages);
2029        }
2030        return result;
2031    }
2032
2033    /**
2034     * Helper method to correct paths in detail page beans if the corresponding resources have been moved.<p>
2035     *
2036     * @param detailPages the original list of detail pages
2037     *
2038     * @return the corrected list of detail pages
2039     */
2040    protected List<CmsDetailPageInfo> updateUris(List<CmsDetailPageInfo> detailPages) {
2041
2042        List<CmsDetailPageInfo> result = new ArrayList<CmsDetailPageInfo>();
2043        for (CmsDetailPageInfo page : detailPages) {
2044            CmsUUID structureId = page.getId();
2045            try {
2046                String rootPath = OpenCms.getADEManager().getRootPath(
2047                    structureId,
2048                    getCms().getRequestContext().getCurrentProject().isOnlineProject());
2049                String iconClasses;
2050                if (page.getType().startsWith(CmsDetailPageInfo.FUNCTION_PREFIX)) {
2051                    iconClasses = CmsIconUtil.getIconClasses(CmsXmlDynamicFunctionHandler.TYPE_FUNCTION, null, false);
2052                } else {
2053                    iconClasses = CmsIconUtil.getIconClasses(page.getType(), null, false);
2054                }
2055                CmsDetailPageInfo correctedPage = new CmsDetailPageInfo(
2056                    structureId,
2057                    rootPath,
2058                    page.getType(),
2059                    page.getQualifier(),
2060                    iconClasses);
2061                result.add(page.isInherited() ? correctedPage.copyAsInherited() : correctedPage);
2062            } catch (CmsException e) {
2063                LOG.warn(e.getLocalizedMessage(), e);
2064            }
2065        }
2066        return result;
2067    }
2068
2069    /**
2070     * Gets a multimap of active formatters for which a formatter key is defined, with the formatter keys as map keys.
2071     *
2072     * @return the map of active formatters by key
2073     */
2074    private Multimap<String, I_CmsFormatterBean> getActiveFormattersByKey() {
2075
2076        if (m_activeFormattersByKey == null) {
2077            ArrayListMultimap<String, I_CmsFormatterBean> activeFormattersByKey = ArrayListMultimap.create();
2078            for (I_CmsFormatterBean formatter : getActiveFormatters().values()) {
2079                for (String key : formatter.getAllKeys()) {
2080                    activeFormattersByKey.put(key, formatter);
2081                }
2082            }
2083            m_activeFormattersByKey = activeFormattersByKey;
2084        }
2085        return m_activeFormattersByKey;
2086    }
2087
2088    /**
2089     * Gets a formatter with the given key from a multimap, and warns if there are multiple values
2090     * for the key.
2091     *
2092     * @param formatterMap the formatter multimap
2093     * @param name the formatter key
2094     * @param noWarn if true, disables warnings
2095     * @return the formatter for the key (null if none are found, the first one if multiple are found)
2096     */
2097    private I_CmsFormatterBean getFormatterAndWarnIfAmbiguous(
2098        Multimap<String, I_CmsFormatterBean> formatterMap,
2099        String name,
2100        boolean noWarn) {
2101
2102        I_CmsFormatterBean result;
2103        result = null;
2104        Collection<I_CmsFormatterBean> activeForKey = formatterMap.get(name);
2105        if (activeForKey.size() > 0) {
2106            if (activeForKey.size() > 1) {
2107                if (!noWarn) {
2108                    String labels = ""
2109                        + activeForKey.stream().map(this::getFormatterLabel).collect(Collectors.toList());
2110                    String message = "Ambiguous formatter for key '"
2111                        + name
2112                        + "' at '"
2113                        + getBasePath()
2114                        + "': found "
2115                        + labels;
2116                    LOG.warn(message);
2117                    OpenCmsServlet.withRequestCache(
2118                        rc -> rc.addLog(REQUEST_LOG_CHANNEL, "warn", REQ_LOG_PREFIX + message));
2119                }
2120            }
2121            result = activeForKey.iterator().next();
2122        }
2123        return result;
2124    }
2125
2126    /**
2127     * Gets a user-friendly formatter label to use for logging.
2128     *
2129     * @param formatter a formatter bean
2130     * @return the formatter label for the log
2131     */
2132    private String getFormatterLabel(I_CmsFormatterBean formatter) {
2133
2134        return formatter.getLocation() != null ? formatter.getLocation() : formatter.getId();
2135    }
2136
2137    /**
2138     * Gets formatters by JSP id.
2139     *
2140     * @return the multimap from JSP id to formatter beans
2141     */
2142    private Multimap<CmsUUID, I_CmsFormatterBean> getFormattersByJspId() {
2143
2144        if (m_formattersByJspId == null) {
2145            ArrayListMultimap<CmsUUID, I_CmsFormatterBean> formattersByJspId = ArrayListMultimap.create();
2146            for (I_CmsFormatterBean formatter : getCachedFormatters().getFormatters().values()) {
2147                formattersByJspId.put(formatter.getJspStructureId(), formatter);
2148            }
2149            m_formattersByJspId = formattersByJspId;
2150        }
2151        return m_formattersByJspId;
2152    }
2153
2154    /**
2155     * Gets a multimap of the formatters for which a formatter key is defined, with the formatter keys as map keys.
2156     *
2157     * @return the map of formatters by key
2158     */
2159    private Multimap<String, I_CmsFormatterBean> getFormattersByKey() {
2160
2161        if (m_formattersByKey == null) {
2162            ArrayListMultimap<String, I_CmsFormatterBean> formattersByKey = ArrayListMultimap.create();
2163            for (I_CmsFormatterBean formatter : getCachedFormatters().getFormatters().values()) {
2164                for (String key : formatter.getAllKeys()) {
2165                    formattersByKey.put(key, formatter);
2166                }
2167            }
2168            m_formattersByKey = formattersByKey;
2169        }
2170        return m_formattersByKey;
2171    }
2172
2173    /**
2174     * Merges detail pages for a specific resource type from a parent and child sitemap.
2175     *
2176     * @param parentPages the detail pages from the parent sitemap
2177     * @param childPages the detail pages from the child sitemap
2178     * @return the merged detail pages
2179     */
2180    private List<CmsDetailPageInfo> mergeDetailPagesForType(
2181        List<CmsDetailPageInfo> parentPages,
2182        List<CmsDetailPageInfo> childPages) {
2183
2184        List<CmsDetailPageInfo> merged = null;
2185        if ((parentPages != null) && (childPages != null)) {
2186            // the only nontrivial case. If the child detail pages contain one with an unqualified type, they completely override the parent detail pages.
2187            // otherwise they only override the parent detail pages for each matching qualifier.
2188
2189            if (childPages.stream().anyMatch(page -> page.getQualifier() == null)) {
2190                merged = childPages;
2191            } else {
2192                Map<String, List<CmsDetailPageInfo>> pagesGroupedByQualifiedType = parentPages.stream().collect(
2193                    Collectors.groupingBy(page -> page.getQualifiedType()));
2194                pagesGroupedByQualifiedType.putAll(
2195                    childPages.stream().collect(Collectors.groupingBy(page -> page.getQualifiedType())));
2196                merged = pagesGroupedByQualifiedType.entrySet().stream().flatMap(
2197                    entry -> entry.getValue().stream()).collect(Collectors.toList());
2198            }
2199        } else if (parentPages != null) {
2200            merged = parentPages;
2201        } else if (childPages != null) {
2202            merged = childPages;
2203        } else {
2204            merged = new ArrayList<>();
2205        }
2206        return merged;
2207    }
2208
2209}