View Javadoc
1   /*
2    *    Copyright 2010-2025 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  
18  import java.io.BufferedReader;
19  import java.io.File;
20  import java.io.FileNotFoundException;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
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.InvalidPathException;
29  import java.nio.file.Path;
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 java.util.logging.Level;
36  import java.util.logging.Logger;
37  
38  /**
39   * A default implementation of {@link VFS} that works for most application servers.
40   *
41   * @author Ben Gunter
42   */
43  public class DefaultVFS extends VFS {
44    private static final Logger log = Logger.getLogger(DefaultVFS.class.getName());
45  
46    /** The magic header that indicates a JAR (ZIP) file. */
47    private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 };
48  
49    @Override
50    public boolean isValid() {
51      return true;
52    }
53  
54    @Override
55    public List<String> list(URL url, String path) throws IOException {
56      InputStream is = null;
57      try {
58        List<String> resources = new ArrayList<>();
59  
60        // First, try to find the URL of a JAR file containing the requested resource. If a JAR
61        // file is found, then we'll list child resources by reading the JAR.
62        URL jarUrl = findJarForResource(url);
63        if (jarUrl != null) {
64          is = jarUrl.openStream();
65          if (log.isLoggable(Level.FINER)) {
66            log.log(Level.FINER, "Listing " + url);
67          }
68          resources = listResources(new JarInputStream(is), path);
69        } else {
70          List<String> children = new ArrayList<>();
71          try {
72            if (isJar(url)) {
73              // Some versions of JBoss VFS might give a JAR stream even if the resource
74              // referenced by the URL isn't actually a JAR
75              is = url.openStream();
76              try (JarInputStream jarInput = new JarInputStream(is)) {
77                if (log.isLoggable(Level.FINER)) {
78                  log.log(Level.FINER, "Listing " + url);
79                }
80                for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
81                  if (log.isLoggable(Level.FINER)) {
82                    log.log(Level.FINER, "Jar 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.isLoggable(Level.FINER)) {
100                   log.log(Level.FINER, "Reader entry: " + line);
101                 }
102                 lines.add(line);
103                 if (getResources(path + "/" + line).isEmpty()) {
104                   lines.clear();
105                   break;
106                 }
107               }
108             } catch (InvalidPathException e) {
109               // #1974
110               lines.clear();
111             }
112             if (!lines.isEmpty()) {
113               if (log.isLoggable(Level.FINER)) {
114                 log.log(Level.FINER, "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 = Path.of(url.getFile()).toFile();
129           if (log.isLoggable(Level.FINER)) {
130             log.log(Level.FINER, "Listing directory " + file.getAbsolutePath());
131           }
132           if (file.isDirectory()) {
133             if (log.isLoggable(Level.FINER)) {
134               log.log(Level.FINER, "Listing " + url);
135             }
136             children = Arrays.asList(file.list());
137           }
138         }
139 
140         // The URL prefix to use when recursively listing child resources
141         String prefix = url.toExternalForm();
142         if (!prefix.endsWith("/")) {
143           prefix = prefix + "/";
144         }
145 
146         // Iterate over immediate children, adding files and recurring into directories
147         for (String child : children) {
148           String resourcePath = path + "/" + child;
149           resources.add(resourcePath);
150           URL childUrl = new URL(prefix + child);
151           resources.addAll(list(childUrl, resourcePath));
152         }
153       }
154 
155       return resources;
156     } finally {
157       if (is != null) {
158         try {
159           is.close();
160         } catch (Exception e) {
161           // Ignore
162         }
163       }
164     }
165   }
166 
167   /**
168    * List the names of the entries in the given {@link JarInputStream} that begin with the specified {@code path}.
169    * Entries will match with or without a leading slash.
170    *
171    * @param jar
172    *          The JAR input stream
173    * @param path
174    *          The leading path to match
175    *
176    * @return The names of all the matching entries
177    *
178    * @throws IOException
179    *           If I/O errors occur
180    */
181   protected List<String> listResources(JarInputStream jar, String path) throws IOException {
182     // Include the leading and trailing slash when matching names
183     if (!path.startsWith("/")) {
184       path = '/' + path;
185     }
186     if (!path.endsWith("/")) {
187       path = path + '/';
188     }
189 
190     // Iterate over the entries and collect those that begin with the requested path
191     List<String> resources = new ArrayList<>();
192     for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
193       if (!entry.isDirectory()) {
194         // Add leading slash if it's missing
195         StringBuilder name = new StringBuilder(entry.getName());
196         if (name.charAt(0) != '/') {
197           name.insert(0, '/');
198         }
199 
200         // Check file name
201         if (name.indexOf(path) == 0) {
202           if (log.isLoggable(Level.FINER)) {
203             log.log(Level.FINER, "Found resource: " + name);
204           }
205           // Trim leading slash
206           resources.add(name.substring(1));
207         }
208       }
209     }
210     return resources;
211   }
212 
213   /**
214    * Attempts to deconstruct the given URL to find a JAR file containing the resource referenced by the URL. That is,
215    * assuming the URL references a JAR entry, this method will return a URL that references the JAR file containing the
216    * entry. If the JAR cannot be located, then this method returns null.
217    *
218    * @param url
219    *          The URL of the JAR entry.
220    *
221    * @return The URL of the JAR file, if one is found. Null if not.
222    *
223    * @throws MalformedURLException
224    *           the malformed URL exception
225    */
226   protected URL findJarForResource(URL url) throws MalformedURLException {
227     if (log.isLoggable(Level.FINER)) {
228       log.log(Level.FINER, "Find JAR URL: " + url);
229     }
230 
231     // If the file part of the URL is itself a URL, then that URL probably points to the JAR
232     boolean continueLoop = true;
233     while (continueLoop) {
234       try {
235         url = new URL(url.getFile());
236         if (log.isLoggable(Level.FINER)) {
237           log.log(Level.FINER, "Inner URL: " + url);
238         }
239       } catch (MalformedURLException e) {
240         // This will happen at some point and serves as a break in the loop
241         continueLoop = false;
242       }
243     }
244 
245     // Look for the .jar extension and chop off everything after that
246     StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
247     int index = jarUrl.lastIndexOf(".jar");
248     if (index < 0) {
249       if (log.isLoggable(Level.FINER)) {
250         log.log(Level.FINER, "Not a JAR: " + jarUrl);
251       }
252       return null;
253     }
254     jarUrl.setLength(index + 4);
255     if (log.isLoggable(Level.FINER)) {
256       log.log(Level.FINER, "Extracted JAR URL: " + jarUrl);
257     }
258 
259     // Try to open and test it
260     try {
261       URL testUrl = new URL(jarUrl.toString());
262       if (isJar(testUrl)) {
263         return testUrl;
264       }
265       // WebLogic fix: check if the URL's file exists in the filesystem.
266       if (log.isLoggable(Level.FINER)) {
267         log.log(Level.FINER, "Not a JAR: " + jarUrl);
268       }
269       jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
270       File file = Path.of(jarUrl.toString()).toFile();
271 
272       // File name might be URL-encoded
273       if (!file.exists()) {
274         file = Path.of(URLEncoder.encode(jarUrl.toString(), StandardCharsets.UTF_8)).toFile();
275       }
276 
277       if (file.exists()) {
278         if (log.isLoggable(Level.FINER)) {
279           log.log(Level.FINER, "Trying real file: " + file.getAbsolutePath());
280         }
281         testUrl = file.toURI().toURL();
282         if (isJar(testUrl)) {
283           return testUrl;
284         }
285       }
286     } catch (MalformedURLException e) {
287       log.log(Level.WARNING, "Invalid JAR URL: " + jarUrl);
288     }
289 
290     if (log.isLoggable(Level.FINER)) {
291       log.log(Level.FINER, "Not a JAR: " + jarUrl);
292     }
293     return null;
294   }
295 
296   /**
297    * Converts a Java package name to a path that can be looked up with a call to
298    * {@link ClassLoader#getResources(String)}.
299    *
300    * @param packageName
301    *          The Java package name to convert to a path
302    *
303    * @return the package path
304    */
305   protected String getPackagePath(String packageName) {
306     return packageName == null ? null : packageName.replace('.', '/');
307   }
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    *
315    * @return true, if is jar
316    */
317   protected boolean isJar(URL url) {
318     return isJar(url, new byte[JAR_MAGIC.length]);
319   }
320 
321   /**
322    * Returns true if the resource located at the given URL is a JAR file.
323    *
324    * @param url
325    *          The URL of the resource to test.
326    * @param buffer
327    *          A buffer into which the first few bytes of the resource are read. The buffer must be at least the size of
328    *          {@link #JAR_MAGIC}. (The same buffer may be reused for multiple calls as an optimization.)
329    *
330    * @return true, if is jar
331    */
332   protected boolean isJar(URL url, byte[] buffer) {
333     try (InputStream is = url.openStream()) {
334       is.read(buffer, 0, JAR_MAGIC.length);
335       if (Arrays.equals(buffer, JAR_MAGIC)) {
336         if (log.isLoggable(Level.FINER)) {
337           log.log(Level.FINER, "Found JAR: " + url);
338         }
339         return true;
340       }
341     } catch (Exception e) {
342       // Failure to read the stream means this is not a JAR
343     }
344 
345     return false;
346   }
347 }