View Javadoc

1   /**
2    * This file Copyright (c) 2003-2014 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.lang3.StringUtils;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  /**
64   * Util to find resources in the classpath (WEB-INF/lib and WEB-INF/classes).
65   */
66  public class ClasspathResourcesUtil {
67      private static final Logger log = LoggerFactory.getLogger(ClasspathResourcesUtil.class);
68  
69      /**
70       * Filter for filtering the resources.
71       */
72      public static interface Filter {
73          public boolean accept(String name);
74      }
75  
76      /**
77       * A filter using a regex pattern.
78       */
79      public static class PatternFilter implements Filter {
80          private final Pattern pattern;
81  
82          public PatternFilter(String pattern) {
83              this.pattern = Pattern.compile(pattern);
84          }
85  
86          @Override
87          public boolean accept(String name) {
88              return pattern.matcher(name).matches();
89          }
90      }
91  
92      private static boolean isCache() {
93          final String devMode = SystemProperty.getProperty("magnolia.develop");
94          return !"true".equalsIgnoreCase(devMode);
95      }
96  
97      /**
98       * Return a collection containing the resource names which match the regular expression.
99       * 
100      * @return string array of found resources TODO : (lazy) cache ?
101      */
102     public static String[] findResources(String regex) {
103         return findResources(new PatternFilter(regex));
104     }
105 
106     /**
107      * Return a collection containing the resource names which passed the filter.
108      * 
109      * @return string array of found resources TODO : (lazy) cache ?
110      */
111     public static String[] findResources(Filter filter) {
112         final Set<String> resources = new HashSet<String>();
113         final ClassLoader cl = getCurrentClassLoader();
114 
115         // if the classloader is an URLClassloader we have a better method for discovering resources
116         // whis will also fetch files from jars outside WEB-INF/lib, useful during development
117         if (cl instanceof URLClassLoader) {
118             final URLClassLoader urlClassLoader = (URLClassLoader) cl;
119             final URL[] urls = urlClassLoader.getURLs();
120             log.debug("Loading resources from: {}", urls);
121             if (urls.length == 1 && urls[0].getPath().endsWith("WEB-INF/classes/")) {
122                 // working around MAGNOLIA-2577
123                 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.");
124             } else {
125                 collectFromURLs(resources, urls, filter);
126                 return resources.toArray(new String[resources.size()]);
127             }
128         }
129 
130         try {
131             // be friendly to WAS developers too...
132             // in development mode under RAD 7.5 here we have an instance of com.ibm.ws.classloader.WsClassLoader
133             // and jars are NOT deployed to WEB-INF/lib by default, so they can't be found without this explicit check
134             //
135             // but since we don't want to depend on WAS stuff we just check if the cl exposes a "classPath" property
136             String classpath = BeanUtils.getProperty(cl, "classPath");
137 
138             if (StringUtils.isNotEmpty(classpath)) {
139                 collectFromClasspathString(resources, classpath, filter);
140                 return resources.toArray(new String[resources.size()]);
141             }
142         } catch (Throwable e) {
143             // no, it's not a classloader we can handle in a special way
144         }
145 
146         // no way, we have to assume a standard war structure and look in the WEB-INF/lib and WEB-INF/classes dirs
147         // read the jars in the lib dir
148         collectFromFileSystem(filter, resources);
149         return resources.toArray(new String[resources.size()]);
150     }
151 
152     protected static void collectFromURLs(Collection<String> resources, URL[] urls, Filter filter) {
153         // tomcat classloader is org.apache.catalina.loader.WebappClassLoader
154         for (int j = 0; j < urls.length; j++) {
155             final File tofile = sanitizeToFile(urls[j]);
156             if (tofile != null) {
157                 collectFiles(resources, tofile, filter);
158             }
159         }
160     }
161 
162     protected static void collectFromClasspathString(Collection<String> resources, String classpath, Filter filter) {
163         String[] paths = classpath.split(File.pathSeparator);
164         for (int j = 0; j < paths.length; j++) {
165             final File tofile = new File(paths[j]);
166             // there can be several missing (optional?) paths here...
167             if (tofile.exists()) {
168                 collectFiles(resources, tofile, filter);
169             }
170         }
171     }
172 
173     protected static void collectFromFileSystem(Filter filter, Collection<String> resources) {
174         // We need to have access to the MAGNOLIA_APP_ROOTDIR. Unfortunately, this is not yet initialised.
175         String rootDire = Components.getComponent(MagnoliaInitPaths.class).getRootPath();
176         String libString = new File(new File(rootDire), "WEB-INF/lib").getAbsolutePath();
177 
178         File dir = new File(libString);
179         if (dir.exists()) {
180             File[] files = dir.listFiles(new FilenameFilter() {
181                 @Override
182                 public boolean accept(File file, String name) {
183                     return name.endsWith(".jar");
184                 }
185             });
186 
187             for (int i = 0; i < files.length; i++) {
188                 collectFiles(resources, files[i], filter);
189             }
190         }
191 
192         // read files in WEB-INF/classes
193         String classString = new File(new File(rootDire), "WEB-INF/classes").getAbsolutePath();
194         File classFileDir = new File(classString);
195 
196         if (classFileDir.exists()) {
197             collectFiles(resources, classFileDir, filter);
198         }
199     }
200 
201     public static File sanitizeToFile(URL url) {
202         if (!"file".equals(url.getProtocol()) && !StringUtils.startsWith(url.toString(), "jar:file:")) { // only file:/ and jar:file/ are supported
203             log.warn("Cannot load resources '{}' from '{}' protocol. Only 'file' and 'jar-file' protocols are supported.", url, url.getProtocol());
204             return null;
205         }
206         try {
207             String fileUrl = url.getFile();
208             // needed because somehow the URLClassLoader has encoded URLs, and getFile does not decode them.
209             fileUrl = URLDecoder.decode(fileUrl, "UTF-8");
210             // needed for Resin - for some reason, its URLs are formed as jar:file:/absolutepath/foo/bar.jar instead of
211             // using the :///abs.. notation
212             fileUrl = StringUtils.removeStart(fileUrl, "file:");
213             fileUrl = StringUtils.removeEnd(fileUrl, "!/");
214             return new File(fileUrl);
215         } catch (UnsupportedEncodingException e) {
216             throw new RuntimeException(e);
217         }
218     }
219 
220     /**
221      * Load resources from jars or directories.
222      * 
223      * @param resources found resources will be added to this collection
224      * @param jarOrDir a File, can be a jar or a directory
225      * @param filter used to filter resources
226      */
227     private static void collectFiles(Collection<String> resources, File jarOrDir, Filter filter) {
228 
229         if (!jarOrDir.exists()) {
230             log.warn("missing file: {}", jarOrDir.getAbsolutePath());
231             return;
232         }
233 
234         if (jarOrDir.isDirectory()) {
235             log.debug("looking in dir {}", jarOrDir.getAbsolutePath());
236 
237             Collection<File> files = FileUtils.listFiles(jarOrDir, TrueFileFilter.TRUE, TrueFileFilter.TRUE);
238             for (File file : files) {
239                 String name = StringUtils.substringAfter(file.getPath(), jarOrDir.getPath());
240 
241                 // please, be kind to Windows!!!
242                 name = StringUtils.replace(name, "\\", "/");
243                 if (!name.startsWith("/")) {
244                     name = "/" + name;
245                 }
246 
247                 if (filter.accept(name)) {
248                     resources.add(name);
249                 }
250             }
251         }
252         else if (jarOrDir.getName().endsWith(".jar")) {
253             log.debug("looking in jar {}", jarOrDir.getAbsolutePath());
254             JarFile jar;
255             try {
256                 jar = new JarFile(jarOrDir);
257             } catch (IOException e) {
258                 log.error("IOException opening file {}, skipping", jarOrDir.getAbsolutePath());
259                 return;
260             }
261             for (Enumeration<JarEntry> em = jar.entries(); em.hasMoreElements();) {
262                 JarEntry entry = em.nextElement();
263                 if (!entry.isDirectory()) {
264                     if (filter.accept("/" + entry.getName())) {
265                         resources.add("/" + entry.getName());
266                     }
267                 }
268             }
269             try {
270                 jar.close();
271             } catch (IOException e) {
272                 log.error("Failed to close jar file : {}", e.getMessage());
273                 log.debug("Failed to close jar file", e);
274             }
275         }
276         else {
277             log.debug("Unknown (not jar) file in classpath: {}, skipping.", jarOrDir.getName());
278         }
279 
280     }
281 
282     public static InputStream getStream(String name) throws IOException {
283         return getStream(name, isCache());
284     }
285 
286     /**
287      * Checks last modified and returns the new content if changed and the cache flag is not set to true.
288      * 
289      * @return the input stream
290      */
291     public static InputStream getStream(String name, boolean cache) throws IOException {
292         if (cache) {
293             return getCurrentClassLoader().getResourceAsStream(StringUtils.removeStart(name, "/"));
294         }
295 
296         // TODO use the last modified attribute
297         URL url = getResource(name);
298         if (url != null) {
299             return url.openStream();
300         }
301 
302         log.debug("Can't find {}", name);
303         return null;
304     }
305 
306     /**
307      * From http://stackoverflow.com/questions/1771679/difference-between-threads-context-class-loader-and-normal-classloader.
308      * <p>
309      * <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.
310      * 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.
311      * 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>
312      * 
313      * @return current classloader
314      */
315     public static ClassLoader getCurrentClassLoader() {
316         return Thread.currentThread().getContextClassLoader();
317     }
318 
319     /**
320      * Get the resource using the current class loader. The leading / is removed as the call to class.getResource()
321      * would do.
322      * 
323      * @return the resource
324      */
325     public static URL getResource(String name) {
326         return getCurrentClassLoader().getResource(StringUtils.removeStart(name, "/"));
327     }
328 
329 }