/*
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* For more information, please refer to <http://unlicense.org/>
*/
package com.sun.javafx.property;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.tuple.TDouble;
import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.value.ChangeListener;
import com.sun.javafx.binding.SelectBinding;
/**
* 屬性參考,提供{@link SelectBinding}界接物件屬性
* <ul>
* <li>改寫原本的{@code PropertyReference}以改善效能及支援非官方定義的屬性</li>
* <li>所有公開成員皆為原本{@code PropertyReference}所定義,不可更動其介面</li>
* <li>額外支援以{@code xxxProperty}命名的屬性方法及欄位</li>
* <li>額外支援常數名稱的方法及欄位,如<i>"nameProperty"</i>取得<i>nameProperty</i>的方法或欄位傳回的屬性本身(而非屬性的值)</li>
* </ul>
*
* @param <T> 屬性型態
*/
public final class PropertyReference<T>
{
/**
* 建立新的屬性參考
*
* @param clazz 屬性來源物件的類別
* @param name 屬性名稱
* @throws NullPointerException {@code clazz}或{@code name}為null
* @throws IllegalArgumentException {@code name}為空字串
*/
public PropertyReference(Class<?> clazz, String name)
{
if (name == null)
throw new NullPointerException("Name must be specified");
if (name.trim().length() == 0)
throw new IllegalArgumentException("Name must be specified");
if (clazz == null)
throw new NullPointerException("Class must be specified");
this.name = name;
this.clazz = clazz;
}
/**
* 屬性是否可寫入
*
* @return {@code true}表示屬性可寫入
*/
public boolean isWritable()
{
this.reflect();
return this.reflectedInfo.setValue != null;
}
/**
* 屬性是否可讀取
*
* @return {@code true}表示屬性可讀取
*/
public boolean isReadable()
{
this.reflect();
return this.reflectedInfo.getValue != null;
}
/**
* 是否提供屬性本身({@link ReadOnlyProperty})
*
* @return {@code true}表示提供屬性本身({@link ReadOnlyProperty})
*/
public boolean hasProperty()
{
this.reflect();
return this.reflectedInfo.getProperty != null;
}
/**
* 取得屬性名稱
*
* @return 屬性名稱
*/
public String getName()
{
return this.name;
}
/**
* 取得屬性所在的類別
*
* @return 屬性所在的類別
*/
public Class<?> getContainingClass()
{
return this.clazz;
}
/**
* 取得屬性的型態
*
* @return 屬性的型態
*/
public Class<?> getType()
{
this.reflect();
return this.reflectedInfo.type;
}
/**
* 設定屬性值
*
* @param bean 要設定屬性值的物件
* @param value 新的屬性值
* @throws IllegalStateException 屬性不可寫入({@link #isWritable})
*/
public void set(Object bean, T value)
{
if (!this.isWritable())
throw new IllegalStateException(
"Cannot write to readonly property " + this.name);
this.reflectedInfo.setValue.accept(bean, value);
}
/**
* 取得屬性值
*
* @param bean 要取得屬性值的物件
* @return 目前屬性值
* @throws IllegalStateException 屬性不可讀取({@link #isReadable})
*/
@SuppressWarnings("unchecked")
public T get(Object bean)
{
if (!this.isReadable())
throw new IllegalStateException(
"Cannot read from unreadable property " + this.name);
return (T) this.reflectedInfo.getValue.apply(bean);
}
/**
* 取得屬性本身
*
* @param bean 要取得屬性本身的物件
* @return 屬性本身
* @throws IllegalStateException 屬性不提供{@link ReadOnlyProperty}
*/
@SuppressWarnings("unchecked")
public ReadOnlyProperty<T> getProperty(Object bean)
{
if (!this.hasProperty())
throw new IllegalStateException("Cannot get property " + this.name);
return (ReadOnlyProperty<T>) this.reflectedInfo.getProperty.apply(bean);
}
@Override
public String toString()
{
return this.name;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (!(obj instanceof PropertyReference))
{
return false;
}
final PropertyReference<?> other = (PropertyReference<?>) obj;
if (this.name != other.name
&& (this.name == null || !this.name.equals(other.name)))
{
return false;
}
if (this.clazz != other.clazz
&& (this.clazz == null || !this.clazz.equals(other.clazz)))
{
return false;
}
return true;
}
@Override
public int hashCode()
{
int hash = 5;
hash = 97 * hash + (this.name != null ? this.name.hashCode() : 0);
hash = 97 * hash + (this.clazz != null ? this.clazz.hashCode() : 0);
return hash;
}
private void reflect()
{
if (this.reflectedInfo != null)
return;
this.reflectedInfo = cachedPropertyInfo.computeIfAbsent(
new TDouble<>(this.clazz, this.name),
k -> new ReflectedPropertyInfo(this.clazz, this.name));
}
private final String name;
private final Class<?> clazz;
private ReflectedPropertyInfo reflectedInfo;
//////////////////////////////////////////////////////////////////////////
private static class ReflectedPropertyInfo
{
public ReflectedPropertyInfo(Class<?> clazz, String name)
{
Objects.requireNonNull(clazz);
Objects.requireNonNull(name);
this.clazz = clazz;
this.name = name;
final String propName = name.length() == 1
? name.substring(0, 1).toUpperCase()
: Character.toUpperCase(name.charAt(0)) + name.substring(1);
Class<?> propType = null;
// getter
Method mGetter = tryGetMethod(clazz, "get" + propName);
if (mGetter == null)
mGetter = tryGetMethod(clazz, "is" + propName);
// setter
Method mSetter = null;
if (mGetter != null)
{
propType = mGetter.getReturnType();
mSetter = tryGetMethod(clazz, "set" + propName, propType);
}
else
{
String setterName = "set" + propName;
for (final Method m : clazz.getMethods())
{
final Class<?>[] parameters = m.getParameterTypes();
if (setterName.equals(m.getName())
&& parameters.length == 1
&& Modifier.isPublic(m.getModifiers()))
{
mSetter = m;
propType = parameters[0];
break;
}
}
}
//
this.valueGetter = mGetter;
this.valueSetter = mSetter;
this.propertyGetter = tryGetMethod(clazz, name + "Property");
this.propertyField = tryGetField(clazz, name + "Property");
this.constantGetter = tryGetMethod(clazz, name);
this.constantField = tryGetField(clazz, name);
this.type = this.resolvePropertyValueType(propType);
// determine getValue
if (this.valueGetter != null)
{
this.getValue = o -> invokeMethod(this.valueGetter, o);
}
else if (this.propertyGetter != null)
{
this.getValue = o ->
{
ReadOnlyProperty<?> p = (ReadOnlyProperty<?>) invokeMethod(this.propertyGetter, o);
return p == null ? null : p.getValue();
};
}
else if (this.propertyField != null)
{
this.getValue = o ->
{
ReadOnlyProperty<?> p = (ReadOnlyProperty<?>) invokeFieldGet(this.propertyField, o);
return p == null ? null : p.getValue();
};
}
else if (this.constantGetter != null)
{
this.getValue = o -> invokeMethod(this.constantGetter, o);
}
else if (this.constantField != null)
{
this.getValue = o -> invokeFieldGet(this.constantField, o);
}
else
{
this.getValue = null;
}
// determine setValue
if (this.valueSetter != null)
{
this.setValue = (o, v) -> invokeMethod(this.valueSetter, o, v);
}
else if (this.propertyGetter != null)
{
if (Property.class.isAssignableFrom(this.propertyGetter.getReturnType()))
this.setValue = (o, v) ->
{
@SuppressWarnings("unchecked") Property<Object> p = (Property<Object>) invokeMethod(this.propertyGetter, o);
if (p != null)
p.setValue(v);
};
else
this.setValue = null;
}
else if (this.propertyField != null)
{
if (Property.class.isAssignableFrom(this.propertyField.getType()))
this.setValue = (o, v) ->
{
@SuppressWarnings("unchecked") Property<Object> p = (Property<Object>) invokeFieldGet(this.propertyField, o);
if (p != null)
p.setValue(v);
};
else
this.setValue = null;
}
else
{
this.setValue = null;
}
// determine getProperty
if (this.propertyGetter != null)
{
this.getProperty = o ->
{
ReadOnlyProperty<?> p = (ReadOnlyProperty<?>) invokeMethod(this.propertyGetter, o);
return p;
};
}
else if (this.propertyField != null)
{
this.getProperty = o ->
{
ReadOnlyProperty<?> p = (ReadOnlyProperty<?>) invokeFieldGet(this.propertyField, o);
return p;
};
}
else if (this.constantGetter != null || this.constantField != null)
{
this.getProperty = o -> new ConstantObservable()
{
@Override
public Object getBean()
{
return o;
}
@Override
public String getName()
{
return name;
}
@Override
public Object getValue()
{
return ReflectedPropertyInfo.this.getValue.apply(o);
}
};
}
else
{
this.getProperty = null;
}
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (!(obj instanceof ReflectedPropertyInfo))
{
return false;
}
final ReflectedPropertyInfo other = (ReflectedPropertyInfo) obj;
if (this.name != other.name
&& (this.name == null || !this.name.equals(other.name)))
{
return false;
}
if (this.clazz != other.clazz
&& (this.clazz == null || !this.clazz.equals(other.clazz)))
{
return false;
}
return true;
}
@Override
public int hashCode()
{
int hash = 5;
hash = 97 * hash + (this.name != null ? this.name.hashCode() : 0);
hash = 97 * hash + (this.clazz != null ? this.clazz.hashCode() : 0);
return hash;
}
private Class<?> resolvePropertyValueType(Class<?> propertyValueType)
{
if (propertyValueType != null)
return propertyValueType;
if (this.propertyGetter != null)
{
Class<?> propertyType = (Class<?>) this.propertyGetter.getGenericReturnType();
Method valueGetter = tryGetMethod(propertyType, "getValue");
if (valueGetter != null)
return (Class<?>) valueGetter.getGenericReturnType();
}
if (this.propertyField != null)
{
Class<?> propertyType = (Class<?>) this.propertyField.getGenericType();
Method valueGetter = tryGetMethod(propertyType, "getValue");
if (valueGetter != null)
return (Class<?>) valueGetter.getGenericReturnType();
}
if (this.constantGetter != null)
return this.constantGetter.getReturnType();
if (this.constantField != null)
return this.constantField.getType();
return null;
}
public final Class<?> clazz;
public final String name;
public final Class<?> type;
public final Function<Object, Object> getValue;
public final BiConsumer<Object, Object> setValue;
public final Function<Object, ReadOnlyProperty<?>> getProperty;
private final Method valueGetter;
private final Method valueSetter;
private final Method propertyGetter;
private final Field propertyField;
private final Method constantGetter;
private final Field constantField;
private static Object invokeFieldGet(Field field, Object instance)
{
try
{
return field.get(instance);
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
}
private static Object invokeMethod(Method method, Object instance, Object... args)
{
try
{
return method.invoke(instance, args);
}
catch (IllegalAccessException | InvocationTargetException e)
{
throw new RuntimeException(e);
}
}
private static Field tryGetField(Class<?> clazz, String name)
{
try
{
return clazz.getField(name);
}
catch (NoSuchFieldException e)
{
return null;
}
}
private static Method tryGetMethod(Class<?> clazz, String name, Class<?>... parameterTypes)
{
try
{
return clazz.getMethod(name, parameterTypes);
}
catch (NoSuchMethodException e)
{
return null;
}
}
}
private static abstract class ConstantObservable implements ReadOnlyProperty<Object>
{
@Override
public void addListener(ChangeListener<? super Object> listener)
{
//
}
@Override
public void removeListener(ChangeListener<? super Object> listener)
{
//
}
@Override
public void addListener(InvalidationListener listener)
{
//
}
@Override
public void removeListener(InvalidationListener listener)
{
//
}
}
private static final ConcurrentHashMap<TDouble<Class<?>, String>, ReflectedPropertyInfo> cachedPropertyInfo =
new ConcurrentHashMap<>(10000);
}