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.ui.apps; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.file.CmsObject; 032import org.opencms.file.CmsProperty; 033import org.opencms.file.CmsPropertyDefinition; 034import org.opencms.file.CmsResource; 035import org.opencms.file.CmsResourceFilter; 036import org.opencms.file.CmsVfsResourceNotFoundException; 037import org.opencms.file.types.CmsResourceTypeXmlContainerPage; 038import org.opencms.file.types.I_CmsResourceType; 039import org.opencms.jsp.CmsJspNavBuilder; 040import org.opencms.jsp.CmsJspNavBuilder.Visibility; 041import org.opencms.jsp.CmsJspNavElement; 042import org.opencms.main.CmsException; 043import org.opencms.main.CmsLog; 044import org.opencms.main.OpenCms; 045import org.opencms.site.CmsSite; 046import org.opencms.util.CmsStringUtil; 047 048import java.io.Serializable; 049import java.util.ArrayList; 050import java.util.Collections; 051import java.util.HashMap; 052import java.util.List; 053import java.util.Locale; 054import java.util.Map; 055import java.util.concurrent.ConcurrentHashMap; 056 057import javax.servlet.http.HttpSession; 058 059import org.apache.commons.logging.Log; 060 061/** 062 * Stores the last opened locations for file explorer, page editor and sitemap editor.<p> 063 */ 064public class CmsQuickLaunchLocationCache implements Serializable { 065 066 /** 067 * Contains contextual information for the last edited page, to find a page "near" it to edit in case the original page was deleted. 068 */ 069 static class PageLocationWithContext { 070 071 /** The original page. */ 072 private CmsResource m_resource; 073 074 /** True if this was a non-container page resource. */ 075 private boolean m_isNotPage; 076 077 /** The original navigation position. */ 078 private float m_navPos; 079 080 /** The ancestor folders, if available. */ 081 private List<CmsResource> m_ancestors = new ArrayList<>(); 082 083 /** The navigation start resource, if available. */ 084 private CmsResource m_navResource; 085 086 public PageLocationWithContext(CmsObject cms, CmsResource resource) { 087 088 m_resource = resource; 089 if (!CmsResourceTypeXmlContainerPage.isContainerPage(resource)) { 090 m_isNotPage = true; 091 } else { 092 try { 093 Map<String, CmsProperty> props = CmsProperty.getPropertyMap( 094 cms.readPropertyObjects(resource, false)); 095 if (hasNavigationProps(props)) { 096 initNavigationData(cms, resource, props); 097 } else { 098 CmsResource parent = cms.readParentFolder(m_resource.getStructureId()); 099 Map<String, CmsProperty> parentProps = CmsProperty.getPropertyMap( 100 cms.readPropertyObjects(parent, false)); 101 if (hasNavigationProps(parentProps)) { 102 initNavigationData(cms, parent, parentProps); 103 } 104 } 105 } catch (Exception e) { 106 LOG.error(e.getLocalizedMessage(), e); 107 } 108 } 109 } 110 111 /** 112 * Does the same thing as getNearestPageInternal, but as an additional fallback, tries to read any 113 * container page of the subsitemap if the former returns null. 114 * 115 * @param cms the current CMS context 116 * @return the 'nearest' page to the original page 117 */ 118 public CmsResource getNearestPage(CmsObject cms) { 119 120 CmsResource result = getNearestPageInternal(cms); 121 if (result == null) { 122 try { 123 CmsADEConfigData config = OpenCms.getADEManager().lookupConfiguration( 124 cms, 125 m_resource.getRootPath()); 126 if (CmsStringUtil.isPrefixPath(cms.getRequestContext().getSiteRoot(), config.getBasePath())) { 127 I_CmsResourceType type = OpenCms.getResourceManager().getResourceType( 128 CmsResourceTypeXmlContainerPage.RESOURCE_TYPE_NAME); 129 List<CmsResource> resources = cms.readResources( 130 cms.getRequestContext().removeSiteRoot(config.getBasePath()), 131 CmsResourceFilter.IGNORE_EXPIRATION.addRequireType(type), 132 true); 133 for (CmsResource resource : resources) { 134 if (resource.getRootPath().endsWith("/index.html")) { 135 return resource; 136 } 137 } 138 for (CmsResource resource : resources) { 139 return resource; 140 } 141 } 142 143 } catch (Exception e) { 144 LOG.debug(e.getLocalizedMessage(), e); 145 } 146 } 147 return result; 148 } 149 150 /** 151 * Gets the navigation position from a map of properties. 152 * 153 * @param props the map of properties 154 * @return the navigation position 155 */ 156 private float getNavPos(Map<String, CmsProperty> props) { 157 158 float result = Float.MAX_VALUE; 159 160 CmsProperty prop = props.get(CmsPropertyDefinition.PROPERTY_NAVPOS); 161 if (prop != null) { 162 try { 163 result = Float.parseFloat(prop.getValue()); 164 } catch (Exception e) { 165 /*ignore*/ 166 } 167 } 168 return result; 169 } 170 171 /** 172 * Gets the 'nearest' page to the original resource. 173 * 174 * <ul> 175 * <li>check if the original resource exists and return it if so 176 * <li>try to find the default file of the parent folder of the original resource, and return it if it exists 177 * <li>try the page with the smallest navigation position greater than the original resource's navigation position in its original folder 178 * <li>try the page with the greatest navigation position less than the original resource's navigation position in its original folder 179 * </ul> 180 * 181 * @param cms the CMS context 182 * @return 183 */ 184 private CmsResource getNearestPageInternal(CmsObject cms) { 185 186 if (cms.existsResource(m_resource.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION)) { 187 return m_resource; 188 } 189 if (m_isNotPage) { 190 return null; 191 } 192 if ((m_navResource != m_resource) 193 && (m_navResource != null) 194 && cms.existsResource(m_navResource.getStructureId(), CmsResourceFilter.IGNORE_EXPIRATION)) { 195 try { 196 CmsResource defaultFile = cms.readDefaultFile( 197 cms.getRequestContext().getSitePath(m_navResource), 198 CmsResourceFilter.IGNORE_EXPIRATION); 199 if (defaultFile != null) { 200 return defaultFile; 201 } 202 } catch (Exception e) { 203 LOG.debug(e.getLocalizedMessage(), e); 204 } 205 } 206 CmsJspNavBuilder builder = new CmsJspNavBuilder(); 207 builder.init(cms, Locale.ENGLISH); 208 for (int ancestorIndex = 0; ancestorIndex < m_ancestors.size(); ancestorIndex++) { 209 try { 210 // re-read to ensure path is correct 211 CmsResource ancestor = cms.readResource( 212 m_ancestors.get(ancestorIndex).getStructureId(), 213 CmsResourceFilter.IGNORE_EXPIRATION); 214 if (!CmsStringUtil.isPrefixPath(cms.getRequestContext().getSiteRoot(), ancestor.getRootPath())) { 215 return null; 216 } 217 if (ancestorIndex == 0) { 218 List<CmsJspNavElement> navigation = builder.getNavigationForFolder( 219 cms.getRequestContext().getSitePath(ancestor), 220 Visibility.navigation, 221 CmsResourceFilter.IGNORE_EXPIRATION); 222 List<CmsJspNavElement> before = new ArrayList<>(); 223 List<CmsJspNavElement> after = new ArrayList<>(); 224 for (CmsJspNavElement elem : navigation) { 225 if (elem.getNavPosition() < m_navPos) { 226 before.add(elem); 227 } else { 228 after.add(elem); 229 } 230 } 231 232 for (CmsJspNavElement afterElem : after) { 233 try { 234 // this is *not* always the same as afterElem.getResource() ! 235 CmsResource candidate = cms.readResource( 236 afterElem.getResourceName(), 237 CmsResourceFilter.IGNORE_EXPIRATION); 238 return candidate; 239 } catch (CmsException e) { 240 LOG.debug(e.getLocalizedMessage(), e); 241 } 242 } 243 Collections.reverse(before); 244 for (CmsJspNavElement beforeElem : before) { 245 try { 246 // this is *not* always the same as beforeElem.getResource() ! 247 CmsResource candidate = cms.readResource( 248 beforeElem.getResourceName(), 249 CmsResourceFilter.IGNORE_EXPIRATION); 250 return candidate; 251 } catch (CmsException e) { 252 LOG.debug(e.getLocalizedMessage(), e); 253 } 254 } 255 } 256 CmsJspNavElement ancestorElem = builder.getNavigationForResource( 257 cms.getRequestContext().getSitePath(ancestor), 258 CmsResourceFilter.IGNORE_EXPIRATION); 259 if ((ancestorElem != null) && ancestorElem.isInNavigation()) { 260 try { 261 CmsResource candidate = cms.readResource( 262 ancestorElem.getResourceName(), 263 CmsResourceFilter.IGNORE_EXPIRATION); 264 return candidate; 265 } catch (CmsException e) { 266 LOG.debug(e.getLocalizedMessage(), e); 267 } 268 } 269 270 } catch (Exception e) { 271 LOG.debug(e.getLocalizedMessage(), e); 272 } 273 } 274 return null; 275 } 276 277 /** 278 * Checks if the property map contains navigation properties. 279 * 280 * @param props the navigation properties 281 * @return true if navigation properties exist in the property map 282 */ 283 private boolean hasNavigationProps(Map<String, CmsProperty> props) { 284 285 return props.containsKey(CmsPropertyDefinition.PROPERTY_NAVTEXT) 286 || props.containsKey(CmsPropertyDefinition.PROPERTY_NAVPOS); 287 } 288 289 /** 290 * Initializes the context information for the given navigation resource. 291 * @param cms the CMS context 292 * @param navResource a resource in the navigation 293 * @param props the properties of the navigation resource 294 */ 295 private void initNavigationData(CmsObject cms, CmsResource navResource, Map<String, CmsProperty> props) { 296 297 m_navPos = getNavPos(props); 298 m_navResource = navResource; 299 CmsResource currentResource = navResource; 300 while (true) { 301 if (cms.getRequestContext().getSitePath(currentResource).equals("/")) { 302 break; 303 } 304 try { 305 currentResource = cms.readParentFolder(currentResource.getStructureId()); 306 if (currentResource != null) { 307 m_ancestors.add(currentResource); 308 } else { 309 break; 310 } 311 } catch (Exception e) { 312 break; 313 } 314 } 315 } 316 } 317 318 /** Logger instance for this class. */ 319 private static final Log LOG = CmsLog.getLog(CmsQuickLaunchLocationCache.class); 320 321 /** The serial version id. */ 322 private static final long serialVersionUID = -6144984854691623070L; 323 324 private Map<String, PageLocationWithContext> m_pageEditorLocations = new ConcurrentHashMap<String, CmsQuickLaunchLocationCache.PageLocationWithContext>(); 325 326 /** The sitemap editor locations. */ 327 private Map<String, String> m_sitemapEditorLocations; 328 329 /** The file explorer locations. */ 330 private Map<String, String> m_fileExplorerLocations; 331 332 /** 333 * Constructor.<p> 334 */ 335 public CmsQuickLaunchLocationCache() { 336 337 m_sitemapEditorLocations = new HashMap<String, String>(); 338 m_fileExplorerLocations = new HashMap<String, String>(); 339 } 340 341 /** 342 * Returns the location cache from the user session.<p> 343 * 344 * @param session the session 345 * 346 * @return the location cache 347 */ 348 public static CmsQuickLaunchLocationCache getLocationCache(HttpSession session) { 349 350 CmsQuickLaunchLocationCache cache = (CmsQuickLaunchLocationCache)session.getAttribute( 351 CmsQuickLaunchLocationCache.class.getName()); 352 if (cache == null) { 353 cache = new CmsQuickLaunchLocationCache(); 354 session.setAttribute(CmsQuickLaunchLocationCache.class.getName(), cache); 355 } 356 return cache; 357 } 358 359 /** 360 * Returns the file explorer location for the given site root.<p> 361 * 362 * @param siteRoot the site root 363 * 364 * @return the location 365 */ 366 public String getFileExplorerLocation(String siteRoot) { 367 368 return m_fileExplorerLocations.get(siteRoot); 369 } 370 371 /** 372 * Returns the page editor location for the given site root.<p> 373 * 374 * @param cms the current CMS context 375 * @param siteRoot the site root 376 * 377 * @return the location 378 */ 379 public String getPageEditorLocation(CmsObject cms, String siteRoot) { 380 381 PageLocationWithContext location = m_pageEditorLocations.get(siteRoot); 382 CmsResource res = null; 383 if (location != null) { 384 res = location.getNearestPage(cms); 385 } 386 387 if (res == null) { 388 return null; 389 } 390 try { 391 String sitePath = cms.getSitePath(res); 392 cms.readResource(sitePath, CmsResourceFilter.ONLY_VISIBLE_NO_DELETED); 393 return sitePath; 394 } catch (CmsVfsResourceNotFoundException e) { 395 try { 396 CmsResource newRes = cms.readResource(res.getStructureId(), CmsResourceFilter.ONLY_VISIBLE_NO_DELETED); 397 CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(newRes.getRootPath()); 398 if (site == null) { 399 return null; 400 } 401 if (normalizePath(site.getSiteRoot()).equals(normalizePath(siteRoot))) { 402 return cms.getSitePath(newRes); 403 } else { 404 return null; 405 } 406 407 } catch (CmsVfsResourceNotFoundException e2) { 408 return null; 409 } catch (CmsException e2) { 410 LOG.error(e.getLocalizedMessage(), e2); 411 return null; 412 } 413 } catch (CmsException e) { 414 LOG.error(e.getLocalizedMessage(), e); 415 return null; 416 } 417 418 } 419 420 /** 421 * Returns the sitemap editor location for the given site root.<p> 422 * 423 * @param siteRoot the site root 424 * 425 * @return the location 426 */ 427 public String getSitemapEditorLocation(String siteRoot) { 428 429 return m_sitemapEditorLocations.get(siteRoot); 430 } 431 432 /** 433 * Sets the latest file explorer location for the given site.<p> 434 * 435 * @param siteRoot the site root 436 * @param location the location 437 */ 438 public void setFileExplorerLocation(String siteRoot, String location) { 439 440 m_fileExplorerLocations.put(siteRoot, location); 441 } 442 443 /** 444 * Sets the latest page editor location for the given site.<p> 445 * 446 * @param siteRoot the site root 447 * @param resource the location resource 448 */ 449 public void setPageEditorResource(CmsObject cms, String siteRoot, CmsResource resource) { 450 451 PageLocationWithContext location = new PageLocationWithContext(cms, resource); 452 m_pageEditorLocations.put(siteRoot, location); 453 } 454 455 /** 456 * Sets the latest sitemap editor location for the given site.<p> 457 * 458 * @param siteRoot the site root 459 * @param location the location 460 */ 461 public void setSitemapEditorLocation(String siteRoot, String location) { 462 463 m_sitemapEditorLocations.put(siteRoot, location); 464 } 465 466 /** 467 * Ensures the given path begins and ends with a slash. 468 * 469 * @param path the path 470 * @return the normalized path 471 */ 472 private String normalizePath(String path) { 473 474 return CmsStringUtil.joinPaths("/", path, "/"); 475 } 476}