View Javadoc
1   /*
2    * *************************************************************************************************************************************************************
3    *
4    * SteelBlue: DCI User Interfaces
5    * http://tidalwave.it/projects/steelblue
6    *
7    * Copyright (C) 2015 - 2024 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;
27  
28  import java.lang.reflect.Proxy;
29  import javax.annotation.Nonnull;
30  import java.util.HashMap;
31  import java.util.Map;
32  import java.util.concurrent.CountDownLatch;
33  import java.util.concurrent.Executor;
34  import java.util.concurrent.TimeUnit;
35  import java.util.concurrent.atomic.AtomicReference;
36  import java.io.IOException;
37  import javafx.fxml.FXMLLoader;
38  import javafx.scene.Node;
39  import javafx.application.Platform;
40  import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
41  import it.tidalwave.role.ui.MenuBarModel;
42  import it.tidalwave.role.ui.ToolBarModel;
43  import it.tidalwave.role.ui.javafx.JavaFXBinder;
44  import it.tidalwave.role.ui.javafx.impl.DefaultJavaFXBinder;
45  import it.tidalwave.role.ui.javafx.impl.JavaFXMenuBarModel;
46  import it.tidalwave.role.ui.javafx.impl.JavaFXToolBarModel;
47  import it.tidalwave.role.ui.javafx.impl.util.JavaFXSafeProxy;
48  import lombok.Getter;
49  import lombok.extern.slf4j.Slf4j;
50  
51  /***************************************************************************************************************************************************************
52   *
53   * This facility class create a thread-safe proxy for the JavaFX delegate (controller). Thread-safe means that it can
54   * be called by any thread and the JavaFX UI related stuff will be safely invoked in the JavaFX UI Thread.
55   * It is usually used in this way:
56   *
57   * <pre>
58   * // This is a Spring bean
59   * public class JavaFxFooBarPresentation implements FooBarPresentation
60   *   {
61   *     private static final String FXML_URL = "/my/package/javafx/FooBar.fxml";
62   *
63   *     {@literal @}Inject
64   *     private FlowController flowController;
65   *
66   *     private final NodeAndDelegate nad = createNodeAndDelegate(getClass(), FXML_URL);
67   *
68   *     private final FooBarPresentation delegate = nad.getDelegate();
69   *
70   *     public void showUp()
71   *       {
72   *         flowController.doSomething(nad.getNode());
73   *       }
74   *
75   *     public void showData (final String data)
76   *       {
77   *         delegate.showData(data);
78   *       }
79   *   }
80   * </pre>
81   *
82   * The method {@link #createNodeAndDelegate(java.lang.Class, java.lang.String)} safely invokes the {@link FXMLLoader}
83   * and returns a {@link NodeAndDelegate} that contains both the visual {@link Node} and its delegate (controller).
84   *
85   * The latter is wrapped by a safe proxy that makes sure that any method invocation (such as {@code showData()} in the
86   * example is again executed in the JavaFX UI Thread. This means that the Presentation object methods can be invoked
87   * in any thread.
88   *
89   * For method returning {@code void}, the method invocation is asynchronous; that is, the caller is not blocked waiting
90   * for the method execution completion. If a return value is provided, the invocation is synchronous, and the caller
91   * will correctly wait the completion of the execution in order to get the result value.
92   *
93   * A typical JavaFX delegate (controller) looks like:
94   *
95   * <pre>
96   * // This is not a Spring bean - created by the FXMLLoader
97   * public class JavaFxFooBarPresentationDelegate implements FooBarPresentation
98   *   {
99   *     {@literal @}FXML
100  *     private Label label;
101  *
102  *     {@literal @}FXML
103  *     private Button button;
104  *
105  *     {@literal @}Inject // the only thing that can be injected, by means of JavaFXSafeProxyCreator
106  *     private JavaFxBinder binder;
107  *
108  *     {@literal @}Override
109  *     public void bind (final UserAction action)
110  *       {
111  *         binder.bind(button, action);
112  *       }
113  *
114  *     {@literal @}Override
115  *     public void showData (final String data)
116  *       {
117  *         label.setText(data);
118  *       }
119  *  }
120  * </pre>
121  *
122  * Not only all the methods invoked on the delegate are guaranteed to run in the JavaFX UI thread, but also its
123  * constructor, as per JavaFX requirements.
124  *
125  * A Presentation Delegate must not try to have dependency injection from Spring (for instance, by means of AOP),
126  * otherwise a deadlock could be triggered. Injection in constructors is safe.
127  *
128  * @author  Fabrizio Giudici
129  *
130  **************************************************************************************************************************************************************/
131 @Slf4j
132 public class JavaFXSafeProxyCreator
133   {
134     private static final String P_TIMEOUT = JavaFXSafeProxyCreator.class.getName() + ".initTimeout";
135     private static final int initializerTimeout = Integer.getInteger(P_TIMEOUT, 10);
136 
137     public static final Map<Class<?>, Object> BEANS = new HashMap<>();
138 
139     @Getter
140     private static final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
141 
142     @Getter
143     private static final JavaFXBinder javaFxBinder = new DefaultJavaFXBinder(executor);
144 
145     @Getter
146     private static final ToolBarModel toolBarModel = new JavaFXToolBarModel();
147 
148     @Getter
149     private static final MenuBarModel menuBarModel = new JavaFXMenuBarModel();
150 
151     static
152       {
153         executor.setWaitForTasksToCompleteOnShutdown(false);
154         executor.setThreadNamePrefix("javafxBinder-");
155         // Fix for STB-26
156         executor.setCorePoolSize(1);
157         executor.setMaxPoolSize(1);
158         executor.setQueueCapacity(10000);
159         BEANS.put(JavaFXBinder.class, javaFxBinder);
160         BEANS.put(Executor.class, executor);
161         BEANS.put(ToolBarModel.class, toolBarModel);
162         BEANS.put(MenuBarModel.class, menuBarModel);
163       }
164 
165     private JavaFXSafeProxyCreator () {}
166 
167     /***********************************************************************************************************************************************************
168      * Creates a {@link NodeAndDelegate} for the given presentation class. The FXML resource name is inferred by
169      * default, For instance, is the class is named {@code JavaFXFooBarPresentation}, the resource name is
170      * {@code FooBar.fxml} and searched in the same packages as the class.
171      *
172      * @see #createNodeAndDelegate(java.lang.Class, java.lang.String)
173      *
174      * @since 1.0-ALPHA-13
175      *
176      * @param   presentationClass   the class of the presentation for which the resources must be created.
177      **********************************************************************************************************************************************************/
178     @Nonnull
179     public static <T> NodeAndDelegate<T> createNodeAndDelegate (@Nonnull final Class<T> presentationClass)
180       {
181         final var resource = presentationClass.getSimpleName().replaceAll("^JavaFX", "")
182                                               .replaceAll("^JavaFx", "")
183                                               .replaceAll("Presentation$", "")
184                              + ".fxml";
185         return createNodeAndDelegate(presentationClass, resource);
186       }
187 
188     /***********************************************************************************************************************************************************
189      * Creates a {@link NodeAndDelegate} for the given presentation class.
190      *
191      * @param   presentationClass   the class of the presentation for which the resources must be created.
192      * @param   fxmlResourcePath    the path of the FXML resource
193      **********************************************************************************************************************************************************/
194     @Nonnull
195     public static <T> NodeAndDelegate<T> createNodeAndDelegate (@Nonnull final Class<T> presentationClass, @Nonnull final String fxmlResourcePath)
196       {
197         log.debug("createNodeAndDelegate({}, {})", presentationClass, fxmlResourcePath);
198 
199         final var latch = new CountDownLatch(1);
200         final var nad = new AtomicReference<NodeAndDelegate<T>>();
201         final var exception = new AtomicReference<RuntimeException>();
202 
203         if (Platform.isFxApplicationThread())
204           {
205             try
206               {
207                 return NodeAndDelegate.load(presentationClass, fxmlResourcePath);
208               }
209             catch (IOException e)
210               {
211                 exception.set(new RuntimeException(e));
212               }
213           }
214 
215         Platform.runLater(() ->
216           {
217             try
218               {
219                 nad.set(NodeAndDelegate.load(presentationClass, fxmlResourcePath));
220               }
221             catch (RuntimeException e)
222               {
223                 exception.set(e);
224               }
225             catch (Exception e)
226               {
227                 exception.set(new RuntimeException(e));
228               }
229 
230             latch.countDown();
231           });
232 
233         try
234           {
235             log.debug("Waiting for NodeAndDelegate initialisation in JavaFX thread...");
236             log.debug("If deadlocks and you need longer time with the debugger, set {} (current value: {})", P_TIMEOUT, initializerTimeout);
237             latch.await(initializerTimeout, TimeUnit.SECONDS); // FIXME
238           }
239         catch (InterruptedException e)
240           {
241             throw new RuntimeException(e);
242           }
243 
244         if (exception.get() != null)
245           {
246             throw exception.get();
247           }
248 
249         if (nad.get() == null)
250           {
251             final var message = String.format("Likely deadlock in the JavaFX Thread: couldn't create NodeAndDelegate: %s, %s",
252                                               presentationClass, fxmlResourcePath);
253             throw new RuntimeException(message);
254           }
255 
256         return nad.get();
257       }
258 
259     /***********************************************************************************************************************************************************
260      *
261      **********************************************************************************************************************************************************/
262     @Nonnull
263     public static <T> T createSafeProxy (@Nonnull final T target, final Class<?>[] interfaces)
264       {
265         return (T)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new JavaFXSafeProxy<>(target));
266       }
267   }