1 /*
2 * Copyright 1996-2008 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26 package java.beans;
27
28 import java.lang.ref.Reference;
29
30 import java.lang.reflect.Method;
31 import java.lang.reflect.Constructor;
32
33 /**
34 * A PropertyDescriptor describes one property that a Java Bean
35 * exports via a pair of accessor methods.
36 */
37 public class PropertyDescriptor extends FeatureDescriptor {
38
39 private Reference<Class> propertyTypeRef;
40 private Reference<Method> readMethodRef;
41 private Reference<Method> writeMethodRef;
42 private Reference<Class> propertyEditorClassRef;
43
44 private boolean bound;
45 private boolean constrained;
46
47 // The base name of the method name which will be prefixed with the
48 // read and write method. If name == "foo" then the baseName is "Foo"
49 private String baseName;
50
51 private String writeMethodName;
52 private String readMethodName;
53
54 /**
55 * Constructs a PropertyDescriptor for a property that follows
56 * the standard Java convention by having getFoo and setFoo
57 * accessor methods. Thus if the argument name is "fred", it will
58 * assume that the writer method is "setFred" and the reader method
59 * is "getFred" (or "isFred" for a boolean property). Note that the
60 * property name should start with a lower case character, which will
61 * be capitalized in the method names.
62 *
63 * @param propertyName The programmatic name of the property.
64 * @param beanClass The Class object for the target bean. For
65 * example sun.beans.OurButton.class.
66 * @exception IntrospectionException if an exception occurs during
67 * introspection.
68 */
69 public PropertyDescriptor(String propertyName, Class<?> beanClass)
70 throws IntrospectionException {
71 this(propertyName, beanClass,
72 Introspector.IS_PREFIX + NameGenerator.capitalize(propertyName),
73 Introspector.SET_PREFIX + NameGenerator.capitalize(propertyName));
74 }
75
76 /**
77 * This constructor takes the name of a simple property, and method
78 * names for reading and writing the property.
79 *
80 * @param propertyName The programmatic name of the property.
81 * @param beanClass The Class object for the target bean. For
82 * example sun.beans.OurButton.class.
83 * @param readMethodName The name of the method used for reading the property
84 * value. May be null if the property is write-only.
85 * @param writeMethodName The name of the method used for writing the property
86 * value. May be null if the property is read-only.
87 * @exception IntrospectionException if an exception occurs during
88 * introspection.
89 */
90 public PropertyDescriptor(String propertyName, Class<?> beanClass,
91 String readMethodName, String writeMethodName)
92 throws IntrospectionException {
93 if (beanClass == null) {
94 throw new IntrospectionException("Target Bean class is null");
95 }
96 if (propertyName == null || propertyName.length() == 0) {
97 throw new IntrospectionException("bad property name");
98 }
99 if ("".equals(readMethodName) || "".equals(writeMethodName)) {
100 throw new IntrospectionException("read or write method name should not be the empty string");
101 }
102 setName(propertyName);
103 setClass0(beanClass);
104
105 this.readMethodName = readMethodName;
106 if (readMethodName != null && getReadMethod() == null) {
107 throw new IntrospectionException("Method not found: " + readMethodName);
108 }
109 this.writeMethodName = writeMethodName;
110 if (writeMethodName != null && getWriteMethod() == null) {
111 throw new IntrospectionException("Method not found: " + writeMethodName);
112 }
113 // If this class or one of its base classes allow PropertyChangeListener,
114 // then we assume that any properties we discover are "bound".
115 // See Introspector.getTargetPropertyInfo() method.
116 String name = "addPropertyChangeListener";
117 Class[] args = {PropertyChangeListener.class};
118 this.bound = (null != Introspector.findMethod(beanClass, name, args.length, args));
119 }
120
121 /**
122 * This constructor takes the name of a simple property, and Method
123 * objects for reading and writing the property.
124 *
125 * @param propertyName The programmatic name of the property.
126 * @param readMethod The method used for reading the property value.
127 * May be null if the property is write-only.
128 * @param writeMethod The method used for writing the property value.
129 * May be null if the property is read-only.
130 * @exception IntrospectionException if an exception occurs during
131 * introspection.
132 */
133 public PropertyDescriptor(String propertyName, Method readMethod, Method writeMethod)
134 throws IntrospectionException {
135 if (propertyName == null || propertyName.length() == 0) {
136 throw new IntrospectionException("bad property name");
137 }
138 setName(propertyName);
139 setReadMethod(readMethod);
140 setWriteMethod(writeMethod);
141 }
142
143 /**
144 * Creates <code>PropertyDescriptor</code> for the specified bean
145 * with the specified name and methods to read/write the property value.
146 *
147 * @param bean the type of the target bean
148 * @param base the base name of the property (the rest of the method name)
149 * @param read the method used for reading the property value
150 * @param write the method used for writing the property value
151 * @exception IntrospectionException if an exception occurs during introspection
152 *
153 * @since 1.7
154 */
155 PropertyDescriptor(Class<?> bean, String base, Method read, Method write) throws IntrospectionException {
156 if (bean == null) {
157 throw new IntrospectionException("Target Bean class is null");
158 }
159 setClass0(bean);
160 setName(Introspector.decapitalize(base));
161 setReadMethod(read);
162 setWriteMethod(write);
163 this.baseName = base;
164 }
165
166 /**
167 * Gets the Class object for the property.
168 *
169 * @return The Java type info for the property. Note that
170 * the "Class" object may describe a built-in Java type such as "int".
171 * The result may be "null" if this is an indexed property that
172 * does not support non-indexed access.
173 * <p>
174 * This is the type that will be returned by the ReadMethod.
175 */
176 public synchronized Class<?> getPropertyType() {
177 Class type = getPropertyType0();
178 if (type == null) {
179 try {
180 type = findPropertyType(getReadMethod(), getWriteMethod());
181 setPropertyType(type);
182 } catch (IntrospectionException ex) {
183 // Fall
184 }
185 }
186 return type;
187 }
188
189 private void setPropertyType(Class type) {
190 this.propertyTypeRef = getWeakReference(type);
191 }
192
193 private Class getPropertyType0() {
194 return (this.propertyTypeRef != null)
195 ? this.propertyTypeRef.get()
196 : null;
197 }
198
199 /**
200 * Gets the method that should be used to read the property value.
201 *
202 * @return The method that should be used to read the property value.
203 * May return null if the property can't be read.
204 */
205 public synchronized Method getReadMethod() {
206 Method readMethod = getReadMethod0();
207 if (readMethod == null) {
208 Class cls = getClass0();
209 if (cls == null || (readMethodName == null && readMethodRef == null)) {
210 // The read method was explicitly set to null.
211 return null;
212 }
213 if (readMethodName == null) {
214 Class type = getPropertyType0();
215 if (type == boolean.class || type == null) {
216 readMethodName = Introspector.IS_PREFIX + getBaseName();
217 } else {
218 readMethodName = Introspector.GET_PREFIX + getBaseName();
219 }
220 }
221
222 // Since there can be multiple write methods but only one getter
223 // method, find the getter method first so that you know what the
224 // property type is. For booleans, there can be "is" and "get"
225 // methods. If an "is" method exists, this is the official
226 // reader method so look for this one first.
227 readMethod = Introspector.findMethod(cls, readMethodName, 0);
228 if (readMethod == null) {
229 readMethodName = Introspector.GET_PREFIX + getBaseName();
230 readMethod = Introspector.findMethod(cls, readMethodName, 0);
231 }
232 try {
233 setReadMethod(readMethod);
234 } catch (IntrospectionException ex) {
235 // fall
236 }
237 }
238 return readMethod;
239 }
240
241 /**
242 * Sets the method that should be used to read the property value.
243 *
244 * @param readMethod The new read method.
245 */
246 public synchronized void setReadMethod(Method readMethod)
247 throws IntrospectionException {
248 if (readMethod == null) {
249 readMethodName = null;
250 readMethodRef = null;
251 return;
252 }
253 // The property type is determined by the read method.
254 setPropertyType(findPropertyType(readMethod, getWriteMethod0()));
255 setClass0(readMethod.getDeclaringClass());
256
257 readMethodName = readMethod.getName();
258 this.readMethodRef = getSoftReference(readMethod);
259 setTransient(readMethod.getAnnotation(Transient.class));
260 }
261
262 /**
263 * Gets the method that should be used to write the property value.
264 *
265 * @return The method that should be used to write the property value.
266 * May return null if the property can't be written.
267 */
268 public synchronized Method getWriteMethod() {
269 Method writeMethod = getWriteMethod0();
270 if (writeMethod == null) {
271 Class cls = getClass0();
272 if (cls == null || (writeMethodName == null && writeMethodRef == null)) {
273 // The write method was explicitly set to null.
274 return null;
275 }
276
277 // We need the type to fetch the correct method.
278 Class type = getPropertyType0();
279 if (type == null) {
280 try {
281 // Can't use getPropertyType since it will lead to recursive loop.
282 type = findPropertyType(getReadMethod(), null);
283 setPropertyType(type);
284 } catch (IntrospectionException ex) {
285 // Without the correct property type we can't be guaranteed
286 // to find the correct method.
287 return null;
288 }
289 }
290
291 if (writeMethodName == null) {
292 writeMethodName = Introspector.SET_PREFIX + getBaseName();
293 }
294
295 writeMethod = Introspector.findMethod(cls, writeMethodName, 1,
296 (type == null) ? null : new Class[] { type });
297 try {
298 setWriteMethod(writeMethod);
299 } catch (IntrospectionException ex) {
300 // fall through
301 }
302 }
303 return writeMethod;
304 }
305
306 /**
307 * Sets the method that should be used to write the property value.
308 *
309 * @param writeMethod The new write method.
310 */
311 public synchronized void setWriteMethod(Method writeMethod)
312 throws IntrospectionException {
313 if (writeMethod == null) {
314 writeMethodName = null;
315 writeMethodRef = null;
316 return;
317 }
318 // Set the property type - which validates the method
319 setPropertyType(findPropertyType(getReadMethod(), writeMethod));
320 setClass0(writeMethod.getDeclaringClass());
321
322 writeMethodName = writeMethod.getName();
323 this.writeMethodRef = getSoftReference(writeMethod);
324 setTransient(writeMethod.getAnnotation(Transient.class));
325 }
326
327 private Method getReadMethod0() {
328 return (this.readMethodRef != null)
329 ? this.readMethodRef.get()
330 : null;
331 }
332
333 private Method getWriteMethod0() {
334 return (this.writeMethodRef != null)
335 ? this.writeMethodRef.get()
336 : null;
337 }
338
339 /**
340 * Overridden to ensure that a super class doesn't take precedent
341 */
342 void setClass0(Class clz) {
343 if (getClass0() != null && clz.isAssignableFrom(getClass0())) {
344 // dont replace a subclass with a superclass
345 return;
346 }
347 super.setClass0(clz);
348 }
349
350 /**
351 * Updates to "bound" properties will cause a "PropertyChange" event to
352 * get fired when the property is changed.
353 *
354 * @return True if this is a bound property.
355 */
356 public boolean isBound() {
357 return bound;
358 }
359
360 /**
361 * Updates to "bound" properties will cause a "PropertyChange" event to
362 * get fired when the property is changed.
363 *
364 * @param bound True if this is a bound property.
365 */
366 public void setBound(boolean bound) {
367 this.bound = bound;
368 }
369
370 /**
371 * Attempted updates to "Constrained" properties will cause a "VetoableChange"
372 * event to get fired when the property is changed.
373 *
374 * @return True if this is a constrained property.
375 */
376 public boolean isConstrained() {
377 return constrained;
378 }
379
380 /**
381 * Attempted updates to "Constrained" properties will cause a "VetoableChange"
382 * event to get fired when the property is changed.
383 *
384 * @param constrained True if this is a constrained property.
385 */
386 public void setConstrained(boolean constrained) {
387 this.constrained = constrained;
388 }
389
390
391 /**
392 * Normally PropertyEditors will be found using the PropertyEditorManager.
393 * However if for some reason you want to associate a particular
394 * PropertyEditor with a given property, then you can do it with
395 * this method.
396 *
397 * @param propertyEditorClass The Class for the desired PropertyEditor.
398 */
399 public void setPropertyEditorClass(Class<?> propertyEditorClass) {
400 this.propertyEditorClassRef = getWeakReference((Class)propertyEditorClass);
401 }
402
403 /**
404 * Gets any explicit PropertyEditor Class that has been registered
405 * for this property.
406 *
407 * @return Any explicit PropertyEditor Class that has been registered
408 * for this property. Normally this will return "null",
409 * indicating that no special editor has been registered,
410 * so the PropertyEditorManager should be used to locate
411 * a suitable PropertyEditor.
412 */
413 public Class<?> getPropertyEditorClass() {
414 return (this.propertyEditorClassRef != null)
415 ? this.propertyEditorClassRef.get()
416 : null;
417 }
418
419 /**
420 * Constructs an instance of a property editor using the current
421 * property editor class.
422 * <p>
423 * If the property editor class has a public constructor that takes an
424 * Object argument then it will be invoked using the bean parameter
425 * as the argument. Otherwise, the default constructor will be invoked.
426 *
427 * @param bean the source object
428 * @return a property editor instance or null if a property editor has
429 * not been defined or cannot be created
430 * @since 1.5
431 */
432 public PropertyEditor createPropertyEditor(Object bean) {
433 Object editor = null;
434
435 Class cls = getPropertyEditorClass();
436 if (cls != null) {
437 Constructor ctor = null;
438 if (bean != null) {
439 try {
440 ctor = cls.getConstructor(new Class[] { Object.class });
441 } catch (Exception ex) {
442 // Fall through
443 }
444 }
445 try {
446 if (ctor == null) {
447 editor = cls.newInstance();
448 } else {
449 editor = ctor.newInstance(new Object[] { bean });
450 }
451 } catch (Exception ex) {
452 // A serious error has occured.
453 // Proably due to an invalid property editor.
454 throw new RuntimeException("PropertyEditor not instantiated",
455 ex);
456 }
457 }
458 return (PropertyEditor)editor;
459 }
460
461
462 /**
463 * Compares this <code>PropertyDescriptor</code> against the specified object.
464 * Returns true if the objects are the same. Two <code>PropertyDescriptor</code>s
465 * are the same if the read, write, property types, property editor and
466 * flags are equivalent.
467 *
468 * @since 1.4
469 */
470 public boolean equals(Object obj) {
471 if (this == obj) {
472 return true;
473 }
474 if (obj != null && obj instanceof PropertyDescriptor) {
475 PropertyDescriptor other = (PropertyDescriptor)obj;
476 Method otherReadMethod = other.getReadMethod();
477 Method otherWriteMethod = other.getWriteMethod();
478
479 if (!compareMethods(getReadMethod(), otherReadMethod)) {
480 return false;
481 }
482
483 if (!compareMethods(getWriteMethod(), otherWriteMethod)) {
484 return false;
485 }
486
487 if (getPropertyType() == other.getPropertyType() &&
488 getPropertyEditorClass() == other.getPropertyEditorClass() &&
489 bound == other.isBound() && constrained == other.isConstrained() &&
490 writeMethodName == other.writeMethodName &&
491 readMethodName == other.readMethodName) {
492 return true;
493 }
494 }
495 return false;
496 }
497
498 /**
499 * Package private helper method for Descriptor .equals methods.
500 *
501 * @param a first method to compare
502 * @param b second method to compare
503 * @return boolean to indicate that the methods are equivalent
504 */
505 boolean compareMethods(Method a, Method b) {
506 // Note: perhaps this should be a protected method in FeatureDescriptor
507 if ((a == null) != (b == null)) {
508 return false;
509 }
510
511 if (a != null && b != null) {
512 if (!a.equals(b)) {
513 return false;
514 }
515 }
516 return true;
517 }
518
519 /**
520 * Package-private constructor.
521 * Merge two property descriptors. Where they conflict, give the
522 * second argument (y) priority over the first argument (x).
523 *
524 * @param x The first (lower priority) PropertyDescriptor
525 * @param y The second (higher priority) PropertyDescriptor
526 */
527 PropertyDescriptor(PropertyDescriptor x, PropertyDescriptor y) {
528 super(x,y);
529
530 if (y.baseName != null) {
531 baseName = y.baseName;
532 } else {
533 baseName = x.baseName;
534 }
535
536 if (y.readMethodName != null) {
537 readMethodName = y.readMethodName;
538 } else {
539 readMethodName = x.readMethodName;
540 }
541
542 if (y.writeMethodName != null) {
543 writeMethodName = y.writeMethodName;
544 } else {
545 writeMethodName = x.writeMethodName;
546 }
547
548 if (y.propertyTypeRef != null) {
549 propertyTypeRef = y.propertyTypeRef;
550 } else {
551 propertyTypeRef = x.propertyTypeRef;
552 }
553
554 // Figure out the merged read method.
555 Method xr = x.getReadMethod();
556 Method yr = y.getReadMethod();
557
558 // Normally give priority to y's readMethod.
559 try {
560 if (yr != null && yr.getDeclaringClass() == getClass0()) {
561 setReadMethod(yr);
562 } else {
563 setReadMethod(xr);
564 }
565 } catch (IntrospectionException ex) {
566 // fall through
567 }
568
569 // However, if both x and y reference read methods in the same class,
570 // give priority to a boolean "is" method over a boolean "get" method.
571 if (xr != null && yr != null &&
572 xr.getDeclaringClass() == yr.getDeclaringClass() &&
573 getReturnType(getClass0(), xr) == boolean.class &&
574 getReturnType(getClass0(), yr) == boolean.class &&
575 xr.getName().indexOf(Introspector.IS_PREFIX) == 0 &&
576 yr.getName().indexOf(Introspector.GET_PREFIX) == 0) {
577 try {
578 setReadMethod(xr);
579 } catch (IntrospectionException ex) {
580 // fall through
581 }
582 }
583
584 Method xw = x.getWriteMethod();
585 Method yw = y.getWriteMethod();
586
587 try {
588 if (yw != null && yw.getDeclaringClass() == getClass0()) {
589 setWriteMethod(yw);
590 } else {
591 setWriteMethod(xw);
592 }
593 } catch (IntrospectionException ex) {
594 // Fall through
595 }
596
597 if (y.getPropertyEditorClass() != null) {
598 setPropertyEditorClass(y.getPropertyEditorClass());
599 } else {
600 setPropertyEditorClass(x.getPropertyEditorClass());
601 }
602
603
604 bound = x.bound | y.bound;
605 constrained = x.constrained | y.constrained;
606 }
607
608 /*
609 * Package-private dup constructor.
610 * This must isolate the new object from any changes to the old object.
611 */
612 PropertyDescriptor(PropertyDescriptor old) {
613 super(old);
614 propertyTypeRef = old.propertyTypeRef;
615 readMethodRef = old.readMethodRef;
616 writeMethodRef = old.writeMethodRef;
617 propertyEditorClassRef = old.propertyEditorClassRef;
618
619 writeMethodName = old.writeMethodName;
620 readMethodName = old.readMethodName;
621 baseName = old.baseName;
622
623 bound = old.bound;
624 constrained = old.constrained;
625 }
626
627 /**
628 * Returns the property type that corresponds to the read and write method.
629 * The type precedence is given to the readMethod.
630 *
631 * @return the type of the property descriptor or null if both
632 * read and write methods are null.
633 * @throws IntrospectionException if the read or write method is invalid
634 */
635 private Class findPropertyType(Method readMethod, Method writeMethod)
636 throws IntrospectionException {
637 Class propertyType = null;
638 try {
639 if (readMethod != null) {
640 Class[] params = getParameterTypes(getClass0(), readMethod);
641 if (params.length != 0) {
642 throw new IntrospectionException("bad read method arg count: "
643 + readMethod);
644 }
645 propertyType = getReturnType(getClass0(), readMethod);
646 if (propertyType == Void.TYPE) {
647 throw new IntrospectionException("read method " +
648 readMethod.getName() + " returns void");
649 }
650 }
651 if (writeMethod != null) {
652 Class params[] = getParameterTypes(getClass0(), writeMethod);
653 if (params.length != 1) {
654 throw new IntrospectionException("bad write method arg count: "
655 + writeMethod);
656 }
657 if (propertyType != null && propertyType != params[0]) {
658 throw new IntrospectionException("type mismatch between read and write methods");
659 }
660 propertyType = params[0];
661 }
662 } catch (IntrospectionException ex) {
663 throw ex;
664 }
665 return propertyType;
666 }
667
668
669 /**
670 * Returns a hash code value for the object.
671 * See {@link java.lang.Object#hashCode} for a complete description.
672 *
673 * @return a hash code value for this object.
674 * @since 1.5
675 */
676 public int hashCode() {
677 int result = 7;
678
679 result = 37 * result + ((getPropertyType() == null) ? 0 :
680 getPropertyType().hashCode());
681 result = 37 * result + ((getReadMethod() == null) ? 0 :
682 getReadMethod().hashCode());
683 result = 37 * result + ((getWriteMethod() == null) ? 0 :
684 getWriteMethod().hashCode());
685 result = 37 * result + ((getPropertyEditorClass() == null) ? 0 :
686 getPropertyEditorClass().hashCode());
687 result = 37 * result + ((writeMethodName == null) ? 0 :
688 writeMethodName.hashCode());
689 result = 37 * result + ((readMethodName == null) ? 0 :
690 readMethodName.hashCode());
691 result = 37 * result + getName().hashCode();
692 result = 37 * result + ((bound == false) ? 0 : 1);
693 result = 37 * result + ((constrained == false) ? 0 : 1);
694
695 return result;
696 }
697
698 // Calculate once since capitalize() is expensive.
699 String getBaseName() {
700 if (baseName == null) {
701 baseName = NameGenerator.capitalize(getName());
702 }
703 return baseName;
704 }
705
706 /*
707 public String toString() {
708 String message = "name=" + getName();
709 message += ", class=" + getClass0();
710 message += ", type=" + getPropertyType();
711
712 message += ", writeMethod=";
713 message += writeMethodName;
714
715 message += ", readMethod=";
716 message += readMethodName;
717
718 message += ", bound=" + bound;
719 message += ", constrained=" + constrained;
720
721 return message;
722 }
723 */
724 }