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