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