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