/*
* This is a common dao with basic CRUD operations and is not limited to any
* persistent layer implementation
*
* Copyright (C) 2008 Imran M Yousuf (imyousuf@smartitengineering.com)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.smartitengineering.exim.impl;
import com.smartitengineering.domain.annotations.Eager;
import com.smartitengineering.domain.annotations.Export;
import com.smartitengineering.domain.annotations.Id;
import com.smartitengineering.domain.annotations.Name;
import com.smartitengineering.domain.annotations.ResourceDomain;
import com.smartitengineering.domain.exim.DomainSelfExporter;
import com.smartitengineering.domain.exim.DomainSelfImporter;
import com.smartitengineering.domain.exim.IdentityCustomizer;
import com.smartitengineering.domain.exim.StringValueProvider;
import com.smartitengineering.exim.AssociationConfig;
import com.smartitengineering.exim.ClassConfigScanner;
import com.smartitengineering.exim.ConfigRegistrar;
import com.smartitengineering.exim.EximResourceConfig;
import com.smartitengineering.exim.PackageConfigScanner;
import com.smartitengineering.util.simple.IOFactory;
import com.smartitengineering.util.simple.reflection.AnnotationConfig;
import com.smartitengineering.util.simple.reflection.ClassAnnotationVisitorImpl;
import com.smartitengineering.util.simple.reflection.ClassScanner;
import com.smartitengineering.util.simple.reflection.VisitCallback;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* This registrar is responsible for scanning and containing configuration of
* all resources. Whenever a class is looked up whose package has not been
* scanned yet, registrar will scanClassForConfig its package to generate cofiguration and
* while generating it will keep scanning until it reaches the leaves, i.e. in
* this case association to an object that isn't {@link ResourceDomain}
* @author imyousuf
* @since 0.4
*/
public class DefaultAnnotationConfigScanner
implements ClassConfigScanner,
PackageConfigScanner {
private static final String GETTER_PREFIX = "get";
private static final String IS_PREFIX = "is";
private static final String HAS_PREFIX = "has";
private static final DefaultAnnotationConfigScanner REGISTRAR;
static {
ConfigRegistrar.registerClassScanner(
DefaultAnnotationConfigScanner.class, 25);
ConfigRegistrar.registerPackageScanner(
DefaultAnnotationConfigScanner.class, 25);
REGISTRAR = new DefaultAnnotationConfigScanner();
}
/**
* Get a singleton instance of the default annotation config scanner; it
* will initialize the singleton instance lazily, i.e. upon first request.
* @return The singleton package & class annotation config scanner
*/
public static DefaultAnnotationConfigScanner getInstance() {
return REGISTRAR;
}
/**
* The class scanner scanning for specified annotations
*/
protected final ClassScanner classScanner;
/**
* The configuration map for holding all resource configurations against
* their domain class.
*/
protected final Map<Class, EximResourceConfig> configuraitons;
/**
* Collection of all classes scanned till thus
*/
protected final Collection<Class> scannedClasses;
/**
* Collection of all packages scanned till thus
*/
protected final Collection<String> scannedPackages;
/**
* This is the annotation visit callback handling resources only
*/
protected final ResourceVisitCallback resourceVisitCallback;
/**
* The callback handler to be invoked by the class scanner
*/
protected final VisitCallback<AnnotationConfig> callbackHandler;
/**
* Initializes the member variables only.
*/
protected DefaultAnnotationConfigScanner() {
classScanner = IOFactory.getDefaultClassScanner();
configuraitons = new HashMap<Class, EximResourceConfig>();
scannedClasses = new HashSet<Class>();
scannedPackages = new HashSet<String>();
resourceVisitCallback = new ResourceVisitCallback();
callbackHandler = resourceVisitCallback;
}
public synchronized Map<Class, EximResourceConfig> getConfigurations() {
return configuraitons;
}
public synchronized Collection<Class> getConfiguredResourceClasses() {
return configuraitons.keySet();
}
/**
* Retrieves the configuration for the resource class, if its not already
* generated then it will search the package of the class and generate its
* configuraiton.
* @param resourceClass The class to scanClassForConfig retrieve configuration for
* @return Configuration for the resource; NULL if no resource is available
* or generateable
* @throws IllegalArgumentException If resource class is null!
*/
public synchronized EximResourceConfig getResourceConfigForClass(
final Class resourceClass) {
if (resourceClass == null) {
throw new IllegalArgumentException("Resource class can't be null!");
}
if (configuraitons.containsKey(resourceClass)) {
return configuraitons.get(resourceClass);
}
else {
String packageName = resourceClass.getPackage().getName();
EximResourceConfig resourceConfig = null;
if (!scannedPackages.contains(packageName)) {
resourceConfig = scanPackage(Package.getPackage(packageName),
resourceClass);
}
if (resourceConfig != null) {
return resourceConfig;
}
else {
//Check direct ancestor
Class parentClass = resourceClass.getSuperclass();
final EximResourceConfig parentClassConfig;
if (parentClass == null || parentClass.equals(Object.class)) {
parentClassConfig = null;
}
else {
parentClassConfig = getResourceConfigForClass(parentClass);
}
if (parentClassConfig == null) {
//Check for directly implemented interfaces
Class[] interfaces = resourceClass.getInterfaces();
Set<Class> interfaceSet = new TreeSet<Class>(new Comparator<Class>() {
public int compare(Class clazz1,
Class clazz2) {
EximResourceConfig config1 =
getConfigurations().get(clazz1);
EximResourceConfig config2 =
getConfigurations().get(clazz2);
Integer priority1 = config1.getPriority();
Integer priority2 = config2.getPriority();
if (priority1.equals(priority2)) {
return clazz1.getName().compareTo(
clazz2.getName()) * -1;
}
else {
return priority1.compareTo(priority2) * -1;
}
}
});
for (Class interfaceImpl : interfaces) {
EximResourceConfig interfaceConfig = getResourceConfigForClass(
interfaceImpl);
if (interfaceConfig != null) {
interfaceSet.add(
interfaceConfig.getDomainClass());
}
}
if (interfaceSet.isEmpty()) {
return null;
}
else {
return getConfigurations().get(interfaceSet.iterator().
next());
}
}
else {
return parentClassConfig;
}
}
}
}
/**
* Scans and prepares all configurations in the package and makes it
* available for future use.
* @param resourcePackage Package to scan and gather configuration
* @throws IllegalArgumentException If package is null
*/
public synchronized void scanPackageForResourceConfigs(
final Package resourcePackage) {
if (resourcePackage == null) {
throw new IllegalArgumentException("Resource class can't be null!");
}
scanPackage(resourcePackage, null);
}
/**
* Given a method name it returns the property name for it. Currently it
* supports methods starting with "get", "is" and "has".
* @param methodName The method name from which to derive the property name
* @return Property name represented by the read accessor
* @throws IllegalArgumentException If prefix is not supported
* @throws NullPointerException If method name is null
*/
protected String getPropertyNameFromMethodName(final String methodName)
throws IllegalArgumentException,
NullPointerException {
StringBuilder propertyNameBuilder =
new StringBuilder(methodName);
int cutLength = -1;
if (methodName.startsWith(GETTER_PREFIX)) {
cutLength = GETTER_PREFIX.length();
}
else if (methodName.startsWith(IS_PREFIX)) {
cutLength = IS_PREFIX.length();
}
else if (methodName.startsWith(HAS_PREFIX)) {
cutLength = HAS_PREFIX.length();
}
else {
throw new IllegalArgumentException(
"Not a valid property read accoessor");
}
propertyNameBuilder.delete(0, cutLength);
char firstChar = propertyNameBuilder.charAt(0);
propertyNameBuilder.delete(0, 1);
propertyNameBuilder.insert(0, Character.toLowerCase(firstChar));
return propertyNameBuilder.toString();
}
/**
* Scan among the class's annotations to find the required configuration for
* exporting and importing resources.
* @param probableResourceClass The probable resource domain class.
* @return The configuration of the class, null if not annotated with
* {@link ResourceDomain}
*/
protected EximResourceConfig scanClassForConfig(
final Class probableResourceClass) {
if (probableResourceClass == null) {
return null;
}
if (scannedClasses.contains(probableResourceClass)) {
return getConfigurations().get(probableResourceClass);
}
Annotation annotation = probableResourceClass.getAnnotation(
ResourceDomain.class);
if (annotation == null) {
return null;
}
EximResourceConfigImpl resourceConfig = new EximResourceConfigImpl();
resourceConfig.setDomainClass(probableResourceClass);
Name nameAnnotation = (Name) probableResourceClass.getAnnotation(
Name.class);
if (nameAnnotation != null) {
resourceConfig.setName(nameAnnotation.value());
}
else {
resourceConfig.setName(probableResourceClass.getName());
}
ResourceDomain domainAnnotation = (ResourceDomain) annotation;
resourceConfig.setAccessByPropertyEnabled(domainAnnotation.
accessByProperty());
resourceConfig.setAssociateExportPolicyAsUri(domainAnnotation.
exportAsURIByDefault());
resourceConfig.setPathToResource(domainAnnotation.path());
resourceConfig.setExporterImplemented(DomainSelfExporter.class.
isAssignableFrom(probableResourceClass));
resourceConfig.setImporterImplemented(DomainSelfImporter.class.
isAssignableFrom(probableResourceClass));
resourceConfig.setIdentityCustomizerImplemented(
IdentityCustomizer.class.isAssignableFrom(probableResourceClass));
resourceConfig.setPriority(domainAnnotation.priority());
resourceConfig.setExportBasicTypesInTypeElementEnabled(domainAnnotation.
exportBasicTypesInTypeElementEnabled());
scanMembers(resourceConfig, probableResourceClass);
scannedClasses.add(probableResourceClass);
//If domain id is not specified then its not a valid domain
if (!resourceConfig.isIdentityCustomizerImplemented() &&
(resourceConfig.getIdPropertyName() == null || resourceConfig.
getIdPropertyName().equals(""))) {
return null;
}
else {
configuraitons.put(probableResourceClass, resourceConfig);
return resourceConfig;
}
}
/**
* Scan a package for extracting configurations of resource domains.
* @param packageToScan Package to scanClassForConfig.
* @param resourceClass Main class scanClassForConfig requested for
* @return The configuration of the resource class, Null if the class is not
* a domain class or resourceClass is null
* @throws java.lang.IllegalArgumentException If package is null
*/
protected EximResourceConfig scanPackage(final Package packageToScan,
final Class resourceClass)
throws IllegalArgumentException {
if (packageToScan == null) {
throw new IllegalArgumentException();
}
EximResourceConfig resourceConfig = null;
classScanner.scan(new String[]{packageToScan.getName()},
new ClassAnnotationVisitorImpl(callbackHandler,
IOFactory.
getAnnotationNameForVisitor(ResourceDomain.class)));
Set<String> classPaths = resourceVisitCallback.getProbableResources();
if (!classPaths.isEmpty()) {
for (String classPath : classPaths) {
try {
Class probableResourceClass =
IOFactory.getClassFromVisitorName(classPath);
EximResourceConfig config = scanClassForConfig(
probableResourceClass);
if (config != null && resourceClass != null &&
probableResourceClass.equals(resourceClass)) {
resourceConfig = config;
}
}
catch (ClassNotFoundException ex) {
}
catch (IndexOutOfBoundsException ex) {
}
catch (RuntimeException ex) {
}
}
}
scannedPackages.add(packageToScan.getName());
return resourceConfig;
}
/**
* It will scan all member attributes and behavior based on configuration on
* the class. It will also scan all inherited attributes and behavior.
* @param resourceConfig The config representing the domain class
* @param resourceClass The domain class
*/
protected void scanMembers(final EximResourceConfigImpl resourceConfig,
final Class resourceClass) {
if (resourceConfig.isAccessByPropertyEnabled()) {
scanMethods(resourceConfig, resourceClass);
}
else {
scanFields(resourceConfig, resourceClass);
}
}
/**
* Scans getter methods for discovering associations of the domain and thier
* respective configurations. It will only scan public getter methods.
* @param resourceConfig The config of the domain resource
* @param resourceClass The domain class
*/
protected void scanMethods(final EximResourceConfigImpl resourceConfig,
final Class resourceClass) {
Method[] methods = resourceClass.getMethods();
if (methods == null || methods.length <= 0) {
return;
}
for (Method method : methods) {
String methodName = method.getName();
//Only scan getter methods as of bean spec, that getter methods with
//no paratmeters and non-void return types and non-static
if (((methodName.startsWith(GETTER_PREFIX) && methodName.length() >
GETTER_PREFIX.length()) || (methodName.startsWith(IS_PREFIX) &&
methodName.length() > IS_PREFIX.length()) || (methodName.
startsWith(HAS_PREFIX) && methodName.length() > HAS_PREFIX.
length())) && method.getReturnType() != null && !method.
getReturnType().equals(Void.class) &&
(method.getParameterTypes() == null ||
method.getParameterTypes().length <= 0) &&
(method.getModifiers() & Modifier.STATIC) <= 0) {
scanGetterMethod(resourceConfig, method);
}
}
}
/**
* Scans a getter method for annotations which is used to cofigure the
* nature of the export
* @param resourceConfig The config to populate with configurations
* @param method The getter method to scan
* @throws java.lang.IllegalArgumentException If its not a getter method
* with non-void return type and
* with a non-zero length bean
* name length and with no
* parameter
*/
protected void scanGetterMethod(final EximResourceConfigImpl resourceConfig,
final Method method)
throws IllegalArgumentException {
String methodName = method.getName();
//Ignore the getClass bean
if (method.getName().equals("getClass") || method.getName().equals(
"hashCode")) {
return;
}
if (!(((methodName.startsWith(GETTER_PREFIX) && methodName.length() >
GETTER_PREFIX.length()) || (methodName.startsWith(IS_PREFIX) &&
methodName.length() > IS_PREFIX.length()) || (methodName.startsWith(
HAS_PREFIX) && methodName.length() > HAS_PREFIX.length())) &&
method.getReturnType() != null && !method.getReturnType().equals(
Void.class) && (method.getParameterTypes() == null || method.
getParameterTypes().length <= 0) && (method.getModifiers() &
Modifier.STATIC) <= 0)) {
throw new IllegalArgumentException();
}
String propertyName = getPropertyNameFromMethodName(methodName);
Class returnType = method.getReturnType();
scanAnnotatedElement(method, methodName, propertyName, returnType,
resourceConfig);
}
/**
* Scans fields for discovering associations of the domain and thier
* respective configurations
* @param resourceConfig The config of the domain resource
* @param resourceClass The domain class
*/
protected void scanFields(EximResourceConfigImpl resourceConfig,
Class resourceClass) {
Field[] fields = resourceClass.getDeclaredFields();
for (Field field : fields) {
scanField(resourceConfig, field);
}
Class parentClass = resourceClass.getSuperclass();
if (!parentClass.equals(Object.class)) {
scanFields(resourceConfig, parentClass);
}
}
/**
* A field is scanned for gathering configurations for export-import.
* @param resourceConfig Configuraton for the field association
* @param field The field to scan
*/
protected void scanField(final EximResourceConfigImpl resourceConfig,
final Field field) {
if ((field.getModifiers() & Modifier.STATIC) > 0) {
return;
}
String propertyName = field.getName();
Class propertyType = field.getType();
scanAnnotatedElement(field, propertyName, propertyName, propertyType,
resourceConfig);
}
/**
* Scan an ennotated element to extract configuration information
* @param element Element to scan
* @param propertyName The name of the property scanning
* @param accessorName The name of the property accessor
* @param propertyType The type of the property
* @param resourceConfig The configuration for the domain class
* @throws java.lang.IllegalArgumentException If any argument is null
*/
protected void scanAnnotatedElement(final AnnotatedElement element,
final String accessorName,
final String propertyName,
final Class propertyType,
final EximResourceConfigImpl resourceConfig)
throws IllegalArgumentException {
if (element == null || propertyName == null || propertyType == null ||
resourceConfig == null) {
throw new IllegalArgumentException();
}
AssociationConfigImpl configImpl =
new AssociationConfigImpl();
configImpl.setAccessorName(accessorName);
Name nameAnnotation = element.getAnnotation(Name.class);
if (nameAnnotation != null) {
configImpl.setName(nameAnnotation.value());
}
else {
configImpl.setName(propertyName);
}
configImpl.setAssociationType(AssociationConfig.AssociationType.
getAssociationType(propertyType));
Eager eager = element.getAnnotation(Eager.class);
configImpl.setStringProviderImplemented(StringValueProvider.class.
isAssignableFrom(propertyType));
configImpl.setEagerSet(eager != null);
Export annotation = element.getAnnotation(Export.class);
if (annotation != null) {
configImpl.setItToBeExportedAsUri(!annotation.asObject());
configImpl.setTransient(annotation.isTransient());
}
else {
configImpl.setItToBeExportedAsUri(false);
configImpl.setTransient(false);
}
resourceConfig.getAssociationConfigs().put(propertyName, configImpl);
Id id = element.getAnnotation(Id.class);
if (id != null) {
resourceConfig.setIdPropertyName(propertyName);
resourceConfig.setIdPrefix(id.path());
}
}
/**
* The visitor callback to be notified for dmain classes.
*/
protected static class ResourceVisitCallback
implements VisitCallback<AnnotationConfig> {
private Set<String> probableResources = new HashSet<String>();
/**
* When a requested annotation is been parsed the callback will be
* triggered and it will maintain a {@link Set} of scanned classes and
* mark them as probable resource
* @param config The annotation been parsed
*/
public void handle(AnnotationConfig config) {
probableResources.add(config.getClassName());
}
/**
* Return the probable resources scanned upto now
* @return Probable resources
*/
public Set<String> getProbableResources() {
return probableResources;
}
}
}