View Javadoc
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  
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  
35  import org.apache.ibatis.logging.Log;
36  import org.apache.ibatis.logging.LogFactory;
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 Log log = LogFactory.getLog(DefaultVFS.class);
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.isDebugEnabled()) {
66            log.debug("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.isDebugEnabled()) {
78                  log.debug("Listing " + url);
79                }
80                for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null; ) {
81                  if (log.isDebugEnabled()) {
82                    log.debug("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
90               * text file, listing the child resources one per line. However, there is no
91               * way to differentiate between directory and file resources just by reading
92               * them. To work around that, as each line is read, try to look it up via
93               * the class loader as a child of the current resource. If any line fails
94               * then we assume the current resource is not a directory.
95               */
96              is = url.openStream();
97              List<String> lines = new ArrayList<>();
98              try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
99                for (String line; (line = reader.readLine()) != null;) {
100                 if (log.isDebugEnabled()) {
101                   log.debug("Reader entry: " + line);
102                 }
103                 lines.add(line);
104                 if (getResources(path + "/" + line).isEmpty()) {
105                   lines.clear();
106                   break;
107                 }
108               }
109             } catch (InvalidPathException e) {
110               // #1974
111               lines.clear();
112             }
113             if (!lines.isEmpty()) {
114               if (log.isDebugEnabled()) {
115                 log.debug("Listing " + url);
116               }
117               children.addAll(lines);
118             }
119           }
120         } catch (FileNotFoundException e) {
121           /*
122            * For file URLs the openStream() call might fail, depending on the servlet
123            * container, because directories can't be opened for reading. If that happens,
124            * then list the directory directly instead.
125            */
126           if ("file".equals(url.getProtocol())) {
127             File file = new File(url.getFile());
128             if (log.isDebugEnabled()) {
129               log.debug("Listing directory " + file.getAbsolutePath());
130             }
131             if (file.isDirectory()) {
132               if (log.isDebugEnabled()) {
133                 log.debug("Listing " + url);
134               }
135               children = Arrays.asList(file.list());
136             }
137           } else {
138             // No idea where the exception came from so rethrow it
139             throw e;
140           }
141         }
142 
143         // The URL prefix to use when recursively listing child resources
144         String prefix = url.toExternalForm();
145         if (!prefix.endsWith("/")) {
146           prefix = prefix + "/";
147         }
148 
149         // Iterate over immediate children, adding files and recurring into directories
150         for (String child : children) {
151           String resourcePath = path + "/" + child;
152           resources.add(resourcePath);
153           URL childUrl = new URL(prefix + child);
154           resources.addAll(list(childUrl, resourcePath));
155         }
156       }
157 
158       return resources;
159     } finally {
160       if (is != null) {
161         try {
162           is.close();
163         } catch (Exception e) {
164           // Ignore
165         }
166       }
167     }
168   }
169 
170   /**
171    * List the names of the entries in the given {@link JarInputStream} that begin with the
172    * specified {@code path}. Entries will match with or without a leading slash.
173    *
174    * @param jar The JAR input stream
175    * @param path The leading path to match
176    * @return The names of all the matching entries
177    * @throws IOException 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.isDebugEnabled()) {
201             log.debug("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
213    * by the URL. That is, assuming the URL references a JAR entry, this method will return a URL
214    * that references the JAR file containing the entry. If the JAR cannot be located, then this
215    * method returns null.
216    *
217    * @param url The URL of the JAR entry.
218    * @return The URL of the JAR file, if one is found. Null if not.
219    * @throws MalformedURLException
220    *           the malformed URL exception
221    */
222   protected URL findJarForResource(URL url) throws MalformedURLException {
223     if (log.isDebugEnabled()) {
224       log.debug("Find JAR URL: " + url);
225     }
226 
227     // If the file part of the URL is itself a URL, then that URL probably points to the JAR
228     boolean continueLoop = true;
229     while (continueLoop) {
230       try {
231         url = new URL(url.getFile());
232         if (log.isDebugEnabled()) {
233           log.debug("Inner URL: " + url);
234         }
235       } catch (MalformedURLException e) {
236         // This will happen at some point and serves as a break in the loop
237         continueLoop = false;
238       }
239     }
240 
241     // Look for the .jar extension and chop off everything after that
242     StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
243     int index = jarUrl.lastIndexOf(".jar");
244     if (index >= 0) {
245       jarUrl.setLength(index + 4);
246       if (log.isDebugEnabled()) {
247         log.debug("Extracted JAR URL: " + jarUrl);
248       }
249     } else {
250       if (log.isDebugEnabled()) {
251         log.debug("Not a JAR: " + jarUrl);
252       }
253       return null;
254     }
255 
256     // Try to open and test it
257     try {
258       URL testUrl = new URL(jarUrl.toString());
259       if (isJar(testUrl)) {
260         return testUrl;
261       } else {
262         // WebLogic fix: check if the URL's file exists in the filesystem.
263         if (log.isDebugEnabled()) {
264           log.debug("Not a JAR: " + jarUrl);
265         }
266         jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
267         File file = new File(jarUrl.toString());
268 
269         // File name might be URL-encoded
270         if (!file.exists()) {
271           try {
272             file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
273           } catch (UnsupportedEncodingException e) {
274             throw new RuntimeException("Unsupported encoding?  UTF-8?  That's impossible.");
275           }
276         }
277 
278         if (file.exists()) {
279           if (log.isDebugEnabled()) {
280             log.debug("Trying real file: " + file.getAbsolutePath());
281           }
282           testUrl = file.toURI().toURL();
283           if (isJar(testUrl)) {
284             return testUrl;
285           }
286         }
287       }
288     } catch (MalformedURLException e) {
289       log.warn("Invalid JAR URL: " + jarUrl);
290     }
291 
292     if (log.isDebugEnabled()) {
293       log.debug("Not a JAR: " + jarUrl);
294     }
295     return null;
296   }
297 
298   /**
299    * Converts a Java package name to a path that can be looked up with a call to
300    * {@link ClassLoader#getResources(String)}.
301    *
302    * @param packageName
303    *          The Java package name to convert to a path
304    * @return the package path
305    */
306   protected String getPackagePath(String packageName) {
307     return packageName == null ? null : packageName.replace('.', '/');
308   }
309 
310   /**
311    * Returns true if the resource located at the given URL is a JAR file.
312    *
313    * @param url
314    *          The URL of the resource to test.
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    * @return true, if is jar
330    */
331   protected boolean isJar(URL url, byte[] buffer) {
332     try (InputStream is = url.openStream()) {
333       is.read(buffer, 0, JAR_MAGIC.length);
334       if (Arrays.equals(buffer, JAR_MAGIC)) {
335         if (log.isDebugEnabled()) {
336           log.debug("Found JAR: " + url);
337         }
338         return true;
339       }
340     } catch (Exception e) {
341       // Failure to read the stream means this is not a JAR
342     }
343 
344     return false;
345   }
346 }