JavaFXSafeProxyCreator.java

  1. /*
  2.  * #%L
  3.  * *********************************************************************************************************************
  4.  *
  5.  * SteelBlue
  6.  * http://steelblue.tidalwave.it - git clone git@bitbucket.org:tidalwave/steelblue-src.git
  7.  * %%
  8.  * Copyright (C) 2015 - 2015 Tidalwave s.a.s. (http://tidalwave.it)
  9.  * %%
  10.  *
  11.  * *********************************************************************************************************************
  12.  *
  13.  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  14.  * the License. You may obtain a copy of the License at
  15.  *
  16.  *     http://www.apache.org/licenses/LICENSE-2.0
  17.  *
  18.  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  19.  * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
  20.  * specific language governing permissions and limitations under the License.
  21.  *
  22.  * *********************************************************************************************************************
  23.  *
  24.  *
  25.  *
  26.  * *********************************************************************************************************************
  27.  * #L%
  28.  */
  29. package it.tidalwave.ui.javafx;

  30. import javax.annotation.Nonnull;
  31. import java.lang.reflect.Proxy;
  32. import java.util.HashMap;
  33. import java.util.Map;
  34. import java.util.concurrent.Executor;
  35. import java.util.concurrent.CountDownLatch;
  36. import java.util.concurrent.TimeUnit;
  37. import java.util.concurrent.atomic.AtomicReference;
  38. import java.io.IOException;
  39. import javafx.fxml.FXMLLoader;
  40. import javafx.scene.Node;
  41. import javafx.application.Platform;
  42. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  43. import it.tidalwave.role.ui.javafx.JavaFXBinder;
  44. import it.tidalwave.role.ui.javafx.impl.util.JavaFXSafeProxy;
  45. import it.tidalwave.role.ui.javafx.impl.DefaultJavaFXBinder;
  46. import it.tidalwave.role.ui.javafx.impl.util.ReflectionUtils;
  47. import lombok.Getter;
  48. import lombok.RequiredArgsConstructor;
  49. import lombok.extern.slf4j.Slf4j;
  50. import static lombok.AccessLevel.PRIVATE;

  51. /***********************************************************************************************************************
  52.  *
  53.  * This facility class create a thread-safe proxy for the JavaFX delegate (controller). Thread-safe means that it can
  54.  * be called by any thread and the JavaFX UI related stuff will be safely invoked in the JavaFX UI Thread.
  55.  * It is usually used in this way:
  56.  *
  57.  * <pre>
  58.  * // This is a Spring bean
  59.  * public class JavaFxFooBarPresentation implements FooBarPresentation
  60.  *   {
  61.  *     private static final String FXML_URL = "/my/package/javafx/FooBar.fxml";
  62.  *
  63.  *     @Inject
  64.  *     private FlowController flowController;
  65.  *
  66.  *     private final NodeAndDelegate nad = createNodeAndDelegate(getClass(), FXML_URL);
  67.  *
  68.  *     private final FooBarPresentation delegate = nad.getDelegate();
  69.  *
  70.  *     public void showUp()
  71.  *       {
  72.  *         flowController.doSomething(nad.getNode());
  73.  *       }
  74.  *
  75.  *     public void showData (final String data)
  76.  *       {
  77.  *         delegate.showData(data);
  78.  *       }
  79.  *   }
  80.  * </pre>
  81.  *
  82.  * The method {@link #createNodeAndDelegate(java.lang.Class, java.lang.String)} safely invokes the {@link FXMLLoader}
  83.  * and returns a {@link NodeAndDelegate} that contains both the visual {@link Node} and its delegate (controller).
  84.  *
  85.  * The latter is wrapped by a safe proxy that makes sure that any method invocation (such as {@code showData()} in the
  86.  * example is again executed in the JavaFX UI Thread. This means that the Presentation object methods can be invoked
  87.  * in any thread.
  88.  *
  89.  * For method returning {@code void}, the method invocation is asynchronous; that is, the caller is not blocked waiting
  90.  * for the method execution completion. If a return value is provided, the invocation is synchronous, and the caller
  91.  * will correctly wait the completion of the execution in order to get the result value.
  92.  *
  93.  * A typical JavaFX delegate (controller) looks like:
  94.  *
  95.  * <pre>
  96.  * // This is not a Spring bean - created by the FXMLLoader
  97.  * public class JavaFxFooBarPresentationDelegate implements FooBarPresentation
  98.  *   {
  99.  *     @FXML
  100.  *     private Label label;
  101.  *
  102.  *     @FXML
  103.  *     private Button button;
  104.  *
  105.  *     @Inject // the only thing that can be injected, by means of JavaFXSafeProxyCreator
  106.  *     private JavaFxBinder binder;
  107.  *
  108.  *     @Override
  109.  *     public void bind (final UserAction action)
  110.  *       {
  111.  *         binder.bind(button, action);
  112.  *       }
  113.  *
  114.  *     @Override
  115.  *     public void showData (final String data)
  116.  *       {
  117.  *         label.setText(data);
  118.  *       }
  119.  *  }
  120.  * </pre>
  121.  *
  122.  * Not only all the methods invoked on the delegate are guaranteed to run in the JavaFX UI thread, but also its
  123.  * constructor, as per JavaFX requirements.
  124.  *
  125.  * A Presentation Delegate must not try to have dependency injection from Spring (for instance, by means of AOP),
  126.  * otherwise a deadlock could be triggered. Injection in constructors is safe.
  127.  *
  128.  * @author  Fabrizio Giudici
  129.  *
  130.  **********************************************************************************************************************/
  131. @Slf4j
  132. public class JavaFXSafeProxyCreator
  133.   {
  134.     private static final String P_TIMEOUT = JavaFXSafeProxyCreator.class.getName() + ".initTimeout";
  135.     private static final int initializerTimeout = Integer.getInteger(P_TIMEOUT, 10);

  136.     public static final Map<Class<?>, Object> BEANS = new HashMap<>();

  137.     @Getter
  138.     private static final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

  139.     @Getter
  140.     private static final JavaFXBinder javaFxBinder = new DefaultJavaFXBinder(executor);

  141.     static
  142.       {
  143.         executor.setWaitForTasksToCompleteOnShutdown(false);
  144.         executor.setThreadNamePrefix("javafxBinder-");
  145.         // Fix for STB-26
  146.         executor.setCorePoolSize(1);
  147.         executor.setMaxPoolSize(1);
  148.         executor.setQueueCapacity(10000);
  149.         BEANS.put(JavaFXBinder.class, javaFxBinder);
  150.         BEANS.put(Executor.class, executor);
  151.       }

  152.     private JavaFXSafeProxyCreator () {}

  153.     /*******************************************************************************************************************
  154.      *
  155.      *
  156.      *
  157.      ******************************************************************************************************************/
  158.     @RequiredArgsConstructor(access = PRIVATE)
  159.     public static final class NodeAndDelegate
  160.       {
  161.         @Getter @Nonnull
  162.         private final Node node;

  163.         @Nonnull
  164.         private final Object delegate;

  165.         @Nonnull
  166.         public <T> T getDelegate()
  167.           {
  168.             return (T)delegate;
  169.           }

  170.         @Nonnull
  171.         public static <T> NodeAndDelegate load (@Nonnull final Class<T> clazz, @Nonnull final String resource)
  172.           throws IOException
  173.           {
  174.             log.debug("NodeAndDelegate({}, {})", clazz, resource);
  175.             assert Platform.isFxApplicationThread() : "Not in JavaFX UI Thread";
  176.             final FXMLLoader loader = new FXMLLoader(clazz.getResource(resource), null, null,
  177.                                                      type -> ReflectionUtils.instantiateWithDependencies(type, BEANS));
  178.             final Node node = (Node)loader.load();
  179.             final T jfxController = loader.getController();
  180.             ReflectionUtils.injectDependencies(jfxController, BEANS);
  181.             final Class<?>[] interfaces = jfxController.getClass().getInterfaces();

  182.             if (interfaces.length == 0)
  183.               {
  184.                 log.warn("{} has no interface: not creating safe proxy", jfxController.getClass());
  185.                 log.debug(">>>> load({}, {}) completed", clazz, resource);
  186.                 return new NodeAndDelegate(node, jfxController);
  187.               }
  188.             else
  189.               {
  190.                 final Class<T> interfaceClass = (Class<T>)interfaces[0]; // FIXME
  191.                 final T safeDelegate = JavaFXSafeProxyCreator.createSafeProxy(jfxController, interfaceClass);
  192.                 log.debug(">>>> load({}, {}) completed", clazz, resource);
  193.                 return new NodeAndDelegate(node, safeDelegate);
  194.               }
  195.           }
  196.       }

  197.     /*******************************************************************************************************************
  198.      *
  199.      * Creates a {@link NodeAndDelegate} for the given presentation class. The FXML resource name is inferred by
  200.      * default, For instance, is the class is named {@code JavaFXFooBarPresentation}, the resource name is
  201.      * {@code FooBar.fxml} and searched in the same packages as the class.
  202.      *
  203.      * @see #createNodeAndDelegate(java.lang.Class, java.lang.String)
  204.      *
  205.      * @since 1.0-ALPHA-13
  206.      *
  207.      * @param   presentationClass   the class of the presentation for which the resources must be created.
  208.      *
  209.      ******************************************************************************************************************/
  210.     @Nonnull
  211.     public static <T> NodeAndDelegate createNodeAndDelegate (@Nonnull final Class<?> presentationClass)
  212.       {
  213.         final String resource = presentationClass.getSimpleName().replaceAll("^JavaFX", "")
  214.                                                                  .replaceAll("^JavaFx", "")
  215.                                                                  .replaceAll("Presentation$", "")
  216.                                                                  + ".fxml";
  217.         return createNodeAndDelegate(presentationClass, resource);
  218.       }

  219.     /*******************************************************************************************************************
  220.      *
  221.      * Creates a {@link NodeAndDelegate} for the given presentation class.
  222.      *
  223.      * @param   presentationClass   the class of the presentation for which the resources must be created.
  224.      * @param   fxmlResourcePath    the path of the FXML resource
  225.      *
  226.      ******************************************************************************************************************/
  227.     @Nonnull
  228.     public static <T> NodeAndDelegate createNodeAndDelegate (@Nonnull final Class<?> presentationClass,
  229.                                                              @Nonnull final String fxmlResourcePath)
  230.       {
  231.         log.debug("createNodeAndDelegate({}, {})", presentationClass, fxmlResourcePath);

  232.         final CountDownLatch latch = new CountDownLatch(1);
  233.         final AtomicReference<NodeAndDelegate> nad = new AtomicReference<>();
  234.         final AtomicReference<RuntimeException> exception = new AtomicReference<>();

  235.         if (Platform.isFxApplicationThread())
  236.           {
  237.             try
  238.               {
  239.                 return NodeAndDelegate.load(presentationClass, fxmlResourcePath);
  240.               }
  241.             catch (IOException e)
  242.               {
  243.                 exception.set(new RuntimeException(e));
  244.               }
  245.           }

  246.         Platform.runLater(() ->
  247.           {
  248.             try
  249.               {
  250.                 nad.set(NodeAndDelegate.load(presentationClass, fxmlResourcePath));
  251.               }
  252.             catch (RuntimeException e)
  253.               {
  254.                 exception.set(e);
  255.               }
  256.             catch (Exception e)
  257.               {
  258.                 exception.set(new RuntimeException(e));
  259.               }

  260.             latch.countDown();
  261.           });

  262.         try
  263.           {
  264.             log.debug("Waiting for NodeAndDelegate initialisation in JavaFX thread...");
  265.             log.debug("If deadlocks and you need longer time with the debugger, set {} (current value: {})",
  266.                       P_TIMEOUT, initializerTimeout);
  267.             latch.await(initializerTimeout, TimeUnit.SECONDS); // FIXME
  268.           }
  269.         catch (InterruptedException e)
  270.           {
  271.             throw new RuntimeException(e);
  272.           }

  273.         if (exception.get() != null)
  274.           {
  275.             throw exception.get();
  276.           }

  277.         if (nad.get() == null)
  278.           {
  279.             final String message = String.format("Likely deadlock in the JavaFX Thread: couldn't create " +
  280.                                                  "NodeAndDelegate: %s, %s", presentationClass, fxmlResourcePath);
  281.             throw new RuntimeException(message);
  282.           }

  283.         return nad.get();
  284.       }

  285.     /*******************************************************************************************************************
  286.      *
  287.      *
  288.      *
  289.      ******************************************************************************************************************/
  290.     @Nonnull
  291.     public static <T> T createSafeProxy (@Nonnull final T target, final Class<T> interfaceClass)
  292.       {
  293.         return (T)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
  294.                                          new Class[] { interfaceClass },
  295.                                          new JavaFXSafeProxy<>(target));
  296.       }
  297.   }