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