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) 213 throws Exception { 214 215 checkPermissionsForMassEdit(cms); 216 BufferedReader reader = new BufferedReader( 217 new InputStreamReader(new ByteArrayInputStream(aliasData), CmsEncoder.ENCODING_UTF_8)); 218 String line = reader.readLine(); 219 List<CmsAliasImportResult> totalResult = new ArrayList<CmsAliasImportResult>(); 220 CmsAliasImportResult result; 221 while (line != null) { 222 result = processAliasLine(cms, siteRoot, line, separator); 223 if (result != null) { 224 totalResult.add(result); 225 } 226 line = reader.readLine(); 227 } 228 return totalResult; 229 } 230 231 /** 232 * Saves the aliases for a given structure id, <b>completely replacing</b> any existing aliases for the same structure id.<p> 233 * 234 * @param cms the current CMS context 235 * @param structureId the structure id of a resource 236 * @param aliases the list of aliases which should be written 237 * 238 * @throws CmsException if something goes wrong 239 */ 240 public synchronized void saveAliases(CmsObject cms, CmsUUID structureId, List<CmsAlias> aliases) 241 throws CmsException { 242 243 m_securityManager.saveAliases(cms.getRequestContext(), cms.readResource(structureId), aliases); 244 touch(cms, cms.readResource(structureId)); 245 } 246 247 /** 248 * Saves the rewrite alias for a given site root.<p> 249 * 250 * @param cms the current CMS context 251 * @param siteRoot the site root for which the rewrite aliases should be saved 252 * @param newAliases the list of aliases to save 253 * 254 * @throws CmsException if something goes wrong 255 */ 256 public void saveRewriteAliases(CmsObject cms, String siteRoot, List<CmsRewriteAlias> newAliases) 257 throws CmsException { 258 259 checkPermissionsForMassEdit(cms, siteRoot); 260 m_securityManager.saveRewriteAliases(cms.getRequestContext(), siteRoot, newAliases); 261 } 262 263 /** 264 * Updates the aliases in the database.<p> 265 * 266 * @param cms the current CMS context 267 * @param toDelete the collection of aliases to delete 268 * @param toAdd the collection of aliases to add 269 * @throws CmsException if something goes wrong 270 */ 271 public synchronized void updateAliases(CmsObject cms, Collection<CmsAlias> toDelete, Collection<CmsAlias> toAdd) 272 throws CmsException { 273 274 checkPermissionsForMassEdit(cms); 275 Set<CmsUUID> allKeys = new HashSet<CmsUUID>(); 276 Multimap<CmsUUID, CmsAlias> toDeleteMap = ArrayListMultimap.create(); 277 278 // first, group the aliases by structure id 279 280 for (CmsAlias alias : toDelete) { 281 toDeleteMap.put(alias.getStructureId(), alias); 282 allKeys.add(alias.getStructureId()); 283 } 284 285 Multimap<CmsUUID, CmsAlias> toAddMap = ArrayListMultimap.create(); 286 for (CmsAlias alias : toAdd) { 287 toAddMap.put(alias.getStructureId(), alias); 288 allKeys.add(alias.getStructureId()); 289 } 290 291 // Do all the deletions first, so we don't run into duplicate key errors for the alias paths 292 for (CmsUUID structureId : allKeys) { 293 Set<CmsAlias> aliasesToSave = new HashSet<CmsAlias>(getAliasesForStructureId(cms, structureId)); 294 Collection<CmsAlias> toDeleteForId = toDeleteMap.get(structureId); 295 if ((toDeleteForId != null) && !toDeleteForId.isEmpty()) { 296 aliasesToSave.removeAll(toDeleteForId); 297 } 298 saveAliases(cms, structureId, new ArrayList<CmsAlias>(aliasesToSave)); 299 } 300 for (CmsUUID structureId : allKeys) { 301 Set<CmsAlias> aliasesToSave = new HashSet<CmsAlias>(getAliasesForStructureId(cms, structureId)); 302 Collection<CmsAlias> toAddForId = toAddMap.get(structureId); 303 if ((toAddForId != null) && !toAddForId.isEmpty()) { 304 aliasesToSave.addAll(toAddForId); 305 } 306 saveAliases(cms, structureId, new ArrayList<CmsAlias>(aliasesToSave)); 307 } 308 } 309 310 /** 311 * Checks whether the current user has the permissions to mass edit the alias table, and throws an 312 * exception otherwise.<p> 313 * 314 * @param cms the current CMS context 315 * 316 * @throws CmsException 317 */ 318 protected void checkPermissionsForMassEdit(CmsObject cms) throws CmsException { 319 320 OpenCms.getRoleManager().checkRoleForResource(cms, CmsRole.ADMINISTRATOR, "/"); 321 } 322 323 /** 324 * Imports a single alias.<p> 325 * 326 * @param cms the current CMS context 327 * @param siteRoot the site root 328 * @param aliasPath the alias path 329 * @param vfsPath the VFS path 330 * @param mode the alias mode 331 * 332 * @return the result of the import 333 * 334 * @throws CmsException if something goes wrong 335 */ 336 protected synchronized CmsAliasImportResult importAlias( 337 CmsObject cms, 338 String siteRoot, 339 String aliasPath, 340 String vfsPath, 341 CmsAliasMode mode) 342 throws CmsException { 343 344 CmsResource resource; 345 Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms); 346 String originalSiteRoot = cms.getRequestContext().getSiteRoot(); 347 try { 348 cms.getRequestContext().setSiteRoot(siteRoot); 349 resource = cms.readResource(vfsPath); 350 } catch (CmsException e) { 351 return new CmsAliasImportResult( 352 CmsAliasImportStatus.aliasImportError, 353 messageImportCantReadResource(locale, vfsPath), 354 aliasPath, 355 vfsPath, 356 mode); 357 } finally { 358 cms.getRequestContext().setSiteRoot(originalSiteRoot); 359 } 360 if (!CmsAlias.ALIAS_PATTERN.matcher(aliasPath).matches()) { 361 return new CmsAliasImportResult( 362 CmsAliasImportStatus.aliasImportError, 363 messageImportInvalidAliasPath(locale, aliasPath), 364 aliasPath, 365 vfsPath, 366 mode); 367 } 368 List<CmsAlias> maybeAlias = getAliasesForPath(cms, siteRoot, aliasPath); 369 if (maybeAlias.isEmpty()) { 370 CmsAlias newAlias = new CmsAlias(resource.getStructureId(), siteRoot, aliasPath, mode); 371 m_securityManager.addAlias(cms.getRequestContext(), newAlias); 372 touch(cms, resource); 373 return new CmsAliasImportResult( 374 CmsAliasImportStatus.aliasNew, 375 messageImportOk(locale), 376 aliasPath, 377 vfsPath, 378 mode); 379 } else { 380 CmsAlias existingAlias = maybeAlias.get(0); 381 CmsAliasFilter deleteFilter = new CmsAliasFilter( 382 siteRoot, 383 existingAlias.getAliasPath(), 384 existingAlias.getStructureId()); 385 m_securityManager.deleteAliases(cms.getRequestContext(), deleteFilter); 386 CmsAlias newAlias = new CmsAlias(resource.getStructureId(), siteRoot, aliasPath, mode); 387 m_securityManager.addAlias(cms.getRequestContext(), newAlias); 388 touch(cms, resource); 389 return new CmsAliasImportResult( 390 CmsAliasImportStatus.aliasChanged, 391 messageImportUpdate(locale), 392 aliasPath, 393 vfsPath, 394 mode); 395 } 396 } 397 398 /** 399 * Processes a single alias import operation which has already been parsed into fields.<p> 400 * 401 * @param cms the current CMS context 402 * @param siteRoot the site root 403 * @param aliasPath the alias path 404 * @param vfsPath the VFS resource path 405 * @param mode the alias mode 406 * 407 * @return the result of the import operation 408 */ 409 protected CmsAliasImportResult processAliasImport( 410 CmsObject cms, 411 String siteRoot, 412 String aliasPath, 413 String vfsPath, 414 CmsAliasMode mode) { 415 416 try { 417 return importAlias(cms, siteRoot, aliasPath, vfsPath, mode); 418 } catch (CmsException e) { 419 return new CmsAliasImportResult( 420 CmsAliasImportStatus.aliasImportError, 421 e.getLocalizedMessage(), 422 aliasPath, 423 vfsPath, 424 mode); 425 } 426 } 427 428 /** 429 * Processes a line from a CSV file containing the alias data to be imported.<p> 430 * 431 * @param cms the current CMS context 432 * @param siteRoot the site root 433 * @param line the line with the data to import 434 * @param separator the field separator 435 * 436 * @return the import result 437 */ 438 protected CmsAliasImportResult processAliasLine(CmsObject cms, String siteRoot, String line, String separator) { 439 440 Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms); 441 line = line.trim(); 442 // ignore empty lines or comments starting with # 443 if (CmsStringUtil.isEmptyOrWhitespaceOnly(line) || line.startsWith("#")) { 444 return null; 445 } 446 CSVParser parser = new CSVParser(separator.charAt(0)); 447 String[] tokens = null; 448 try { 449 tokens = parser.parseLine(line); 450 for (int i = 0; i < tokens.length; i++) { 451 tokens[i] = tokens[i].trim(); 452 } 453 } catch (IOException e) { 454 return new CmsAliasImportResult( 455 line, 456 CmsAliasImportStatus.aliasParseError, 457 messageImportInvalidFormat(locale)); 458 } 459 int numTokens = tokens.length; 460 String alias = null; 461 String vfsPath = null; 462 if (numTokens >= 2) { 463 alias = tokens[0]; 464 vfsPath = tokens[1]; 465 } 466 CmsAliasMode mode = CmsAliasMode.permanentRedirect; 467 if (numTokens >= 3) { 468 try { 469 mode = CmsAliasMode.valueOf(tokens[2].trim()); 470 } catch (Exception e) { 471 return new CmsAliasImportResult( 472 line, 473 CmsAliasImportStatus.aliasParseError, 474 messageImportInvalidFormat(locale)); 475 } 476 } 477 boolean isRewrite = false; 478 if (numTokens == 4) { 479 if (!tokens[3].equals("rewrite")) { 480 return new CmsAliasImportResult( 481 line, 482 CmsAliasImportStatus.aliasParseError, 483 messageImportInvalidFormat(locale)); 484 } else { 485 isRewrite = true; 486 } 487 } 488 if ((numTokens < 2) || (numTokens > 4)) { 489 return new CmsAliasImportResult( 490 line, 491 CmsAliasImportStatus.aliasParseError, 492 messageImportInvalidFormat(locale)); 493 } 494 CmsAliasImportResult returnValue = null; 495 if (isRewrite) { 496 returnValue = processRewriteImport(cms, siteRoot, alias, vfsPath, mode); 497 } else { 498 returnValue = processAliasImport(cms, siteRoot, alias, vfsPath, mode); 499 } 500 returnValue.setLine(line); 501 return returnValue; 502 } 503 504 /** 505 * Checks that the user has permissions for a mass edit operation in a given site.<p> 506 * 507 * @param cms the current CMS context 508 * @param siteRoot the site for which the permissions should be checked 509 * 510 * @throws CmsException if something goes wrong 511 */ 512 private void checkPermissionsForMassEdit(CmsObject cms, String siteRoot) throws CmsException { 513 514 String originalSiteRoot = cms.getRequestContext().getSiteRoot(); 515 try { 516 cms.getRequestContext().setSiteRoot(siteRoot); 517 checkPermissionsForMassEdit(cms); 518 } finally { 519 cms.getRequestContext().setSiteRoot(originalSiteRoot); 520 } 521 } 522 523 /** 524 * Message accessor.<p> 525 * 526 * @param locale the message locale 527 * @param path a path 528 * 529 * @return the message string 530 */ 531 private String messageImportCantReadResource(Locale locale, String path) { 532 533 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_COULD_NOT_READ_RESOURCE_0); 534 535 } 536 537 /** 538 * Message accessor.<p> 539 * 540 * @param locale the message locale 541 * @param path a path 542 * 543 * @return the message string 544 */ 545 private String messageImportInvalidAliasPath(Locale locale, String path) { 546 547 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_INVALID_ALIAS_PATH_0); 548 549 } 550 551 /** 552 * Message accessor.<p> 553 * 554 * @param locale the message locale 555 * 556 * @return the message string 557 */ 558 private String messageImportInvalidFormat(Locale locale) { 559 560 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_BAD_FORMAT_0); 561 } 562 563 /** 564 * Message accessor.<p> 565 * 566 * @param locale the message locale 567 * 568 * @return the message string 569 */ 570 private String messageImportOk(Locale locale) { 571 572 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_OK_0); 573 } 574 575 /** 576 * Message accessor.<p> 577 * 578 * @param locale the message locale 579 * 580 * @return the message string 581 */ 582 private String messageImportUpdate(Locale locale) { 583 584 return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_UPDATED_0); 585 } 586 587 /** 588 * Handles the import of a rewrite alias.<p> 589 * 590 * @param cms the current CMS context 591 * @param siteRoot the site root 592 * @param source the rewrite pattern 593 * @param target the rewrite replacement 594 * @param mode the alias mode 595 * 596 * @return the import result 597 */ 598 private CmsAliasImportResult processRewriteImport( 599 CmsObject cms, 600 String siteRoot, 601 String source, 602 String target, 603 CmsAliasMode mode) { 604 605 try { 606 return m_securityManager.importRewriteAlias(cms.getRequestContext(), siteRoot, source, target, mode); 607 } catch (CmsException e) { 608 return new CmsAliasImportResult( 609 CmsAliasImportStatus.aliasImportError, 610 e.getLocalizedMessage(), 611 source, 612 target, 613 mode); 614 } 615 616 } 617 618 /** 619 * Tries to to touch a resource by setting its last modification date, but only if its state is 'unchanged'.<p> 620 * 621 * @param cms the current CMS context 622 * @param resource the resource which should be 'touched'. 623 */ 624 private void touch(CmsObject cms, CmsResource resource) { 625 626 if (resource.getState().isUnchanged()) { 627 try { 628 CmsLock lock = cms.getLock(resource); 629 if (lock.isUnlocked() || !lock.isOwnedBy(cms.getRequestContext().getCurrentUser())) { 630 cms.lockResourceTemporary(resource); 631 long now = System.currentTimeMillis(); 632 resource.setDateLastModified(now); 633 cms.writeResource(resource); 634 if (lock.isUnlocked()) { 635 cms.unlockResource(resource); 636 } 637 } 638 } catch (CmsException e) { 639 LOG.warn("Could not touch resource after alias modification: " + resource.getRootPath(), e); 640 } 641 } 642 } 643 644}