3 * Copyright (C) 2009 EDIT
4 * European Distributed Institute of Taxonomy
5 * http://www.e-taxonomy.eu
7 * The contents of this file are subject to the Mozilla Public License Version 1.1
8 * See LICENSE.TXT at the top of this package for the full license terms.
11 package eu
.etaxonomy
.cdm
.server
;
13 import static eu
.etaxonomy
.cdm
.server
.CommandOptions
.DATASOURCES_FILE
;
14 import static eu
.etaxonomy
.cdm
.server
.CommandOptions
.HELP
;
15 import static eu
.etaxonomy
.cdm
.server
.CommandOptions
.HTTP_PORT
;
16 import static eu
.etaxonomy
.cdm
.server
.CommandOptions
.JMX
;
17 import static eu
.etaxonomy
.cdm
.server
.CommandOptions
.WEBAPP
;
20 import java
.io
.FileNotFoundException
;
21 import java
.io
.FileOutputStream
;
22 import java
.io
.IOException
;
23 import java
.io
.InputStream
;
24 import java
.io
.OutputStream
;
25 import java
.lang
.management
.ManagementFactory
;
26 import java
.lang
.reflect
.InvocationTargetException
;
28 import java
.sql
.Connection
;
29 import java
.sql
.SQLException
;
32 import javax
.naming
.NamingException
;
33 import javax
.sql
.DataSource
;
35 import org
.apache
.commons
.cli
.CommandLine
;
36 import org
.apache
.commons
.cli
.CommandLineParser
;
37 import org
.apache
.commons
.cli
.GnuParser
;
38 import org
.apache
.commons
.cli
.HelpFormatter
;
39 import org
.apache
.commons
.cli
.ParseException
;
40 import org
.apache
.commons
.io
.FileUtils
;
41 import org
.apache
.log4j
.Logger
;
42 import org
.apache
.log4j
.PatternLayout
;
43 import org
.apache
.log4j
.RollingFileAppender
;
44 import org
.eclipse
.jetty
.jmx
.MBeanContainer
;
45 import org
.eclipse
.jetty
.server
.Server
;
46 import org
.eclipse
.jetty
.server
.handler
.ContextHandlerCollection
;
47 import org
.eclipse
.jetty
.util
.component
.LifeCycle
;
48 import org
.eclipse
.jetty
.util
.log
.Log
;
49 import org
.eclipse
.jetty
.webapp
.WebAppClassLoader
;
50 import org
.eclipse
.jetty
.webapp
.WebAppContext
;
54 * A bootstrap class for starting Jetty Runner using an embedded war.
56 * Recommended start options for the java virtual machine:
61 * -XX:MaxPermSize=192m
63 * -XX:+UseConcMarkSweepGC
64 * -XX:+CMSClassUnloadingEnabled
65 * -XX:+CMSPermGenSweepingEnabled
70 public final class Bootloader
{
71 //private static final String DEFAULT_WARFILE = "target/";
73 private static final Logger logger
= Logger
.getLogger(Bootloader
.class);
75 private static final String DATASOURCE_BEANDEF_FILE
= "datasources.xml";
76 private static final String USERHOME_CDM_LIBRARY_PATH
= System
.getProperty("user.home")+File
.separator
+".cdmLibrary"+File
.separator
;
77 private static final String TMP_PATH
= USERHOME_CDM_LIBRARY_PATH
+ "server" + File
.separator
;
78 private static final String LOG_PATH
= USERHOME_CDM_LIBRARY_PATH
+ "log" + File
.separator
;
80 private static final String APPLICATION_NAME
= "CDM Server";
81 private static final String WAR_POSTFIX
= ".war";
83 private static final String CDM_WEBAPP_WAR_NAME
= "cdmserver";
84 private static final String DEFAULT_WEBAPP_WAR_NAME
= "default-webapp";
85 private static final File DEFAULT_WEBAPP_TEMP_FOLDER
= new File(TMP_PATH
+ DEFAULT_WEBAPP_WAR_NAME
);
86 private static final File CDM_WEBAPP_TEMP_FOLDER
= new File(TMP_PATH
+ CDM_WEBAPP_WAR_NAME
);
88 private static final String ATTRIBUTE_JDBC_JNDI_NAME
= "cdm.jdbcJndiName";
89 private static final String CDM_LOGFILE
= "cdm.logfile";
91 private static final int KB
= 1024;
93 private Set
<CdmInstanceProperties
> configAndStatus
= null;
95 public Set
<CdmInstanceProperties
> getConfigAndStatus() {
96 return configAndStatus
;
99 private File webappFile
= null;
100 private File defaultWebAppFile
= null;
102 private Server server
= null;
103 private ContextHandlerCollection contexts
= new ContextHandlerCollection();
105 private CommandLine cmdLine
;
107 /* thread save singleton implementation */
109 private static Bootloader instance
= new Bootloader();
111 private Bootloader() {}
113 public synchronized static Bootloader
getBootloader(){
117 /* end of singleton implementation */
119 private Set
<CdmInstanceProperties
> loadDataSources(){
120 if(configAndStatus
== null){
121 File datasourcesFile
= new File(USERHOME_CDM_LIBRARY_PATH
, DATASOURCE_BEANDEF_FILE
);
122 configAndStatus
= DataSourcePropertyParser
.parseDataSourceConfigs(datasourcesFile
);
123 logger
.info("cdm server instance names loaded: "+ configAndStatus
.toString());
125 return configAndStatus
;
128 public int writeStreamTo(final InputStream input
, final OutputStream output
, int bufferSize
) throws IOException
{
129 int available
= Math
.min(input
.available(), 256 * KB
);
130 byte[] buffer
= new byte[Math
.max(bufferSize
, available
)];
132 int count
= input
.read(buffer
);
134 output
.write(buffer
, 0, count
);
136 count
= input
.read(buffer
);
141 private boolean bindJndiDataSource(CdmInstanceProperties conf
) {
143 Class
<DataSource
> dsCass
= (Class
<DataSource
>) Thread
.currentThread().getContextClassLoader().loadClass("com.mchange.v2.c3p0.ComboPooledDataSource");
144 DataSource datasource
= dsCass
.newInstance();
145 dsCass
.getMethod("setDriverClass", new Class
[] {String
.class}).invoke(datasource
, new Object
[] {conf
.getDriverClass()});
146 dsCass
.getMethod("setJdbcUrl", new Class
[] {String
.class}).invoke(datasource
, new Object
[] {conf
.getUrl()});
147 dsCass
.getMethod("setUser", new Class
[] {String
.class}).invoke(datasource
, new Object
[] {conf
.getUsername()});
148 dsCass
.getMethod("setPassword", new Class
[] {String
.class}).invoke(datasource
, new Object
[] {conf
.getPassword()});
150 Connection connection
= null;
151 String sqlerror
= null;
153 connection
= datasource
.getConnection();
155 } catch (SQLException e
) {
156 sqlerror
= e
.getMessage() + "["+ e
.getSQLState() + "]";
157 conf
.getProblems().add(sqlerror
);
158 if(connection
!= null){
159 try {connection
.close();} catch (SQLException e1
) { /* IGNORE */ }
161 logger
.error(conf
.toString() + " has problem : "+ sqlerror
);
164 if(!conf
.hasProblems()){
165 logger
.info("binding jndi datasource at " + conf
.getJdbcJndiName() + " with "+conf
.getUsername() +"@"+ conf
.getUrl());
166 org
.eclipse
.jetty
.plus
.jndi
.Resource jdbcResource
= new org
.eclipse
.jetty
.plus
.jndi
.Resource(conf
.getJdbcJndiName(), datasource
);
170 } catch (IllegalArgumentException e
) {
173 } catch (SecurityException e
) {
175 } catch (ClassNotFoundException e
) {
177 } catch (InstantiationException e
) {
179 } catch (IllegalAccessException e
) {
181 } catch (InvocationTargetException e
) {
183 } catch (NoSuchMethodException e
) {
185 } catch (NamingException e
) {
191 private void parseCommandOptions(String
[] args
) throws ParseException
{
192 CommandLineParser parser
= new GnuParser();
193 cmdLine
= parser
.parse( CommandOptions
.getOptions(), args
);
195 // print the help message
196 if(cmdLine
.hasOption(HELP
.getOpt())){
197 HelpFormatter formatter
= new HelpFormatter();
198 formatter
.printHelp( "java .. ", CommandOptions
.getOptions() );
204 private File
extractWar(String warName
) throws IOException
, FileNotFoundException
{
205 ClassLoader classLoader
= Thread
.currentThread().getContextClassLoader();
206 String warFileName
= warName
+ WAR_POSTFIX
;
207 URL resource
= classLoader
.getResource(warFileName
);
208 if (resource
== null) {
209 logger
.error("Could not find the " + warFileName
+ " on classpath!");
213 File warFile
= new File(TMP_PATH
, warName
+ "-" + WAR_POSTFIX
);
214 logger
.info("Extracting " + warFileName
+ " to " + warFile
+ " ...");
216 writeStreamTo(resource
.openStream(), new FileOutputStream(warFile
), 8 * KB
);
218 logger
.info("Extracted " + warFileName
);
229 public static void main(String
[] args
) throws Exception
{
231 Bootloader bootloader
= Bootloader
.getBootloader();
233 bootloader
.parseCommandOptions(args
);
235 bootloader
.startServer();
238 private void startServer() throws IOException
,
239 FileNotFoundException
, Exception
, InterruptedException
{
242 //assure LOG_PATH exists
243 File logPath
= new File(LOG_PATH
);
244 if(!logPath
.exists()){
245 FileUtils
.forceMkdir(new File(LOG_PATH
));
249 configureFileLogger();
251 logger
.info("Starting "+APPLICATION_NAME
);
252 logger
.info("Using " + System
.getProperty("user.home") + " as home directory. Can be specified by -Duser.home=<FOLDER>");
254 //assure TMP_PATH exists and clean it up
255 File tempDir
= new File(TMP_PATH
);
256 if(!tempDir
.exists() && !tempDir
.mkdirs()){
257 logger
.error("Error creating temporary directory for webapplications " + tempDir
.getAbsolutePath());
260 if(FileUtils
.deleteQuietly(tempDir
)){
262 logger
.info("Old webapplications successfully cleared");
269 if(cmdLine
.hasOption(WEBAPP
.getOpt())){
270 webappFile
= new File(cmdLine
.getOptionValue(WEBAPP
.getOpt()));
271 if(webappFile
.isDirectory()){
272 logger
.info("using user defined web application folder: " + webappFile
.getAbsolutePath());
274 logger
.info("using user defined warfile: " + webappFile
.getAbsolutePath());
276 if(isRunningFromSource()){
277 //FIXME check if all local paths are valid !!!!
278 defaultWebAppFile
= new File("./src/main/webapp");
281 //defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME);
284 webappFile
= extractWar(CDM_WEBAPP_WAR_NAME
);
285 defaultWebAppFile
= extractWar(DEFAULT_WEBAPP_WAR_NAME
);
290 if(cmdLine
.hasOption(HTTP_PORT
.getOpt())){
292 httpPort
= Integer
.parseInt(cmdLine
.getOptionValue(HTTP_PORT
.getOpt()));
293 logger
.info(HTTP_PORT
.getOpt()+" set to "+cmdLine
.getOptionValue(HTTP_PORT
.getOpt()));
294 } catch (NumberFormatException e
) {
295 logger
.error("Supplied portnumber is not an integer");
300 if(cmdLine
.hasOption(DATASOURCES_FILE
.getOpt())){
301 logger
.error(DATASOURCES_FILE
.getOpt() + " NOT JET IMPLEMENTED!!!");
306 //System.setProperty("DEBUG", "true");
308 server
= new Server(httpPort
);
311 if(cmdLine
.hasOption(JMX
.getOpt())){
312 logger
.info("adding JMX support ...");
313 MBeanContainer mBeanContainer
= new MBeanContainer(ManagementFactory
.getPlatformMBeanServer());
314 server
.getContainer().addEventListener(mBeanContainer
);
315 mBeanContainer
.addBean(Log
.getLog());
316 mBeanContainer
.start();
319 // add servelet contexts
323 // 1. default context
325 logger
.info("preparing default WebAppContext");
326 WebAppContext defaultWebappContext
= new WebAppContext();
327 setWebApp(defaultWebappContext
, defaultWebAppFile
);
328 defaultWebappContext
.setContextPath("/");
329 defaultWebappContext
.setTempDirectory(DEFAULT_WEBAPP_TEMP_FOLDER
);
331 // the defaultWebappContext MUST USE the super classloader
332 // otherwise the status page (index.jsp) might not work
333 defaultWebappContext
.setClassLoader(this.getClass().getClassLoader());
334 contexts
.addHandler(defaultWebappContext
);
337 // 2. cdm server contexts
339 server
.addLifeCycleListener(new LifeCycle
.Listener(){
342 public void lifeCycleFailure(LifeCycle event
, Throwable cause
) {
346 public void lifeCycleStarted(LifeCycle event
) {
347 logger
.info("cdmserver has started, now adding CDM server contexts");
349 addCdmServerContexts(true);
350 } catch (IOException e1
) {
356 public void lifeCycleStarting(LifeCycle event
) {
360 public void lifeCycleStopped(LifeCycle event
) {
364 public void lifeCycleStopping(LifeCycle event
) {
370 logger
.info("setting contexts ...");
371 server
.setHandler(contexts
);
372 logger
.info("starting jetty ...");
375 logger
.info(APPLICATION_NAME
+" stopped.");
380 * Configueres and adds a {@link RollingFileAppender} to the root logger
382 * The log files of the cdm-remote instances are configured by the
383 * {@link eu.etaxonomy.cdm.remote.config.LoggingConfigurer}
385 private void configureFileLogger() {
387 PatternLayout layout
= new PatternLayout("%d %p [%c] - %m%n");
389 String logFile
= LOG_PATH
+ File
.separator
+ "cdmserver.log";
390 RollingFileAppender appender
= new RollingFileAppender(layout
, logFile
);
391 appender
.setMaxBackupIndex(3);
392 appender
.setMaxFileSize("250MB");
393 Logger
.getRootLogger().addAppender(appender
);
394 logger
.info("logging to :" + logFile
);
395 } catch (IOException e
) {
396 logger
.error("Creating RollingFileAppender failed:", e
);
400 private void addCdmServerContexts(boolean austostart
) throws IOException
{
402 for(CdmInstanceProperties conf
: configAndStatus
){
403 conf
.setStatus(CdmInstanceProperties
.Status
.initializing
);
404 logger
.info("preparing WebAppContext for '"+ conf
.getDataSourceName() + "'");
405 WebAppContext cdmWebappContext
= new WebAppContext();
407 cdmWebappContext
.setContextPath("/"+conf
.getDataSourceName());
408 cdmWebappContext
.setTempDirectory(CDM_WEBAPP_TEMP_FOLDER
);
410 if(!bindJndiDataSource(conf
)){
411 // a problem with the datasource occurred skip this webapp
412 cdmWebappContext
= null;
413 logger
.error("a problem with the datasource occurred -> skipping /" + conf
.getDataSourceName());
414 conf
.setStatus(CdmInstanceProperties
.Status
.error
);
418 cdmWebappContext
.setAttribute(ATTRIBUTE_JDBC_JNDI_NAME
, conf
.getJdbcJndiName());
419 setWebApp(cdmWebappContext
, webappFile
);
421 cdmWebappContext
.setAttribute(CDM_LOGFILE
,
422 LOG_PATH
+ File
.separator
+ "cdm-"
423 + conf
.getDataSourceName() + ".log");
425 if(webappFile
.isDirectory() && isRunningFromSource()){
428 * when running the webapp from {projectpath} src/main/webapp we
429 * must assure that each web application is using it's own
430 * classloader thus we tell the WebAppClassLoader where the
431 * dependencies of the webapplication can be found. Otherwise
432 * the system classloader would load these resources.
434 logger
.info("Running webapp from source folder, thus adding java.class.path to WebAppClassLoader");
436 WebAppClassLoader classLoader
= new WebAppClassLoader(cdmWebappContext
);
438 String classPath
= System
.getProperty("java.class.path");
439 classLoader
.addClassPath(classPath
);
440 cdmWebappContext
.setClassLoader(classLoader
);
443 contexts
.addHandler(cdmWebappContext
);
447 conf
.setStatus(CdmInstanceProperties
.Status
.starting
);
448 cdmWebappContext
.start();
449 conf
.setStatus(CdmInstanceProperties
.Status
.started
);
450 } catch (Exception e
) {
451 logger
.error("Could not start " + cdmWebappContext
.getContextPath());
452 conf
.setStatus(CdmInstanceProperties
.Status
.error
);
459 private void setWebApp(WebAppContext context
, File webApplicationResource
) {
460 if(webApplicationResource
.isDirectory()){
461 context
.setResourceBase(webApplicationResource
.getAbsolutePath());
462 logger
.debug("setting directory " + webApplicationResource
.getAbsolutePath() + " as webapplication");
464 context
.setWar(webApplicationResource
.getAbsolutePath());
465 logger
.debug("setting war file " + webApplicationResource
.getAbsolutePath() + " as webapplication");
469 private boolean isRunningFromSource() {
470 String webappPathNormalized
= webappFile
.getAbsolutePath().replace('\\', '/');
471 return webappPathNormalized
.endsWith("src/main/webapp") || webappPathNormalized
.endsWith("cdmlib-remote/target/cdmserver");