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