JavaFXSafeProxy.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.util;

  27. import java.lang.reflect.InvocationHandler;
  28. import java.lang.reflect.Method;
  29. import java.lang.reflect.Proxy;
  30. import jakarta.annotation.Nonnull;
  31. import java.util.concurrent.CountDownLatch;
  32. import java.util.concurrent.atomic.AtomicReference;
  33. import javafx.application.Platform;
  34. import org.slf4j.LoggerFactory;
  35. import lombok.Getter;
  36. import lombok.RequiredArgsConstructor;
  37. import lombok.Setter;
  38. import lombok.extern.slf4j.Slf4j;
  39. import static java.util.Arrays.asList;
  40. import static it.tidalwave.util.CollectionUtils.concat;

  41. /***************************************************************************************************************************************************************
  42.  *
  43.  * An {@link InvocationHandler} that safely wraps all method calls with {@link Platform#runLater(Runnable)}. The caller
  44.  * is not blocked if the method is declared as {@code void}; it is blocked otherwise, so it can immediately retrieve
  45.  * the result.
  46.  *
  47.  * This behaviour is required by {@link it.tidalwave.ui.javafx.NodeAndDelegate#of(Class)} ()}.
  48.  *
  49.  * TODO: add support for aysnc returning a Future.
  50.  *
  51.  * @author  Fabrizio Giudici
  52.  *
  53.  **************************************************************************************************************************************************************/
  54. @RequiredArgsConstructor @Slf4j
  55. public class JavaFXSafeProxy implements InvocationHandler
  56.   {
  57.     /***********************************************************************************************************************************************************
  58.      * An auxiliary interface that is always injected to the proxy, allowing to retrive the class of the proxied object.
  59.      **********************************************************************************************************************************************************/
  60.     public interface Proxied
  61.       {
  62.         /** {@return the class of the proxied object}. */
  63.         @Nonnull
  64.         public Class<?> __getProxiedClass();
  65.       }

  66.     @Getter @Setter
  67.     private static boolean logDelegateInvocations = false;

  68.     @Nonnull @Getter @Setter
  69.     private Object delegate;

  70.     /***********************************************************************************************************************************************************
  71.      *
  72.      **********************************************************************************************************************************************************/
  73.     @Nonnull @SuppressWarnings("unchecked")
  74.     public static <T> T of (@Nonnull final T target, @Nonnull final Class<?>[] interfaces)
  75.       {
  76.         final var augmentedInterfaces = concat(asList(interfaces), Proxied.class).toArray(Class<?>[]::new);
  77.         final var contextClassLoader = Thread.currentThread().getContextClassLoader();
  78.         return (T)Proxy.newProxyInstance(contextClassLoader, augmentedInterfaces, new JavaFXSafeProxy(target));
  79.       }

  80.     /***********************************************************************************************************************************************************
  81.      * {@inheritDoc}
  82.      **********************************************************************************************************************************************************/
  83.     @Override
  84.     public Object invoke (@Nonnull final Object proxy, @Nonnull final Method method, @Nonnull final Object[] args)
  85.       throws Throwable
  86.       {
  87.         if ("__getProxiedClass".equals(method.getName()))
  88.           {
  89.             return delegate.getClass();
  90.           }

  91.         final var result = new AtomicReference<>();
  92.         final var throwable = new AtomicReference<Throwable>();
  93.         final var waitForReturn = new CountDownLatch(1);

  94.         JavaFXSafeRunner.runSafely(() ->
  95.           {
  96.             try
  97.               {
  98.                 if (logDelegateInvocations)
  99.                   {
  100.                     logInvocation(delegate.getClass(), method, args);
  101.                   }

  102.                 result.set(method.invoke(delegate, args));
  103.               }
  104.             catch (Throwable t)
  105.               {
  106.                 throwable.set(t);
  107.                 log.error("Exception while calling JavaFX", t);
  108.               }
  109.             finally
  110.               {
  111.                 waitForReturn.countDown();
  112.               }
  113.           });

  114.         log.trace(">>>> waiting for method completion");
  115.         waitForReturn.await();

  116.         // This is probably useless - void methods return asynchronously
  117.         if (throwable.get() != null)
  118.           {
  119.             throw throwable.get();
  120.           }

  121.         return method.getReturnType().equals(void.class) ? null : result.get();
  122.       }

  123.     /***********************************************************************************************************************************************************
  124.      *
  125.      **********************************************************************************************************************************************************/
  126.     private static void logInvocation (@Nonnull final Class<?> clazz, @Nonnull final Method method, @Nonnull final Object[] args)
  127.       {
  128.         final var logger = LoggerFactory.getLogger(clazz);

  129.         if (logger.isDebugEnabled())
  130.           {
  131.             final var builder = new StringBuilder();
  132.             builder.append(method.getName());
  133.             builder.append("(");
  134.             var separator = "";

  135.             for (final Object arg : args)
  136.               {
  137.                 builder.append(separator);
  138.                 builder.append(arg != null ? arg.toString() : null);
  139.                 separator = ", ";
  140.               }

  141.             builder.append(")");
  142.             logger.debug(builder.toString());
  143.           }
  144.       }
  145.   }