Project

General

Profile

Download (18.1 KB) Statistics
| Branch: | Tag: | Revision:
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
import static eu.etaxonomy.cdm.server.CommandOptions.WIN32SERVICE;
19

    
20
import java.io.File;
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;
28
import java.net.URL;
29
import java.sql.Connection;
30
import java.sql.SQLException;
31
import java.util.Set;
32

    
33
import javax.naming.NamingException;
34
import javax.sql.DataSource;
35

    
36
import org.apache.commons.cli.CommandLine;
37
import org.apache.commons.cli.CommandLineParser;
38
import org.apache.commons.cli.GnuParser;
39
import org.apache.commons.cli.HelpFormatter;
40
import org.apache.commons.cli.ParseException;
41
import org.apache.commons.io.FileUtils;
42
import org.apache.log4j.Logger;
43
import org.apache.log4j.PatternLayout;
44
import org.apache.log4j.RollingFileAppender;
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
import eu.etaxonomy.cdm.server.CdmInstanceProperties.Status;
54
import eu.etaxonomy.cdm.server.win32service.Win32Service;
55

    
56

    
57
/**
58
 * A bootstrap class for starting Jetty Runner using an embedded war.
59
 * 
60
 * Recommended start options for the java virtual machine: 
61
 * <pre>
62
 * -Xmx1024M
63
 * 
64
 * -XX:PermSize=128m 
65
 * -XX:MaxPermSize=192m
66
 * 
67
 * -XX:+UseConcMarkSweepGC 
68
 * -XX:+CMSClassUnloadingEnabled
69
 * -XX:+CMSPermGenSweepingEnabled
70
 * </pre>
71
 * 
72
 * @version $Revision$
73
 */
74
public final class Bootloader {
75
	//private static final String DEFAULT_WARFILE = "target/";
76

    
77
	private static final Logger logger = Logger.getLogger(Bootloader.class);
78
	
79
	private static final String DATASOURCE_BEANDEF_FILE = "datasources.xml";
80
	private static final String USERHOME_CDM_LIBRARY_PATH = System.getProperty("user.home")+File.separator+".cdmLibrary"+File.separator;
81
	private static final String TMP_PATH = USERHOME_CDM_LIBRARY_PATH + "server" + File.separator;
82
	private static final String LOG_PATH = USERHOME_CDM_LIBRARY_PATH + "log" + File.separator;
83
	
84
	private static final String APPLICATION_NAME = "CDM Server";
85
    private static final String WAR_POSTFIX = ".war";
86
    
87
    private static final String CDM_WEBAPP_WAR_NAME = "cdmserver";
88
    private static final String DEFAULT_WEBAPP_WAR_NAME = "default-webapp";
89
    private static final File DEFAULT_WEBAPP_TEMP_FOLDER = new File(TMP_PATH + DEFAULT_WEBAPP_WAR_NAME);
90
    private static final File CDM_WEBAPP_TEMP_FOLDER = new File(TMP_PATH + CDM_WEBAPP_WAR_NAME);
91
    
92
    private static final String ATTRIBUTE_JDBC_JNDI_NAME = "cdm.jdbcJndiName";
93
    private static final String CDM_LOGFILE = "cdm.logfile";
94
    
95
    // memory requirements
96
    private static final long MB = 1024 * 1024;
97
    private static final long PERM_GEN_SPACE_PER_INSTANCE = 64 * MB;
98
    private static final long HEAP_PER_INSTANCE = 300 * MB;
99
    
100
    private static final int KB = 1024;
101
    
102
    private Set<CdmInstanceProperties> configAndStatus = null;
103
    
104
    public Set<CdmInstanceProperties> getConfigAndStatus() {
105
		return configAndStatus;
106
	}
107

    
108
	private File webappFile = null;
109
    private File defaultWebAppFile = null;
110
    
111
    private Server server = null;
112
    private ContextHandlerCollection contexts = new ContextHandlerCollection();
113
    
114
    private CommandLine cmdLine;
115
    
116
    /* thread save singleton implementation */
117

    
118
	private static Bootloader instance = new Bootloader();
119

    
120
    private Bootloader() {}
121
    
122
    public synchronized static Bootloader getBootloader(){
123
    	return instance;
124
    }
125
    
126
    /* end of singleton implementation */
127
    
128
    private Set<CdmInstanceProperties> loadDataSources(){
129
    	if(configAndStatus == null){
130
    		File datasourcesFile = new File(USERHOME_CDM_LIBRARY_PATH, DATASOURCE_BEANDEF_FILE); 
131
    		configAndStatus = DataSourcePropertyParser.parseDataSourceConfigs(datasourcesFile);
132
        	logger.info("cdm server instance names loaded: "+ configAndStatus.toString());
133
    	}
134
    	return configAndStatus;
135
    }
136

    
137
    public int writeStreamTo(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
138
        int available = Math.min(input.available(), 256 * KB);
139
        byte[] buffer = new byte[Math.max(bufferSize, available)];
140
        int answer = 0;
141
        int count = input.read(buffer);
142
        while (count >= 0) {
143
            output.write(buffer, 0, count);
144
            answer += count;
145
            count = input.read(buffer);
146
        }
147
        return answer;
148
    }
149

    
150
	private boolean bindJndiDataSource(CdmInstanceProperties conf) {
151
		try {
152
			Class<DataSource> dsCass = (Class<DataSource>) Thread.currentThread().getContextClassLoader().loadClass("com.mchange.v2.c3p0.ComboPooledDataSource");
153
			DataSource datasource = dsCass.newInstance();
154
			dsCass.getMethod("setDriverClass", new Class[] {String.class}).invoke(datasource, new Object[] {conf.getDriverClass()});
155
			dsCass.getMethod("setJdbcUrl", new Class[] {String.class}).invoke(datasource, new Object[] {conf.getUrl()});
156
			dsCass.getMethod("setUser", new Class[] {String.class}).invoke(datasource, new Object[] {conf.getUsername()});
157
			dsCass.getMethod("setPassword", new Class[] {String.class}).invoke(datasource, new Object[] {conf.getPassword()});
158
			
159
			Connection connection = null;
160
			String sqlerror = null;
161
			try {
162
				connection = datasource.getConnection();
163
				connection.close();
164
			} catch (SQLException e) {
165
				sqlerror = e.getMessage() + "["+ e.getSQLState() + "]";
166
				conf.getProblems().add(sqlerror);
167
				if(connection !=  null){
168
					try {connection.close();} catch (SQLException e1) { /* IGNORE */ }
169
				}
170
				logger.error(conf.toString() + " has problem : "+ sqlerror );
171
			}
172
			
173
			if(!conf.hasProblems()){
174
				logger.info("binding jndi datasource at " + conf.getJdbcJndiName() + " with "+conf.getUsername() +"@"+ conf.getUrl());
175
				org.eclipse.jetty.plus.jndi.Resource jdbcResource = new org.eclipse.jetty.plus.jndi.Resource(conf.getJdbcJndiName(), datasource);
176
				return true;				
177
			}
178
			
179
		} catch (IllegalArgumentException e) {
180
			logger.error(e);
181
			e.printStackTrace();
182
		} catch (SecurityException e) {
183
			logger.error(e);
184
		} catch (ClassNotFoundException e) {
185
			logger.error(e);
186
		} catch (InstantiationException e) {
187
			logger.error(e);
188
		} catch (IllegalAccessException e) {
189
			logger.error(e);
190
		} catch (InvocationTargetException e) {
191
			logger.error(e);
192
		} catch (NoSuchMethodException e) {
193
			logger.error(e);
194
		} catch (NamingException e) {
195
			logger.error(e);
196
		}
197
		return false;
198
	}
199
	
200
	private void parseCommandOptions(String[] args) throws ParseException {
201
		CommandLineParser parser = new GnuParser();
202
		cmdLine = parser.parse( CommandOptions.getOptions(), args );
203
		
204
		 // print the help message
205
		 if(cmdLine.hasOption(HELP.getOpt())){
206
			 HelpFormatter formatter = new HelpFormatter();
207
			 formatter.printHelp( "java .. ", CommandOptions.getOptions() );
208
			 System.exit(0);
209
		 }
210
	}
211

    
212

    
213
	private File extractWar(String warName) throws IOException, FileNotFoundException {
214
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
215
		String warFileName = warName + WAR_POSTFIX;
216
    	URL resource = classLoader.getResource(warFileName);
217
    	if (resource == null) {
218
    		logger.error("Could not find the " + warFileName + " on classpath!");
219
    		System.exit(1);
220
    	}
221
    	
222
    	File warFile = new File(TMP_PATH, warName + "-" + WAR_POSTFIX);
223
    	logger.info("Extracting " + warFileName + " to " + warFile + " ...");
224
    	
225
    	writeStreamTo(resource.openStream(), new FileOutputStream(warFile), 8 * KB);
226
    	
227
    	logger.info("Extracted " + warFileName);
228
		return warFile;
229
	}
230
    
231
    
232
	/**
233
	 * MAIN METHOD
234
	 * 
235
	 * @param args
236
	 * @throws Exception
237
	 */
238
	public static void main(String[] args) throws Exception {
239
	
240
		Bootloader bootloader = Bootloader.getBootloader();
241
    	
242
		bootloader.parseCommandOptions(args);
243
		
244
		bootloader.startServer();
245
    }
246

    
247
	private void startServer() throws IOException,
248
			FileNotFoundException, Exception, InterruptedException {
249
		
250
		
251
		//assure LOG_PATH exists
252
		File logPath = new File(LOG_PATH);
253
		if(!logPath.exists()){
254
			FileUtils.forceMkdir(new File(LOG_PATH));
255
		}
256
		
257
		//append logger
258
		configureFileLogger();
259
		
260
		logger.info("Starting "+APPLICATION_NAME);
261
		logger.info("Using  " + System.getProperty("user.home") + " as home directory. Can be specified by -Duser.home=<FOLDER>");
262
    	
263
    	//assure TMP_PATH exists and clean it up
264
    	File tempDir = new File(TMP_PATH);
265
    	if(!tempDir.exists() && !tempDir.mkdirs()){
266
    		logger.error("Error creating temporary directory for webapplications " + tempDir.getAbsolutePath());
267
    		System.exit(-1);
268
    	} else {
269
    		if(FileUtils.deleteQuietly(tempDir)){
270
    			tempDir.mkdirs();
271
    			logger.info("Old webapplications successfully cleared");
272
    		}
273
    	}
274
    	tempDir = null;
275
    	 
276
    	
277
    	 // WARFILE
278
    	 if(cmdLine.hasOption(WEBAPP.getOpt())){
279
    		 webappFile = new File(cmdLine.getOptionValue(WEBAPP.getOpt()));
280
    		 if(webappFile.isDirectory()){
281
    			 logger.info("using user defined web application folder: " + webappFile.getAbsolutePath());    			     			 
282
    		 } else {
283
    			 logger.info("using user defined warfile: " + webappFile.getAbsolutePath());
284
    		 }
285
    		 if(isRunningFromSource()){
286
    			 //FIXME check if all local paths are valid !!!!
287
    	    	defaultWebAppFile = new File("./src/main/webapp");	
288
    	    	
289
    	     } else {
290
    	    	//defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME);
291
    	     }
292
    	 } else {    	 
293
    		 webappFile = extractWar(CDM_WEBAPP_WAR_NAME);
294
    		 defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME);
295
    	 }
296
    	 
297
    	 // HTTP Port
298
    	 int httpPort = 8080;
299
    	 if(cmdLine.hasOption(HTTP_PORT.getOpt())){
300
    		 try {
301
				httpPort = Integer.parseInt(cmdLine.getOptionValue(HTTP_PORT.getOpt()));
302
				logger.info(HTTP_PORT.getOpt()+" set to "+cmdLine.getOptionValue(HTTP_PORT.getOpt()));
303
			} catch (NumberFormatException e) {
304
				logger.error("Supplied portnumber is not an integer");
305
				System.exit(-1);
306
			}
307
    	 }
308
    	 
309
    	 if(cmdLine.hasOption(DATASOURCES_FILE.getOpt())){
310
    		 logger.error(DATASOURCES_FILE.getOpt() + " NOT JET IMPLEMENTED!!!");
311
    	 }
312
    	
313
    	loadDataSources();
314
    	
315
    	verifyMemoryRequirements();
316
    	
317
    	
318
		server = new Server(httpPort);
319
		
320
		// JMX support
321
		if(cmdLine.hasOption(JMX.getOpt())){
322
			logger.info("adding JMX support ...");
323
			MBeanContainer mBeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
324
			server.getContainer().addEventListener(mBeanContainer);
325
			mBeanContainer.addBean(Log.getLog());
326
			mBeanContainer.start();
327
		}
328
		
329
		if(cmdLine.hasOption(WIN32SERVICE.getOpt())){
330
			Win32Service win32Service = new Win32Service();
331
			win32Service.setServer(server);
332
			server.setStopAtShutdown(true);
333
			server.addBean(win32Service);
334
		}
335
		
336
		// add servelet contexts
337
		
338
		
339
		//
340
		// 1. default context
341
		//
342
		logger.info("preparing default WebAppContext");
343
    	WebAppContext defaultWebappContext = new WebAppContext();
344
    	setWebApp(defaultWebappContext, defaultWebAppFile);
345
        defaultWebappContext.setContextPath("/");
346
        defaultWebappContext.setTempDirectory(DEFAULT_WEBAPP_TEMP_FOLDER);
347
        // Important:
348
        // the defaultWebappContext MUST USE the super classloader 
349
        // otherwise the status page (index.jsp) might not work
350
        defaultWebappContext.setClassLoader(this.getClass().getClassLoader());
351
    	contexts.addHandler(defaultWebappContext);
352
    	
353
    	//
354
		// 2. cdm server contexts
355
    	//
356
    	server.addLifeCycleListener(new LifeCycle.Listener(){
357

    
358
			@Override
359
			public void lifeCycleFailure(LifeCycle event, Throwable cause) {
360
			}
361

    
362
			@Override
363
			public void lifeCycleStarted(LifeCycle event) {
364
				logger.info("cdmserver has started, now adding CDM server contexts");
365
				try {
366
					addCdmServerContexts(true);
367
				} catch (IOException e1) {
368
					logger.error(e1);
369
				}		
370
			}
371

    
372
			@Override
373
			public void lifeCycleStarting(LifeCycle event) {
374
			}
375

    
376
			@Override
377
			public void lifeCycleStopped(LifeCycle event) {
378
			}
379

    
380
			@Override
381
			public void lifeCycleStopping(LifeCycle event) {
382
			}
383
			
384
			});
385
        
386
        
387
        logger.info("setting contexts ...");
388
        server.setHandler(contexts);
389
        if(cmdLine.hasOption(WIN32SERVICE.getOpt())){
390
        	logger.info("jetty is waiting to be started as win32 service");
391
        } else {
392
	        logger.info("starting jetty ...");
393
	        server.start();
394
	        server.join();
395
	        logger.info(APPLICATION_NAME+" stopped.");
396
	    	System.exit(0);
397
        }
398
	}
