/*
* Copyright 2005 JBoss Inc
*
* 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.drools.core.util.asm;
import java.beans.Introspector;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.drools.core.RuntimeDroolsException;
import org.kie.api.io.Resource;
import org.kie.internal.builder.KnowledgeBuilderResult;
import org.kie.internal.builder.ResultSeverity;
import org.mvel2.asm.AnnotationVisitor;
import org.mvel2.asm.Attribute;
import org.mvel2.asm.ClassReader;
import org.mvel2.asm.ClassVisitor;
import org.mvel2.asm.FieldVisitor;
import org.mvel2.asm.MethodVisitor;
import org.mvel2.asm.Opcodes;
import org.mvel2.asm.Type;
/**
* Visit a POJO user class, and extract the property getter methods that are public, in the
* order in which they are declared actually in the class itself (not using introspection).
*
* This may be enhanced in the future to allow annotations or perhaps external meta data
* configure the order of the indexes, as this may provide fine tuning options in special cases.
*/
public class ClassFieldInspector {
private final Map<String, Integer> fieldNames = new HashMap<String, Integer>();
private final Map<String, Class< ? >> fieldTypes = new HashMap<String, Class< ? >>();
private final Map<String, Field> fieldTypesField = new HashMap<String, Field>();
private final Map<String, Method> getterMethods = new HashMap<String, Method>();
private final Map<String, Method> setterMethods = new HashMap<String, Method>();
private final Set<String> nonGetters = new HashSet<String>();
private Class< ? > classUnderInspection = null;
private Map<String, Collection<KnowledgeBuilderResult>>
results = null;
/**
* @param classUnderInspection The class that the fields to be shadowed are extracted for.
* @throws IOException
*/
public ClassFieldInspector(final Class< ? > classUnderInspection) throws IOException {
this( classUnderInspection,
true );
}
public ClassFieldInspector(final Class< ? > classUnderInspection,
final boolean includeFinalMethods) throws IOException {
this.classUnderInspection = classUnderInspection;
final String name = getResourcePath( classUnderInspection );
final InputStream stream = classUnderInspection.getResourceAsStream( name );
if ( stream != null ) {
try {
processClassWithByteCode( classUnderInspection,
stream,
includeFinalMethods );
} finally {
stream.close();
}
} else {
processClassWithoutByteCode( classUnderInspection,
includeFinalMethods );
}
}
/** Walk up the inheritance hierarchy recursively, reading in fields */
private void processClassWithByteCode( final Class< ? > clazz,
final InputStream stream,
final boolean includeFinalMethods ) throws IOException {
final ClassReader reader = new ClassReader( stream );
final ClassFieldVisitor visitor = new ClassFieldVisitor( clazz,
includeFinalMethods,
this );
reader.accept( visitor,
0 );
if ( clazz.getSuperclass() != null ) {
final String name = getResourcePath( clazz.getSuperclass() );
final InputStream parentStream = clazz.getResourceAsStream( name );
if ( parentStream != null ) {
try {
processClassWithByteCode( clazz.getSuperclass(),
parentStream,
includeFinalMethods );
} finally {
parentStream.close();
}
} else {
processClassWithoutByteCode( clazz.getSuperclass(),
includeFinalMethods );
}
}
if ( clazz.isInterface() ) {
final Class< ? >[] interfaces = clazz.getInterfaces();
for ( int i = 0; i < interfaces.length; i++ ) {
final String name = getResourcePath( interfaces[i] );
final InputStream parentStream = clazz.getResourceAsStream( name );
if ( parentStream != null ) {
try {
processClassWithByteCode( interfaces[i],
parentStream,
includeFinalMethods );
} finally {
parentStream.close();
}
} else {
processClassWithoutByteCode( interfaces[i],
includeFinalMethods );
}
}
}
}
private void processClassWithoutByteCode( final Class< ? > clazz,
final boolean includeFinalMethods ) {
final List<Method> methods = Arrays.asList( clazz.getMethods() );
// different JVMs might return the methods in different order, so has to be sorted in order
// to be compatible with all JVMs
Collections.sort( methods, new Comparator<Method>() {
public int compare(Method m1,
Method m2) {
return m1.getName().compareTo( m2.getName() );
}
});
for ( int i = 0; i < methods.size(); i++ ) {
// modifiers mask
final int mask = includeFinalMethods ? Modifier.PUBLIC : Modifier.PUBLIC | Modifier.FINAL;
Method method = methods.get( i );
if ( ((method.getModifiers() & mask) == Opcodes.ACC_PUBLIC) && (method.getParameterTypes().length == 0) && (!method.getName().equals( "<init>" )) && (!method.getName().equals( "<clinit>" ))
&& (method.getReturnType() != void.class) ) {
// want public methods that start with 'get' or 'is' and have no args, and return a value
final int fieldIndex = this.fieldNames.size();
addToMapping( method,
fieldIndex );
} else if ( ((method.getModifiers() & mask) == Opcodes.ACC_PUBLIC) && (method.getParameterTypes().length == 1) && (method.getName().startsWith( "set" )) ) {
// want public methods that start with 'set' and have one arg
final int fieldIndex = this.fieldNames.size();
addToMapping( method,
fieldIndex );
}
}
final List<Field> flds = Arrays.asList( clazz.getFields() );
Collections.sort( flds, new Comparator<Field>() {
public int compare(Field f1, Field f2) {
return f1.getName().compareTo( f2.getName() );
}
});
for ( Field fld : flds ) {
if ( ! Modifier.isStatic( fld.getModifiers() ) && ! fieldNames.containsKey( fld.getName() ) ) {
final int fieldIndex = this.fieldNames.size();
this.fieldNames.put( fld.getName(), fieldIndex );
this.fieldTypes.put( fld.getName(), fld.getType() );
this.fieldTypesField.put( fld.getName(), fld );
}
}
}
/**
* Convert it to a form so we can load the bytes from the classpath.
*/
private String getResourcePath( final Class< ? > clazz ) {
return "/" + clazz.getCanonicalName() + ".class";
}
/**
* Return a mapping of the field "names" (ie bean property name convention)
* to the numerical index by which they can be accessed.
*/
public Map<String, Integer> getFieldNames() {
return this.fieldNames;
}
/**
* sotty:
* Checks whether a returned field is actually a getter or not
*
* @param name the field to test
* @return true id the name does not correspond to a getter field
*/
public boolean isNonGetter( String name ) {
return nonGetters.contains( name );
}
/**
* @return A mapping of field types (unboxed).
*/
public Map<String, Field> getFieldTypesField() {
return this.fieldTypesField;
}
/**
* @return A mapping of field types (unboxed).
*/
public Map<String, Class< ? >> getFieldTypes() {
return this.fieldTypes;
}
/**
* @return A mapping of methods for the getters.
*/
public Map<String, Method> getGetterMethods() {
return this.getterMethods;
}
/**
* @return A mapping of methods for the getters.
*/
public Map<String, Method> getSetterMethods() {
return this.setterMethods;
}
private void addToMapping( final Method method,
final int index ) {
final String name = method.getName();
int offset;
if ( name.startsWith( "is" ) ) {
offset = 2;
} else if ( name.startsWith( "get" ) || name.startsWith( "set" ) ) {
offset = 3;
} else {
offset = 0;
}
final String fieldName = calcFieldName( name,
offset );
if ( this.fieldNames.containsKey( fieldName ) ) {
//only want it once, the first one thats found
if ( offset != 0 && this.nonGetters.contains( fieldName ) ) {
//replace the non getter method with the getter one
Integer oldIndex = removeOldField( fieldName );
storeField( method,
oldIndex,
fieldName );
storeGetterSetter( method,
fieldName );
this.nonGetters.remove( fieldName );
} else if ( offset != 0 ) {
storeGetterSetter( method,
fieldName );
}
} else {
storeField( method,
new Integer( index ),
fieldName );
storeGetterSetter( method,
fieldName );
if ( offset == 0 ) {
// only if it is a non-standard getter method
this.nonGetters.add( fieldName );
}
}
}
private Integer removeOldField( final String fieldName ) {
Integer index = this.fieldNames.remove( fieldName );
this.fieldTypes.remove( fieldName );
this.getterMethods.remove( fieldName );
return index;
}
private void storeField( final Method method,
final Integer index,
final String fieldName ) {
this.fieldNames.put( fieldName,
index );
}
//class.getDeclaredField(String) doesn't walk the inheritance tree; this does
private Map<String, Field> getAllFields(Class<?> type) {
Map<String, Field> fields = new HashMap<String, Field>();
for (Class<?> c = type; c != null; c = c.getSuperclass()) {
for(Field f : c.getDeclaredFields()) {
fields.put(f.getName(), f);
}
}
return fields;
}
/**
* @param method
* @param fieldName
*/
private void storeGetterSetter( final Method method,
final String fieldName ) {
Field f = null;
f = getAllFields( classUnderInspection ).get( fieldName );
if ( method.getName().startsWith( "set" ) && method.getParameterTypes().length == 1 ) {
this.setterMethods.put( fieldName,
method );
if ( !fieldTypes.containsKey( fieldName ) ) {
this.fieldTypes.put( fieldName,
method.getParameterTypes()[0] );
}
if ( !fieldTypesField.containsKey( fieldName ) ) {
this.fieldTypesField.put( fieldName,
f );
}
} else if( ! void.class.isAssignableFrom( method.getReturnType() ) ) {
if ( getterMethods.containsKey( fieldName ) ) {
addResult( fieldName, new GetterOverloadWarning( classUnderInspection,
this.getterMethods.get( fieldName ).getName(), this.fieldTypes.get( fieldName ),
method.getName(), method.getReturnType() ) );
}
this.getterMethods.put( fieldName,
method );
this.fieldTypes.put( fieldName,
method.getReturnType() );
this.fieldTypesField.put( fieldName,
f );
}
}
private String calcFieldName( String name,
final int offset ) {
name = name.substring( offset );
return Introspector.decapitalize( name );
}
public Collection<KnowledgeBuilderResult> getInspectionResults( String fieldName ) {
return results != null && results.containsKey( fieldName ) ? results.get( fieldName ) : Collections.EMPTY_LIST;
}
private void addResult( String fieldName, KnowledgeBuilderResult result ) {
Map<String, Collection<KnowledgeBuilderResult>> results = getResults();
Collection<KnowledgeBuilderResult> fieldResults = results.get( fieldName );
if ( fieldResults == null ) {
fieldResults = new ArrayList<KnowledgeBuilderResult>( 3 );
results.put( fieldName, fieldResults );
}
fieldResults.add( result );
}
protected Map<String, Collection<KnowledgeBuilderResult>> getResults() {
if ( results == null ) {
results = new HashMap<String, Collection<KnowledgeBuilderResult>>( );
}
return results;
}
/**
* Using the ASM classfield extractor to pluck it out in the order they appear in the class file.
*/
static class ClassFieldVisitor
implements
ClassVisitor {
private Class< ? > clazz;
private ClassFieldInspector inspector;
private boolean includeFinalMethods;
ClassFieldVisitor(final Class< ? > cls,
final boolean includeFinalMethods,
final ClassFieldInspector inspector) {
this.clazz = cls;
this.includeFinalMethods = includeFinalMethods;
this.inspector = inspector;
}
public MethodVisitor visitMethod( final int access,
final String name,
final String desc,
final String signature,
final String[] exceptions ) {
//only want public methods
//and have no args, and return a value
final int mask = this.includeFinalMethods ? Opcodes.ACC_PUBLIC : Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL;
if ( (access & mask) == Opcodes.ACC_PUBLIC ) {
try {
if ( desc.startsWith( "()" ) && (!name.equals( "<init>" )) && (!name.equals( "<clinit>" )) ) {// && ( name.startsWith("get") || name.startsWith("is") ) ) {
final Method method = this.clazz.getMethod( name,
(Class[]) null );
if ( method.getReturnType() != void.class ) {
final int fieldIndex = this.inspector.fieldNames.size();
this.inspector.addToMapping( method,
fieldIndex );
}
} else if ( name.startsWith( "set" ) ) {
// I found no safe way of getting the method object from the descriptor, so doing the other way around
Method[] methods = this.clazz.getMethods();
for ( int i = 0; i < methods.length; i++ ) {
if ( name.equals( methods[i].getName() ) && desc.equals( Type.getMethodDescriptor( methods[i] ) ) ) {
final int fieldIndex = this.inspector.fieldNames.size();
this.inspector.addToMapping( methods[i],
fieldIndex );
break;
}
}
}
} catch ( final Exception e ) {
throw new RuntimeDroolsException( "Error getting field access method: " + name + ": " + e.getMessage(),
e );
}
}
return null;
}
public void visit( final int arg0,
final int arg1,
final String arg2,
final String arg3,
final String[] arg4,
final String arg5 ) {
}
public void visitInnerClass( final String arg0,
final String arg1,
final String arg2,
final int arg3 ) {
}
public void visitField( final int access,
final String arg1,
final String arg2,
final Object arg3,
final Attribute arg4 ) {
}
public void visitAttribute( final Attribute arg0 ) {
}
public void visitEnd() {
}
public void visit( final int arg0,
final int arg1,
final String arg2,
final String arg3,
final String arg4,
final String[] arg5 ) {
}
public void visitSource( final String arg0,
final String arg1 ) {
}
public void visitOuterClass( final String arg0,
final String arg1,
final String arg2 ) {
}
public AnnotationVisitor visitAnnotation( final String arg0,
final boolean arg1 ) {
return new ClassFieldAnnotationVisitor();
}
public FieldVisitor visitField( final int arg0,
final String arg1,
final String arg2,
final String arg3,
final Object arg4 ) {
return null;
}
}
/**
* This is required for POJOs that have annotations.
* It may also come in handy if we want to allow custom annotations for marking field numbers etc.
*/
static class ClassFieldAnnotationVisitor
implements
AnnotationVisitor {
public void visit( final String arg0,
final Object arg1 ) {
}
public void visitEnum( final String arg0,
final String arg1,
final String arg2 ) {
}
public AnnotationVisitor visitAnnotation( final String arg0,
final String arg1 ) {
return new ClassFieldAnnotationVisitor();
}
public AnnotationVisitor visitArray( final String arg0 ) {
return new ClassFieldAnnotationVisitor();
}
public void visitEnd() {
}
}
public class GetterOverloadWarning implements KnowledgeBuilderResult {
private Class klass;
private String oldName;
private Class oldType;
private String newName;
private Class newType;
public GetterOverloadWarning( Class klass, String oldName, Class oldType, String newName, Class newType ) {
this.klass = klass;
this.oldName = oldName;
this.oldType = oldType;
this.newName = newName;
this.newType = newType;
}
public ResultSeverity getSeverity() {
return ResultSeverity.WARNING;
}
public String getMessage() {
return " Getter overloading detected in class " + klass.getName() + " : " + oldName + " (" + oldType + ") vs " + newName + " (" + newType + ") ";
}
public int[] getLines() {
return new int[ 0 ];
}
public Resource getResource() {
return null;
}
}
}