001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ade.configuration;
029
030import org.opencms.ade.configuration.CmsADEConfigData.DetailInfo;
031import org.opencms.ade.configuration.CmsADEConfigDataInternal.ConfigReference;
032import org.opencms.ade.configuration.CmsADEConfigDataInternal.ConfigReferenceInstance;
033import org.opencms.ade.configuration.CmsADEConfigDataInternal.ConfigReferenceMeta;
034import org.opencms.ade.configuration.plugins.CmsSitePlugin;
035import org.opencms.ade.detailpage.CmsDetailPageInfo;
036import org.opencms.file.CmsObject;
037import org.opencms.file.CmsResource;
038import org.opencms.file.CmsResourceFilter;
039import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
040import org.opencms.main.CmsException;
041import org.opencms.main.CmsLog;
042import org.opencms.main.OpenCms;
043import org.opencms.util.CmsStringUtil;
044import org.opencms.util.CmsUUID;
045
046import java.util.ArrayList;
047import java.util.Collections;
048import java.util.HashMap;
049import java.util.HashSet;
050import java.util.List;
051import java.util.Map;
052import java.util.Set;
053import java.util.concurrent.ExecutionException;
054import java.util.concurrent.TimeUnit;
055import java.util.stream.Collectors;
056
057import org.apache.commons.logging.Log;
058
059import com.google.common.base.Supplier;
060import com.google.common.base.Suppliers;
061import com.google.common.cache.Cache;
062import com.google.common.cache.CacheBuilder;
063import com.google.common.collect.HashMultimap;
064import com.google.common.collect.Lists;
065import com.google.common.collect.Maps;
066import com.google.common.collect.Multimaps;
067import com.google.common.collect.SetMultimap;
068
069/**
070 * An immutable object which represents the complete ADE configuration (sitemap and module configurations)
071 * at a certain instant in time.<p>
072 */
073public class CmsADEConfigCacheState {
074
075    /** The logger instance for this class. */
076    private static final Log LOG = CmsLog.getLog(CmsADEConfigCacheState.class);
077
078    /** The CMS context used for VFS operations. */
079    private CmsObject m_cms;
080
081    /** Cached set of names of content types anywhere in the configuration. */
082    private volatile Set<String> m_contentTypes;
083
084    /** Cache for detail page lists. */
085    private Map<String, List<String>> m_detailPageCache;
086
087    /** Memoized supplier for the cached detail page ids. */
088    private Supplier<Set<CmsUUID>> m_detailPageIdCache;
089
090    /** Cached detail page types. */
091    private volatile Set<String> m_detailPageTypes;
092
093    /** The available element views. */
094    private Map<CmsUUID, CmsElementView> m_elementViews;
095
096    /** The cached content types for folders. */
097    private SetMultimap<String, String> m_folderTypes = HashMultimap.create();
098
099    /** The merged configuration from all the modules. */
100    private CmsADEConfigDataInternal m_moduleConfiguration;
101
102    /** The list of module configurations. */
103    private List<CmsADEConfigDataInternal> m_moduleConfigurations;
104
105    /** The map of sitemap configurations by structure id. */
106    private Map<CmsUUID, CmsADEConfigDataInternal> m_siteConfigurations = new HashMap<CmsUUID, CmsADEConfigDataInternal>();
107
108    /** The configurations from the sitemap / VFS. */
109    private Map<String, CmsADEConfigDataInternal> m_siteConfigurationsByPath = new HashMap<String, CmsADEConfigDataInternal>();
110
111    /** The sitemap attribute editor configurations. */
112    private Map<CmsUUID, CmsSitemapAttributeEditorConfiguration> m_sitemapAttributeEditorConfigurations;
113
114    /** Site plugins. */
115    private Map<CmsUUID, CmsSitePlugin> m_sitePlugins;
116
117    /** Cache for attribute overview maps - keys are attribute names, values are maps from paths to attribute values. */
118    private Cache<String, Map<String, String>> m_attributeValuesByPathCache = CacheBuilder.newBuilder().expireAfterWrite(
119        3,
120        TimeUnit.SECONDS).concurrencyLevel(2).build();
121
122    /** Cached list of subsites to be included in the site selector. */
123    private volatile List<String> m_subsitesForSiteSelector;
124
125    /**
126     * Creates a new configuration cache state.<p>
127     *
128     * @param cms the CMS context to use
129     * @param siteConfigurations the map of sitemap configuration beans by structure id
130     * @param moduleConfigs the complete list of module configurations
131     * @param elementViews the available element views
132     * @param sitePlugins the map of sitemap plugins
133     * @param attributeEditorConfigurations the attribute editor configurations
134     */
135    public CmsADEConfigCacheState(
136        CmsObject cms,
137        Map<CmsUUID, CmsADEConfigDataInternal> siteConfigurations,
138        List<CmsADEConfigDataInternal> moduleConfigs,
139        Map<CmsUUID, CmsElementView> elementViews,
140        Map<CmsUUID, CmsSitePlugin> sitePlugins,
141        Map<CmsUUID, CmsSitemapAttributeEditorConfiguration> attributeEditorConfigurations) {
142
143        m_cms = cms;
144        m_siteConfigurations = siteConfigurations;
145        m_moduleConfigurations = moduleConfigs;
146        m_elementViews = elementViews;
147        m_sitePlugins = sitePlugins;
148        m_sitemapAttributeEditorConfigurations = attributeEditorConfigurations;
149        for (CmsADEConfigDataInternal data : siteConfigurations.values()) {
150            if (data.getBasePath() != null) {
151                m_siteConfigurationsByPath.put(data.getBasePath(), data);
152            } else {
153                LOG.info("Empty base path for sitemap configuration: " + data.getResource().getRootPath());
154            }
155        }
156        m_moduleConfiguration = mergeConfigurations(moduleConfigs);
157        try {
158            m_folderTypes = computeFolderTypes();
159        } catch (Exception e) {
160            m_folderTypes = HashMultimap.create();
161            LOG.error(e.getLocalizedMessage(), e);
162        }
163        CacheBuilder<?, ?> detailPageCacheBuilder = CacheBuilder.newBuilder().concurrencyLevel(8).expireAfterWrite(
164            30,
165            TimeUnit.SECONDS);
166        m_detailPageCache = (Map<String, List<String>>)(detailPageCacheBuilder.build().asMap());
167        m_detailPageIdCache = Suppliers.memoize(this::collectDetailPageIds);
168
169    }
170
171    /**
172     * Creates an empty ADE configuration cache state.<p>
173     *
174     * @param cms the CMS context
175     * @return the empty configuration cache state
176     */
177    public static CmsADEConfigCacheState emptyState(CmsObject cms) {
178
179        return new CmsADEConfigCacheState(
180            cms,
181            Collections.<CmsUUID, CmsADEConfigDataInternal> emptyMap(),
182            Collections.<CmsADEConfigDataInternal> emptyList(),
183            Collections.<CmsUUID, CmsElementView> emptyMap(),
184            Collections.emptyMap(),
185            Collections.emptyMap());
186    }
187
188    /**
189     * Computes the map from folder paths to content types for this ADE configuration state.<p>
190     *
191     * @return the map of content types by folder root paths
192     *
193     * @throws CmsException if something goes wrong
194     */
195    public SetMultimap<String, String> computeFolderTypes() throws CmsException {
196
197        HashMultimap<String, String> folderTypes = HashMultimap.create();
198        // do this first, since folder types from modules should be overwritten by folder types from sitemaps
199        if (m_moduleConfiguration != null) {
200            folderTypes.putAll(wrap(m_moduleConfiguration).getFolderTypes());
201        }
202
203        List<CmsADEConfigDataInternal> configDataObjects = new ArrayList<CmsADEConfigDataInternal>(
204            m_siteConfigurationsByPath.values());
205        for (CmsADEConfigDataInternal configData : configDataObjects) {
206            folderTypes.putAll(wrap(configData).getFolderTypes());
207        }
208        return folderTypes;
209    }
210
211    /**
212     * Creates a new object which represents the changed configuration state given some updates, without
213     * changing the current configuration state (this object instance).
214     *
215     * @param sitemapUpdates a map containing changed sitemap configurations indexed by structure id (the map values are null if the corresponding sitemap configuration is not valid or could not be found)
216     * @param moduleUpdates the list of *all* module configurations, or null if no module configuration update is needed
217     * @param elementViewUpdates the updated element views, or null if no update needed
218     * @param sitePluginUpdates the new map of site plugins, or null if no update needed
219     * @param attributeEditorConfigurations the sitemap attribute editor configurations
220     *
221     * @return the new configuration state
222     */
223    public CmsADEConfigCacheState createUpdatedCopy(
224        Map<CmsUUID, CmsADEConfigDataInternal> sitemapUpdates,
225        List<CmsADEConfigDataInternal> moduleUpdates,
226        Map<CmsUUID, CmsElementView> elementViewUpdates,
227        Map<CmsUUID, CmsSitePlugin> sitePluginUpdates,
228        Map<CmsUUID, CmsSitemapAttributeEditorConfiguration> attributeEditorConfigurations) {
229
230        Map<CmsUUID, CmsADEConfigDataInternal> newSitemapConfigs = Maps.newHashMap(m_siteConfigurations);
231        if (sitemapUpdates != null) {
232            for (Map.Entry<CmsUUID, CmsADEConfigDataInternal> entry : sitemapUpdates.entrySet()) {
233                CmsUUID key = entry.getKey();
234                CmsADEConfigDataInternal value = entry.getValue();
235                if (value != null) {
236                    newSitemapConfigs.put(key, value);
237                } else {
238                    newSitemapConfigs.remove(key);
239                }
240            }
241        }
242        List<CmsADEConfigDataInternal> newModuleConfigs = m_moduleConfigurations;
243        if (moduleUpdates != null) {
244            newModuleConfigs = moduleUpdates;
245        }
246        Map<CmsUUID, CmsElementView> newElementViews = m_elementViews;
247        if (elementViewUpdates != null) {
248            newElementViews = elementViewUpdates;
249        }
250
251        Map<CmsUUID, CmsSitePlugin> newSitePlugins = m_sitePlugins;
252        if (sitePluginUpdates != null) {
253            newSitePlugins = sitePluginUpdates;
254        }
255
256        Map<CmsUUID, CmsSitemapAttributeEditorConfiguration> newAttributeEditorConfigs = m_sitemapAttributeEditorConfigurations;
257        if (attributeEditorConfigurations != null) {
258            newAttributeEditorConfigs = attributeEditorConfigurations;
259        }
260
261        return new CmsADEConfigCacheState(
262            m_cms,
263            newSitemapConfigs,
264            newModuleConfigs,
265            newElementViews,
266            newSitePlugins,
267            newAttributeEditorConfigs);
268    }
269
270    /**
271     * Gets the sitemap attribute editor configuration with the given id (or null, if there isn't one).
272     *
273     * @param id the structure id of an attribute editor configuration
274     * @return the attribute editor configuration for the id
275     */
276    public CmsSitemapAttributeEditorConfiguration getAttributeEditorConfiguration(CmsUUID id) {
277
278        return m_sitemapAttributeEditorConfigurations.get(id);
279    }
280
281    /**
282     * Gets a map of the values of a specific sitemap attribute for each subsite.
283     *
284     * <p>The keys of the map are the root paths of the subsite, and the values are the attribute values.
285     *
286     * @param attribute the attribute we want to get
287     *
288     * @return the map of attribute values
289     */
290    public Map<String, String> getAttributeValuesByPath(String attribute) {
291
292        try {
293            return m_attributeValuesByPathCache.get(attribute, () -> {
294                Map<String, String> result = new HashMap<>();
295                for (String path : m_siteConfigurationsByPath.keySet()) {
296                    CmsADEConfigData config = lookupConfiguration(path);
297                    String value = config.getAttribute(attribute, null);
298                    if (value != null) {
299                        result.put(path, value);
300                    }
301                }
302                return Collections.unmodifiableMap(result);
303            });
304        } catch (ExecutionException e) {
305            // shouldn't happen
306            LOG.error(e.getLocalizedMessage(), e);
307            return Collections.emptyMap();
308        }
309    }
310
311    /**
312     * Gets the set of content types configured anywhere in sitemap configurations.
313     *
314     * @return the set of content types
315     */
316    public Set<String> getContentTypes() {
317
318        if (m_contentTypes == null) {
319            Set<String> contentTypes = new HashSet<>();
320            for (CmsADEConfigDataInternal config : m_siteConfigurations.values()) {
321                for (CmsResourceTypeConfig typeConfig : config.getOwnResourceTypes()) {
322                    contentTypes.add(typeConfig.getTypeName());
323                }
324            }
325            for (CmsResourceTypeConfig typeConfig : m_moduleConfiguration.getOwnResourceTypes()) {
326                contentTypes.add(typeConfig.getTypeName());
327            }
328            m_contentTypes = Collections.unmodifiableSet(contentTypes);
329        }
330        return m_contentTypes;
331    }
332
333    /**
334     * Gets the detail page information for everything.<p>
335     *
336     * @param cms the current CMS context
337     * @return the list containing all detail information
338     */
339    public List<DetailInfo> getDetailInfosForSubsites(CmsObject cms) {
340
341        List<DetailInfo> result = Lists.newArrayList();
342        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
343            List<DetailInfo> infosForSubsite = wrap(configData).getDetailInfos(cms);
344            result.addAll(infosForSubsite);
345        }
346        return result;
347    }
348
349    /**
350     * Gets the set of type names for which detail pages are configured in any sitemap configuration.<p>
351     *
352     * @return the set of type names with configured detail pages
353     */
354    public Set<String> getDetailPageTypes() {
355
356        if (m_detailPageTypes != null) {
357            return m_detailPageTypes;
358        }
359        Set<String> result = new HashSet<String>();
360        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
361            List<CmsDetailPageInfo> detailPageInfos = configData.getOwnDetailPages();
362            for (CmsDetailPageInfo info : detailPageInfos) {
363                result.add(info.getType());
364            }
365        }
366        m_detailPageTypes = result;
367        return result;
368    }
369
370    /**
371     * Returns the element views.<p>
372     *
373     * @return the element views
374     */
375    public Map<CmsUUID, CmsElementView> getElementViews() {
376
377        return Collections.unmodifiableMap(m_elementViews);
378    }
379
380    /**
381     * Gets the map of folder types.<p>
382     *
383     * @return the map of folder types
384     */
385    public SetMultimap<String, String> getFolderTypes() {
386
387        return Multimaps.unmodifiableSetMultimap(m_folderTypes);
388    }
389
390    public Set<String> getParentFolderTypes(String rootPath) {
391
392        String parent = CmsResource.getParentFolder(rootPath);
393        if (parent == null) {
394            return Collections.emptySet();
395        }
396        return getFolderTypes().get(parent);
397
398    }
399
400    /**
401     * Gets the raw detail page information, with no existence checks or path corrections.
402     *
403     * @return the detail page information
404     */
405    public List<CmsDetailPageInfo> getRawDetailPages() {
406
407        List<CmsDetailPageInfo> result = new ArrayList<>();
408        for (CmsADEConfigDataInternal config : m_siteConfigurationsByPath.values()) {
409            result.addAll(config.getOwnDetailPages());
410        }
411        return result;
412    }
413
414    /**
415     * Returns the root paths to all configured sites and sub sites.<p>
416     *
417     * @return the root paths to all configured sites and sub sites
418     */
419    public Set<String> getSiteConfigurationPaths() {
420
421        return Collections.unmodifiableSet(m_siteConfigurationsByPath.keySet());
422    }
423
424    /**
425     * The map of site plugins, by structure id.
426     *
427     * @return the map of site plugins
428     */
429    public Map<CmsUUID, CmsSitePlugin> getSitePlugins() {
430
431        return m_sitePlugins;
432    }
433
434    /**
435     * Gets subsites to be included in the site selector.
436     *
437     * @return the list of root paths of subsites that should be included in the site selector
438     */
439    public List<String> getSubsitesForSiteSelector() {
440
441        if (m_subsitesForSiteSelector == null) {
442            List<String> paths = m_siteConfigurations.values().stream().filter(
443                conf -> conf.isIncludeInSiteSelector()).map(conf -> conf.getBasePath()).collect(Collectors.toList());
444            m_subsitesForSiteSelector = Collections.unmodifiableList(paths);
445        }
446        return m_subsitesForSiteSelector;
447    }
448
449    /**
450     * Helper method to retrieve the parent folder type or <code>null</code> if none available.<p>
451     *
452     * @param rootPath the path of a resource
453     * @return the parent folder content type
454     */
455    public boolean hasParentFolderType(String rootPath) {
456
457        String parent = CmsResource.getParentFolder(rootPath);
458        if (parent == null) {
459            return false;
460        }
461        return m_folderTypes.containsKey(parent);
462    }
463
464    /**
465     * Looks up the sitemap configuration for a root path.<p>
466     * @param rootPath the root path for which to look up the configuration
467     *
468     * @return the sitemap configuration for the given root path
469     */
470    public CmsADEConfigData lookupConfiguration(String rootPath) {
471
472        CmsADEConfigDataInternal internalSiteConfig = getSiteConfigData(rootPath);
473        CmsADEConfigData result;
474        if (internalSiteConfig == null) {
475            result = wrap(m_moduleConfiguration);
476        } else {
477            result = wrap(internalSiteConfig);
478        }
479        return result;
480    }
481
482    /**
483     * Gets all detail page info beans which are defined anywhere in the configuration.<p>
484     *
485     * @return the list of detail page info beans
486     */
487    protected List<CmsDetailPageInfo> getAllDetailPages() {
488
489        List<CmsDetailPageInfo> result = new ArrayList<CmsDetailPageInfo>();
490        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
491            result.addAll(wrap(configData).getAllDetailPages(true));
492        }
493        return result;
494    }
495
496    /**
497     * Gets the CMS context used for VFS operations.<p>
498     *
499     * @return the CMS context used for VFS operations
500     */
501    protected CmsObject getCms() {
502
503        return m_cms;
504    }
505
506    /**
507     * Gets all the detail pages for a given type.<p>
508     *
509     * @param type the name of the type
510     *
511     * @return the detail pages for that type
512     */
513    protected List<String> getDetailPages(String type) {
514
515        List<String> result = m_detailPageCache.get(type);
516        if (result == null) {
517            result = new ArrayList<>();
518            for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
519                for (CmsDetailPageInfo pageInfo : wrap(configData).getDetailPagesForType(type)) {
520                    result.add(pageInfo.getUri());
521                }
522            }
523            m_detailPageCache.put(type, result);
524        }
525        return Collections.unmodifiableList(result);
526    }
527
528    /**
529     * Gets the merged module configuration.<p>
530     * @return the merged module configuration instance
531     */
532    protected CmsADEConfigData getModuleConfiguration() {
533
534        return wrap(m_moduleConfiguration);
535    }
536
537    /**
538     * Helper method for getting the best matching sitemap configuration object for a given root path, ignoring the module
539     * configuration.<p>
540     *
541     * For example, if there are configurations available for the paths /a, /a/b/c, /a/b/x and /a/b/c/d/e, then
542     * the method will return the configuration object for /a/b/c when passed the path /a/b/c/d.
543     *
544     * If no configuration data is found for the path, null will be returned.<p>
545     *
546     * @param path a root path
547     * @return the configuration data for the given path, or null if none was found
548     */
549    protected CmsADEConfigDataInternal getSiteConfigData(String path) {
550
551        if (path == null) {
552            return null;
553        }
554        List<String> prefixes = getSiteConfigPaths(path);
555        if (prefixes.size() == 0) {
556            return null;
557        }
558        // for any two prefixes of a string, one is a prefix of the other. so the alphabetically last
559        // prefix is the longest prefix of all.
560        return m_siteConfigurationsByPath.get(prefixes.get(prefixes.size() - 1));
561    }
562
563    /**
564     * Finds the paths of sitemap configuration base paths above a given path.<p>
565     *
566     * @param path the path for which to find the base paths of all valid sitemap configurations
567     *
568     * @return the list of base paths
569     */
570    protected List<String> getSiteConfigPaths(String path) {
571
572        String normalizedPath = CmsStringUtil.joinPaths("/", path, "/");
573        List<String> prefixes = new ArrayList<String>();
574
575        List<String> parents = new ArrayList<String>();
576        String currentPath = normalizedPath;
577        while (currentPath != null) {
578            parents.add(currentPath);
579            currentPath = CmsResource.getParentFolder(currentPath);
580        }
581
582        for (String parent : parents) {
583            if (m_siteConfigurationsByPath.containsKey(parent)) {
584                prefixes.add(parent);
585            }
586        }
587        Collections.sort(prefixes);
588        return prefixes;
589    }
590
591    /**
592     * Checks whether the given resource is configured as a detail page.<p>
593     *
594     * @param cms the current CMS context
595     * @param resource the resource to test
596     *
597     * @return true if the resource is configured as a detail page
598     */
599    protected boolean isDetailPage(CmsObject cms, CmsResource resource) {
600
601        if (CmsResourceTypeXmlContainerPage.isContainerPage(resource)) {
602            Set<CmsUUID> detailPageIds = m_detailPageIdCache.get();
603            if (detailPageIds.contains(resource.getStructureId())) {
604                // page may have been created/replaced after the detail page id cache was generated,
605                // so we don't just return false if it doesn't contain the id.
606                // instead we check the parent folder too in the next step.
607                return true;
608            }
609
610            try {
611                CmsResource parent = getCms().readResource(
612                    CmsResource.getParentFolder(resource.getRootPath()),
613                    CmsResourceFilter.ALL);
614                return detailPageIds.contains(parent.getStructureId());
615
616            } catch (Exception e) {
617                LOG.info(e.getLocalizedMessage(), e);
618                return false;
619            }
620        } else if (resource.isFolder()) {
621            return m_detailPageIdCache.get().contains(resource.getStructureId());
622        } else {
623            return false;
624        }
625    }
626
627    /**
628     * Merges a list of multiple configuration objects into a single configuration object.<p>
629     *
630     * @param configurations the list of configuration objects.<p>
631     *
632     * @return the merged configuration object
633     */
634    protected CmsADEConfigDataInternal mergeConfigurations(List<CmsADEConfigDataInternal> configurations) {
635
636        if (configurations.isEmpty()) {
637            return new CmsADEConfigDataInternal(null);
638        }
639        for (int i = 0; i < (configurations.size() - 1); i++) {
640            configurations.get(i + 1).mergeParent(configurations.get(i));
641        }
642        CmsADEConfigDataInternal result = configurations.get(configurations.size() - 1);
643        result.processModuleOrdering();
644        return result;
645    }
646
647    /**
648     * Internal method for collecting structure ids of all configured detail pages and their parent folders.
649     *
650     * @return the structure ids of configured detail pages and their parent folders
651     */
652    private Set<CmsUUID> collectDetailPageIds() {
653
654        List<CmsDetailPageInfo> allDetailPages = new ArrayList<CmsDetailPageInfo>();
655        // First collect all detail page infos
656        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
657            List<CmsDetailPageInfo> detailPageInfos = configData.getOwnDetailPages();
658            allDetailPages.addAll(detailPageInfos);
659        }
660        Set<CmsUUID> detailPageOrDetailPageFolderIds = new HashSet<>();
661        for (CmsDetailPageInfo detailPageInfo : allDetailPages) {
662            try {
663                CmsResource detailPageRes = getCms().readResource(detailPageInfo.getId(), CmsResourceFilter.ALL);
664                detailPageOrDetailPageFolderIds.add(detailPageInfo.getId());
665                if (detailPageRes.isFile()) {
666                    CmsResource parent = getCms().readParentFolder(detailPageInfo.getId());
667                    detailPageOrDetailPageFolderIds.add(parent.getStructureId());
668                } else {
669                    CmsResource defaultfile = getCms().readDefaultFile("" + detailPageInfo.getId());
670                    if (defaultfile != null) {
671                        detailPageOrDetailPageFolderIds.add(defaultfile.getStructureId());
672                    }
673                }
674            } catch (Exception e) {
675                LOG.info(e.getLocalizedMessage(), e);
676            }
677        }
678        return Collections.unmodifiableSet(detailPageOrDetailPageFolderIds);
679    }
680
681    /**
682     * For a given master configuration, lists all directly and indirectly referenced master configurations, in sitemap config inheritance order (i.e. referenced master configurations preceding the
683     * configurations from which they are referenced).
684     *
685     * @param result the list to append the results to
686     * @param current the configuration reference to start with
687     * @param seen the set of structure ids of sitemap configurations already visited
688     */
689    private void fillMasterConfigurations(
690        List<ConfigReferenceInstance> result,
691        ConfigReferenceInstance current,
692        Set<CmsUUID> seen) {
693
694        CmsUUID currentId = current.getConfig().getResource().getStructureId();
695        if (seen.contains(currentId)) {
696            LOG.warn("Loop in sitemap configuration references, target = " + current.getConfig().getBasePath());
697            return;
698        }
699        seen.add(currentId);
700        // Recursively add the referenced master configurations before adding the current configuration in the end
701        for (ConfigReference configRef : current.getConfig().getMasterConfigs()) {
702            CmsADEConfigDataInternal config = m_siteConfigurations.get(configRef.getId());
703            if (config != null) {
704                ConfigReferenceMeta combinedMeta = current.getMeta().combine(configRef.getMeta());
705                ConfigReferenceInstance combinedRef = new ConfigReferenceInstance(config, combinedMeta);
706                fillMasterConfigurations(result, combinedRef, seen);
707            } else {
708                LOG.warn(
709                    "Master configuration with id "
710                        + configRef.getId()
711                        + " not found, referenced by "
712                        + current.getConfig().getResource().getRootPath());
713            }
714        }
715        result.add(current);
716        seen.remove(currentId);
717    }
718
719    /**
720     * Wraps the internal config data into a bean which manages the lookup of inherited configurations.<p>
721     *
722     * @param data the config data to wrap
723     *
724     * @return the wrapper object
725     */
726    private CmsADEConfigData wrap(CmsADEConfigDataInternal data) {
727
728        String path = data.getBasePath();
729        List<ConfigReferenceInstance> configList = Lists.newArrayList();
730        configList.add(new ConfigReferenceInstance(m_moduleConfiguration));
731        if (path != null) {
732            List<String> siteConfigPaths = getSiteConfigPaths(path);
733            for (String siteConfigPath : siteConfigPaths) {
734                CmsADEConfigDataInternal currentConfig = m_siteConfigurationsByPath.get(siteConfigPath);
735                fillMasterConfigurations(configList, new ConfigReferenceInstance(currentConfig), new HashSet<>());
736            }
737        }
738        CmsADEConfigData result = new CmsADEConfigData(data, this, new CmsADEConfigurationSequence(configList));
739        List<I_CmsSitemapExtraInfo> extraInfo = new ArrayList<>();
740        for (I_CmsSitemapExtraInfoProvider provider : OpenCms.getADEManager().getSitemapExtraInfoProviders()) {
741            extraInfo.add(provider.getExtraInfo(m_cms));
742        }
743        result.m_extraInfo = extraInfo;
744        return result;
745    }
746}