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