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 GmbH & Co. KG, 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.util; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsProperty; 032import org.opencms.file.CmsPropertyDefinition; 033import org.opencms.file.CmsRequestContext; 034import org.opencms.file.CmsResource; 035import org.opencms.flex.CmsFlexCache; 036import org.opencms.i18n.CmsEncoder; 037import org.opencms.main.CmsException; 038import org.opencms.main.CmsIllegalArgumentException; 039import org.opencms.main.CmsLog; 040import org.opencms.main.CmsSystemInfo; 041import org.opencms.main.OpenCms; 042import org.opencms.staticexport.CmsLinkManager; 043 044import java.io.ByteArrayInputStream; 045import java.io.ByteArrayOutputStream; 046import java.io.File; 047import java.io.FileFilter; 048import java.io.FileInputStream; 049import java.io.FileNotFoundException; 050import java.io.FileOutputStream; 051import java.io.IOException; 052import java.io.InputStream; 053import java.io.OutputStreamWriter; 054import java.net.URL; 055import java.util.ArrayList; 056import java.util.Arrays; 057import java.util.Collections; 058import java.util.Comparator; 059import java.util.Iterator; 060import java.util.List; 061import java.util.ListIterator; 062import java.util.Locale; 063 064import org.apache.commons.collections.Closure; 065import org.apache.commons.logging.Log; 066 067/** 068 * Provides File utility functions.<p> 069 * 070 * @since 6.0.0 071 */ 072public final class CmsFileUtil { 073 074 /** 075 * Data bean which walkFileSystem passes to its callback.<p> 076 * 077 * The list of directories is mutable, which can be used by the callback to exclude certain directories.<p> 078 */ 079 public static class FileWalkState { 080 081 /** Current directory. */ 082 private File m_currentDir; 083 084 /** List of subdirectories of the current directory. */ 085 private List<File> m_directories; 086 087 /** List of files of the current directory. */ 088 private List<File> m_files; 089 090 /** 091 * Creates a new file walk state.<P> 092 * 093 * @param currentDir the current directory 094 * @param dirs the list of subdirectories 095 * @param files the list of files 096 */ 097 public FileWalkState(File currentDir, List<File> dirs, List<File> files) { 098 099 m_currentDir = currentDir; 100 m_directories = dirs; 101 m_files = files; 102 } 103 104 /** 105 * Gets the current directory.<p> 106 * 107 * @return the current directory 108 */ 109 public File getCurrentDir() { 110 111 return m_currentDir; 112 } 113 114 /** 115 * Gets the list of subdirectories.<p> 116 * 117 * @return the list of subdirectories 118 */ 119 public List<File> getDirectories() { 120 121 return m_directories; 122 } 123 124 /** 125 * Returns the list of files.<p> 126 * 127 * @return the list of files 128 */ 129 public List<File> getFiles() { 130 131 return m_files; 132 } 133 } 134 135 /** The static log object for this class. */ 136 private static final Log LOG = CmsLog.getLog(CmsFileUtil.class); 137 138 /** 139 * Hides the public constructor.<p> 140 */ 141 private CmsFileUtil() { 142 143 // empty 144 } 145 146 /** 147 * Adds a trailing separator to a path if required.<p> 148 * 149 * @param path the path to add the trailing separator to 150 * @return the path with a trailing separator 151 */ 152 public static String addTrailingSeparator(String path) { 153 154 int l = path.length(); 155 if ((l == 0) || (path.charAt(l - 1) != '/')) { 156 return path.concat("/"); 157 } else { 158 return path; 159 } 160 } 161 162 /** 163 * Checks if all resources are present.<p> 164 * 165 * @param cms an initialized OpenCms user context which must have read access to all resources 166 * @param resources a list of vfs resource names to check 167 * 168 * @throws CmsIllegalArgumentException in case not all resources exist or can be read with the given OpenCms user context 169 */ 170 public static void checkResources(CmsObject cms, List<String> resources) throws CmsIllegalArgumentException { 171 172 StringBuffer result = new StringBuffer(128); 173 ListIterator<String> it = resources.listIterator(); 174 while (it.hasNext()) { 175 String resourcePath = it.next(); 176 try { 177 CmsResource resource = cms.readResource(resourcePath); 178 // append folder separator, if resource is a folder and does not and with a slash 179 if (resource.isFolder() && !resourcePath.endsWith("/")) { 180 it.set(resourcePath + "/"); 181 } 182 } catch (@SuppressWarnings("unused") CmsException e) { 183 result.append(resourcePath); 184 result.append('\n'); 185 } 186 } 187 if (result.length() > 0) { 188 throw new CmsIllegalArgumentException( 189 Messages.get().container(Messages.ERR_MISSING_RESOURCES_1, result.toString())); 190 } 191 } 192 193 /** 194 * Simply version of a 1:1 binary file copy.<p> 195 * 196 * @param fromFile the name of the file to copy 197 * @param toFile the name of the target file 198 * @throws IOException if any IO error occurs during the copy operation 199 */ 200 public static void copy(String fromFile, String toFile) throws IOException { 201 202 File inputFile = new File(fromFile); 203 File outputFile = new File(toFile); 204 if (!outputFile.getParentFile().isDirectory()) { 205 outputFile.getParentFile().mkdirs(); 206 } 207 FileInputStream in = new FileInputStream(inputFile); 208 FileOutputStream out = new FileOutputStream(outputFile); 209 210 // transfer bytes from in to out 211 byte[] buf = new byte[1024]; 212 int len; 213 while ((len = in.read(buf)) > 0) { 214 out.write(buf, 0, len); 215 } 216 in.close(); 217 out.close(); 218 } 219 220 /** 221 * Returns the formatted filesize to Bytes, KB, MB or GB depending on the given value.<p> 222 * 223 * @param filesize in bytes 224 * @param locale the locale of the current OpenCms user or the System's default locale if the first choice 225 * is not at hand. 226 * 227 * @return the formatted filesize to Bytes, KB, MB or GB depending on the given value 228 **/ 229 public static String formatFilesize(long filesize, Locale locale) { 230 231 String result; 232 filesize = Math.abs(filesize); 233 234 if (Math.abs(filesize) < 1024) { 235 result = Messages.get().getBundle(locale).key(Messages.GUI_FILEUTIL_FILESIZE_BYTES_1, Long.valueOf(filesize)); 236 } else if (Math.abs(filesize) < 1048576) { 237 // 1048576 = 1024.0 * 1024.0 238 result = Messages.get().getBundle(locale).key( 239 Messages.GUI_FILEUTIL_FILESIZE_KBYTES_1, 240 Double.valueOf(filesize / 1024.0)); 241 } else if (Math.abs(filesize) < 1073741824) { 242 // 1024.0^3 = 1073741824 243 result = Messages.get().getBundle(locale).key( 244 Messages.GUI_FILEUTIL_FILESIZE_MBYTES_1, 245 Double.valueOf(filesize / 1048576.0)); 246 } else { 247 result = Messages.get().getBundle(locale).key( 248 Messages.GUI_FILEUTIL_FILESIZE_GBYTES_1, 249 Double.valueOf(filesize / 1073741824.0)); 250 } 251 return result; 252 } 253 254 /** 255 * Returns a comma separated list of resource paths names, with the site root 256 * from the given OpenCms user context removed.<p> 257 * 258 * @param context the current users OpenCms context (optional, may be <code>null</code>) 259 * @param resources a List of <code>{@link CmsResource}</code> instances to get the names from 260 * 261 * @return a comma separated list of resource paths names 262 */ 263 public static String formatResourceNames(CmsRequestContext context, List<CmsResource> resources) { 264 265 if (resources == null) { 266 return null; 267 } 268 StringBuffer result = new StringBuffer(128); 269 Iterator<CmsResource> i = resources.iterator(); 270 while (i.hasNext()) { 271 CmsResource res = i.next(); 272 String path = res.getRootPath(); 273 if (context != null) { 274 path = context.removeSiteRoot(path); 275 } 276 result.append(path); 277 if (i.hasNext()) { 278 result.append(", "); 279 } 280 } 281 return result.toString(); 282 } 283 284 /** 285 * Returns the encoding of the file. 286 * Encoding is read from the content-encoding property and defaults to the systems default encoding. 287 * Since properties can change without rewriting content, the actual encoding can differ. 288 * 289 * @param cms {@link CmsObject} used to read properties of the given file. 290 * @param file the file for which the encoding is requested 291 * @return the file's encoding according to the content-encoding property, or the system's default encoding as default. 292 */ 293 public static String getEncoding(CmsObject cms, CmsResource file) { 294 295 CmsProperty encodingProperty = CmsProperty.getNullProperty(); 296 try { 297 encodingProperty = cms.readPropertyObject(file, CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING, true); 298 } catch (CmsException e) { 299 LOG.debug(e.getLocalizedMessage(), e); 300 } 301 return CmsEncoder.lookupEncoding(encodingProperty.getValue(""), OpenCms.getSystemInfo().getDefaultEncoding()); 302 } 303 304 /** 305 * Returns the extension of the given resource name, that is the part behind the last '.' char, 306 * converted to lower case letters.<p> 307 * 308 * The extension of a file is the part of the name after the last dot, including the dot. 309 * The extension of a folder is empty. 310 * All extensions are returned as lower case<p> 311 * 312 * Please note: No check is performed to ensure the given file name is not <code>null</code>.<p> 313 * 314 * Examples:<br> 315 * <ul> 316 * <li><code>/folder.test/</code> has an empty extension. 317 * <li><code>/folder.test/config</code> has an empty extension. 318 * <li><code>/strange.filename.</code> has an empty extension. 319 * <li><code>/document.PDF</code> has the extension <code>.pdf</code>. 320 * </ul> 321 * 322 * @param resourceName the resource to get the extension for 323 * 324 * @return the extension of a resource 325 */ 326 public static String getExtension(String resourceName) { 327 328 // if the resource name indicates a folder 329 if (resourceName.charAt(resourceName.length() - 1) == '/') { 330 // folders have no extensions 331 return ""; 332 } 333 // get just the name of the resource 334 String name = CmsResource.getName(resourceName); 335 // get the position of the last dot 336 int pos = name.lastIndexOf('.'); 337 // if no dot or if no chars after the dot 338 if ((pos < 0) || ((pos + 1) == name.length())) { 339 return ""; 340 } 341 // return the extension 342 return name.substring(pos).toLowerCase(); 343 } 344 345 /** 346 * Returns the extension of the given file name, that is the part behind the last '.' char, 347 * converted to lower case letters.<p> 348 * 349 * The result does contain the '.' char. For example, if the input is <code>"opencms.html"</code>, 350 * then the result will be <code>".html"</code>.<p> 351 * 352 * If the given file name does not contain a '.' char, the empty String <code>""</code> is returned.<p> 353 * 354 * Please note: No check is performed to ensure the given file name is not <code>null</code>.<p> 355 * 356 * @param filename the file name to get the extension for 357 * @return the extension of the given file name 358 * 359 * @deprecated use {@link #getExtension(String)} instead, it is better implemented 360 */ 361 @Deprecated 362 public static String getFileExtension(String filename) { 363 364 int pos = filename.lastIndexOf('.'); 365 return (pos >= 0) ? filename.substring(pos).toLowerCase() : ""; 366 } 367 368 /** 369 * Returns a list of all filtered files in the RFS.<p> 370 * 371 * If the <code>name</code> is not a folder the folder that contains the 372 * given file will be used instead.<p> 373 * 374 * Despite the filter may not accept folders, every subfolder is traversed 375 * if the <code>includeSubtree</code> parameter is set.<p> 376 * 377 * @param name a folder or file name 378 * @param filter a filter 379 * @param includeSubtree if to include subfolders 380 * 381 * @return a list of filtered <code>{@link File}</code> objects 382 */ 383 public static List<File> getFiles(String name, FileFilter filter, boolean includeSubtree) { 384 385 List<File> ret = new ArrayList<File>(); 386 387 File file = new File(name); 388 if (!file.isDirectory()) { 389 file = new File(file.getParent()); 390 if (!file.isDirectory()) { 391 return ret; 392 } 393 } 394 File[] dirContent = file.listFiles(); 395 for (int i = 0; i < dirContent.length; i++) { 396 File f = dirContent[i]; 397 if (filter.accept(f)) { 398 ret.add(f); 399 } 400 if (includeSubtree && f.isDirectory()) { 401 ret.addAll(getFiles(f.getAbsolutePath(), filter, true)); 402 } 403 } 404 405 return ret; 406 } 407 408 /** 409 * Returns the file name for a given VFS name that has to be written to a repository in the "real" file system, 410 * by appending the VFS root path to the given base repository path, also adding an 411 * folder for the "online" or "offline" project.<p> 412 * 413 * @param repository the base repository path 414 * @param vfspath the VFS root path to write to use 415 * @param online flag indicates if the result should be used for the online project (<code>true</code>) or not 416 * 417 * @return The full uri to the JSP 418 */ 419 public static String getRepositoryName(String repository, String vfspath, boolean online) { 420 421 StringBuffer result = new StringBuffer(64); 422 result.append(repository); 423 result.append(online ? CmsFlexCache.REPOSITORY_ONLINE : CmsFlexCache.REPOSITORY_OFFLINE); 424 result.append(vfspath); 425 return result.toString(); 426 } 427 428 /** 429 * Creates unique, valid RFS name for the given filename that contains 430 * a coded version of the given parameters, with the given file extension appended.<p> 431 * 432 * This is used to create file names for the static export, 433 * or in a vfs disk cache.<p> 434 * 435 * @param filename the base file name 436 * @param extension the extension to use 437 * @param parameters the parameters to code in the result file name 438 * 439 * @return a unique, valid RFS name for the given parameters 440 * 441 * @see org.opencms.staticexport.CmsStaticExportManager 442 */ 443 public static String getRfsPath(String filename, String extension, String parameters) { 444 445 StringBuffer buf = new StringBuffer(128); 446 buf.append(filename); 447 buf.append('_'); 448 int h = parameters.hashCode(); 449 // ensure we do have a positive id value 450 buf.append(h > 0 ? h : -h); 451 buf.append(extension); 452 return buf.toString(); 453 } 454 455 /** 456 * Normalizes a file path that might contain <code>'../'</code> or <code>'./'</code> or <code>'//'</code> 457 * elements to a normal absolute path, the path separator char used is {@link File#separatorChar}.<p> 458 * 459 * @param path the path to normalize 460 * 461 * @return the normalized path 462 * 463 * @see #normalizePath(String, char) 464 */ 465 public static String normalizePath(String path) { 466 467 return normalizePath(path, File.separatorChar); 468 } 469 470 /** 471 * Normalizes a file path that might contain <code>'../'</code> or <code>'./'</code> or <code>'//'</code> 472 * elements to a normal absolute path.<p> 473 * 474 * Can also handle Windows like path information containing a drive letter, 475 * like <code>C:\path\..\</code>.<p> 476 * 477 * @param path the path to normalize 478 * @param separatorChar the file separator char to use, for example {@link File#separatorChar} 479 * 480 * @return the normalized path 481 */ 482 public static String normalizePath(String path, char separatorChar) { 483 484 if (CmsStringUtil.isNotEmpty(path)) { 485 486 // handle windows paths including drive-information first 487 String drive = null; 488 if ((path.length() > 1) && (path.charAt(1) == ':')) { 489 // windows path like C:\home\ 490 drive = path.substring(0, 2); 491 path = path.substring(2); 492 } else if ((path.length() > 1) && (path.charAt(0) == '\\') && (path.charAt(1) == '\\')) { 493 // windows path like \\home\ (network mapped drives) 494 drive = path.substring(0, 2); 495 path = path.substring(2); 496 } 497 // ensure all File separators are '/' 498 path = path.replace('\\', '/'); 499 if (drive != null) { 500 drive = drive.replace('\\', '/'); 501 } 502 if (path.charAt(0) == '/') { 503 // trick to resolve all ../ inside a path 504 path = '.' + path; 505 } 506 // resolve all '../' or './' elements in the path 507 path = CmsLinkManager.getAbsoluteUri(path, "/"); 508 // still some '//' elements might persist 509 path = CmsStringUtil.substitute(path, "//", "/"); 510 // re-append drive if required 511 if (drive != null) { 512 path = drive.concat(path); 513 } 514 // switch '/' back to OS dependend File separator if required 515 if (separatorChar != '/') { 516 path = path.replace('/', separatorChar); 517 } 518 } 519 return path; 520 } 521 522 /** 523 * Returns the normalized file path created from the given URL.<p> 524 * 525 * The path part {@link URL#getPath()} is used, unescaped and 526 * normalized using {@link #normalizePath(String, char)} using {@link File#separatorChar}.<p> 527 * 528 * @param url the URL to extract the path information from 529 * 530 * @return the normalized file path created from the given URL using {@link File#separatorChar} 531 * 532 * @see #normalizePath(URL, char) 533 */ 534 public static String normalizePath(URL url) { 535 536 return normalizePath(url, File.separatorChar); 537 } 538 539 /** 540 * Returns the normalized file path created from the given URL.<p> 541 * 542 * The path part {@link URL#getPath()} is used, unescaped and 543 * normalized using {@link #normalizePath(String, char)}.<p> 544 * 545 * @param url the URL to extract the path information from 546 * @param separatorChar the file separator char to use, for example {@link File#separatorChar} 547 * 548 * @return the normalized file path created from the given URL 549 */ 550 public static String normalizePath(URL url, char separatorChar) { 551 552 // get the path part from the URL 553 String path = new File(url.getPath()).getAbsolutePath(); 554 // trick to get the OS default encoding, taken from the official Java i18n FAQ 555 String systemEncoding = (new OutputStreamWriter(new ByteArrayOutputStream())).getEncoding(); 556 // decode url in order to remove spaces and escaped chars from path 557 return CmsFileUtil.normalizePath(CmsEncoder.decode(path, systemEncoding), separatorChar); 558 } 559 560 /** 561 * Deletes a directory in the file system and all subfolders of that directory.<p> 562 * 563 * @param directory the directory to delete 564 */ 565 public static void purgeDirectory(File directory) { 566 567 if (directory.canRead() && directory.isDirectory()) { 568 File[] files = directory.listFiles(); 569 for (int i = 0; i < files.length; i++) { 570 File f = files[i]; 571 if (f.isDirectory()) { 572 purgeDirectory(f); 573 } 574 if (f.canWrite()) { 575 f.delete(); 576 } 577 } 578 directory.delete(); 579 } 580 } 581 582 /** 583 * Reads a file from the RFS and returns the file content.<p> 584 * 585 * @param file the file to read 586 * @return the read file content 587 * 588 * @throws IOException in case of file access errors 589 */ 590 @SuppressWarnings("resource") 591 public static byte[] readFile(File file) throws IOException { 592 593 // create input and output stream 594 FileInputStream in = new FileInputStream(file); 595 596 // read the content 597 return readFully(in, (int)file.length()); 598 } 599 600 /** 601 * Reads a file with the given name from the class loader and returns the file content.<p> 602 * 603 * @param filename the file to read 604 * @return the read file content 605 * 606 * @throws IOException in case of file access errors 607 */ 608 @SuppressWarnings("resource") 609 public static byte[] readFile(String filename) throws IOException { 610 611 // create input and output stream 612 InputStream in = CmsFileUtil.class.getClassLoader().getResourceAsStream(filename); 613 if (in == null) { 614 throw new FileNotFoundException(filename); 615 } 616 617 return readFully(in); 618 } 619 620 /** 621 * Reads a file from the class loader and converts it to a String with the specified encoding.<p> 622 * 623 * @param filename the file to read 624 * @param encoding the encoding to use when converting the file content to a String 625 * @return the read file convered to a String 626 * @throws IOException in case of file access errors 627 */ 628 public static String readFile(String filename, String encoding) throws IOException { 629 630 return new String(readFile(filename), encoding); 631 } 632 633 /** 634 * Reads all bytes from the given input stream, closes it 635 * and returns the result in an array.<p> 636 * 637 * @param in the input stream to read the bytes from 638 * @return the byte content of the input stream 639 * 640 * @throws IOException in case of errors in the underlying java.io methods used 641 */ 642 public static byte[] readFully(InputStream in) throws IOException { 643 644 return readFully(in, true); 645 } 646 647 /** 648 * Reads all bytes from the given input stream, conditionally closes the given input stream 649 * and returns the result in an array.<p> 650 * 651 * @param in the input stream to read the bytes from 652 * @return the byte content of the input stream 653 * @param closeInputStream if true the given stream will be closed afterwards 654 * 655 * @throws IOException in case of errors in the underlying java.io methods used 656 */ 657 public static byte[] readFully(InputStream in, boolean closeInputStream) throws IOException { 658 659 if (in instanceof ByteArrayInputStream) { 660 // content can be read in one pass 661 return readFully(in, in.available(), closeInputStream); 662 } 663 664 // copy buffer 665 byte[] xfer = new byte[2048]; 666 // output buffer 667 ByteArrayOutputStream out = new ByteArrayOutputStream(xfer.length); 668 669 // transfer data from input to output in xfer-sized chunks. 670 for (int bytesRead = in.read(xfer, 0, xfer.length); bytesRead >= 0; bytesRead = in.read(xfer, 0, xfer.length)) { 671 if (bytesRead > 0) { 672 out.write(xfer, 0, bytesRead); 673 } 674 } 675 if (closeInputStream) { 676 in.close(); 677 } 678 out.close(); 679 return out.toByteArray(); 680 } 681 682 /** 683 * Reads the specified number of bytes from the given input stream and returns the result in an array.<p> 684 * 685 * @param in the input stream to read the bytes from 686 * @param size the number of bytes to read 687 * 688 * @return the byte content read from the input stream 689 * 690 * @throws IOException in case of errors in the underlying java.io methods used 691 */ 692 public static byte[] readFully(InputStream in, int size) throws IOException { 693 694 return readFully(in, size, true); 695 } 696 697 /** 698 * Reads the specified number of bytes from the given input stream, conditionally closes the stream 699 * and returns the result in an array.<p> 700 * 701 * @param in the input stream to read the bytes from 702 * @param size the number of bytes to read 703 * @param closeStream if true the given stream will be closed 704 * 705 * @return the byte content read from the input stream 706 * 707 * @throws IOException in case of errors in the underlying java.io methods used 708 */ 709 public static byte[] readFully(InputStream in, int size, boolean closeStream) throws IOException { 710 711 // create the byte array to hold the data 712 byte[] bytes = new byte[size]; 713 714 // read in the bytes 715 int offset = 0; 716 717 try { 718 int numRead = 0; 719 while (offset < size) { 720 numRead = in.read(bytes, offset, size - offset); 721 if (numRead >= 0) { 722 offset += numRead; 723 } else { 724 break; 725 } 726 } 727 } finally { 728 // close the input stream 729 if (closeStream) { 730 in.close(); 731 } 732 } 733 734 // ensure all the bytes have been read in 735 if (offset < bytes.length) { 736 throw new IOException("Could not read requested " + size + " bytes from input stream"); 737 } 738 739 return bytes; 740 } 741 742 /** 743 * Removes a leading separator from a path if required.<p> 744 * 745 * @param path the path to remove the leading separator from 746 * @return the path without a trailing separator 747 */ 748 public static String removeLeadingSeparator(String path) { 749 750 int l = path.length(); 751 if (l == 0) { 752 return ""; 753 } else if (path.charAt(0) != '/') { 754 return path; 755 } else if (l == 1) { 756 return ""; 757 } else { 758 return path.substring(1, l); 759 } 760 } 761 762 /** 763 * Removes all resource names in the given List that are "redundant" because the parent folder name 764 * is also contained in the List.<p> 765 * 766 * The content of the input list is not modified.<p> 767 * 768 * @param resourcenames a list of VFS pathnames to check for redundencies (Strings) 769 * 770 * @return a new list with all redundancies removed 771 * 772 * @see #removeRedundantResources(List) 773 */ 774 public static List<String> removeRedundancies(List<String> resourcenames) { 775 776 if ((resourcenames == null) || (resourcenames.isEmpty())) { 777 return new ArrayList<String>(); 778 } 779 if (resourcenames.size() == 1) { 780 // if there is only one resource name in the list, there can be no redundancies 781 return new ArrayList<String>(resourcenames); 782 } 783 // check all resources names and see if a parent folder name is contained 784 List<String> result = new ArrayList<String>(resourcenames.size()); 785 List<String> base = new ArrayList<String>(resourcenames); 786 Collections.sort(base); 787 Iterator<String> i = base.iterator(); 788 while (i.hasNext()) { 789 // check all resource names in the list 790 String resourcename = i.next(); 791 if (CmsStringUtil.isEmptyOrWhitespaceOnly(resourcename)) { 792 // skip empty strings 793 continue; 794 } 795 boolean valid = true; 796 for (int j = (result.size() - 1); j >= 0; j--) { 797 // check if this resource name is indirectly contained because a parent folder name is contained 798 String check = result.get(j); 799 if ((CmsResource.isFolder(check) && resourcename.startsWith(check)) || resourcename.equals(check)) { 800 valid = false; 801 break; 802 } 803 } 804 if (valid) { 805 // a parent folder name is not already contained in the result 806 result.add(resourcename); 807 } 808 } 809 return result; 810 } 811 812 /** 813 * Removes all resources in the given List that are "redundant" because the parent folder 814 * is also contained in the List.<p> 815 * 816 * The content of the input list is not modified.<p> 817 * 818 * @param resources a list of <code>{@link CmsResource}</code> objects to check for redundancies 819 * 820 * @return a the given list with all redundancies removed 821 * 822 * @see #removeRedundancies(List) 823 */ 824 public static List<CmsResource> removeRedundantResources(List<CmsResource> resources) { 825 826 if ((resources == null) || (resources.isEmpty())) { 827 return new ArrayList<CmsResource>(); 828 } 829 if (resources.size() == 1) { 830 // if there is only one resource in the list, there can be no redundancies 831 return new ArrayList<CmsResource>(resources); 832 } 833 // check all resources and see if a parent folder name is contained 834 List<CmsResource> result = new ArrayList<CmsResource>(resources.size()); 835 List<CmsResource> base = new ArrayList<CmsResource>(resources); 836 Collections.sort(base); 837 Iterator<CmsResource> i = base.iterator(); 838 while (i.hasNext()) { 839 // check all folders in the list 840 CmsResource resource = i.next(); 841 boolean valid = true; 842 for (int j = (result.size() - 1); j >= 0; j--) { 843 // check if this resource is indirectly contained because a parent folder is contained 844 CmsResource check = result.get(j); 845 if ((check.isFolder() && resource.getRootPath().startsWith(check.getRootPath())) 846 || resource.getRootPath().equals(check.getRootPath())) { 847 valid = false; 848 break; 849 } 850 } 851 if (valid) { 852 // the parent folder is not already contained in the result 853 result.add(resource); 854 } 855 } 856 return result; 857 } 858 859 /** 860 * Removes a trailing separator from a path if required.<p> 861 * 862 * In case we have the root folder "/", the separator is not removed.<p> 863 * 864 * @param path the path to remove the trailing separator from 865 * @return the path without a trailing separator 866 */ 867 public static String removeTrailingSeparator(String path) { 868 869 int l = path.length(); 870 if ((l <= 1) || (path.charAt(l - 1) != '/')) { 871 return path; 872 } else { 873 return path.substring(0, l - 1); 874 } 875 } 876 877 /** 878 * Searches for the OpenCms web application 'WEB-INF' folder during system startup, code or 879 * <code>null</code> if the 'WEB-INF' folder can not be found.<p> 880 * 881 * @param startFolder the folder where to start searching 882 * 883 * @return String the path of the 'WEB-INF' folder in the 'real' file system, or <code>null</code> 884 */ 885 public static String searchWebInfFolder(String startFolder) { 886 887 if (CmsStringUtil.isEmpty(startFolder)) { 888 return null; 889 } 890 891 File f = new File(startFolder); 892 if (!f.exists() || !f.isDirectory()) { 893 return null; 894 } 895 896 File configFile = new File(f, CmsSystemInfo.FILE_TLD); 897 if (configFile.exists() && configFile.isFile()) { 898 return f.getAbsolutePath(); 899 } 900 901 String webInfFolder = null; 902 File[] subFiles = f.listFiles(); 903 List<File> fileList = new ArrayList<File>(Arrays.asList(subFiles)); 904 Collections.sort(fileList, new Comparator<File>() { 905 906 public int compare(File arg0, File arg1) { 907 908 // make sure that the WEB-INF folder, if it has that name, comes earlier 909 boolean a = arg0.getPath().contains("WEB-INF"); 910 boolean b = arg1.getPath().contains("WEB-INF"); 911 return Boolean.valueOf(b).compareTo(Boolean.valueOf(a)); 912 913 } 914 }); 915 916 for (File file : fileList) { 917 if (file.isDirectory()) { 918 webInfFolder = searchWebInfFolder(file.getAbsolutePath()); 919 if (webInfFolder != null) { 920 break; 921 } 922 } 923 } 924 925 return webInfFolder; 926 } 927 928 public static String toggleTrailingSeparator(String path) { 929 930 if (path.endsWith("/")) { 931 return path.substring(0, path.length() - 1); 932 } else { 933 return path + "/"; 934 } 935 } 936 937 /** 938 * Traverses the file system starting from a base folder and executes a callback for every directory found.<p> 939 * 940 * @param base the base folder 941 * @param action a callback which will be passed a FileWalkState object for every directory encountered 942 */ 943 public static void walkFileSystem(File base, Closure action) { 944 945 List<FileWalkState> m_states = new ArrayList<FileWalkState>(); 946 m_states.add(createFileWalkState(base)); 947 while (!m_states.isEmpty()) { 948 // pop the top off the state stack, process it, then push states for all subdirectories onto it 949 FileWalkState last = m_states.remove(m_states.size() - 1); 950 action.execute(last); 951 for (File dir : last.getDirectories()) { 952 m_states.add(createFileWalkState(dir)); 953 } 954 } 955 } 956 957 /** 958 * Helper method for creating a FileWalkState object from a File object.<p> 959 * 960 * @param file the file 961 * 962 * @return the file walk state 963 */ 964 private static FileWalkState createFileWalkState(File file) { 965 966 File[] contents = file.listFiles(); 967 List<File> dirs = new ArrayList<File>(); 968 List<File> files = new ArrayList<File>(); 969 for (File subFile : contents) { 970 if (subFile.isDirectory()) { 971 dirs.add(subFile); 972 } else { 973 files.add(subFile); 974 } 975 } 976 return new FileWalkState(file, dirs, files); 977 } 978}