DefaultNodeAndDelegate.java

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

  27. import jakarta.annotation.Nonnull;
  28. import java.util.HashMap;
  29. import java.util.Map;
  30. import java.util.concurrent.CountDownLatch;
  31. import java.util.concurrent.Executor;
  32. import java.util.concurrent.TimeUnit;
  33. import java.util.concurrent.atomic.AtomicReference;
  34. import java.io.IOException;
  35. import javafx.fxml.FXMLLoader;
  36. import javafx.scene.Node;
  37. import javafx.application.Platform;
  38. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  39. import it.tidalwave.ui.javafx.JavaFXBinder;
  40. import it.tidalwave.ui.javafx.JavaFXMenuBarControl;
  41. import it.tidalwave.ui.javafx.JavaFXToolBarControl;
  42. import it.tidalwave.ui.javafx.NodeAndDelegate;
  43. import it.tidalwave.ui.javafx.impl.util.JavaFXSafeProxy;
  44. import org.slf4j.LoggerFactory;
  45. import it.tidalwave.util.PreferencesHandler;
  46. import it.tidalwave.util.ReflectionUtils;
  47. import lombok.Getter;
  48. import lombok.RequiredArgsConstructor;
  49. import lombok.Setter;
  50. import lombok.extern.slf4j.Slf4j;

  51. /***************************************************************************************************************************************************************
  52.  *
  53.  * The implementation of {@link NodeAndDelegate}.
  54.  *
  55.  * @author  Fabrizio Giudici
  56.  *
  57.  **************************************************************************************************************************************************************/
  58. @RequiredArgsConstructor @Getter @Slf4j
  59. public class DefaultNodeAndDelegate<T> implements NodeAndDelegate<T>
  60.   {
  61.     private static final String P_TIMEOUT = DefaultNodeAndDelegate.class.getName() + ".initTimeout";
  62.     private static final int INITIALIZER_TIMEOUT = Integer.getInteger(P_TIMEOUT, 10);

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

  64.     @Getter
  65.     private static final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

  66.     @Getter
  67.     private static final JavaFXBinder javaFxBinder = new DefaultJavaFXBinder(executor);

  68.     @Getter
  69.     private static final JavaFXToolBarControl toolBarControl = new DefaultJavaFXToolBarControl();

  70.     @Getter
  71.     private static final JavaFXMenuBarControl menuBarControl = new DefaultJavaFXMenuBarControl();

  72.     // @Getter
  73.     // private static final JavaFXPanelGroupControl panelGroupControl = new DefaultJavaFXPanelGroupControl();

  74.     @Getter @Setter
  75.     private static boolean logDelegateInvocations = false;

  76.     static
  77.       {
  78.         executor.setWaitForTasksToCompleteOnShutdown(false);
  79.         executor.setThreadNamePrefix("javafxBinder-");
  80.         // Fix for STB-26
  81.         executor.setCorePoolSize(1);
  82.         executor.setMaxPoolSize(1);
  83.         executor.setQueueCapacity(10000);
  84.         BEANS.put(JavaFXBinder.class, javaFxBinder);
  85.         BEANS.put(Executor.class, executor);
  86.         BEANS.put(JavaFXToolBarControl.class, toolBarControl);
  87.         BEANS.put(JavaFXMenuBarControl.class, menuBarControl);
  88.         // BEANS.put(JavaFXPanelGroupControl.class, panelGroupControl);
  89.         BEANS.put(PreferencesHandler.class, PreferencesHandler.getInstance());
  90.       }

  91.     @Nonnull
  92.     private final Node node;

  93.     @Nonnull
  94.     private final T delegate;

  95.     /***********************************************************************************************************************************************************
  96.      * Creates a {@link NodeAndDelegate} for the given presentation class. The FXML resource name is inferred by
  97.      * default, For instance, is the class is named {@code JavaFXFooBarPresentation}, the resource name is
  98.      * {@code FooBar.fxml} and searched in the same packages as the class.
  99.      *
  100.      * @see #of(java.lang.Class, java.lang.String)
  101.      *
  102.      * @since 1.0-ALPHA-13
  103.      *
  104.      * @param   presentationClass   the class of the presentation for which the resources must be created.
  105.      **********************************************************************************************************************************************************/
  106.     @Nonnull
  107.     public static <T> NodeAndDelegate<T> of (@Nonnull final Class<T> presentationClass)
  108.       {
  109.         final var resource = presentationClass.getSimpleName().replaceAll("^JavaFX", "")
  110.                                               .replaceAll("^JavaFx", "")
  111.                                               .replaceAll("Presentation$", "")
  112.                              + ".fxml";
  113.         return of(presentationClass, resource);
  114.       }

  115.     /***********************************************************************************************************************************************************
  116.      * Creates a {@link NodeAndDelegate} for the given presentation class.
  117.      *
  118.      * @param   presentationClass   the class of the presentation for which the resources must be created.
  119.      * @param   fxmlResourcePath    the path of the FXML resource
  120.      **********************************************************************************************************************************************************/
  121.     @Nonnull
  122.     public static <T> NodeAndDelegate<T> of (@Nonnull final Class<T> presentationClass, @Nonnull final String fxmlResourcePath)
  123.       {
  124.         final var log = LoggerFactory.getLogger(NodeAndDelegate.class);
  125.         log.debug("of({}, {})", presentationClass, fxmlResourcePath);

  126.         final var latch = new CountDownLatch(1);
  127.         final var nad = new AtomicReference<NodeAndDelegate<T>>();
  128.         final var exception = new AtomicReference<RuntimeException>();

  129.         if (Platform.isFxApplicationThread())
  130.           {
  131.             try
  132.               {
  133.                 return load(presentationClass, fxmlResourcePath);
  134.               }
  135.             catch (IOException e)
  136.               {
  137.                 exception.set(new RuntimeException(e));
  138.               }
  139.           }

  140.         Platform.runLater(() ->
  141.           {
  142.             try
  143.               {
  144.                 nad.set(load(presentationClass, fxmlResourcePath));
  145.               }
  146.             catch (RuntimeException e)
  147.               {
  148.                 exception.set(e);
  149.               }
  150.             catch (Exception e)
  151.               {
  152.                 exception.set(new RuntimeException(e));
  153.               }

  154.             latch.countDown();
  155.           });

  156.         try
  157.           {
  158.             log.debug("Waiting for NodeAndDelegate initialisation in JavaFX thread...");
  159.             log.debug("If deadlocks and you need longer time with the debugger, set {} (current value: {})", P_TIMEOUT, INITIALIZER_TIMEOUT);
  160.             latch.await(INITIALIZER_TIMEOUT, TimeUnit.SECONDS); // FIXME
  161.           }
  162.         catch (InterruptedException e)
  163.           {
  164.             throw new RuntimeException(e);
  165.           }

  166.         if (exception.get() != null)
  167.           {
  168.             throw exception.get();
  169.           }

  170.         if (nad.get() == null)
  171.           {
  172.             final var message = String.format("Likely deadlock in the JavaFX Thread: couldn't create NodeAndDelegate: %s, %s",
  173.                                               presentationClass, fxmlResourcePath);
  174.             throw new RuntimeException(message);
  175.           }

  176.         return nad.get();
  177.       }

  178.     @Nonnull
  179.     public static <T> NodeAndDelegate<T> load (@Nonnull final Class<T> clazz, @Nonnull final String resource)
  180.             throws IOException
  181.       {
  182.         final var log = LoggerFactory.getLogger(NodeAndDelegate.class);
  183.         log.debug("NodeAndDelegate({}, {})", clazz, resource);
  184.         assert Platform.isFxApplicationThread() : "Not in JavaFX UI Thread";
  185.         final var loader = new FXMLLoader(clazz.getResource(resource), null, null,
  186.                                           type -> ReflectionUtils.instantiateWithDependencies(type, BEANS));
  187.         try
  188.           {
  189.             final Node node = loader.load();
  190.             final T jfxController = loader.getController();
  191.             ReflectionUtils.injectDependencies(jfxController, BEANS);
  192.             final var interfaces = jfxController.getClass().getInterfaces();

  193.             if (interfaces.length == 0)
  194.               {
  195.                 log.warn("{} has no interface: not creating safe proxy", jfxController.getClass());
  196.                 log.debug(">>>> load({}, {}) completed", clazz, resource);
  197.                 return new DefaultNodeAndDelegate<>(node, jfxController);
  198.               }
  199.             else
  200.               {
  201.                 final var safeDelegate = JavaFXSafeProxy.of(jfxController, interfaces);
  202.                 log.debug(">>>> load({}, {}) completed", clazz, resource);
  203.                 return new DefaultNodeAndDelegate<>(node, safeDelegate);
  204.               }
  205.           }
  206.         catch (IllegalStateException e)
  207.           {
  208.             final var message = String.format("ERROR: Cannot find resource: %s/%s", clazz.getPackageName().replace('.','/'), resource);
  209.             log.error("ERROR: Cannot find resource: {}", message);
  210.             throw new IllegalStateException(message);
  211.           }
  212.       }
  213.   }