View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.context;
21  
22  import org.apache.myfaces.tobago.context.ThemeImpl;
23  import org.apache.myfaces.tobago.internal.config.TobagoConfigFragment;
24  import org.apache.myfaces.tobago.internal.config.TobagoConfigParser;
25  import org.apache.myfaces.tobago.internal.util.IoUtils;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import javax.servlet.ServletContext;
30  import javax.servlet.ServletException;
31  import java.io.File;
32  import java.io.FileNotFoundException;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.io.InputStreamReader;
36  import java.io.LineNumberReader;
37  import java.net.MalformedURLException;
38  import java.net.URL;
39  import java.util.Enumeration;
40  import java.util.Properties;
41  import java.util.Set;
42  import java.util.zip.ZipEntry;
43  import java.util.zip.ZipInputStream;
44  
45  /**
46   * <p>
47   * This class helps to locate all resources of the ResourceManager.
48   * It will be called in the initialization phase.
49   * </p>
50   * <p>
51   * Basically it looks at the following places:
52   * <ul>
53   * <li>Directly in the root of the webapp.</li>
54   * <li>The root of all JARs which containing a <code>/META-INF/tobago-config.xml</code></li>
55   * <li>The directory <code>/META-INF/resources</code> of all JARs, if they contains such directory.</li>
56   * </ul>
57   * </p>
58   *
59   * @since 1.0.7
60   */
61  class ResourceLocator {
62  
63    private static final Logger LOG = LoggerFactory.getLogger(ResourceLocator.class);
64  
65    private static final String META_INF_TOBAGO_CONFIG_XML = "META-INF/tobago-config.xml";
66    private static final String META_INF_RESOURCE_INDEX = "META-INF/tobago-resource-index.txt";
67    private static final String META_INF_RESOURCES = "META-INF/resources";
68  
69    private ServletContext servletContext;
70    private ResourceManagerImpl resourceManager;
71  
72    public ResourceLocator(final ServletContext servletContext, final ResourceManagerImpl resourceManager) {
73      this.servletContext = servletContext;
74      this.resourceManager = resourceManager;
75    }
76  
77    public void locate()
78        throws ServletException {
79      // TBD should the resource dir used from tobago-config.xml?
80      locateResourcesInWar(servletContext, resourceManager, "/");
81      locateResourcesFromClasspath(resourceManager);
82      locateResourcesServlet30Alike(resourceManager);
83    }
84  
85    private void locateResourcesInWar(
86        final ServletContext servletContext, final ResourceManagerImpl resources, String path)
87        throws ServletException {
88  
89      if (path.startsWith("/WEB-INF/")) {
90        return; // ignore
91      }
92      // fix for jetty6
93      if (path.endsWith("/") && path.length() > 1) {
94        path = path.substring(0, path.length() - 1);
95      }
96      final Set<String> resourcePaths = servletContext.getResourcePaths(path);
97      if (resourcePaths == null || resourcePaths.isEmpty()) {
98        if (LOG.isDebugEnabled()) {
99          LOG.debug("Skipping empty resource path: path='{}'", path);
100       }
101       return;
102     }
103     for (final String childPath : resourcePaths) {
104       if (childPath.endsWith("/")) {
105         // ignore, because weblogic puts the path directory itself in the Set
106         if (!childPath.equals(path)) {
107           if (LOG.isDebugEnabled()) {
108             LOG.debug("childPath dir {}", childPath);
109           }
110           locateResourcesInWar(servletContext, resources, childPath);
111         }
112       } else {
113         //Log.debug("add resc " + childPath);
114         if (childPath.endsWith(".properties")) {
115           final InputStream inputStream = servletContext.getResourceAsStream(childPath);
116           try {
117             addProperties(inputStream, resources, childPath, false, 0);
118           } finally {
119             IoUtils.closeQuietly(inputStream);
120           }
121         } else if (childPath.endsWith(".properties.xml")) {
122           final InputStream inputStream = servletContext.getResourceAsStream(childPath);
123           try {
124             addProperties(inputStream, resources, childPath, true, 0);
125           } catch (final RuntimeException e) {
126             LOG.error("childPath = \"" + childPath + "\" ", e);
127             throw e;
128           } finally {
129             IoUtils.closeQuietly(inputStream);
130           }
131         } else {
132           resources.add(childPath);
133         }
134       }
135     }
136   }
137 
138   private void locateResourcesFromClasspath(final ResourceManagerImpl resources)
139       throws ServletException {
140 
141     try {
142       if (LOG.isInfoEnabled()) {
143         LOG.info("Searching for and '" + META_INF_TOBAGO_CONFIG_XML + "'");
144       }
145       final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
146       final Enumeration<URL> urls = classLoader.getResources(META_INF_TOBAGO_CONFIG_XML);
147 
148       while (urls.hasMoreElements()) {
149         final URL tobagoConfigUrl = urls.nextElement();
150         final TobagoConfigFragment tobagoConfig = new TobagoConfigParser().parse(tobagoConfigUrl);
151         for (final ThemeImpl theme : tobagoConfig.getThemeDefinitions()) {
152           detectThemeVersion(tobagoConfigUrl, theme);
153           final String prefix = ensureSlash(theme.getResourcePath());
154           final String protocol = tobagoConfigUrl.getProtocol();
155           // tomcat uses jar // weblogic uses zip // IBM WebSphere uses wsjar
156           if (!"jar".equals(protocol) && !"zip".equals(protocol) && !"wsjar".equals(protocol)) {
157             LOG.warn("Unknown protocol '" + tobagoConfigUrl + "'");
158           }
159           addResources(resources, tobagoConfigUrl, prefix, 0);
160         }
161       }
162     } catch (final Exception e) {
163       if (e instanceof ServletException) {
164         throw (ServletException) e;
165       } else {
166         throw new ServletException(e);
167       }
168     }
169   }
170 
171   /**
172    * Searches the /WEB-INF/lib directory for *.jar files which contains /META-INF/resources directory
173    * to hold resources and add them to the ResourceManager.
174    *
175    * @param resources Resource Manager which collects all the resources.
176    * @throws ServletException An error while accessing the resource.
177    */
178   private void locateResourcesServlet30Alike(final ResourceManagerImpl resources) throws ServletException {
179 
180     try {
181       if (LOG.isInfoEnabled()) {
182         LOG.info("Searching for '" + META_INF_RESOURCES + "'");
183       }
184       final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
185       final Enumeration<URL> urls = classLoader.getResources(META_INF_RESOURCES);
186 
187       while (urls.hasMoreElements()) {
188         final URL resourcesUrl = urls.nextElement();
189 
190         LOG.info("resourcesUrl='" + resourcesUrl + "'");
191         if (!resourcesUrl.toString().matches(".*/WEB-INF/lib/.*\\.jar\\!.*")) {
192           LOG.info("skip ...");
193           continue;
194           // only resources from jar files in the /WEB-INF/lib should be considered (like in Servlet 3.0 spec.)
195         }
196         LOG.info("going on ...");
197 
198         final String protocol = resourcesUrl.getProtocol();
199         // tomcat uses jar
200         // weblogic uses zip
201         // IBM WebSphere uses wsjar
202         if (!"jar".equals(protocol) && !"zip".equals(protocol) && !"wsjar".equals(protocol)) {
203           LOG.warn("Unknown protocol '" + resourcesUrl + "'");
204         }
205         addResourcesFromZip(
206             resources,
207             resourcesUrl.getFile(),
208             resourcesUrl.getProtocol(),
209             "/" + META_INF_RESOURCES,
210             META_INF_RESOURCES.length() + 1);
211       }
212     } catch (final IOException e) {
213       final String msg = "while loading ";
214       LOG.error(msg, e);
215       throw new ServletException(msg, e);
216     }
217   }
218 
219   private void addResources(
220       final ResourceManagerImpl resources, final URL themeUrl, final String prefix, final int skipPrefix)
221       throws IOException, ServletException {
222 
223     final String fileName = themeUrl.getFile();
224     LOG.info("fileName='" + fileName + "'");
225     final String resourceIndex
226         = fileName.substring(0, fileName.lastIndexOf(META_INF_TOBAGO_CONFIG_XML)) + META_INF_RESOURCE_INDEX;
227     LOG.info("resourceIndex='" + resourceIndex + "'");
228 
229 
230 
231 
232     final URL resource = findMatchingResourceIndexUrl(themeUrl);
233 
234 
235     LOG.info("resource='" + resource + "'");
236     if (resource != null) {
237       addResourcesFromIndexFile(resources, resource.openStream(), skipPrefix);
238     } else {
239       addResourcesFromZip(resources, fileName, themeUrl.getProtocol(), prefix, skipPrefix);
240     }
241   }
242 
243   /**
244    * Find a matching tobago-resource-index.txt to the given tobago-config.xml
245    * We look here all possible URL from ClassLoader, because an AppServer may protect direct access...
246    */
247   private URL findMatchingResourceIndexUrl(final URL themeUrl) throws IOException {
248     final String themeProtocol = themeUrl.getProtocol();
249     final String themeDir
250         = themeUrl.getFile().substring(0, themeUrl.getFile().length() - META_INF_TOBAGO_CONFIG_XML.length());
251     final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
252     final Enumeration<URL> urls = classLoader.getResources(META_INF_RESOURCE_INDEX);
253     URL url = null;
254     while (urls.hasMoreElements()) {
255       url = urls.nextElement();
256       if (url.getProtocol().equals(themeProtocol)
257           && url.getFile().startsWith(themeDir)) {
258         break;
259       }
260       url = null;
261     }
262     return url;
263   }
264 
265   private void addResourcesFromIndexFile(
266       final ResourceManagerImpl resources, final InputStream indexStream, final int skipPrefix)
267       throws IOException, ServletException {
268     final LineNumberReader reader = new LineNumberReader(new InputStreamReader(indexStream));
269     String name;
270     while (null != (name = reader.readLine())) {
271       addResource(resources, name, skipPrefix);
272     }
273   }
274 
275   private void addResourcesFromZip(
276       final ResourceManagerImpl resources, String fileName, final String protocol,
277       final String prefix, final int skipPrefix)
278       throws ServletException, IOException {
279 
280     final int exclamationPoint = fileName.indexOf("!");
281     if (exclamationPoint != -1) {
282       fileName = fileName.substring(0, exclamationPoint);
283     }
284     if (LOG.isInfoEnabled()) {
285       LOG.info("Adding resources from fileName='" + fileName + "' prefix='" + prefix + "' skip=" + skipPrefix + "");
286     }
287 
288     // JBoss 6 introduced vfs protocol
289     if (protocol.equals("vfs")) {
290       LOG.warn("Protocol '" + protocol + "' is not supported. If resource is needed by the application, you'll"
291           + "need to put a index file tobago-resource-index.txt in the JAR. File='" + fileName + "'");
292       return;
293     }
294 
295     URL jarFile;
296     try {
297       // JBoss 5.0.0 introduced vfszip protocol
298       if (protocol.equals("vfszip")) {
299         fileName = new File(fileName).getParentFile().getParentFile().getPath();
300         if (File.separatorChar == '\\' && fileName.contains("\\")) {
301           fileName = fileName.replace('\\', '/');
302           if (LOG.isInfoEnabled()) {
303             LOG.info("Fixed slashes for virtual filesystem protocol on windows system: " + fileName);
304           }
305         }
306       }
307       jarFile = new URL(fileName);
308     } catch (final MalformedURLException e) {
309       // workaround for weblogic on windows
310       jarFile = new URL("file:" + fileName);
311     }
312     InputStream stream = null;
313     ZipInputStream zipStream = null;
314     try {
315       stream = jarFile.openStream();
316       zipStream = new ZipInputStream(stream);
317       for (ZipEntry nextEntry = zipStream.getNextEntry(); nextEntry != null; nextEntry = zipStream.getNextEntry()) {
318         if (nextEntry.isDirectory()) {
319           continue;
320         }
321         final String name = "/" + nextEntry.getName();
322         if (name.startsWith(prefix)) {
323           addResource(resources, name, skipPrefix);
324         }
325       }
326     } finally {
327       IoUtils.closeQuietly(stream);
328       IoUtils.closeQuietly(zipStream);
329     }
330   }
331 
332   private void addResource(final ResourceManagerImpl resources, final String name, final int skipPrefix)
333       throws ServletException {
334 
335     if (name.endsWith(".class")) {
336       // ignore the class files
337     } else if (name.endsWith(".properties")) {
338       if (LOG.isInfoEnabled()) {
339         LOG.info("Adding properties from: '" + name.substring(1) + "'");
340       }
341       final InputStream inputStream
342           = Thread.currentThread().getContextClassLoader().getResourceAsStream(name.substring(1));
343       try {
344         addProperties(inputStream, resources, name, false, skipPrefix);
345       } finally {
346         IoUtils.closeQuietly(inputStream);
347       }
348     } else if (name.endsWith(".properties.xml")) {
349       if (LOG.isInfoEnabled()) {
350         LOG.info("Adding properties from: '" + name.substring(1) + "'");
351       }
352       final InputStream inputStream
353           = Thread.currentThread().getContextClassLoader().getResourceAsStream(name.substring(1));
354       try {
355         addProperties(inputStream, resources, name, true, skipPrefix);
356       } finally {
357         IoUtils.closeQuietly(inputStream);
358       }
359     } else {
360       resources.add(name.substring(skipPrefix));
361     }
362   }
363 
364   private String ensureSlash(String resourcePath) {
365     if (!resourcePath.startsWith("/")) {
366       resourcePath = '/' + resourcePath;
367     }
368     if (!resourcePath.endsWith("/")) {
369       resourcePath = resourcePath + '/';
370     }
371     return resourcePath;
372   }
373 
374   private void addProperties(
375       final InputStream stream, final ResourceManagerImpl resources, final String childPath, final boolean xml,
376       final int skipPrefix)
377       throws ServletException {
378 
379     final String directory = childPath.substring(skipPrefix, childPath.lastIndexOf('/'));
380     final String filename = childPath.substring(childPath.lastIndexOf('/') + 1);
381 
382     int end = filename.lastIndexOf('.');
383     if (xml) {
384       end = filename.lastIndexOf('.', end - 1);
385     }
386     final String locale = filename.substring(0, end);
387 
388     final Properties temp = new Properties();
389     try {
390       if (xml) {
391         temp.loadFromXML(stream);
392         if (LOG.isDebugEnabled()) {
393           LOG.debug(childPath);
394           LOG.debug("xml properties: {}", temp.size());
395         }
396       } else {
397         temp.load(stream);
398         if (LOG.isDebugEnabled()) {
399           LOG.debug(childPath);
400           LOG.debug("    properties: {}", temp.size());
401         }
402       }
403     } catch (final IOException e) {
404       final String msg = "while loading " + childPath;
405       LOG.error(msg, e);
406       throw new ServletException(msg, e);
407     } finally {
408       IoUtils.closeQuietly(stream);
409     }
410 
411     final Enumeration e = temp.propertyNames();
412     while (e.hasMoreElements()) {
413       final String key = (String) e.nextElement();
414       resources.add(directory + '/' + locale + '/' + key, temp.getProperty(key));
415       if (LOG.isDebugEnabled()) {
416         LOG.debug(directory + '/' + locale + '/' + key + "=" + temp.getProperty(key));
417       }
418     }
419   }
420 
421   private void detectThemeVersion(final URL tobagoConfigUrl, final ThemeImpl theme) throws IOException {
422     if (theme.isVersioned()) {
423       final String themeUrlStr = tobagoConfigUrl.toString();
424       final int index = themeUrlStr.indexOf(META_INF_TOBAGO_CONFIG_XML);
425       final String metaInf = themeUrlStr.substring(0, index) + "META-INF/MANIFEST.MF";
426       final Properties properties = new Properties();
427       final URL url = new URL(metaInf);
428       InputStream inputStream = null;
429       String version = null;
430       try {
431         inputStream = url.openStream();
432         properties.load(inputStream);
433         version = properties.getProperty("Implementation-Version");
434       } catch (final FileNotFoundException e) {
435         // may happen (e. g. in tests)
436         LOG.error("No Manifest-File found.");
437       } finally {
438         IoUtils.closeQuietly(inputStream);
439       }
440       if (version != null) {
441         theme.setVersion(version);
442       } else {
443         theme.setVersioned(false);
444         LOG.error("No Implementation-Version found in Manifest-File for theme: '" + theme.getName()
445             + "'. Resetting the theme to unversioned. Please correct the Manifest-File.");
446       }
447     }
448   }
449 
450 }