1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 package it.tidalwave.ui.javafx.impl;
27
28 import jakarta.annotation.Nonnull;
29 import java.util.HashMap;
30 import java.util.Map;
31 import java.util.concurrent.CountDownLatch;
32 import java.util.concurrent.Executor;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.atomic.AtomicReference;
35 import java.io.IOException;
36 import javafx.fxml.FXMLLoader;
37 import javafx.scene.Node;
38 import javafx.application.Platform;
39 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
40 import it.tidalwave.ui.javafx.JavaFXBinder;
41 import it.tidalwave.ui.javafx.JavaFXMenuBarControl;
42 import it.tidalwave.ui.javafx.JavaFXToolBarControl;
43 import it.tidalwave.ui.javafx.NodeAndDelegate;
44 import it.tidalwave.ui.javafx.impl.util.JavaFXSafeProxy;
45 import org.slf4j.LoggerFactory;
46 import it.tidalwave.util.PreferencesHandler;
47 import it.tidalwave.util.ReflectionUtils;
48 import lombok.Getter;
49 import lombok.RequiredArgsConstructor;
50 import lombok.Setter;
51 import lombok.extern.slf4j.Slf4j;
52
53
54
55
56
57
58
59
60 @RequiredArgsConstructor @Getter @Slf4j
61 public class DefaultNodeAndDelegate<T> implements NodeAndDelegate<T>
62 {
63 private static final String P_TIMEOUT = DefaultNodeAndDelegate.class.getName() + ".initTimeout";
64 private static final int INITIALIZER_TIMEOUT = Integer.getInteger(P_TIMEOUT, 10);
65
66 public static final Map<Class<?>, Object> BEANS = new HashMap<>();
67
68 @Getter
69 private static final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
70
71 @Getter
72 private static final JavaFXBinder javaFxBinder = new DefaultJavaFXBinder(executor);
73
74 @Getter
75 private static final JavaFXToolBarControl toolBarControl = new DefaultJavaFXToolBarControl();
76
77 @Getter
78 private static final JavaFXMenuBarControl menuBarControl = new DefaultJavaFXMenuBarControl();
79
80
81
82
83 @Getter @Setter
84 private static boolean logDelegateInvocations = false;
85
86 static
87 {
88 executor.setWaitForTasksToCompleteOnShutdown(false);
89 executor.setThreadNamePrefix("javafxBinder-");
90
91 executor.setCorePoolSize(1);
92 executor.setMaxPoolSize(1);
93 executor.setQueueCapacity(10000);
94 BEANS.put(JavaFXBinder.class, javaFxBinder);
95 BEANS.put(Executor.class, executor);
96 BEANS.put(JavaFXToolBarControl.class, toolBarControl);
97 BEANS.put(JavaFXMenuBarControl.class, menuBarControl);
98
99 BEANS.put(PreferencesHandler.class, PreferencesHandler.getInstance());
100 }
101
102 @Nonnull
103 private final Node node;
104
105 @Nonnull
106 private final T delegate;
107
108
109
110
111
112
113
114
115
116
117
118
119 @Nonnull
120 public static <T> NodeAndDelegate<T> of (@Nonnull final Class<T> presentationClass)
121 {
122 final var resource = presentationClass.getSimpleName().replaceAll("^JavaFX", "")
123 .replaceAll("^JavaFx", "")
124 .replaceAll("Presentation$", "")
125 + ".fxml";
126 return of(presentationClass, resource);
127 }
128
129
130
131
132
133
134
135 @Nonnull
136 public static <T> NodeAndDelegate<T> of (@Nonnull final Class<T> presentationClass, @Nonnull final String fxmlResourcePath)
137 {
138 final var log = LoggerFactory.getLogger(NodeAndDelegate.class);
139 log.debug("of({}, {})", presentationClass, fxmlResourcePath);
140
141 final var latch = new CountDownLatch(1);
142 final var nad = new AtomicReference<NodeAndDelegate<T>>();
143 final var exception = new AtomicReference<RuntimeException>();
144
145 if (Platform.isFxApplicationThread())
146 {
147 try
148 {
149 return load(presentationClass, fxmlResourcePath);
150 }
151 catch (IOException e)
152 {
153 exception.set(new RuntimeException(e));
154 }
155 }
156
157 Platform.runLater(() ->
158 {
159 try
160 {
161 nad.set(load(presentationClass, fxmlResourcePath));
162 }
163 catch (RuntimeException e)
164 {
165 exception.set(e);
166 }
167 catch (Exception e)
168 {
169 exception.set(new RuntimeException(e));
170 }
171
172 latch.countDown();
173 });
174
175 try
176 {
177 log.debug("Waiting for NodeAndDelegate initialisation in JavaFX thread...");
178 log.debug("If deadlocks and you need longer time with the debugger, set {} (current value: {})", P_TIMEOUT, INITIALIZER_TIMEOUT);
179 latch.await(INITIALIZER_TIMEOUT, TimeUnit.SECONDS);
180 }
181 catch (InterruptedException e)
182 {
183 throw new RuntimeException(e);
184 }
185
186 if (exception.get() != null)
187 {
188 throw exception.get();
189 }
190
191 if (nad.get() == null)
192 {
193 final var message = String.format("Likely deadlock in the JavaFX Thread: couldn't create NodeAndDelegate: %s, %s",
194 presentationClass, fxmlResourcePath);
195 throw new RuntimeException(message);
196 }
197
198 return nad.get();
199 }
200
201 @Nonnull
202 public static <T> NodeAndDelegate<T> load (@Nonnull final Class<T> clazz, @Nonnull final String resource)
203 throws IOException
204 {
205 final var log = LoggerFactory.getLogger(NodeAndDelegate.class);
206 log.debug("NodeAndDelegate({}, {})", clazz, resource);
207 assert Platform.isFxApplicationThread() : "Not in JavaFX UI Thread";
208 final var loader = new FXMLLoader(clazz.getResource(resource), null, null,
209 type -> ReflectionUtils.instantiateWithDependencies(type, BEANS));
210 try
211 {
212 final Node node = loader.load();
213 final T jfxController = loader.getController();
214 ReflectionUtils.injectDependencies(jfxController, BEANS);
215 final var interfaces = jfxController.getClass().getInterfaces();
216
217 if (interfaces.length == 0)
218 {
219 log.warn("{} has no interface: not creating safe proxy", jfxController.getClass());
220 log.debug(">>>> load({}, {}) completed", clazz, resource);
221 return new DefaultNodeAndDelegate<>(node, jfxController);
222 }
223 else
224 {
225 final var safeDelegate = JavaFXSafeProxy.of(jfxController, interfaces);
226 log.debug(">>>> load({}, {}) completed", clazz, resource);
227 return new DefaultNodeAndDelegate<>(node, safeDelegate);
228 }
229 }
230 catch (IllegalStateException e)
231 {
232 final var message = String.format("ERROR: Cannot find resource: %s/%s", clazz.getPackageName().replace('.','/'), resource);
233 log.error("ERROR: Cannot find resource: {}", message);
234 throw new IllegalStateException(message);
235 }
236 }
237 }