Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8092763

TextInputControl issue with null String returned by Converter (from TextFormatter)

XMLWordPrintable

      Apparently any control that inherits from TextInputControl has an issue when it has a TextFormatter that contains a StringConverter which has a toString() method that returns null (for example when the input value is null).

      This raises a NullPointerException in the method private <T> void updateText(TextFormatter<T> formatter) in the class TexInputControl because the value returned from converter.toString() is not checked before calling replaceText(). As of now the code for this method is as follows in TextInputControl:

          private <T> void updateText(TextFormatter<T> formatter) {
              T value = formatter.getValue();
              StringConverter<T> converter = formatter.getValueConverter();
              if (converter != null) {
                  String text = converter.toString(value);
                  replaceText(0, getLength(), text, text.length(), text.length());
              }
          }

      The documentation from StringConverter.toString() does not specify if null is an illegal value or not.

      A way to circumvent the issue is to return the empty string ("") instead of null.

      The issue arose to me while doing a TextFormatter for a spinner that allows people to enter a port value. The value of the spinner is initially null in order to let the user see the prompt text.

      Value factory for the Spinner:

      public final class PortValueFactory extends SpinnerValueFactory<Integer> {

          @Override
          public void decrement(int steps) {
              final Integer value = getValue();
              final int nextValue = (value == null) ? PortTextFormatter.MIN_PORT : Math.max(value - 1, PortTextFormatter.MIN_PORT);
              setValue(nextValue);
          }

          @Override
          public void increment(int steps) {
              final Integer value = getValue();
              final int nextValue = (value == null) ? PortTextFormatter.MIN_PORT : Math.min(value + 1, PortTextFormatter.MAX_PORT);
              setValue(nextValue);
          }
      }

      Textformatter for the Spinner's editor:

      public final class PortTextFormatter extends TextFormatter<Integer> {

          /**
           * The min port value.
           */
          public static final int MIN_PORT = 0;
          /**
           * The max port value.
           */
          public static final int MAX_PORT = 2 * Short.MAX_VALUE + 1;

          /**
           * Creates a new instance.
           */
          public PortTextFormatter() {
      // super(Converter.getInstance(), null, Filter.getInstance());
              super(Converter.getInstance(), null, null);
          }

          /**
           * A filter that only accept short values that are valid for a port.
           * @author Fabrice Bouyé (fabriceb@spc.int)
           */
          public final static class Filter implements UnaryOperator<TextFormatter.Change> {

              /**
               * The unique instance of this class.
               */
              private static Filter INSTANCE;

              /**
               * Hidden constructor.
               */
              private Filter() {
              }

              @Override
              public TextFormatter.Change apply(TextFormatter.Change change) {
                  final String text = change.getText();
                  TextFormatter.Change result = (text.isEmpty() || text.matches("[0-9]+")) ? change : null; // NOI18N.
                  try {
                      final int value = Integer.parseInt(change.getControlNewText());
                      if (value < MIN_PORT || value > MAX_PORT) {
                          result = null;
                      }
                  } catch (NumberFormatException ex) {
      // MOViTLogger.LOGGER.log(Level.FINEST, ex.getMessage(), ex);
                  }
                  return result;
              }

              /**
               * Gets the unique instance of this class.
               * @return The unique instance of this class.
               */
              public synchronized static Filter getInstance() {
                  if (INSTANCE == null) {
                      INSTANCE = new Filter();
                  }
                  return INSTANCE;
              }
          }

          /**
           * A converter that only accept short values that are valid for a port.
           * @author Fabrice Bouyé (fabriceb@spc.int)
           */
          public final static class Converter extends StringConverter<Integer> {

              /**
               * The unique instance of this class.
               */
              private static Converter INSTANCE;

              /**
               * Hidden constructor.
               */
              private Converter() {
              }

              @Override
              public String toString(final Integer value) {
                  final Integer val = (value == null) ? null : Math.max(MIN_PORT, Math.min(MAX_PORT, value));
      // return (val == null) ? "" : val.toString();
                  return (val == null) ? null : val.toString();
              }

              @Override
              public Integer fromString(final String text) {
                  Integer result = (text == null || text.trim().isEmpty()) ? null : Integer.parseInt(text.trim());
                  result = (result == null) ? MIN_PORT : Math.max(MIN_PORT, Math.min(MAX_PORT, result));
                  return result;
              }

              /**
               * Gets the unique instance of this class.
               * @return The unique instance of this class.
               */
              public synchronized static Converter getInstance() {
                  if (INSTANCE == null) {
                      INSTANCE = new Converter();
                  }
                  return INSTANCE;
              }
          }
      }

      Test code:

      public class Main extends Application {

          @Override
          public void start(Stage primaryStage) {
              Spinner<Integer> spinner = new Spinner();
              spinner.setValueFactory(new PortValueFactory());
              spinner.getEditor().setTextFormatter(new PortTextFormatter());
              StackPane root = new StackPane();
              root.getChildren().add(spinner);
              Scene scene = new Scene(root, 300, 250);
              primaryStage.setTitle("Test");
              primaryStage.setScene(scene);
              primaryStage.show();
          }

          /**
           * @param args the command line arguments
           */
          public static void main(String[] args) {
              launch(args);
          }
      }

      Upon launch the following exception is raised:

      java.lang.reflect.InvocationTargetException
      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.lang.reflect.Method.invoke(Method.java:497)
      at com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:389)
      at com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:328)
      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.lang.reflect.Method.invoke(Method.java:497)
      at sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:767)
      Caused by: java.lang.RuntimeException: Exception in Application start method
      at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:917)
      at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$152(LauncherImpl.java:182)
      at com.sun.javafx.application.LauncherImpl$$Lambda$50/1642360923.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:745)
      Caused by: java.lang.NullPointerException
      at javafx.scene.control.TextInputControl.updateText(TextInputControl.java:1215)
      at javafx.scene.control.TextInputControl.access$300(TextInputControl.java:79)
      at javafx.scene.control.TextInputControl$5.invalidated(TextInputControl.java:320)
      at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:111)
      at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
      at javafx.scene.control.TextInputControl.setTextFormatter(TextInputControl.java:334)
      at test.Main.start(Main.java:20)
      at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$159(LauncherImpl.java:863)
      at com.sun.javafx.application.LauncherImpl$$Lambda$53/1379051502.run(Unknown Source)
      at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$172(PlatformImpl.java:326)
      at com.sun.javafx.application.PlatformImpl$$Lambda$45/355629945.run(Unknown Source)
      at com.sun.javafx.application.PlatformImpl.lambda$null$170(PlatformImpl.java:295)
      at com.sun.javafx.application.PlatformImpl$$Lambda$48/1675761123.run(Unknown Source)
      at java.security.AccessController.doPrivileged(Native Method)
      at com.sun.javafx.application.PlatformImpl.lambda$runLater$171(PlatformImpl.java:294)
      at com.sun.javafx.application.PlatformImpl$$Lambda$47/1915503092.run(Unknown Source)
      at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
      at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
      at com.sun.glass.ui.win.WinApplication.lambda$null$145(WinApplication.java:101)
      at com.sun.glass.ui.win.WinApplication$$Lambda$36/1963387170.run(Unknown Source)
      ... 1 more
      Exception running application test.Main

      This can be fixed by returning "" instead of null in the toString() method of the StringConverter wihtin the TextFormatter.

            leifs Leif Samuelsson (Inactive)
            fbouyajfx Fabrice Bouyé (Inactive)
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved:
              Imported: