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.role.ui.javafx.impl.util;
27
28 import java.lang.ref.WeakReference;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Proxy;
31 import javax.annotation.Nonnull;
32 import java.util.HashMap;
33 import java.util.Map;
34 import java.util.concurrent.CountDownLatch;
35 import java.util.concurrent.atomic.AtomicReference;
36 import javafx.fxml.FXML;
37 import javafx.fxml.FXMLLoader;
38 import javafx.application.Platform;
39 import it.tidalwave.ui.javafx.JavaFXSafeProxyCreator;
40 import it.tidalwave.util.ReflectionUtils;
41 import lombok.RequiredArgsConstructor;
42 import lombok.extern.slf4j.Slf4j;
43 import static lombok.AccessLevel.PRIVATE;
44
45 /***************************************************************************************************************************************************************
46 *
47 * @stereotype Factory
48 *
49 * @author Fabrizio Giudici
50 *
51 **************************************************************************************************************************************************************/
52 @RequiredArgsConstructor(access = PRIVATE) @Slf4j
53 public final class JavaFXSafeComponentBuilder<I, T extends I>
54 {
55 @Nonnull
56 private final Class<T> componentClass;
57
58 @Nonnull
59 private final Class<I> interfaceClass;
60
61 private WeakReference<T> presentationRef = new WeakReference<>(null);
62
63 @Nonnull
64 public static <J, X extends J> JavaFXSafeComponentBuilder<J, X> builderFor (@Nonnull final Class<X> componentClass)
65 {
66 final var interfaceClass = (Class<J>)componentClass.getInterfaces()[0]; // FIXME: guess
67 return new JavaFXSafeComponentBuilder<>(componentClass, interfaceClass);
68 }
69
70 @Nonnull
71 public static <J, X extends J> JavaFXSafeComponentBuilder<J, X> builderFor (@Nonnull final Class<J> interfaceClass,
72 @Nonnull final Class<X> componentClass)
73 {
74 return new JavaFXSafeComponentBuilder<>(componentClass, interfaceClass);
75 }
76
77 /***********************************************************************************************************************************************************
78 * Creates an instance of a surrogate JavaFX delegate. JavaFX delegates (controllers in JavaFX jargon) are those
79 * objects with fields annotated with {@link @FXML} that are created by the {@link FXMLLoader} starting from a
80 * {@code .fxml} file. Sometimes a surrogate delegate is needed, that is a class that is not mapped to any
81 * {@link @FXML} file, but whose fields are copied from another existing delegate.
82 *
83 * @param componentClass the class of the surrogate
84 * @param fxmlFieldsSource the existing JavaFX delegate with {@code @FXML} annotated fields.
85 * @return the new surrogate delegate
86 **********************************************************************************************************************************************************/
87 @Nonnull
88 public static <J, X extends J> X createInstance (@Nonnull final Class<X> componentClass,
89 @Nonnull final Object fxmlFieldsSource)
90 {
91 final JavaFXSafeComponentBuilder<J, X> builder = builderFor(componentClass);
92 return builder.createInstance(fxmlFieldsSource);
93 }
94
95 /***********************************************************************************************************************************************************
96 * Creates an instance of a surrogate JavaFX delegate. JavaFX delegates (controllers in JavaFX jargon) are those
97 * objects with fields annotated with {@link @FXML} that are created by the {@link FXMLLoader} starting from a
98 * {@code .fxml} file. Sometimes a surrogate delegate is needed, that is a class that is not mapped to any
99 * {@link @FXML} file, but whose fields are copied from another existing delegate.
100 *
101 * @param fxmlFieldsSource the existing JavaFX delegate with {@code @FXML} annotated fields.
102 * @return the new surrogate delegate
103 **********************************************************************************************************************************************************/
104 @Nonnull
105 public synchronized T createInstance (@Nonnull final Object fxmlFieldsSource)
106 {
107 log.trace("createInstance({})", fxmlFieldsSource);
108 var presentation = presentationRef.get();
109
110 if (presentation == null)
111 {
112 presentation = Platform.isFxApplicationThread() ? createComponentInstance() : createComponentInstanceInJAT();
113 copyFxmlFields(presentation, fxmlFieldsSource); // FIXME: in JFX thread?
114
115 try // FIXME // FIXME: in JFX thread?
116 {
117 presentation.getClass().getDeclaredMethod("initialize").invoke(presentation);
118 }
119 catch (NoSuchMethodException | SecurityException | IllegalAccessException
120 | InvocationTargetException e)
121 {
122 log.warn("No postconstruct in {}", presentation);
123 }
124
125 presentation = (T)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
126 new Class[] { interfaceClass },
127 new JavaFXSafeProxy<>(presentation));
128 presentationRef = new WeakReference<>(presentation);
129 }
130
131 return presentation;
132 }
133
134 /***********************************************************************************************************************************************************
135 *
136 **********************************************************************************************************************************************************/
137 @Nonnull
138 private T createComponentInstance()
139 {
140 return ReflectionUtils.instantiateWithDependencies(componentClass, JavaFXSafeProxyCreator.BEANS);
141 }
142
143 /***********************************************************************************************************************************************************
144 *
145 **********************************************************************************************************************************************************/
146 @Nonnull
147 private T createComponentInstanceInJAT()
148 {
149 final var reference = new AtomicReference<T>();
150 final var countDownLatch = new CountDownLatch(1);
151
152 Platform.runLater(() ->
153 {
154 reference.set(createComponentInstance());
155 countDownLatch.countDown();
156 });
157
158 try
159 {
160 countDownLatch.await();
161 }
162 catch (InterruptedException e)
163 {
164 log.error("", e);
165 throw new RuntimeException(e);
166 }
167
168 return reference.get();
169 }
170
171 /***********************************************************************************************************************************************************
172 * Inject fields annotated with {@link FXML} in {@code source} to {@code target}.
173 *
174 * @param target the target object
175 * @param source the source object
176 **********************************************************************************************************************************************************/
177 private void copyFxmlFields (@Nonnull final Object target, @Nonnull final Object source)
178 {
179 log.debug("injecting {} with fields from {}", target, source);
180 final Map<String, Object> valuesMapByFieldName = new HashMap<>();
181
182 for (final var field : source.getClass().getDeclaredFields())
183 {
184 if (field.getAnnotation(FXML.class) != null)
185 {
186 final var name = field.getName();
187
188 try
189 {
190 field.setAccessible(true);
191 final var value = field.get(source);
192 valuesMapByFieldName.put(name, value);
193 log.trace(">>>> available field {}: {}", name, value);
194 }
195 catch (IllegalArgumentException | IllegalAccessException e)
196 {
197 throw new RuntimeException("Cannot read field " + name + " from " + source, e);
198 }
199 }
200 }
201
202 for (final var field : target.getClass().getDeclaredFields())
203 {
204 final var fxml = field.getAnnotation(FXML.class);
205
206 if (fxml != null)
207 {
208 final var name = field.getName();
209 final var value = valuesMapByFieldName.get(name);
210
211 if (value == null)
212 {
213 throw new RuntimeException("Can't inject " + name + ": available: " + valuesMapByFieldName.keySet());
214 }
215
216 field.setAccessible(true);
217
218 try
219 {
220 field.set(target, value);
221 }
222 catch (IllegalArgumentException | IllegalAccessException e)
223 {
224 throw new RuntimeException("Cannot inject field " + name + " to " + target, e);
225 }
226 }
227 }
228
229 ReflectionUtils.injectDependencies(target, JavaFXSafeProxyCreator.BEANS);
230 }
231 }