View Javadoc
1   /**
2    *    Copyright 2009-2015 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(ResolverUtil.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<String>();
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<String>();
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              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              jarInput.close();
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              BufferedReader reader = new BufferedReader(new InputStreamReader(is));
99              List<String> lines = new ArrayList<String>();
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<String>();
189     for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
190       if (!entry.isDirectory()) {
191         // Add leading slash if it's missing
192         String name = entry.getName();
193         if (!name.startsWith("/")) {
194           name = "/" + name;
195         }
196 
197         // Check file name
198         if (name.startsWith(path)) {
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     try {
227       for (;;) {
228         url = new URL(url.getFile());
229         if (log.isDebugEnabled()) {
230           log.debug("Inner URL: " + url);
231         }
232       }
233     } catch (MalformedURLException e) {
234       // This will happen at some point and serves as a break in the loop
235     }
236 
237     // Look for the .jar extension and chop off everything after that
238     StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
239     int index = jarUrl.lastIndexOf(".jar");
240     if (index >= 0) {
241       jarUrl.setLength(index + 4);
242       if (log.isDebugEnabled()) {
243         log.debug("Extracted JAR URL: " + jarUrl);
244       }
245     }
246     else {
247       if (log.isDebugEnabled()) {
248         log.debug("Not a JAR: " + jarUrl);
249       }
250       return null;
251     }
252 
253     // Try to open and test it
254     try {
255       URL testUrl = new URL(jarUrl.toString());
256       if (isJar(testUrl)) {
257         return testUrl;
258       }
259       else {
260         // WebLogic fix: check if the URL's file exists in the filesystem.
261         if (log.isDebugEnabled()) {
262           log.debug("Not a JAR: " + jarUrl);
263         }
264         jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
265         File file = new File(jarUrl.toString());
266 
267         // File name might be URL-encoded
268         if (!file.exists()) {
269           try {
270             file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
271           } catch (UnsupportedEncodingException e) {
272             throw new RuntimeException("Unsupported encoding?  UTF-8?  That's unpossible.");
273           }
274         }
275 
276         if (file.exists()) {
277           if (log.isDebugEnabled()) {
278             log.debug("Trying real file: " + file.getAbsolutePath());
279           }
280           testUrl = file.toURI().toURL();
281           if (isJar(testUrl)) {
282             return testUrl;
283           }
284         }
285       }
286     } catch (MalformedURLException e) {
287       log.warn("Invalid JAR URL: " + jarUrl);
288     }
289 
290     if (log.isDebugEnabled()) {
291       log.debug("Not a JAR: " + jarUrl);
292     }
293     return null;
294   }
295 
296   /**
297    * Converts a Java package name to a path that can be looked up with a call to
298    * {@link ClassLoader#getResources(String)}.
299    * 
300    * @param packageName The Java package name to convert to a path
301    */
302   protected String getPackagePath(String packageName) {
303     return packageName == null ? null : packageName.replace('.', '/');
304   }
305 
306   /**
307    * Returns true if the resource located at the given URL is a JAR file.
308    * 
309    * @param url The URL of the resource to test.
310    */
311   protected boolean isJar(URL url) {
312     return isJar(url, new byte[JAR_MAGIC.length]);
313   }
314 
315   /**
316    * Returns true if the resource located at the given URL is a JAR file.
317    * 
318    * @param url The URL of the resource to test.
319    * @param buffer A buffer into which the first few bytes of the resource are read. The buffer
320    *            must be at least the size of {@link #JAR_MAGIC}. (The same buffer may be reused
321    *            for multiple calls as an optimization.)
322    */
323   protected boolean isJar(URL url, byte[] buffer) {
324     InputStream is = null;
325     try {
326       is = url.openStream();
327       is.read(buffer, 0, JAR_MAGIC.length);
328       if (Arrays.equals(buffer, JAR_MAGIC)) {
329         if (log.isDebugEnabled()) {
330           log.debug("Found JAR: " + url);
331         }
332         return true;
333       }
334     } catch (Exception e) {
335       // Failure to read the stream means this is not a JAR
336     } finally {
337       if (is != null) {
338         try {
339           is.close();
340         } catch (Exception e) {
341           // Ignore
342         }
343       }
344     }
345 
346     return false;
347   }
348 }