001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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.CmsADEConfigData.DetailInfo;
031import org.opencms.ade.detailpage.CmsDetailPageInfo;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.file.types.CmsResourceTypeXmlContainerPage;
035import org.opencms.main.CmsException;
036import org.opencms.main.CmsLog;
037import org.opencms.util.CmsStringUtil;
038import org.opencms.util.CmsUUID;
039
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.HashMap;
043import java.util.HashSet;
044import java.util.List;
045import java.util.Map;
046import java.util.Set;
047
048import org.apache.commons.logging.Log;
049
050import com.google.common.collect.Lists;
051import com.google.common.collect.Maps;
052
053/**
054 * An immutable object which represents the complete ADE configuration (sitemap and module configurations)
055 * at a certain instant in time.<p>
056 */
057public class CmsADEConfigCacheState {
058
059    /** The logger instance for this class. */
060    private static final Log LOG = CmsLog.getLog(CmsADEConfigCacheState.class);
061
062    /** The CMS context used for VFS operations. */
063    private CmsObject m_cms;
064
065    /** Cached detail page types. */
066    private volatile Set<String> m_detailPageTypes;
067
068    /** The available element views. */
069    private Map<CmsUUID, CmsElementView> m_elementViews;
070
071    /** The cached content types for folders. */
072    private Map<String, String> m_folderTypes = new HashMap<String, String>();
073
074    /** The merged configuration from all the modules. */
075    private CmsADEConfigDataInternal m_moduleConfiguration;
076
077    /** The list of module configurations. */
078    private List<CmsADEConfigDataInternal> m_moduleConfigurations;
079
080    /** The map of sitemap configurations by structure id. */
081    private Map<CmsUUID, CmsADEConfigDataInternal> m_siteConfigurations = new HashMap<CmsUUID, CmsADEConfigDataInternal>();
082
083    /** The configurations from the sitemap / VFS. */
084    private Map<String, CmsADEConfigDataInternal> m_siteConfigurationsByPath = new HashMap<String, CmsADEConfigDataInternal>();
085
086    /**
087     * Creates a new configuration cache state.<p>
088     *
089     * @param cms the CMS context to use
090     * @param siteConfigurations the map of sitemap configuration beans by structure id
091     * @param moduleConfigs the complete list of module configurations
092     * @param elementViews the available element views
093     */
094    public CmsADEConfigCacheState(
095        CmsObject cms,
096        Map<CmsUUID, CmsADEConfigDataInternal> siteConfigurations,
097        List<CmsADEConfigDataInternal> moduleConfigs,
098        Map<CmsUUID, CmsElementView> elementViews) {
099
100        m_cms = cms;
101        m_siteConfigurations = siteConfigurations;
102        m_moduleConfigurations = moduleConfigs;
103        m_elementViews = elementViews;
104        for (CmsADEConfigDataInternal data : siteConfigurations.values()) {
105            if (data.getBasePath() != null) {
106                // In theory, the base path should never be null
107                m_siteConfigurationsByPath.put(data.getBasePath(), data);
108            } else {
109                LOG.info("Empty base path for sitemap configuration: " + data.getResource().getRootPath());
110            }
111        }
112        m_moduleConfiguration = mergeConfigurations(moduleConfigs);
113        try {
114            m_folderTypes = computeFolderTypes();
115        } catch (Exception e) {
116            m_folderTypes = Maps.newHashMap();
117            LOG.error(e.getLocalizedMessage(), e);
118        }
119    }
120
121    /**
122     * Creates an empty ADE configuration cache state.<p>
123     *
124     * @param cms the CMS context
125     * @return the empty configuration cache state
126     */
127    public static CmsADEConfigCacheState emptyState(CmsObject cms) {
128
129        return new CmsADEConfigCacheState(
130            cms,
131            Collections.<CmsUUID, CmsADEConfigDataInternal> emptyMap(),
132            Collections.<CmsADEConfigDataInternal> emptyList(),
133            Collections.<CmsUUID, CmsElementView> emptyMap());
134    }
135
136    /**
137     * Computes the map from folder paths to content types for this ADE configuration state.<p>
138     *
139     * @return the map of content types by folder root paths
140     *
141     * @throws CmsException if something goes wrong
142     */
143    public Map<String, String> computeFolderTypes() throws CmsException {
144
145        Map<String, String> folderTypes = Maps.newHashMap();
146        // do this first, since folder types from modules should be overwritten by folder types from sitemaps
147        if (m_moduleConfiguration != null) {
148            folderTypes.putAll(wrap(m_moduleConfiguration).getFolderTypes());
149        }
150
151        List<CmsADEConfigDataInternal> configDataObjects = new ArrayList<CmsADEConfigDataInternal>(
152            m_siteConfigurationsByPath.values());
153        for (CmsADEConfigDataInternal configData : configDataObjects) {
154            folderTypes.putAll(wrap(configData).getFolderTypes());
155        }
156        return folderTypes;
157    }
158
159    /**
160     * Creates a new object which represents the changed configuration state given some updates, without
161     * changing the current configuration state (this object instance).
162     *
163     * @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)
164     * @param moduleUpdates the list of *all* module configurations, or null if no module configuration update is needed
165     * @param elementViewUpdates the updated element views, or null if no update needed
166     *
167     * @return the new configuration state
168     */
169    public CmsADEConfigCacheState createUpdatedCopy(
170        Map<CmsUUID, CmsADEConfigDataInternal> sitemapUpdates,
171        List<CmsADEConfigDataInternal> moduleUpdates,
172        Map<CmsUUID, CmsElementView> elementViewUpdates) {
173
174        Map<CmsUUID, CmsADEConfigDataInternal> newSitemapConfigs = Maps.newHashMap(m_siteConfigurations);
175        if (sitemapUpdates != null) {
176            for (Map.Entry<CmsUUID, CmsADEConfigDataInternal> entry : sitemapUpdates.entrySet()) {
177                CmsUUID key = entry.getKey();
178                CmsADEConfigDataInternal value = entry.getValue();
179                if (value != null) {
180                    newSitemapConfigs.put(key, value);
181                } else {
182                    newSitemapConfigs.remove(key);
183                }
184            }
185        }
186        List<CmsADEConfigDataInternal> newModuleConfigs = m_moduleConfigurations;
187        if (moduleUpdates != null) {
188            newModuleConfigs = moduleUpdates;
189        }
190        Map<CmsUUID, CmsElementView> newElementViews = m_elementViews;
191        if (elementViewUpdates != null) {
192            newElementViews = elementViewUpdates;
193        }
194
195        return new CmsADEConfigCacheState(m_cms, newSitemapConfigs, newModuleConfigs, newElementViews);
196    }
197
198    /**
199     * Gets the detail page information for everything.<p>
200     *
201     * @param cms the current CMS context
202     * @return the list containing all detail information
203     */
204    public List<DetailInfo> getDetailInfosForSubsites(CmsObject cms) {
205
206        List<DetailInfo> result = Lists.newArrayList();
207        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
208            List<DetailInfo> infosForSubsite = wrap(configData).getDetailInfos(cms);
209            result.addAll(infosForSubsite);
210        }
211        return result;
212    }
213
214    /**
215     * Gets the set of type names for which detail pages are configured in any sitemap configuration.<p>
216     *
217     * @return the set of type names with configured detail pages
218     */
219    public Set<String> getDetailPageTypes() {
220
221        if (m_detailPageTypes != null) {
222            return m_detailPageTypes;
223        }
224        Set<String> result = new HashSet<String>();
225        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
226            List<CmsDetailPageInfo> detailPageInfos = configData.getOwnDetailPages();
227            for (CmsDetailPageInfo info : detailPageInfos) {
228                result.add(info.getType());
229            }
230        }
231        m_detailPageTypes = result;
232        return result;
233    }
234
235    /**
236     * Returns the element views.<p>
237     *
238     * @return the element views
239     */
240    public Map<CmsUUID, CmsElementView> getElementViews() {
241
242        return Collections.unmodifiableMap(m_elementViews);
243    }
244
245    /**
246     * Gets the map of folder types.<p>
247     *
248     * @return the map of folder types
249     */
250    public Map<String, String> getFolderTypes() {
251
252        return Collections.unmodifiableMap(m_folderTypes);
253    }
254
255    /**
256     * Helper method to retrieve the parent folder type or <code>null</code> if none available.<p>
257     *
258     * @param rootPath the path of a resource
259     * @return the parent folder content type
260     */
261    public String getParentFolderType(String rootPath) {
262
263        String parent = CmsResource.getParentFolder(rootPath);
264        if (parent == null) {
265            return null;
266        }
267        String type = m_folderTypes.get(parent);
268        // type may be null
269        return type;
270    }
271
272    /**
273     * Returns the root paths to all configured sites and sub sites.<p>
274     *
275     * @return the root paths to all configured sites and sub sites
276     */
277    public Set<String> getSiteConfigurationPaths() {
278
279        return m_siteConfigurationsByPath.keySet();
280    }
281
282    /**
283     * Looks up the sitemap configuration for a root path.<p>
284     * @param rootPath the root path for which to look up the configuration
285     *
286     * @return the sitemap configuration for the given root path
287     */
288    public CmsADEConfigData lookupConfiguration(String rootPath) {
289
290        CmsADEConfigDataInternal internalSiteConfig = getSiteConfigData(rootPath);
291        CmsADEConfigData result;
292        if (internalSiteConfig == null) {
293            result = wrap(m_moduleConfiguration);
294        } else {
295            result = wrap(internalSiteConfig);
296        }
297        return result;
298    }
299
300    /**
301     * Gets all detail page info beans which are defined anywhere in the configuration.<p>
302     *
303     * @return the list of detail page info beans
304     */
305    protected List<CmsDetailPageInfo> getAllDetailPages() {
306
307        List<CmsDetailPageInfo> result = new ArrayList<CmsDetailPageInfo>();
308        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
309            result.addAll(wrap(configData).getAllDetailPages(true));
310        }
311        return result;
312    }
313
314    /**
315     * Gets the CMS context used for VFS operations.<p>
316     *
317     * @return the CMS context used for VFS operations
318     */
319    protected CmsObject getCms() {
320
321        return m_cms;
322    }
323
324    /**
325     * Gets all the detail pages for a given type.<p>
326     *
327     * @param type the name of the type
328     *
329     * @return the detail pages for that type
330     */
331    protected List<String> getDetailPages(String type) {
332
333        List<String> result = new ArrayList<String>();
334        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
335            for (CmsDetailPageInfo pageInfo : wrap(configData).getDetailPagesForType(type)) {
336                result.add(pageInfo.getUri());
337            }
338        }
339        return result;
340    }
341
342    /**
343     * Gets the merged module configuration.<p>
344     * @return the merged module configuration instance
345     */
346    protected CmsADEConfigData getModuleConfiguration() {
347
348        return wrap(m_moduleConfiguration);
349    }
350
351    /**
352     * Helper method for getting the best matching sitemap configuration object for a given root path, ignoring the module
353     * configuration.<p>
354     *
355     * For example, if there are configurations available for the paths /a, /a/b/c, /a/b/x and /a/b/c/d/e, then
356     * the method will return the configuration object for /a/b/c when passed the path /a/b/c/d.
357     *
358     * If no configuration data is found for the path, null will be returned.<p>
359     *
360     * @param path a root path
361     * @return the configuration data for the given path, or null if none was found
362     */
363    protected CmsADEConfigDataInternal getSiteConfigData(String path) {
364
365        if (path == null) {
366            return null;
367        }
368        List<String> prefixes = getSiteConfigPaths(path);
369        if (prefixes.size() == 0) {
370            return null;
371        }
372        // for any two prefixes of a string, one is a prefix of the other. so the alphabetically last
373        // prefix is the longest prefix of all.
374        return m_siteConfigurationsByPath.get(prefixes.get(prefixes.size() - 1));
375    }
376
377    /**
378     * Finds the paths of sitemap configuration base paths above a given path.<p>
379     *
380     * @param path the path for which to find the base paths of all valid sitemap configurations
381     *
382     * @return the list of base paths
383     */
384    protected List<String> getSiteConfigPaths(String path) {
385
386        String normalizedPath = CmsStringUtil.joinPaths("/", path, "/");
387        List<String> prefixes = new ArrayList<String>();
388
389        List<String> parents = new ArrayList<String>();
390        String currentPath = normalizedPath;
391        while (currentPath != null) {
392            parents.add(currentPath);
393            currentPath = CmsResource.getParentFolder(currentPath);
394        }
395
396        for (String parent : parents) {
397            if (m_siteConfigurationsByPath.containsKey(parent)) {
398                prefixes.add(parent);
399            }
400        }
401        Collections.sort(prefixes);
402        return prefixes;
403    }
404
405    /**
406     * Checks whether the given resource is configured as a detail page.<p>
407     *
408     * @param cms the current CMS context
409     * @param resource the resource to test
410     *
411     * @return true if the resource is configured as a detail page
412     */
413    protected boolean isDetailPage(CmsObject cms, CmsResource resource) {
414
415        CmsResource folder;
416        if (resource.isFile()) {
417            if (!CmsResourceTypeXmlContainerPage.isContainerPage(resource)) {
418                return false;
419            }
420            try {
421                folder = getCms().readResource(CmsResource.getParentFolder(resource.getRootPath()));
422            } catch (CmsException e) {
423                LOG.debug(e.getLocalizedMessage(), e);
424                return false;
425            }
426        } else {
427            folder = resource;
428        }
429        List<CmsDetailPageInfo> allDetailPages = new ArrayList<CmsDetailPageInfo>();
430        // First collect all detail page infos
431        for (CmsADEConfigDataInternal configData : m_siteConfigurationsByPath.values()) {
432            List<CmsDetailPageInfo> detailPageInfos = wrap(configData).getAllDetailPages();
433            allDetailPages.addAll(detailPageInfos);
434        }
435        // First pass: check if the structure id or path directly match one of the configured detail pages.
436        for (CmsDetailPageInfo info : allDetailPages) {
437            if (folder.getStructureId().equals(info.getId())
438                || folder.getRootPath().equals(info.getUri())
439                || resource.getStructureId().equals(info.getId())
440                || resource.getRootPath().equals(info.getUri())) {
441                return true;
442            }
443        }
444        // Second pass: configured detail pages may be actual container pages rather than folders
445        String normalizedFolderRootPath = CmsStringUtil.joinPaths(folder.getRootPath(), "/");
446        for (CmsDetailPageInfo info : allDetailPages) {
447            String parentPath = CmsResource.getParentFolder(info.getUri());
448            if (parentPath != null) {
449                String normalizedParentPath = CmsStringUtil.joinPaths(parentPath, "/");
450                if (normalizedParentPath.equals(normalizedFolderRootPath)) {
451                    try {
452                        CmsResource infoResource = getCms().readResource(info.getId());
453                        if (infoResource.isFile()) {
454                            return true;
455                        }
456                    } catch (CmsException e) {
457                        LOG.warn(e.getLocalizedMessage(), e);
458                    }
459                }
460            }
461        }
462        return false;
463    }
464
465    /**
466     * Merges a list of multiple configuration objects into a single configuration object.<p>
467     *
468     * @param configurations the list of configuration objects.<p>
469     *
470     * @return the merged configuration object
471     */
472    protected CmsADEConfigDataInternal mergeConfigurations(List<CmsADEConfigDataInternal> configurations) {
473
474        if (configurations.isEmpty()) {
475            return new CmsADEConfigDataInternal(null);
476        }
477        for (int i = 0; i < (configurations.size() - 1); i++) {
478            configurations.get(i + 1).mergeParent(configurations.get(i));
479        }
480        CmsADEConfigDataInternal result = configurations.get(configurations.size() - 1);
481        result.processModuleOrdering();
482        return result;
483    }
484
485    /**
486     * Wraps the internal config data into a bean which manages the lookup of inherited configurations.<p>
487     *
488     * @param data the config data to wrap
489     *
490     * @return the wrapper object
491     */
492    private CmsADEConfigData wrap(CmsADEConfigDataInternal data) {
493
494        String path = data.getBasePath();
495        List<CmsADEConfigDataInternal> configList = Lists.newArrayList();
496        configList.add(m_moduleConfiguration);
497        if (path != null) {
498            List<String> siteConfigPaths = getSiteConfigPaths(path);
499            for (String siteConfigPath : siteConfigPaths) {
500                CmsADEConfigDataInternal currentConfig = m_siteConfigurationsByPath.get(siteConfigPath);
501                CmsResource masterConfigResource = currentConfig.getMasterConfig();
502                if (currentConfig.getMasterConfig() != null) {
503                    CmsADEConfigDataInternal masterConfig = m_siteConfigurations.get(
504                        masterConfigResource.getStructureId());
505                    if (masterConfig != null) {
506                        configList.add(masterConfig);
507                    } else {
508                        LOG.warn(
509                            "Master configuration "
510                                + masterConfigResource.getRootPath()
511                                + " not found for sitemap configuration in "
512                                + currentConfig.getBasePath());
513                    }
514                }
515                configList.add(currentConfig);
516            }
517        }
518        return new CmsADEConfigData(data, this, new CmsADEConfigurationSequence(configList));
519    }
520}