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 java.lang.reflect.Proxy;
  30. import javax.annotation.CheckForNull;
  31. import jakarta.annotation.Nonnull;
  32. import java.util.Arrays;
  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.Function;
  39. import java.io.IOException;
  40. import javafx.stage.Stage;
  41. import javafx.application.Application;
  42. import javafx.application.Platform;
  43. import org.springframework.context.ApplicationContext;
  44. import org.springframework.context.ConfigurableApplicationContext;
  45. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  46. import it.tidalwave.ui.core.annotation.PresentationAssembler;
  47. import it.tidalwave.ui.core.annotation.Assemble;
  48. import it.tidalwave.ui.core.message.PowerOffEvent;
  49. import it.tidalwave.ui.core.message.PowerOnEvent;
  50. import it.tidalwave.ui.javafx.JavaFXApplicationWithSplash;
  51. import it.tidalwave.ui.javafx.NodeAndDelegate;
  52. import it.tidalwave.ui.javafx.impl.DefaultNodeAndDelegate;
  53. import it.tidalwave.ui.javafx.impl.util.JavaFXSafeProxy.Proxied;
  54. import org.slf4j.Logger;
  55. import org.slf4j.LoggerFactory;
  56. import it.tidalwave.util.Key;
  57. import it.tidalwave.util.PreferencesHandler;
  58. import it.tidalwave.util.TypeSafeMap;
  59. import it.tidalwave.util.annotation.VisibleForTesting;
  60. import it.tidalwave.messagebus.MessageBus;
  61. import lombok.AccessLevel;
  62. import lombok.Getter;
  63. import lombok.RequiredArgsConstructor;
  64. import lombok.With;
  65. import static java.util.stream.Collectors.*;
  66. import static it.tidalwave.util.FunctionalCheckedExceptionWrappers.*;
  67. import static it.tidalwave.util.ShortNames.*;
  68. import static lombok.AccessLevel.PRIVATE;

  69. /***************************************************************************************************************************************************************
  70.  *
  71.  * A base class for all variants of JavaFX applications with Spring.
  72.  *
  73.  * @author  Fabrizio Giudici
  74.  *
  75.  **************************************************************************************************************************************************************/
  76. public abstract class AbstractJavaFXSpringApplication extends JavaFXApplicationWithSplash
  77.   {
  78.     /***********************************************************************************************************************************************************
  79.      * The initialisation parameters to pass to {@link #launch(Class, InitParameters)}.
  80.      * @since   1.1-ALPHA-6
  81.      **********************************************************************************************************************************************************/
  82.     @RequiredArgsConstructor(access = PRIVATE) @With
  83.     public static class InitParameters
  84.       {
  85.         @Nonnull
  86.         private final String[] args;

  87.         @Nonnull
  88.         private final String applicationName;

  89.         @Nonnull
  90.         private final String logFolderPropertyName;

  91.         private final boolean implicitExit;

  92.         @Nonnull
  93.         private final TypeSafeMap propertyMap;

  94.         @Nonnull
  95.         public <T> InitParameters withProperty (@Nonnull final Key<T> key, @Nonnull final T value)
  96.           {
  97.             return new InitParameters(args, applicationName, logFolderPropertyName, implicitExit, propertyMap.with(key, value));
  98.           }

  99.         public void validate()
  100.           {
  101.             requireNotEmpty(applicationName, "applicationName");
  102.             requireNotEmpty(logFolderPropertyName, "logFolderPropertyName");
  103.           }

  104.         private void requireNotEmpty (@CheckForNull final String name, @Nonnull final String message)
  105.           {
  106.             if (name == null || name.isEmpty())
  107.               {
  108.                 throw new IllegalArgumentException(message);
  109.               }
  110.           }
  111.       }

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

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

  115.     private ConfigurableApplicationContext applicationContext;

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

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

  119.     /***********************************************************************************************************************************************************
  120.      * Launches the application.
  121.      * @param   appClass          the class of the application to instantiate
  122.      * @param   initParameters    the initialisation parameters
  123.      **********************************************************************************************************************************************************/
  124.     @SuppressFBWarnings("DM_EXIT")
  125.     public static void launch (@Nonnull final Class<? extends Application> appClass, @Nonnull final InitParameters initParameters)
  126.       {
  127.         try
  128.           {
  129.             initParameters.validate();
  130.             System.setProperty(PreferencesHandler.PROP_APP_NAME, initParameters.applicationName);
  131.             Platform.setImplicitExit(initParameters.implicitExit);
  132.             final var preferencesHandler = PreferencesHandler.getInstance();
  133.             initParameters.propertyMap.forEach(preferencesHandler::setProperty);
  134.             System.setProperty(initParameters.logFolderPropertyName, preferencesHandler.getLogFolder().toAbsolutePath().toString());
  135.             DefaultNodeAndDelegate.setLogDelegateInvocations(initParameters.propertyMap.getOptional(K_LOG_DELEGATE_INVOCATIONS).orElse(false));
  136.             Application.launch(appClass, initParameters.args);
  137.           }
  138.         catch (Throwable t)
  139.           {
  140.             // Don't use logging facilities here, they could be not initialized
  141.             t.  printStackTrace();
  142.             System.exit(-1);
  143.           }
  144.       }

  145.     /***********************************************************************************************************************************************************
  146.      * {@return an empty set of parameters} to populate and pass to {@link #launch(Class, InitParameters)}
  147.      * @since   1.1-ALPHA-6
  148.      **********************************************************************************************************************************************************/
  149.     @Nonnull
  150.     protected static InitParameters params()
  151.       {
  152.         return new InitParameters(new String[0], "", "", true, TypeSafeMap.newInstance());
  153.       }

  154.     /***********************************************************************************************************************************************************
  155.      *
  156.      **********************************************************************************************************************************************************/
  157.     @Override @Nonnull
  158.     protected NodeAndDelegate<?> createParent()
  159.             throws IOException
  160.       {
  161.         return NodeAndDelegate.load(getClass(), applicationFxml);
  162.       }

  163.     /***********************************************************************************************************************************************************
  164.      *
  165.      **********************************************************************************************************************************************************/
  166.     @Override
  167.     protected void initializeInBackground()
  168.       {
  169.         log.info("initializeInBackground()");

  170.         try
  171.           {
  172.             logProperties();
  173.             // TODO: workaround for NWRCA-41
  174.             System.setProperty("it.tidalwave.util.spring.ClassScanner.basePackages", "it");
  175.             applicationContext = createApplicationContext();
  176.             applicationContext.registerShutdownHook(); // this actually seems not working, onClosing() does

  177.             if (applicationContext.containsBean(APPLICATION_MESSAGE_BUS_BEAN_NAME))
  178.               {
  179.                 messageBus = Optional.of(applicationContext.getBean(APPLICATION_MESSAGE_BUS_BEAN_NAME, MessageBus.class));
  180.               }
  181.           }
  182.         catch (Throwable t)
  183.           {
  184.             log.error("", t);
  185.           }
  186.       }

  187.     /***********************************************************************************************************************************************************
  188.      * {@return a created application context.}
  189.      **********************************************************************************************************************************************************/
  190.     @Nonnull
  191.     protected abstract ConfigurableApplicationContext createApplicationContext();

  192.     /***********************************************************************************************************************************************************
  193.      * {@inheritDoc}
  194.      **********************************************************************************************************************************************************/
  195.     @Override
  196.     protected final void onStageCreated (@Nonnull final Stage stage, @Nonnull final NodeAndDelegate<?> applicationNad)
  197.       {
  198.         assert Platform.isFxApplicationThread();
  199.         DefaultNodeAndDelegate.getJavaFxBinder().setMainWindow(stage);
  200.         onStageCreated2(applicationNad);
  201.       }

  202.     /***********************************************************************************************************************************************************
  203.      * This method is separated to make testing simpler (it does not depend on JavaFX stuff).
  204.      * @param   applicationNad
  205.      **********************************************************************************************************************************************************/
  206.     @VisibleForTesting final void onStageCreated2 (@Nonnull final NodeAndDelegate<?> applicationNad)
  207.       {
  208.         Objects.requireNonNull(applicationContext, "applicationContext is null");
  209.         final var delegate = applicationNad.getDelegate();
  210.         final var actualClass = getActualClass(delegate);
  211.         log.info("Application presentation delegate: {}", delegate);

  212.         if (!actualClass.equals(delegate.getClass()))
  213.           {
  214.             log.info(">>>> delegate class {} is a proxy of {}", delegate.getClass().getName(), actualClass.getName());
  215.           }

  216.         if (actualClass.getAnnotation(PresentationAssembler.class) != null)
  217.           {
  218.             callAssemble(delegate);
  219.           }

  220.         callPresentationAssemblers();
  221.         executorService.execute(() ->
  222.           {
  223.             onStageCreated(applicationContext);
  224.             messageBus.ifPresent(mb -> mb.publish(new PowerOnEvent()));
  225.           });
  226.       }

  227.     /***********************************************************************************************************************************************************
  228.      * Invoked when the {@link Stage} is created and the {@link ApplicationContext} has been initialized. Typically, the main class overrides this, retrieves
  229.      * a reference to the main controller and boots it. This method is executed in a background thread.
  230.      * @param   applicationContext  the application context
  231.      **********************************************************************************************************************************************************/
  232.     protected void onStageCreated (@Nonnull final ApplicationContext applicationContext)
  233.       {
  234.       }

  235.     /***********************************************************************************************************************************************************
  236.      * {@inheritDoc}
  237.      **********************************************************************************************************************************************************/
  238.     @Override
  239.     protected void onClosing()
  240.       {
  241.         messageBus.ifPresent(mb -> mb.publish(new PowerOffEvent()));
  242.         applicationContext.close();
  243.       }

  244.     /***********************************************************************************************************************************************************
  245.      * Finds all classes annotated with {@link PresentationAssembler} and invokes methods annotated with {@link Assemble}.
  246.      **********************************************************************************************************************************************************/
  247.     private void callPresentationAssemblers()
  248.       {
  249.         applicationContext.getBeansWithAnnotation(PresentationAssembler.class).values().forEach(this::callAssemble);
  250.       }

  251.     /***********************************************************************************************************************************************************
  252.      * Call a method annotated with {@link Assemble} in the given object. Since the object is likely to be a dynamic proxy, and proxies don't carry the
  253.      * annotation of proxied classes, the proxied class is the one being probed for the annotation.
  254.      * @param     delegate          the presentation delegate
  255.      **********************************************************************************************************************************************************/
  256.     private void callAssemble (@Nonnull final Object delegate)
  257.       {
  258.         log.info("Calling presentation assembler: {}", delegate);
  259.         final var actualClass = getActualClass(delegate);
  260.         Arrays.stream(actualClass.getDeclaredMethods())
  261.               .filter(_p(m -> m.getDeclaredAnnotation(Assemble.class) != null))
  262.               .forEach(_c(m -> invokeInjecting(delegate.getClass().getDeclaredMethod(m.getName(), m.getParameterTypes()), delegate, this::resolveBean)));
  263.       }

  264.     /***********************************************************************************************************************************************************
  265.      * Instantiates an object of the given class performing dependency injections through the constructor.
  266.      * TODO: possibly replace with a Spring utility doing method injection.
  267.      * @throws        RuntimeException if something fails
  268.      **********************************************************************************************************************************************************/
  269.     private void invokeInjecting (@Nonnull final Method method, @Nonnull final Object object, @Nonnull final Function<Class<?>, Object> beanFactory)
  270.       {
  271.         try
  272.           {
  273.             final var parameters = Arrays.stream(method.getParameterTypes()).map(beanFactory).collect(toList());
  274.             log.info(">>>> calling {}({})", method.getName(), shortIds(parameters));
  275.             method.invoke(object, parameters.toArray());
  276.           }
  277.         catch (IllegalAccessException | InvocationTargetException e)
  278.           {
  279.             throw new RuntimeException(e);
  280.           }
  281.       }

  282.     /***********************************************************************************************************************************************************
  283.      *
  284.      **********************************************************************************************************************************************************/
  285.     @Nonnull
  286.     private <T> T resolveBean (@Nonnull final Class<T> type)
  287.       {
  288.         return type.cast(Optional.ofNullable(DefaultNodeAndDelegate.BEANS.get(type)).orElseGet(() -> applicationContext.getBean(type)));
  289.       }

  290.     /***********************************************************************************************************************************************************
  291.      * {@return the class of the given object or its proxied class if it's a proxy}.
  292.      * @param   object      the object
  293.      **********************************************************************************************************************************************************/
  294.     @Nonnull
  295.     private Class<?> getActualClass (@Nonnull final Object object)
  296.       {
  297.         final var clazz = object.getClass();
  298.         return !Proxy.isProxyClass(clazz) ? clazz : ((Proxied)object).__getProxiedClass();
  299.       }

  300.     /***********************************************************************************************************************************************************
  301.      * Logs all the system properties.
  302.      **********************************************************************************************************************************************************/
  303.     private void logProperties()
  304.       {
  305.         for (final var e : new TreeMap<>(System.getProperties()).entrySet())
  306.           {
  307.             log.debug("{}: {}", e.getKey(), e.getValue());
  308.           }
  309.       }
  310.   }