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