399

    
400
	/**
401
	 * 
402
	 */
403
	private void verifyMemoryRequirements() {
404
		
405
		verifyMemoryRequirement("PermGenSpace", PERM_GEN_SPACE_PER_INSTANCE, JvmManager.getPermGenSpaceUsage().getMax());
406
		verifyMemoryRequirement("HeapSpace", HEAP_PER_INSTANCE, JvmManager.getHeapMemoryUsage().getMax());
407
		
408
	}
409

    
410
	private void verifyMemoryRequirement(String memoryName, long requiredSpacePerIntance, long availableSpace) {
411
		
412

    
413
		long requiredSpace = configAndStatus.size() * requiredSpacePerIntance;
414
		
415
		if(requiredSpace > availableSpace){
416
			
417
			String message = memoryName + " (" 
418
				+ (availableSpace / MB)  
419
				+ "MB) insufficient for " 
420
				+ configAndStatus.size()
421
				+ " instances. Increase " + memoryName + " by " 
422
				+ ((requiredSpace - availableSpace)/MB) 
423
				+ "MB";
424
				;
425
			logger.error(message + " => disabling some instances!!!");
426
			
427
			// disabling some instances 
428
			int i=0;
429
			for(CdmInstanceProperties instanceProps : configAndStatus){
430
				i++;
431
				if(i * requiredSpacePerIntance > availableSpace){
432
					instanceProps.setStatus(Status.disabled);
433
					instanceProps.getProblems().add("Disbled due to: " + message);
434
				}
435
			}
436
		}
437
	}
