DefaultVFS.java

  1. /*
  2.  *    Copyright 2009-2021 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.  *       http://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.file.InvalidPathException;
  28. import java.util.ArrayList;
  29. import java.util.Arrays;
  30. import java.util.List;
  31. import java.util.jar.JarEntry;
  32. import java.util.jar.JarInputStream;

  33. import org.apache.ibatis.logging.Log;
  34. import org.apache.ibatis.logging.LogFactory;

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

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

  44.   @Override
  45.   public boolean isValid() {
  46.     return true;
  47.   }

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

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

  135.         // The URL prefix to use when recursively listing child resources
  136.         String prefix = url.toExternalForm();
  137.         if (!prefix.endsWith("/")) {
  138.           prefix = prefix + "/";
  139.         }

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

  148.       return resources;
  149.     } finally {
  150.       if (is != null) {
  151.         try {
  152.           is.close();
  153.         } catch (Exception e) {
  154.           // Ignore
  155.         }
  156.       }
  157.     }
  158.   }

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

  176.     // Iterate over the entries and collect those that begin with the requested path
  177.     List<String> resources = new ArrayList<>();
  178.     for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
  179.       if (!entry.isDirectory()) {
  180.         // Add leading slash if it's missing
  181.         StringBuilder name = new StringBuilder(entry.getName());
  182.         if (name.charAt(0) != '/') {
  183.           name.insert(0, '/');
  184.         }

  185.         // Check file name
  186.         if (name.indexOf(path) == 0) {
  187.           if (log.isDebugEnabled()) {
  188.             log.debug("Found resource: " + name);
  189.           }
  190.           // Trim leading slash
  191.           resources.add(name.substring(1));
  192.         }
  193.       }
  194.     }
  195.     return resources;
  196.   }

  197.   /**
  198.    * Attempts to deconstruct the given URL to find a JAR file containing the resource referenced
  199.    * by the URL. That is, assuming the URL references a JAR entry, this method will return a URL
  200.    * that references the JAR file containing the entry. If the JAR cannot be located, then this
  201.    * method returns null.
  202.    *
  203.    * @param url The URL of the JAR entry.
  204.    * @return The URL of the JAR file, if one is found. Null if not.
  205.    * @throws MalformedURLException
  206.    *           the malformed URL exception
  207.    */
  208.   protected URL findJarForResource(URL url) throws MalformedURLException {
  209.     if (log.isDebugEnabled()) {
  210.       log.debug("Find JAR URL: " + url);
  211.     }

  212.     // If the file part of the URL is itself a URL, then that URL probably points to the JAR
  213.     boolean continueLoop = true;
  214.     while (continueLoop) {
  215.       try {
  216.         url = new URL(url.getFile());
  217.         if (log.isDebugEnabled()) {
  218.           log.debug("Inner URL: " + url);
  219.         }
  220.       } catch (MalformedURLException e) {
  221.         // This will happen at some point and serves as a break in the loop
  222.         continueLoop = false;
  223.       }
  224.     }

  225.     // Look for the .jar extension and chop off everything after that
  226.     StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
  227.     int index = jarUrl.lastIndexOf(".jar");
  228.     if (index >= 0) {
  229.       jarUrl.setLength(index + 4);
  230.       if (log.isDebugEnabled()) {
  231.         log.debug("Extracted JAR URL: " + jarUrl);
  232.       }
  233.     } else {
  234.       if (log.isDebugEnabled()) {
  235.         log.debug("Not a JAR: " + jarUrl);
  236.       }
  237.       return null;
  238.     }

  239.     // Try to open and test it
  240.     try {
  241.       URL testUrl = new URL(jarUrl.toString());
  242.       if (isJar(testUrl)) {
  243.         return testUrl;
  244.       } else {
  245.         // WebLogic fix: check if the URL's file exists in the filesystem.
  246.         if (log.isDebugEnabled()) {
  247.           log.debug("Not a JAR: " + jarUrl);
  248.         }
  249.         jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
  250.         File file = new File(jarUrl.toString());

  251.         // File name might be URL-encoded
  252.         if (!file.exists()) {
  253.           try {
  254.             file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
  255.           } catch (UnsupportedEncodingException e) {
  256.             throw new RuntimeException("Unsupported encoding?  UTF-8?  That's impossible.");
  257.           }
  258.         }

  259.         if (file.exists()) {
  260.           if (log.isDebugEnabled()) {
  261.             log.debug("Trying real file: " + file.getAbsolutePath());
  262.           }
  263.           testUrl = file.toURI().toURL();
  264.           if (isJar(testUrl)) {
  265.             return testUrl;
  266.           }
  267.         }
  268.       }
  269.     } catch (MalformedURLException e) {
  270.       log.warn("Invalid JAR URL: " + jarUrl);
  271.     }

  272.     if (log.isDebugEnabled()) {
  273.       log.debug("Not a JAR: " + jarUrl);
  274.     }
  275.     return null;
  276.   }

  277.   /**
  278.    * Converts a Java package name to a path that can be looked up with a call to
  279.    * {@link ClassLoader#getResources(String)}.
  280.    *
  281.    * @param packageName
  282.    *          The Java package name to convert to a path
  283.    * @return the package path
  284.    */
  285.   protected String getPackagePath(String packageName) {
  286.     return packageName == null ? null : packageName.replace('.', '/');
  287.   }

  288.   /**
  289.    * Returns true if the resource located at the given URL is a JAR file.
  290.    *
  291.    * @param url
  292.    *          The URL of the resource to test.
  293.    * @return true, if is jar
  294.    */
  295.   protected boolean isJar(URL url) {
  296.     return isJar(url, new byte[JAR_MAGIC.length]);
  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.    * @param buffer
  304.    *          A buffer into which the first few bytes of the resource are read. The buffer must be at least the size of
  305.    *          {@link #JAR_MAGIC}. (The same buffer may be reused for multiple calls as an optimization.)
  306.    * @return true, if is jar
  307.    */
  308.   protected boolean isJar(URL url, byte[] buffer) {
  309.     try (InputStream is = url.openStream()) {
  310.       is.read(buffer, 0, JAR_MAGIC.length);
  311.       if (Arrays.equals(buffer, JAR_MAGIC)) {
  312.         if (log.isDebugEnabled()) {
  313.           log.debug("Found JAR: " + url);
  314.         }
  315.         return true;
  316.       }
  317.     } catch (Exception e) {
  318.       // Failure to read the stream means this is not a JAR
  319.     }

  320.     return false;
  321.   }
  322. }