blob: b42e27a0b1b0d60a126a21baebd924681dcf5fde [file] [log] [blame]
/*
* 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.view.facelets.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.myfaces.util.lang.ClassUtils;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.PrivilegedAction;
/**
* @author Jacob Hookom
* @author Roland Huss
* @author Ales Justin (ales.justin@jboss.org)
* @version $Id$
*/
public final class Classpath
{
private static final Charset UTF8 = Charset.forName("UTF-8");
private static final Set<String> EXCLUDED_PREFIX_SET = new HashSet<String>(Arrays.asList("rar:", "sar:"));
private static final Set<String> EXCLUDED_SUFFIX_SET = new HashSet<String>(Arrays.asList(".rar", ".sar"));
private Classpath()
{
}
public static URL[] search(String prefix, String suffix) throws IOException
{
return search(ClassUtils.getContextClassLoader(), prefix, suffix);
}
public static URL[] search(ClassLoader loader, String prefix, String suffix) throws IOException
{
Set<URL> all = new LinkedHashSet<>();
_searchResource(all, loader, prefix, prefix, suffix);
_searchResource(all, loader, prefix + "MANIFEST.MF", prefix, suffix);
URL[] urlArray = (URL[]) all.toArray(new URL[all.size()]);
return urlArray;
}
private static void _searchResource(Set<URL> result, ClassLoader loader, String resource, String prefix,
String suffix) throws IOException
{
for (Enumeration<URL> urls = loader.getResources(resource); urls.hasMoreElements();)
{
URL url = urls.nextElement();
URLConnection conn = url.openConnection();
conn.setUseCaches(false);
conn.setDefaultUseCaches(false);
try (JarFile jar = (conn instanceof JarURLConnection) ?
((JarURLConnection) conn).getJarFile() : _getAlternativeJarFile(url))
{
if (jar != null)
{
_searchJar(loader, result, jar, prefix, suffix);
}
else
{
if (!_searchDir(result, new File(URLDecoder.decode(url.getFile(), "UTF-8")), suffix))
{
_searchFromURL(result, prefix, suffix, url);
}
}
}
catch (Throwable e)
{
// This can happen if the classloader provided us a URL that it thinks exists
// but really doesn't. In particular, if a JAR contains META-INF/MANIFEST.MF
// but not META-INF/, some classloaders may incorrectly report that META-INF/
// exists and we'll end up here. Just ignore this case.
continue;
}
}
}
private static boolean _searchDir(Set<URL> result, File dir, String suffix) throws IOException
{
boolean dirExists = false;
if (System.getSecurityManager() != null)
{
final File finalDir = dir;
dirExists = (Boolean) AccessController.doPrivileged((PrivilegedAction) () -> finalDir.exists());
}
else
{
dirExists = dir.exists();
}
if (dirExists && dir.isDirectory())
{
File[] dirFiles = dir.listFiles();
if (dirFiles != null)
{
for (File file : dirFiles)
{
String path = file.getAbsolutePath();
if (file.isDirectory())
{
_searchDir(result, file, suffix);
}
else if (path.endsWith(suffix))
{
result.add(file.toURI().toURL());
}
}
return true;
}
}
return false;
}
/**
* Search from URL. Fall back on prefix tokens if not able to read from original url param.
*
* @param result
* the result urls
* @param prefix
* the current prefix
* @param suffix
* the suffix to match
* @param url
* the current url to start search
* @throws IOException
* for any error
*/
private static void _searchFromURL(Set<URL> result, String prefix, String suffix, URL url) throws IOException
{
boolean done = false;
try (InputStream is = _getInputStream(url))
{
if (is != null)
{
try
{
ZipInputStream zis;
if (is instanceof ZipInputStream)
{
zis = (ZipInputStream) is;
}
else
{
zis = new ZipInputStream(is);
}
try
{
ZipEntry entry = zis.getNextEntry();
// initial entry should not be null
// if we assume this is some inner jar
done = entry != null;
while (entry != null)
{
String entryName = entry.getName();
if (entryName.endsWith(suffix))
{
result.add(new URL(url.toExternalForm() + entryName));
}
entry = zis.getNextEntry();
}
}
finally
{
zis.close();
}
}
catch (Exception ignore)
{
}
}
}
if (!done && prefix.length() > 0)
{
// we add '/' at the end since join adds it as well
String urlString = url.toExternalForm() + '/';
String[] split = prefix.split("/");
prefix = _join(split, true);
String end = _join(split, false);
urlString = urlString.substring(0, urlString.lastIndexOf(end));
if (isExcludedPrefix(urlString))
{
// excluded URL found, ignore it
return;
}
url = new URL(urlString);
_searchFromURL(result, prefix, suffix, url);
}
}
/**
* Join tokens, exlude last if param equals true.
*
* @param tokens
* the tokens
* @param excludeLast
* do we exclude last token
* @return joined tokens
*/
private static String _join(String[] tokens, boolean excludeLast)
{
StringBuilder join = new StringBuilder();
int length = tokens.length - (excludeLast ? 1 : 0);
for (int i = 0; i < length; i++)
{
join.append(tokens[i]).append('/');
}
return join.toString();
}
/**
* Open input stream from url. Ignore any errors.
*
* @param url
* the url to open
* @return input stream or null if not possible
*/
private static InputStream _getInputStream(URL url)
{
try
{
return url.openStream();
}
catch (Throwable t)
{
return null;
}
}
/**
* For URLs to JARs that do not use JarURLConnection - allowed by the servlet spec - attempt to produce a JarFile
* object all the same. Known servlet engines that function like this include Weblogic and OC4J. This is not a full
* solution, since an unpacked WAR or EAR will not have JAR "files" as such.
*/
private static JarFile _getAlternativeJarFile(URL url) throws IOException
{
String urlFile = url.getFile();
// Find suffix prefixed by "!/" on Weblogic
int wlIndex = urlFile.indexOf("!/");
// Find suffix prefixed by '!' on OC4J
int oc4jIndex = urlFile.indexOf('!');
// Take the first found suffix
int separatorIndex = wlIndex == -1 && oc4jIndex == -1 ? -1 : wlIndex < oc4jIndex ? wlIndex : oc4jIndex;
if (separatorIndex != -1)
{
String jarFileUrl = urlFile.substring(0, separatorIndex);
// And trim off any "file:" prefix.
if (jarFileUrl.startsWith("file:"))
{
jarFileUrl = jarFileUrl.substring("file:".length());
}
// make sure this is a valid file system path by removing escaping of white-space characters, etc.
jarFileUrl = decodeFilesystemUrl(jarFileUrl);
if (isExcludedPrefix(jarFileUrl) || isExcludedSuffix(jarFileUrl))
{
// excluded URL found, ignore it
return null;
}
return new JarFile(jarFileUrl);
}
return null;
}
private static boolean isExcludedPrefix(String url)
{
return EXCLUDED_PREFIX_SET.contains(url.substring(0, 4));
}
private static boolean isExcludedSuffix(String url)
{
int length = url.length();
return EXCLUDED_SUFFIX_SET.contains(url.substring(length - 4, length));
}
private static void _searchJar(ClassLoader loader, Set<URL> result, JarFile file, String prefix, String suffix)
throws IOException
{
Enumeration<JarEntry> e = file.entries();
while (e.hasMoreElements())
{
try
{
String name = e.nextElement().getName();
if (name.startsWith(prefix) && name.endsWith(suffix))
{
Enumeration<URL> e2 = loader.getResources(name);
while (e2.hasMoreElements())
{
result.add(e2.nextElement());
}
}
}
catch (Throwable t)
{
// shallow
}
}
}
private static String decodeFilesystemUrl(String url)
{
//borrowed from commons-io FileUtils.
String decoded = url;
if (url != null && url.indexOf('%') >= 0)
{
int n = url.length();
StringBuilder buffer = new StringBuilder();
ByteBuffer bytes = ByteBuffer.allocate(n);
for (int i = 0; i < n; )
{
if (url.charAt(i) == '%')
{
try
{
do
{
byte octet = (byte) Integer.parseInt(url.substring(i + 1, i + 3), 16);
bytes.put(octet);
i += 3;
} while (i < n && url.charAt(i) == '%');
continue;
}
catch (RuntimeException e)
{
// malformed percent-encoded octet, fall through and
// append characters literally
}
finally
{
if (bytes.position() > 0)
{
bytes.flip();
buffer.append(UTF8.decode(bytes).toString());
bytes.clear();
}
}
}
buffer.append(url.charAt(i++));
}
decoded = buffer.toString();
}
return decoded;
}
}