View Javadoc
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  
28  import java.lang.ref.WeakReference;
29  import java.lang.reflect.InvocationTargetException;
30  import java.lang.reflect.Proxy;
31  import jakarta.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   }