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