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