/* * * This library is part of OpenCms - * the Open Source Content Management System * * Copyright (C) Alkacon Software (http://www.alkacon.com) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * For further information about Alkacon Software, please see the * company website: http://www.alkacon.com * * For further information about OpenCms, please see the * project website: http://www.opencms.org * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.opencms.ade.configuration; import org.opencms.ade.detailpage.CmsDetailPageInfo; import org.opencms.db.CmsPublishedResource; import org.opencms.db.CmsResourceState; import org.opencms.file.CmsObject; import org.opencms.file.CmsResource; import org.opencms.file.CmsResourceFilter; import org.opencms.file.types.CmsResourceTypeXmlContainerPage; import org.opencms.file.types.I_CmsResourceType; import org.opencms.main.CmsException; import org.opencms.main.CmsLog; import org.opencms.main.CmsRuntimeException; import org.opencms.util.CmsStringUtil; import org.opencms.util.CmsUUID; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; /** * This is the internal cache class used for storing configuration data. It is not public because it is only meant * for internal use.<p> * * It stores an instance of {@link CmsADEConfigData} for each active configuration file in the sitemap, * and a single instance which represents the merged configuration from all the modules. When a sitemap configuration * file is updated, only the single instance for that configuration file is updated, whereas if a module configuration file * is changed, the configuration of all modules will be read again.<p> */ class CmsConfigurationCache { /** The log instance for this class. */ private static final Log LOG = CmsLog.getLog(CmsConfigurationCache.class); /** The key that is used for the map entry which indicates that the module configuration needs to be read. */ private static final String MODULE_CONFIG_KEY = "__MODULE_CONFIG_KEY__"; /** The resource type for sitemap configurations. */ protected I_CmsResourceType m_configType; /** The resource type for module configurations. */ protected I_CmsResourceType m_moduleConfigType; /** The CMS context used for reading configuration data. */ private CmsObject m_cms; /** The configuration files which have been changed but not read yet. */ private Map<String, CmsUUID> m_configurationsToRead = new HashMap<String, CmsUUID>(); /** The cached content types for folders. */ private Map<String, String> m_folderTypes = new HashMap<String, String>(); /** The merged configuration from all the modules. */ private CmsADEConfigData m_moduleConfiguration; /** A cache which stores resources' paths by their structure IDs. */ private Map<CmsUUID, String> m_pathCache = Collections.synchronizedMap(new HashMap<CmsUUID, String>()); /** The configurations from the sitemap / VFS. */ private Map<String, CmsADEConfigData> m_siteConfigurations = new HashMap<String, CmsADEConfigData>(); /** * Creates a new cache instance.<p> * * @param cms the CMS object used for reading the configuration data * @param configType the sitemap configuration file type * @param moduleConfigType the module configuration file type */ public CmsConfigurationCache(CmsObject cms, I_CmsResourceType configType, I_CmsResourceType moduleConfigType) { m_cms = cms; m_configType = configType; m_moduleConfigType = moduleConfigType; } /** * Looks up the root path for a given structure id.<p> * * This is used for correcting the paths of cached resource objects.<p> * * @param structureId the structure id * @return the root path for the structure id * * @throws CmsException if the resource with the given id was not found or another error occurred */ public String getPathForStructureId(CmsUUID structureId) throws CmsException { String rootPath = m_pathCache.get(structureId); if (rootPath != null) { return rootPath; } CmsResource res = m_cms.readResource(structureId); m_pathCache.put(structureId, res.getRootPath()); return res.getRootPath(); } /** * Gets the base path for a given sitemap configuration file.<p> * * @param siteConfigFile the root path of the sitemap configuration file * * @return the base path for the sitemap configuration file */ protected String getBasePath(String siteConfigFile) { if (siteConfigFile.endsWith(CmsADEManager.CONFIG_SUFFIX)) { return CmsResource.getParentFolder(CmsResource.getParentFolder(siteConfigFile)); } return siteConfigFile; } /** * Gets all the detail pages for a given type.<p> * * @param type the name of the type * * @return the detail pages for that type */ protected synchronized List<String> getDetailPages(String type) { readRemainingConfigurations(); List<String> result = new ArrayList<String>(); for (CmsADEConfigData configData : m_siteConfigurations.values()) { for (CmsDetailPageInfo pageInfo : configData.getDetailPagesForType(type)) { result.add(pageInfo.getUri()); } } return result; } /** * Gets the merged module configuration.<p> * @return the merged module configuration instance */ protected synchronized CmsADEConfigData getModuleConfiguration() { return m_moduleConfiguration; } /** * Helper method to retrieve the parent folder type.<p> * * @param rootPath the path of a resource * @return the parent folder content type */ protected synchronized String getParentFolderType(String rootPath) { readRemainingConfigurations(); String parent = CmsResource.getParentFolder(rootPath); if (parent == null) { return null; } String type = m_folderTypes.get(parent); if (type == null) { return null; } return type; } /** * Helper method for getting the best matching sitemap configuration object for a given root path, ignoring the module * configuration.<p> * * For example, if there are configurations available for the paths /a, /a/b/c, /a/b/x and /a/b/c/d/e, then * the method will return the configuration object for /a/b/c when passed the path /a/b/c/d. * * If no configuration data is found for the path, null will be returned.<p> * * @param path a root path * @return the configuration data for the given path, or null if none was found */ protected synchronized CmsADEConfigData getSiteConfigData(String path) { if (path == null) { return null; } readRemainingConfigurations(); String normalizedPath = CmsStringUtil.joinPaths("/", path, "/"); List<String> prefixes = new ArrayList<String>(); for (String key : m_siteConfigurations.keySet()) { if (normalizedPath.startsWith(CmsStringUtil.joinPaths("/", key, "/"))) { prefixes.add(key); } } if (prefixes.size() == 0) { return null; } Collections.sort(prefixes); // for any two prefixes of a string, one is a prefix of the other. so the alphabetically last // prefix is the longest prefix of all. return m_siteConfigurations.get(prefixes.get(prefixes.size() - 1)); } /** * Initializes the cache by reading in all the configuration files.<p> */ protected synchronized void initialize() { m_siteConfigurations.clear(); try { List<CmsResource> configFileCandidates = m_cms.readResources( "/", CmsResourceFilter.DEFAULT.addRequireType(m_configType.getTypeId())); for (CmsResource candidate : configFileCandidates) { if (isSitemapConfiguration(candidate.getRootPath(), candidate.getTypeId())) { update(candidate); } } } catch (Exception e) { LOG.error(e.getLocalizedMessage(), e); } refreshModuleConfiguration(); try { initializeFolderTypes(); } catch (Exception e) { LOG.error(e.getLocalizedMessage(), e); } } /** * Initializes the cached folder types.<p> * * @throws CmsException if something goes wrong */ protected synchronized void initializeFolderTypes() throws CmsException { LOG.info("Computing folder types for detail pages..."); m_folderTypes.clear(); List<CmsADEConfigData> configDataObjects = new ArrayList<CmsADEConfigData>(m_siteConfigurations.values()); for (CmsADEConfigData configData : configDataObjects) { Map<String, String> folderTypes = configData.getFolderTypes(); m_folderTypes.putAll(folderTypes); } if (m_moduleConfiguration != null) { Map<String, String> folderTypes = m_moduleConfiguration.getFolderTypes(); m_folderTypes.putAll(folderTypes); } } /** * Checks whether the given resource is configured as a detail page.<p> * * @param cms the current CMS context * @param resource the resource to test * * @return true if the resource is configured as a detail page */ protected synchronized boolean isDetailPage(CmsObject cms, CmsResource resource) { readRemainingConfigurations(); CmsResource folder; if (resource.isFile()) { if (!CmsResourceTypeXmlContainerPage.isContainerPage(resource)) { return false; } try { folder = m_cms.readResource(CmsResource.getParentFolder(resource.getRootPath())); } catch (CmsException e) { LOG.error(e.getLocalizedMessage(), e); return false; } } else { folder = resource; } List<CmsDetailPageInfo> allDetailPages = new ArrayList<CmsDetailPageInfo>(); // First collect all detail page infos for (CmsADEConfigData configData : m_siteConfigurations.values()) { List<CmsDetailPageInfo> detailPageInfos = configData.getAllDetailPages(); allDetailPages.addAll(detailPageInfos); } // First pass: check if the structure id or path directly match one of the configured detail pages. for (CmsDetailPageInfo info : allDetailPages) { if (folder.getStructureId().equals(info.getId()) || folder.getRootPath().equals(info.getUri()) || resource.getStructureId().equals(info.getId()) || resource.getRootPath().equals(info.getUri())) { return true; } } // Second pass: configured detail pages may be actual container pages rather than folders String normalizedFolderRootPath = CmsStringUtil.joinPaths(folder.getRootPath(), "/"); for (CmsDetailPageInfo info : allDetailPages) { String parentPath = CmsResource.getParentFolder(info.getUri()); String normalizedParentPath = CmsStringUtil.joinPaths(parentPath, "/"); if (normalizedParentPath.equals(normalizedFolderRootPath)) { try { CmsResource infoResource = m_cms.readResource(info.getId()); if (infoResource.isFile()) { return true; } } catch (CmsException e) { LOG.warn(e.getLocalizedMessage(), e); } } } return false; } /** * Checks whether the given path/type combination belongs to a module configuration file.<p> * * @param rootPath the root path of the resource * @param type the type id of the resource * * @return true if the path/type combination belongs to a module configuration */ protected boolean isModuleConfiguration(String rootPath, int type) { return type == m_moduleConfigType.getTypeId(); } /** * Returns true if this an online configuration cache.<p> * * @return true if this is an online cache, false if it is an offline cache */ protected boolean isOnline() { return m_cms.getRequestContext().getCurrentProject().isOnlineProject(); } /** * Checks whether the given path/type combination belongs to a sitemap configuration.<p> * * @param rootPath the root path * @param type the resource type id * * @return true if the path/type belong to an active sitemap configuration */ protected boolean isSitemapConfiguration(String rootPath, int type) { return rootPath.endsWith(CmsADEManager.CONFIG_SUFFIX) && (type == m_configType.getTypeId()); } /** * Reloads the module configuration.<p> */ protected synchronized void refreshModuleConfiguration() { LOG.info("Refreshing module configuration."); CmsConfigurationReader reader = new CmsConfigurationReader(m_cms); m_moduleConfiguration = reader.readModuleConfigurations(); m_moduleConfiguration.initialize(m_cms); } /** * Removes a published resource from the cache.<p> * * @param res the published resource */ protected void remove(CmsPublishedResource res) { remove(res.getStructureId(), res.getRootPath(), res.getType()); } /** * Removes a resource from the cache.<p> * * @param res the resource to remove */ protected void remove(CmsResource res) { remove(res.getStructureId(), res.getRootPath(), res.getTypeId()); } /** * Removes the cache entry for the given resource data.<p> * * @param structureId the resource structure id * @param rootPath the resource root path * @param type the resource type */ protected void remove(CmsUUID structureId, String rootPath, int type) { if (CmsResource.isTemporaryFileName(rootPath)) { return; } try { updateFolderTypes(rootPath); } catch (CmsException e) { LOG.error(e.getLocalizedMessage(), e); } m_pathCache.remove(structureId); if (isSitemapConfiguration(rootPath, type)) { synchronized (this) { String basePath = getBasePath(rootPath); removePath(basePath); LOG.info("Removing config file from cache: " + rootPath); } } else if (isModuleConfiguration(rootPath, type)) { LOG.info("Removing module configuration " + rootPath); synchronized (this) { m_configurationsToRead.put(MODULE_CONFIG_KEY, CmsUUID.getNullUUID()); } } } /** * Updates the cache entry for the given published resource.<p> * * @param res a published resource */ protected void update(CmsPublishedResource res) { try { update(res.getStructureId(), res.getRootPath(), res.getType(), res.getState()); } catch (CmsRuntimeException e) { // may happen during import of org.opencms.ade.configuration module LOG.warn(e.getLocalizedMessage(), e); } } /** * Updates the cache entry for the given resource.<p> * * @param res the resource for which the cache entry should be updated */ protected void update(CmsResource res) { try { update(res.getStructureId(), res.getRootPath(), res.getTypeId(), res.getState()); } catch (CmsRuntimeException e) { // may happen during import of org.opencms.ade.configuration module LOG.warn(e.getLocalizedMessage(), e); } } /** * Updates the cache entry for the given resource data.<p> * * @param structureId the structure id of the resource * @param rootPath the root path of the resource * @param type the type id of the resource * @param state the state of the resource */ protected void update(CmsUUID structureId, String rootPath, int type, CmsResourceState state) { if (CmsResource.isTemporaryFileName(rootPath)) { return; } try { updateFolderTypes(rootPath); } catch (CmsException e) { LOG.error(e.getLocalizedMessage(), e); } synchronized (m_pathCache) { m_pathCache.remove(structureId); m_pathCache.put(structureId, rootPath); } if (isSitemapConfiguration(rootPath, type)) { synchronized (this) { // Do not update the configuration right now, because reading configuration files while handling // an event may lead to cache problems. Instead, the configuration file is read when the configuration // is queried. LOG.info("Changed configuration file " + rootPath + "(" + structureId + "), will be read later"); m_configurationsToRead.put(rootPath, structureId); } } else if (isModuleConfiguration(rootPath, type)) { LOG.info("Changed module configuration file " + rootPath + "(" + structureId + ")"); synchronized (this) { m_configurationsToRead.put(MODULE_CONFIG_KEY, CmsUUID.getNullUUID()); } } } /** * Updates the cached folder types.<p> * * @param rootPath the folder root path * @throws CmsException if something goes wrong */ protected synchronized void updateFolderTypes(String rootPath) throws CmsException { if (m_folderTypes.containsKey(rootPath)) { LOG.info("Updating folder types because of a change at " + rootPath); synchronized (this) { initializeFolderTypes(); } } } /** * Reads the configuration files which have changed but not been read yet.<p> */ private synchronized void readRemainingConfigurations() { if (m_configurationsToRead.isEmpty()) { // do not initialize folder types if there were no changes! return; } for (Map.Entry<String, CmsUUID> entry : m_configurationsToRead.entrySet()) { String rootPath = entry.getKey(); CmsUUID structureId = entry.getValue(); if (rootPath.equals(MODULE_CONFIG_KEY)) { refreshModuleConfiguration(); } else { try { // remove the original entry first, so that the configuration will be gone if reading the // configuration file fails. m_siteConfigurations.remove(rootPath); CmsResource configRes = m_cms.readResource(structureId); CmsConfigurationReader reader = new CmsConfigurationReader(m_cms); LOG.info("Reading configuration file " + rootPath + "(" + structureId + ")"); String basePath = getBasePath(rootPath); CmsADEConfigData configData = reader.parseSitemapConfiguration(basePath, configRes); configData.initialize(m_cms); m_siteConfigurations.put(basePath, configData); } catch (CmsException e) { LOG.warn(e.getLocalizedMessage(), e); } catch (CmsRuntimeException e) { LOG.warn(e.getLocalizedMessage(), e); } } } m_configurationsToRead.clear(); // Methods which recursively call this method must be called after this point, // because it will lead to an infinite recursion otherwise. try { initializeFolderTypes(); } catch (CmsException e) { LOG.warn(e.getLocalizedMessage(), e); } catch (CmsRuntimeException e) { LOG.warn(e.getLocalizedMessage(), e); } } /** * Remove a sitemap configuration from the cache by its base path.<p> * * @param rootPath the base path for the sitemap configuration */ private void removePath(String rootPath) { m_configurationsToRead.remove(rootPath); m_siteConfigurations.remove(rootPath); } }