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