1 /*
   2  * Copyright (c) 2010, 2016, 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 sun.util.logging.PlatformLogger;
  47 import sun.util.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                         Logging.getLogger().warning("Exception while evaluating select-binding " + stepsToString());
 490                         if (ex instanceof  IllegalStateException) {
 491                             logger.warning("Property '" + propertyNames[i] + "' does not exist in " + obj.getClass(), ex);
 492                         } else if (ex instanceof NullPointerException) {
 493                             logger.fine("Property '" + propertyNames[i] + "' in " + properties[i] + " is null", ex);
 494                         } else {
 495                             Logging.getLogger().warning("", ex);
 496                         }
 497                     }
 498                     // return default
 499                     updateDependencies();
 500                     return null;
 501                 }
 502                 properties[i + 1].addListener(observer);
 503             }
 504             updateDependencies();
 505             final ObservableValue<?> result = properties[n-1];
 506             if (result == null) {
 507                 Logging.getLogger().fine("Property '" + propertyNames[n-1] + "' in " + properties[n-1] + " is null", new NullPointerException());
 508             }
 509             return result;
 510         }
 511 
 512         private String stepsToString() {
 513             return Arrays.toString(propertyNames);
 514         }
 515 
 516         private void unregisterListener() {
 517             final int n = properties.length;
 518             for (int i = 1; i < n; i++) {
 519                 if (properties[i] == null) {
 520                     break;
 521                 }
 522                 properties[i].removeListener(observer);
 523                 properties[i] = null;
 524             }
 525             updateDependencies();
 526         }
 527 
 528         private void updateDependencies() {
 529             if (dependencies != null) {
 530                 dependencies.clear();
 531                 final int n = properties.length;
 532                 for (int i = 0; i < n; i++) {
 533                     if (properties[i] == null) {
 534                         break;
 535                     }
 536                     dependencies.add(properties[i]);
 537                 }
 538             }
 539         }
 540 
 541         public ObservableList<ObservableValue<?>> getDependencies() {
 542             if (dependencies == null) {
 543                 dependencies = FXCollections.observableArrayList();
 544                 updateDependencies();
 545             }
 546 
 547             return FXCollections.unmodifiableObservableList(dependencies);
 548         }
 549 
 550     }
 551 
 552 }