| /* |
| * 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(); |
| } |
| } |