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 return getDetailOnlyPage(cms, req, pageRootPath, true); 178 } 179 180 /** 181 * Returns the detail only container page bean or <code>null</code> if none available.<p> 182 * 183 * @param cms the cms context 184 * @param req the current request 185 * @param pageRootPath the root path of the page 186 * @param lookupContextFirst flag, indicating if the bean should be looked up in the standard context first. 187 * 188 * @return the container page bean 189 */ 190 public static CmsContainerPageBean getDetailOnlyPage( 191 CmsObject cms, 192 ServletRequest req, 193 String pageRootPath, 194 boolean lookupContextFirst) { 195 196 CmsJspStandardContextBean standardContext = CmsJspStandardContextBean.getInstance(req); 197 CmsContainerPageBean detailOnlyPage = lookupContextFirst ? standardContext.getDetailOnlyPage() : null; 198 if (standardContext.isDetailRequest() && (detailOnlyPage == null)) { 199 200 try { 201 CmsObject rootCms = OpenCms.initCmsObject(cms); 202 rootCms.getRequestContext().setSiteRoot(""); 203 String locale = getDetailContainerLocale( 204 cms, 205 cms.getRequestContext().getLocale().toString(), 206 cms.readResource(cms.getRequestContext().getUri())); 207 208 String resourceName = getDetailOnlyPageNameWithoutLocaleCheck( 209 standardContext.getDetailContent().getRootPath(), 210 locale); 211 CmsResource resource = null; 212 if (rootCms.existsResource(resourceName)) { 213 resource = rootCms.readResource(resourceName); 214 } else { 215 // check if the deprecated locale independent detail container page exists 216 resourceName = getDetailOnlyPageNameWithoutLocaleCheck( 217 standardContext.getDetailContent().getRootPath(), 218 null); 219 if (rootCms.existsResource(resourceName)) { 220 resource = rootCms.readResource(resourceName); 221 } 222 } 223 224 CmsXmlContainerPage xmlContainerPage = null; 225 if (resource != null) { 226 xmlContainerPage = CmsXmlContainerPageFactory.unmarshal(rootCms, resource, req); 227 } 228 if (xmlContainerPage != null) { 229 detailOnlyPage = xmlContainerPage.getContainerPage(rootCms); 230 detailOnlyPage = CmsTemplateMapper.get(req).transformContainerpageBean( 231 rootCms, 232 detailOnlyPage, 233 pageRootPath); 234 standardContext.setDetailOnlyPage(detailOnlyPage); 235 } 236 } catch (CmsException e) { 237 LOG.error(e.getLocalizedMessage(), e); 238 } 239 } 240 return detailOnlyPage; 241 } 242 243 /** 244 * Returns the site/root path to the detail only container page, for site/root path of the detail content.<p> 245 * 246 * @param cms the current cms context 247 * @param pageResource the detail page resource 248 * @param detailPath the site or root path to the detail content (accordingly site or root path's will be returned) 249 * @param locale the locale for which we want the detail only page 250 * 251 * @return the site or root path to the detail only container page (dependent on providing site or root path for the detailPath). 252 */ 253 public static String getDetailOnlyPageName( 254 CmsObject cms, 255 CmsResource pageResource, 256 String detailPath, 257 String locale) { 258 259 return getDetailOnlyPageNameWithoutLocaleCheck(detailPath, getDetailContainerLocale(cms, locale, pageResource)); 260 } 261 262 /** 263 * Gets the detail only resource for a given detail content and locale.<p> 264 * 265 * @param cms the current cms context 266 * @param contentLocale the locale for which we want the detail only resource 267 * @param detailContentRes the detail content resource 268 * @param pageRes the page resource 269 * 270 * @return an Optional wrapping a detail only resource 271 */ 272 public static Optional<CmsResource> getDetailOnlyResource( 273 CmsObject cms, 274 String contentLocale, 275 CmsResource detailContentRes, 276 CmsResource pageRes) { 277 278 Optional<CmsResource> detailOnlyRes = getDetailOnlyPage( 279 cms, 280 detailContentRes, 281 getDetailContainerLocale(cms, contentLocale, pageRes)); 282 return detailOnlyRes; 283 } 284 285 /** 286 * Returns a list of detail only container pages associated with the given resource.<p> 287 * 288 * @param cms the cms context 289 * @param resource the resource 290 * 291 * @return the list of detail only container pages 292 */ 293 public static List<CmsResource> getDetailOnlyResources(CmsObject cms, CmsResource resource) { 294 295 List<CmsResource> result = new ArrayList<CmsResource>(); 296 Set<String> resourcePaths = new HashSet<String>(); 297 String sitePath = cms.getSitePath(resource); 298 for (Locale locale : OpenCms.getLocaleManager().getAvailableLocales()) { 299 resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, locale.toString())); 300 } 301 // in case the deprecated locale less detail container resource exists 302 resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, null)); 303 // add the locale independent detail container resource 304 resourcePaths.add(getDetailOnlyPageNameWithoutLocaleCheck(sitePath, LOCALE_ALL)); 305 for (String path : resourcePaths) { 306 try { 307 CmsResource detailContainers = cms.readResource(path, CmsResourceFilter.IGNORE_EXPIRATION); 308 result.add(detailContainers); 309 } catch (CmsException e) { 310 // will happen in case resource does not exist, ignore 311 } 312 } 313 return result; 314 } 315 316 /** 317 * Checks whether the given resource path is of a detail containers page.<p> 318 * 319 * @param cms the cms context 320 * @param detailContainersPage the resource site path 321 * 322 * @return <code>true</code> if the given resource path is of a detail containers page 323 */ 324 public static boolean isDetailContainersPage(CmsObject cms, String detailContainersPage) { 325 326 boolean result = false; 327 try { 328 String detailName = CmsResource.getName(detailContainersPage); 329 String parentFolder = CmsResource.getParentFolder(detailContainersPage); 330 if (!parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/")) { 331 // this will be the case for locale dependent detail only pages, move one level up 332 parentFolder = CmsResource.getParentFolder(parentFolder); 333 } 334 detailName = CmsStringUtil.joinPaths(CmsResource.getParentFolder(parentFolder), detailName); 335 result = parentFolder.endsWith("/" + DETAIL_CONTAINERS_FOLDER_NAME + "/") 336 && cms.existsResource(detailName, CmsResourceFilter.IGNORE_EXPIRATION); 337 } catch (Throwable t) { 338 // may happen in case string operations fail 339 LOG.debug(t.getLocalizedMessage(), t); 340 } 341 return result; 342 } 343 344 /** 345 * Creates an empty detail-only page for a content, or just reads the resource if the detail-only page already exists.<p> 346 * 347 * @param cms the current CMS context 348 * @param detailId the structure id of the detail content 349 * @param detailOnlyRootPath the path of the detail only page 350 * 351 * @return the detail-only page 352 * 353 * @throws CmsException if something goes wrong 354 */ 355 public static CmsResource readOrCreateDetailOnlyPage(CmsObject cms, CmsUUID detailId, String detailOnlyRootPath) 356 throws CmsException { 357 358 CmsObject rootCms = OpenCms.initCmsObject(cms); 359 rootCms.getRequestContext().setSiteRoot(""); 360 CmsResource containerpage; 361 if (rootCms.existsResource(detailOnlyRootPath)) { 362 containerpage = rootCms.readResource(detailOnlyRootPath); 363 } else { 364 String parentFolder = CmsResource.getFolderPath(detailOnlyRootPath); 365 List<String> foldersToCreate = new ArrayList<String>(); 366 // ensure the parent folder exists 367 while (!rootCms.existsResource(parentFolder)) { 368 foldersToCreate.add(0, parentFolder); 369 parentFolder = CmsResource.getParentFolder(parentFolder); 370 } 371 for (String folderName : foldersToCreate) { 372 CmsResource parentRes = rootCms.createResource( 373 folderName, 374 OpenCms.getResourceManager().getResourceType(CmsResourceTypeFolder.getStaticTypeName())); 375 // set the search exclude property on parent folder 376 rootCms.writePropertyObject( 377 folderName, 378 new CmsProperty( 379 CmsPropertyDefinition.PROPERTY_SEARCH_EXCLUDE, 380 A_CmsSearchIndex.PROPERTY_SEARCH_EXCLUDE_VALUE_ALL, 381 null)); 382 CmsLockUtil.tryUnlock(rootCms, parentRes); 383 } 384 containerpage = rootCms.createResource( 385 detailOnlyRootPath, 386 OpenCms.getResourceManager().getResourceType(CmsResourceTypeXmlContainerPage.getStaticTypeName())); 387 // after creation, the file has a non-temporary exclusive lock. Unlock it so we can lock it with a temporary lock after this. 388 CmsLockUtil.tryUnlock(rootCms, containerpage); 389 } 390 CmsLockUtil.ensureLock(rootCms, containerpage); 391 try { 392 CmsResource detailResource = cms.readResource(detailId, CmsResourceFilter.IGNORE_EXPIRATION); 393 String title = cms.readPropertyObject( 394 detailResource, 395 CmsPropertyDefinition.PROPERTY_TITLE, 396 true).getValue(); 397 if (title != null) { 398 title = Messages.get().getBundle(OpenCms.getWorkplaceManager().getWorkplaceLocale(cms)).key( 399 Messages.GUI_DETAIL_CONTENT_PAGE_TITLE_1, 400 title); 401 CmsProperty titleProp = new CmsProperty(CmsPropertyDefinition.PROPERTY_TITLE, title, null); 402 cms.writePropertyObjects(containerpage, Arrays.asList(titleProp)); 403 } 404 405 List<CmsRelation> relations = cms.readRelations( 406 CmsRelationFilter.relationsFromStructureId(detailId).filterType(CmsRelationType.DETAIL_ONLY)); 407 boolean hasRelation = false; 408 for (CmsRelation relation : relations) { 409 if (relation.getTargetId().equals(containerpage.getStructureId())) { 410 hasRelation = true; 411 break; 412 } 413 } 414 if (!hasRelation) { 415 CmsLockActionRecord lockRecord = null; 416 try { 417 lockRecord = CmsLockUtil.ensureLock(cms, detailResource); 418 cms.addRelationToResource(detailResource, containerpage, CmsRelationType.DETAIL_ONLY.getName()); 419 } finally { 420 if ((lockRecord != null) && (lockRecord.getChange() == LockChange.locked)) { 421 cms.unlockResource(detailResource); 422 } 423 } 424 } 425 } catch (CmsException e) { 426 CmsContainerpageService.LOG.error(e.getLocalizedMessage(), e); 427 } 428 return containerpage; 429 } 430 431 /** 432 * Saves a detail-only page for a content.<p> 433 * 434 * If the detail-only page already exists, it is overwritten. 435 * 436 * @param cms the current CMS context 437 * @param content the content for which to save the detail-only page 438 * @param locale the locale 439 * @param page the container page data to save in the detail-only page 440 441 * @throws CmsException if something goes wrong 442 * @return the container page that was saved 443 */ 444 public static CmsXmlContainerPage saveDetailOnlyPage( 445 CmsObject cms, 446 CmsResource content, 447 String locale, 448 CmsContainerPageBean page) 449 450 throws CmsException { 451 452 String detailOnlyPath = getDetailOnlyPageNameWithoutLocaleCheck(content.getRootPath(), locale); 453 CmsResource resource = readOrCreateDetailOnlyPage(cms, content.getStructureId(), detailOnlyPath); 454 CmsXmlContainerPage xmlCntPage = CmsXmlContainerPageFactory.unmarshal(cms, cms.readFile(resource)); 455 xmlCntPage.save(cms, page); 456 return xmlCntPage; 457 } 458 459 /** 460 * Checks whether single locale detail containers should be used for the given site root.<p> 461 * 462 * @param siteRoot the site root to check 463 * 464 * @return <code>true</code> if single locale detail containers should be used for the given site root 465 */ 466 public static boolean useSingleLocaleDetailContainers(String siteRoot) { 467 468 boolean result = false; 469 if ((siteRoot != null) 470 && (OpenCms.getLocaleManager().getLocaleHandler() instanceof CmsSingleTreeLocaleHandler)) { 471 CmsSite site = OpenCms.getSiteManager().getSiteForSiteRoot(siteRoot); 472 result = (site != null) && CmsSite.LocalizationMode.singleTree.equals(site.getLocalizationMode()); 473 } 474 return result; 475 } 476 477 /** 478 * Returns the site path to the detail only container page.<p> 479 * 480 * This does not perform any further checks regarding the locale and assumes that all these checks have been done before. 481 * 482 * @param detailContentSitePath the detail content site path 483 * @param contentLocale the content locale 484 * 485 * @return the site path to the detail only container page 486 */ 487 static String getDetailOnlyPageNameWithoutLocaleCheck(String detailContentSitePath, String contentLocale) { 488 489 String result = CmsResource.getFolderPath(detailContentSitePath); 490 if (contentLocale != null) { 491 result = CmsStringUtil.joinPaths( 492 result, 493 DETAIL_CONTAINERS_FOLDER_NAME, 494 contentLocale.toString(), 495 CmsResource.getName(detailContentSitePath)); 496 } else { 497 result = CmsStringUtil.joinPaths( 498 result, 499 DETAIL_CONTAINERS_FOLDER_NAME, 500 CmsResource.getName(detailContentSitePath)); 501 } 502 return result; 503 } 504 505}