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