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

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

    Details

      Description

      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.

        Attachments

          Activity

            People

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

              Dates

              • Created:
                Updated:
                Resolved:
                Imported: