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 javafx.application.Platform;
  32. import org.slf4j.LoggerFactory;
  33. import lombok.Getter;
  34. import lombok.RequiredArgsConstructor;
  35. import lombok.Setter;
  36. import lombok.extern.slf4j.Slf4j;
  37. import static it.tidalwave.ui.javafx.impl.util.JavaFXSafeRunner.runSafelyAndWait;
  38. import static java.util.Arrays.asList;
  39. import static it.tidalwave.util.CollectionUtils.concat;

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

  65.     @Getter @Setter
  66.     private static boolean logDelegateInvocations;

  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 ("__getProxiedObject".equals(method.getName()))
  87.           {
  88.             return delegate;
  89.           }

  90.         final var result = runSafelyAndWait(() ->
  91.           {
  92.             if (logDelegateInvocations)
  93.               {
  94.                 logInvocation(delegate.getClass(), method, args);
  95.               }

  96.             return method.invoke(delegate, args);
  97.           });

  98.         return method.getReturnType().equals(void.class) ? null : result;
  99.       }

  100.     /***********************************************************************************************************************************************************
  101.      *
  102.      **********************************************************************************************************************************************************/
  103.     private static void logInvocation (@Nonnull final Class<?> clazz, @Nonnull final Method method, @Nonnull final Object[] args)
  104.       {
  105.         final var logger = LoggerFactory.getLogger(clazz);

  106.         if (logger.isDebugEnabled())
  107.           {
  108.             final var builder = new StringBuilder();
  109.             builder.append(method.getName());
  110.             builder.append("(");
  111.             var separator = "";

  112.             for (final Object arg : args)
  113.               {
  114.                 builder.append(separator);
  115.                 builder.append(arg != null ? arg.toString() : null);
  116.                 separator = ", ";
  117.               }

  118.             builder.append(")");
  119.             logger.debug(builder.toString());
  120.           }
  121.       }
  122.   }