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 javax.annotation.CheckForNull;
  28. import jakarta.annotation.Nonnull;
  29. import java.util.Objects;
  30. import java.util.Optional;
  31. import java.util.TreeMap;
  32. import java.util.concurrent.ExecutorService;
  33. import java.util.concurrent.Executors;
  34. import java.io.IOException;
  35. import javafx.stage.Stage;
  36. import javafx.application.Application;
  37. import javafx.application.Platform;
  38. import org.springframework.context.ApplicationContext;
  39. import org.springframework.context.ConfigurableApplicationContext;
  40. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  41. import it.tidalwave.ui.core.message.PowerOffEvent;
  42. import it.tidalwave.ui.core.message.PowerOnEvent;
  43. import it.tidalwave.ui.javafx.ApplicationPresentationAssembler;
  44. import it.tidalwave.ui.javafx.JavaFXApplicationWithSplash;
  45. import it.tidalwave.ui.javafx.JavaFXSafeProxyCreator;
  46. import it.tidalwave.ui.javafx.NodeAndDelegate;
  47. import it.tidalwave.ui.javafx.PresentationAssembler;
  48. import org.slf4j.Logger;
  49. import org.slf4j.LoggerFactory;
  50. import it.tidalwave.util.Key;
  51. import it.tidalwave.util.PreferencesHandler;
  52. import it.tidalwave.util.TypeSafeMap;
  53. import it.tidalwave.util.annotation.VisibleForTesting;
  54. import it.tidalwave.messagebus.MessageBus;
  55. import lombok.AccessLevel;
  56. import lombok.Getter;
  57. import lombok.RequiredArgsConstructor;
  58. import lombok.With;
  59. import static lombok.AccessLevel.PRIVATE;

  60. /***************************************************************************************************************************************************************
  61.  *
  62.  * A base class for all variants of JavaFX applications with Spring.
  63.  *
  64.  * @author  Fabrizio Giudici
  65.  *
  66.  **************************************************************************************************************************************************************/
  67. public abstract class AbstractJavaFXSpringApplication extends JavaFXApplicationWithSplash
  68.   {
  69.     /***********************************************************************************************************************************************************
  70.      * The initialisation parameters to pass to {@link #launch(Class, InitParameters)}.
  71.      * @since   1.1-ALPHA-6
  72.      **********************************************************************************************************************************************************/
  73.     @RequiredArgsConstructor(access = PRIVATE) @With
  74.     public static class InitParameters
  75.       {
  76.         @Nonnull
  77.         private final String[] args;

  78.         @Nonnull
  79.         private final String applicationName;

  80.         @Nonnull
  81.         private final String logFolderPropertyName;

  82.         private final boolean implicitExit;

  83.         @Nonnull
  84.         private final TypeSafeMap propertyMap;

  85.         @Nonnull
  86.         public <T> InitParameters withProperty (@Nonnull final Key<T> key, @Nonnull final T value)
  87.           {
  88.             return new InitParameters(args, applicationName, logFolderPropertyName, implicitExit, propertyMap.with(key, value));
  89.           }

  90.         public void validate()
  91.           {
  92.             requireNotEmpty(applicationName, "applicationName");
  93.             requireNotEmpty(logFolderPropertyName, "logFolderPropertyName");
  94.           }

  95.         private void requireNotEmpty (@CheckForNull final String name, @Nonnull final String message)
  96.           {
  97.             if (name == null || name.isEmpty())
  98.               {
  99.                 throw new IllegalArgumentException(message);
  100.               }
  101.           }
  102.       }

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

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

  106.     private ConfigurableApplicationContext applicationContext;

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

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

  110.     /***********************************************************************************************************************************************************
  111.      * Launches the application.
  112.      * @param   appClass          the class of the application to instantiate
  113.      * @param   initParameters    the initialisation parameters
  114.      **********************************************************************************************************************************************************/
  115.     @SuppressFBWarnings("DM_EXIT")
  116.     public static void launch (@Nonnull final Class<? extends Application> appClass, @Nonnull final InitParameters initParameters)
  117.       {
  118.         try
  119.           {
  120.             initParameters.validate();
  121.             System.setProperty(PreferencesHandler.PROP_APP_NAME, initParameters.applicationName);
  122.             Platform.setImplicitExit(initParameters.implicitExit);
  123.             final var preferencesHandler = PreferencesHandler.getInstance();
  124.             initParameters.propertyMap.forEach(preferencesHandler::setProperty);
  125.             System.setProperty(initParameters.logFolderPropertyName, preferencesHandler.getLogFolder().toAbsolutePath().toString());
  126.             Application.launch(appClass, initParameters.args);
  127.           }
  128.         catch (Throwable t)
  129.           {
  130.             // Don't use logging facilities here, they could be not initialized
  131.             t.printStackTrace();
  132.             System.exit(-1);
  133.           }
  134.       }

  135.     /***********************************************************************************************************************************************************
  136.      * {@return an empty set of parameters} to populate and pass to {@link #launch(Class, InitParameters)}
  137.      * @since   1.1-ALPHA-6
  138.      **********************************************************************************************************************************************************/
  139.     @Nonnull
  140.     protected static InitParameters params()
  141.       {
  142.         return new InitParameters(new String[0], "", "", true, TypeSafeMap.newInstance());
  143.       }

  144.     /***********************************************************************************************************************************************************
  145.      *
  146.      **********************************************************************************************************************************************************/
  147.     @Override @Nonnull
  148.     protected NodeAndDelegate<?> createParent()
  149.             throws IOException
  150.       {
  151.         return NodeAndDelegate.load(getClass(), applicationFxml);
  152.       }

  153.     /***********************************************************************************************************************************************************
  154.      *
  155.      **********************************************************************************************************************************************************/
  156.     @Override
  157.     protected void initializeInBackground()
  158.       {
  159.         log.info("initializeInBackground()");

  160.         try
  161.           {
  162.             logProperties();
  163.             // TODO: workaround for NWRCA-41
  164.             System.setProperty("it.tidalwave.util.spring.ClassScanner.basePackages", "it");
  165.             applicationContext = createApplicationContext();
  166.             applicationContext.registerShutdownHook(); // this actually seems not working, onClosing() does

  167.             if (applicationContext.getBeanFactory().containsBean(APPLICATION_MESSAGE_BUS_BEAN_NAME))
  168.               {
  169.                 messageBus = Optional.of(applicationContext.getBeanFactory().getBean(APPLICATION_MESSAGE_BUS_BEAN_NAME, MessageBus.class));
  170.               }
  171.           }
  172.         catch (Throwable t)
  173.           {
  174.             log.error("", t);
  175.           }
  176.       }

  177.     /***********************************************************************************************************************************************************
  178.      * {@return a created application context.}
  179.      **********************************************************************************************************************************************************/
  180.     @Nonnull
  181.     protected abstract ConfigurableApplicationContext createApplicationContext();

  182.     /***********************************************************************************************************************************************************
  183.      * {@inheritDoc}
  184.      **********************************************************************************************************************************************************/
  185.     @Override
  186.     protected final void onStageCreated (@Nonnull final Stage stage, @Nonnull final NodeAndDelegate<?> applicationNad)
  187.       {
  188.         assert Platform.isFxApplicationThread();
  189.         JavaFXSafeProxyCreator.getJavaFxBinder().setMainWindow(stage);
  190.         onStageCreated2(applicationNad);
  191.       }

  192.     /***********************************************************************************************************************************************************
  193.      * This method is separated to make testing simpler (it does not depend on JavaFX stuff).
  194.      * @param   applicationNad
  195.      **********************************************************************************************************************************************************/
  196.     @VisibleForTesting final void onStageCreated2 (@Nonnull final NodeAndDelegate<?> applicationNad)
  197.       {
  198.         final var delegate = applicationNad.getDelegate();

  199.         if (PresentationAssembler.class.isAssignableFrom(delegate.getClass()))
  200.           {
  201.             ((PresentationAssembler)delegate).assemble(applicationContext);
  202.           }

  203.         runApplicationAssemblers(applicationNad);
  204.         executorService.execute(() ->
  205.           {
  206.             onStageCreated(applicationContext);
  207.             messageBus.ifPresent(mb -> mb.publish(new PowerOnEvent()));
  208.           });
  209.       }

  210.     /***********************************************************************************************************************************************************
  211.      * Invoked when the {@link Stage} is created and the {@link ApplicationContext} has been initialized. Typically, the main class overrides this, retrieves
  212.      * a reference to the main controller and boots it. This method is executed in a background thread.
  213.      * @param   applicationContext  the application context
  214.      **********************************************************************************************************************************************************/
  215.     protected void onStageCreated (@Nonnull final ApplicationContext applicationContext)
  216.       {
  217.       }

  218.     /***********************************************************************************************************************************************************
  219.      * {@inheritDoc}
  220.      **********************************************************************************************************************************************************/
  221.     @Override
  222.     protected void onClosing()
  223.       {
  224.         messageBus.ifPresent(mb -> mb.publish(new PowerOffEvent()));
  225.         applicationContext.close();
  226.       }

  227.     /***********************************************************************************************************************************************************
  228.      *
  229.      **********************************************************************************************************************************************************/
  230.     private void runApplicationAssemblers (@Nonnull final NodeAndDelegate applicationNad)
  231.       {
  232.         Objects.requireNonNull(applicationContext, "applicationContext is null");
  233.         applicationContext.getBeansOfType(ApplicationPresentationAssembler.class).values().forEach(a -> a.assemble(applicationNad.getDelegate()));
  234.       }

  235.     /***********************************************************************************************************************************************************
  236.      * Logs all the system properties.
  237.      **********************************************************************************************************************************************************/
  238.     private void logProperties()
  239.       {
  240.         for (final var e : new TreeMap<>(System.getProperties()).entrySet())
  241.           {
  242.             log.debug("{}: {}", e.getKey(), e.getValue());
  243.           }
  244.       }
  245.   }