DefaultVFS.java

  1. /*
  2.  *    Copyright 2010-2023 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.migration.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 java.util.logging.Level;
  34. import java.util.logging.Logger;

  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 Logger log = Logger.getLogger(DefaultVFS.class.getName());

  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.isLoggable(Level.FINER)) {
  59.           log.log(Level.FINER, "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.isLoggable(Level.FINER)) {
  71.                 log.log(Level.FINER, "Listing " + url);
  72.               }
  73.               for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
  74.                 if (log.isLoggable(Level.FINER)) {
  75.                   log.log(Level.FINER, "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 text file, listing the child
  83.              * resources one per line. However, there is no way to differentiate between directory and file resources
  84.              * just by reading them. To work around that, as each line is read, try to look it up via the class loader
  85.              * as a child of the current resource. If any line fails then we assume the current resource is not a
  86.              * directory.
  87.              */
  88.             is = url.openStream();
  89.             List<String> lines = new ArrayList<>();
  90.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
  91.               for (String line; (line = reader.readLine()) != null;) {
  92.                 if (log.isLoggable(Level.FINER)) {
  93.                   log.log(Level.FINER, "Reader entry: " + line);
  94.                 }
  95.                 lines.add(line);
  96.                 if (getResources(path + "/" + line).isEmpty()) {
  97.                   lines.clear();
  98.                   break;
  99.                 }
  100.               }
  101.             } catch (InvalidPathException e) {
  102.               // #1974
  103.               lines.clear();
  104.             }
  105.             if (!lines.isEmpty()) {
  106.               if (log.isLoggable(Level.FINER)) {
  107.                 log.log(Level.FINER, "Listing " + url);
  108.               }
  109.               children.addAll(lines);
  110.             }
  111.           }
  112.         } catch (FileNotFoundException e) {
  113.           /*
  114.            * For file URLs the openStream() call might fail, depending on the servlet container, because directories
  115.            * can't be opened for reading. If that happens, then list the directory directly instead.
  116.            */
  117.           if (!"file".equals(url.getProtocol())) {
  118.             // No idea where the exception came from so rethrow it
  119.             throw e;
  120.           }
  121.           File file = new File(url.getFile());
  122.           if (log.isLoggable(Level.FINER)) {
  123.             log.log(Level.FINER, "Listing directory " + file.getAbsolutePath());
  124.           }
  125.           if (file.isDirectory()) {
  126.             if (log.isLoggable(Level.FINER)) {
  127.               log.log(Level.FINER, "Listing " + url);
  128.             }
  129.             children = Arrays.asList(file.list());
  130.           }
  131.         }

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

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

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

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

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

  187.         // Check file name
  188.         if (name.indexOf(path) == 0) {
  189.           if (log.isLoggable(Level.FINER)) {
  190.             log.log(Level.FINER, "Found resource: " + name);
  191.           }
  192.           // Trim leading slash
  193.           resources.add(name.substring(1));
  194.         }
  195.       }
  196.     }
  197.     return resources;
  198.   }

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

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

  229.     // Look for the .jar extension and chop off everything after that
  230.     StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
  231.     int index = jarUrl.lastIndexOf(".jar");
  232.     if (index < 0) {
  233.       if (log.isLoggable(Level.FINER)) {
  234.         log.log(Level.FINER, "Not a JAR: " + jarUrl);
  235.       }
  236.       return null;
  237.     }
  238.     jarUrl.setLength(index + 4);
  239.     if (log.isLoggable(Level.FINER)) {
  240.       log.log(Level.FINER, "Extracted JAR URL: " + jarUrl);
  241.     }

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

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

  262.       if (file.exists()) {
  263.         if (log.isLoggable(Level.FINER)) {
  264.           log.log(Level.FINER, "Trying real file: " + file.getAbsolutePath());
  265.         }
  266.         testUrl = file.toURI().toURL();
  267.         if (isJar(testUrl)) {
  268.           return testUrl;
  269.         }
  270.       }
  271.     } catch (MalformedURLException e) {
  272.       log.log(Level.WARNING, "Invalid JAR URL: " + jarUrl);
  273.     }

  274.     if (log.isLoggable(Level.FINER)) {
  275.       log.log(Level.FINER, "Not a JAR: " + jarUrl);
  276.     }
  277.     return null;
  278.   }

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

  291.   /**
  292.    * Returns true if the resource located at the given URL is a JAR file.
  293.    *
  294.    * @param url
  295.    *          The URL of the resource to test.
  296.    *
  297.    * @return true, if is jar
  298.    */
  299.   protected boolean isJar(URL url) {
  300.     return isJar(url, new byte[JAR_MAGIC.length]);
  301.   }

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

  325.     return false;
  326.   }
  327. }