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.concurrent.CountDownLatch;
  29. import java.util.concurrent.TimeUnit;
  30. import java.util.concurrent.atomic.AtomicReference;
  31. import java.io.IOException;
  32. import javafx.fxml.FXMLLoader;
  33. import javafx.scene.Node;
  34. import javafx.application.Platform;
  35. import it.tidalwave.ui.javafx.NodeAndDelegate;
  36. import it.tidalwave.ui.javafx.impl.util.JavaFXSafeProxy;
  37. import org.slf4j.LoggerFactory;
  38. import it.tidalwave.util.ReflectionUtils;
  39. import lombok.Getter;
  40. import lombok.RequiredArgsConstructor;
  41. import lombok.extern.slf4j.Slf4j;
  42. import static it.tidalwave.ui.javafx.impl.util.JavaFXSafeComponentBuilder.BEANS;

  43. /***************************************************************************************************************************************************************
  44.  *
  45.  * The implementation of {@link NodeAndDelegate}.
  46.  *
  47.  * @author  Fabrizio Giudici
  48.  *
  49.  **************************************************************************************************************************************************************/
  50. @RequiredArgsConstructor @Getter @Slf4j
  51. public class DefaultNodeAndDelegate<T> implements NodeAndDelegate<T>
  52.   {
  53.     private static final String P_TIMEOUT = DefaultNodeAndDelegate.class.getName() + ".initTimeout";
  54.     private static final int INITIALIZER_TIMEOUT = Integer.getInteger(P_TIMEOUT, 10);

  55.     @Nonnull
  56.     private final Node node;

  57.     @Nonnull
  58.     private final T delegate;

  59.     /***********************************************************************************************************************************************************
  60.      * Creates a {@link NodeAndDelegate} for the given presentation class. The FXML resource name is inferred by
  61.      * default, For instance, is the class is named {@code JavaFXFooBarPresentation}, the resource name is
  62.      * {@code FooBar.fxml} and searched in the same packages as the class.
  63.      *
  64.      * @see #of(java.lang.Class, java.lang.String)
  65.      *
  66.      * @since 1.0-ALPHA-13
  67.      *
  68.      * @param   presentationClass   the class of the presentation for which the resources must be created.
  69.      **********************************************************************************************************************************************************/
  70.     @Nonnull
  71.     public static <T> NodeAndDelegate<T> of (@Nonnull final Class<T> presentationClass)
  72.       {
  73.         final var resource = presentationClass.getSimpleName().replaceAll("^JavaFX", "")
  74.                                               .replaceAll("^JavaFx", "")
  75.                                               .replaceAll("Presentation$", "")
  76.                              + ".fxml";
  77.         return of(presentationClass, resource);
  78.       }

  79.     /***********************************************************************************************************************************************************
  80.      * Creates a {@link NodeAndDelegate} for the given presentation class.
  81.      *
  82.      * @param   presentationClass   the class of the presentation for which the resources must be created.
  83.      * @param   fxmlResourcePath    the path of the FXML resource
  84.      **********************************************************************************************************************************************************/
  85.     @Nonnull
  86.     public static <T> NodeAndDelegate<T> of (@Nonnull final Class<T> presentationClass, @Nonnull final String fxmlResourcePath)
  87.       {
  88.         final var log = LoggerFactory.getLogger(NodeAndDelegate.class);
  89.         log.debug("of({}, {})", presentationClass, fxmlResourcePath);

  90.         final var latch = new CountDownLatch(1);
  91.         final var nad = new AtomicReference<NodeAndDelegate<T>>();
  92.         final var exception = new AtomicReference<RuntimeException>();
  93.         final var message = String.format("Likely deadlock in the JavaFX Thread: couldn't create NodeAndDelegate: %s, %s",
  94.                                           presentationClass, fxmlResourcePath);

  95.         if (Platform.isFxApplicationThread())
  96.           {
  97.             try
  98.               {
  99.                 return load(presentationClass, fxmlResourcePath);
  100.               }
  101.             catch (IOException e)
  102.               {
  103.                 exception.set(new RuntimeException(e));
  104.               }
  105.           }

  106.         Platform.runLater(() ->
  107.           {
  108.             try
  109.               {
  110.                 nad.set(load(presentationClass, fxmlResourcePath));
  111.               }
  112.             catch (RuntimeException e)
  113.               {
  114.                 exception.set(e);
  115.               }
  116.             catch (Exception e)
  117.               {
  118.                 exception.set(new RuntimeException(e));
  119.               }

  120.             latch.countDown();
  121.           });

  122.         try
  123.           {
  124.             log.debug("Waiting for NodeAndDelegate initialisation in JavaFX thread...");
  125.             log.debug("If deadlocks and you need longer time with the debugger, set {} (current value: {})", P_TIMEOUT, INITIALIZER_TIMEOUT);

  126.             if (!latch.await(INITIALIZER_TIMEOUT, TimeUnit.SECONDS))
  127.               {
  128.                 throw new RuntimeException(message);
  129.               }
  130.           }
  131.         catch (InterruptedException e)
  132.           {
  133.             throw new RuntimeException(e);
  134.           }

  135.         if (exception.get() != null)
  136.           {
  137.             throw exception.get();
  138.           }

  139.         if (nad.get() == null)
  140.           {
  141.             throw new RuntimeException(message);
  142.           }

  143.         return nad.get();
  144.       }

  145.     @Nonnull
  146.     public static <T> NodeAndDelegate<T> load (@Nonnull final Class<T> clazz, @Nonnull final String resource)
  147.             throws IOException
  148.       {
  149.         final var log = LoggerFactory.getLogger(NodeAndDelegate.class);
  150.         log.debug("NodeAndDelegate({}, {})", clazz, resource);
  151.         assert Platform.isFxApplicationThread() : "Not in JavaFX UI Thread";
  152.         final var loader = new FXMLLoader(clazz.getResource(resource), null, null,
  153.                                           type -> ReflectionUtils.instantiateWithDependencies(type, BEANS));
  154.         try
  155.           {
  156.             final Node node = loader.load();
  157.             final T jfxController = loader.getController();
  158.             ReflectionUtils.injectDependencies(jfxController, BEANS);
  159.             final var interfaces = jfxController.getClass().getInterfaces();

  160.             if (interfaces.length == 0)
  161.               {
  162.                 log.warn("{} has no interface: not creating safe proxy", jfxController.getClass());
  163.                 log.debug(">>>> load({}, {}) completed", clazz, resource);
  164.                 return new DefaultNodeAndDelegate<>(node, jfxController);
  165.               }
  166.             else
  167.               {
  168.                 final var safeDelegate = JavaFXSafeProxy.of(jfxController, interfaces);
  169.                 log.debug(">>>> load({}, {}) completed", clazz, resource);
  170.                 return new DefaultNodeAndDelegate<>(node, safeDelegate);
  171.               }
  172.           }
  173.         catch (IllegalStateException e)
  174.           {
  175.             final var message = String.format("ERROR: Cannot find resource: %s/%s", clazz.getPackageName().replace('.','/'), resource);
  176.             log.error("ERROR: Cannot find resource: {}", message);
  177.             throw new IllegalStateException(message);
  178.           }
  179.       }
  180.   }