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