blob: bfaa296f2986bbebe46ac7ad904f1a615eda26e7 [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.config.annotation;
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.enterprise.inject.spi.BeanManager;
import javax.faces.FacesException;
import javax.faces.component.FacesComponent;
import javax.faces.component.behavior.FacesBehavior;
import javax.faces.context.ExternalContext;
import javax.faces.convert.FacesConverter;
import javax.faces.event.NamedEvent;
import javax.faces.render.FacesBehaviorRenderer;
import javax.faces.render.FacesRenderer;
import javax.faces.validator.FacesValidator;
import javax.faces.view.facelets.FaceletsResourceResolver;
import org.apache.myfaces.cdi.util.CDIUtils;
import org.apache.myfaces.config.MyfacesConfig;
import org.apache.myfaces.util.lang.ClassUtils;
import org.apache.myfaces.spi.AnnotationProvider;
import org.apache.myfaces.spi.AnnotationProviderFactory;
import org.apache.myfaces.view.facelets.util.Classpath;
/**
*
* @since 2.0.2
* @author Leonardo Uribe
*/
public class DefaultAnnotationProvider extends AnnotationProvider
{
private static final Logger log = Logger.getLogger(DefaultAnnotationProvider.class.getName());
/**
* <p>Prefix path used to locate web application classes for this
* web application.</p>
*/
private static final String WEB_CLASSES_PREFIX = "/WEB-INF/classes/";
/**
* <p>Prefix path used to locate web application libraries for this
* web application.</p>
*/
private static final String WEB_LIB_PREFIX = "/WEB-INF/lib/";
private static final String META_INF_PREFIX = "META-INF/";
private static final String FACES_CONFIG_SUFFIX = ".faces-config.xml";
/**
* <p>Resource path used to acquire implicit resources buried
* inside application JARs.</p>
*/
private static final String FACES_CONFIG_IMPLICIT = "META-INF/faces-config.xml";
/**
* This set contains the annotation names that this AnnotationConfigurator is able to scan
* in the format that is read from .class file.
*/
private static final Set<String> JSF_ANNOTATION_NAMES;
private static final Set<Class<? extends Annotation>> JSF_ANNOTATION_CLASSES;
static
{
Set<String> bcan = new HashSet<>(10, 1f);
bcan.add("Ljavax/faces/component/FacesComponent;");
bcan.add("Ljavax/faces/component/behavior/FacesBehavior;");
bcan.add("Ljavax/faces/convert/FacesConverter;");
bcan.add("Ljavax/faces/validator/FacesValidator;");
bcan.add("Ljavax/faces/render/FacesRenderer;");
bcan.add("Ljavax/faces/event/NamedEvent;");
//bcan.add("Ljavax/faces/event/ListenerFor;");
//bcan.add("Ljavax/faces/event/ListenersFor;");
bcan.add("Ljavax/faces/render/FacesBehaviorRenderer;");
bcan.add("Ljavax/faces/view/facelets/FaceletsResourceResolver;");
JSF_ANNOTATION_NAMES = Collections.unmodifiableSet(bcan);
Set<Class<? extends Annotation>> ancl = new HashSet<>(10, 1f);
ancl.add(FacesComponent.class);
ancl.add(FacesBehavior.class);
ancl.add(FacesConverter.class);
ancl.add(FacesValidator.class);
ancl.add(FacesRenderer.class);
ancl.add(NamedEvent.class);
ancl.add(FacesBehaviorRenderer.class);
ancl.add(FaceletsResourceResolver.class);
JSF_ANNOTATION_CLASSES = Collections.unmodifiableSet(ancl);
}
public DefaultAnnotationProvider()
{
super();
}
@Override
public Map<Class<? extends Annotation>, Set<Class<?>>> getAnnotatedClasses(ExternalContext ctx)
{
if (MyfacesConfig.getCurrentInstance(ctx).isUseCdiForAnnotationScanning())
{
BeanManager beanManager = CDIUtils.getBeanManager(ctx);
CdiAnnotationProviderExtension extension = CDIUtils.getOptional(beanManager,
CdiAnnotationProviderExtension.class);
if (extension != null)
{
return extension.getMap();
}
}
Map<Class<? extends Annotation>, Set<Class<?>>> map = new HashMap<>();
Collection<Class<?>> classes = null;
//1. Scan for annotations on /WEB-INF/classes
try
{
classes = getAnnotatedWebInfClasses(ctx);
}
catch (IOException e)
{
throw new FacesException(e);
}
for (Class<?> clazz : classes)
{
processClass(map, clazz);
}
//2. Scan for annotations on classpath
try
{
AnnotationProvider provider
= AnnotationProviderFactory.getAnnotationProviderFactory(ctx).getAnnotationProvider(ctx);
classes = getAnnotatedMetaInfClasses(ctx, provider.getBaseUrls(ctx));
}
catch (IOException e)
{
throw new FacesException(e);
}
for (Class<?> clazz : classes)
{
processClass(map, clazz);
}
return map;
}
@Override
public Set<URL> getBaseUrls(ExternalContext context) throws IOException
{
ClassLoader cl = ClassUtils.getCurrentLoader(this);
Set<URL> urlSet = new HashSet<>();
//This usually happens when maven-jetty-plugin is used
//Scan jars looking for paths including META-INF/faces-config.xml
Enumeration<URL> resources = cl.getResources(FACES_CONFIG_IMPLICIT);
while (resources.hasMoreElements())
{
urlSet.add(resources.nextElement());
}
//Scan files inside META-INF ending with .faces-config.xml
URL[] urls = Classpath.search(cl, META_INF_PREFIX, FACES_CONFIG_SUFFIX);
Collections.addAll(urlSet, urls);
return urlSet;
}
protected Collection<Class<?>> getAnnotatedMetaInfClasses(ExternalContext ctx, Set<URL> urls)
{
if (urls != null && !urls.isEmpty())
{
List<Class<?>> list = new ArrayList<>();
for (URL url : urls)
{
try
{
JarFile jarFile = getJarFile(url);
if (jarFile != null)
{
archiveClasses(jarFile, list);
}
}
catch(IOException e)
{
log.log(Level.SEVERE, "cannot scan jar file for annotations:"+url, e);
}
}
return list;
}
return Collections.emptyList();
}
protected Collection<Class<?>> getAnnotatedWebInfClasses(ExternalContext ctx) throws IOException
{
String scanPackages = MyfacesConfig.getCurrentInstance(ctx).getScanPackages();
if (scanPackages != null)
{
try
{
return packageClasses(ctx, scanPackages);
}
catch (ClassNotFoundException | IOException e)
{
throw new FacesException(e);
}
}
else
{
return webClasses(ctx);
}
}
/**
* <p>Return a list of the classes defined within the given packages
* If there are no such classes, a zero-length list will be returned.</p>
*
* @param scanPackages the package configuration
*
* @exception ClassNotFoundException if a located class cannot be loaded
* @exception IOException if an input/output error occurs
*/
private List<Class<?>> packageClasses(final ExternalContext externalContext, final String scanPackages)
throws ClassNotFoundException, IOException
{
List<Class<?>> list = new ArrayList<>();
String[] scanPackageTokens = scanPackages.split(",");
for (String scanPackageToken : scanPackageTokens)
{
if (scanPackageToken.toLowerCase().endsWith(".jar"))
{
URL jarResource = externalContext.getResource(WEB_LIB_PREFIX + scanPackageToken);
String jarURLString = "jar:" + jarResource.toString() + "!/";
URL url = new URL(jarURLString);
JarFile jarFile = ((JarURLConnection) url.openConnection()).getJarFile();
archiveClasses(jarFile, list);
}
else
{
Class[] classes = PackageInfo.getClasses(scanPackageToken);
for (Class c : classes)
{
list.add(c);
}
}
}
return list;
}
/**
* <p>Return a list of classes to examine from the specified JAR archive.
* If this archive has no classes in it, a zero-length list is returned.</p>
*
* @param context <code>ExternalContext</code> instance for
* this application
* @param jar <code>JarFile</code> for the archive to be scanned
*
* @exception ClassNotFoundException if a located class cannot be loaded
*/
private List<Class<?>> archiveClasses(JarFile jar, List<Class<?>> list)
{
// Accumulate and return a list of classes in this JAR file
ClassLoader loader = ClassUtils.getContextClassLoader();
if (loader == null)
{
loader = this.getClass().getClassLoader();
}
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements())
{
JarEntry entry = entries.nextElement();
if (entry.isDirectory())
{
continue; // This is a directory
}
String name = entry.getName();
if (name.startsWith("META-INF/"))
{
continue; // Attribute files
}
if (!name.endsWith(".class"))
{
continue; // This is not a class
}
DataInputStream in = null;
boolean couldContainAnnotation = false;
try
{
in = new DataInputStream(jar.getInputStream(entry));
couldContainAnnotation = _ClassByteCodeAnnotationFilter.couldContainAnnotationsOnClassDef(in,
JSF_ANNOTATION_NAMES);
}
catch (IOException e)
{
// Include this class - we can't scan this class using
// the filter, but it could be valid, so we need to
// load it using the classLoader. Anyway, log a debug
// message.
couldContainAnnotation = true;
if (log.isLoggable(Level.FINE))
{
log.fine("IOException when filtering class " + name + " for annotations");
}
}
finally
{
if (in != null)
{
try
{
in.close();
}
catch (IOException e)
{
// No Op
}
}
}
if (couldContainAnnotation)
{
name = name.substring(0, name.length() - 6); // Trim ".class"
Class<?> clazz = null;
try
{
clazz = loader.loadClass(name.replace('/', '.'));
}
catch (NoClassDefFoundError | Exception e)
{
// Skip this class - we cannot analyze classes we cannot load
}
// Skip this class - we cannot analyze classes we cannot load
if (clazz != null)
{
list.add(clazz);
}
}
}
return list;
}
/**
* <p>Return a list of the classes defined under the
* <code>/WEB-INF/classes</code> directory of this web
* application. If there are no such classes, a zero-length list
* will be returned.</p>
*
* @param externalContext <code>ExternalContext</code> instance for
* this application
*
* @exception ClassNotFoundException if a located class cannot be loaded
*/
private List<Class<?>> webClasses(ExternalContext externalContext)
{
List<Class<?>> list = new ArrayList<>();
webClasses(externalContext, WEB_CLASSES_PREFIX, list);
return list;
}
/**
* <p>Add classes found in the specified directory to the specified
* list, recursively calling this method when a directory is encountered.</p>
*
* @param externalContext <code>ExternalContext</code> instance for
* this application
* @param prefix Prefix specifying the "directory path" to be searched
* @param list List to be appended to
*
* @exception ClassNotFoundException if a located class cannot be loaded
*/
private void webClasses(ExternalContext externalContext, String prefix, List<Class<?>> list)
{
ClassLoader loader = ClassUtils.getCurrentLoader(this);
Set<String> paths = externalContext.getResourcePaths(prefix);
if (paths == null)
{
return; //need this in case there is no WEB-INF/classes directory
}
if (log.isLoggable(Level.FINEST))
{
log.finest("webClasses(" + prefix + ") - Received " + paths.size() + " paths to check");
}
String path = null;
if (paths.isEmpty())
{
if (log.isLoggable(Level.WARNING))
{
log.warning("AnnotationConfigurator does not found classes "
+ "for annotations in "
+ prefix
+ " ."
+ " This could happen because maven jetty plugin is used"
+ " (goal jetty:run). Try configure "
+ MyfacesConfig.SCAN_PACKAGES + " init parameter "
+ "or use jetty:run-exploded instead.");
}
}
else
{
for (Object pathObject : paths)
{
path = (String) pathObject;
if (path.endsWith("/"))
{
webClasses(externalContext, path, list);
}
else if (path.endsWith(".class"))
{
DataInputStream in = null;
boolean couldContainAnnotation = false;
try
{
in = new DataInputStream(externalContext.getResourceAsStream(path));
couldContainAnnotation = _ClassByteCodeAnnotationFilter.couldContainAnnotationsOnClassDef(in,
JSF_ANNOTATION_NAMES);
}
catch (IOException e)
{
// Include this class - we can't scan this class using
// the filter, but it could be valid, so we need to
// load it using the classLoader. Anyway, log a debug
// message.
couldContainAnnotation = true;
if (log.isLoggable(Level.FINE))
{
log.fine("IOException when filtering class " + path + " for annotations");
}
}
finally
{
if (in != null)
{
try
{
in.close();
}
catch (IOException e)
{
// No Op
}
}
}
if (couldContainAnnotation)
{
//Load it and add it to list for later processing
path = path.substring(WEB_CLASSES_PREFIX.length()); // Strip prefix
path = path.substring(0, path.length() - 6); // Strip suffix
path = path.replace('/', '.'); // Convert to FQCN
Class<?> clazz = null;
try
{
clazz = loader.loadClass(path);
}
catch (NoClassDefFoundError | Exception e)
{
// Skip this class - we cannot analyze classes we cannot load
}
// Skip this class - we cannot analyze classes we cannot load
if (clazz != null)
{
list.add(clazz);
}
}
}
}
}
}
private JarFile getJarFile(URL url) throws IOException
{
URLConnection conn = url.openConnection();
conn.setUseCaches(false);
conn.setDefaultUseCaches(false);
JarFile jarFile;
if (conn instanceof JarURLConnection)
{
jarFile = ((JarURLConnection) conn).getJarFile();
}
else
{
jarFile = _getAlternativeJarFile(url);
}
return jarFile;
}
/**
* taken from org.apache.myfaces.view.facelets.util.Classpath
*
* 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();
// Trim off any suffix - which is prefixed by "!/" on Weblogic
int separatorIndex = urlFile.indexOf("!/");
// OK, didn't find that. Try the less safe "!", used on OC4J
if (separatorIndex == -1)
{
separatorIndex = urlFile.indexOf('!');
}
if (separatorIndex != -1)
{
String jarFileUrl = urlFile.substring(0, separatorIndex);
// And trim off any "file:" prefix.
if (jarFileUrl.startsWith("file:"))
{
jarFileUrl = jarFileUrl.substring("file:".length());
}
return new JarFile(jarFileUrl);
}
return null;
}
private void processClass(Map<Class<? extends Annotation>,Set<Class<?>>> map, Class<?> clazz)
{
Annotation[] annotations = clazz.getAnnotations();
for (Annotation anno : annotations)
{
Class<? extends Annotation> annotationClass = anno.annotationType();
if (JSF_ANNOTATION_CLASSES.contains(annotationClass))
{
Set<Class<?>> set = map.computeIfAbsent(annotationClass, k -> new HashSet<>());
set.add(clazz);
}
}
}
}