/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.tobago.internal.context;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.myfaces.tobago.context.ThemeImpl;
import org.apache.myfaces.tobago.internal.config.ThemeParser;
import org.apache.myfaces.tobago.internal.config.TobagoConfigFragment;
import org.apache.myfaces.tobago.internal.config.TobagoConfigParser;
import org.apache.myfaces.tobago.internal.util.Deprecation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* This class helps to locate all resources of the ResourceManager.
* It will be called in the initialization phase.
*
* @since 1.0.7
*/
class ResourceLocator {
private static final Logger LOG = LoggerFactory.getLogger(ResourceLocator.class);
private static final String META_INF_TOBAGO_CONFIG_XML = "META-INF/tobago-config.xml";
private static final String META_INF_TOBAGO_THEME_XML = "META-INF/tobago-theme.xml";
private static final String META_INF_RESOURCES = "META-INF/resources";
private ServletContext servletContext;
private ResourceManagerImpl resourceManager;
private ThemeBuilder themeBuilder;
public ResourceLocator(
ServletContext servletContext, ResourceManagerImpl resourceManager, ThemeBuilder themeBuilder) {
this.servletContext = servletContext;
this.resourceManager = resourceManager;
this.themeBuilder = themeBuilder;
}
public void locate()
throws ServletException {
// TODO should the resourcedir used from tobago-config.xml?
locateResourcesInWar(servletContext, resourceManager, "/");
locateResourcesFromClasspath(resourceManager);
locateResourcesServlet30Alike(resourceManager);
}
private void locateResourcesInWar(
ServletContext servletContext, ResourceManagerImpl resources, String path)
throws ServletException {
if (path.startsWith("/WEB-INF/")) {
return; // ignore
}
// fix for jetty6
if (path.endsWith("/") && path.length() > 1) {
path = path.substring(0, path.length() - 1);
}
Set<String> resourcePaths = servletContext.getResourcePaths(path);
if (resourcePaths == null || resourcePaths.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Skipping empty resource path: path='{}'", path);
}
return;
}
for (String childPath : resourcePaths) {
if (childPath.endsWith("/")) {
// ignore, because weblogic puts the path directory itself in the Set
if (!childPath.equals(path)) {
if (LOG.isDebugEnabled()) {
LOG.debug("childPath dir {}", childPath);
}
locateResourcesInWar(servletContext, resources, childPath);
}
} else {
//Log.debug("add resc " + childPath);
if (childPath.endsWith(".properties")) {
InputStream inputStream = servletContext.getResourceAsStream(childPath);
try {
addProperties(inputStream, resources, childPath, false, 0);
} finally {
IOUtils.closeQuietly(inputStream);
}
} else if (childPath.endsWith(".properties.xml")) {
InputStream inputStream = servletContext.getResourceAsStream(childPath);
try {
addProperties(inputStream, resources, childPath, true, 0);
} catch (RuntimeException e) {
LOG.error("childPath = \"" + childPath + "\" ", e);
throw e;
} finally {
IOUtils.closeQuietly(inputStream);
}
} else {
resources.add(childPath);
}
}
}
}
private void locateResourcesFromClasspath(ResourceManagerImpl resources)
throws ServletException {
ThemeParser parser = new ThemeParser();
try {
if (LOG.isInfoEnabled()) {
LOG.info("Searching for '" + META_INF_TOBAGO_THEME_XML + "' and '" + META_INF_TOBAGO_CONFIG_XML +"'");
}
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
List<URL> urls = new ArrayList<URL>();
CollectionUtils.addAll(urls, classLoader.getResources(META_INF_TOBAGO_CONFIG_XML));
CollectionUtils.addAll(urls, classLoader.getResources(META_INF_TOBAGO_THEME_XML));
for (URL themeUrl : urls) {
if (themeUrl.toString().endsWith(META_INF_TOBAGO_CONFIG_XML)) {
TobagoConfigFragment tobagoConfig = new TobagoConfigParser().parse(themeUrl);
for (ThemeImpl theme : tobagoConfig.getThemeDefinitions()) {
if (theme.isVersioned()) {
String themeUrlStr = themeUrl.toString();
int index = themeUrlStr.indexOf(META_INF_TOBAGO_CONFIG_XML);
String metaInf = themeUrlStr.substring(0, index) + "META-INF/MANIFEST.MF";
Properties properties = new Properties();
final URL url = new URL(metaInf);
InputStream inputStream = null;
String version = null;
try {
inputStream = url.openStream();
properties.load(inputStream);
version = properties.getProperty("Implementation-Version");
} catch (FileNotFoundException e) {
// may happen (e. g. in tests)
LOG.error("No Manifest-File found.");
} finally {
IOUtils.closeQuietly(inputStream);
}
if (version != null) {
theme.setVersion(version);
} else {
theme.setVersioned(false);
LOG.error("No Implementation-Version found in Manifest-File for theme: '" + theme.getName()
+ "'. Resetting the theme to unversioned. Please correct the Manifest-File.");
}
}
addThemeResources(resources, themeUrl, theme);
}
} else {
// the old way
addThemeResources(resources, themeUrl, parser.parse(themeUrl));
}
}
} catch (IOException e) {
String msg = "while loading ";
LOG.error(msg, e);
throw new ServletException(msg, e);
} catch (SAXException e) {
String msg = "while loading ";
LOG.error(msg, e);
throw new ServletException(msg, e);
}
}
private void addThemeResources(ResourceManagerImpl resources, URL themeUrl, ThemeImpl theme)
throws IOException, ServletException {
themeBuilder.addTheme(theme);
String prefix = ensureSlash(theme.getResourcePath());
String protocol = themeUrl.getProtocol();
// tomcat uses jar
// weblogic uses zip
// IBM WebSphere uses wsjar
if (!"jar".equals(protocol) && !"zip".equals(protocol) && !"wsjar".equals(protocol)) {
LOG.warn("Unknown protocol '" + themeUrl + "'");
}
addResources(resources, themeUrl, prefix, 0);
}
/**
* Searches the /WEB-INF/lib directory for *.jar files which contains /META-INF/resources directory
* to hold resources and add them to the ResourceManager.
*
* @param resources Resource Manager which collects all the resources.
* @throws ServletException An error while accessing the resource.
*/
private void locateResourcesServlet30Alike(ResourceManagerImpl resources) throws ServletException {
try {
if (LOG.isInfoEnabled()) {
LOG.info("Searching for " + META_INF_RESOURCES);
}
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> urls = classLoader.getResources(META_INF_RESOURCES);
while (urls.hasMoreElements()) {
URL resourcesUrl = urls.nextElement();
LOG.info("resourcesUrl='"+resourcesUrl + "'");
if (!resourcesUrl.toString().matches(".*/WEB-INF/lib/.*\\.jar\\!.*")) {
LOG.info("skip ...");
continue;
// only resources from jar files in the /WEB-INF/lib should be considered (like in Servlet 3.0 spec.)
}
LOG.info("going on ...");
String protocol = resourcesUrl.getProtocol();
// tomcat uses jar
// weblogic uses zip
// IBM WebSphere uses wsjar
if (!"jar".equals(protocol) && !"zip".equals(protocol) && !"wsjar".equals(protocol)) {
LOG.warn("Unknown protocol '" + resourcesUrl + "'");
}
addResources(resources, resourcesUrl, "/" + META_INF_RESOURCES, META_INF_RESOURCES.length() + 1);
}
} catch (IOException e) {
String msg = "while loading ";
LOG.error(msg, e);
throw new ServletException(msg, e);
}
}
private void addResources(ResourceManagerImpl resources, URL themeUrl, String prefix, int skipPrefix)
throws IOException, ServletException {
String fileName = themeUrl.toString();
if (fileName.endsWith(META_INF_TOBAGO_THEME_XML)) {
Deprecation.LOG.warn(
"The use of 'tobago-theme.xml' is deprecated, please use 'tobago-config.xml' to define a theme!");
}
int index = fileName.indexOf("!");
String protocol = themeUrl.getProtocol();
if (index != -1) {
fileName = fileName.substring(protocol.length() + 1, index);
}
if (LOG.isInfoEnabled()) {
LOG.info("Adding resources from fileName='" + fileName + "' prefix='" + prefix + "' skip=" + skipPrefix + "");
}
// JBoss 5.0.0 introduced vfszip protocol
if (!protocol.equals("vfszip")
&& (fileName.endsWith(META_INF_TOBAGO_THEME_XML) || fileName.endsWith(META_INF_TOBAGO_CONFIG_XML))) {
try {
URI uri = themeUrl.toURI();
File tobagoThemeXml = new File(uri);
File directoryFile = tobagoThemeXml.getParentFile().getParentFile();
String resourcePath = "";
resolveTheme(resources, directoryFile, resourcePath, prefix, false);
} catch (URISyntaxException e) {
LOG.error("themeUrl='" + themeUrl + "'", e);
}
} else {
URL jarFile;
try {
// JBoss 5.0.0 introduced vfszip protocol
if (protocol.equals("vfszip")) {
fileName = new File(fileName).getParentFile().getParentFile().getPath();
if (File.separatorChar == '\\' && fileName.contains("\\")) {
fileName = fileName.replace('\\', '/');
if (LOG.isInfoEnabled()) {
LOG.info("Fixed slashes for virtual filesystem protocol on windows system: " + fileName);
}
}
}
jarFile = new URL(fileName);
} catch (MalformedURLException e) {
// workaround for weblogic on windows
jarFile = new URL("file:" + fileName);
}
InputStream stream = null;
ZipInputStream zipStream = null;
try {
stream = jarFile.openStream();
zipStream = new ZipInputStream(stream);
while (zipStream.available() > 0) {
ZipEntry nextEntry = zipStream.getNextEntry();
if (nextEntry == null || nextEntry.isDirectory()) {
continue;
}
String name = "/" + nextEntry.getName();
if (name.startsWith(prefix)) {
addResource(resources, name, skipPrefix);
}
}
} finally {
IOUtils.closeQuietly(stream);
IOUtils.closeQuietly(zipStream);
}
}
}
private void resolveTheme(ResourceManagerImpl resources, File directoryFile,
String resourcePath, String prefix, boolean inResourcePath) throws ServletException {
File[] files = directoryFile.listFiles();
for (File file : files) {
if (file.isDirectory()) {
String currentResourcePath = resourcePath + File.separator + file.getName();
if (!inResourcePath && currentResourcePath.startsWith(prefix)) {
inResourcePath = true;
}
resolveTheme(resources, file, currentResourcePath, prefix, inResourcePath);
} else {
if (LOG.isInfoEnabled()) {
LOG.info(resourcePath + File.separator + file.getName());
}
if (inResourcePath) {
addResource(resources, resourcePath + File.separator + file.getName(), 0);
}
}
}
}
private void addResource(ResourceManagerImpl resources, String name, int skipPrefix)
throws ServletException {
if (name.endsWith(".class")) {
// ignore the class files
} else if (name.endsWith(".properties")) {
if (LOG.isInfoEnabled()) {
LOG.info("Adding properties from: '" + name.substring(1) + "'");
}
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(name.substring(1));
try {
addProperties(inputStream, resources, name, false, skipPrefix);
} finally {
IOUtils.closeQuietly(inputStream);
}
} else if (name.endsWith(".properties.xml")) {
if (LOG.isInfoEnabled()) {
LOG.info("Adding properties from: '" + name.substring(1) + "'");
}
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(name.substring(1));
try {
addProperties(inputStream, resources, name, true, skipPrefix);
} finally {
IOUtils.closeQuietly(inputStream);
}
} else {
resources.add(name.substring(skipPrefix));
}
}
private String ensureSlash(String resourcePath) {
if (!resourcePath.startsWith("/")) {
resourcePath = '/' + resourcePath;
}
if (!resourcePath.endsWith("/")) {
resourcePath = resourcePath + '/';
}
return resourcePath;
}
private void addProperties(
InputStream stream, ResourceManagerImpl resources, String childPath, boolean xml, int skipPrefix)
throws ServletException {
String directory = childPath.substring(skipPrefix, childPath.lastIndexOf('/'));
String filename = childPath.substring(childPath.lastIndexOf('/') + 1);
int end = filename.lastIndexOf('.');
if (xml) {
end = filename.lastIndexOf('.', end - 1);
}
String locale = filename.substring(0, end);
Properties temp = new Properties();
try {
if (xml) {
temp.loadFromXML(stream);
if (LOG.isDebugEnabled()) {
LOG.debug(childPath);
LOG.debug("xml properties: {}", temp.size());
}
} else {
temp.load(stream);
if (LOG.isDebugEnabled()) {
LOG.debug(childPath);
LOG.debug(" properties: {}", temp.size());
}
}
} catch (IOException e) {
String msg = "while loading " + childPath;
LOG.error(msg, e);
throw new ServletException(msg, e);
} finally {
IOUtils.closeQuietly(stream);
}
for (Enumeration e = temp.propertyNames(); e.hasMoreElements();) {
String key = (String) e.nextElement();
resources.add(directory + '/' + locale + '/' + key, temp.getProperty(key));
if (LOG.isDebugEnabled()) {
LOG.debug(directory + '/' + locale + '/' + key + "=" + temp.getProperty(key));
}
}
}
}