View Javadoc
1   /**
2    *    Copyright 2009-2019 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.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.List;
31  import java.util.jar.JarEntry;
32  import java.util.jar.JarInputStream;
33  
34  import org.apache.ibatis.logging.Log;
35  import org.apache.ibatis.logging.LogFactory;
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 Log log = LogFactory.getLog(DefaultVFS.class);
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.isDebugEnabled()) {
65            log.debug("Listing " + url);
66          }
67          resources = listResources(new JarInputStream(is), path);
68        }
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            }
88            else {
89              /*
90               * Some servlet containers allow reading from directory resources like a
91               * text file, listing the child resources one per line. However, there is no
92               * way to differentiate between directory and file resources just by reading
93               * them. To work around that, as each line is read, try to look it up via
94               * the class loader as a child of the current resource. If any line fails
95               * then we assume the current resource is not a 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             }
111             if (!lines.isEmpty()) {
112               if (log.isDebugEnabled()) {
113                 log.debug("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
121            * container, because directories can't be opened for reading. If that happens,
122            * then list the directory directly instead.
123            */
124           if ("file".equals(url.getProtocol())) {
125             File file = new File(url.getFile());
126             if (log.isDebugEnabled()) {
127                 log.debug("Listing directory " + file.getAbsolutePath());
128             }
129             if (file.isDirectory()) {
130               if (log.isDebugEnabled()) {
131                   log.debug("Listing " + url);
132               }
133               children = Arrays.asList(file.list());
134             }
135           }
136           else {
137             // No idea where the exception came from so rethrow it
138             throw e;
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 recursing 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
171    * specified {@code path}. Entries will match with or without a leading slash.
172    *
173    * @param jar The JAR input stream
174    * @param path The leading path to match
175    * @return The names of all the matching entries
176    * @throws IOException If I/O errors occur
177    */
178   protected List<String> listResources(JarInputStream jar, String path) throws IOException {
179     // Include the leading and trailing slash when matching names
180     if (!path.startsWith("/")) {
181       path = "/" + path;
182     }
183     if (!path.endsWith("/")) {
184       path = path + "/";
185     }
186 
187     // Iterate over the entries and collect those that begin with the requested path
188     List<String> resources = new ArrayList<>();
189     for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
190       if (!entry.isDirectory()) {
191         // Add leading slash if it's missing
192         StringBuilder name = new StringBuilder(entry.getName());
193         if (name.charAt(0) != '/') {
194           name.insert(0, '/');
195         }
196 
197         // Check file name
198         if (name.indexOf(path) == 0) {
199           if (log.isDebugEnabled()) {
200             log.debug("Found resource: " + name);
201           }
202           // Trim leading slash
203           resources.add(name.substring(1));
204         }
205       }
206     }
207     return resources;
208   }
209 
210   /**
211    * Attempts to deconstruct the given URL to find a JAR file containing the resource referenced
212    * by the URL. That is, assuming the URL references a JAR entry, this method will return a URL
213    * that references the JAR file containing the entry. If the JAR cannot be located, then this
214    * method returns null.
215    *
216    * @param url The URL of the JAR entry.
217    * @return The URL of the JAR file, if one is found. Null if not.
218    * @throws MalformedURLException
219    */
220   protected URL findJarForResource(URL url) throws MalformedURLException {
221     if (log.isDebugEnabled()) {
222       log.debug("Find JAR URL: " + url);
223     }
224 
225     // If the file part of the URL is itself a URL, then that URL probably points to the JAR
226     boolean continueLoop = true;
227     while (continueLoop) {
228       try {
229         url = new URL(url.getFile());
230         if (log.isDebugEnabled()) {
231           log.debug("Inner URL: " + url);
232         }
233       } catch (MalformedURLException e) {
234         // This will happen at some point and serves as a break in the loop
235         continueLoop = false;
236       }
237     }
238 
239     // Look for the .jar extension and chop off everything after that
240     StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
241     int index = jarUrl.lastIndexOf(".jar");
242     if (index >= 0) {
243       jarUrl.setLength(index + 4);
244       if (log.isDebugEnabled()) {
245         log.debug("Extracted JAR URL: " + jarUrl);
246       }
247     }
248     else {
249       if (log.isDebugEnabled()) {
250         log.debug("Not a JAR: " + jarUrl);
251       }
252       return null;
253     }
254 
255     // Try to open and test it
256     try {
257       URL testUrl = new URL(jarUrl.toString());
258       if (isJar(testUrl)) {
259         return testUrl;
260       }
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 unpossible.");
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 The Java package name to convert to a path
303    */
304   protected String getPackagePath(String packageName) {
305     return packageName == null ? null : packageName.replace('.', '/');
306   }
307 
308   /**
309    * Returns true if the resource located at the given URL is a JAR file.
310    *
311    * @param url The URL of the resource to test.
312    */
313   protected boolean isJar(URL url) {
314     return isJar(url, new byte[JAR_MAGIC.length]);
315   }
316 
317   /**
318    * Returns true if the resource located at the given URL is a JAR file.
319    *
320    * @param url The URL of the resource to test.
321    * @param buffer A buffer into which the first few bytes of the resource are read. The buffer
322    *            must be at least the size of {@link #JAR_MAGIC}. (The same buffer may be reused
323    *            for multiple calls as an optimization.)
324    */
325   protected boolean isJar(URL url, byte[] buffer) {
326     InputStream is = null;
327     try {
328       is = url.openStream();
329       is.read(buffer, 0, JAR_MAGIC.length);
330       if (Arrays.equals(buffer, JAR_MAGIC)) {
331         if (log.isDebugEnabled()) {
332           log.debug("Found JAR: " + url);
333         }
334         return true;
335       }
336     } catch (Exception e) {
337       // Failure to read the stream means this is not a JAR
338     } finally {
339       if (is != null) {
340         try {
341           is.close();
342         } catch (Exception e) {
343           // Ignore
344         }
345       }
346     }
347 
348     return false;
349   }
350 }