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 }