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}