/* * Copyright 2004-2010 Brian S O'Neill * * 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.cojen.util; import java.lang.ref.SoftReference; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.math.BigInteger; import java.security.AccessController; import java.security.PrivilegedAction; import org.cojen.classfile.ClassFile; import org.cojen.classfile.CodeBuilder; import org.cojen.classfile.Label; import org.cojen.classfile.LocalVariable; import org.cojen.classfile.MethodInfo; import org.cojen.classfile.Modifiers; import org.cojen.classfile.Opcode; import org.cojen.classfile.RuntimeClassFile; import org.cojen.classfile.TypeDesc; /** * Provides a simple and efficient means of reading and writing bean * properties. BeanPropertyAccessor auto-generates code, eliminating the * need to invoke methods via reflection. Bean access methods are bound-to * directly, using a special hash/switch design pattern. * * @author Brian S O'Neill * @see BeanPropertyMapFactory */ public abstract class BeanPropertyAccessor<B> { public static enum PropertySet { /** Set of all properties */ ALL, /** Set of all properties which declare only unchecked exceptions */ UNCHECKED_EXCEPTIONS, /** Set of all read-write properties */ READ_WRITE, /** Set of all read-write properties which declare only unchecked exceptions */ READ_WRITE_UNCHECKED_EXCEPTIONS, } private static final int READ_METHOD = 1; private static final int WRITE_METHOD = 2; private static final int TRY_READ_METHOD = 3; private static final int TRY_WRITE_METHOD = 4; private static final int HAS_READ_METHOD = 5; private static final int HAS_WRITE_METHOD = 6; private static final Map<PropertySet, Map<Class, SoftReference<BeanPropertyAccessor>>> cAccessors = new HashMap<PropertySet, Map<Class, SoftReference<BeanPropertyAccessor>>>(); /** * Returns a new or cached BeanPropertyAccessor for the given class. */ public static <B> BeanPropertyAccessor<B> forClass(Class<B> clazz) { return forClass(clazz, PropertySet.ALL); } public static <B> BeanPropertyAccessor<B> forClass(Class<B> clazz, PropertySet set) { synchronized (cAccessors) { Map<Class, SoftReference<BeanPropertyAccessor>> accessors = cAccessors.get(set); if (accessors == null) { accessors = new WeakIdentityMap<Class, SoftReference<BeanPropertyAccessor>>(); cAccessors.put(set, accessors); } BeanPropertyAccessor bpa; SoftReference<BeanPropertyAccessor> ref = accessors.get(clazz); if (ref != null) { bpa = ref.get(); if (bpa != null) { return bpa; } } bpa = generate(clazz, set); accessors.put(clazz, new SoftReference<BeanPropertyAccessor>(bpa)); return bpa; } } private static <B> BeanPropertyAccessor<B> generate(final Class<B> beanType, final PropertySet set) { return AccessController.doPrivileged(new PrivilegedAction<BeanPropertyAccessor<B>>() { public BeanPropertyAccessor<B> run() { Class clazz = generateClassFile(beanType, set).defineClass(); try { return (BeanPropertyAccessor<B>) clazz.newInstance(); } catch (InstantiationException e) { throw new InternalError(e.toString()); } catch (IllegalAccessException e) { throw new InternalError(e.toString()); } } }); } private static RuntimeClassFile generateClassFile(Class beanType, PropertySet set) { BeanProperty[][] props = getBeanProperties(beanType, set); RuntimeClassFile cf = new RuntimeClassFile (BeanPropertyAccessor.class.getName(), BeanPropertyAccessor.class.getName(), beanType.getClassLoader()); cf.markSynthetic(); cf.setSourceFile(BeanPropertyAccessor.class.getName()); try { cf.setTarget(System.getProperty("java.specification.version")); } catch (Exception e) { } MethodInfo ctor = cf.addConstructor(Modifiers.PUBLIC, null); ctor.markSynthetic(); CodeBuilder b = new CodeBuilder(ctor); b.loadThis(); b.invokeSuperConstructor(null); b.returnVoid(); generateAccessMethod(cf, beanType, props[0], READ_METHOD); generateAccessMethod(cf, beanType, props[0], TRY_READ_METHOD); generateAccessMethod(cf, beanType, props[0], HAS_READ_METHOD); generateAccessMethod(cf, beanType, props[1], WRITE_METHOD); generateAccessMethod(cf, beanType, props[1], TRY_WRITE_METHOD); generateAccessMethod(cf, beanType, props[1], HAS_WRITE_METHOD); generateSearchMethod(cf, beanType, props[0]); return cf; } private static void generateAccessMethod(ClassFile cf, Class beanType, BeanProperty[] properties, int methodType) { MethodInfo mi; switch (methodType) { case READ_METHOD: default: { TypeDesc[] params = {TypeDesc.OBJECT, TypeDesc.STRING}; mi = cf.addMethod (Modifiers.PUBLIC, "getPropertyValue", TypeDesc.OBJECT, params); break; } case WRITE_METHOD: { TypeDesc[] params = new TypeDesc[] { TypeDesc.OBJECT, TypeDesc.STRING, TypeDesc.OBJECT }; mi = cf.addMethod(Modifiers.PUBLIC, "setPropertyValue", null, params); break; } case TRY_READ_METHOD: { TypeDesc[] params = {TypeDesc.OBJECT, TypeDesc.STRING}; mi = cf.addMethod (Modifiers.PUBLIC, "tryGetPropertyValue", TypeDesc.OBJECT, params); break; } case TRY_WRITE_METHOD: { TypeDesc[] params = new TypeDesc[] { TypeDesc.OBJECT, TypeDesc.STRING, TypeDesc.OBJECT }; mi = cf.addMethod(Modifiers.PUBLIC, "trySetPropertyValue", TypeDesc.BOOLEAN, params); break; } case HAS_READ_METHOD: { TypeDesc[] params = {TypeDesc.STRING}; mi = cf.addMethod(Modifiers.PUBLIC, "hasReadableProperty", TypeDesc.BOOLEAN, params); break; } case HAS_WRITE_METHOD: { TypeDesc[] params = {TypeDesc.STRING}; mi = cf.addMethod(Modifiers.PUBLIC, "hasWritableProperty", TypeDesc.BOOLEAN, params); break; } } mi.markSynthetic(); CodeBuilder b = new CodeBuilder(mi); LocalVariable beanVar, propertyVar, valueVar; switch (methodType) { case READ_METHOD: case TRY_READ_METHOD: default: beanVar = b.getParameter(0); propertyVar = b.getParameter(1); valueVar = null; break; case WRITE_METHOD: case TRY_WRITE_METHOD: beanVar = b.getParameter(0); propertyVar = b.getParameter(1); valueVar = b.getParameter(2); break; case HAS_READ_METHOD: case HAS_WRITE_METHOD: beanVar = null; propertyVar = b.getParameter(0); valueVar = null; break; } if (beanVar != null) { b.loadLocal(beanVar); b.checkCast(TypeDesc.forClass(beanType)); b.storeLocal(beanVar); } if (properties.length > 0) { int[] cases = new int[hashCapacity(properties.length)]; int caseCount = cases.length; for (int i=0; i<caseCount; i++) { cases[i] = i; } Label[] switchLabels = new Label[caseCount]; Label noMatch = b.createLabel(); List[] caseMethods = caseMethods(caseCount, properties); for (int i=0; i<caseCount; i++) { List matches = caseMethods[i]; if (matches == null || matches.size() == 0) { switchLabels[i] = noMatch; } else { switchLabels[i] = b.createLabel(); } } if (properties.length > 1) { b.loadLocal(propertyVar); b.invokeVirtual(String.class.getName(), "hashCode", TypeDesc.INT, null); b.loadConstant(0x7fffffff); b.math(Opcode.IAND); b.loadConstant(caseCount); b.math(Opcode.IREM); b.switchBranch(cases, switchLabels, noMatch); } // Params to invoke String.equals. TypeDesc[] params = {TypeDesc.OBJECT}; for (int i=0; i<caseCount; i++) { List matches = caseMethods[i]; if (matches == null || matches.size() == 0) { continue; } switchLabels[i].setLocation(); int matchCount = matches.size(); for (int j=0; j<matchCount; j++) { BeanProperty bp = (BeanProperty)matches.get(j); // Test against name to find exact match. b.loadConstant(bp.getName()); b.loadLocal(propertyVar); b.invokeVirtual(String.class.getName(), "equals", TypeDesc.BOOLEAN, params); Label notEqual; if (j == matchCount - 1) { notEqual = null; b.ifZeroComparisonBranch(noMatch, "=="); } else { notEqual = b.createLabel(); b.ifZeroComparisonBranch(notEqual, "=="); } switch (methodType) { case READ_METHOD: case TRY_READ_METHOD: default: { b.loadLocal(beanVar); b.invoke(bp.getReadMethod()); TypeDesc type = TypeDesc.forClass(bp.getType()); b.convert(type, type.toObjectType()); b.returnValue(TypeDesc.OBJECT); break; } case WRITE_METHOD: case TRY_WRITE_METHOD: { b.loadLocal(beanVar); b.loadLocal(valueVar); TypeDesc type = TypeDesc.forClass(bp.getType()); b.checkCast(type.toObjectType()); b.convert(type.toObjectType(), type); b.invoke(bp.getWriteMethod()); if (methodType == WRITE_METHOD) { b.returnVoid(); } else { b.loadConstant(true); b.returnValue(TypeDesc.BOOLEAN); } break; } case HAS_READ_METHOD: case HAS_WRITE_METHOD: { b.loadConstant(true); b.returnValue(TypeDesc.BOOLEAN); break; } } if (notEqual != null) { notEqual.setLocation(); } } } noMatch.setLocation(); } if (methodType == HAS_READ_METHOD || methodType == HAS_WRITE_METHOD || methodType == TRY_WRITE_METHOD) { b.loadConstant(false); b.returnValue(TypeDesc.BOOLEAN); } else if (methodType == TRY_READ_METHOD) { b.loadNull(); b.returnValue(TypeDesc.OBJECT); } else { b.newObject(TypeDesc.forClass(NoSuchPropertyException.class)); b.dup(); b.loadLocal(propertyVar); b.loadConstant(methodType == READ_METHOD); // Params to invoke NoSuchPropertyException.<init>. TypeDesc[] params = {TypeDesc.STRING, TypeDesc.BOOLEAN}; b.invokeConstructor(NoSuchPropertyException.class.getName(), params); b.throwObject(); } } /** * Returns a prime number, at least twice as large as needed. This should * minimize hash collisions. Since all the hash keys are known up front, * the capacity could be tweaked until there are no collisions, but this * technique is easier and deterministic. */ private static int hashCapacity(int min) { BigInteger capacity = BigInteger.valueOf(min * 2 + 1); while (!capacity.isProbablePrime(10)) { capacity = capacity.add(BigInteger.valueOf(2)); } return capacity.intValue(); } /** * Returns an array of Lists of BeanProperties. The first index * matches a switch case, the second index provides a list of all the * BeanProperties whose name hash matched on the case. */ private static List[] caseMethods(int caseCount, BeanProperty[] props) { List[] cases = new List[caseCount]; for (int i=0; i<props.length; i++) { BeanProperty prop = props[i]; int hashCode = prop.getName().hashCode(); int caseValue = (hashCode & 0x7fffffff) % caseCount; List matches = cases[caseValue]; if (matches == null) { matches = cases[caseValue] = new ArrayList(); } matches.add(prop); } return cases; } private static void generateSearchMethod(ClassFile cf, Class beanType, BeanProperty[] properties) { MethodInfo mi; { TypeDesc[] params = {TypeDesc.OBJECT, TypeDesc.OBJECT}; mi = cf.addMethod(Modifiers.PUBLIC, "hasPropertyValue", TypeDesc.BOOLEAN, params); } mi.markSynthetic(); CodeBuilder b = new CodeBuilder(mi); LocalVariable beanVar = b.getParameter(0); b.loadLocal(beanVar); b.checkCast(TypeDesc.forClass(beanType)); b.storeLocal(beanVar); LocalVariable valueVar = b.getParameter(1); // If search value is null, only check properties which might be null. b.loadLocal(valueVar); Label searchNotNull = b.createLabel(); b.ifNullBranch(searchNotNull, false); for (BeanProperty bp : properties) { if (bp.getType().isPrimitive()) { continue; } b.loadLocal(beanVar); b.invoke(bp.getReadMethod()); Label noMatch = b.createLabel(); b.ifNullBranch(noMatch, false); b.loadConstant(true); b.returnValue(TypeDesc.BOOLEAN); noMatch.setLocation(); } b.loadConstant(false); b.returnValue(TypeDesc.BOOLEAN); searchNotNull.setLocation(); // Handle search for non-null value. Search non-primitive properties // first, to avoid object conversion. // Params to invoke Object.equals. TypeDesc[] params = {TypeDesc.OBJECT}; for (int pass = 1; pass <= 2; pass++) { for (BeanProperty bp : properties) { boolean primitive = bp.getType().isPrimitive(); if (pass == 1 && primitive) { continue; } else if (pass == 2 && !primitive) { continue; } b.loadLocal(valueVar); b.loadLocal(beanVar); b.invoke(bp.getReadMethod()); b.convert(TypeDesc.forClass(bp.getType()), TypeDesc.OBJECT); b.invokeVirtual(Object.class.getName(), "equals", TypeDesc.BOOLEAN, params); Label noMatch = b.createLabel(); b.ifZeroComparisonBranch(noMatch, "=="); b.loadConstant(true); b.returnValue(TypeDesc.BOOLEAN); noMatch.setLocation(); } } b.loadConstant(false); b.returnValue(TypeDesc.BOOLEAN); } /** * Returns two arrays of BeanProperties. Array 0 contains read * BeanProperties, array 1 contains the write BeanProperties. */ private static BeanProperty[][] getBeanProperties(Class beanType, PropertySet set) { List readProperties = new ArrayList(); List writeProperties = new ArrayList(); Map map = BeanIntrospector.getAllProperties(beanType); Iterator it = map.values().iterator(); while (it.hasNext()) { BeanProperty bp = (BeanProperty)it.next(); if (set == PropertySet.READ_WRITE || set == PropertySet.READ_WRITE_UNCHECKED_EXCEPTIONS) { if (bp.getReadMethod() == null || bp.getWriteMethod() == null) { continue; } } boolean checkedAllowed = set != PropertySet.UNCHECKED_EXCEPTIONS && set != PropertySet.READ_WRITE_UNCHECKED_EXCEPTIONS; if (bp.getReadMethod() != null) { if (checkedAllowed || !throwsCheckedException(bp.getReadMethod())) { readProperties.add(bp); } } if (bp.getWriteMethod() != null) { if (checkedAllowed || !throwsCheckedException(bp.getWriteMethod())) { writeProperties.add(bp); } } } BeanProperty[][] props = new BeanProperty[2][]; props[0] = new BeanProperty[readProperties.size()]; readProperties.toArray(props[0]); props[1] = new BeanProperty[writeProperties.size()]; writeProperties.toArray(props[1]); return props; } static boolean throwsCheckedException(Method method) { Class<?>[] exceptionTypes = method.getExceptionTypes(); if (exceptionTypes == null) { return false; } for (Class<?> exceptionType : exceptionTypes) { if (RuntimeException.class.isAssignableFrom(exceptionType)) { continue; } if (Error.class.isAssignableFrom(exceptionType)) { continue; } return true; } return false; } protected BeanPropertyAccessor() { } // The actual public methods that will need to be defined. public abstract Object getPropertyValue(B bean, String property) throws NoSuchPropertyException; public abstract void setPropertyValue(B bean, String property, Object value) throws NoSuchPropertyException; /** * Returns true if readable bean property exists. * * @since 2.1 */ public abstract boolean hasReadableProperty(String property); /** * Returns true if writable bean property exists. * * @since 2.1 */ public abstract boolean hasWritableProperty(String property); /** * Returns true if at least one property is set to the given value. * * @since 2.1 */ public abstract boolean hasPropertyValue(B bean, Object value); /** * Returns property value or null if it does not exist. * * @since 2.1 */ public abstract Object tryGetPropertyValue(B bean, String property); /** * Tries to set property value, if it exists. * * @return false if property doesn't exist * @since 2.1 */ public abstract boolean trySetPropertyValue(B bean, String property, Object value); // Auto-generated code sample: /* public Object getPropertyValue(Object bean, String property) { Bean bean = (Bean)bean; switch ((property.hashCode() & 0x7fffffff) % 11) { case 0: if ("name".equals(property)) { return bean.getName(); } break; case 1: // No case break; case 2: // Hash collision if ("value".equals(property)) { return bean.getValue(); } else if ("age".equals(property)) { return new Integer(bean.getAge()); } break; case 3: if ("start".equals(property)) { return bean.getStart(); } break; case 4: case 5: case 6: // No case break; case 7: if ("end".equals(property)) { return bean.isEnd() ? Boolean.TRUE : Boolean.FALSE; } break; case 8: case 9: case 10: // No case break; } throw new NoSuchPropertyException(property, true); } public void setPropertyValue(Object bean, String property, Object value) { Bean bean = (Bean)bean; switch ((property.hashCode() & 0x7fffffff) % 11) { case 0: if ("name".equals(property)) { bean.setName(value); } break; case 1: // No case break; case 2: // Hash collision if ("value".equals(property)) { bean.setValue(value); } else if ("age".equals(property)) { bean.setAge(((Integer)value).intValue()); } break; case 3: if ("start".equals(property)) { bean.setStart(value); } break; case 4: case 5: case 6: // No case break; case 7: if ("end".equals(property)) { bean.setEnd(((Boolean)value).booleanValue()); } break; case 8: case 9: case 10: // No case break; } throw new NoSuchPropertyException(property, false); } */ }