/*
* Copyright 2002-2004 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.timefinder.core.springrc;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.richclient.application.splash.MonitoringSplashScreen;
import org.springframework.richclient.application.splash.SplashScreen;
import org.springframework.richclient.progress.ProgressMonitor;
import org.springframework.richclient.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.richclient.application.Application;
import org.springframework.richclient.application.ProgressMonitoringBeanFactoryPostProcessor;
import org.springframework.richclient.application.support.DefaultApplicationDescriptor;
import java.lang.reflect.InvocationTargetException;
import javax.swing.SwingUtilities;
/**
* The main driver for a Spring Rich Client application.
*
* <p>
* This class displays a configurable splash screen and launches a rich client
* {@link Application}. Both the splash screen and the application to be
* launched are expected to be defined as beans, under the names
* {@link #SPLASH_SCREEN_BEAN_ID} and {@link #APPLICATION_BEAN_ID}
* respectively, in one of the application contexts provided to the constructor.
* </p>
*
* <p>
* For quick loading and display of the splash screen while the rest of the
* application is being initialized, constructors are provided that take a
* separate startup context. The startup context will be searched for the
* {@link #SPLASH_SCREEN_BEAN_ID} bean, which will then be displayed before the
* main application context is loaded and the application launched. If no
* startup context is provided or it doesn't contain an appropriately named
* splash screen bean, an attempt will be made to load a splash screen from the
* main application context. This can only happen after the main application
* context has been loaded in its entirety so it is not the recommended approach
* for displaying a splash screen.
* </p>
*
* @author Keith Donald
* @see Application
*/
public class MyApplicationLauncher {
/**
* The name of the bean that defines the application's splash screen.
* {@value}
*/
public static final String SPLASH_SCREEN_BEAN_ID = "splashScreen";
public static final String APPLICATION_DESCRIPTOR_ID = "applicationDescriptor";
/**
* The name of the bean that defines the {@code Application} that this class
* will launch.
* {@value}
*/
public static final String APPLICATION_BEAN_ID = "application";
private final Log logger = LogFactory.getLog(getClass());
private ApplicationContext startupContext;
private SplashScreen splashScreen;
private ApplicationContext rootApplicationContext;
/**
* Launches the application defined by the Spring application context file
* at the provided classpath-relative location.
*
* @param rootContextPath The classpath-relative location of the application context file.
*
* @throws IllegalArgumentException if {@code rootContextPath} is null or empty.
*/
public MyApplicationLauncher(String rootContextPath) {
this(new String[]{rootContextPath});
}
/**
* Launches the application defined by the Spring application context files
* at the provided classpath-relative locations.
*
* @param rootContextConfigLocations the classpath-relative locations of the
* application context files.
*
* @throws IllegalArgumentException if {@code rootContextPath} is null or empty.
*/
public MyApplicationLauncher(String[] rootContextConfigLocations) {
this(null, rootContextConfigLocations);
}
/**
* Launches the application defined by the Spring application context files
* at the provided classpath-relative locations. The application context
* file specified by {@code startupContext} is loaded first to allow for
* quick loading of the application splash screen. It is recommended that
* the startup context only contains the bean definition for the splash
* screen and any other beans that it depends upon. Any beans defined in the
* startup context will not be available to the main application once
* launched.
*
* @param startupContextPath The classpath-relative location of the startup
* application context file. May be null or empty.
* @param rootContextPath The classpath-relative location of the main
* application context file.
*
* @throws IllegalArgumentException if {@code rootContextPath} is null or empty.
*/
public MyApplicationLauncher(String startupContextPath, String rootContextPath) {
this(startupContextPath, new String[]{rootContextPath});
}
/**
* Launches the application defined by the Spring application context files
* at the provided classpath-relative locations. The application context
* file specified by {@code startupContextPath} is loaded first to allow for
* quick loading of the application splash screen. It is recommended that
* the startup context only contains the bean definition for the splash
* screen and any other beans that it depends upon. Any beans defined in the
* startup context will not be available to the main application once
* launched.
*
* @param startupContextPath The classpath-relative location of the startup
* context file. May be null or empty.
* @param rootContextConfigLocations The classpath-relative locations of the main
* application context files.
*
* @throws IllegalArgumentException if {@code rootContextConfigLocations} is null or empty.
*/
public MyApplicationLauncher(String startupContextPath, String[] rootContextConfigLocations) {
Assert.noElementsNull(rootContextConfigLocations, "rootContextConfigLocations");
Assert.notEmpty(rootContextConfigLocations,
"One or more root rich client application context paths must be provided");
this.startupContext = loadStartupContext(startupContextPath);
if (startupContext != null) {
displaySplashScreen(startupContext);
}
try {
setRootApplicationContext(loadRootApplicationContext(rootContextConfigLocations, startupContext));
launchMyRichClient();
} finally {
destroySplashScreen();
}
}
/**
* Launches the application from the pre-loaded application context.
*
* @param rootApplicationContext The main application context.
*
* @throws IllegalArgumentException if {@code rootApplicationContext} is
* null.
*/
public MyApplicationLauncher(ApplicationContext rootApplicationContext) {
this(null, rootApplicationContext);
}
/**
* Launch the application using a startup context from the given location
* and a pre-loaded application context.
*
* @param startupContextPath the classpath-relative location of the starup
* application context file. If null or empty, no splash screen will be
* displayed.
* @param rootApplicationContext the main application context.
*
* @throws IllegalArgumentException if {@code rootApplicationContext} is
* null.
*
*/
public MyApplicationLauncher(String startupContextPath, ApplicationContext rootApplicationContext) {
this.startupContext = loadStartupContext(startupContextPath);
if (startupContext != null) {
displaySplashScreen(startupContext);
}
try {
setRootApplicationContext(rootApplicationContext);
launchMyRichClient();
} finally {
destroySplashScreen();
}
}
/**
* Returns an application context loaded from the bean definition file at
* the given classpath-relative location.
*
* @param startupContextPath The classpath-relative location of the
* application context file to be loaded. May be null or empty.
*
* @return An application context loaded from the given location, or null if
* {@code startupContextPath} is null or empty.
*/
private ApplicationContext loadStartupContext(String startupContextPath) {
if (StringUtils.hasText(startupContextPath)) {
if (logger.isInfoEnabled()) {
logger.info("Loading startup context from classpath resource [" + startupContextPath + "]");
}
return new ClassPathXmlApplicationContext(startupContextPath);
} else {
return null;
}
}
/**
* Returns an {@code ApplicationContext}, loaded from the bean definition
* files at the classpath-relative locations specified by
* {@code configLocations}.
*
* <p>
* If a splash screen has been created, the application context will be
* loaded with a bean post processor that will notify the splash screen's
* progress monitor as each bean is initialized.
* </p>
*
* @param configLocations The classpath-relative locations of the files from
* which the application context will be loaded.
*
* @return The main application context, never null.
*/
private ApplicationContext loadRootApplicationContext(String[] configLocations, MessageSource messageSource) {
final ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocations, false);
if (splashScreen instanceof MonitoringSplashScreen) {
final ProgressMonitor tracker = ((MonitoringSplashScreen) splashScreen).getProgressMonitor();
applicationContext.addBeanFactoryPostProcessor(
new ProgressMonitoringBeanFactoryPostProcessor(tracker, messageSource));
}
applicationContext.refresh();
return applicationContext;
}
private void setRootApplicationContext(ApplicationContext context) {
Assert.notNull(context, "The root rich client application context is required");
this.rootApplicationContext = context;
}
/**
* Launches the rich client application. If no startup context has so far
* been provided, the main application context will be searched for a splash
* screen to display. The main application context will then be searched for
* the {@link Application} to be launched, using the bean name
* {@link #APPLICATION_BEAN_ID}. If the application is found, it will be
* started.
*
*/
private void launchMyRichClient() {
if (startupContext == null) {
displaySplashScreen(rootApplicationContext);
}
final Application application;
try {
application = (Application) rootApplicationContext.getBean(APPLICATION_BEAN_ID, Application.class);
} catch (NoSuchBeanDefinitionException e) {
throw new IllegalArgumentException(
"A single bean definition with id " + APPLICATION_BEAN_ID + ", of type " + Application.class.getName() + " must be defined in the main application context",
e);
}
try {
// To avoid deadlocks when events fire during initialization of some swing components
// Possible to do: in theory not a single Swing component should be created (=modified) in the launcher thread...
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
application.start();
}
});
} catch (InterruptedException e) {
logger.warn("Application start interrupted", e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
throw new IllegalStateException("Application start thrown an exception: " + cause.getMessage(), cause);
}
logger.debug("Launcher thread exiting...");
}
public DefaultApplicationDescriptor getApplicationDescriptor() {
rootApplicationContext.getBean(APPLICATION_DESCRIPTOR_ID, DefaultApplicationDescriptor.class);
return (DefaultApplicationDescriptor) rootApplicationContext.getBean(APPLICATION_DESCRIPTOR_ID, DefaultApplicationDescriptor.class);
}
/**
* Searches the given bean factory for a {@link SplashScreen} defined with
* the bean name {@link #SPLASH_SCREEN_BEAN_ID} and displays it, if found.
*
* @param beanFactory The bean factory that is expected to contain the
* splash screen bean definition. Must not be null.
*
* @throws NullPointerException if {@code beanFactory} is null.
* @throws BeanNotOfRequiredTypeException if the bean found under the splash
* screen bean name is not a {@link SplashScreen}.
*
*/
private void displaySplashScreen(BeanFactory beanFactory) {
if (beanFactory.containsBean(SPLASH_SCREEN_BEAN_ID)) {
this.splashScreen = (SplashScreen) beanFactory.getBean(SPLASH_SCREEN_BEAN_ID, SplashScreen.class);
logger.debug("Displaying application splash screen...");
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
MyApplicationLauncher.this.splashScreen.splash();
}
});
} catch (Exception e) {
throw new RuntimeException("EDT threading issue while showing splash screen", e);
}
} else {
logger.info("No splash screen bean found to display. Continuing...");
}
}
private void destroySplashScreen() {
if (splashScreen != null) {
logger.debug("Closing splash screen...");
SwingUtilities.invokeLater(new Runnable() {
public void run() {
splashScreen.dispose();
splashScreen = null;
}
});
}
}
}