/* * 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.CmsDetailPageConfigurationWriter; import org.opencms.ade.detailpage.CmsDetailPageInfo; import org.opencms.ade.detailpage.CmsSitemapDetailPageFinder; import org.opencms.ade.detailpage.I_CmsDetailPageFinder; import org.opencms.configuration.CmsSystemConfiguration; import org.opencms.db.CmsDriverManager; import org.opencms.db.CmsPublishedResource; import org.opencms.db.I_CmsProjectDriver; import org.opencms.file.CmsObject; import org.opencms.file.CmsProject; import org.opencms.file.CmsResource; import org.opencms.file.CmsUser; import org.opencms.file.types.CmsResourceTypeXmlContent; import org.opencms.file.types.I_CmsResourceType; import org.opencms.json.JSONArray; import org.opencms.json.JSONException; import org.opencms.json.JSONObject; import org.opencms.jsp.util.CmsJspStandardContextBean; import org.opencms.main.CmsEvent; import org.opencms.main.CmsException; import org.opencms.main.CmsLog; import org.opencms.main.I_CmsEventListener; import org.opencms.main.OpenCms; import org.opencms.monitor.CmsMemoryMonitor; import org.opencms.util.CmsCollectionsGenericWrapper; import org.opencms.util.CmsUUID; import org.opencms.xml.CmsXmlContentDefinition; import org.opencms.xml.containerpage.CmsADECache; import org.opencms.xml.containerpage.CmsADECacheSettings; import org.opencms.xml.containerpage.CmsContainerElementBean; import org.opencms.xml.containerpage.Messages; import org.opencms.xml.content.CmsXmlContentProperty; import org.opencms.xml.content.CmsXmlContentPropertyHelper; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.servlet.ServletRequest; import org.apache.commons.logging.Log; /** * This is the main class used to access the ADE configuration and also accomplish some other related tasks * like loading/saving favorite and recent lists.<p> */ public class CmsADEManager implements I_CmsEventListener { /** JSON property name constant. */ protected enum FavListProp { /** element property. */ ELEMENT, /** formatter property. */ FORMATTER, /** properties property. */ PROPERTIES; } /** * A status enum for the initialization status.<p> */ protected enum Status { /** already initialized. */ initialized, /** currently initializing. */ initializing, /** not initialized. */ notInitialized } /** User additional info key constant. */ public static final String ADDINFO_ADE_FAVORITE_LIST_SIZE = "ADE_FAVORITE_LIST_SIZE"; /** User additional info key constant. */ public static final String ADDINFO_ADE_RECENT_LIST_SIZE = "ADE_RECENT_LIST_SIZE"; /** User additional info key constant. */ public static final String ADDINFO_ADE_SEARCH_PAGE_SIZE = "ADE_SEARCH_PAGE_SIZE"; /** The client id separator. */ public static final String CLIENT_ID_SEPERATOR = "#"; /** The configuration file name. */ public static final String CONFIG_FILE_NAME = ".config"; /** The content folder name. */ public static final String CONFIG_FOLDER_NAME = ".content"; /** The name of the sitemap configuration file type. */ public static final String CONFIG_FOLDER_TYPE = "content_folder"; /** The path for sitemap configuration files relative from the base path. */ public static final String CONFIG_SUFFIX = "/" + CmsADEManager.CONFIG_FOLDER_NAME + "/" + CmsADEManager.CONFIG_FILE_NAME; /** The name of the sitemap configuration file type. */ public static final String CONFIG_TYPE = "sitemap_config"; /** Default favorite list size constant. */ public static final int DEFAULT_FAVORITE_LIST_SIZE = 10; /** Default recent list size constant. */ public static final int DEFAULT_RECENT_LIST_SIZE = 10; /** The name of the module configuration file type. */ public static final String MODULE_CONFIG_TYPE = "module_config"; /** The path to the sitemap editor JSP. */ public static final String PATH_SITEMAP_EDITOR_JSP = "/system/modules/org.opencms.ade.sitemap/pages/sitemap.jsp"; /** User additional info key constant. */ protected static final String ADDINFO_ADE_FAVORITE_LIST = "ADE_FAVORITE_LIST"; /** User additional info key constant. */ protected static final String ADDINFO_ADE_RECENT_LIST = "ADE_RECENT_LIST"; /** The logger instance for this class. */ private static final Log LOG = CmsLog.getLog(CmsADEManager.class); /** The cache instance. */ private CmsADECache m_cache; /** The sitemap configuration file type. */ private I_CmsResourceType m_configType; /** The detail page finder. */ private I_CmsDetailPageFinder m_detailPageFinder = new CmsSitemapDetailPageFinder(); /** The initialization status. */ private Status m_initStatus = Status.notInitialized; /** The module configuration file type. */ private I_CmsResourceType m_moduleConfigType; /** The online cache instance. */ private CmsConfigurationCache m_offlineCache; /** The offline CMS context. */ private CmsObject m_offlineCms; /** The offline cache instance. */ private CmsConfigurationCache m_onlineCache; /** The online CMS context. */ private CmsObject m_onlineCms; /** * Creates a new ADE manager.<p> * * @param adminCms a CMS context with admin privileges * @param memoryMonitor the memory monitor instance * @param systemConfiguration the system configuration */ public CmsADEManager(CmsObject adminCms, CmsMemoryMonitor memoryMonitor, CmsSystemConfiguration systemConfiguration) { // initialize the ade cache CmsADECacheSettings cacheSettings = systemConfiguration.getAdeCacheSettings(); if (cacheSettings == null) { cacheSettings = new CmsADECacheSettings(); } m_onlineCms = adminCms; m_cache = new CmsADECache(memoryMonitor, cacheSettings); // further initialization is done by the initialize() method. We don't do that in the constructor, // because during the setup the configuration resource types don't exist yet. } /** * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent) */ public void cmsEvent(CmsEvent event) { CmsResource resource = null; List<CmsResource> resources = null; //System.out.println(); switch (event.getType()) { case I_CmsEventListener.EVENT_RESOURCE_AND_PROPERTIES_MODIFIED: case I_CmsEventListener.EVENT_RESOURCE_MODIFIED: case I_CmsEventListener.EVENT_RESOURCE_CREATED: //System.out.print(getEventName(event.getType())); Object change = event.getData().get(I_CmsEventListener.KEY_CHANGE); if ((change != null) && change.equals(new Integer(CmsDriverManager.NOTHING_CHANGED))) { // skip lock & unlock return; } resource = (CmsResource)event.getData().get(I_CmsEventListener.KEY_RESOURCE); m_offlineCache.update(resource); //System.out.print(" " + resource.getRootPath()); break; case I_CmsEventListener.EVENT_RESOURCES_AND_PROPERTIES_MODIFIED: // a list of resources and all of their properties have been modified //System.out.print(getEventName(event.getType())); resources = CmsCollectionsGenericWrapper.list(event.getData().get(I_CmsEventListener.KEY_RESOURCES)); for (CmsResource res : resources) { m_offlineCache.update(res); //System.out.print(" " + res.getRootPath()); } break; case I_CmsEventListener.EVENT_RESOURCE_MOVED: resources = CmsCollectionsGenericWrapper.list(event.getData().get(I_CmsEventListener.KEY_RESOURCES)); // source, source folder, dest, dest folder // - OR - // source, dest, dest folder m_offlineCache.remove(resources.get(0)); m_offlineCache.update(resources.get(resources.size() - 2)); break; case I_CmsEventListener.EVENT_RESOURCE_DELETED: resources = CmsCollectionsGenericWrapper.list(event.getData().get(I_CmsEventListener.KEY_RESOURCES)); for (CmsResource res : resources) { m_offlineCache.remove(res); } break; case I_CmsEventListener.EVENT_RESOURCES_MODIFIED: //System.out.print(getEventName(event.getType())); // a list of resources has been modified resources = CmsCollectionsGenericWrapper.list(event.getData().get(I_CmsEventListener.KEY_RESOURCES)); for (CmsResource res : resources) { m_offlineCache.update(res); } break; case I_CmsEventListener.EVENT_CLEAR_ONLINE_CACHES: m_onlineCache.initialize(); break; case I_CmsEventListener.EVENT_PUBLISH_PROJECT: //System.out.print(getEventName(event.getType())); String publishIdStr = (String)event.getData().get(I_CmsEventListener.KEY_PUBLISHID); if (publishIdStr != null) { CmsUUID publishId = new CmsUUID(publishIdStr); try { List<CmsPublishedResource> publishedResources = m_onlineCms.readPublishedResources(publishId); if (publishedResources.isEmpty()) { // normally, the list of published resources should not be empty. // If it is, the publish event is not coming from a normal publish process, // so we re-initialize the whole cache to be on the safe side. m_onlineCache.initialize(); } else { for (CmsPublishedResource res : publishedResources) { if (res.getState().isDeleted()) { m_onlineCache.remove(res); } else { m_onlineCache.update(res); } } } } catch (CmsException e) { LOG.error(e.getLocalizedMessage(), e); } } break; case I_CmsEventListener.EVENT_CLEAR_CACHES: //System.out.print(getEventName(event.getType())); m_offlineCache.initialize(); m_onlineCache.initialize(); break; case I_CmsEventListener.EVENT_CLEAR_OFFLINE_CACHES: //System.out.print(getEventName(event.getType())); m_offlineCache.initialize(); break; default: // noop break; } } /** * Finds the entry point to a sitemap.<p> * * @param cms the CMS context * @param openPath the resource path to find the sitemap to * * @return the sitemap entry point */ public String findEntryPoint(CmsObject cms, String openPath) { CmsADEConfigData configData = lookupConfiguration(cms, openPath); String result = configData.getBasePath(); if (result == null) { return cms.getRequestContext().addSiteRoot("/"); } return result; } /** * Gets the containerpage cache instance.<p> * * @return the containerpage cache instance */ public CmsADECache getCache() { return m_cache; } /** * Gets the configuration file type.<p> * * @return the configuration file type */ public I_CmsResourceType getConfigurationType() { return m_configType; } /** * Reads the current element bean from the request.<p> * * @param req the servlet request * * @return the element bean * * @throws CmsException if no current element is set */ public CmsContainerElementBean getCurrentElement(ServletRequest req) throws CmsException { CmsContainerElementBean element = CmsJspStandardContextBean.getInstance(req).getElement(); if (element == null) { throw new CmsException(Messages.get().container(Messages.ERR_READING_ELEMENT_FROM_REQUEST_0)); } return element; } /** * Gets the detail page for a content element.<p> * * @param cms the CMS context * @param pageRootPath the element's root path * @param originPath the path in which the the detail page is being requested * * @return the detail page for the content element */ public String getDetailPage(CmsObject cms, String pageRootPath, String originPath) { boolean online = cms.getRequestContext().getCurrentProject().isOnlineProject(); CmsConfigurationCache cache = online ? m_onlineCache : m_offlineCache; String resType = cache.getParentFolderType(pageRootPath); if (resType == null) { return null; } String originRootPath = cms.getRequestContext().addSiteRoot(originPath); CmsADEConfigData configData = lookupConfiguration(cms, originRootPath); List<CmsDetailPageInfo> pageInfo = configData.getDetailPagesForType(resType); if ((pageInfo == null) || pageInfo.isEmpty()) { return null; } return pageInfo.get(0).getUri(); } /** * Gets the detail page finder.<p> * * @return the detail page finder */ public I_CmsDetailPageFinder getDetailPageFinder() { return m_detailPageFinder; } /** * Returns the main detail pages for a type in all of the VFS tree.<p> * * @param cms the current CMS context * @param type the resource type name * @return a list of detail page root paths */ public List<String> getDetailPages(CmsObject cms, String type) { CmsConfigurationCache cache = cms.getRequestContext().getCurrentProject().isOnlineProject() ? m_onlineCache : m_offlineCache; return cache.getDetailPages(type); } /** * Returns the element settings for a given resource.<p> * * @param cms the current cms context * @param resource the resource * * @return the element settings for a given resource * * @throws CmsException if something goes wrong */ public Map<String, CmsXmlContentProperty> getElementSettings(CmsObject cms, CmsResource resource) throws CmsException { if (CmsResourceTypeXmlContent.isXmlContent(resource)) { Map<String, CmsXmlContentProperty> result = new LinkedHashMap<String, CmsXmlContentProperty>(); Map<String, CmsXmlContentProperty> settings = CmsXmlContentDefinition.getContentHandlerForResource( cms, resource).getSettings(cms, resource); result.putAll(settings); return CmsXmlContentPropertyHelper.copyPropertyConfiguration(result); } return Collections.<String, CmsXmlContentProperty> emptyMap(); } /** * Returns the favorite list, or creates it if not available.<p> * * @param cms the cms context * * @return the favorite list * * @throws CmsException if something goes wrong */ public List<CmsContainerElementBean> getFavoriteList(CmsObject cms) throws CmsException { CmsUser user = cms.getRequestContext().getCurrentUser(); Object obj = user.getAdditionalInfo(ADDINFO_ADE_FAVORITE_LIST); List<CmsContainerElementBean> favList = new ArrayList<CmsContainerElementBean>(); if (obj instanceof String) { try { JSONArray array = new JSONArray((String)obj); for (int i = 0; i < array.length(); i++) { try { favList.add(elementFromJson(array.getJSONObject(i))); } catch (Throwable e) { // should never happen, catches wrong or no longer existing values LOG.warn(e.getLocalizedMessage()); } } } catch (Throwable e) { // should never happen, catches json parsing LOG.warn(e.getLocalizedMessage()); } } else { // save to be better next time saveFavoriteList(cms, favList); } return favList; } /** * Gets the maximum sitemap depth.<p> * * @return the maximum sitemap depth */ public int getMaxSitemapDepth() { return 20; } /** * Gets the module configuration resource type.<p> * * @return the module configuration resource type */ public I_CmsResourceType getModuleConfigurationType() { return m_moduleConfigType; } /** * Returns the favorite list, or creates it if not available.<p> * * @param cms the cms context * * @return the favorite list * * @throws CmsException if something goes wrong */ public List<CmsContainerElementBean> getRecentList(CmsObject cms) throws CmsException { CmsUser user = cms.getRequestContext().getCurrentUser(); Object obj = user.getAdditionalInfo(ADDINFO_ADE_RECENT_LIST); List<CmsContainerElementBean> recentList = new ArrayList<CmsContainerElementBean>(); if (obj instanceof String) { try { JSONArray array = new JSONArray((String)obj); for (int i = 0; i < array.length(); i++) { try { recentList.add(elementFromJson(array.getJSONObject(i))); } catch (Throwable e) { // should never happen, catches wrong or no longer existing values LOG.warn(e.getLocalizedMessage()); } } } catch (Throwable e) { // should never happen, catches json parsing LOG.warn(e.getLocalizedMessage()); } } else { // save to be better next time saveRecentList(cms, recentList); } return recentList; } /** * Gets the maximum length of the recent list.<p> * * @param user the user for which to get the maximum length * * @return the maximum recent list size for the user */ public int getRecentListMaxSize(CmsUser user) { Integer maxElems = (Integer)user.getAdditionalInfo(ADDINFO_ADE_RECENT_LIST_SIZE); if (maxElems == null) { maxElems = new Integer(DEFAULT_RECENT_LIST_SIZE); } return maxElems.intValue(); } /** * Tries to get the subsite root for a given resource root path.<p> * * @param cms the current CMS context * @param rootPath the root path for which the subsite root should be found * * @return the subsite root */ public String getSubSiteRoot(CmsObject cms, String rootPath) { CmsADEConfigData configData = lookupConfiguration(cms, rootPath); String basePath = configData.getBasePath(); if (basePath == null) { return OpenCms.getSiteManager().getSiteRoot(rootPath); } else { return basePath; } } /** * Initializes the configuration by reading all configuration files and caching their data.<p> */ public synchronized void initialize() { if (m_initStatus == Status.notInitialized) { try { m_initStatus = Status.initializing; m_configType = OpenCms.getResourceManager().getResourceType(CONFIG_TYPE); m_moduleConfigType = OpenCms.getResourceManager().getResourceType(MODULE_CONFIG_TYPE); CmsProject temp = getTempfileProject(m_onlineCms); m_offlineCms = OpenCms.initCmsObject(m_onlineCms); m_offlineCms.getRequestContext().setCurrentProject(temp); m_onlineCache = new CmsConfigurationCache(m_onlineCms, m_configType, m_moduleConfigType); m_offlineCache = new CmsConfigurationCache(m_offlineCms, m_configType, m_moduleConfigType); m_onlineCache.initialize(); m_offlineCache.initialize(); OpenCms.getEventManager().addCmsEventListener(this); m_initStatus = Status.initialized; } catch (CmsException e) { m_initStatus = Status.notInitialized; LOG.error(e.getLocalizedMessage(), e); } } } /** * Checks whether the given resource is configured as a detail page.<p> * * @param cms the current CMS context * @param resource the resource which should be tested * * @return true if the resource is configured as a detail page */ public boolean isDetailPage(CmsObject cms, CmsResource resource) { CmsConfigurationCache cache = cms.getRequestContext().getCurrentProject().isOnlineProject() ? m_onlineCache : m_offlineCache; return cache.isDetailPage(cms, resource); } /** * Checks whether the ADE manager is initialized (this should usually be the case except during the setup).<p> * * @return true if the ADE manager is initialized */ public boolean isInitialized() { return m_initStatus == Status.initialized; } /** * Looks up the configuration data for a given sitemap path.<p> * * @param cms the current CMS context * @param rootPath the root path for which the configuration data should be looked up * * @return the configuration data */ public CmsADEConfigData lookupConfiguration(CmsObject cms, String rootPath) { CmsADEConfigData configData = internalLookupConfiguration(cms, rootPath); if (configData == null) { configData = new CmsADEConfigData(); configData.initialize(cms.getRequestContext().getCurrentProject().isOnlineProject() ? m_onlineCms : m_offlineCms); } return configData; } /** * Reloads the configuration.<p> * * Normally you shouldn't call this directly since the event handlers take care of updating the configuration. */ public void refresh() { m_onlineCache.initialize(); m_offlineCache.initialize(); } /** * Saves a list of detail pages.<p> * @param cms the cms context * @param rootPath the root path * @param detailPages the detail pages * @param newId the id to use for new detail pages without an id * @return true if the detail pages could be successfully saved * * @throws CmsException if something goes wrong */ public boolean saveDetailPages(CmsObject cms, String rootPath, List<CmsDetailPageInfo> detailPages, CmsUUID newId) throws CmsException { CmsADEConfigData configData = lookupConfiguration(cms, rootPath); CmsDetailPageConfigurationWriter configWriter; String originalSiteRoot = cms.getRequestContext().getSiteRoot(); try { cms.getRequestContext().setSiteRoot(""); if (configData.isModuleConfiguration()) { return false; } CmsResource configFile = configData.getResource(); configWriter = new CmsDetailPageConfigurationWriter(cms, configFile); configWriter.updateAndSave(detailPages, newId); return true; } finally { cms.getRequestContext().setSiteRoot(originalSiteRoot); } } /** * Saves the favorite list, user based.<p> * * @param cms the cms context * @param favoriteList the element list * * @throws CmsException if something goes wrong */ public void saveFavoriteList(CmsObject cms, List<CmsContainerElementBean> favoriteList) throws CmsException { JSONArray data = new JSONArray(); for (CmsContainerElementBean element : favoriteList) { data.put(elementToJson(element)); } CmsUser user = cms.getRequestContext().getCurrentUser(); user.setAdditionalInfo(ADDINFO_ADE_FAVORITE_LIST, data.toString()); cms.writeUser(user); } /** * Saves the favorite list, user based.<p> * * @param cms the cms context * @param recentList the element list * * @throws CmsException if something goes wrong */ public void saveRecentList(CmsObject cms, List<CmsContainerElementBean> recentList) throws CmsException { JSONArray data = new JSONArray(); for (CmsContainerElementBean element : recentList) { data.put(elementToJson(element)); } CmsUser user = cms.getRequestContext().getCurrentUser(); user.setAdditionalInfo(ADDINFO_ADE_RECENT_LIST, data.toString()); cms.writeUser(user); } /** * The method which is called when the OpenCms instance is shut down.<p> */ public void shutdown() { // do nothing } /** * Creates an element from its serialized data.<p> * * @param data the serialized data * * @return the restored element bean * * @throws JSONException if the serialized data got corrupted */ protected CmsContainerElementBean elementFromJson(JSONObject data) throws JSONException { CmsUUID element = new CmsUUID(data.getString(FavListProp.ELEMENT.name().toLowerCase())); CmsUUID formatter = null; if (data.has(FavListProp.FORMATTER.name().toLowerCase())) { formatter = new CmsUUID(data.getString(FavListProp.FORMATTER.name().toLowerCase())); } Map<String, String> properties = new HashMap<String, String>(); JSONObject props = data.getJSONObject(FavListProp.PROPERTIES.name().toLowerCase()); Iterator<String> keys = props.keys(); while (keys.hasNext()) { String key = keys.next(); properties.put(key, props.getString(key)); } return new CmsContainerElementBean(element, formatter, properties, false); } /** * Converts the given element to JSON.<p> * * @param element the element to convert * * @return the JSON representation */ protected JSONObject elementToJson(CmsContainerElementBean element) { JSONObject data = null; try { data = new JSONObject(); data.put(FavListProp.ELEMENT.name().toLowerCase(), element.getId().toString()); if (element.getFormatterId() != null) { data.put(FavListProp.FORMATTER.name().toLowerCase(), element.getFormatterId().toString()); } JSONObject properties = new JSONObject(); for (Map.Entry<String, String> entry : element.getIndividualSettings().entrySet()) { properties.put(entry.getKey(), entry.getValue()); } data.put(FavListProp.PROPERTIES.name().toLowerCase(), properties); } catch (JSONException e) { // should never happen if (!LOG.isDebugEnabled()) { LOG.warn(e.getLocalizedMessage()); } LOG.debug(e.getLocalizedMessage(), e); return null; } return data; } /** * Gets the root path for a given resource structure id.<p> * * @param structureId the structure id * @param online if true, the resource will be looked up in the online project ,else in the offline project * * @return the root path for the given structure id * * @throws CmsException if something goes wrong */ protected String getRootPath(CmsUUID structureId, boolean online) throws CmsException { CmsConfigurationCache cache = online ? m_onlineCache : m_offlineCache; return cache.getPathForStructureId(structureId); } /** * Gets a tempfile project, creating one if it doesn't exist already.<p> * * @param cms the CMS context to use * @return the tempfile project * * @throws CmsException if something goes wrong */ protected CmsProject getTempfileProject(CmsObject cms) throws CmsException { try { return cms.readProject(I_CmsProjectDriver.TEMP_FILE_PROJECT_NAME); } catch (CmsException e) { return cms.createTempfileProject(); } } /** * Internal configuration lookup method.<p> * * @param cms the cms context * @param rootPath the root path for which to look up the configuration * * @return the configuration for the given path */ protected CmsADEConfigData internalLookupConfiguration(CmsObject cms, String rootPath) { boolean online = cms.getRequestContext().getCurrentProject().isOnlineProject(); CmsConfigurationCache cache = online ? m_onlineCache : m_offlineCache; CmsADEConfigData result = cache.getSiteConfigData(rootPath); if (result == null) { result = cache.getModuleConfiguration(); } return result; } }