001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (C) Alkacon Software (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.db; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsResource; 032import org.opencms.gwt.shared.alias.CmsAliasImportResult; 033import org.opencms.gwt.shared.alias.CmsAliasImportStatus; 034import org.opencms.gwt.shared.alias.CmsAliasMode; 035import org.opencms.i18n.CmsEncoder; 036import org.opencms.lock.CmsLock; 037import org.opencms.main.CmsException; 038import org.opencms.main.CmsLog; 039import org.opencms.main.OpenCms; 040import org.opencms.security.CmsRole; 041import org.opencms.util.CmsStringUtil; 042import org.opencms.util.CmsUUID; 043 044import java.io.BufferedReader; 045import java.io.ByteArrayInputStream; 046import java.io.IOException; 047import java.io.InputStreamReader; 048import java.util.ArrayList; 049import java.util.Collection; 050import java.util.Collections; 051import java.util.Comparator; 052import java.util.HashSet; 053import java.util.List; 054import java.util.Locale; 055import java.util.Set; 056 057import org.apache.commons.logging.Log; 058 059import com.google.common.collect.ArrayListMultimap; 060import com.google.common.collect.Multimap; 061 062import au.com.bytecode.opencsv.CSVParser; 063 064/** 065 * The alias manager provides access to the aliases stored in the database.<p> 066 */ 067public class CmsAliasManager { 068 069 /** The logger instance for this class. */ 070 private static final Log LOG = CmsLog.getLog(CmsAliasManager.class); 071 072 /** The security manager for accessing the database. */ 073 protected CmsSecurityManager m_securityManager; 074 075 /** 076 * Creates a new alias manager instance.<p> 077 * 078 * @param securityManager the security manager 079 */ 080 public CmsAliasManager(CmsSecurityManager securityManager) { 081 082 m_securityManager = securityManager; 083 } 084 085 /** 086 * Gets the list of aliases for a path in a given site.<p> 087 * 088 * This should only return either an empty list or a list with a single element. 089 * 090 * 091 * @param cms the current CMS context 092 * @param siteRoot the site root for which we want the aliases 093 * @param aliasPath the alias path 094 * 095 * @return the aliases for the given site root and path 096 * 097 * @throws CmsException if something goes wrong 098 */ 099 public List<CmsAlias> getAliasesForPath(CmsObject cms, String siteRoot, String aliasPath) throws CmsException { 100 101 CmsAlias alias = m_securityManager.readAliasByPath(cms.getRequestContext(), siteRoot, aliasPath); 102 if (alias == null) { 103 return Collections.emptyList(); 104 } else { 105 return Collections.singletonList(alias); 106 } 107 } 108 109 /** 110 * Gets the list of aliases for a given site root.<p> 111 * 112 * @param cms the current CMS context 113 * @param siteRoot the site root 114 * @return the list of aliases for the given site 115 * 116 * @throws CmsException if something goes wrong 117 */ 118 public List<CmsAlias> getAliasesForSite(CmsObject cms, String siteRoot) throws CmsException { 119 120 return m_securityManager.getAliasesForSite(cms.getRequestContext(), siteRoot); 121 } 122 123 /** 124 * Gets the aliases for a given structure id.<p> 125 * 126 * @param cms the current CMS context 127 * @param structureId the structure id of a resource 128 * 129 * @return the aliases which point to the resource with the given structure id 130 * 131 * @throws CmsException if something goes wrong 132 */ 133 public List<CmsAlias> getAliasesForStructureId(CmsObject cms, CmsUUID structureId) throws CmsException { 134 135 List<CmsAlias> aliases = m_securityManager.readAliasesById(cms.getRequestContext(), structureId); 136 Collections.sort(aliases, new Comparator<CmsAlias>() { 137 138 public int compare(CmsAlias first, CmsAlias second) { 139 140 return first.getAliasPath().compareTo(second.getAliasPath()); 141 } 142 }); 143 return aliases; 144 } 145 146 /** 147 * Reads the rewrite aliases for a given site root.<p> 148 * 149 * @param cms the current CMS context 150 * @param siteRoot the site root for which the rewrite aliases should be retrieved 151 * @return the list of rewrite aliases for the given site root 152 * 153 * @throws CmsException if something goes wrong 154 */ 155 public List<CmsRewriteAlias> getRewriteAliases(CmsObject cms, String siteRoot) throws CmsException { 156 157 CmsRewriteAliasFilter filter = new CmsRewriteAliasFilter().setSiteRoot(siteRoot); 158 List<CmsRewriteAlias> result = m_securityManager.getRewriteAliases(cms.getRequestContext(), filter); 159 return result; 160 } 161 162 /** 163 * Gets the rewrite alias matcher for the given site.<p> 164 * 165 * @param cms the CMS context to use 166 * @param siteRoot the site root 167 * 168 * @return the alias matcher for the site with the given site root 169 * 170 * @throws CmsException if something goes wrong 171 */ 172 public CmsRewriteAliasMatcher getRewriteAliasMatcher(CmsObject cms, String siteRoot) throws CmsException { 173 174 List<CmsRewriteAlias> aliases = getRewriteAliases(cms, siteRoot); 175 return new CmsRewriteAliasMatcher(aliases); 176 } 177 178 /** 179 * Checks whether the current user has permissions for mass editing the alias table.<p> 180 * 181 * @param cms the current CMS context 182 * @param siteRoot the site root to check 183 * @return true if the user from the CMS context is allowed to mass edit the alias table 184 */ 185 public boolean hasPermissionsForMassEdit(CmsObject cms, String siteRoot) { 186 187 String originalSiteRoot = cms.getRequestContext().getSiteRoot(); 188 try { 189 cms.getRequestContext().setSiteRoot(siteRoot); 190 return OpenCms.getRoleManager().hasRoleForResource(cms, CmsRole.ADMINISTRATOR, "/"); 191 } finally { 192 cms.getRequestContext().setSiteRoot(originalSiteRoot); 193 } 194 195 } 196 197 /** 198 * Imports alias CSV data.<p> 199 * 200 * @param cms the current CMS context 201 * @param aliasData the alias data 202 * @param siteRoot the root of the site into which the alias data should be imported 203 * @param separator the field separator which is used by the imported data 204 * @return the list of import results 205 * 206 * @throws Exception if something goes wrong 207 */ 208 public synchronized List<CmsAliasImportResult> importAliases( 209 CmsObject cms, 210 byte[] aliasData, 211 String siteRoot, 212 String separator) throws Exception { 213 214 checkPermissionsForMassEdit(cms); 215 BufferedReader reader = new BufferedReader( 216 new InputStreamReader(new ByteArrayInputStream(aliasData), CmsEncoder.ENCODING_UTF_8)); 217 String line = reader.readLine(); 218 List<CmsAliasImportResult> totalResult = new ArrayList<CmsAliasImportResult>(); 219 CmsAliasImportResult result; 220 while (line != null) { 221 result = processAliasLine(cms, siteRoot, line, separator); 222 if (result != null) { 223 totalResult.add(result); 224 } 225 line = reader.readLine(); 226 } 227 return totalResult; 228 } 229 230 /** 231 * Saves the aliases for a given structure id, <b>completely replacing</b> any existing aliases for the same structure id.<p> 232 * 233 * @param cms the current CMS context 234 * @param structureId the structure id of a resource 235 * @param aliases the list of aliases which should be written 236 * 237 * @throws CmsException if something goes wrong 238 */ 239 public synchronized void saveAliases(CmsObject cms, CmsUUID structureId, List<CmsAlias> aliases) 240 throws CmsException { 241 242 m_securityManager.saveAliases(cms.getRequestContext(), cms.readResource(structureId), aliases); 243 touch(cms, cms.readResource(structureId)); 244 } 245 246 /** 247 * Saves the rewrite alias for a given site root.<p> 248 * 249 * @param cms the current CMS context 250 * @param siteRoot the site root for which the rewrite aliases should be saved 251 * @param newAliases the list of aliases to save 252 * 253 * @throws CmsException if something goes wrong 254 */ 255 public void saveRewriteAliases(CmsObject cms, String siteRoot, List<CmsRewriteAlias> newAliases) 256 throws CmsException { 257 258 checkPermissionsForMassEdit(cms, siteRoot); 259 m_securityManager.saveRewriteAliases(cms.getRequestContext(), siteRoot, newAliases); 260 } 261 262 /** 263 * Updates the aliases in the database.<p> 264 * 265 * @param cms the current CMS context 266 * @param toDelete the collection of aliases to delete 267 * @param toAdd the collection of aliases to add 268 * @throws CmsException if something goes wrong 269 */ 270 public synchronized void updateAliases(CmsObject cms, Collection<CmsAlias> toDelete, Collection<CmsAlias> toAdd) 271 throws CmsException { 272 273 checkPermissionsForMassEdit(cms); 274 Set<CmsUUID> allKeys = new HashSet<CmsUUID>(); 275 Multimap<CmsUUID, CmsAlias> toDeleteMap = ArrayListMultimap.create(); 276 277 // first, group the aliases by structure id 278 279 for (CmsAlias alias : toDelete) { 280 toDeleteMap.put(alias.getStructureId(), alias); 281 allKeys.add(alias.getStructureId()); 282 } 283 284 Multimap<CmsUUID, CmsAlias> toAddMap = ArrayListMultimap.create(); 285 for (CmsAlias alias : toAdd) { 286 toAddMap.put(alias.getStructureId(), alias); 287 allKeys.add(alias.getStructureId()); 288 } 289 290 // Do all the deletions first, so we don't run into duplicate key errors for the alias paths 291 for (CmsUUID structureId : allKeys) { 292 Set<CmsAlias> aliasesToSave = new HashSet<CmsAlias>(getAliasesForStructureId(cms, structureId)); 293 Collection<CmsAlias> toDeleteForId = toDeleteMap.get(structureId); 294 if ((toDeleteForId != null) && !toDeleteForId.isEmpty()) { 295 aliasesToSave.removeAll(toDeleteForId); 296 } 297 saveAliases(cms, structureId, new ArrayList<CmsAlias>(aliasesToSave)); 298 } 299 for (CmsUUID structureId : allKeys) { 300 Set<CmsAlias> aliasesToSave = new HashSet<CmsAlias>(getAliasesForStructureId(cms, structureId)); 301 Collection<CmsAlias> toAddForId = toAddMap.get(structureId); 302 if ((toAddForId != null) && !toAddForId.isEmpty()) { 303 aliasesToSave.addAll(toAddForId); 304 } 305 saveAliases(cms, structureId, new ArrayList<CmsAlias>(aliasesToSave)); 306 } 307 } 308 309 /** 310 * Checks whether the current user has the permissions to mass edit the alias table, and throws an 311 * exception otherwise.<p> 312 * 313 * @param cms the current CMS context 314 * 315 * @throws CmsException 316 */ 317 protected void checkPermissionsForMassEdit(CmsObject cms) throws CmsException { 318 319 OpenCms.getRoleManager().checkRoleForResource(cms, CmsRole.ADMINISTRATOR, "/"); 320 } 321 322 /** 323 * Imports a single alias.<p> 324 * 325 * @param cms the current CMS context 326 * @param siteRoot the site root 327 * @param aliasPath the alias path 328 * @param vfsPath the VFS path 329 * @param mode the alias mode 330 * 331 * @return the result of the import 332 * 333 * @throws CmsException if something goes wrong 334 */ 335 protected synchronized CmsAliasImportResult importAlias( 336 CmsObject cms, 337 String siteRoot, 338 String aliasPath, 339 String vfsPath, 340 CmsAliasMode mode) throws CmsException { 341 342 CmsResource resource; 343 Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms); 344 String originalSiteRoot = cms.getRequestContext().getSiteRoot(); 345 try { 346 cms.getRequestContext().setSiteRoot(siteRoot); 347 resource = cms.readResource(vfsPath); 348 } catch (CmsException e) { 349 return new CmsAliasImportResult( 350 CmsAliasImportStatus.aliasImportError, 351 messageImportCantReadResource(locale, vfsPath), 352 aliasPath, 353 vfsPath, 354 mode); 355 } finally { 356 cms.getRequestContext().setSiteRoot(originalSiteRoot); 357 } 358 if (!CmsAlias.ALIAS_PATTERN.matcher(aliasPath).matches()) { 359 return new CmsAliasImportResult( 360 CmsAliasImportStatus.aliasImportError, 361 messageImportInvalidAliasPath(locale, aliasPath), 362 aliasPath, 363 vfsPath, 364 mode); 365 } 366 List<CmsAlias> maybeAlias = getAliasesForPath(cms, siteRoot, aliasPath); 367 if (maybeAlias.isEmpty()) { 368 CmsAlias newAlias = new CmsAlias(resource.getStructureId(), siteRoot, aliasPath, mode); 369 m_securityManager.addAlias(cms.getRequestContext(), newAlias); 370 touch(cms, resource); 371 return new CmsAliasImportResult( 372 CmsAliasImportStatus.aliasNew, 373 messageImportOk(locale), 374 aliasPath, 375 vfsPath, 376 mode); 377 } else { 378 CmsAlias existingAlias = maybeAlias.get(0); 379 CmsAliasFilter deleteFilter = new CmsAliasFilter( 380 siteRoot, 381 existingAlias.getAliasPath(), 382 existingAlias.getStructureId()); 383 m_securityManager.deleteAliases(cms.getRequestContext(), deleteFilter); 384 CmsAlias newAlias = new CmsAlias(resource.getStructureId(), siteRoot, aliasPath, mode); 385 m_securityManager.addAlias(cms.getRequestContext(), newAlias); 386 touch(cms, resource); 387 return new CmsAliasImportResult( 388 CmsAliasImportStatus.aliasChanged, 389 messageImportUpdate(locale), 390 aliasPath, 391 vfsPath, 392 mode); 393 } 394 } 395 396 /** 397 * Processes a single alias import operation which has already been parsed into fields.<p> 398 * 399 * @param cms the current CMS context 400 * @param siteRoot the site root 401 * @param aliasPath the alias path 402 * @param vfsPath the VFS resource path 403 * @param mode the alias mode 404 * 405 * @return the result of the import operation 406 */ 407 protected CmsAliasImportResult processAliasImport( 408 CmsObject cms, 409 String siteRoot, 410 String aliasPath, 411 String vfsPath, 412 CmsAliasMode mode) { 413 414 try { 415 return importAlias(cms, siteRoot, aliasPath, vfsPath, mode); 416 } catch (CmsException e) { 417 return new CmsAliasImportResult( 418 CmsAliasImportStatus.aliasImportError, 419 e.getLocalizedMessage(), 420 aliasPath, 421 vfsPath, 422 mode); 423 } 424 } 425 426 /** 427 * Processes a line from a CSV file containing the alias data to be imported.<p> 428 * 429 * @param cms the current CMS context 430 * @param siteRoot the site root 431 * @param line the line with the data to import 432 * @param separator the field separator 433 * 434 * @return the import result 435 */ 436 protected CmsAliasImportResult processAliasLine(CmsObject cms, String siteRoot, String line, String separator) { 437 438 Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms); 439 line = line.trim(); 440 // ignore empty lines or comments starting with # 441 if (CmsStringUtil.isEmptyOrWhitespaceOnly(line) || line.startsWith("#")) { 442 return null; 443 } 444 CSVParser parser = new CSVParser(separator.charAt(0)); 445 String[] tokens = null; 446 try { 447 tokens = parser.parseLine(line); 448 for (int i = 0; i < tokens.length; i++) { 449 tokens[i] = tokens[i].trim(); 450 } 451 } catch (IOException e) { 452 return new CmsAliasImportResult( 453 line, 454 CmsAliasImportStatus.aliasParseError, 455 messageImportInvalidFormat(locale)); 456 } 457 int numTokens = tokens.length; 458 String alias = null; 459 String vfsPath = null; 460 if (numTokens >= 2) { 461 alias = tokens[0]; 462 vfsPath = tokens[1]; 463 } 464 CmsAliasMode mode = CmsAliasMode.permanentRedirect; 465 if (numTokens >= 3) { 466 try { 467 mode = CmsAliasMode.valueOf(tokens[2].trim()); 468 } catch (Exception e) { 469 return new CmsAliasImportResult( 470 line, 471 CmsAliasImportStatus.aliasParseError, 472 messageImportInvalidFormat(locale)); 473 } 474 } 475 boolean isRewrite = false; 476 if (numTokens == 4) { 477 if (!tokens[3].equals("rewrite")) { 478 return new CmsAliasImportResult( 479 line, 480 CmsAliasImportStatus.aliasParseError, 481 messageImportInvalidFormat(locale)); 482 } else { 483 isRewrite = true; 484 } 485 } 486 if ((numTokens < 2) || (numTokens > 4)) { 487 return new CmsAliasImportResult( 488 line, 489 CmsAliasImportStatus.aliasParseError, 490 messageImportInvalidFormat(locale)); 491 } 492 CmsAliasImportResult returnValue = null; 493 if (isRewrite) { 494 returnValue = processRewriteImport(cms, siteRoot, alias, vfsPath, mode); 495 } else { 496 returnValue = processAliasImport(cms, siteRoot, alias, vfsPath, mode); 497 } 498 returnValue.setLine(line); 499 return returnValue; 500 } 501 502 /** 503 * Checks that the user has permissions for a mass edit operation in a given site.<p> 504 * 505 * @param cms the current CMS context 506 * @param siteRoot the site for which the permissions should be checked 507 * 508 * @throws CmsException if something goes wrong 509 */ 510 private void checkPermissionsForMassEdit(CmsObject cms, String siteRoot) throws CmsException { 511 512 String originalSiteRoot = cms.getRequestContext().getSiteRoot(); 513 try { 514 cms.getRequestContext().setSiteRoot(siteRoot); 515 checkPermissionsForMassEdit(cms); 516 } finally { 517 cms.getRequestContext().setSiteRoot(originalSiteRoot); 518 } 519 } 520 521 /** 522 * Message accessor.<p> 523 * 524 * @param locale the message locale 525 * @param path a path 526 * 527 * @return the message string 528 */ 529 private String messageImportCantReadResource(Locale locale, String path) { 530 531 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_COULD_NOT_READ_RESOURCE_0); 532 533 } 534 535 /** 536 * Message accessor.<p> 537 * 538 * @param locale the message locale 539 * @param path a path 540 * 541 * @return the message string 542 */ 543 private String messageImportInvalidAliasPath(Locale locale, String path) { 544 545 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_INVALID_ALIAS_PATH_0); 546 547 } 548 549 /** 550 * Message accessor.<p> 551 * 552 * @param locale the message locale 553 * 554 * @return the message string 555 */ 556 private String messageImportInvalidFormat(Locale locale) { 557 558 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_BAD_FORMAT_0); 559 } 560 561 /** 562 * Message accessor.<p> 563 * 564 * @param locale the message locale 565 * 566 * @return the message string 567 */ 568 private String messageImportOk(Locale locale) { 569 570 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_OK_0); 571 } 572 573 /** 574 * Message accessor.<p> 575 * 576 * @param locale the message locale 577 * 578 * @return the message string 579 */ 580 private String messageImportUpdate(Locale locale) { 581 582 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_UPDATED_0); 583 } 584 585 /** 586 * Handles the import of a rewrite alias.<p> 587 * 588 * @param cms the current CMS context 589 * @param siteRoot the site root 590 * @param source the rewrite pattern 591 * @param target the rewrite replacement 592 * @param mode the alias mode 593 * 594 * @return the import result 595 */ 596 private CmsAliasImportResult processRewriteImport( 597 CmsObject cms, 598 String siteRoot, 599 String source, 600 String target, 601 CmsAliasMode mode) { 602 603 try { 604 return m_securityManager.importRewriteAlias(cms.getRequestContext(), siteRoot, source, target, mode); 605 } catch (CmsException e) { 606 return new CmsAliasImportResult( 607 CmsAliasImportStatus.aliasImportError, 608 e.getLocalizedMessage(), 609 source, 610 target, 611 mode); 612 } 613 614 } 615 616 /** 617 * Tries to to touch a resource by setting its last modification date, but only if its state is 'unchanged'.<p> 618 * 619 * @param cms the current CMS context 620 * @param resource the resource which should be 'touched'. 621 */ 622 private void touch(CmsObject cms, CmsResource resource) { 623 624 if (resource.getState().isUnchanged()) { 625 try { 626 CmsLock lock = cms.getLock(resource); 627 if (lock.isUnlocked() || !lock.isOwnedBy(cms.getRequestContext().getCurrentUser())) { 628 cms.lockResourceTemporary(resource); 629 long now = System.currentTimeMillis(); 630 resource.setDateLastModified(now); 631 cms.writeResource(resource); 632 if (lock.isUnlocked()) { 633 cms.unlockResource(resource); 634 } 635 } 636 } catch (CmsException e) { 637 LOG.warn("Could not touch resource after alias modification: " + resource.getRootPath(), e); 638 } 639 } 640 } 641 642}