DefaultVFS.java

  1. /*
  2.  *    Copyright 2009-2024 the original author or authors.
  3.  *
  4.  *    Licensed under the Apache License, Version 2.0 (the "License");
  5.  *    you may not use this file except in compliance with the License.
  6.  *    You may obtain a copy of the License at
  7.  *
  8.  *       https://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  *    Unless required by applicable law or agreed to in writing, software
  11.  *    distributed under the License is distributed on an "AS IS" BASIS,
  12.  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  *    See the License for the specific language governing permissions and
  14.  *    limitations under the License.
  15.  */
  16. package org.apache.ibatis.io;

  17. import java.io.BufferedReader;
  18. import java.io.File;
  19. import java.io.FileNotFoundException;
  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.io.InputStreamReader;
  23. import java.io.UnsupportedEncodingException;
  24. import java.net.MalformedURLException;
  25. import java.net.URL;
  26. import java.net.URLEncoder;
  27. import java.nio.charset.StandardCharsets;
  28. import java.nio.file.FileSystemException;
  29. import java.nio.file.InvalidPathException;
  30. import java.util.ArrayList;
  31. import java.util.Arrays;
  32. import java.util.List;
  33. import java.util.jar.JarEntry;
  34. import java.util.jar.JarInputStream;

  35. import org.apache.ibatis.logging.Log;
  36. import org.apache.ibatis.logging.LogFactory;

  37. /**
  38.  * A default implementation of {@link VFS} that works for most application servers.
  39.  *
  40.  * @author Ben Gunter
  41.  */
  42. public class DefaultVFS extends VFS {
  43.   private static final Log log = LogFactory.getLog(DefaultVFS.class);

  44.   /** The magic header that indicates a JAR (ZIP) file. */
  45.   private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 };

  46.   @Override
  47.   public boolean isValid() {
  48.     return true;
  49.   }

  50.   @Override
  51.   public List<String> list(URL url, String path) throws IOException {
  52.     InputStream is = null;
  53.     try {
  54.       List<String> resources = new ArrayList<>();

  55.       // First, try to find the URL of a JAR file containing the requested resource. If a JAR
  56.       // file is found, then we'll list child resources by reading the JAR.
  57.       URL jarUrl = findJarForResource(url);
  58.       if (jarUrl != null) {
  59.         is = jarUrl.openStream();
  60.         if (log.isDebugEnabled()) {
  61.           log.debug("Listing " + url);
  62.         }
  63.         resources = listResources(new JarInputStream(is), path);
  64.       } else {
  65.         List<String> children = new ArrayList<>();
  66.         try {
  67.           if (isJar(url)) {
  68.             // Some versions of JBoss VFS might give a JAR stream even if the resource
  69.             // referenced by the URL isn't actually a JAR
  70.             is = url.openStream();
  71.             try (JarInputStream jarInput = new JarInputStream(is)) {
  72.               if (log.isDebugEnabled()) {
  73.                 log.debug("Listing " + url);
  74.               }
  75.               File destinationDir = new File(path);
  76.               for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
  77.                 if (log.isDebugEnabled()) {
  78.                   log.debug("Jar entry: " + entry.getName());
  79.                 }
  80.                 File entryFile = new File(destinationDir, entry.getName()).getCanonicalFile();
  81.                 if (!entryFile.getPath().startsWith(destinationDir.getCanonicalPath())) {
  82.                   throw new IOException("Bad zip entry: " + entry.getName());
  83.                 }
  84.                 children.add(entry.getName());
  85.               }
  86.             }
  87.           } else {
  88.             /*
  89.              * Some servlet containers allow reading from directory resources like a text file, listing the child
  90.              * resources one per line. However, there is no way to differentiate between directory and file resources
  91.              * just by reading them. To work around that, as each line is read, try to look it up via the class loader
  92.              * as a child of the current resource. If any line fails then we assume the current resource is not a
  93.              * directory.
  94.              */
  95.             is = url.openStream();
  96.             List<String> lines = new ArrayList<>();
  97.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
  98.               for (String line; (line = reader.readLine()) != null;) {
  99.                 if (log.isDebugEnabled()) {
  100.                   log.debug("Reader entry: " + line);
  101.                 }
  102.                 lines.add(line);
  103.                 if (getResources(path + "/" + line).isEmpty()) {
  104.                   lines.clear();
  105.                   break;
  106.                 }
  107.               }
  108.             } catch (InvalidPathException | FileSystemException e) {
  109.               // #1974 #2598
  110.               lines.clear();
  111.             }
  112.             if (!lines.isEmpty()) {
  113.               if (log.isDebugEnabled()) {
  114.                 log.debug("Listing " + url);
  115.               }
  116.               children.addAll(lines);
  117.             }
  118.           }
  119.         } catch (FileNotFoundException e) {
  120.           /*
  121.            * For file URLs the openStream() call might fail, depending on the servlet container, because directories
  122.            * can't be opened for reading. If that happens, then list the directory directly instead.
  123.            */
  124.           if (!"file".equals(url.getProtocol())) {
  125.             // No idea where the exception came from so rethrow it
  126.             throw e;
  127.           }
  128.           File file = new File(url.getFile());
  129.           if (log.isDebugEnabled()) {
  130.             log.debug("Listing directory " + file.getAbsolutePath());
  131.           }
  132.           if (file.isDirectory()) {
  133.             if (log.isDebugEnabled()) {
  134.               log.debug("Listing " + url);
  135.             }
  136.             children = Arrays.asList(file.list());
  137.           }
  138.         }

  139.         // The URL prefix to use when recursively listing child resources
  140.         String prefix = url.toExternalForm();
  141.         if (!prefix.endsWith("/")) {
  142.           prefix = prefix + "/";
  143.         }

  144.         // Iterate over immediate children, adding files and recurring into directories
  145.         for (String child : children) {
  146.           String resourcePath = path + "/" + child;
  147.           resources.add(resourcePath);
  148.           URL childUrl = new URL(prefix + child);
  149.           resources.addAll(list(childUrl, resourcePath));
  150.         }
  151.       }

  152.       return resources;
  153.     } finally {
  154.       if (is != null) {
  155.         try {
  156.           is.close();
  157.         } catch (Exception e) {
  158.           // Ignore
  159.         }
  160.       }
  161.     }
  162.   }

  163.   /**
  164.    * List the names of the entries in the given {@link JarInputStream} that begin with the specified {@code path}.
  165.    * Entries will match with or without a leading slash.
  166.    *
  167.    * @param jar
  168.    *          The JAR input stream
  169.    * @param path
  170.    *          The leading path to match
  171.    *
  172.    * @return The names of all the matching entries
  173.    *
  174.    * @throws IOException
  175.    *           If I/O errors occur
  176.    */
  177.   protected List<String> listResources(JarInputStream jar, String path) throws IOException {
  178.     // Include the leading and trailing slash when matching names
  179.     if (!path.startsWith("/")) {
  180.       path = "/" + path;
  181.     }
  182.     if (!path.endsWith("/")) {
  183.       path = path + "/";
  184.     }

  185.     // Iterate over the entries and collect those that begin with the requested path
  186.     List<String> resources = new ArrayList<>();
  187.     for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
  188.       if (!entry.isDirectory()) {
  189.         // Add leading slash if it's missing
  190.         StringBuilder name = new StringBuilder(entry.getName());
  191.         if (name.charAt(0) != '/') {
  192.           name.insert(0, '/');
  193.         }

  194.         // Check file name
  195.         if (name.indexOf(path) == 0) {
  196.           if (log.isDebugEnabled()) {
  197.             log.debug("Found resource: " + name);
  198.           }
  199.           // Trim leading slash
  200.           resources.add(name.substring(1));
  201.         }
  202.       }
  203.     }
  204.     return resources;
  205.   }

  206.   /**
  207.    * Attempts to deconstruct the given URL to find a JAR file containing the resource referenced by the URL. That is,
  208.    * assuming the URL references a JAR entry, this method will return a URL that references the JAR file containing the
  209.    * entry. If the JAR cannot be located, then this method returns null.
  210.    *
  211.    * @param url
  212.    *          The URL of the JAR entry.
  213.    *
  214.    * @return The URL of the JAR file, if one is found. Null if not.
  215.    *
  216.    * @throws MalformedURLException
  217.    *           the malformed URL exception
  218.    */
  219.   protected URL findJarForResource(URL url) throws MalformedURLException {
  220.     if (log.isDebugEnabled()) {
  221.       log.debug("Find JAR URL: " + url);
  222.     }

  223.     // If the file part of the URL is itself a URL, then that URL probably points to the JAR
  224.     boolean continueLoop = true;
  225.     while (continueLoop) {
  226.       try {
  227.         url = new URL(url.getFile());
  228.         if (log.isDebugEnabled()) {
  229.           log.debug("Inner URL: " + url);
  230.         }
  231.       } catch (MalformedURLException e) {
  232.         // This will happen at some point and serves as a break in the loop
  233.         continueLoop = false;
  234.       }
  235.     }

  236.     // Look for the .jar extension and chop off everything after that
  237.     StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
  238.     int index = jarUrl.lastIndexOf(".jar");
  239.     if (index < 0) {
  240.       if (log.isDebugEnabled()) {
  241.         log.debug("Not a JAR: " + jarUrl);
  242.       }
  243.       return null;
  244.     }
  245.     jarUrl.setLength(index + 4);
  246.     if (log.isDebugEnabled()) {
  247.       log.debug("Extracted JAR URL: " + jarUrl);
  248.     }

  249.     // Try to open and test it
  250.     try {
  251.       URL testUrl = new URL(jarUrl.toString());
  252.       if (isJar(testUrl)) {
  253.         return testUrl;
  254.       }
  255.       // WebLogic fix: check if the URL's file exists in the filesystem.
  256.       if (log.isDebugEnabled()) {
  257.         log.debug("Not a JAR: " + jarUrl);
  258.       }
  259.       jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
  260.       File file = new File(jarUrl.toString());

  261.       // File name might be URL-encoded
  262.       if (!file.exists()) {
  263.         try {
  264.           file = new File(URLEncoder.encode(jarUrl.toString(), StandardCharsets.UTF_8.name()));
  265.         } catch (UnsupportedEncodingException e) {
  266.           throw new RuntimeException("Unsupported encoding?  UTF-8?  That's impossible.");
  267.         }
  268.       }

  269.       if (file.exists()) {
  270.         if (log.isDebugEnabled()) {
  271.           log.debug("Trying real file: " + file.getAbsolutePath());
  272.         }
  273.         testUrl = file.toURI().toURL();
  274.         if (isJar(testUrl)) {
  275.           return testUrl;
  276.         }
  277.       }
  278.     } catch (MalformedURLException e) {
  279.       log.warn("Invalid JAR URL: " + jarUrl);
  280.     }

  281.     if (log.isDebugEnabled()) {
  282.       log.debug("Not a JAR: " + jarUrl);
  283.     }
  284.     return null;
  285.   }

  286.   /**
  287.    * Converts a Java package name to a path that can be looked up with a call to
  288.    * {@link ClassLoader#getResources(String)}.
  289.    *
  290.    * @param packageName
  291.    *          The Java package name to convert to a path
  292.    *
  293.    * @return the package path
  294.    */
  295.   protected String getPackagePath(String packageName) {
  296.     return packageName == null ? null : packageName.replace('.', '/');
  297.   }

  298.   /**
  299.    * Returns true if the resource located at the given URL is a JAR file.
  300.    *
  301.    * @param url
  302.    *          The URL of the resource to test.
  303.    *
  304.    * @return true, if is jar
  305.    */
  306.   protected boolean isJar(URL url) {
  307.     return isJar(url, new byte[JAR_MAGIC.length]);
  308.   }

  309.   /**
  310.    * Returns true if the resource located at the given URL is a JAR file.
  311.    *
  312.    * @param url
  313.    *          The URL of the resource to test.
  314.    * @param buffer
  315.    *          A buffer into which the first few bytes of the resource are read. The buffer must be at least the size of
  316.    *          {@link #JAR_MAGIC}. (The same buffer may be reused for multiple calls as an optimization.)
  317.    *
  318.    * @return true, if is jar
  319.    */
  320.   protected boolean isJar(URL url, byte[] buffer) {
  321.     try (InputStream is = url.openStream()) {
  322.       is.read(buffer, 0, JAR_MAGIC.length);
  323.       if (Arrays.equals(buffer, JAR_MAGIC)) {
  324.         if (log.isDebugEnabled()) {
  325.           log.debug("Found JAR: " + url);
  326.         }
  327.         return true;
  328.       }
  329.     } catch (Exception e) {
  330.       // Failure to read the stream means this is not a JAR
  331.     }

  332.     return false;
  333.   }
  334. }