AbstractJavaFXSpringApplication.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.spi;

  27. import java.lang.reflect.InvocationTargetException;
  28. import java.lang.reflect.Method;
  29. import javax.annotation.CheckForNull;
  30. import jakarta.annotation.Nonnull;
  31. import java.util.Arrays;
  32. import java.util.List;
  33. import java.util.Objects;
  34. import java.util.Optional;
  35. import java.util.TreeMap;
  36. import java.util.concurrent.ExecutorService;
  37. import java.util.concurrent.Executors;
  38. import java.util.function.Consumer;
  39. import java.util.function.Function;
  40. import java.io.IOException;
  41. import javafx.scene.Parent;
  42. import javafx.scene.Scene;
  43. import javafx.stage.Stage;
  44. import javafx.application.Application;
  45. import javafx.application.Platform;
  46. import org.springframework.context.ApplicationContext;
  47. import org.springframework.context.ConfigurableApplicationContext;
  48. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  49. import it.tidalwave.ui.core.annotation.PresentationAssembler;
  50. import it.tidalwave.ui.core.annotation.Assemble;
  51. import it.tidalwave.ui.core.message.PowerOffEvent;
  52. import it.tidalwave.ui.core.message.PowerOnEvent;
  53. import it.tidalwave.ui.javafx.JavaFXApplicationWithSplash;
  54. import it.tidalwave.ui.javafx.NodeAndDelegate;
  55. import it.tidalwave.ui.javafx.impl.util.JavaFXSafeComponentBuilder;
  56. import it.tidalwave.ui.javafx.impl.util.JavaFXSafeProxy;
  57. import it.tidalwave.ui.javafx.impl.util.JavaFXSafeProxy.Proxied;
  58. import jfxtras.styles.jmetro.JMetro;
  59. import jfxtras.styles.jmetro.Style;
  60. import org.slf4j.Logger;
  61. import org.slf4j.LoggerFactory;
  62. import it.tidalwave.util.Key;
  63. import it.tidalwave.util.PreferencesHandler;
  64. import it.tidalwave.util.TypeSafeMap;
  65. import it.tidalwave.util.annotation.VisibleForTesting;
  66. import it.tidalwave.messagebus.MessageBus;
  67. import lombok.AccessLevel;
  68. import lombok.Getter;
  69. import lombok.RequiredArgsConstructor;
  70. import lombok.With;
  71. import static java.util.stream.Collectors.*;
  72. import static it.tidalwave.util.CollectionUtils.concat;
  73. import static it.tidalwave.util.FunctionalCheckedExceptionWrappers.*;
  74. import static it.tidalwave.util.ShortNames.*;
  75. import static lombok.AccessLevel.PRIVATE;

  76. /***************************************************************************************************************************************************************
  77.  *
  78.  * A base class for all variants of JavaFX applications with Spring.
  79.  *
  80.  * @author  Fabrizio Giudici
  81.  *
  82.  **************************************************************************************************************************************************************/
  83. public abstract class AbstractJavaFXSpringApplication extends JavaFXApplicationWithSplash
  84.   {
  85.     /** Configures the JMetro style, light mode. @since 3.0-ALPHA-1 */
  86.     public static final Consumer<Scene> STYLE_METRO_LIGHT = scene ->  new JMetro(Style.LIGHT).setScene(scene);

  87.     /** Configures the JMetro style, dark mode.  @since 3.0-ALPHA-1 */
  88.     public static final Consumer<Scene> STYLE_METRO_DARK = scene ->  new JMetro(Style.DARK).setScene(scene);

  89.     /***********************************************************************************************************************************************************
  90.      * The initialisation parameters to pass to {@link #launch(Class, InitParameters)}.
  91.      * @since   1.1-ALPHA-6
  92.      **********************************************************************************************************************************************************/
  93.     @RequiredArgsConstructor(access = PRIVATE) @With
  94.     public static class InitParameters
  95.       {
  96.         @Nonnull
  97.         private final String[] args;

  98.         @Nonnull
  99.         private final String applicationName;

  100.         @Nonnull
  101.         private final String logFolderPropertyName;

  102.         private final boolean implicitExit;

  103.         @Nonnull
  104.         private final TypeSafeMap propertyMap;

  105.         @Nonnull
  106.         private final List<Consumer<Scene>> sceneFinalizers;

  107.         @Nonnull
  108.         public <T> InitParameters withProperty (@Nonnull final Key<T> key, @Nonnull final T value)
  109.           {
  110.             return new InitParameters(args, applicationName, logFolderPropertyName, implicitExit, propertyMap.with(key, value), sceneFinalizers);
  111.           }

  112.         @Nonnull
  113.         public InitParameters withSceneFinalizer (@Nonnull final Consumer<Scene> stageFinalizer)
  114.           {
  115.             return new InitParameters(args, applicationName, logFolderPropertyName, implicitExit, propertyMap, concat(sceneFinalizers, stageFinalizer));
  116.           }

  117.         public void validate()
  118.           {
  119.             requireNotEmpty(applicationName, "applicationName");
  120.             requireNotEmpty(logFolderPropertyName, "logFolderPropertyName");
  121.           }

  122.         private void requireNotEmpty (@CheckForNull final String name, @Nonnull final String message)
  123.           {
  124.             if (name == null || name.isEmpty())
  125.               {
  126.                 throw new IllegalArgumentException(message);
  127.               }
  128.           }
  129.       }

  130.     public static final String APPLICATION_MESSAGE_BUS_BEAN_NAME = "applicationMessageBus";

  131.     // Don't use Slf4j and its static logger - give Main a chance to initialize things
  132.     private final Logger log = LoggerFactory.getLogger(AbstractJavaFXSpringApplication.class);

  133.     private ConfigurableApplicationContext applicationContext;

  134.     private Optional<MessageBus> messageBus = Optional.empty();

  135.     @Getter(AccessLevel.PACKAGE) @Nonnull
  136.     private final ExecutorService executorService = Executors.newSingleThreadExecutor();

  137.     private static InitParameters initParameters;

  138.     /***********************************************************************************************************************************************************
  139.      * Launches the application.
  140.      * @param   appClass          the class of the application to instantiate
  141.      * @param   initParameters    the initialisation parameters
  142.      **********************************************************************************************************************************************************/
  143.     @SuppressFBWarnings("DM_EXIT")
  144.     public static void launch (@Nonnull final Class<? extends Application> appClass, @Nonnull final InitParameters initParameters)
  145.       {
  146.         try
  147.           {
  148.             initParameters.validate();
  149.             System.setProperty(PreferencesHandler.PROP_APP_NAME, initParameters.applicationName);
  150.             Platform.setImplicitExit(initParameters.implicitExit);
  151.             final var preferencesHandler = PreferencesHandler.getInstance();
  152.             initParameters.propertyMap.forEach(preferencesHandler::setProperty);
  153.             System.setProperty(initParameters.logFolderPropertyName, preferencesHandler.getLogFolder().toAbsolutePath().toString());
  154.             JavaFXSafeProxy.setLogDelegateInvocations(initParameters.propertyMap.getOptional(K_LOG_DELEGATE_INVOCATIONS).orElse(false));
  155.             AbstractJavaFXSpringApplication.initParameters = initParameters;
  156.             Application.launch(appClass, initParameters.args);
  157.           }
  158.         catch (Throwable t)
  159.           {
  160.             // Don't use logging facilities here, they could be not initialized
  161.             t.  printStackTrace();
  162.             System.exit(-1);
  163.           }
  164.       }

  165.     /***********************************************************************************************************************************************************
  166.      * {@return an empty set of parameters} to populate and pass to {@link #launch(Class, InitParameters)}
  167.      * @since   1.1-ALPHA-6
  168.      **********************************************************************************************************************************************************/
  169.     @Nonnull
  170.     protected static InitParameters params()
  171.       {
  172.         return new InitParameters(new String[0], "", "", true, TypeSafeMap.newInstance(), List.of());
  173.       }

  174.     /***********************************************************************************************************************************************************
  175.      *
  176.      **********************************************************************************************************************************************************/
  177.     @Override @Nonnull
  178.     protected NodeAndDelegate<?> createParent()
  179.             throws IOException
  180.       {
  181.         return NodeAndDelegate.load(getClass(), applicationFxml);
  182.       }

  183.     /***********************************************************************************************************************************************************
  184.      *
  185.      **********************************************************************************************************************************************************/
  186.     @Override
  187.     protected void initializeInBackground()
  188.       {
  189.         log.info("initializeInBackground()");

  190.         try
  191.           {
  192.             logProperties();
  193.             // TODO: workaround for NWRCA-41
  194.             System.setProperty("it.tidalwave.util.spring.ClassScanner.basePackages", "it");
  195.             applicationContext = createApplicationContext();
  196.             applicationContext.registerShutdownHook(); // this actually seems not working, onClosing() does

  197.             if (applicationContext.containsBean(APPLICATION_MESSAGE_BUS_BEAN_NAME))
  198.               {
  199.                 messageBus = Optional.of(applicationContext.getBean(APPLICATION_MESSAGE_BUS_BEAN_NAME, MessageBus.class));
  200.               }
  201.           }
  202.         catch (Throwable t)
  203.           {
  204.             log.error("", t);
  205.           }
  206.       }

  207.     /***********************************************************************************************************************************************************
  208.      * {@return a created application context.}
  209.      **********************************************************************************************************************************************************/
  210.     @Nonnull
  211.     protected abstract ConfigurableApplicationContext createApplicationContext();

  212.     /***********************************************************************************************************************************************************
  213.      *
  214.      **********************************************************************************************************************************************************/
  215.     @Override
  216.     protected Scene createScene (@Nonnull final Parent parent)
  217.       {
  218.         final var scene = super.createScene(parent);
  219.         initParameters.sceneFinalizers.forEach(f -> f.accept(scene));
  220.         return scene;
  221.       }

  222.     /***********************************************************************************************************************************************************
  223.      * {@inheritDoc}
  224.      **********************************************************************************************************************************************************/
  225.     @Override
  226.     protected final void onStageCreated (@Nonnull final Stage stage, @Nonnull final NodeAndDelegate<?> applicationNad)
  227.       {
  228.         assert Platform.isFxApplicationThread();
  229.         JavaFXSafeComponentBuilder.getJavaFxBinder().setMainWindow(stage);
  230.         onStageCreated2(applicationNad);
  231.       }

  232.     /***********************************************************************************************************************************************************
  233.      * This method is separated to make testing simpler (it does not depend on JavaFX stuff).
  234.      * @param   applicationNad
  235.      **********************************************************************************************************************************************************/
  236.     @VisibleForTesting final void onStageCreated2 (@Nonnull final NodeAndDelegate<?> applicationNad)
  237.       {
  238.         Objects.requireNonNull(applicationContext, "applicationContext is null");
  239.         final var delegate = applicationNad.getDelegate();
  240.         final var actualDelegate = getActualDelegate(delegate);
  241.         log.info("Application presentation delegate: {} --- actual: {}", delegate, actualDelegate);

  242.         if (actualDelegate.getClass().getAnnotation(PresentationAssembler.class) != null)
  243.           {
  244.             callAssemble(actualDelegate);
  245.           }

  246.         callPresentationAssemblers();
  247.         executorService.execute(() ->
  248.           {
  249.             onStageCreated(applicationContext);
  250.             messageBus.ifPresent(mb -> mb.publish(new PowerOnEvent()));
  251.           });
  252.       }

  253.     /***********************************************************************************************************************************************************
  254.      * Invoked when the {@link Stage} is created and the {@link ApplicationContext} has been initialized. Typically, the main class overrides this, retrieves
  255.      * a reference to the main controller and boots it. This method is executed in a background thread.
  256.      * @param   applicationContext  the application context
  257.      **********************************************************************************************************************************************************/
  258.     protected void onStageCreated (@Nonnull final ApplicationContext applicationContext)
  259.       {
  260.       }

  261.     /***********************************************************************************************************************************************************
  262.      * {@inheritDoc}
  263.      **********************************************************************************************************************************************************/
  264.     @Override
  265.     protected void onClosing()
  266.       {
  267.         messageBus.ifPresent(mb -> mb.publish(new PowerOffEvent()));
  268.         applicationContext.close();
  269.       }

  270.     /***********************************************************************************************************************************************************
  271.      * Finds all classes annotated with {@link PresentationAssembler} and invokes methods annotated with {@link Assemble}.
  272.      **********************************************************************************************************************************************************/
  273.     private void callPresentationAssemblers()
  274.       {
  275.         applicationContext.getBeansWithAnnotation(PresentationAssembler.class).values().forEach(this::callAssemble);
  276.       }

  277.     /***********************************************************************************************************************************************************
  278.      * Call a method annotated with {@link Assemble} in the given object.
  279.      * @param     assembler       the assembler
  280.      **********************************************************************************************************************************************************/
  281.     private void callAssemble (@Nonnull final Object assembler)
  282.       {
  283.         log.info("Calling presentation assembler: {}", assembler);
  284.         Arrays.stream(assembler.getClass().getDeclaredMethods())
  285.               .filter(_p(m -> m.getDeclaredAnnotation(Assemble.class) != null))
  286.               .forEach(_c(m -> invokeInjecting(m, assembler, this::resolveBean)));
  287.       }

  288.     /***********************************************************************************************************************************************************
  289.      * Instantiates an object of the given class performing dependency injections through the constructor.
  290.      * TODO: possibly replace with a Spring utility doing method injection.
  291.      * @throws        RuntimeException if something fails
  292.      **********************************************************************************************************************************************************/
  293.     private void invokeInjecting (@Nonnull final Method method, @Nonnull final Object object, @Nonnull final Function<Class<?>, Object> beanFactory)
  294.       {
  295.         try
  296.           {
  297.             final var parameters = Arrays.stream(method.getParameterTypes()).map(beanFactory).collect(toList());
  298.             log.info(">>>> calling {}({})", method.getName(), shortIds(parameters));
  299.             method.invoke(object, parameters.toArray());
  300.           }
  301.         catch (IllegalAccessException | InvocationTargetException e)
  302.           {
  303.             throw new RuntimeException(e);
  304.           }
  305.       }

  306.     /***********************************************************************************************************************************************************
  307.      *
  308.      **********************************************************************************************************************************************************/
  309.     @Nonnull
  310.     private <T> T resolveBean (@Nonnull final Class<T> type)
  311.       {
  312.         return type.cast(Optional.ofNullable(JavaFXSafeComponentBuilder.BEANS.get(type)).orElseGet(() -> applicationContext.getBean(type)));
  313.       }

  314.     /***********************************************************************************************************************************************************
  315.      *
  316.      **********************************************************************************************************************************************************/
  317.     @Nonnull
  318.     private static Object getActualDelegate (@Nonnull final Object delegate)
  319.       {
  320.         return delegate instanceof Proxied ? ((Proxied)delegate).__getProxiedObject() : delegate;
  321.       }

  322.     /***********************************************************************************************************************************************************
  323.      * Logs all the system properties.
  324.      **********************************************************************************************************************************************************/
  325.     private void logProperties()
  326.       {
  327.         for (final var e : new TreeMap<>(System.getProperties()).entrySet())
  328.           {
  329.             log.debug("{}: {}", e.getKey(), e.getValue());
  330.           }
  331.       }
  332.   }