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 }