| /* |
| * 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); |
| } |
| } |
| } |
| } |