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