content: /*
   * This library is part of OpenCms -
   * the Open Source Content Management System
   *
   * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
   *
   * This library is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public
   * License as published by the Free Software Foundation; either
   * version 2.1 of the License, or (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
   * Lesser General Public License for more details.
   *
   * For further information about Alkacon Software GmbH & Co. KG, please see the
   * company website: https://www.alkacon.com
   *
   * For further information about OpenCms, please see the
   * project website: https://www.opencms.org
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package org.opencms.util;
  
  import org.opencms.file.CmsObject;
  import org.opencms.file.CmsProperty;
  import org.opencms.file.CmsPropertyDefinition;
  import org.opencms.file.CmsRequestContext;
  import org.opencms.file.CmsResource;
  import org.opencms.flex.CmsFlexCache;
  import org.opencms.i18n.CmsEncoder;
  import org.opencms.main.CmsException;
  import org.opencms.main.CmsIllegalArgumentException;
  import org.opencms.main.CmsLog;
  import org.opencms.main.CmsSystemInfo;
  import org.opencms.main.OpenCms;
  import org.opencms.staticexport.CmsLinkManager;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.FileFilter;
  import java.io.FileInputStream;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStreamWriter;
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.Iterator;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Locale;
  
  import org.apache.commons.collections.Closure;
  import org.apache.commons.logging.Log;
  
  /**
   * Provides File utility functions.<p>
   *
   * @since 6.0.0
   */
  public final class CmsFileUtil {
  
      /**
       * Data bean which walkFileSystem passes to its callback.<p>
       *
       * The list of directories is mutable, which can be used by the callback to exclude certain directories.<p>
       */
      public static class FileWalkState {
  
          /** Current directory. */
          private File m_currentDir;
  
          /** List of subdirectories of the current directory. */
          private List<File> m_directories;
  
          /** List of files of the current directory. */
          private List<File> m_files;
  
          /**
           * Creates a new file walk state.<P>
           *
           * @param currentDir the current directory
           * @param dirs the list of subdirectories
           * @param files the list of files
           */
          public FileWalkState(File currentDir, List<File> dirs, List<File> files) {
  
              m_currentDir = currentDir;
              m_directories = dirs;
              m_files = files;
          }
  
          /**
           * Gets the current directory.<p>
           *
           * @return the current directory
           */
          public File getCurrentDir() {
  
              return m_currentDir;
          }
  
          /**
           * Gets the list of subdirectories.<p>
           *
           * @return the list of subdirectories
           */
          public List<File> getDirectories() {
  
              return m_directories;
          }
  
          /**
           * Returns the list of files.<p>
           *
           * @return the list of files
           */
          public List<File> getFiles() {
  
              return m_files;
          }
      }
  
      /** The static log object for this class. */
      private static final Log LOG = CmsLog.getLog(CmsFileUtil.class);
  
      /**
       * Hides the public constructor.<p>
       */
      private CmsFileUtil() {
  
          // empty
      }
  
      /**
       * Adds a trailing separator to a path if required.<p>
       *
       * @param path the path to add the trailing separator to
       * @return the path with a trailing separator
       */
      public static String addTrailingSeparator(String path) {
  
          int l = path.length();
          if ((l == 0) || (path.charAt(l - 1) != '/')) {
              return path.concat("/");
          } else {
              return path;
          }
      }
  
      /**
       * Checks if all resources are present.<p>
       *
       * @param cms an initialized OpenCms user context which must have read access to all resources
       * @param resources a list of vfs resource names to check
       *
       * @throws CmsIllegalArgumentException in case not all resources exist or can be read with the given OpenCms user context
       */
      public static void checkResources(CmsObject cms, List<String> resources) throws CmsIllegalArgumentException {
  
          StringBuffer result = new StringBuffer(128);
          ListIterator<String> it = resources.listIterator();
          while (it.hasNext()) {
              String resourcePath = it.next();
              try {
                  CmsResource resource = cms.readResource(resourcePath);
                  // append folder separator, if resource is a folder and does not and with a slash
                  if (resource.isFolder() && !resourcePath.endsWith("/")) {
                      it.set(resourcePath + "/");
                  }
              } catch (@SuppressWarnings("unused") CmsException e) {
                  result.append(resourcePath);
                  result.append('\n');
              }
          }
          if (result.length() > 0) {
              throw new CmsIllegalArgumentException(
                  Messages.get().container(Messages.ERR_MISSING_RESOURCES_1, result.toString()));
          }
      }
  
      /**
       * Simply version of a 1:1 binary file copy.<p>
       *
       * @param fromFile the name of the file to copy
       * @param toFile the name of the target file
       * @throws IOException if any IO error occurs during the copy operation
       */
      public static void copy(String fromFile, String toFile) throws IOException {
  
          File inputFile = new File(fromFile);
          File outputFile = new File(toFile);
          if (!outputFile.getParentFile().isDirectory()) {
              outputFile.getParentFile().mkdirs();
          }
          FileInputStream in = new FileInputStream(inputFile);
          FileOutputStream out = new FileOutputStream(outputFile);
  
          // transfer bytes from in to out
          byte[] buf = new byte[1024];
          int len;
          while ((len = in.read(buf)) > 0) {
              out.write(buf, 0, len);
          }
          in.close();
          out.close();
      }
  
      /**
       * Returns the formatted filesize to Bytes, KB, MB or GB depending on the given value.<p>
       *
       * @param filesize in bytes
       * @param locale the locale of the current OpenCms user or the System's default locale if the first choice
       *               is not at hand.
       *
       * @return the formatted filesize to Bytes, KB, MB or GB depending on the given value
       **/
      public static String formatFilesize(long filesize, Locale locale) {
  
          String result;
          filesize = Math.abs(filesize);
  
          if (Math.abs(filesize) < 1024) {
              result = Messages.get().getBundle(locale).key(Messages.GUI_FILEUTIL_FILESIZE_BYTES_1, Long.valueOf(filesize));
          } else if (Math.abs(filesize) < 1048576) {
              // 1048576 = 1024.0 * 1024.0
              result = Messages.get().getBundle(locale).key(
                  Messages.GUI_FILEUTIL_FILESIZE_KBYTES_1,
                  Double.valueOf(filesize / 1024.0));
          } else if (Math.abs(filesize) < 1073741824) {
              // 1024.0^3 =  1073741824
              result = Messages.get().getBundle(locale).key(
                  Messages.GUI_FILEUTIL_FILESIZE_MBYTES_1,
                  Double.valueOf(filesize / 1048576.0));
          } else {
              result = Messages.get().getBundle(locale).key(
                  Messages.GUI_FILEUTIL_FILESIZE_GBYTES_1,
                  Double.valueOf(filesize / 1073741824.0));
          }
          return result;
      }
  
      /**
       * Returns a comma separated list of resource paths names, with the site root
       * from the given OpenCms user context removed.<p>
       *
       * @param context the current users OpenCms context (optional, may be <code>null</code>)
       * @param resources a List of <code>{@link CmsResource}</code> instances to get the names from
       *
       * @return a comma separated list of resource paths names
       */
      public static String formatResourceNames(CmsRequestContext context, List<CmsResource> resources) {
  
          if (resources == null) {
              return null;
          }
          StringBuffer result = new StringBuffer(128);
          Iterator<CmsResource> i = resources.iterator();
          while (i.hasNext()) {
              CmsResource res = i.next();
              String path = res.getRootPath();
              if (context != null) {
                  path = context.removeSiteRoot(path);
              }
              result.append(path);
              if (i.hasNext()) {
                  result.append(", ");
              }
          }
          return result.toString();
      }
  
      /**
       * Returns the encoding of the file.
       * Encoding is read from the content-encoding property and defaults to the systems default encoding.
       * Since properties can change without rewriting content, the actual encoding can differ.
       *
       * @param cms {@link CmsObject} used to read properties of the given file.
       * @param file the file for which the encoding is requested
       * @return the file's encoding according to the content-encoding property, or the system's default encoding as default.
       */
      public static String getEncoding(CmsObject cms, CmsResource file) {
  
          CmsProperty encodingProperty = CmsProperty.getNullProperty();
          try {
              encodingProperty = cms.readPropertyObject(file, CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING, true);
          } catch (CmsException e) {
              LOG.debug(e.getLocalizedMessage(), e);
          }
          return CmsEncoder.lookupEncoding(encodingProperty.getValue(""), OpenCms.getSystemInfo().getDefaultEncoding());
      }
  
      /**
       * Returns the extension of the given resource name, that is the part behind the last '.' char,
       * converted to lower case letters.<p>
       *
       * The extension of a file is the part of the name after the last dot, including the dot.
       * The extension of a folder is empty.
       * All extensions are returned as lower case<p>
       *
       * Please note: No check is performed to ensure the given file name is not <code>null</code>.<p>
       *
       * Examples:<br>
       * <ul>
       *   <li><code>/folder.test/</code> has an empty extension.
       *   <li><code>/folder.test/config</code> has an empty extension.
       *   <li><code>/strange.filename.</code> has an empty extension.
       *   <li><code>/document.PDF</code> has the extension <code>.pdf</code>.
       * </ul>
       *
       * @param resourceName the resource to get the extension for
       *
       * @return the extension of a resource
       */
      public static String getExtension(String resourceName) {
  
          // if the resource name indicates a folder
          if (resourceName.charAt(resourceName.length() - 1) == '/') {
              // folders have no extensions
              return "";
          }
          // get just the name of the resource
          String name = CmsResource.getName(resourceName);
          // get the position of the last dot
          int pos = name.lastIndexOf('.');
          // if no dot or if no chars after the dot
          if ((pos < 0) || ((pos + 1) == name.length())) {
              return "";
          }
          // return the extension
          return name.substring(pos).toLowerCase();
      }
  
      /**
       * Returns the extension of the given file name, that is the part behind the last '.' char,
       * converted to lower case letters.<p>
       *
       * The result does contain the '.' char. For example, if the input is <code>"opencms.html"</code>,
       * then the result will be <code>".html"</code>.<p>
       *
       * If the given file name does not contain a '.' char, the empty String <code>""</code> is returned.<p>
       *
       * Please note: No check is performed to ensure the given file name is not <code>null</code>.<p>
       *
       * @param filename the file name to get the extension for
       * @return the extension of the given file name
       *
       * @deprecated use {@link #getExtension(String)} instead, it is better implemented
       */
      @Deprecated
      public static String getFileExtension(String filename) {
  
          int pos = filename.lastIndexOf('.');
          return (pos >= 0) ? filename.substring(pos).toLowerCase() : "";
      }
  
      /**
       * Returns a list of all filtered files in the RFS.<p>
       *
       * If the <code>name</code> is not a folder the folder that contains the
       * given file will be used instead.<p>
       *
       * Despite the filter may not accept folders, every subfolder is traversed
       * if the <code>includeSubtree</code> parameter is set.<p>
       *
       * @param name a folder or file name
       * @param filter a filter
       * @param includeSubtree if to include subfolders
       *
       * @return a list of filtered <code>{@link File}</code> objects
       */
      public static List<File> getFiles(String name, FileFilter filter, boolean includeSubtree) {
  
          List<File> ret = new ArrayList<File>();
  
          File file = new File(name);
          if (!file.isDirectory()) {
              file = new File(file.getParent());
              if (!file.isDirectory()) {
                  return ret;
              }
          }
          File[] dirContent = file.listFiles();
          for (int i = 0; i < dirContent.length; i++) {
              File f = dirContent[i];
              if (filter.accept(f)) {
                  ret.add(f);
              }
              if (includeSubtree && f.isDirectory()) {
                  ret.addAll(getFiles(f.getAbsolutePath(), filter, true));
              }
          }
  
          return ret;
      }
  
      /**
       * Returns the file name for a given VFS name that has to be written to a repository in the "real" file system,
       * by appending the VFS root path to the given base repository path, also adding an
       * folder for the "online" or "offline" project.<p>
       *
       * @param repository the base repository path
       * @param vfspath the VFS root path to write to use
       * @param online flag indicates if the result should be used for the online project (<code>true</code>) or not
       *
       * @return The full uri to the JSP
       */
      public static String getRepositoryName(String repository, String vfspath, boolean online) {
  
          StringBuffer result = new StringBuffer(64);
          result.append(repository);
          result.append(online ? CmsFlexCache.REPOSITORY_ONLINE : CmsFlexCache.REPOSITORY_OFFLINE);
          result.append(vfspath);
          return result.toString();
      }
  
      /**
       * Creates unique, valid RFS name for the given filename that contains
       * a coded version of the given parameters, with the given file extension appended.<p>
       *
       * This is used to create file names for the static export,
       * or in a vfs disk cache.<p>
       *
       * @param filename the base file name
       * @param extension the extension to use
       * @param parameters the parameters to code in the result file name
       *
       * @return a unique, valid RFS name for the given parameters
       *
       * @see org.opencms.staticexport.CmsStaticExportManager
       */
      public static String getRfsPath(String filename, String extension, String parameters) {
  
          StringBuffer buf = new StringBuffer(128);
          buf.append(filename);
          buf.append('_');
          int h = parameters.hashCode();
          // ensure we do have a positive id value
          buf.append(h > 0 ? h : -h);
          buf.append(extension);
          return buf.toString();
      }
  
      /**
       * Normalizes a file path that might contain <code>'../'</code> or <code>'./'</code> or <code>'//'</code>
       * elements to a normal absolute path, the path separator char used is {@link File#separatorChar}.<p>
       *
       * @param path the path to normalize
       *
       * @return the normalized path
       *
       * @see #normalizePath(String, char)
       */
      public static String normalizePath(String path) {
  
          return normalizePath(path, File.separatorChar);
      }
  
      /**
       * Normalizes a file path that might contain <code>'../'</code> or <code>'./'</code> or <code>'//'</code>
       * elements to a normal absolute path.<p>
       *
       * Can also handle Windows like path information containing a drive letter,
       * like <code>C:\path\..\</code>.<p>
       *
       * @param path the path to normalize
       * @param separatorChar the file separator char to use, for example {@link File#separatorChar}
       *
       * @return the normalized path
       */
      public static String normalizePath(String path, char separatorChar) {
  
          if (CmsStringUtil.isNotEmpty(path)) {
  
              // handle windows paths including drive-information first
              String drive = null;
              if ((path.length() > 1) && (path.charAt(1) == ':')) {
                  // windows path like C:\home\
                  drive = path.substring(0, 2);
                  path = path.substring(2);
              } else if ((path.length() > 1) && (path.charAt(0) == '\\') && (path.charAt(1) == '\\')) {
                  // windows path like \\home\ (network mapped drives)
                  drive = path.substring(0, 2);
                  path = path.substring(2);
              }
              // ensure all File separators are '/'
              path = path.replace('\\', '/');
              if (drive != null) {
                  drive = drive.replace('\\', '/');
              }
              if (path.charAt(0) == '/') {
                  // trick to resolve all ../ inside a path
                  path = '.' + path;
              }
              // resolve all '../' or './' elements in the path
              path = CmsLinkManager.getAbsoluteUri(path, "/");
              // still some '//' elements might persist
              path = CmsStringUtil.substitute(path, "//", "/");
              // re-append drive if required
              if (drive != null) {
                  path = drive.concat(path);
              }
              // switch '/' back to OS dependend File separator if required
              if (separatorChar != '/') {
                  path = path.replace('/', separatorChar);
              }
          }
          return path;
      }
  
      /**
       * Returns the normalized file path created from the given URL.<p>
       *
       * The path part {@link URL#getPath()} is used, unescaped and
       * normalized using {@link #normalizePath(String, char)} using {@link File#separatorChar}.<p>
       *
       * @param url the URL to extract the path information from
       *
       * @return the normalized file path created from the given URL using {@link File#separatorChar}
       *
       * @see #normalizePath(URL, char)
       */
      public static String normalizePath(URL url) {
  
          return normalizePath(url, File.separatorChar);
      }
  
      /**
       * Returns the normalized file path created from the given URL.<p>
       *
       * The path part {@link URL#getPath()} is used, unescaped and
       * normalized using {@link #normalizePath(String, char)}.<p>
       *
       * @param url the URL to extract the path information from
       * @param separatorChar the file separator char to use, for example {@link File#separatorChar}
       *
       * @return the normalized file path created from the given URL
       */
      public static String normalizePath(URL url, char separatorChar) {
  
          // get the path part from the URL
          String path = new File(url.getPath()).getAbsolutePath();
          // trick to get the OS default encoding, taken from the official Java i18n FAQ
          String systemEncoding = (new OutputStreamWriter(new ByteArrayOutputStream())).getEncoding();
          // decode url in order to remove spaces and escaped chars from path
          return CmsFileUtil.normalizePath(CmsEncoder.decode(path, systemEncoding), separatorChar);
      }
  
      /**
       * Deletes a directory in the file system and all subfolders of that directory.<p>
       *
       * @param directory the directory to delete
       */
      public static void purgeDirectory(File directory) {
  
          if (directory.canRead() && directory.isDirectory()) {
              File[] files = directory.listFiles();
              for (int i = 0; i < files.length; i++) {
                  File f = files[i];
                  if (f.isDirectory()) {
                      purgeDirectory(f);
                  }
                  if (f.canWrite()) {
                      f.delete();
                  }
              }
              directory.delete();
          }
      }
  
      /**
       * Reads a file from the RFS and returns the file content.<p>
       *
       * @param file the file to read
       * @return the read file content
       *
       * @throws IOException in case of file access errors
       */
      @SuppressWarnings("resource")
      public static byte[] readFile(File file) throws IOException {
  
          // create input and output stream
          FileInputStream in = new FileInputStream(file);
  
          // read the content
          return readFully(in, (int)file.length());
      }
  
      /**
       * Reads a file with the given name from the class loader and returns the file content.<p>
       *
       * @param filename the file to read
       * @return the read file content
       *
       * @throws IOException in case of file access errors
       */
      @SuppressWarnings("resource")
      public static byte[] readFile(String filename) throws IOException {
  
          // create input and output stream
          InputStream in = CmsFileUtil.class.getClassLoader().getResourceAsStream(filename);
          if (in == null) {
              throw new FileNotFoundException(filename);
          }
  
          return readFully(in);
      }
  
      /**
       * Reads a file from the class loader and converts it to a String with the specified encoding.<p>
       *
       * @param filename the file to read
       * @param encoding the encoding to use when converting the file content to a String
       * @return the read file convered to a String
       * @throws IOException in case of file access errors
       */
      public static String readFile(String filename, String encoding) throws IOException {
  
          return new String(readFile(filename), encoding);
      }
  
      /**
       * Reads all bytes from the given input stream, closes it
       * and returns the result in an array.<p>
       *
       * @param in the input stream to read the bytes from
       * @return the byte content of the input stream
       *
       * @throws IOException in case of errors in the underlying java.io methods used
       */
      public static byte[] readFully(InputStream in) throws IOException {
  
          return readFully(in, true);
      }
  
      /**
       * Reads all bytes from the given input stream, conditionally closes the given input stream
       * and returns the result in an array.<p>
       *
       * @param in the input stream to read the bytes from
       * @return the byte content of the input stream
       * @param closeInputStream if true the given stream will be closed afterwards
       *
       * @throws IOException in case of errors in the underlying java.io methods used
       */
      public static byte[] readFully(InputStream in, boolean closeInputStream) throws IOException {
  
          if (in instanceof ByteArrayInputStream) {
              // content can be read in one pass
              return readFully(in, in.available(), closeInputStream);
          }
  
          // copy buffer
          byte[] xfer = new byte[2048];
          // output buffer
          ByteArrayOutputStream out = new ByteArrayOutputStream(xfer.length);
  
          // transfer data from input to output in xfer-sized chunks.
          for (int bytesRead = in.read(xfer, 0, xfer.length); bytesRead >= 0; bytesRead = in.read(xfer, 0, xfer.length)) {
              if (bytesRead > 0) {
                  out.write(xfer, 0, bytesRead);
              }
          }
          if (closeInputStream) {
              in.close();
          }
          out.close();
          return out.toByteArray();
      }
  
      /**
       * Reads the specified number of bytes from the given input stream and returns the result in an array.<p>
       *
       * @param in the input stream to read the bytes from
       * @param size the number of bytes to read
       *
       * @return the byte content read from the input stream
       *
       * @throws IOException in case of errors in the underlying java.io methods used
       */
      public static byte[] readFully(InputStream in, int size) throws IOException {
  
          return readFully(in, size, true);
      }
  
      /**
       * Reads the specified number of bytes from the given input stream, conditionally closes the stream
       * and returns the result in an array.<p>
       *
       * @param in the input stream to read the bytes from
       * @param size the number of bytes to read
       * @param closeStream if true the given stream will be closed
       *
       * @return the byte content read from the input stream
       *
       * @throws IOException in case of errors in the underlying java.io methods used
       */
      public static byte[] readFully(InputStream in, int size, boolean closeStream) throws IOException {
  
          // create the byte array to hold the data
          byte[] bytes = new byte[size];
  
          // read in the bytes
          int offset = 0;
          
          try {
              int numRead = 0;
              while (offset < size) {
                  numRead = in.read(bytes, offset, size - offset);
                  if (numRead >= 0) {
                      offset += numRead;
                  } else {
                      break;
                  }
              }
          } finally {
              // close the input stream
              if (closeStream) {
                  in.close();
              }
          }
  
          // ensure all the bytes have been read in
          if (offset < bytes.length) {
              throw new IOException("Could not read requested " + size + " bytes from input stream");
          }
  
          return bytes;
      }
  
      /**
       * Removes a leading separator from a path if required.<p>
       *
       * @param path the path to remove the leading separator from
       * @return the path without a trailing separator
       */
      public static String removeLeadingSeparator(String path) {
  
          int l = path.length();
          if (l == 0) {
              return "";
          } else if (path.charAt(0) != '/') {
              return path;
          } else if (l == 1) {
              return "";
          } else {
              return path.substring(1, l);
          }
      }
  
      /**
       * Removes all resource names in the given List that are "redundant" because the parent folder name
       * is also contained in the List.<p>
       *
       * The content of the input list is not modified.<p>
       *
       * @param resourcenames a list of VFS pathnames to check for redundencies (Strings)
       *
       * @return a new list with all redundancies removed
       *
       * @see #removeRedundantResources(List)
       */
      public static List<String> removeRedundancies(List<String> resourcenames) {
  
          if ((resourcenames == null) || (resourcenames.isEmpty())) {
              return new ArrayList<String>();
          }
          if (resourcenames.size() == 1) {
              // if there is only one resource name in the list, there can be no redundancies
              return new ArrayList<String>(resourcenames);
          }
          // check all resources names and see if a parent folder name is contained
          List<String> result = new ArrayList<String>(resourcenames.size());
          List<String> base = new ArrayList<String>(resourcenames);
          Collections.sort(base);
          Iterator<String> i = base.iterator();
          while (i.hasNext()) {
              // check all resource names in the list
              String resourcename = i.next();
              if (CmsStringUtil.isEmptyOrWhitespaceOnly(resourcename)) {
                  // skip empty strings
                  continue;
              }
              boolean valid = true;
              for (int j = (result.size() - 1); j >= 0; j--) {
                  // check if this resource name is indirectly contained because a parent folder name is contained
                  String check = result.get(j);
                  if ((CmsResource.isFolder(check) && resourcename.startsWith(check)) || resourcename.equals(check)) {
                      valid = false;
                      break;
                  }
              }
              if (valid) {
                  // a parent folder name is not already contained in the result
                  result.add(resourcename);
              }
          }
          return result;
      }
  
      /**
       * Removes all resources in the given List that are "redundant" because the parent folder
       * is also contained in the List.<p>
       *
       * The content of the input list is not modified.<p>
       *
       * @param resources a list of <code>{@link CmsResource}</code> objects to check for redundancies
       *
       * @return a the given list with all redundancies removed
       *
       * @see #removeRedundancies(List)
       */
      public static List<CmsResource> removeRedundantResources(List<CmsResource> resources) {
  
          if ((resources == null) || (resources.isEmpty())) {
              return new ArrayList<CmsResource>();
          }
          if (resources.size() == 1) {
              // if there is only one resource in the list, there can be no redundancies
              return new ArrayList<CmsResource>(resources);
          }
          // check all resources and see if a parent folder name is contained
          List<CmsResource> result = new ArrayList<CmsResource>(resources.size());
          List<CmsResource> base = new ArrayList<CmsResource>(resources);
          Collections.sort(base);
          Iterator<CmsResource> i = base.iterator();
          while (i.hasNext()) {
              // check all folders in the list
              CmsResource resource = i.next();
              boolean valid = true;
              for (int j = (result.size() - 1); j >= 0; j--) {
                  // check if this resource is indirectly contained because a parent folder is contained
                  CmsResource check = result.get(j);
                  if ((check.isFolder() && resource.getRootPath().startsWith(check.getRootPath()))
                      || resource.getRootPath().equals(check.getRootPath())) {
                      valid = false;
                      break;
                  }
              }
              if (valid) {
                  // the parent folder is not already contained in the result
                  result.add(resource);
              }
          }
          return result;
      }
  
      /**
       * Removes a trailing separator from a path if required.<p>
       *
       * In case we have the root folder "/", the separator is not removed.<p>
       *
       * @param path the path to remove the trailing separator from
       * @return the path without a trailing separator
       */
      public static String removeTrailingSeparator(String path) {
  
          int l = path.length();
          if ((l <= 1) || (path.charAt(l - 1) != '/')) {
              return path;
          } else {
              return path.substring(0, l - 1);
          }
      }
  
      /**
       * Searches for the OpenCms web application 'WEB-INF' folder during system startup, code or
       * <code>null</code> if the 'WEB-INF' folder can not be found.<p>
       *
       * @param startFolder the folder where to start searching
       *
       * @return String the path of the 'WEB-INF' folder in the 'real' file system, or <code>null</code>
       */
      public static String searchWebInfFolder(String startFolder) {
  
          if (CmsStringUtil.isEmpty(startFolder)) {
              return null;
          }
  
          File f = new File(startFolder);
          if (!f.exists() || !f.isDirectory()) {
              return null;
          }
  
          File configFile = new File(f, CmsSystemInfo.FILE_TLD);
          if (configFile.exists() && configFile.isFile()) {
              return f.getAbsolutePath();
          }
  
          String webInfFolder = null;
          File[] subFiles = f.listFiles();
          List<File> fileList = new ArrayList<File>(Arrays.asList(subFiles));
          Collections.sort(fileList, new Comparator<File>() {
  
              public int compare(File arg0, File arg1) {
  
                  // make sure that the WEB-INF folder, if it has that name, comes earlier
                  boolean a = arg0.getPath().contains("WEB-INF");
                  boolean b = arg1.getPath().contains("WEB-INF");
                  return Boolean.valueOf(b).compareTo(Boolean.valueOf(a));
  
              }
          });
  
          for (File file : fileList) {
              if (file.isDirectory()) {
                  webInfFolder = searchWebInfFolder(file.getAbsolutePath());
                  if (webInfFolder != null) {
                      break;
                  }
              }
          }
  
          return webInfFolder;
      }
  
      public static String toggleTrailingSeparator(String path) {
  
          if (path.endsWith("/")) {
              return path.substring(0, path.length() - 1);
          } else {
              return path + "/";
          }
      }
  
      /**
       * Traverses the file system starting from a base folder and executes a callback for every directory found.<p>
       *
       * @param base the base folder
       * @param action a callback which will be passed a FileWalkState object for every directory encountered
       */
      public static void walkFileSystem(File base, Closure action) {
  
          List<FileWalkState> m_states = new ArrayList<FileWalkState>();
          m_states.add(createFileWalkState(base));
          while (!m_states.isEmpty()) {
              // pop the top off the state stack, process it, then push states for all subdirectories onto it
              FileWalkState last = m_states.remove(m_states.size() - 1);
              action.execute(last);
              for (File dir : last.getDirectories()) {
                  m_states.add(createFileWalkState(dir));
              }
          }
      }
  
      /**
       * Helper method for creating a FileWalkState object from a File object.<p>
       *
       * @param file the file
       *
       * @return the file walk state
       */
      private static FileWalkState createFileWalkState(File file) {
  
          File[] contents = file.listFiles();
          List<File> dirs = new ArrayList<File>();
          List<File> files = new ArrayList<File>();
          for (File subFile : contents) {
              if (subFile.isDirectory()) {
                  dirs.add(subFile);
              } else {
                  files.add(subFile);
              }
          }
          return new FileWalkState(file, dirs, files);
      }
  }