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