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, new Long(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 new Double(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 new Double(filesize / 1048576.0)); 246 } else { 247 result = Messages.get().getBundle(locale).key( 248 Messages.GUI_FILEUTIL_FILESIZE_GBYTES_1, 249 new Double(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 int numRead = 0; 717 while (offset < size) { 718 numRead = in.read(bytes, offset, size - offset); 719 if (numRead >= 0) { 720 offset += numRead; 721 } else { 722 break; 723 } 724 } 725 726 // close the input stream and return bytes 727 if (closeStream) { 728 in.close(); 729 } 730 731 // ensure all the bytes have been read in 732 if (offset < bytes.length) { 733 throw new IOException("Could not read requested " + size + " bytes from input stream"); 734 } 735 736 return bytes; 737 } 738 739 /** 740 * Removes a leading separator from a path if required.<p> 741 * 742 * @param path the path to remove the leading separator from 743 * @return the path without a trailing separator 744 */ 745 public static String removeLeadingSeparator(String path) { 746 747 int l = path.length(); 748 if (l == 0) { 749 return ""; 750 } else if (path.charAt(0) != '/') { 751 return path; 752 } else if (l == 1) { 753 return ""; 754 } else { 755 return path.substring(1, l); 756 } 757 } 758 759 /** 760 * Removes all resource names in the given List that are "redundant" because the parent folder name 761 * is also contained in the List.<p> 762 * 763 * The content of the input list is not modified.<p> 764 * 765 * @param resourcenames a list of VFS pathnames to check for redundencies (Strings) 766 * 767 * @return a new list with all redundancies removed 768 * 769 * @see #removeRedundantResources(List) 770 */ 771 public static List<String> removeRedundancies(List<String> resourcenames) { 772 773 if ((resourcenames == null) || (resourcenames.isEmpty())) { 774 return new ArrayList<String>(); 775 } 776 if (resourcenames.size() == 1) { 777 // if there is only one resource name in the list, there can be no redundancies 778 return new ArrayList<String>(resourcenames); 779 } 780 // check all resources names and see if a parent folder name is contained 781 List<String> result = new ArrayList<String>(resourcenames.size()); 782 List<String> base = new ArrayList<String>(resourcenames); 783 Collections.sort(base); 784 Iterator<String> i = base.iterator(); 785 while (i.hasNext()) { 786 // check all resource names in the list 787 String resourcename = i.next(); 788 if (CmsStringUtil.isEmptyOrWhitespaceOnly(resourcename)) { 789 // skip empty strings 790 continue; 791 } 792 boolean valid = true; 793 for (int j = (result.size() - 1); j >= 0; j--) { 794 // check if this resource name is indirectly contained because a parent folder name is contained 795 String check = result.get(j); 796 if ((CmsResource.isFolder(check) && resourcename.startsWith(check)) || resourcename.equals(check)) { 797 valid = false; 798 break; 799 } 800 } 801 if (valid) { 802 // a parent folder name is not already contained in the result 803 result.add(resourcename); 804 } 805 } 806 return result; 807 } 808 809 /** 810 * Removes all resources in the given List that are "redundant" because the parent folder 811 * is also contained in the List.<p> 812 * 813 * The content of the input list is not modified.<p> 814 * 815 * @param resources a list of <code>{@link CmsResource}</code> objects to check for redundancies 816 * 817 * @return a the given list with all redundancies removed 818 * 819 * @see #removeRedundancies(List) 820 */ 821 public static List<CmsResource> removeRedundantResources(List<CmsResource> resources) { 822 823 if ((resources == null) || (resources.isEmpty())) { 824 return new ArrayList<CmsResource>(); 825 } 826 if (resources.size() == 1) { 827 // if there is only one resource in the list, there can be no redundancies 828 return new ArrayList<CmsResource>(resources); 829 } 830 // check all resources and see if a parent folder name is contained 831 List<CmsResource> result = new ArrayList<CmsResource>(resources.size()); 832 List<CmsResource> base = new ArrayList<CmsResource>(resources); 833 Collections.sort(base); 834 Iterator<CmsResource> i = base.iterator(); 835 while (i.hasNext()) { 836 // check all folders in the list 837 CmsResource resource = i.next(); 838 boolean valid = true; 839 for (int j = (result.size() - 1); j >= 0; j--) { 840 // check if this resource is indirectly contained because a parent folder is contained 841 CmsResource check = result.get(j); 842 if ((check.isFolder() && resource.getRootPath().startsWith(check.getRootPath())) 843 || resource.getRootPath().equals(check.getRootPath())) { 844 valid = false; 845 break; 846 } 847 } 848 if (valid) { 849 // the parent folder is not already contained in the result 850 result.add(resource); 851 } 852 } 853 return result; 854 } 855 856 /** 857 * Removes a trailing separator from a path if required.<p> 858 * 859 * In case we have the root folder "/", the separator is not removed.<p> 860 * 861 * @param path the path to remove the trailing separator from 862 * @return the path without a trailing separator 863 */ 864 public static String removeTrailingSeparator(String path) { 865 866 int l = path.length(); 867 if ((l <= 1) || (path.charAt(l - 1) != '/')) { 868 return path; 869 } else { 870 return path.substring(0, l - 1); 871 } 872 } 873 874 /** 875 * Searches for the OpenCms web application 'WEB-INF' folder during system startup, code or 876 * <code>null</code> if the 'WEB-INF' folder can not be found.<p> 877 * 878 * @param startFolder the folder where to start searching 879 * 880 * @return String the path of the 'WEB-INF' folder in the 'real' file system, or <code>null</code> 881 */ 882 public static String searchWebInfFolder(String startFolder) { 883 884 if (CmsStringUtil.isEmpty(startFolder)) { 885 return null; 886 } 887 888 File f = new File(startFolder); 889 if (!f.exists() || !f.isDirectory()) { 890 return null; 891 } 892 893 File configFile = new File(f, CmsSystemInfo.FILE_TLD); 894 if (configFile.exists() && configFile.isFile()) { 895 return f.getAbsolutePath(); 896 } 897 898 String webInfFolder = null; 899 File[] subFiles = f.listFiles(); 900 List<File> fileList = new ArrayList<File>(Arrays.asList(subFiles)); 901 Collections.sort(fileList, new Comparator<File>() { 902 903 public int compare(File arg0, File arg1) { 904 905 // make sure that the WEB-INF folder, if it has that name, comes earlier 906 boolean a = arg0.getPath().contains("WEB-INF"); 907 boolean b = arg1.getPath().contains("WEB-INF"); 908 return Boolean.valueOf(b).compareTo(Boolean.valueOf(a)); 909 910 } 911 }); 912 913 for (File file : fileList) { 914 if (file.isDirectory()) { 915 webInfFolder = searchWebInfFolder(file.getAbsolutePath()); 916 if (webInfFolder != null) { 917 break; 918 } 919 } 920 } 921 922 return webInfFolder; 923 } 924 925 /** 926 * Traverses the file system starting from a base folder and executes a callback for every directory found.<p> 927 * 928 * @param base the base folder 929 * @param action a callback which will be passed a FileWalkState object for every directory encountered 930 */ 931 public static void walkFileSystem(File base, Closure action) { 932 933 List<FileWalkState> m_states = new ArrayList<FileWalkState>(); 934 m_states.add(createFileWalkState(base)); 935 while (!m_states.isEmpty()) { 936 // pop the top off the state stack, process it, then push states for all subdirectories onto it 937 FileWalkState last = m_states.remove(m_states.size() - 1); 938 action.execute(last); 939 for (File dir : last.getDirectories()) { 940 m_states.add(createFileWalkState(dir)); 941 } 942 } 943 } 944 945 /** 946 * Helper method for creating a FileWalkState object from a File object.<p> 947 * 948 * @param file the file 949 * 950 * @return the file walk state 951 */ 952 private static FileWalkState createFileWalkState(File file) { 953 954 File[] contents = file.listFiles(); 955 List<File> dirs = new ArrayList<File>(); 956 List<File> files = new ArrayList<File>(); 957 for (File subFile : contents) { 958 if (subFile.isDirectory()) { 959 dirs.add(subFile); 960 } else { 961 files.add(subFile); 962 } 963 } 964 return new FileWalkState(file, dirs, files); 965 } 966}