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