1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.property;
  27 
  28 import static java.security.AccessController.doPrivileged;
  29 
  30 import java.lang.reflect.Method;
  31 import java.lang.reflect.Modifier;
  32 import java.security.PrivilegedAction;
  33 
  34 import javafx.beans.property.ReadOnlyProperty;
  35 
  36 import sun.reflect.misc.MethodUtil;
  37 import sun.reflect.misc.ReflectUtil;
  38 
  39 /**
  40  * A handle to a specific property defined on some {@link Bean}.
  41  */
  42 public final class PropertyReference<T> {
  43     private String name;
  44     private Method getter;
  45     private Method setter;
  46     private Method propertyGetter;
  47     private Class<?> clazz;
  48     private Class<?> type;
  49     private boolean reflected = false;
  50 
  51     // uses reflection to implement the get / set methods
  52     /**
  53      * Creates a new {@code PropertyReference} for a property of a bean.
  54      *
  55      * @param clazz
  56      *            The class of the {@link Bean} that contains the property
  57      * @param name
  58      *            The name of the property
  59      * @throws NullPointerException
  60      *             if {@code clazz} or {@code name} are null
  61      * @throws IllegalArgumentException
  62      *             if {@code name} is an empty {@code String}
  63      */
  64     public PropertyReference(Class<?> clazz, String name) {
  65         if (name == null)
  66             throw new NullPointerException("Name must be specified");
  67         if (name.trim().length() == 0)
  68             throw new IllegalArgumentException("Name must be specified");
  69         if (clazz == null)
  70             throw new NullPointerException("Class must be specified");
  71         ReflectUtil.checkPackageAccess(clazz);
  72         this.name = name;
  73         this.clazz = clazz;
  74     }
  75 
  76     /**
  77      * Can be used to determine if a property can be set.
  78      *
  79      * @return {@code true}, if the property can be set, {@code false} otherwise
  80      */
  81     public boolean isWritable() {
  82         reflect();
  83         return setter != null;
  84     }
  85 
  86     /**
  87      * Can be used to determine if a property can be get.
  88      *
  89      * @return {@code true}, if the property can be get, {@code false} otherwise
  90      */
  91     public boolean isReadable() {
  92         reflect();
  93         return getter != null;
  94     }
  95 
  96     /**
  97      * Can be used to determine if a property provides an implementation of
  98      * {@link javafx.beans.value.ObservableValue}.
  99      *
 100      * @return
 101      */
 102     public boolean hasProperty() {
 103         reflect();
 104         return propertyGetter != null;
 105     }
 106 
 107     /**
 108      * Returns the name of the property.
 109      *
 110      * @return name of the propery
 111      */
 112     public String getName() {
 113         return name;
 114     }
 115 
 116     /**
 117      * Returns the class of the {@link Bean} that contains the property.
 118      *
 119      * @return the class of the {@link Bean}
 120      */
 121     public Class<?> getContainingClass() {
 122         return clazz;
 123     }
 124 
 125     /**
 126      * Returns the type of the property. This type is only evaluated correctly
 127      * if a getter or a setter was found.
 128      *
 129      * @return the type of the property
 130      */
 131     public Class<?> getType() {
 132         reflect();
 133         return type;
 134     }
 135 
 136     /**
 137      * Set the property to a new value.
 138      *
 139      * @param bean
 140      *            The {@link Bean} instance for which the property should be set
 141      * @param value
 142      *            The new value
 143      * @throws IllegalStateException
 144      *             if the property is not writable
 145      */
 146     public void set(Object bean, T value) {
 147         if (!isWritable())
 148             throw new IllegalStateException(
 149                     "Cannot write to readonly property " + name);
 150         assert setter != null;
 151         try {
 152             MethodUtil.invoke(setter, bean, new Object[] {value});
 153         } catch (Exception ex) {
 154             throw new RuntimeException(ex);
 155         }
 156     }
 157 
 158     /**
 159      * Get the value of the property.
 160      *
 161      * @param bean
 162      *            The {@code Bean} instance for which the property should be
 163      *            read
 164      * @return The value of the property
 165      * @throws IllegalStateException
 166      *             if the property is not readable
 167      */
 168     @SuppressWarnings("unchecked")
 169     public T get(Object bean) {
 170         if (!isReadable())
 171             throw new IllegalStateException(
 172                     "Cannot read from unreadable property " + name);
 173         assert getter != null;
 174         try {
 175             return (T)MethodUtil.invoke(getter, bean, (Object[])null);
 176         } catch (Exception ex) {
 177             throw new RuntimeException(ex);
 178         }
 179     }
 180 
 181     /**
 182      * Get the {@link javafx.beans.value.ObservableValue} implementation of the
 183      * property.
 184      *
 185      * @param bean
 186      *            The {@code Bean} instance for which the property should be
 187      *            read
 188      * @return The {@code ObservableValue} of the property
 189      * @throws IllegalStateException
 190      *             if the property does not provide an implementation
 191      */
 192     @SuppressWarnings("unchecked")
 193     public ReadOnlyProperty<T> getProperty(Object bean) {
 194         if (!hasProperty())
 195             throw new IllegalStateException("Cannot get property " + name);
 196         assert propertyGetter != null;
 197         try {
 198             return (ReadOnlyProperty<T>)MethodUtil.invoke(propertyGetter, bean, (Object[])null);
 199         } catch (Exception ex) {
 200             throw new RuntimeException(ex);
 201         }
 202     }
 203 
 204     /**
 205      * {@inheritDoc}
 206      */
 207     @Override
 208     public String toString() {
 209         return name;
 210     }
 211 
 212     private void reflect() {
 213         // If both the getter and setter are null then we have not reflected
 214         // on this property before
 215         if (!reflected) {
 216             reflected = true;
 217             try {
 218                 // Since we use it in several places, construct the
 219                 // first-letter-capitalized version of name
 220                 final String properName = name.length() == 1 ?
 221                         name.substring(0, 1).toUpperCase() :
 222                         Character.toUpperCase(name.charAt(0))
 223                         + name.substring(1);
 224 
 225                 // Now look for the getter. It will be named either
 226                 // "get" + name with the first letter of name
 227                 // capitalized, or it will be named "is" + name with
 228                 // the first letter of the name capitalized. However it
 229                 // is only named with "is" as a prefix if the type is
 230                 // boolean.
 231                 type = null;
 232                 // first we check for getXXX
 233                 String getterName = "get" + properName;
 234                 try {
 235                     final Method m = clazz.getMethod(getterName);
 236                     if (Modifier.isPublic(m.getModifiers())) {
 237                         getter = m;
 238                     }
 239                 } catch (NoSuchMethodException ex) {
 240                     // This is a legitimate error
 241                 }
 242 
 243                 // Then if it wasn't found we look for isXXX
 244                 if (getter == null) {
 245                     getterName = "is" + properName;
 246                     try {
 247                         final Method m = clazz.getMethod(getterName);
 248                         if (Modifier.isPublic(m.getModifiers())) {
 249                             getter = m;
 250                         }
 251                     } catch (NoSuchMethodException ex) {
 252                         // This is a legitimate error
 253                     }
 254                 }
 255 
 256                 // Now attempt to look for the setter. It is simply
 257                 // "set" + name with the first letter of name
 258                 // capitalized.
 259                 final String setterName = "set" + properName;
 260 
 261                 // If we found the getter, we can get the type
 262                 // and the setter easily.
 263                 if (getter != null) {
 264                     type = getter.getReturnType();
 265                     try {
 266                         final Method m = clazz.getMethod(setterName, type);
 267                         if (Modifier.isPublic(m.getModifiers())) {
 268                             setter = m;
 269                         }
 270                     } catch (NoSuchMethodException ex) {
 271                         // This is a legitimate error
 272                     }
 273                 } else { // no getter found
 274                     final Method[] methods = clazz.getMethods();
 275                     for (final Method m : methods) {
 276                         final Class<?>[] parameters = m.getParameterTypes();
 277                         if (setterName.equals(m.getName())
 278                                 && (parameters.length == 1)
 279                                 && Modifier.isPublic(m.getModifiers()))
 280                         {
 281                             setter = m;
 282                             type = parameters[0];
 283                             break;
 284                         }
 285                     }
 286                 }
 287 
 288                 // Now attempt to look for the property-getter.
 289                 final String propertyGetterName = name + "Property";
 290                 try {
 291                     final Method m = clazz.getMethod(propertyGetterName);
 292                     if (Modifier.isPublic(m.getModifiers())) {
 293                         propertyGetter = m;
 294                     } else
 295                         propertyGetter = null;
 296                 } catch (NoSuchMethodException ex) {
 297                     // This is a legitimate error
 298                 }
 299             } catch (RuntimeException e) {
 300                 System.err.println("Failed to introspect property " + name);
 301             }
 302         }
 303     }
 304 
 305     /**
 306      * {@inheritDoc}
 307      */
 308     @Override
 309     public boolean equals(Object obj) {
 310         if (this == obj) {
 311             return true;
 312         }
 313         if (!(obj instanceof PropertyReference)) {
 314             return false;
 315         }
 316         final PropertyReference<?> other = (PropertyReference<?>) obj;
 317         if (this.name != other.name
 318                 && (this.name == null || !this.name.equals(other.name))) {
 319             return false;
 320         }
 321         if (this.clazz != other.clazz
 322                 && (this.clazz == null || !this.clazz.equals(other.clazz))) {
 323             return false;
 324         }
 325         return true;
 326     }
 327 
 328     /**
 329      * {@inheritDoc}
 330      */
 331     @Override
 332     public int hashCode() {
 333         int hash = 5;
 334         hash = 97 * hash + (this.name != null ? this.name.hashCode() : 0);
 335         hash = 97 * hash + (this.clazz != null ? this.clazz.hashCode() : 0);
 336         return hash;
 337     }
 338 }