View Javadoc
1   /**
2    * This file Copyright (c) 2003-2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.util;
35  
36  import info.magnolia.cms.core.SystemProperty;
37  import info.magnolia.init.MagnoliaInitPaths;
38  import info.magnolia.objectfactory.Components;
39  
40  import java.io.File;
41  import java.io.FilenameFilter;
42  import java.io.IOException;
43  import java.io.InputStream;
44  import java.io.UnsupportedEncodingException;
45  import java.net.URL;
46  import java.net.URLClassLoader;
47  import java.net.URLDecoder;
48  import java.util.Collection;
49  import java.util.Enumeration;
50  import java.util.HashSet;
51  import java.util.Set;
52  import java.util.jar.JarEntry;
53  import java.util.jar.JarFile;
54  import java.util.regex.Pattern;
55  
56  import org.apache.commons.beanutils.BeanUtils;
57  import org.apache.commons.io.FileUtils;
58  import org.apache.commons.io.filefilter.TrueFileFilter;
59  import org.apache.commons.lang.ArrayUtils;
60  import org.apache.commons.lang.StringUtils;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /**
65   * Util to find resources in the classpath (WEB-INF/lib and WEB-INF/classes).
66   */
67  public class ClasspathResourcesUtil {
68      private static final Logger log = LoggerFactory.getLogger(ClasspathResourcesUtil.class);
69  
70      /**
71       * Filter for filtering the resources.
72       */
73      public static interface Filter {
74          public boolean accept(String name);
75      }
76  
77      /**
78       * A filter using a regex pattern.
79       */
80      public static class PatternFilter implements Filter {
81          private final Pattern pattern;
82  
83          public PatternFilter(String pattern) {
84              this.pattern = Pattern.compile(pattern);
85          }
86  
87          @Override
88          public boolean accept(String name) {
89              return pattern.matcher(name).matches();
90          }
91      }
92  
93      private static boolean isCache() {
94          final String devMode = SystemProperty.getProperty("magnolia.develop");
95          return !"true".equalsIgnoreCase(devMode);
96      }
97  
98      /**
99       * Return a collection containing the resource names which match the regular expression.
100      * 
101      * @return string array of found resources TODO : (lazy) cache ?
102      */
103     public static String[] findResources(String regex) {
104         return findResources(new PatternFilter(regex));
105     }
106 
107     /**
108      * Return a collection containing the resource names which passed the filter.
109      * 
110      * @return string array of found resources TODO : (lazy) cache ?
111      */
112     public static String[] findResources(Filter filter) {
113         final Set<String> resources = new HashSet<String>();
114         final ClassLoader cl = getCurrentClassLoader();
115 
116         // if the classloader is an URLClassloader we have a better method for discovering resources
117         // whis will also fetch files from jars outside WEB-INF/lib, useful during development
118         if (cl instanceof URLClassLoader) {
119             final URLClassLoader urlClassLoader = (URLClassLoader) cl;
120             final URL[] urls = urlClassLoader.getURLs();
121             if (log.isDebugEnabled()) {
122                 log.debug("Loading resources from: " + ArrayUtils.toString(urls));
123             }
124             if (urls.length == 1 && urls[0].getPath().endsWith("WEB-INF/classes/")) {
125                 // working around MAGNOLIA-2577
126                 log.warn("Looks like we're in a JBoss 5 expanded war directory, will attempt to load resources from the file system instead; see MAGNOLIA-2577.");
127             } else {
128                 collectFromURLs(resources, urls, filter);
129                 return resources.toArray(new String[resources.size()]);
130             }
131         }
132 
133         try {
134             // be friendly to WAS developers too...
135             // in development mode under RAD 7.5 here we have an instance of com.ibm.ws.classloader.WsClassLoader
136             // and jars are NOT deployed to WEB-INF/lib by default, so they can't be found without this explicit check
137             //
138             // but since we don't want to depend on WAS stuff we just check if the cl exposes a "classPath" property
139             String classpath = BeanUtils.getProperty(cl, "classPath");
140 
141             if (StringUtils.isNotEmpty(classpath)) {
142                 collectFromClasspathString(resources, classpath, filter);
143                 return resources.toArray(new String[resources.size()]);
144             }
145         } catch (Throwable e) {
146             // no, it's not a classloader we can handle in a special way
147         }
148 
149         // no way, we have to assume a standard war structure and look in the WEB-INF/lib and WEB-INF/classes dirs
150         // read the jars in the lib dir
151         collectFromFileSystem(filter, resources);
152         return resources.toArray(new String[resources.size()]);
153     }
154 
155     protected static void collectFromURLs(Collection<String> resources, URL[] urls, Filter filter) {
156         // tomcat classloader is org.apache.catalina.loader.WebappClassLoader
157         for (int j = 0; j < urls.length; j++) {
158             final File tofile = sanitizeToFile(urls[j]);
159             if (tofile != null) {
160                 collectFiles(resources, tofile, filter);
161             }
162         }
163     }
164 
165     protected static void collectFromClasspathString(Collection<String> resources, String classpath, Filter filter) {
166         String[] paths = classpath.split(File.pathSeparator);
167         for (int j = 0; j < paths.length; j++) {
168             final File tofile = new File(paths[j]);
169             // there can be several missing (optional?) paths here...
170             if (tofile.exists()) {
171                 collectFiles(resources, tofile, filter);
172             }
173         }
174     }
175 
176     protected static void collectFromFileSystem(Filter filter, Collection<String> resources) {
177         // We need to have access to the MAGNOLIA_APP_ROOTDIR. Unfortunately, this is not yet initialised.
178         String rootDire = Components.getComponent(MagnoliaInitPaths.class).getRootPath();
179         String libString = new File(new File(rootDire), "WEB-INF/lib").getAbsolutePath();
180 
181         File dir = new File(libString);
182         if (dir.exists()) {
183             File[] files = dir.listFiles(new FilenameFilter() {
184                 @Override
185                 public boolean accept(File file, String name) {
186                     return name.endsWith(".jar");
187                 }
188             });
189 
190             for (int i = 0; i < files.length; i++) {
191                 collectFiles(resources, files[i], filter);
192             }
193         }
194 
195         // read files in WEB-INF/classes
196         String classString = new File(new File(rootDire), "WEB-INF/classes").getAbsolutePath();
197         File classFileDir = new File(classString);
198 
199         if (classFileDir.exists()) {
200             collectFiles(resources, classFileDir, filter);
201         }
202     }
203 
204     public static File sanitizeToFile(URL url) {
205         if (!"file".equals(url.getProtocol()) && !StringUtils.startsWith(url.toString(), "jar:file:")) { // only file:/ and jar:file/ are supported
206             log.warn("Cannot load resources '{}' from '{}' protocol. Only 'file' and 'jar-file' protocols are supported.", url, url.getProtocol());
207             return null;
208         }
209         try {
210             String fileUrl = url.getFile();
211             // needed because somehow the URLClassLoader has encoded URLs, and getFile does not decode them.
212             fileUrl = URLDecoder.decode(fileUrl, "UTF-8");
213             // needed for Resin - for some reason, its URLs are formed as jar:file:/absolutepath/foo/bar.jar instead of
214             // using the :///abs.. notation
215             fileUrl = StringUtils.removeStart(fileUrl, "file:");
216             fileUrl = StringUtils.removeEnd(fileUrl, "!/");
217             return new File(fileUrl);
218         } catch (UnsupportedEncodingException e) {
219             throw new RuntimeException(e);
220         }
221     }
222 
223     /**
224      * Load resources from jars or directories.
225      * 
226      * @param resources found resources will be added to this collection
227      * @param jarOrDir a File, can be a jar or a directory
228      * @param filter used to filter resources
229      */
230     private static void collectFiles(Collection<String> resources, File jarOrDir, Filter filter) {
231 
232         if (!jarOrDir.exists()) {
233             log.warn("missing file: {}", jarOrDir.getAbsolutePath());
234             return;
235         }
236 
237         if (jarOrDir.isDirectory()) {
238             log.debug("looking in dir {}", jarOrDir.getAbsolutePath());
239 
240             Collection<File> files = FileUtils.listFiles(jarOrDir, TrueFileFilter.TRUE, TrueFileFilter.TRUE);
241             for (File file : files) {
242                 String name = StringUtils.substringAfter(file.getPath(), jarOrDir.getPath());
243 
244                 // please, be kind to Windows!!!
245                 name = StringUtils.replace(name, "\\", "/");
246                 if (!name.startsWith("/")) {
247                     name = "/" + name;
248                 }
249 
250                 if (filter.accept(name)) {
251                     resources.add(name);
252                 }
253             }
254         }
255         else if (jarOrDir.getName().endsWith(".jar")) {
256             log.debug("looking in jar {}", jarOrDir.getAbsolutePath());
257             JarFile jar;
258             try {
259                 jar = new JarFile(jarOrDir);
260             } catch (IOException e) {
261                 log.error("IOException opening file {}, skipping", jarOrDir.getAbsolutePath());
262                 return;
263             }
264             for (Enumeration<JarEntry> em = jar.entries(); em.hasMoreElements();) {
265                 JarEntry entry = em.nextElement();
266                 if (!entry.isDirectory()) {
267                     if (filter.accept("/" + entry.getName())) {
268                         resources.add("/" + entry.getName());
269                     }
270                 }
271             }
272             try {
273                 jar.close();
274             } catch (IOException e) {
275                 log.error("Failed to close jar file : " + e.getMessage());
276                 log.debug("Failed to close jar file", e);
277             }
278         }
279         else {
280             log.debug("Unknown (not jar) file in classpath: {}, skipping.", jarOrDir.getName());
281         }
282 
283     }
284 
285     public static InputStream getStream(String name) throws IOException {
286         return getStream(name, isCache());
287     }
288 
289     /**
290      * Checks last modified and returns the new content if changed and the cache flag is not set to true.
291      * 
292      * @return the input stream
293      */
294     public static InputStream getStream(String name, boolean cache) throws IOException {
295         if (cache) {
296             return getCurrentClassLoader().getResourceAsStream(StringUtils.removeStart(name, "/"));
297         }
298 
299         // TODO use the last modified attribute
300         URL url = getResource(name);
301         if (url != null) {
302             return url.openStream();
303         }
304 
305         log.debug("Can't find {}", name);
306         return null;
307     }
308 
309     /**
310      * From http://stackoverflow.com/questions/1771679/difference-between-threads-context-class-loader-and-normal-classloader.
311      * <p>
312      * <em>"Each class will use its own classloader to load other classes. So if ClassA.class references ClassB.class then ClassB needs to be on the classpath of the classloader of ClassA, or its parents.
313      * The thread context classloader is the current classloader for the current thread. An object can be created from a class in ClassLoaderC and then passed to a thread owned by ClassLoaderD.
314      * In this case the object needs to use Thread.currentThread().getContextClassLoader() directly if it wants to load resources that are not available on its own classloader."</em>
315      * 
316      * @return current classloader
317      */
318     public static ClassLoader getCurrentClassLoader() {
319         return Thread.currentThread().getContextClassLoader();
320     }
321 
322     /**
323      * Get the resource using the current class loader. The leading / is removed as the call to class.getResource()
324      * would do.
325      * 
326      * @return the resource
327      */
328     public static URL getResource(String name) {
329         return getCurrentClassLoader().getResource(StringUtils.removeStart(name, "/"));
330     }
331 
332 }