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 it.tidalwave.ui.javafx.impl.DefaultNodeAndDelegate;
  35. import org.slf4j.LoggerFactory;
  36. import lombok.Getter;
  37. import lombok.RequiredArgsConstructor;
  38. import lombok.Setter;
  39. import lombok.extern.slf4j.Slf4j;
  40. import static java.util.Arrays.asList;
  41. import static it.tidalwave.util.CollectionUtils.concat;

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

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

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

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

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

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

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

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

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

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

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

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

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

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