/*
* Copyright 2002-20011 the original author or authors.
*
* Licensed 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.springframework.flex.core.io;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.SystemPropertyUtils;
/**
* Generic extension of {@link AbstractAmfConversionServiceConfigProcessor} that configures the Spring {@link ConversionService}-based AMF
* serialization/deserialization support via classpath scanning.
*
* <p>
* This implementation will recursively scan for classes starting with the base package provided in the
* {@link ClassPathScanningAmfConversionServiceConfigProcessor#ClassPathScanningAmfConversionServiceConfigProcessor(String) constructor}.
* By default, a {@link SpringPropertyProxy} will be registered for every class found in the scan.
*
* <p>
* The scanning process may be more finely tuned by providing {@link TypeFilter} implementations to be used in
* {@link ClassPathScanningAmfConversionServiceConfigProcessor#setIncludeFilters(List) including} and
* {@link ClassPathScanningAmfConversionServiceConfigProcessor#setExcludeFilters(List) excluding} specific classes. For example, you
* can filter by {@link RegexPatternTypeFilter RegEx patterns}, {@link AnnotationTypeFilter custom annotations}, or anything else that
* can be used for matching in a {@code TypeFilter}.
*
* <p>
* This implementation does not register any additional {@link Converter Converters} beyond those registered in the parent class.
* Additional {@code TypeConverters} may be registered by extending this class and overriding
* {@link AbstractAmfConversionServiceConfigProcessor#configureConverters(org.springframework.core.convert.converter.ConverterRegistry) configureConverters}.
*
* <p>
* The implementation is heavily derived from {@link ClassPathScanningCandidateComponentProvider}.
*
* @author Jeremy Grelle
*/
public class ClassPathScanningAmfConversionServiceConfigProcessor extends AbstractAmfConversionServiceConfigProcessor implements ResourceLoaderAware, BeanClassLoaderAware {
private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
private String resourcePattern = DEFAULT_RESOURCE_PATTERN;
private ClassLoader beanClassLoader;
private List<TypeFilter> includeFilters = new LinkedList<TypeFilter>();
private List<TypeFilter> excludeFilters = new LinkedList<TypeFilter>();
private final String basePackage;
/**
* Create a ClassPathScanningAmfConversionServiceConfigProcessor
* @param basePackage the base package to scan for classes to be registered for AMF serialization/deserialization
*/
public ClassPathScanningAmfConversionServiceConfigProcessor(String basePackage) {
this.basePackage = basePackage;
}
/**
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet() throws Exception {
if (CollectionUtils.isEmpty(includeFilters)) {
addIncludeFilter(new IncludeAllFilter());
}
super.afterPropertiesSet();
}
/**
* {@inheritDoc}
*/
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
/**
* Set the ResourceLoader to use for resource locations.
* This will typically be a ResourcePatternResolver implementation.
* <p>Default is PathMatchingResourcePatternResolver, also capable of
* resource pattern resolving through the ResourcePatternResolver interface.
* @see org.springframework.core.io.support.ResourcePatternResolver
* @see org.springframework.core.io.support.PathMatchingResourcePatternResolver
*/
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
}
/**
* Return the ResourceLoader used in locating matching resources.
*/
public final ResourceLoader getResourceLoader() {
return this.resourcePatternResolver;
}
/**
* Set the resource pattern to use when scanning the classpath.
* This value will be appended to each base package name.
* @see #findTypesToRegister()
* @see #DEFAULT_RESOURCE_PATTERN
*/
public void setResourcePattern(String resourcePattern) {
Assert.notNull(resourcePattern, "'resourcePattern' must not be null");
this.resourcePattern = resourcePattern;
}
/**
* Sets the list of type filters to use for inclusion.
*/
public void setIncludeFilters(List<TypeFilter> includeFilters) {
this.includeFilters = includeFilters;
}
/**
* Sets the list of type filters to use for exclusion.
*/
public void setExcludeFilters(List<TypeFilter> excludeFilters) {
this.excludeFilters = excludeFilters;
}
/**
* Add an include type filter to the <i>end</i> of the inclusion list.
*/
public void addIncludeFilter(TypeFilter includeFilter) {
this.includeFilters.add(includeFilter);
}
/**
* Add an exclude type filter to the <i>front</i> of the exclusion list.
*/
public void addExcludeFilter(TypeFilter excludeFilter) {
this.excludeFilters.add(0, excludeFilter);
}
/**
* {@inheritDoc}
*/
@Override
protected Set<Class<?>> findTypesToRegister() {
Set<Class<?>> typesToRegister = new HashSet<Class<?>>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + "/" + this.resourcePattern;
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
boolean traceEnabled = log.isTraceEnabled();
boolean debugEnabled = log.isDebugEnabled();
for (Resource resource : resources) {
if (traceEnabled) {
log.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
if (applyFilters(metadataReader)) {
if (isCandidateForAmf(metadataReader.getAnnotationMetadata())) {
if (debugEnabled) {
log.debug("Identified candidate AMF class: " + resource);
}
typesToRegister.add(ClassUtils.forName(metadataReader.getAnnotationMetadata().getClassName(), this.beanClassLoader));
}
else {
if (debugEnabled) {
log.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
log.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate AMF class: " + resource, ex);
}
}
else {
if (traceEnabled) {
log.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
return typesToRegister;
}
/**
* Resolve the specified base package into a pattern specification for
* the package search path.
* <p>The default implementation resolves placeholders against system properties,
* and converts a "."-based package path to a "/"-based resource path.
* @param basePackage the base package as specified by the user
* @return the pattern specification to be used for package searching
*/
protected String resolveBasePackage(String basePackage) {
return ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage));
}
/**
* Determine whether the given class does not match any exclude filter
* and does match at least one include filter.
* @param metadataReader the ASM ClassReader for the class
* @return whether the class passes the configured filters
*/
protected boolean applyFilters(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, this.metadataReaderFactory)) {
return true;
}
}
return false;
}
/**
* Determine whether the given class qualifies for AMF conversion
* <p>The default implementation checks whether the class is concrete
* (i.e. not abstract and not an interface). Can be overridden in subclasses.
* @param metadata the metadata for the class to check
* @return whether the class qualifies for AMF conversion
*/
protected boolean isCandidateForAmf(AnnotationMetadata metadata) {
return (metadata.isConcrete() && metadata.isIndependent());
}
private final class IncludeAllFilter implements TypeFilter{
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return isCandidateForAmf(metadataReader.getAnnotationMetadata());
}
}
}