001: /*
002: * Copyright 2004 Brian S O'Neill
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.cojen.util;
018:
019: import java.lang.ref.SoftReference;
020: import java.lang.reflect.Method;
021: import java.util.ArrayList;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025: import java.math.BigInteger;
026: import org.cojen.classfile.ClassFile;
027: import org.cojen.classfile.CodeBuilder;
028: import org.cojen.classfile.Label;
029: import org.cojen.classfile.LocalVariable;
030: import org.cojen.classfile.MethodInfo;
031: import org.cojen.classfile.Modifiers;
032: import org.cojen.classfile.Opcode;
033: import org.cojen.classfile.TypeDesc;
034:
035: /**
036: * Provides a simple and efficient means of reading and writing bean
037: * properties. BeanPropertyAccessor auto-generates code, eliminating the
038: * need to invoke methods via reflection. Bean access methods are bound-to
039: * directly, using a special hash/switch design pattern.
040: *
041: * @author Brian S O'Neill
042: */
043: public abstract class BeanPropertyAccessor {
044: // Maps classes to softly referenced BeanPropertyAccessors.
045: private static Map cAccessors = new WeakIdentityMap();
046:
047: /**
048: * Returns a new or cached BeanPropertyAccessor for the given class.
049: */
050: public static BeanPropertyAccessor forClass(Class clazz) {
051: synchronized (cAccessors) {
052: BeanPropertyAccessor bpa;
053: SoftReference ref = (SoftReference) cAccessors.get(clazz);
054: if (ref != null) {
055: bpa = (BeanPropertyAccessor) ref.get();
056: if (bpa != null) {
057: return bpa;
058: }
059: }
060: bpa = generate(clazz);
061: cAccessors.put(clazz, new SoftReference(bpa));
062: return bpa;
063: }
064: }
065:
066: private static BeanPropertyAccessor generate(Class beanType) {
067: ClassInjector ci = ClassInjector.create(
068: BeanPropertyAccessor.class.getName(), beanType
069: .getClassLoader());
070: Class clazz = ci.defineClass(generateClassFile(ci
071: .getClassName(), beanType));
072:
073: try {
074: return (BeanPropertyAccessor) clazz.newInstance();
075: } catch (InstantiationException e) {
076: throw new InternalError(e.toString());
077: } catch (IllegalAccessException e) {
078: throw new InternalError(e.toString());
079: }
080: }
081:
082: private static ClassFile generateClassFile(String className,
083: Class beanType) {
084: BeanProperty[][] props = getBeanProperties(beanType);
085:
086: ClassFile cf = new ClassFile(className,
087: BeanPropertyAccessor.class);
088: cf.markSynthetic();
089: cf.setSourceFile(BeanPropertyAccessor.class.getName());
090: try {
091: cf.setTarget(System
092: .getProperty("java.specification.version"));
093: } catch (Exception e) {
094: }
095:
096: MethodInfo ctor = cf.addConstructor(Modifiers.PUBLIC, null);
097: ctor.markSynthetic();
098: CodeBuilder builder = new CodeBuilder(ctor);
099:
100: builder.loadThis();
101: builder.invokeSuperConstructor(null);
102: builder.returnVoid();
103:
104: generateMethod(cf, beanType, props[0], true);
105: generateMethod(cf, beanType, props[1], false);
106:
107: return cf;
108: }
109:
110: private static void generateMethod(ClassFile cf, Class beanType,
111: BeanProperty[] properties, boolean forRead) {
112: TypeDesc objectType = TypeDesc.OBJECT;
113: TypeDesc stringType = TypeDesc.STRING;
114: TypeDesc intType = TypeDesc.INT;
115: TypeDesc booleanType = TypeDesc.BOOLEAN;
116: TypeDesc exceptionType = TypeDesc
117: .forClass(NoSuchPropertyException.class);
118:
119: MethodInfo mi;
120: if (forRead) {
121: TypeDesc[] params = { objectType, stringType };
122: mi = cf.addMethod(Modifiers.PUBLIC, "getPropertyValue",
123: objectType, params);
124: } else {
125: TypeDesc[] params = new TypeDesc[] { objectType,
126: stringType, objectType };
127: mi = cf.addMethod(Modifiers.PUBLIC, "setPropertyValue",
128: null, params);
129: }
130:
131: mi.markSynthetic();
132: CodeBuilder builder = new CodeBuilder(mi);
133:
134: LocalVariable beanVar = builder.getParameter(0);
135: LocalVariable propertyVar = builder.getParameter(1);
136: LocalVariable valueVar;
137: if (forRead) {
138: valueVar = null;
139: } else {
140: valueVar = builder.getParameter(2);
141: }
142:
143: builder.loadLocal(beanVar);
144: builder.checkCast(TypeDesc.forClass(beanType));
145: builder.storeLocal(beanVar);
146:
147: if (properties.length > 0) {
148: int[] cases = new int[hashCapacity(properties.length)];
149: int caseCount = cases.length;
150: for (int i = 0; i < caseCount; i++) {
151: cases[i] = i;
152: }
153:
154: Label[] switchLabels = new Label[caseCount];
155: Label noMatch = builder.createLabel();
156: List[] caseMethods = caseMethods(caseCount, properties);
157:
158: for (int i = 0; i < caseCount; i++) {
159: List matches = caseMethods[i];
160: if (matches == null || matches.size() == 0) {
161: switchLabels[i] = noMatch;
162: } else {
163: switchLabels[i] = builder.createLabel();
164: }
165: }
166:
167: if (properties.length > 1) {
168: builder.loadLocal(propertyVar);
169: builder.invokeVirtual(String.class.getName(),
170: "hashCode", intType, null);
171: builder.loadConstant(0x7fffffff);
172: builder.math(Opcode.IAND);
173: builder.loadConstant(caseCount);
174: builder.math(Opcode.IREM);
175:
176: builder.switchBranch(cases, switchLabels, noMatch);
177: }
178:
179: // Params to invoke String.equals.
180: TypeDesc[] params = { objectType };
181:
182: for (int i = 0; i < caseCount; i++) {
183: List matches = caseMethods[i];
184: if (matches == null || matches.size() == 0) {
185: continue;
186: }
187:
188: switchLabels[i].setLocation();
189:
190: int matchCount = matches.size();
191: for (int j = 0; j < matchCount; j++) {
192: BeanProperty bp = (BeanProperty) matches.get(j);
193:
194: // Test against name to find exact match.
195:
196: builder.loadConstant(bp.getName());
197: builder.loadLocal(propertyVar);
198: builder.invokeVirtual(String.class.getName(),
199: "equals", booleanType, params);
200:
201: Label notEqual;
202:
203: if (j == matchCount - 1) {
204: notEqual = null;
205: builder.ifZeroComparisonBranch(noMatch, "==");
206: } else {
207: notEqual = builder.createLabel();
208: builder.ifZeroComparisonBranch(notEqual, "==");
209: }
210:
211: if (forRead) {
212: builder.loadLocal(beanVar);
213: builder.invoke(bp.getReadMethod());
214: TypeDesc type = TypeDesc.forClass(bp.getType());
215: builder.convert(type, type.toObjectType());
216: builder.returnValue(TypeDesc.OBJECT);
217: } else {
218: builder.loadLocal(beanVar);
219: builder.loadLocal(valueVar);
220: TypeDesc type = TypeDesc.forClass(bp.getType());
221: builder.checkCast(type.toObjectType());
222: builder.convert(type.toObjectType(), type);
223: builder.invoke(bp.getWriteMethod());
224: builder.returnVoid();
225: }
226:
227: if (notEqual != null) {
228: notEqual.setLocation();
229: }
230: }
231: }
232:
233: noMatch.setLocation();
234: }
235:
236: builder.newObject(exceptionType);
237: builder.dup();
238: builder.loadLocal(propertyVar);
239: builder.loadConstant(forRead);
240:
241: // Params to invoke NoSuchPropertyException.<init>.
242: TypeDesc[] params = { stringType, booleanType };
243:
244: builder.invokeConstructor(NoSuchPropertyException.class
245: .getName(), params);
246: builder.throwObject();
247: }
248:
249: /**
250: * Returns a prime number, at least twice as large as needed. This should
251: * minimize hash collisions. Since all the hash keys are known up front,
252: * the capacity could be tweaked until there are no collisions, but this
253: * technique is easier and deterministic.
254: */
255: private static int hashCapacity(int min) {
256: BigInteger capacity = BigInteger.valueOf(min * 2 + 1);
257: while (!capacity.isProbablePrime(100)) {
258: capacity = capacity.add(BigInteger.valueOf(2));
259: }
260: return capacity.intValue();
261: }
262:
263: /**
264: * Returns an array of Lists of BeanProperties. The first index
265: * matches a switch case, the second index provides a list of all the
266: * BeanProperties whose name hash matched on the case.
267: */
268: private static List[] caseMethods(int caseCount,
269: BeanProperty[] props) {
270: List[] cases = new List[caseCount];
271:
272: for (int i = 0; i < props.length; i++) {
273: BeanProperty prop = props[i];
274: int hashCode = prop.getName().hashCode();
275: int caseValue = (hashCode & 0x7fffffff) % caseCount;
276: List matches = cases[caseValue];
277: if (matches == null) {
278: matches = cases[caseValue] = new ArrayList();
279: }
280: matches.add(prop);
281: }
282:
283: return cases;
284: }
285:
286: /**
287: * Returns two arrays of BeanProperties. Array 0 contains read
288: * BeanProperties, array 1 contains the write BeanProperties.
289: */
290: private static BeanProperty[][] getBeanProperties(Class beanType) {
291: List readProperties = new ArrayList();
292: List writeProperties = new ArrayList();
293:
294: Map map = BeanIntrospector.getAllProperties(beanType);
295:
296: Iterator it = map.values().iterator();
297: while (it.hasNext()) {
298: BeanProperty bp = (BeanProperty) it.next();
299: if (bp.getReadMethod() != null) {
300: readProperties.add(bp);
301: }
302: if (bp.getWriteMethod() != null) {
303: writeProperties.add(bp);
304: }
305: }
306:
307: BeanProperty[][] props = new BeanProperty[2][];
308:
309: props[0] = new BeanProperty[readProperties.size()];
310: readProperties.toArray(props[0]);
311: props[1] = new BeanProperty[writeProperties.size()];
312: writeProperties.toArray(props[1]);
313:
314: return props;
315: }
316:
317: protected BeanPropertyAccessor() {
318: }
319:
320: // The actual public methods that will need to be defined.
321:
322: public abstract Object getPropertyValue(Object bean, String property)
323: throws NoSuchPropertyException;
324:
325: public abstract void setPropertyValue(Object bean, String property,
326: Object value) throws NoSuchPropertyException;
327:
328: // Auto-generated code sample:
329: /*
330: public Object getPropertyValue(Object bean, String property) {
331: Bean bean = (Bean)bean;
332:
333: switch ((property.hashCode() & 0x7fffffff) % 11) {
334: case 0:
335: if ("name".equals(property)) {
336: return bean.getName();
337: }
338: break;
339: case 1:
340: // No case
341: break;
342: case 2:
343: // Hash collision
344: if ("value".equals(property)) {
345: return bean.getValue();
346: } else if ("age".equals(property)) {
347: return new Integer(bean.getAge());
348: }
349: break;
350: case 3:
351: if ("start".equals(property)) {
352: return bean.getStart();
353: }
354: break;
355: case 4:
356: case 5:
357: case 6:
358: // No case
359: break;
360: case 7:
361: if ("end".equals(property)) {
362: return bean.isEnd() ? Boolean.TRUE : Boolean.FALSE;
363: }
364: break;
365: case 8:
366: case 9:
367: case 10:
368: // No case
369: break;
370: }
371:
372: throw new NoSuchPropertyException(property, true);
373: }
374:
375: public void setPropertyValue(Object bean, String property, Object value) {
376: Bean bean = (Bean)bean;
377:
378: switch ((property.hashCode() & 0x7fffffff) % 11) {
379: case 0:
380: if ("name".equals(property)) {
381: bean.setName(value);
382: }
383: break;
384: case 1:
385: // No case
386: break;
387: case 2:
388: // Hash collision
389: if ("value".equals(property)) {
390: bean.setValue(value);
391: } else if ("age".equals(property)) {
392: bean.setAge(((Integer)value).intValue());
393: }
394: break;
395: case 3:
396: if ("start".equals(property)) {
397: bean.setStart(value);
398: }
399: break;
400: case 4:
401: case 5:
402: case 6:
403: // No case
404: break;
405: case 7:
406: if ("end".equals(property)) {
407: bean.setEnd(((Boolean)value).booleanValue());
408: }
409: break;
410: case 8:
411: case 9:
412: case 10:
413: // No case
414: break;
415: }
416:
417: throw new NoSuchPropertyException(property, false);
418: }
419: */
420: }
|