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