improved startup strategy & better status reporting
[cdmlib.git] / cdm-server / src / main / java / eu / etaxonomy / cdm / server / Bootloader.java
1 // $Id$
2 /**
3 * Copyright (C) 2009 EDIT
4 * European Distributed Institute of Taxonomy
5 * http://www.e-taxonomy.eu
6 *
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.
9 */
10
11 package eu.etaxonomy.cdm.server;
12
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
19 import java.io.File;
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;
27 import java.net.URL;
28 import java.sql.Connection;
29 import java.sql.SQLException;
30 import java.util.EventListener;
31 import java.util.Set;
32
33 import javax.naming.NamingException;
34 import javax.servlet.ServletContextEvent;
35 import javax.servlet.ServletContextListener;
36 import javax.sql.DataSource;
37
38 import org.apache.commons.cli.CommandLine;
39 import org.apache.commons.cli.CommandLineParser;
40 import org.apache.commons.cli.GnuParser;
41 import org.apache.commons.cli.HelpFormatter;
42 import org.apache.commons.cli.ParseException;
43 import org.apache.commons.io.FileUtils;
44 import org.apache.log4j.Logger;
45 import org.eclipse.jetty.jmx.MBeanContainer;
46 import org.eclipse.jetty.server.Server;
47 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
48 import org.eclipse.jetty.util.component.LifeCycle;
49 import org.eclipse.jetty.util.log.Log;
50 import org.eclipse.jetty.webapp.WebAppClassLoader;
51 import org.eclipse.jetty.webapp.WebAppContext;
52
53
54 /**
55 * A bootstrap class for starting Jetty Runner using an embedded war.
56 *
57 * Recommended start options for the java virtual machine:
58 * <pre>
59 * -Xmx1024M
60 *
61 * -XX:PermSize=128m
62 * -XX:MaxPermSize=192m
63 *
64 * -XX:+UseConcMarkSweepGC
65 * -XX:+CMSClassUnloadingEnabled
66 * -XX:+CMSPermGenSweepingEnabled
67 * </pre>
68 *
69 * @version $Revision$
70 */
71 public final class Bootloader {
72 //private static final String DEFAULT_WARFILE = "target/";
73
74 private static final Logger logger = Logger.getLogger(Bootloader.class);
75
76 private static final String DATASOURCE_BEANDEF_FILE = "datasources.xml";
77 private static final String USERHOME_CDM_LIBRARY_PATH = System.getProperty("user.home")+File.separator+".cdmLibrary"+File.separator;
78 private static final String TMP_PATH = USERHOME_CDM_LIBRARY_PATH + "server" + File.separator;
79
80 private static final String APPLICATION_NAME = "CDM Server";
81 private static final String WAR_POSTFIX = ".war";
82
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);
87
88 private static final String ATTRIBUTE_JDBC_JNDI_NAME = "cdm.jdbcJndiName";
89
90 private static final int KB = 1024;
91
92 private static Set<CdmInstanceProperties> configAndStatus = null;
93
94 public static Set<CdmInstanceProperties> getConfigAndStatus() {
95 return configAndStatus;
96 }
97
98 private static File webappFile = null;
99 private static File defaultWebAppFile = null;
100
101 private static Server server = null;
102 private static ContextHandlerCollection contexts = new ContextHandlerCollection();
103
104 private static Bootloader instance = null;
105
106 private static CommandLine cmdLine;
107
108
109 private Bootloader() {
110
111
112 }
113
114 public static Bootloader getBootloader(){
115 if(instance == null){
116 instance = new Bootloader();
117 }
118 return instance;
119 }
120
121 private static Set<CdmInstanceProperties> loadDataSources(){
122 if(configAndStatus == null){
123 File datasourcesFile = new File(USERHOME_CDM_LIBRARY_PATH, DATASOURCE_BEANDEF_FILE);
124 configAndStatus = DataSourcePropertyParser.parseDataSourceConfigs(datasourcesFile);
125 logger.info("cdm server instance names loaded: "+ configAndStatus.toString());
126 }
127 return configAndStatus;
128 }
129
130 public static int writeStreamTo(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
131 int available = Math.min(input.available(), 256 * KB);
132 byte[] buffer = new byte[Math.max(bufferSize, available)];
133 int answer = 0;
134 int count = input.read(buffer);
135 while (count >= 0) {
136 output.write(buffer, 0, count);
137 answer += count;
138 count = input.read(buffer);
139 }
140 return answer;
141 }
142
143 private static boolean bindJndiDataSource(CdmInstanceProperties conf) {
144 try {
145 Class<DataSource> dsCass = (Class<DataSource>) Thread.currentThread().getContextClassLoader().loadClass("com.mchange.v2.c3p0.ComboPooledDataSource");
146 DataSource datasource = dsCass.newInstance();
147 dsCass.getMethod("setDriverClass", new Class[] {String.class}).invoke(datasource, new Object[] {conf.getDriverClass()});
148 dsCass.getMethod("setJdbcUrl", new Class[] {String.class}).invoke(datasource, new Object[] {conf.getUrl()});
149 dsCass.getMethod("setUser", new Class[] {String.class}).invoke(datasource, new Object[] {conf.getUsername()});
150 dsCass.getMethod("setPassword", new Class[] {String.class}).invoke(datasource, new Object[] {conf.getPassword()});
151
152 Connection connection = null;
153 String sqlerror = null;
154 try {
155 connection = datasource.getConnection();
156 connection.close();
157 } catch (SQLException e) {
158 sqlerror = e.getMessage() + "["+ e.getSQLState() + "]";
159 conf.getProblems().add(sqlerror);
160 if(connection != null){
161 try {connection.close();} catch (SQLException e1) { /* IGNORE */ }
162 }
163 logger.error(conf.toString() + " has problem : "+ sqlerror );
164 }
165
166 if(!conf.hasProblems()){
167 logger.info("binding jndi datasource at " + conf.getJdbcJndiName() + " with "+conf.getUsername() +"@"+ conf.getUrl());
168 org.eclipse.jetty.plus.jndi.Resource jdbcResource = new org.eclipse.jetty.plus.jndi.Resource(conf.getJdbcJndiName(), datasource);
169 return true;
170 }
171
172 } catch (IllegalArgumentException e) {
173 logger.error(e);
174 e.printStackTrace();
175 } catch (SecurityException e) {
176 logger.error(e);
177 } catch (ClassNotFoundException e) {
178 logger.error(e);
179 } catch (InstantiationException e) {
180 logger.error(e);
181 } catch (IllegalAccessException e) {
182 logger.error(e);
183 } catch (InvocationTargetException e) {
184 logger.error(e);
185 } catch (NoSuchMethodException e) {
186 logger.error(e);
187 } catch (NamingException e) {
188 logger.error(e);
189 }
190 return false;
191 }
192
193 private static void parseCommandOptions(String[] args) throws ParseException {
194 CommandLineParser parser = new GnuParser();
195 cmdLine = parser.parse( CommandOptions.getOptions(), args );
196
197 // print the help message
198 if(cmdLine.hasOption(HELP.getOpt())){
199 HelpFormatter formatter = new HelpFormatter();
200 formatter.printHelp( "java .. ", CommandOptions.getOptions() );
201 System.exit(0);
202 }
203 }
204
205
206 private static File extractWar(String warName) throws IOException, FileNotFoundException {
207 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
208 String warFileName = warName + WAR_POSTFIX;
209 URL resource = classLoader.getResource(warFileName);
210 if (resource == null) {
211 logger.error("Could not find the " + warFileName + " on classpath!");
212 System.exit(1);
213 }
214
215 File warFile = new File(TMP_PATH, warName + "-" + WAR_POSTFIX);
216 logger.info("Extracting " + warFileName + " to " + warFile + " ...");
217
218 writeStreamTo(resource.openStream(), new FileOutputStream(warFile), 8 * KB);
219
220 logger.info("Extracted " + warFileName);
221 return warFile;
222 }
223
224
225 /**
226 * MAIN METHOD
227 *
228 * @param args
229 * @throws Exception
230 */
231 public static void main(String[] args) throws Exception {
232
233 Bootloader bootloader = Bootloader.getBootloader();
234
235 bootloader.parseCommandOptions(args);
236
237 bootloader.startServer();
238 }
239
240 private static void startServer() throws IOException,
241 FileNotFoundException, Exception, InterruptedException {
242 logger.info("Starting "+APPLICATION_NAME);
243 logger.info("Using " + System.getProperty("user.home") + " as home directory. Can be specified by -Duser.home=<FOLDER>");
244
245 //assure TMP_PATH exists and clean it up
246 File tempDir = new File(TMP_PATH);
247 if(!tempDir.exists() && !tempDir.mkdirs()){
248 logger.error("Error creating temporary directory for webapplications " + tempDir.getAbsolutePath());
249 System.exit(-1);
250 } else {
251 if(FileUtils.deleteQuietly(tempDir)){
252 tempDir.mkdirs();
253 logger.info("Old webapplications successfully cleared");
254 }
255 }
256 tempDir = null;
257
258 // WARFILE
259 if(cmdLine.hasOption(WEBAPP.getOpt())){
260 webappFile = new File(cmdLine.getOptionValue(WEBAPP.getOpt()));
261 if(webappFile.isDirectory()){
262 logger.info("using user defined web application folder: " + webappFile.getAbsolutePath());
263 } else {
264 logger.info("using user defined warfile: " + webappFile.getAbsolutePath());
265 }
266 if(isRunningFromSource()){
267 defaultWebAppFile = new File("./src/main/webapp");
268 } else {
269 //defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME);
270 }
271 } else {
272 webappFile = extractWar(CDM_WEBAPP_WAR_NAME);
273 defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME);
274 }
275
276 // HTTP Port
277 int httpPort = 8080;
278 if(cmdLine.hasOption(HTTP_PORT.getOpt())){
279 try {
280 httpPort = Integer.parseInt(cmdLine.getOptionValue(HTTP_PORT.getOpt()));
281 logger.info(HTTP_PORT.getOpt()+" set to "+cmdLine.getOptionValue(HTTP_PORT.getOpt()));
282 } catch (NumberFormatException e) {
283 logger.error("Supplied portnumber is not an integer");
284 System.exit(-1);
285 }
286 }
287
288 if(cmdLine.hasOption(DATASOURCES_FILE.getOpt())){
289 logger.error(DATASOURCES_FILE.getOpt() + " NOT JET IMPLEMENTED!!!");
290 }
291
292 loadDataSources();
293
294 //System.setProperty("DEBUG", "true");
295
296 server = new Server(httpPort);
297
298 // JMX support
299 if(cmdLine.hasOption(JMX.getOpt())){
300 logger.info("adding JMX support ...");
301 MBeanContainer mBeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
302 server.getContainer().addEventListener(mBeanContainer);
303 mBeanContainer.addBean(Log.getLog());
304 mBeanContainer.start();
305 }
306
307 // add servelet contexts
308
309
310 //
311 // 1. default context
312 //
313 logger.info("preparing default WebAppContext");
314 WebAppContext defaultWebappContext = new WebAppContext();
315 setWebApp(defaultWebappContext, defaultWebAppFile);
316 defaultWebappContext.setContextPath("/");
317 defaultWebappContext.setTempDirectory(DEFAULT_WEBAPP_TEMP_FOLDER);
318 contexts.addHandler(defaultWebappContext);
319
320 //
321 // 2. cdm server contexts
322 //
323 server.addLifeCycleListener(new LifeCycle.Listener(){
324
325 @Override
326 public void lifeCycleFailure(LifeCycle event, Throwable cause) {
327 }
328
329 @Override
330 public void lifeCycleStarted(LifeCycle event) {
331 logger.info("cdmserver has started, now adding CDM server contexts");
332 try {
333 addCdmServerContexts(true);
334 } catch (IOException e1) {
335 logger.error(e1);
336 }
337 }
338
339 @Override
340 public void lifeCycleStarting(LifeCycle event) {
341 }
342
343 @Override
344 public void lifeCycleStopped(LifeCycle event) {
345 }
346
347 @Override
348 public void lifeCycleStopping(LifeCycle event) {
349 }
350
351 });
352
353
354 logger.info("setting contexts ...");
355 server.setHandler(contexts);
356 logger.info("starting jetty ...");
357 server.start();
358 server.join();
359 logger.info(APPLICATION_NAME+" stopped.");
360 System.exit(0);
361 }
362
363 private static void addCdmServerContexts(boolean austostart) throws IOException {
364
365 for(CdmInstanceProperties conf : configAndStatus){
366 conf.setStatus(CdmInstanceProperties.Status.initializing);
367 logger.info("preparing WebAppContext for '"+ conf.getDataSourceName() + "'");
368 WebAppContext cdmWebappContext = new WebAppContext();
369
370 cdmWebappContext.setContextPath("/"+conf.getDataSourceName());
371 cdmWebappContext.setTempDirectory(CDM_WEBAPP_TEMP_FOLDER);
372
373 if(!bindJndiDataSource(conf)){
374 // a problem with the datasource occurred skip this webapp
375 cdmWebappContext = null;
376 logger.error("a problem with the datasource occurred -> skipping /" + conf.getDataSourceName());
377 conf.setStatus(CdmInstanceProperties.Status.error);
378 continue;
379 }
380
381 cdmWebappContext.setAttribute(ATTRIBUTE_JDBC_JNDI_NAME, conf.getJdbcJndiName());
382 setWebApp(cdmWebappContext, webappFile);
383
384 if(webappFile.isDirectory() && isRunningFromSource()){
385
386 /*
387 * when running the webapp from {projectpath} src/main/webapp we
388 * must assure that each web application is using it's own
389 * classloader thus we tell the WebAppClassLoader where the
390 * dependencies of the webapplication can be found. Otherwise
391 * the system classloader would load these resources.
392 */
393 logger.info("Running webapp from source folder, thus adding java.class.path to WebAppClassLoader");
394 String classPath = System.getProperty("java.class.path");
395 WebAppClassLoader classLoader = new WebAppClassLoader(cdmWebappContext);
396 classLoader.addClassPath(classPath);
397 cdmWebappContext.setClassLoader(classLoader);
398 }
399
400 contexts.addHandler(cdmWebappContext);
401
402 if(austostart){
403 try {
404 conf.setStatus(CdmInstanceProperties.Status.starting);
405 cdmWebappContext.start();
406 conf.setStatus(CdmInstanceProperties.Status.started);
407 } catch (Exception e) {
408 logger.error("Could not start " + cdmWebappContext.getContextPath());
409 conf.setStatus(CdmInstanceProperties.Status.error);
410 }
411 }
412
413 }
414 }
415
416 private static void setWebApp(WebAppContext context, File webApplicationResource) {
417 if(webApplicationResource.isDirectory()){
418 context.setResourceBase(webApplicationResource.getAbsolutePath());
419 logger.debug("setting directory " + webApplicationResource.getAbsolutePath() + " as webapplication");
420 } else {
421 context.setWar(webApplicationResource.getAbsolutePath());
422 logger.debug("setting war file " + webApplicationResource.getAbsolutePath() + " as webapplication");
423 }
424 }
425
426 private static boolean isRunningFromSource() {
427 String webappPathNormalized = webappFile.getAbsolutePath().replace('\\', '/');
428 return webappPathNormalized.endsWith("src/main/webapp") || webappPathNormalized.endsWith("cdmlib-remote/target/cdmserver");
429 }
430 }