1 /*
  2  * Copyright (c) 2010, 2019, 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.binding;
 27 
 28 import javafx.beans.InvalidationListener;
 29 import javafx.beans.Observable;
 30 import javafx.beans.WeakInvalidationListener;
 31 import javafx.beans.binding.Binding;
 32 import javafx.beans.binding.BooleanBinding;
 33 import javafx.beans.binding.DoubleBinding;
 34 import javafx.beans.binding.FloatBinding;
 35 import javafx.beans.binding.IntegerBinding;
 36 import javafx.beans.binding.LongBinding;
 37 import javafx.beans.binding.ObjectBinding;
 38 import javafx.beans.binding.StringBinding;
 39 import javafx.beans.value.ObservableBooleanValue;
 40 import javafx.beans.value.ObservableNumberValue;
 41 import javafx.beans.value.ObservableValue;
 42 import javafx.collections.FXCollections;
 43 import javafx.collections.ObservableList;
 44 
 45 import com.sun.javafx.property.JavaBeanAccessHelper;
 46 import com.sun.javafx.logging.PlatformLogger;
 47 import com.sun.javafx.logging.PlatformLogger.Level;
 48 import com.sun.javafx.property.PropertyReference;
 49 import java.util.Arrays;
 50 
 51 /**
 52  * A binding used to get a member, such as <code>a.b.c</code>. The value of the
 53  * binding will be "c", or null if c could not be reached (due to "b" not having
 54  * a "c" property, or "b" being null). "a" must be passed to the constructor of
 55  * the SelectBinding and may be any dependency. All subsequent links are simply
 56  * PropertyReferences.
 57  * <p>
 58  * With a SelectBinding, "a" must always exist. Usually "a" will refer to
 59  * "this", or some concrete object. "b"* will be some intermediate step in the
 60  * select binding.
 61  */
 62 public class SelectBinding {
 63 
 64     private SelectBinding() {}
 65 
 66     public static class AsObject<T> extends ObjectBinding<T> {
 67 
 68         private final SelectBindingHelper helper;
 69 
 70         public AsObject(ObservableValue<?> root, String... steps) {
 71             helper = new SelectBindingHelper(this, root, steps);
 72         }
 73 
 74         public AsObject(Object root, String... steps) {
 75             helper = new SelectBindingHelper(this, root, steps);
 76         }
 77 
 78         @Override
 79         public void dispose() {
 80             helper.unregisterListener();
 81         }
 82 
 83         @Override
 84         protected void onInvalidating() {
 85             helper.unregisterListener();
 86         }
 87 
 88         @SuppressWarnings("unchecked")
 89         @Override
 90         protected T computeValue() {
 91             final ObservableValue<?> observable = helper.getObservableValue();
 92             if (observable == null) {
 93                 return null;
 94             }
 95             try {
 96                 return (T)observable.getValue();
 97             } catch (ClassCastException ex) {
 98                 Logging.getLogger().warning("Value of select-binding has wrong type, returning null.", ex);
 99             }
100             return null;
101         }
102 
103 
104         @Override
105         public ObservableList<ObservableValue<?>> getDependencies() {
106             return helper.getDependencies();
107         }
108 
109     }
110 
111     public static class AsBoolean extends BooleanBinding {
112 
113         private static final boolean DEFAULT_VALUE = false;
114 
115         private final SelectBindingHelper helper;
116 
117         public AsBoolean(ObservableValue<?> root, String... steps) {
118             helper = new SelectBindingHelper(this, root, steps);
119         }
120 
121         public AsBoolean(Object root, String... steps) {
122             helper = new SelectBindingHelper(this, root, steps);
123         }
124 
125         @Override
126         public void dispose() {
127             helper.unregisterListener();
128         }
129 
130         @Override
131         protected void onInvalidating() {
132             helper.unregisterListener();
133         }
134 
135         @Override
136         protected boolean computeValue() {
137             final ObservableValue<?> observable = helper.getObservableValue();
138             if (observable == null) {
139                 return DEFAULT_VALUE;
140             }
141             if (observable instanceof ObservableBooleanValue) {
142                 return ((ObservableBooleanValue)observable).get();
143             }
144             try {
145                 return (Boolean)observable.getValue();
146             } catch (NullPointerException ex) {
147                 Logging.getLogger().fine("Value of select binding is null, returning default value", ex);
148             } catch (ClassCastException ex) {
149                 Logging.getLogger().warning("Value of select-binding has wrong type, returning default value.", ex);
150             }
151             return DEFAULT_VALUE;
152         }
153 
154         @Override
155         public ObservableList<ObservableValue<?>> getDependencies() {
156             return helper.getDependencies();
157         }
158 
159     }
160 
161     public static class AsDouble extends DoubleBinding {
162 
163         private static final double DEFAULT_VALUE = 0.0;
164 
165         private final SelectBindingHelper helper;
166 
167         public AsDouble(ObservableValue<?> root, String... steps) {
168             helper = new SelectBindingHelper(this, root, steps);
169         }
170 
171         public AsDouble(Object root, String... steps) {
172             helper = new SelectBindingHelper(this, root, steps);
173         }
174 
175         @Override
176         public void dispose() {
177             helper.unregisterListener();
178         }
179 
180         @Override
181         protected void onInvalidating() {
182             helper.unregisterListener();
183         }
184 
185         @Override
186         protected double computeValue() {
187             final ObservableValue<?> observable = helper.getObservableValue();
188             if (observable == null) {
189                 return DEFAULT_VALUE;
190             }
191             if (observable instanceof ObservableNumberValue) {
192                 return ((ObservableNumberValue)observable).doubleValue();
193             }
194             try {
195                 return ((Number)observable.getValue()).doubleValue();
196             } catch (NullPointerException ex) {
197                 Logging.getLogger().fine("Value of select binding is null, returning default value", ex);
198             } catch (ClassCastException ex) {
199                 Logging.getLogger().warning("Exception while evaluating select-binding", ex);
200             }
201             return DEFAULT_VALUE;
202         }
203 
204         @Override
205         public ObservableList<ObservableValue<?>> getDependencies() {
206             return helper.getDependencies();
207         }
208 
209     }
210 
211     public static class AsFloat extends FloatBinding {
212 
213         private static final float DEFAULT_VALUE = 0.0f;
214 
215         private final SelectBindingHelper helper;
216 
217         public AsFloat(ObservableValue<?> root, String... steps) {
218             helper = new SelectBindingHelper(this, root, steps);
219         }
220 
221         public AsFloat(Object root, String... steps) {
222             helper = new SelectBindingHelper(this, root, steps);
223         }
224 
225         @Override
226         public void dispose() {
227             helper.unregisterListener();
228         }
229 
230         @Override
231         protected void onInvalidating() {
232             helper.unregisterListener();
233         }
234 
235         @Override
236         protected float computeValue() {
237             final ObservableValue<?> observable = helper.getObservableValue();
238             if (observable == null) {
239                 return DEFAULT_VALUE;
240             }
241             if (observable instanceof ObservableNumberValue) {
242                 return ((ObservableNumberValue)observable).floatValue();
243             }
244             try {
245                 return ((Number)observable.getValue()).floatValue();
246             } catch (NullPointerException ex) {
247                 Logging.getLogger().fine("Value of select binding is null, returning default value", ex);
248             } catch (ClassCastException ex) {
249                 Logging.getLogger().warning("Exception while evaluating select-binding", ex);
250             }
251             return DEFAULT_VALUE;
252         }
253 
254         @Override
255         public ObservableList<ObservableValue<?>> getDependencies() {
256             return helper.getDependencies();
257         }
258 
259     }
260 
261     public static class AsInteger extends IntegerBinding {
262 
263         private static final int DEFAULT_VALUE = 0;
264 
265         private final SelectBindingHelper helper;
266 
267         public AsInteger(ObservableValue<?> root, String... steps) {
268             helper = new SelectBindingHelper(this, root, steps);
269         }
270 
271         public AsInteger(Object root, String... steps) {
272             helper = new SelectBindingHelper(this, root, steps);
273         }
274 
275         @Override
276         public void dispose() {
277             helper.unregisterListener();
278         }
279 
280         @Override
281         protected void onInvalidating() {
282             helper.unregisterListener();
283         }
284 
285         @Override
286         protected int computeValue() {
287             final ObservableValue<?> observable = helper.getObservableValue();
288             if (observable == null) {
289                 return DEFAULT_VALUE;
290             }
291             if (observable instanceof ObservableNumberValue) {
292                 return ((ObservableNumberValue)observable).intValue();
293             }
294             try {
295                 return ((Number)observable.getValue()).intValue();
296             } catch (NullPointerException ex) {
297                 Logging.getLogger().fine("Value of select binding is null, returning default value", ex);
298             } catch (ClassCastException ex) {
299                 Logging.getLogger().warning("Exception while evaluating select-binding", ex);
300             }
301             return DEFAULT_VALUE;
302         }
303 
304         @Override
305         public ObservableList<ObservableValue<?>> getDependencies() {
306             return helper.getDependencies();
307         }
308 
309     }
310 
311     public static class AsLong extends LongBinding {
312 
313         private static final long DEFAULT_VALUE = 0L;
314 
315         private final SelectBindingHelper helper;
316 
317         public AsLong(ObservableValue<?> root, String... steps) {
318             helper = new SelectBindingHelper(this, root, steps);
319         }
320 
321         public AsLong(Object root, String... steps) {
322             helper = new SelectBindingHelper(this, root, steps);
323         }
324 
325         @Override
326         public void dispose() {
327             helper.unregisterListener();
328         }
329 
330         @Override
331         protected void onInvalidating() {
332             helper.unregisterListener();
333         }
334 
335         @Override
336         protected long computeValue() {
337             final ObservableValue<?> observable = helper.getObservableValue();
338             if (observable == null) {
339                 return DEFAULT_VALUE;
340             }
341             if (observable instanceof ObservableNumberValue) {
342                 return ((ObservableNumberValue)observable).longValue();
343             }
344             try {
345                 return ((Number)observable.getValue()).longValue();
346             } catch (NullPointerException ex) {
347                 Logging.getLogger().fine("Value of select binding is null, returning default value", ex);
348             } catch (ClassCastException ex) {
349                 Logging.getLogger().warning("Exception while evaluating select-binding", ex);
350             }
351             return DEFAULT_VALUE;
352         }
353 
354         @Override
355         public ObservableList<ObservableValue<?>> getDependencies() {
356             return helper.getDependencies();
357         }
358 
359     }
360 
361     public static class AsString extends StringBinding {
362 
363         private static final String DEFAULT_VALUE = null;
364 
365         private final SelectBindingHelper helper;
366 
367         public AsString(ObservableValue<?> root, String... steps) {
368             helper = new SelectBindingHelper(this, root, steps);
369         }
370 
371         public AsString(Object root, String... steps) {
372             helper = new SelectBindingHelper(this, root, steps);
373         }
374 
375         @Override
376         public void dispose() {
377             helper.unregisterListener();
378         }
379 
380         @Override
381         protected void onInvalidating() {
382             helper.unregisterListener();
383         }
384 
385         @Override
386         protected String computeValue() {
387             final ObservableValue<?> observable = helper.getObservableValue();
388             if (observable == null) {
389                 return DEFAULT_VALUE;
390             }
391             try {
392                 return observable.getValue().toString();
393             } catch (RuntimeException ex) {
394                 Logging.getLogger().warning("Exception while evaluating select-binding", ex);
395                 // return default
396                 return DEFAULT_VALUE;
397             }
398         }
399 
400         @Override
401         public ObservableList<ObservableValue<?>> getDependencies() {
402             return helper.getDependencies();
403         }
404 
405     }
406 
407     private static class SelectBindingHelper implements InvalidationListener {
408 
409         private final Binding<?> binding;
410         private final String[] propertyNames;
411         private final ObservableValue<?>[] properties;
412         private final PropertyReference<?>[] propRefs;
413         private final WeakInvalidationListener observer;
414 
415         private ObservableList<ObservableValue<?>> dependencies;
416 
417         private SelectBindingHelper(Binding<?> binding, ObservableValue<?> firstProperty, String... steps) {
418             if (firstProperty == null) {
419                 throw new NullPointerException("Must specify the root");
420             }
421             if (steps == null) {
422                 steps = new String[0];
423             }
424 
425             this.binding = binding;
426 
427             final int n = steps.length;
428             for (int i = 0; i < n; i++) {
429                 if (steps[i] == null) {
430                     throw new NullPointerException("all steps must be specified");
431                 }
432             }
433 
434             observer = new WeakInvalidationListener(this);
435             propertyNames = new String[n];
436             System.arraycopy(steps, 0, propertyNames, 0, n);
437             propRefs = new PropertyReference<?>[n];
438             properties = new ObservableValue<?>[n + 1];
439             properties[0] = firstProperty;
440             properties[0].addListener(observer);
441         }
442 
443         private static ObservableValue<?> checkAndCreateFirstStep(Object root, String[] steps) {
444             if (root == null || steps == null || steps[0] == null) {
445                 throw new NullPointerException("Must specify the root and the first property");
446             }
447             try {
448                 return JavaBeanAccessHelper.createReadOnlyJavaBeanProperty(root, steps[0]);
449             } catch (NoSuchMethodException ex) {
450                 throw new IllegalArgumentException("The first property '" + steps[0] + "' doesn't exist");
451             }
452         }
453 
454         private SelectBindingHelper(Binding<?> binding, Object root, String... steps) {
455             this(binding, checkAndCreateFirstStep(root, steps), Arrays.copyOfRange(steps, 1, steps.length));
456         }
457 
458         @Override
459         public void invalidated(Observable observable) {
460             binding.invalidate();
461         }
462 
463         public ObservableValue<?> getObservableValue() {
464             // Step through each of the steps, and at each step add a listener as
465             // appropriate, accumulating the result.
466             final int n = properties.length;
467             for (int i = 0; i < n - 1; i++) {
468                 final Object obj = properties[i].getValue();
469                 try {
470                     if ((propRefs[i] == null)
471                             || (!obj.getClass().equals(
472                             propRefs[i].getContainingClass()))) {
473                         propRefs[i] = new PropertyReference<Object>(obj.getClass(),
474                                 propertyNames[i]);
475                     }
476                     if (propRefs[i].hasProperty()) {
477                         properties[i + 1] = propRefs[i].getProperty(obj);
478                     } else {
479                         properties[i + 1] = JavaBeanAccessHelper.createReadOnlyJavaBeanProperty(obj, propRefs[i].getName());
480                     }
481                 } catch (NoSuchMethodException ex) {
482                     Logging.getLogger().warning("Exception while evaluating select-binding " + stepsToString(), ex);
483                     // return default
484                     updateDependencies();
485                     return null;
486                 } catch (RuntimeException ex) {
487                     final PlatformLogger logger = Logging.getLogger();
488                     if (logger.isLoggable(Level.WARNING)) {
489                         String msg = "Exception while evaluating select-binding " + stepsToString();
490                         if (ex instanceof IllegalStateException) {
491                             logger.warning(msg);
492                             logger.warning("Property '" + propertyNames[i] + "' does not exist in " + obj.getClass(), ex);
493                         } else if (ex instanceof NullPointerException) {
494                             logger.fine(msg);
495                             logger.fine("Property '" + propertyNames[i] + "' in " + properties[i] + " is null", ex);
496                         } else {
497                             logger.warning(msg, ex);
498                         }
499                     }
500                     // return default
501                     updateDependencies();
502                     return null;
503                 }
504                 properties[i + 1].addListener(observer);
505             }
506             updateDependencies();
507             final ObservableValue<?> result = properties[n-1];
508             if (result == null) {
509                 Logging.getLogger().fine("Property '" + propertyNames[n-1] + "' in " + properties[n-1] + " is null", new NullPointerException());
510             }
511             return result;
512         }
513 
514         private String stepsToString() {
515             return Arrays.toString(propertyNames);
516         }
517 
518         private void unregisterListener() {
519             final int n = properties.length;
520             for (int i = 1; i < n; i++) {
521                 if (properties[i] == null) {
522                     break;
523                 }
524                 properties[i].removeListener(observer);
525                 properties[i] = null;
526             }
527             updateDependencies();
528         }
529 
530         private void updateDependencies() {
531             if (dependencies != null) {
532                 dependencies.clear();
533                 final int n = properties.length;
534                 for (int i = 0; i < n; i++) {
535                     if (properties[i] == null) {
536                         break;
537                     }
538                     dependencies.add(properties[i]);
539                 }
540             }
541         }
542 
543         public ObservableList<ObservableValue<?>> getDependencies() {
544             if (dependencies == null) {
545                 dependencies = FXCollections.observableArrayList();
546                 updateDependencies();
547             }
548 
549             return FXCollections.unmodifiableObservableList(dependencies);
550         }
551 
552     }
553 
554 }