logging to file improved
[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.Set;
31
32 import javax.naming.NamingException;
33 import javax.sql.DataSource;
34
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;
51
52
53 /**
54 * A bootstrap class for starting Jetty Runner using an embedded war.
55 *
56 * Recommended start options for the java virtual machine:
57 * <pre>
58 * -Xmx1024M
59 *
60 * -XX:PermSize=128m
61 * -XX:MaxPermSize=192m
62 *
63 * -XX:+UseConcMarkSweepGC
64 * -XX:+CMSClassUnloadingEnabled
65 * -XX:+CMSPermGenSweepingEnabled
66 * </pre>
67 *
68 * @version $Revision$
69 */
70 public final class Bootloader {
71 //private static final String DEFAULT_WARFILE = "target/";
72
73 private static final Logger logger = Logger.getLogger(Bootloader.class);
74
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;
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 private static final String CDM_LOGFILE = "cdm.logfile";
90
91 private static final int KB = 1024;
92
93 private Set<CdmInstanceProperties> configAndStatus = null;
94
95 public Set<CdmInstanceProperties> getConfigAndStatus() {
96 return configAndStatus;
97 }
98
99 private File webappFile = null;
100 private File defaultWebAppFile = null;
101
102 private Server server = null;
103 private ContextHandlerCollection contexts = new ContextHandlerCollection();
104
105 private CommandLine cmdLine;
106
107 /* thread save singleton implementation */
108
109 private static Bootloader instance = new Bootloader();
110
111 private Bootloader() {}
112
113 public synchronized static Bootloader getBootloader(){
114 return instance;
115 }
116
117 /* end of singleton implementation */
118
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());
124 }
125 return configAndStatus;
126 }
127
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)];
131 int answer = 0;
132 int count = input.read(buffer);
133 while (count >= 0) {
134 output.write(buffer, 0, count);
135 answer += count;
136 count = input.read(buffer);
137 }
138 return answer;
139 }
140
141 private boolean bindJndiDataSource(CdmInstanceProperties conf) {
142 try {
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()});
149
150 Connection connection = null;
151 String sqlerror = null;
152 try {
153 connection = datasource.getConnection();
154 connection.close();
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 */ }
160 }
161 logger.error(conf.toString() + " has problem : "+ sqlerror );
162 }
163
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);
167 return true;
168 }
169
170 } catch (IllegalArgumentException e) {
171 logger.error(e);
172 e.printStackTrace();
173 } catch (SecurityException e) {
174 logger.error(e);
175 } catch (ClassNotFoundException e) {
176 logger.error(e);
177 } catch (InstantiationException e) {
178 logger.error(e);
179 } catch (IllegalAccessException e) {
180 logger.error(e);
181 } catch (InvocationTargetException e) {
182 logger.error(e);
183 } catch (NoSuchMethodException e) {
184 logger.error(e);
185 } catch (NamingException e) {
186 logger.error(e);
187 }
188 return false;
189 }
190
191 private void parseCommandOptions(String[] args) throws ParseException {
192 CommandLineParser parser = new GnuParser();
193 cmdLine = parser.parse( CommandOptions.getOptions(), args );
194
195 // print the help message
196 if(cmdLine.hasOption(HELP.getOpt())){
197 HelpFormatter formatter = new HelpFormatter();
198 formatter.printHelp( "java .. ", CommandOptions.getOptions() );
199 System.exit(0);
200 }
201 }
202
203
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!");
210 System.exit(1);
211 }
212
213 File warFile = new File(TMP_PATH, warName + "-" + WAR_POSTFIX);
214 logger.info("Extracting " + warFileName + " to " + warFile + " ...");
215
216 writeStreamTo(resource.openStream(), new FileOutputStream(warFile), 8 * KB);
217
218 logger.info("Extracted " + warFileName);
219 return warFile;
220 }
221
222
223 /**
224 * MAIN METHOD
225 *
226 * @param args
227 * @throws Exception
228 */
229 public static void main(String[] args) throws Exception {
230
231 Bootloader bootloader = Bootloader.getBootloader();
232
233 bootloader.parseCommandOptions(args);
234
235 bootloader.startServer();
236 }
237
238 private void startServer() throws IOException,
239 FileNotFoundException, Exception, InterruptedException {
240
241
242 //assure LOG_PATH exists
243 File logPath = new File(LOG_PATH);
244 if(!logPath.exists()){
245 FileUtils.forceMkdir(new File(LOG_PATH));
246 }
247
248 //append logger
249 configureFileLogger();
250
251 logger.info("Starting "+APPLICATION_NAME);
252 logger.info("Using " + System.getProperty("user.home") + " as home directory. Can be specified by -Duser.home=<FOLDER>");
253
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());
258 System.exit(-1);
259 } else {
260 if(FileUtils.deleteQuietly(tempDir)){
261 tempDir.mkdirs();
262 logger.info("Old webapplications successfully cleared");
263 }
264 }
265 tempDir = null;
266
267
268 // WARFILE
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());
273 } else {
274 logger.info("using user defined warfile: " + webappFile.getAbsolutePath());
275 }
276 if(isRunningFromSource()){
277 //FIXME check if all local paths are valid !!!!
278 defaultWebAppFile = new File("./src/main/webapp");
279
280 } else {
281 //defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME);
282 }
283 } else {
284 webappFile = extractWar(CDM_WEBAPP_WAR_NAME);
285 defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME);
286 }
287
288 // HTTP Port
289 int httpPort = 8080;
290 if(cmdLine.hasOption(HTTP_PORT.getOpt())){
291 try {
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");
296 System.exit(-1);
297 }
298 }
299
300 if(cmdLine.hasOption(DATASOURCES_FILE.getOpt())){
301 logger.error(DATASOURCES_FILE.getOpt() + " NOT JET IMPLEMENTED!!!");
302 }
303
304 loadDataSources();
305
306 //System.setProperty("DEBUG", "true");
307
308 server = new Server(httpPort);
309
310 // JMX support
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();
317 }
318
319 // add servelet contexts
320
321
322 //
323 // 1. default context
324 //
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);
330 // Important:
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);
335
336 //
337 // 2. cdm server contexts
338 //
339 server.addLifeCycleListener(new LifeCycle.Listener(){
340
341 @Override
342 public void lifeCycleFailure(LifeCycle event, Throwable cause) {
343 }
344
345 @Override
346 public void lifeCycleStarted(LifeCycle event) {
347 logger.info("cdmserver has started, now adding CDM server contexts");
348 try {
349 addCdmServerContexts(true);
350 } catch (IOException e1) {
351 logger.error(e1);
352 }
353 }
354
355 @Override
356 public void lifeCycleStarting(LifeCycle event) {
357 }
358
359 @Override
360 public void lifeCycleStopped(LifeCycle event) {
361 }
362
363 @Override
364 public void lifeCycleStopping(LifeCycle event) {
365 }
366
367 });
368
369
370 logger.info("setting contexts ...");
371 server.setHandler(contexts);
372 logger.info("starting jetty ...");
373 server.start();
374 server.join();
375 logger.info(APPLICATION_NAME+" stopped.");
376 System.exit(0);
377 }
378
379 /**
380 * Configueres and adds a {@link RollingFileAppender} to the root logger
381 *
382 * The log files of the cdm-remote instances are configured by the
383 * {@link eu.etaxonomy.cdm.remote.config.LoggingConfigurer}
384 */
385 private void configureFileLogger() {
386
387 PatternLayout layout = new PatternLayout("%d %p [%c] - %m%n");
388 try {
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);
397 }
398 }
399
400 private void addCdmServerContexts(boolean austostart) throws IOException {
401
402 for(CdmInstanceProperties conf : configAndStatus){
403 conf.setStatus(CdmInstanceProperties.Status.initializing);
404 logger.info("preparing WebAppContext for '"+ conf.getDataSourceName() + "'");
405 WebAppContext cdmWebappContext = new WebAppContext();
406
407 cdmWebappContext.setContextPath("/"+conf.getDataSourceName());
408 cdmWebappContext.setTempDirectory(CDM_WEBAPP_TEMP_FOLDER);
409
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);
415 continue;
416 }
417
418 cdmWebappContext.setAttribute(ATTRIBUTE_JDBC_JNDI_NAME, conf.getJdbcJndiName());
419 setWebApp(cdmWebappContext, webappFile);
420
421 cdmWebappContext.setAttribute(CDM_LOGFILE,
422 LOG_PATH + File.separator + "cdm-"
423 + conf.getDataSourceName() + ".log");
424
425 if(webappFile.isDirectory() && isRunningFromSource()){
426
427 /*
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.
433 */
434 logger.info("Running webapp from source folder, thus adding java.class.path to WebAppClassLoader");
435
436 WebAppClassLoader classLoader = new WebAppClassLoader(cdmWebappContext);
437
438 String classPath = System.getProperty("java.class.path");
439 classLoader.addClassPath(classPath);
440 cdmWebappContext.setClassLoader(classLoader);
441 }
442
443 contexts.addHandler(cdmWebappContext);
444
445 if(austostart){
446 try {
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);
453 }
454 }
455
456 }
457 }
458
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");
463 } else {
464 context.setWar(webApplicationResource.getAbsolutePath());
465 logger.debug("setting war file " + webApplicationResource.getAbsolutePath() + " as webapplication");
466 }
467 }
468
469 private boolean isRunningFromSource() {
470 String webappPathNormalized = webappFile.getAbsolutePath().replace('\\', '/');
471 return webappPathNormalized.endsWith("src/main/webapp") || webappPathNormalized.endsWith("cdmlib-remote/target/cdmserver");
472 }
473 }