blob: 9ed2cae15ee632a66602c6d64221f954208bd7d8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.openide.awt;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.openide.awt.ContextAction.StatefulMonitor;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
/**
* Enabler, which will work against a certain property on a target object. It attaches to the object when it
* changes, and monitors its property's state using either {@link PropertyChangeListener} or {@link ChangeListener}.
* If the monitored property's state changes, it fires a change to alert the owner Action.
*
* @param <T> data type
*/
class PropertyMonitor<T> implements ContextAction.StatefulMonitor<T>, PropertyChangeListener, ChangeListener {
private static final Logger LOG = Logger.getLogger(PropertyMonitor.class.getName());
static final String KEY_CHECKED_VALUE = "Value"; // NOI18N
static final String KEY_LISTEN_INTERFACE = "ChangeListener"; // NOI18N
static final String KEY_INTERFACE_METHOD = "Method"; // NOI18N
static final String KEY_CUSTOM_CHECK = "ActionProperty"; // NOI18N
static final String KEY_NULL = "Null"; // NO18N
/**
* Reflection not initialized
*/
private static final int UNINITIALIZED = -1;
/**
* Listeners not supported
*/
private static final int NONE = 0;
/**
* Property change listener registered against specific property name
*/
private static final int PROPERTY_NAME = 1;
/**
* Listener registered using general add method
*/
private static final int PROPERTY_ALL = 2;
/**
* ChangeListener is used,
*/
private static final int CHANGE = 3;
/**
* Custom listener interace.
*/
private static final int CUSTOM = 4;
/**
* Type being monitored, {@link Action} treated specially.
*/
private final Class<T> type;
/**
* The property being monitored
*/
private final String property;
/**
* The value which makes the action selected.
*/
private final Object checkedValue;
private Class valType;
/**
* Reflective access to the property's value
*/
private Method refGetter;
/**
* Reflective access to add listener
*/
private Method refAddListener;
/**
* Reflective access to remove listener
*/
private Method refRemoveListener;
/**
* Detected listener type
*/
private int listenerType = UNINITIALIZED;
/**
* The Weak listener attached to the monitored data
*/
private EventListener weakListener;
/**
* The last data being monitored
*/
private Reference<T> attachedTo;
/**
* Change Listeners added to this monitor.
*/
private List<ChangeListener> listeners = null;
/**
* Listener interface to listen
*/
private Class listenerInterface;
/**
* Method name to intercept; null for all methods
*/
private final String methodName;
private final StatefulMonitor actionMonitor;
private final Function<Object, Object> valueFactory;
public PropertyMonitor(Class<T> type, String property) {
this(type, property, "", Collections.emptyMap());
}
public PropertyMonitor(Class<T> type, String property, String keyPrefix, Map data) {
this.type = type;
this.property = property;
Object cv = data.get(keyPrefix + KEY_CHECKED_VALUE);
if (cv == null) {
Object b= data.get(keyPrefix + KEY_NULL);
if (b instanceof Boolean) {
cv = ((Boolean)b).booleanValue() ? ActionState.NULL_VALUE : ActionState.NON_NULL_VALUE;
}
}
checkedValue = cv;
valueFactory = initValueAccess();
Object o = data.get(keyPrefix + KEY_LISTEN_INTERFACE);
String mn = null;
if (o instanceof String) {
listenerInterface = GeneralAction.readClass(o);
o = data.get(keyPrefix + KEY_INTERFACE_METHOD);
if (o instanceof String) {
mn = (String)o;
}
}
Object customCheck = data.get(keyPrefix + KEY_CUSTOM_CHECK);
if (customCheck != null) {
actionMonitor = new PropertyMonitor(Action.class, customCheck.toString());
} else {
if (property == null) {
throw new IllegalArgumentException("Delegate or guard property must be specified");
}
actionMonitor = null;
}
methodName = mn;
}
public Class<T> getType() {
return type;
}
private T data() {
synchronized (this) {
return attachedTo != null ? attachedTo.get() : null;
}
}
public void clear() {
Object o = data();
if (o != null) {
clearListeners(o);
}
if (actionMonitor != null) {
actionMonitor.clear();
}
synchronized (this) {
attachedTo = null;
}
}
public void addChangeListener(ChangeListener l) {
boolean start = false;
synchronized (this) {
if (listeners == null) {
listeners = new ArrayList<>();
start = true;
}
listeners.add(l);
}
if (start) {
T d = data();
LOG.log(Level.FINER, "{0}: attaching listener to {1}", new Object[] { this, d });
if (d != null) {
addListeners(d);
}
}
}
public void removeChangeListener(ChangeListener l) {
boolean stop = false;
synchronized (this) {
if (listeners == null) {
return;
}
listeners.remove(l);
stop = listeners.isEmpty();
if (stop) {
listeners = null;
}
}
if (stop) {
T d = data();
if (d != null) {
clearListeners(d);
}
}
}
private void clearListeners(Object data) {
if (weakListener == null || refRemoveListener == null) {
return;
}
LOG.log(Level.FINER, "{0}: adding listener to {1}", new Object[] { this, data });
try {
switch (listenerType) {
case PROPERTY_NAME:
refRemoveListener.invoke(data, property, weakListener);
break;
case CUSTOM:
((ProxyListener)Proxy.getInvocationHandler(weakListener)).unregister(data);
break;
case PROPERTY_ALL:
case CHANGE:
refRemoveListener.invoke(data, weakListener);
break;
case NONE:
break;
default:
throw new IllegalStateException();
}
} catch (ReflectiveOperationException | IllegalArgumentException ex) {
ex.printStackTrace();
}
weakListener = null;
if (actionMonitor != null) {
actionMonitor.removeChangeListener(this);
}
}
/**
* Initializes listener reflective access.
* @param data
*/
private void initListenerReflection() {
if (listenerType != UNINITIALIZED) {
return;
}
Method add = null;
try {
if (listenerInterface != null) {
add = type.getMethod("add" + listenerInterface.getSimpleName(), listenerInterface);
listenerType = CUSTOM;
} else {
try {
if (property != null) {
add = type.getMethod("addPropertyChangeListener", String.class, PropertyChangeListener.class);
listenerType = PROPERTY_NAME;
}
} catch (NoSuchMethodException ex) {
// expected, ignore
}
if (add == null) {
try {
add = type.getMethod("addPropertyChangeListener", PropertyChangeListener.class);
listenerType = PROPERTY_ALL;
} catch (NoSuchMethodException ex2) {
add = type.getMethod("addChangeListener", ChangeListener.class);
listenerType = CHANGE;
}
}
}
} catch (NoSuchMethodException | SecurityException ex3) {
listenerType = NONE;
return;
}
Method remove = null;
try {
switch (listenerType) {
case PROPERTY_NAME:
remove = type.getMethod("removePropertyChangeListener", String.class, PropertyChangeListener.class);
break;
case PROPERTY_ALL:
remove = type.getMethod("removePropertyChangeListener", PropertyChangeListener.class);
break;
case CHANGE:
remove = type.getMethod("removeChangeListener", ChangeListener.class);
break;
case CUSTOM:
remove = type.getMethod("remove" + listenerInterface.getSimpleName(), listenerInterface);
break;
}
} catch (NoSuchMethodException | SecurityException ex) {
listenerType = -1;
return;
}
refAddListener = add;
refRemoveListener = remove;
}
// method accessed by reflection
public boolean falseGetter(Object data) {
return false;
}
// method accessed by reflection
public boolean trueGetter(Object data) {
return true;
}
private void addListeners(Object data) {
if (weakListener != null || listenerType == NONE) {
return;
}
initListenerReflection();
synchronized (this) {
if (listeners == null) {
return;
}
}
PropertyChangeListener pcl;
ChangeListener chl;
LOG.log(Level.FINER, "{0}: adding listener to {1}", new Object[] { this, data });
try {
switch (listenerType) {
case PROPERTY_NAME:
weakListener = pcl = WeakListeners.propertyChange(this, property, data);
refAddListener.invoke(data, property, pcl);
break;
case PROPERTY_ALL:
weakListener = pcl = WeakListeners.propertyChange(this, data);
refAddListener.invoke(data, pcl);
break;
case CHANGE:
weakListener = chl = WeakListeners.change(this, data);
refAddListener.invoke(data, chl);
break;
case NONE:
return;
case CUSTOM: {
ProxyListener pl = new ProxyListener(data, methodName, refRemoveListener, this);
Object o = Proxy.newProxyInstance(listenerInterface.getClassLoader(), new Class[] { listenerInterface, EventListener.class }, pl);
pl.proxy = weakListener = (EventListener)o;
refAddListener.invoke(data, weakListener);
break;
}
default:
throw new IllegalStateException();
}
} catch (ReflectiveOperationException | IllegalArgumentException ex) {
listenerType = NONE;
}
if (actionMonitor != null) {
actionMonitor.addChangeListener(this);
}
}
private Function<Object, Object> initValueAccess() {
Method getter = null;
if (property != null) {
String capitalizedName = Character.toUpperCase(property.charAt(0)) + property.substring(1);
String isGetter = "is" + capitalizedName; // NOI18N
String getGetter = "get" + capitalizedName; // NOI18N
try {
try {
getter = type.getMethod(isGetter);
} catch (NoSuchMethodException ex) {
getter = type.getMethod(getGetter);
}
Class c = getter.getReturnType();
if (!(c != Boolean.TYPE || c != Boolean.class || c != String.class || !c.isEnum()) &&
!(checkedValue == ActionState.NULL_VALUE || checkedValue == ActionState.NON_NULL_VALUE)) {
getter = null;
}
valType = c;
this.refGetter = getter;
return (o) -> reflectiveGet(o);
} catch (SecurityException | NoSuchMethodException ex) {
}
}
if (type == Action.class) {
return (o) -> inspectAction((Action)o);
} else {
return (o) -> property == null;
}
}
private void update() {
ChangeListener[] ll;
synchronized (this) {
if (listeners == null) {
return;
}
ll = listeners.toArray(new ChangeListener[listeners.size()]);
}
ChangeEvent ev = new ChangeEvent(this);
for (ChangeListener l : ll) {
l.stateChanged(ev);
}
}
private void refreshListeners(T data) {
Object prevData = data();
if (prevData == data) {
return;
}
if (actionMonitor != null) {
actionMonitor.clear();
}
if (prevData != null) {
clearListeners(prevData);
}
if (data != null) {
addListeners(data);
}
attachedTo = new WeakReference<>(data);
}
private Object reflectiveGet(Object instance) {
try {
return refGetter.invoke(instance);
} catch (ReflectiveOperationException | IllegalArgumentException ex) {
return false;
}
}
public boolean enabled(List<? extends T> data, Supplier<Action> aFactory) {
T first = data.isEmpty() ? null : data.get(0);
if (data.isEmpty()) {
return false;
}
refreshListeners(first);
if (first == null) {
return false;
}
if (type == Action.class) {
return inspectAction((Action)first);
}
Object o = valueFactory.apply(first);
if (!interpretAsBoolean(o)) {
return false;
}
if (aFactory != null && actionMonitor != null) {
return actionMonitor.enabled(Collections.singletonList(aFactory.get()), null);
} else {
return true;
}
}
public boolean inspectAction(Action a) {
if (a == null) {
return false;
}
if ("enabled".equals(property)) { // NOI18N
return a.isEnabled();
}
return a.getValue(property) == Boolean.TRUE;
}
private boolean interpretAsBoolean(Object v) {
if (v == null) {
if (checkedValue == ActionState.NULL_VALUE) {
return true;
}
return false;
}
if (valType == null || valType == Boolean.TYPE || valType == Boolean.class) {
if (checkedValue == null) {
return Boolean.TRUE.equals(v);
} else {
return checkedValue.equals(v.toString());
}
}
if (checkedValue == null) {
// v is not null;
if (v instanceof Collection) {
return !((Collection) v).isEmpty();
} else if (v instanceof Map) {
return !((Map) v).isEmpty();
} else if (Number.class.isInstance(v)) {
return ((Number)v).intValue() > 0;
}
return false;
}
if (checkedValue == ActionState.NON_NULL_VALUE) {
return true;
}
if (!(checkedValue instanceof String)) {
return checkedValue.equals(v);
}
return checkedValue.equals(v.toString());
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName() != null && property != null && !property.equals(evt.getPropertyName())) {
return;
}
update();
}
@Override
public void stateChanged(ChangeEvent e) {
update();
}
public PropertyMonitor(PropertyMonitor other) {
this.type = other.type;
this.property = other.property;
this.checkedValue = other.checkedValue;
this.listenerType = other.listenerType;
this.refGetter = other.refGetter;
this.valueFactory = other.valueFactory;
this.valType = other.valType;
this.refAddListener = other.refAddListener;
this.refRemoveListener = other.refRemoveListener;
this.listenerInterface = other.listenerInterface;
this.methodName = other.methodName;
if (other.actionMonitor == null) {
this.actionMonitor = null;
} else {
this.actionMonitor = other.actionMonitor.createContextMonitor(Lookup.EMPTY);
}
}
@Override
public ContextAction.StatefulMonitor<T> createContextMonitor(Lookup context) {
return new PropertyMonitor<>(this);
}
private static final Method OBJECT_EQUALS = getObjectMethod("equals", Object.class); // NOI18N
private static final Method OBJECT_HASHCODE = getObjectMethod("hashCode"); // NOI18N
private static Method getObjectMethod(String name, Class... types) {
try {
return Object.class.getMethod(name, types);
} catch (ReflectiveOperationException | SecurityException ex) {
throw new IllegalStateException(ex);
}
}
private static class ProxyListener extends WeakReference<ChangeListener> implements EventListener, InvocationHandler, Runnable {
private final Reference theData;
private final String methodName;
private final Method removeMethod;
volatile EventListener proxy;
public ProxyListener(Object theData, String methodName, Method removeMethod, ChangeListener referent) {
super(referent, Utilities.activeReferenceQueue());
this.theData = new WeakReference<>(theData);
this.methodName = methodName;
this.removeMethod = removeMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
// a method from object => call it on your self
if (method == OBJECT_EQUALS) {
return equals(args[0]);
} else if (method == OBJECT_HASHCODE) {
return proxy.hashCode();
}
return method.invoke(this, args);
}
ChangeListener target = get();
Object data = theData.get();
if (data == null) {
return null;
}
if (target == null) {
return null;
}
if (methodName == null || method.getName().equals(methodName)) {
ChangeEvent ev = new ChangeEvent(data);
target.stateChanged(ev);
}
return null;
}
private void unregister(Object data) {
if (data == null) {
return;
}
if (removeMethod != null) {
try {
removeMethod.invoke(data, proxy);
} catch (ReflectiveOperationException | SecurityException ex) {
Exceptions.printStackTrace(ex);
}
}
theData.clear();
}
@Override
public void run() {
unregister(theData.get());
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("PropertyMonitor@").append(System.identityHashCode(this)).append("{")
.append("class = ").append(type.getName())
.append(", property = ").append(property)
.append(", valtype = ").append(valType == null ? "null" : valType.getName())
.append(", checkval = ").append(checkedValue)
.append("}");
return sb.toString();
}
}