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.containerpage; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsProperty; 032import org.opencms.file.CmsPropertyDefinition; 033import org.opencms.file.CmsResource; 034import org.opencms.file.CmsResourceFilter; 035import org.opencms.file.types.CmsResourceTypeFolder; 036import org.opencms.file.types.CmsResourceTypeXmlContainerPage; 037import org.opencms.i18n.CmsSingleTreeLocaleHandler; 038import org.opencms.jsp.util.CmsJspStandardContextBean; 039import org.opencms.lock.CmsLockActionRecord; 040import org.opencms.lock.CmsLockActionRecord.LockChange; 041import org.opencms.lock.CmsLockUtil; 042import org.opencms.main.CmsException; 043import org.opencms.main.CmsLog; 044import org.opencms.main.OpenCms; 045import org.opencms.relations.CmsRelation; 046import org.opencms.relations.CmsRelationFilter; 047import org.opencms.relations.CmsRelationType; 048import org.opencms.search.A_CmsSearchIndex; 049import org.opencms.site.CmsSite; 050import org.opencms.util.CmsStringUtil; 051import org.opencms.util.CmsUUID; 052import org.opencms.xml.containerpage.CmsContainerPageBean; 053import org.opencms.xml.containerpage.CmsXmlContainerPage; 054import org.opencms.xml.containerpage.CmsXmlContainerPageFactory; 055import org.opencms.xml.templatemapper.CmsTemplateMapper; 056 057import java.util.ArrayList; 058import java.util.Arrays; 059import java.util.HashSet; 060import java.util.List; 061import java.util.Locale; 062import java.util.Set; 063 064import javax.servlet.ServletRequest; 065 066import org.apache.commons.logging.Log; 067 068import com.google.common.base.Optional; 069 070/** 071 * Static utility class for functions related to detail-only containers.<p> 072 */ 073public final class CmsDetailOnlyContainerUtil { 074 075 /** The detail containers folder name. */ 076 public static final String DETAIL_CONTAINERS_FOLDER_NAME = ".detailContainers"; 077 078 /** Use this locale string for locale independent detail only container resources. */ 079 public static final String LOCALE_ALL = "ALL"; 080 081 /** Logger instance for this class. */ 082 private static final Log LOG = CmsLog.getLog(CmsDetailOnlyContainerUtil.class); 083 084 /** 085 * Private constructor.<p> 086 */ 087 private CmsDetailOnlyContainerUtil() { 088 089 // do nothing 090 } 091 092 /** 093 * Returns the detail container resource locale appropriate for the given detail page.<p> 094 * 095 * @param cms the cms context 096 * @param contentLocale the content locale 097 * @param resource the detail page resource 098 * 099 * @return the locale String 100 */ 101 public static String getDetailContainerLocale(CmsObject cms, String contentLocale, CmsResource resource) { 102 103 boolean singleLocale = useSingleLocaleDetailContainers(cms.getRequestContext().getSiteRoot()); 104 if (!singleLocale) { 105 try { 106 CmsProperty prop = cms.readPropertyObject( 107 resource, 108 CmsPropertyDefinition.PROPERTY_LOCALE_INDEPENDENT_DETAILS, 109 true); 110 singleLocale = Boolean.parseBoolean(prop.getValue()); 111 } catch (Exception e) { 112 LOG.warn(e.getMessage(), e); 113 } 114 } 115 return singleLocale ? LOCALE_ALL : contentLocale; 116 } 117 118 /** 119 * Returns the path to the associated detail content.<p> 120 * 121 * @param detailContainersPage the detail containers page path 122 * 123 * @return the path to the associated detail content 124 */ 125 public static String getDetailContentPath(String detailContainersPage) { 126 127 String detailName = CmsResource.getName(detailContainersPage); 128 String parentFolder = CmsResource.getParentFolder(CmsResource.getParentFolder(detailContainersPage)); 129 if (parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/")) { 130 // this will be the case for locale dependent detail only pages, move one level up 131 parentFolder = CmsResource.getParentFolder(parentFolder); 132 } 133 detailName = CmsStringUtil.joinPaths(parentFolder, detailName); 134 return detailName; 135 } 136 137 /** 138 * Gets the detail only page for a detail content.<p> 139 * 140 * @param cms the CMS context 141 * @param detailContent the detail content 142 * @param contentLocale the content locale 143 * 144 * @return the detail only page, or Optional.absent() if there is no detail only page 145 */ 146 public static Optional<CmsResource> getDetailOnlyPage( 147 CmsObject cms, 148 CmsResource detailContent, 149 String contentLocale) { 150 151 try { 152 CmsObject rootCms = OpenCms.initCmsObject(cms); 153 rootCms.getRequestContext().setSiteRoot(""); 154 String path = getDetailOnlyPageNameWithoutLocaleCheck(detailContent.getRootPath(), contentLocale); 155 if (rootCms.existsResource(path, CmsResourceFilter.ALL)) { 156 CmsResource detailOnlyRes = rootCms.readResource(path, CmsResourceFilter.ALL); 157 return Optional.of(detailOnlyRes); 158 } 159 return Optional.absent(); 160 } catch (CmsException e) { 161 LOG.warn(e.getLocalizedMessage(), e); 162 return Optional.absent(); 163 } 164 } 165 166 /** 167 * Returns the detail only container page bean or <code>null</code> if none available.<p> 168 * 169 * @param cms the cms context 170 * @param req the current request 171 * @param pageRootPath the root path of the page 172 * 173 * @return the container page bean 174 */ 175 public static CmsContainerPageBean getDetailOnlyPage(CmsObject cms, ServletRequest req, String pageRootPath) { 176 177 CmsJspStandardContextBean standardContext = CmsJspStandardContextBean.getInstance(req); 178 CmsContainerPageBean detailOnlyPage = standardContext.getDetailOnlyPage(); 179 if (standardContext.isDetailRequest() && (detailOnlyPage == null)) { 180 181 try { 182 CmsObject rootCms = OpenCms.initCmsObject(cms); 183 rootCms.getRequestContext().setSiteRoot(""); 184 String locale = getDetailContainerLocale( 185 cms, 186 cms.getRequestContext().getLocale().toString(), 187 cms.readResource(cms.getRequestContext().getUri())); 188 189 String resourceName = getDetailOnlyPageNameWithoutLocaleCheck( 190 standardContext.getDetailContent().getRootPath(), 191 locale); 192 CmsResource resource = null; 193 if (rootCms.existsResource(resourceName)) { 194 resource = rootCms.readResource(resourceName); 195 } else { 196 // check if the deprecated locale independent detail container page exists 197 resourceName = getDetailOnlyPageNameWithoutLocaleCheck( 198 standardContext.getDetailContent().getRootPath(), 199 null); 200 if (rootCms.existsResource(resourceName)) { 201 resource = rootCms.readResource(resourceName); 202 } 203 } 204 205 CmsXmlContainerPage xmlContainerPage = null; 206 if (resource != null) { 207 xmlContainerPage = CmsXmlContainerPageFactory.unmarshal(rootCms, resource, req); 208 } 209 if (xmlContainerPage != null) { 210 detailOnlyPage = xmlContainerPage.getContainerPage(rootCms); 211 detailOnlyPage = CmsTemplateMapper.get(req).transformContainerpageBean( 212 rootCms, 213 detailOnlyPage, 214 pageRootPath); 215 standardContext.setDetailOnlyPage(detailOnlyPage); 216 } 217 } catch (CmsException e) { 218 LOG.error(e.getLocalizedMessage(), e); 219 } 220 } 221 return detailOnlyPage; 222 } 223 224 /** 225 * Returns the site/root path to the detail only container page, for site/root path of the detail content.<p> 226 * 227 * @param cms the current cms context 228 * @param pageResource the detail page resource 229 * @param detailPath the site or root path to the detail content (accordingly site or root path's will be returned) 230 * @param locale the locale for which we want the detail only page 231 * 232 * @return the site or root path to the detail only container page (dependent on providing site or root path for the detailPath). 233 */ 234 public static String getDetailOnlyPageName( 235 CmsObject cms, 236 CmsResource pageResource, 237 String detailPath, 238 String locale) { 239 240 return getDetailOnlyPageNameWithoutLocaleCheck(detailPath, getDetailContainerLocale(cms, locale, pageResource)); 241 } 242 243 /** 244 * Gets the detail only resource for a given detail content and locale.<p> 245 * 246 * @param cms the current cms context 247 * @param contentLocale the locale for which we want the detail only resource 248 * @param detailContentRes the detail content resource 249 * @param pageRes the page resource 250 * 251 * @return an Optional wrapping a detail only resource 252 */ 253 public static Optional<CmsResource> getDetailOnlyResource( 254 CmsObject cms, 255 String contentLocale, 256 CmsResource detailContentRes, 257 CmsResource pageRes) { 258 259 Optional<CmsResource> detailOnlyRes = getDetailOnlyPage( 260 cms, 261 detailContentRes, 262 getDetailContainerLocale(cms, contentLocale, pageRes)); 263 return detailOnlyRes; 264 } 265 266 /** 267 * Returns a list of detail only container pages associated with the given resource.<p> 268 * 269 * @param cms the cms context 270 * @param resource the resource 271 * 272 * @return the list of detail only container pages 273 */ 274 public static List<CmsResource> getDetailOnlyResources(CmsObject cms, CmsResource resource) { 275 276 List<CmsResource> result = new ArrayList<CmsResource>(); 277 Set<String> resourcePaths = new HashSet<String>(); 278 String sitePath = cms.getSitePath(resource); 279 for (Locale locale : OpenCms.getLocaleManager().getAvailableLocales()) { 280 resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, locale.toString())); 281 } 282 // in case the deprecated locale less detail container resource exists 283 resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, null)); 284 // add the locale independent detail container resource 285 resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, LOCALE_ALL)); 286 for (String path : resourcePaths) { 287 try { 288 CmsResource detailContainers = cms.readResource(path, CmsResourceFilter.IGNORE_EXPIRATION); 289 result.add(detailContainers); 290 } catch (CmsException e) { 291 // will happen in case resource does not exist, ignore 292 } 293 } 294 return result; 295 } 296 297 /** 298 * Checks whether the given resource path is of a detail containers page.<p> 299 * 300 * @param cms the cms context 301 * @param detailContainersPage the resource site path 302 * 303 * @return <code>true</code> if the given resource path is of a detail containers page 304 */ 305 public static boolean isDetailContainersPage(CmsObject cms, String detailContainersPage) { 306 307 boolean result = false; 308 try { 309 String detailName = CmsResource.getName(detailContainersPage); 310 String parentFolder = CmsResource.getParentFolder(detailContainersPage); 311 if (!parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/")) { 312 // this will be the case for locale dependent detail only pages, move one level up 313 parentFolder = CmsResource.getParentFolder(parentFolder); 314 } 315 detailName = CmsStringUtil.joinPaths(CmsResource.getParentFolder(parentFolder), detailName); 316 result = parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/") 317 && cms.existsResource(detailName, CmsResourceFilter.IGNORE_EXPIRATION); 318 } catch (Throwable t) { 319 // may happen in case string operations fail 320 LOG.debug(t.getLocalizedMessage(), t); 321 } 322 return result; 323 } 324 325 /** 326 * Creates an empty detail-only page for a content, or just reads the resource if the detail-only page already exists.<p> 327 * 328 * @param cms the current CMS context 329 * @param detailId the structure id of the detail content 330 * @param detailOnlyRootPath the path of the detail only page 331 * 332 * @return the detail-only page 333 * 334 * @throws CmsException if something goes wrong 335 */ 336 public static CmsResource readOrCreateDetailOnlyPage(CmsObject cms, CmsUUID detailId, String detailOnlyRootPath) 337 throws CmsException { 338 339 CmsObject rootCms = OpenCms.initCmsObject(cms); 340 rootCms.getRequestContext().setSiteRoot(""); 341 CmsResource containerpage; 342 if (rootCms.existsResource(detailOnlyRootPath)) { 343 containerpage = rootCms.readResource(detailOnlyRootPath); 344 } else { 345 String parentFolder = CmsResource.getFolderPath(detailOnlyRootPath); 346 List<String> foldersToCreate = new ArrayList<String>(); 347 // ensure the parent folder exists 348 while (!rootCms.existsResource(parentFolder)) { 349 foldersToCreate.add(0, parentFolder); 350 parentFolder = CmsResource.getParentFolder(parentFolder); 351 } 352 for (String folderName : foldersToCreate) { 353 CmsResource parentRes = rootCms.createResource( 354 folderName, 355 OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.getStaticTypeName())); 356 // set the search exclude property on parent folder 357 rootCms.writePropertyObject( 358 folderName, 359 new CmsProperty( 360 CmsPropertyDefinition.PROPERTY_SEARCH_EXCLUDE, 361 A_CmsSearchIndex.PROPERTY_SEARCH_EXCLUDE_VALUE_ALL, 362 null)); 363 CmsLockUtil.tryUnlock(rootCms, parentRes); 364 } 365 containerpage = rootCms.createResource( 366 detailOnlyRootPath, 367 OpenCms.getResourceManager().getResourceType(CmsResourceTypeXmlContainerPage.getStaticTypeName())); 368 } 369 CmsLockUtil.ensureLock(rootCms, containerpage); 370 try { 371 CmsResource detailResource = cms.readResource(detailId, CmsResourceFilter.IGNORE_EXPIRATION); 372 String title = cms.readPropertyObject( 373 detailResource, 374 CmsPropertyDefinition.PROPERTY_TITLE, 375 true).getValue(); 376 if (title != null) { 377 title = Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key( 378 Messages.GUI_DETAIL_CONTENT_PAGE_TITLE_1, 379 title); 380 CmsProperty titleProp = new CmsProperty(CmsPropertyDefinition.PROPERTY_TITLE, title, null); 381 cms.writePropertyObjects(containerpage, Arrays.asList(titleProp)); 382 } 383 384 List<CmsRelation> relations = cms.readRelations( 385 CmsRelationFilter.relationsFromStructureId(detailId).filterType(CmsRelationType.DETAIL_ONLY)); 386 boolean hasRelation = false; 387 for (CmsRelation relation : relations) { 388 if (relation.getTargetId().equals(containerpage.getStructureId())) { 389 hasRelation = true; 390 break; 391 } 392 } 393 if (!hasRelation) { 394 CmsLockActionRecord lockRecord = null; 395 try { 396 lockRecord = CmsLockUtil.ensureLock(cms, detailResource); 397 cms.addRelationToResource(detailResource, containerpage, CmsRelationType.DETAIL_ONLY.getName()); 398 } finally { 399 if ((lockRecord != null) && (lockRecord.getChange() == LockChange.locked)) { 400 cms.unlockResource(detailResource); 401 } 402 } 403 } 404 } catch (CmsException e) { 405 CmsContainerpageService.LOG.error(e.getLocalizedMessage(), e); 406 } 407 return containerpage; 408 } 409 410 /** 411 * Saves a detail-only page for a content.<p> 412 * 413 * If the detail-only page already exists, it is overwritten. 414 * 415 * @param cms the current CMS context 416 * @param content the content for which to save the detail-only page 417 * @param locale the locale 418 * @param page the container page data to save in the detail-only page 419 420 * @throws CmsException if something goes wrong 421 * @return the container page that was saved 422 */ 423 public static CmsXmlContainerPage saveDetailOnlyPage( 424 CmsObject cms, 425 CmsResource content, 426 String locale, 427 CmsContainerPageBean page) 428 429 throws CmsException { 430 431 String detailOnlyPath = getDetailOnlyPageNameWithoutLocaleCheck(content.getRootPath(), locale); 432 CmsResource resource = readOrCreateDetailOnlyPage(cms, content.getStructureId(), detailOnlyPath); 433 CmsXmlContainerPage xmlCntPage = CmsXmlContainerPageFactory.unmarshal(cms, cms.readFile(resource)); 434 xmlCntPage.save(cms, page); 435 return xmlCntPage; 436 } 437 438 /** 439 * Checks whether single locale detail containers should be used for the given site root.<p> 440 * 441 * @param siteRoot the site root to check 442 * 443 * @return <code>true</code> if single locale detail containers should be used for the given site root 444 */ 445 public static boolean useSingleLocaleDetailContainers(String siteRoot) { 446 447 boolean result = false; 448 if ((siteRoot != null) 449 && (OpenCms.getLocaleManager().getLocaleHandler() instanceof CmsSingleTreeLocaleHandler)) { 450 CmsSite site = OpenCms.getSiteManager().getSiteForSiteRoot(siteRoot); 451 result = (site != null) && CmsSite.LocalizationMode.singleTree.equals(site.getLocalizationMode()); 452 } 453 return result; 454 } 455 456 /** 457 * Returns the site path to the detail only container page.<p> 458 * 459 * This does not perform any further checks regarding the locale and assumes that all these checks have been done before. 460 * 461 * @param detailContentSitePath the detail content site path 462 * @param contentLocale the content locale 463 * 464 * @return the site path to the detail only container page 465 */ 466 static String getDetailOnlyPageNameWithoutLocaleCheck(String detailContentSitePath, String contentLocale) { 467 468 String result = CmsResource.getFolderPath(detailContentSitePath); 469 if (contentLocale != null) { 470 result = CmsStringUtil.joinPaths( 471 result, 472 DETAIL_CONTAINERS_FOLDER_NAME, 473 contentLocale.toString(), 474 CmsResource.getName(detailContentSitePath)); 475 } else { 476 result = CmsStringUtil.joinPaths( 477 result, 478 DETAIL_CONTAINERS_FOLDER_NAME, 479 CmsResource.getName(detailContentSitePath)); 480 } 481 return result; 482 } 483 484}