001    /*
002     * Copyright 2002-2004 the original author or authors.
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     * 
008     * http://www.apache.org/licenses/LICENSE-2.0
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.application;
017    
018    import org.apache.commons.logging.Log;
019    import org.apache.commons.logging.LogFactory;
020    import org.springframework.beans.factory.BeanFactory;
021    import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
022    import org.springframework.beans.factory.NoSuchBeanDefinitionException;
023    import org.springframework.context.ApplicationContext;
024    import org.springframework.context.MessageSource;
025    import org.springframework.context.support.ClassPathXmlApplicationContext;
026    import org.springframework.richclient.application.splash.MonitoringSplashScreen;
027    import org.springframework.richclient.application.splash.SplashScreen;
028    import org.springframework.richclient.progress.ProgressMonitor;
029    import org.springframework.richclient.util.Assert;
030    import org.springframework.util.StringUtils;
031    
032    import javax.swing.*;
033    import java.lang.reflect.InvocationTargetException;
034    
035    /**
036     * The main driver for a Spring Rich Client application.
037     * 
038     * <p>
039     * This class displays a configurable splash screen and launches a rich client
040     * {@link Application}. Both the splash screen and the application to be
041     * launched are expected to be defined as beans, under the names
042     * {@link #SPLASH_SCREEN_BEAN_ID} and {@link #APPLICATION_BEAN_ID}
043     * respectively, in one of the application contexts provided to the constructor.
044     * </p>
045     * 
046     * <p>
047     * For quick loading and display of the splash screen while the rest of the
048     * application is being initialized, constructors are provided that take a
049     * separate startup context. The startup context will be searched for the
050     * {@link #SPLASH_SCREEN_BEAN_ID} bean, which will then be displayed before the
051     * main application context is loaded and the application launched. If no
052     * startup context is provided or it doesn't contain an appropriately named
053     * splash screen bean, an attempt will be made to load a splash screen from the
054     * main application context. This can only happen after the main application
055     * context has been loaded in its entirety so it is not the recommended approach
056     * for displaying a splash screen.
057     * </p>
058     * 
059     * @author Keith Donald
060     * @see Application
061     */
062    public class ApplicationLauncher {
063    
064            /**
065             * The name of the bean that defines the application's splash screen.
066         * {@value}
067             */
068            public static final String SPLASH_SCREEN_BEAN_ID = "splashScreen";
069    
070            /**
071             * The name of the bean that defines the {@code Application} that this class
072             * will launch.
073         * {@value}
074             */
075            public static final String APPLICATION_BEAN_ID = "application";
076    
077            private final Log logger = LogFactory.getLog(getClass());
078    
079            private ApplicationContext startupContext;
080    
081            private SplashScreen splashScreen;
082    
083            private ApplicationContext rootApplicationContext;
084    
085            /**
086             * Launches the application defined by the Spring application context file
087             * at the provided classpath-relative location.
088             * 
089             * @param rootContextPath The classpath-relative location of the application context file.
090             * 
091             * @throws IllegalArgumentException if {@code rootContextPath} is null or empty.
092             */
093            public ApplicationLauncher(String rootContextPath) {
094                    this(new String[] { rootContextPath });
095            }
096    
097            /**
098             * Launches the application defined by the Spring application context files
099             * at the provided classpath-relative locations.
100             * 
101             * @param rootContextConfigLocations the classpath-relative locations of the
102             * application context files.
103             * 
104             * @throws IllegalArgumentException if {@code rootContextPath} is null or empty.
105             */
106            public ApplicationLauncher(String[] rootContextConfigLocations) {
107                    this(null, rootContextConfigLocations);
108            }
109    
110            /**
111             * Launches the application defined by the Spring application context files
112             * at the provided classpath-relative locations. The application context
113             * file specified by {@code startupContext} is loaded first to allow for
114             * quick loading of the application splash screen. It is recommended that
115             * the startup context only contains the bean definition for the splash
116             * screen and any other beans that it depends upon. Any beans defined in the
117             * startup context will not be available to the main application once
118             * launched.
119             * 
120             * @param startupContextPath The classpath-relative location of the startup
121             * application context file. May be null or empty.
122             * @param rootContextPath The classpath-relative location of the main
123             * application context file.
124             * 
125             * @throws IllegalArgumentException if {@code rootContextPath} is null or empty.
126             */
127            public ApplicationLauncher(String startupContextPath, String rootContextPath) {
128                    this(startupContextPath, new String[] { rootContextPath });
129            }
130    
131            /**
132             * Launches the application defined by the Spring application context files
133             * at the provided classpath-relative locations. The application context
134             * file specified by {@code startupContextPath} is loaded first to allow for
135             * quick loading of the application splash screen. It is recommended that
136             * the startup context only contains the bean definition for the splash
137             * screen and any other beans that it depends upon. Any beans defined in the
138             * startup context will not be available to the main application once
139             * launched.
140             * 
141             * @param startupContextPath The classpath-relative location of the startup
142             * context file. May be null or empty.
143             * @param rootContextConfigLocations The classpath-relative locations of the main
144             * application context files.
145             * 
146             * @throws IllegalArgumentException if {@code rootContextConfigLocations} is null or empty.
147             */
148            public ApplicationLauncher(String startupContextPath, String[] rootContextConfigLocations) {
149            
150            Assert.noElementsNull(rootContextConfigLocations, "rootContextConfigLocations");
151            Assert.notEmpty(rootContextConfigLocations, 
152                            "One or more root rich client application context paths must be provided");
153            
154                    this.startupContext = loadStartupContext(startupContextPath);
155                    if (startupContext != null) {
156                            displaySplashScreen(startupContext);
157                    }
158                    try {
159                            setRootApplicationContext(loadRootApplicationContext(rootContextConfigLocations, startupContext));
160                            launchMyRichClient();
161                    }
162                    finally {
163                            destroySplashScreen();
164                    }
165            }
166    
167            /**
168             * Launches the application from the pre-loaded application context.
169             * 
170             * @param rootApplicationContext The main application context.
171             * 
172             * @throws IllegalArgumentException if {@code rootApplicationContext} is
173             * null.
174             */
175            public ApplicationLauncher(ApplicationContext rootApplicationContext) {
176                    this(null, rootApplicationContext);
177            }
178    
179            /**
180             * Launch the application using a startup context from the given location
181             * and a pre-loaded application context.
182             * 
183             * @param startupContextPath the classpath-relative location of the starup
184             * application context file. If null or empty, no splash screen will be
185             * displayed.
186             * @param rootApplicationContext the main application context.
187             * 
188             * @throws IllegalArgumentException if {@code rootApplicationContext} is
189             * null.
190             * 
191             */
192            public ApplicationLauncher(String startupContextPath, ApplicationContext rootApplicationContext) {
193                    this.startupContext = loadStartupContext(startupContextPath);
194                    if (startupContext != null) {
195                            displaySplashScreen(startupContext);
196                    }
197                    try {
198                            setRootApplicationContext(rootApplicationContext);
199                            launchMyRichClient();
200                    }
201                    finally {
202                            destroySplashScreen();
203                    }
204            }
205    
206            /**
207             * Returns an application context loaded from the bean definition file at
208             * the given classpath-relative location.
209             * 
210             * @param startupContextPath The classpath-relative location of the
211             * application context file to be loaded. May be null or empty.
212             * 
213             * @return An application context loaded from the given location, or null if
214             * {@code startupContextPath} is null or empty.
215             */
216            private ApplicationContext loadStartupContext(String startupContextPath) {
217                    
218            if (StringUtils.hasText(startupContextPath)) {
219                
220                if (logger.isInfoEnabled()) {
221                    logger.info("Loading startup context from classpath resource [" 
222                                + startupContextPath
223                                + "]");
224                }
225                
226                            return new ClassPathXmlApplicationContext(startupContextPath);
227                
228                    }
229                    else {
230                            return null;
231                    }
232            
233            }
234    
235            /**
236             * Returns an {@code ApplicationContext}, loaded from the bean definition
237             * files at the classpath-relative locations specified by
238             * {@code configLocations}.
239             * 
240             * <p>
241             * If a splash screen has been created, the application context will be
242             * loaded with a bean post processor that will notify the splash screen's
243             * progress monitor as each bean is initialized.
244             * </p>
245             * 
246             * @param configLocations The classpath-relative locations of the files from
247             * which the application context will be loaded.
248             * 
249             * @return The main application context, never null.
250             */
251            private ApplicationContext loadRootApplicationContext(String[] configLocations, MessageSource messageSource) {
252                    final ClassPathXmlApplicationContext applicationContext 
253                    = new ClassPathXmlApplicationContext(configLocations, false);
254    
255                    if (splashScreen instanceof MonitoringSplashScreen) {
256                            final ProgressMonitor tracker = ((MonitoringSplashScreen) splashScreen).getProgressMonitor();
257    
258                            applicationContext.addBeanFactoryPostProcessor(
259                        new ProgressMonitoringBeanFactoryPostProcessor(tracker, messageSource));
260    
261                    }
262    
263                    applicationContext.refresh();
264    
265                    return applicationContext;
266            }
267    
268            private void setRootApplicationContext(ApplicationContext context) {
269                    Assert.notNull(context, "The root rich client application context is required");
270                    this.rootApplicationContext = context;
271            }
272    
273            /**
274             * Launches the rich client application. If no startup context has so far
275             * been provided, the main application context will be searched for a splash
276             * screen to display. The main application context will then be searched for
277             * the {@link Application} to be launched, using the bean name
278             * {@link #APPLICATION_BEAN_ID}. If the application is found, it will be
279             * started.
280             * 
281             */
282            private void launchMyRichClient() {
283            
284                    if (startupContext == null) {
285                            displaySplashScreen(rootApplicationContext);
286                    }
287                    
288            final Application application;
289            
290                    try {
291                        application = (Application) rootApplicationContext.getBean(APPLICATION_BEAN_ID, Application.class);
292                    }
293                    catch (NoSuchBeanDefinitionException e) {
294                            throw new IllegalArgumentException(
295                                            "A single bean definition with id "
296                        + APPLICATION_BEAN_ID
297                        + ", of type "
298                        + Application.class.getName()
299                        + " must be defined in the main application context", 
300                        e);
301                    }
302            
303                    try {
304                            // To avoid deadlocks when events fire during initialization of some swing components
305                            // Possible to do: in theory not a single Swing component should be created (=modified) in the launcher thread...
306                            SwingUtilities.invokeAndWait(new Runnable() {
307                                    public void run() {
308                                            application.start();
309                                    }
310                            });
311                    } 
312            catch (InterruptedException e) {
313                            logger.warn("Application start interrupted", e);
314                    } 
315            catch (InvocationTargetException e) {
316                            Throwable cause = e.getCause();
317                            throw new IllegalStateException("Application start thrown an exception: " + cause.getMessage(), cause);
318                    }
319            
320                    logger.debug("Launcher thread exiting...");
321            
322            }
323    
324            /**
325             * Searches the given bean factory for a {@link SplashScreen} defined with
326             * the bean name {@link #SPLASH_SCREEN_BEAN_ID} and displays it, if found.
327             * 
328             * @param beanFactory The bean factory that is expected to contain the
329             * splash screen bean definition. Must not be null.
330             * 
331             * @throws NullPointerException if {@code beanFactory} is null.
332             * @throws BeanNotOfRequiredTypeException if the bean found under the splash
333             * screen bean name is not a {@link SplashScreen}.
334             * 
335             */
336            private void displaySplashScreen(BeanFactory beanFactory) {
337                    if (beanFactory.containsBean(SPLASH_SCREEN_BEAN_ID)) {
338                            this.splashScreen = (SplashScreen) beanFactory.getBean(SPLASH_SCREEN_BEAN_ID, SplashScreen.class);
339                            logger.debug("Displaying application splash screen...");
340                try
341                {
342                    SwingUtilities.invokeAndWait(new Runnable() {
343                        public void run() {
344                            ApplicationLauncher.this.splashScreen.splash();
345                        }
346                    });
347                }
348                catch (Exception e)
349                {
350                    throw new RuntimeException("EDT threading issue while showing splash screen", e);
351                }
352                    }
353                    else {
354                            logger.info("No splash screen bean found to display. Continuing...");
355                    }
356            }
357    
358            private void destroySplashScreen() {
359                    if (splashScreen != null) {
360                            logger.debug("Closing splash screen...");
361    
362                            SwingUtilities.invokeLater(new Runnable() {
363                                    public void run() {
364                                            splashScreen.dispose();
365                                            splashScreen = null;
366                                    }
367                            });
368                    }
369            }
370    
371    }