438

    
439
	/**
440
	 * Configures and adds a {@link RollingFileAppender} to the root logger
441
	 * 
442
	 * The log files of the cdm-remote instances are configured by the 
443
	 * {@link eu.etaxonomy.cdm.remote.config.LoggingConfigurer}
444
	 */
445
	private void configureFileLogger() {
446

    
447
		PatternLayout layout = new PatternLayout("%d %p [%c] - %m%n");
448
		try {
449
			String logFile = LOG_PATH + File.separator + "cdmserver.log";
450
			RollingFileAppender appender = new RollingFileAppender(layout, logFile);
451
			appender.setMaxBackupIndex(3);
452
			appender.setMaxFileSize("2MB");
453
			Logger.getRootLogger().addAppender(appender);
454
			logger.info("logging to :" + logFile);
455
		} catch (IOException e) {
456
			logger.error("Creating RollingFileAppender failed:", e);
457
		}
458
	}
459

    
460
	private void addCdmServerContexts(boolean austostart) throws IOException {
461
		
462
		for(CdmInstanceProperties conf : configAndStatus){
463
			
464
			if(!conf.isEnabled()){
465
				logger.info(conf.getDataSourceName() + " is disabled => skipping");
466
				continue;
467
			}
468
			conf.setStatus(CdmInstanceProperties.Status.initializing);
469
        	logger.info("preparing WebAppContext for '"+ conf.getDataSourceName() + "'");
470
        	WebAppContext cdmWebappContext = new WebAppContext();
471
         
472
	        cdmWebappContext.setContextPath("/"+conf.getDataSourceName());
473
	        cdmWebappContext.setTempDirectory(CDM_WEBAPP_TEMP_FOLDER);
474
	        
475
            if(!bindJndiDataSource(conf)){
476
            	// a problem with the datasource occurred skip this webapp
477
            	cdmWebappContext = null;
478
            	logger.error("a problem with the datasource occurred -> skipping /" + conf.getDataSourceName());
479
				conf.setStatus(CdmInstanceProperties.Status.error);
480
            	continue;
481
            }
482
            
483
            cdmWebappContext.setAttribute(ATTRIBUTE_JDBC_JNDI_NAME, conf.getJdbcJndiName());
484
	        setWebApp(cdmWebappContext, webappFile);
485
	        
486
			cdmWebappContext.setAttribute(CDM_LOGFILE,
487
					LOG_PATH + File.separator + "cdm-"
488
							+ conf.getDataSourceName() + ".log");
489
   
490
	        if(webappFile.isDirectory() && isRunningFromSource()){
491
        		
492
				/*
493
				 * when running the webapp from {projectpath} src/main/webapp we
494
				 * must assure that each web application is using it's own
495
				 * classloader thus we tell the WebAppClassLoader where the
496
				 * dependencies of the webapplication can be found. Otherwise
497
				 * the system classloader would load these resources.
498
				 */
499
        		logger.info("Running webapp from source folder, thus adding java.class.path to WebAppClassLoader");
500

    
501
        		WebAppClassLoader classLoader = new WebAppClassLoader(cdmWebappContext);
502
	        	
503
	        	String classPath = System.getProperty("java.class.path");
504
	        	classLoader.addClassPath(classPath);
505
	        	cdmWebappContext.setClassLoader(classLoader);
506
        	}
507
	        
508
	        contexts.addHandler(cdmWebappContext);  
509
	        
510
	        if(austostart){
511
		        try {
512
		        	conf.setStatus(CdmInstanceProperties.Status.starting);
513
					cdmWebappContext.start();
514
					conf.setStatus(CdmInstanceProperties.Status.started);
515
				} catch (Exception e) {
516
					logger.error("Could not start " + cdmWebappContext.getContextPath());
517
					conf.setStatus(CdmInstanceProperties.Status.error);
518
				}
519
	        }
520

    
521
        }
522
	}
523

    
524
	private void setWebApp(WebAppContext context, File webApplicationResource) {
525
		if(webApplicationResource.isDirectory()){
526
			context.setResourceBase(webApplicationResource.getAbsolutePath());
527
			logger.debug("setting directory " + webApplicationResource.getAbsolutePath() + " as webapplication");
528
		} else {
529
			context.setWar(webApplicationResource.getAbsolutePath());
530
			logger.debug("setting war file " + webApplicationResource.getAbsolutePath() + " as webapplication");
531
		}
532
	}
533

    
534
	private boolean isRunningFromSource() {
535
		String webappPathNormalized = webappFile.getAbsolutePath().replace('\\', '/');
536
		return webappPathNormalized.endsWith("src/main/webapp") || webappPathNormalized.endsWith("cdmlib-remote/target/cdmserver");
537
	}
538
}
(1-1/5)