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
;
18 import static eu
.etaxonomy
.cdm
.server
.CommandOptions
.WIN32SERVICE
;
21 import java
.io
.FileNotFoundException
;
22 import java
.io
.FileOutputStream
;
23 import java
.io
.IOException
;
24 import java
.io
.InputStream
;
25 import java
.io
.OutputStream
;
26 import java
.lang
.management
.ManagementFactory
;
27 import java
.lang
.reflect
.InvocationTargetException
;
29 import java
.sql
.Connection
;
30 import java
.sql
.SQLException
;
31 import java
.util
.List
;
34 import javax
.naming
.NamingException
;
35 import javax
.sql
.DataSource
;
37 import org
.apache
.commons
.cli
.CommandLine
;
38 import org
.apache
.commons
.cli
.CommandLineParser
;
39 import org
.apache
.commons
.cli
.GnuParser
;
40 import org
.apache
.commons
.cli
.HelpFormatter
;
41 import org
.apache
.commons
.cli
.ParseException
;
42 import org
.apache
.commons
.io
.FileUtils
;
43 import org
.apache
.log4j
.Logger
;
44 import org
.apache
.log4j
.PatternLayout
;
45 import org
.apache
.log4j
.RollingFileAppender
;
46 import org
.eclipse
.jetty
.jmx
.MBeanContainer
;
47 import org
.eclipse
.jetty
.security
.HashLoginService
;
48 import org
.eclipse
.jetty
.server
.Server
;
49 import org
.eclipse
.jetty
.server
.handler
.ContextHandlerCollection
;
50 import org
.eclipse
.jetty
.server
.handler
.ContextHandler
.Context
;
51 import org
.eclipse
.jetty
.util
.component
.LifeCycle
;
52 import org
.eclipse
.jetty
.util
.component
.LifeCycle
.Listener
;
53 import org
.eclipse
.jetty
.util
.log
.Log
;
54 import org
.eclipse
.jetty
.webapp
.WebAppClassLoader
;
55 import org
.eclipse
.jetty
.webapp
.WebAppContext
;
57 import eu
.etaxonomy
.cdm
.server
.CdmInstanceProperties
.Status
;
58 import eu
.etaxonomy
.cdm
.server
.win32service
.Win32Service
;
62 * A bootstrap class for starting Jetty Runner using an embedded war.
64 * Recommended start options for the java virtual machine:
69 * -XX:MaxPermSize=192m
71 * -XX:+UseConcMarkSweepGC
72 * -XX:+CMSClassUnloadingEnabled
73 * -XX:+CMSPermGenSweepingEnabled
78 public final class Bootloader
{
79 //private static final String DEFAULT_WARFILE = "target/";
82 * @author a.kohlbecker
86 private class WebAppContextListener
implements Listener
{
88 WebAppContext cdmWebappContext
;
90 * @param cdmWebappContext
92 public WebAppContextListener(WebAppContext cdmWebappContext
) {
93 this.cdmWebappContext
= cdmWebappContext
;
97 public void lifeCycleStopping(LifeCycle event
) {
98 logger
.error("lifeCycleStopping");
102 public void lifeCycleStopped(LifeCycle event
) {
103 logger
.error("lifeCycleStopped");
108 public void lifeCycleStarting(LifeCycle event
) {
109 logger
.error("lifeCycleStarting");
112 @SuppressWarnings("unchecked")
114 public void lifeCycleStarted(LifeCycle event
) {
115 logger
.error("lifeCycleStarted");
117 List
<String
> messages
= getServletContextAttribute(cdmWebappContext
, ATTRIBUTE_ERROR_MESSAGES
, List
.class);
118 String dataSourceName
= getServletContextAttribute(cdmWebappContext
, ATTRIBUTE_DATASOURCE_NAME
, String
.class);
120 if(messages
!= null && dataSourceName
!= null){
121 CdmInstanceProperties configAndStatus
= findConfigAndStatusFor(dataSourceName
);
122 configAndStatus
.getProblems().addAll(messages
);
123 configAndStatus
.setStatus(Status
.error
);
125 logger
.warn("Stopping context '" + dataSourceName
+ "' due to errors reported in ServletContext");
126 cdmWebappContext
.stop();
127 } catch (Exception e
) {
135 public void lifeCycleFailure(LifeCycle event
, Throwable cause
) {
136 logger
.error("lifeCycleFailure");
140 private static final Logger logger
= Logger
.getLogger(Bootloader
.class);
142 private static final String DATASOURCE_BEANDEF_FILE
= "datasources.xml";
143 private static final String REALM_PROPERTIES_FILE
= "cdm-server-realm.properties";
145 private static final String USERHOME_CDM_LIBRARY_PATH
= System
.getProperty("user.home")+File
.separator
+".cdmLibrary"+File
.separator
;
146 private static final String TMP_PATH
= USERHOME_CDM_LIBRARY_PATH
+ "server" + File
.separator
;
147 private static final String LOG_PATH
= USERHOME_CDM_LIBRARY_PATH
+ "log" + File
.separator
;
149 private static final String APPLICATION_NAME
= "CDM Server";
150 private static final String WAR_POSTFIX
= ".war";
152 private static final String CDM_WEBAPP_WAR_NAME
= "cdmserver";
153 private static final String DEFAULT_WEBAPP_WAR_NAME
= "default-webapp";
154 private static final File DEFAULT_WEBAPP_TEMP_FOLDER
= new File(TMP_PATH
+ DEFAULT_WEBAPP_WAR_NAME
);
155 private static final File CDM_WEBAPP_TEMP_FOLDER
= new File(TMP_PATH
+ CDM_WEBAPP_WAR_NAME
);
157 private static final String ATTRIBUTE_JDBC_JNDI_NAME
= "cdm.jdbcJndiName";
158 private static final String ATTRIBUTE_DATASOURCE_NAME
= "cdm.datasource";
159 private static final String ATTRIBUTE_CDM_LOGFILE
= "cdm.logfile";
161 * same as in eu.etaxonomy.cdm.remote.config.DataSourceConfigurer
163 private static final String ATTRIBUTE_ERROR_MESSAGES
= "cdm.errorMessages";
166 // memory requirements
167 private static final long MB
= 1024 * 1024;
168 private static final long PERM_GEN_SPACE_PER_INSTANCE
= 64 * MB
;
169 private static final long HEAP_PER_INSTANCE
= 300 * MB
;
171 private static final int KB
= 1024;
174 private Set
<CdmInstanceProperties
> configAndStatusSet
= null;
176 public Set
<CdmInstanceProperties
> getConfigAndStatus() {
177 return configAndStatusSet
;
180 private File webappFile
= null;
181 private File defaultWebAppFile
= null;
183 private Server server
= null;
184 private ContextHandlerCollection contexts
= new ContextHandlerCollection();
186 private CommandLine cmdLine
;
188 /* thread save singleton implementation */
190 private static Bootloader instance
= new Bootloader();
192 private Bootloader() {}
194 public synchronized static Bootloader
getBootloader(){
198 /* end of singleton implementation */
200 private Set
<CdmInstanceProperties
> loadDataSources(){
201 if(configAndStatusSet
== null){
202 File datasourcesFile
= new File(USERHOME_CDM_LIBRARY_PATH
, DATASOURCE_BEANDEF_FILE
);
203 configAndStatusSet
= DataSourcePropertyParser
.parseDataSourceConfigs(datasourcesFile
);
204 logger
.info("cdm server instance names loaded: "+ configAndStatusSet
.toString());
206 return configAndStatusSet
;
209 public int writeStreamTo(final InputStream input
, final OutputStream output
, int bufferSize
) throws IOException
{
210 int available
= Math
.min(input
.available(), 256 * KB
);
211 byte[] buffer
= new byte[Math
.max(bufferSize
, available
)];
213 int count
= input
.read(buffer
);
215 output
.write(buffer
, 0, count
);
217 count
= input
.read(buffer
);
222 private boolean bindJndiDataSource(CdmInstanceProperties conf
) {
224 Class
<DataSource
> dsCass
= (Class
<DataSource
>) Thread
.currentThread().getContextClassLoader().loadClass("com.mchange.v2.c3p0.ComboPooledDataSource");
225 DataSource datasource
= dsCass
.newInstance();
226 dsCass
.getMethod("setDriverClass", new Class
[] {String
.class}).invoke(datasource
, new Object
[] {conf
.getDriverClass()});
227 dsCass
.getMethod("setJdbcUrl", new Class
[] {String
.class}).invoke(datasource
, new Object
[] {conf
.getUrl()});
228 dsCass
.getMethod("setUser", new Class
[] {String
.class}).invoke(datasource
, new Object
[] {conf
.getUsername()});
229 dsCass
.getMethod("setPassword", new Class
[] {String
.class}).invoke(datasource
, new Object
[] {conf
.getPassword()});
231 Connection connection
= null;
232 String sqlerror
= null;
234 connection
= datasource
.getConnection();
236 } catch (SQLException e
) {
237 sqlerror
= e
.getMessage() + "["+ e
.getSQLState() + "]";
238 conf
.getProblems().add(sqlerror
);
239 if(connection
!= null){
240 try {connection
.close();} catch (SQLException e1
) { /* IGNORE */ }
242 logger
.error(conf
.toString() + " has problem : "+ sqlerror
);
245 if(!conf
.hasProblems()){
246 logger
.info("binding jndi datasource at " + conf
.getJdbcJndiName() + " with "+conf
.getUsername() +"@"+ conf
.getUrl());
247 org
.eclipse
.jetty
.plus
.jndi
.Resource jdbcResource
= new org
.eclipse
.jetty
.plus
.jndi
.Resource(conf
.getJdbcJndiName(), datasource
);
251 } catch (IllegalArgumentException e
) {
254 } catch (SecurityException e
) {
256 } catch (ClassNotFoundException e
) {
258 } catch (InstantiationException e
) {
260 } catch (IllegalAccessException e
) {
262 } catch (InvocationTargetException e
) {
264 } catch (NoSuchMethodException e
) {
266 } catch (NamingException e
) {
272 private void parseCommandOptions(String
[] args
) throws ParseException
{
273 CommandLineParser parser
= new GnuParser();
274 cmdLine
= parser
.parse( CommandOptions
.getOptions(), args
);
276 // print the help message
277 if(cmdLine
.hasOption(HELP
.getOpt())){
278 HelpFormatter formatter
= new HelpFormatter();
279 formatter
.printHelp( "java .. ", CommandOptions
.getOptions() );
285 private File
extractWar(String warName
) throws IOException
, FileNotFoundException
{
286 ClassLoader classLoader
= Thread
.currentThread().getContextClassLoader();
287 String warFileName
= warName
+ WAR_POSTFIX
;
289 // 1. find in classpath
290 URL resource
= classLoader
.getResource(warFileName
);
291 if (resource
== null) {
292 logger
.error("Could not find the " + warFileName
+ " on classpath!");
294 File pomxml
= new File("pom.xml");
296 // 2. try finding in target folder of maven project
297 File warFile
= new File("target" + File
.separator
+ warFileName
);
298 logger
.debug("looging for war file at " + warFile
.getAbsolutePath());
299 if (warFile
.canRead()) {
300 resource
= warFile
.toURI().toURL();
302 logger
.error("Also could not find the " + warFileName
+ " in maven project, try excuting 'mvn install'");
307 if (resource
== null) {
308 // no way finding the war file :-(
313 File warFile
= new File(TMP_PATH
, warName
+ "-" + WAR_POSTFIX
);
314 logger
.info("Extracting " + warFileName
+ " to " + warFile
+ " ...");
316 writeStreamTo(resource
.openStream(), new FileOutputStream(warFile
), 8 * KB
);
318 logger
.info("Extracted " + warFileName
);
329 public static void main(String
[] args
) throws Exception
{
331 Bootloader bootloader
= Bootloader
.getBootloader();
333 bootloader
.parseCommandOptions(args
);
335 bootloader
.startServer();
338 private void startServer() throws IOException
,
339 FileNotFoundException
, Exception
, InterruptedException
{
342 //assure LOG_PATH exists
343 File logPath
= new File(LOG_PATH
);
344 if(!logPath
.exists()){
345 FileUtils
.forceMkdir(new File(LOG_PATH
));
349 configureFileLogger();
351 logger
.info("Starting "+APPLICATION_NAME
);
352 logger
.info("Using " + System
.getProperty("user.home") + " as home directory. Can be specified by -Duser.home=<FOLDER>");
354 //assure TMP_PATH exists and clean it up
355 File tempDir
= new File(TMP_PATH
);
356 if(!tempDir
.exists() && !tempDir
.mkdirs()){
357 logger
.error("Error creating temporary directory for webapplications " + tempDir
.getAbsolutePath());
360 if(FileUtils
.deleteQuietly(tempDir
)){
362 logger
.info("Old webapplications successfully cleared");
369 if(cmdLine
.hasOption(WEBAPP
.getOpt())){
370 webappFile
= new File(cmdLine
.getOptionValue(WEBAPP
.getOpt()));
371 if(webappFile
.isDirectory()){
372 logger
.info("using user defined web application folder: " + webappFile
.getAbsolutePath());
374 logger
.info("using user defined warfile: " + webappFile
.getAbsolutePath());
376 if(isRunningFromSource()){
377 //FIXME check if all local paths are valid !!!!
378 defaultWebAppFile
= new File("./src/main/webapp");
381 defaultWebAppFile
= extractWar(DEFAULT_WEBAPP_WAR_NAME
);
384 webappFile
= extractWar(CDM_WEBAPP_WAR_NAME
);
385 defaultWebAppFile
= extractWar(DEFAULT_WEBAPP_WAR_NAME
);
390 if(cmdLine
.hasOption(HTTP_PORT
.getOpt())){
392 httpPort
= Integer
.parseInt(cmdLine
.getOptionValue(HTTP_PORT
.getOpt()));
393 logger
.info(HTTP_PORT
.getOpt()+" set to "+cmdLine
.getOptionValue(HTTP_PORT
.getOpt()));
394 } catch (NumberFormatException e
) {
395 logger
.error("Supplied portnumber is not an integer");
400 if(cmdLine
.hasOption(DATASOURCES_FILE
.getOpt())){
401 logger
.error(DATASOURCES_FILE
.getOpt() + " NOT JET IMPLEMENTED!!!");
406 verifyMemoryRequirements();
409 server
= new Server(httpPort
);
412 if(cmdLine
.hasOption(JMX
.getOpt())){
413 logger
.info("adding JMX support ...");
414 MBeanContainer mBeanContainer
= new MBeanContainer(ManagementFactory
.getPlatformMBeanServer());
415 server
.getContainer().addEventListener(mBeanContainer
);
416 mBeanContainer
.addBean(Log
.getLog());
417 mBeanContainer
.start();
420 if(cmdLine
.hasOption(WIN32SERVICE
.getOpt())){
421 Win32Service win32Service
= new Win32Service();
422 win32Service
.setServer(server
);
423 server
.setStopAtShutdown(true);
424 server
.addBean(win32Service
);
427 // add servelet contexts
431 // 1. default context
433 logger
.info("preparing default WebAppContext");
434 WebAppContext defaultWebappContext
= new WebAppContext();
436 setWebApp(defaultWebappContext
, defaultWebAppFile
);
437 defaultWebappContext
.setContextPath("/");
438 defaultWebappContext
.setTempDirectory(DEFAULT_WEBAPP_TEMP_FOLDER
);
440 // configure security context
441 // see for reference * http://docs.codehaus.org/display/JETTY/Realms
442 // * http://wiki.eclipse.org/Jetty/Starting/Porting_to_Jetty_7
443 HashLoginService loginService
= new HashLoginService();
444 loginService
.setConfig(USERHOME_CDM_LIBRARY_PATH
+ REALM_PROPERTIES_FILE
);
445 defaultWebappContext
.getSecurityHandler().setLoginService(loginService
);
448 // the defaultWebappContext MUST USE the super classloader
449 // otherwise the status page (index.jsp) might not work
450 defaultWebappContext
.setClassLoader(this.getClass().getClassLoader());
451 contexts
.addHandler(defaultWebappContext
);
454 // 2. cdm server contexts
456 server
.addLifeCycleListener(new LifeCycle
.Listener(){
459 public void lifeCycleFailure(LifeCycle event
, Throwable cause
) {
460 logger
.error("Jetty LifeCycleFailure", cause
);
464 public void lifeCycleStarted(LifeCycle event
) {
465 logger
.info("cdmserver has started, now adding CDM server contexts");
467 addCdmServerContexts(true);
468 } catch (IOException e1
) {
474 public void lifeCycleStarting(LifeCycle event
) {
478 public void lifeCycleStopped(LifeCycle event
) {
482 public void lifeCycleStopping(LifeCycle event
) {
488 logger
.info("setting contexts ...");
489 server
.setHandler(contexts
);
490 logger
.info("starting jetty ...");
495 // } catch(org.springframework.beans.BeansException e){
496 // Throwable rootCause = null;
497 // while(e.getCause() != null){
498 // rootCause = e.getCause();
500 // if(rootCause != null && rootCause.getClass().getSimpleName().equals("InvalidCdmVersionException")){
502 // logger.error("rootCause ----------->" + rootCause.getMessage());
503 //// for(CdmInstanceProperties props : configAndStatus){
504 //// if(props.getDataSourceName())
509 if(cmdLine
.hasOption(WIN32SERVICE
.getOpt())){
510 logger
.info("jetty has started as win32 service");
513 logger
.info(APPLICATION_NAME
+" stopped.");
521 private void verifyMemoryRequirements() {
523 verifyMemoryRequirement("PermGenSpace", PERM_GEN_SPACE_PER_INSTANCE
, JvmManager
.getPermGenSpaceUsage().getMax());
524 verifyMemoryRequirement("HeapSpace", HEAP_PER_INSTANCE
, JvmManager
.getHeapMemoryUsage().getMax());
528 private void verifyMemoryRequirement(String memoryName
, long requiredSpacePerIntance
, long availableSpace
) {
531 long requiredSpace
= configAndStatusSet
.size() * requiredSpacePerIntance
;
533 if(requiredSpace
> availableSpace
){
535 String message
= memoryName
+ " ("
536 + (availableSpace
/ MB
)
537 + "MB) insufficient for "
538 + configAndStatusSet
.size()
539 + " instances. Increase " + memoryName
+ " by "
540 + ((requiredSpace
- availableSpace
)/MB
)
543 logger
.error(message
+ " => disabling some instances!!!");
545 // disabling some instances
547 for(CdmInstanceProperties instanceProps
: configAndStatusSet
){
549 if(i
* requiredSpacePerIntance
> availableSpace
){
550 instanceProps
.setStatus(Status
.disabled
);
551 instanceProps
.getProblems().add("Disbled due to: " + message
);
558 * Configures and adds a {@link RollingFileAppender} to the root logger
560 * The log files of the cdm-remote instances are configured by the
561 * {@link eu.etaxonomy.cdm.remote.config.LoggingConfigurer}
563 private void configureFileLogger() {
565 PatternLayout layout
= new PatternLayout("%d %p [%c] - %m%n");
567 String logFile
= LOG_PATH
+ File
.separator
+ "cdmserver.log";
568 RollingFileAppender appender
= new RollingFileAppender(layout
, logFile
);
569 appender
.setMaxBackupIndex(3);
570 appender
.setMaxFileSize("2MB");
571 Logger
.getRootLogger().addAppender(appender
);
572 logger
.info("logging to :" + logFile
);
573 } catch (IOException e
) {
574 logger
.error("Creating RollingFileAppender failed:", e
);
578 private void addCdmServerContexts(boolean austostart
) throws IOException
{
580 for(CdmInstanceProperties conf
: configAndStatusSet
){
582 if(!conf
.isEnabled()){
583 logger
.info(conf
.getDataSourceName() + " is disabled => skipping");
586 conf
.setStatus(CdmInstanceProperties
.Status
.initializing
);
587 logger
.info("preparing WebAppContext for '"+ conf
.getDataSourceName() + "'");
588 WebAppContext cdmWebappContext
= new WebAppContext();
590 cdmWebappContext
.setContextPath("/"+conf
.getDataSourceName());
591 cdmWebappContext
.setTempDirectory(CDM_WEBAPP_TEMP_FOLDER
);
593 if(!bindJndiDataSource(conf
)){
594 // a problem with the datasource occurred skip this webapp
595 cdmWebappContext
= null;
596 logger
.error("a problem with the datasource occurred -> skipping /" + conf
.getDataSourceName());
597 conf
.setStatus(CdmInstanceProperties
.Status
.error
);
601 cdmWebappContext
.setAttribute(ATTRIBUTE_DATASOURCE_NAME
, conf
.getDataSourceName());
602 cdmWebappContext
.setAttribute(ATTRIBUTE_JDBC_JNDI_NAME
, conf
.getJdbcJndiName());
603 setWebApp(cdmWebappContext
, webappFile
);
605 cdmWebappContext
.setAttribute(ATTRIBUTE_CDM_LOGFILE
,
606 LOG_PATH
+ File
.separator
+ "cdm-"
607 + conf
.getDataSourceName() + ".log");
609 if(webappFile
.isDirectory() && isRunningFromSource()){
612 * when running the webapp from {projectpath} src/main/webapp we
613 * must assure that each web application is using it's own
614 * classloader thus we tell the WebAppClassLoader where the
615 * dependencies of the webapplication can be found. Otherwise
616 * the system classloader would load these resources.
618 logger
.info("Running webapp from source folder, thus adding java.class.path to WebAppClassLoader");
620 WebAppClassLoader classLoader
= new WebAppClassLoader(cdmWebappContext
);
622 String classPath
= System
.getProperty("java.class.path");
623 classLoader
.addClassPath(classPath
);
624 cdmWebappContext
.setClassLoader(classLoader
);
627 cdmWebappContext
.addLifeCycleListener(new WebAppContextListener(cdmWebappContext
));
628 contexts
.addHandler(cdmWebappContext
);
632 conf
.setStatus(CdmInstanceProperties
.Status
.starting
);
633 cdmWebappContext
.start();
634 if(!conf
.getStatus().equals(Status
.error
)){
635 conf
.setStatus(CdmInstanceProperties
.Status
.started
);
637 } catch (Exception e
) {
638 logger
.error("Could not start " + cdmWebappContext
.getContextPath());
639 conf
.setStatus(CdmInstanceProperties
.Status
.error
);
648 * @param webApplicationResource
650 private void setWebApp(WebAppContext context
, File webApplicationResource
) {
651 if(webApplicationResource
.isDirectory()){
652 context
.setResourceBase(webApplicationResource
.getAbsolutePath());
653 logger
.debug("setting directory " + webApplicationResource
.getAbsolutePath() + " as webapplication");
655 context
.setWar(webApplicationResource
.getAbsolutePath());
656 logger
.debug("setting war file " + webApplicationResource
.getAbsolutePath() + " as webapplication");
663 private boolean isRunningFromSource() {
664 String webappPathNormalized
= webappFile
.getAbsolutePath().replace('\\', '/');
665 return webappPathNormalized
.endsWith("src/main/webapp") || webappPathNormalized
.endsWith("cdmlib-remote/target/cdmserver");
669 * @param dataSourceName
672 private CdmInstanceProperties
findConfigAndStatusFor(String dataSourceName
){
673 for(CdmInstanceProperties props
: configAndStatusSet
){
674 if(props
.getDataSourceName().equals(dataSourceName
)){
683 * @param webAppContext
684 * @param attributeName
688 @SuppressWarnings("unchecked")
689 private <T
> T
getServletContextAttribute(WebAppContext webAppContext
, String attributeName
, Class
<T
> type
) {
691 Context servletContext
= webAppContext
.getServletContext();
692 Object value
= servletContext
.getAttribute(attributeName
);
693 if( value
!= null && type
.isAssignableFrom(value
.getClass())){