Project

General

Profile

Download (37.3 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
 * Copyright (C) 2009 EDIT
3
 * European Distributed Institute of Taxonomy
4
 * http://www.e-taxonomy.eu
5
 *
6
 * The contents of this file are subject to the Mozilla Public License Version 1.1
7
 * See LICENSE.TXT at the top of this package for the full license terms.
8
 */
9
package eu.etaxonomy.cdm.server;
10

    
11
import static eu.etaxonomy.cdm.server.AssumedMemoryRequirements.KB;
12
import static eu.etaxonomy.cdm.server.CommandOptions.CONTEXT_PATH_PREFIX;
13
import static eu.etaxonomy.cdm.server.CommandOptions.DATASOURCES_FILE;
14
import static eu.etaxonomy.cdm.server.CommandOptions.FORCE_SCHEMA_UPDATE;
15
import static eu.etaxonomy.cdm.server.CommandOptions.HELP;
16
import static eu.etaxonomy.cdm.server.CommandOptions.HTTP_PORT;
17
import static eu.etaxonomy.cdm.server.CommandOptions.JMX;
18
import static eu.etaxonomy.cdm.server.CommandOptions.WEBAPP;
19
import static eu.etaxonomy.cdm.server.CommandOptions.WEBAPP_CLASSPATH;
20
import static eu.etaxonomy.cdm.server.CommandOptions.WIN32SERVICE;
21

    
22
import java.io.BufferedReader;
23
import java.io.File;
24
import java.io.FileNotFoundException;
25
import java.io.FileOutputStream;
26
import java.io.FilenameFilter;
27
import java.io.IOException;
28
import java.io.InputStream;
29
import java.io.InputStreamReader;
30
import java.io.OutputStream;
31
import java.lang.management.ManagementFactory;
32
import java.lang.reflect.InvocationTargetException;
33
import java.net.MalformedURLException;
34
import java.net.URL;
35
import java.net.URLClassLoader;
36
import java.util.ArrayList;
37
import java.util.List;
38
import java.util.Properties;
39
import java.util.jar.Attributes;
40
import java.util.jar.JarFile;
41
import java.util.jar.Manifest;
42
import java.util.regex.Pattern;
43

    
44
import org.apache.commons.cli.CommandLine;
45
import org.apache.commons.cli.CommandLineParser;
46
import org.apache.commons.cli.GnuParser;
47
import org.apache.commons.cli.HelpFormatter;
48
import org.apache.commons.cli.ParseException;
49
import org.apache.commons.io.FileUtils;
50
import org.apache.commons.io.FilenameUtils;
51
import org.apache.log4j.Logger;
52
import org.apache.tomcat.SimpleInstanceManager;
53
import org.apache.tomcat.util.scan.StandardJarScanner;
54
import org.eclipse.jetty.annotations.AnnotationConfiguration;
55
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
56
import org.eclipse.jetty.jmx.MBeanContainer;
57
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
58
import org.eclipse.jetty.security.HashLoginService;
59
import org.eclipse.jetty.server.Handler;
60
import org.eclipse.jetty.server.Server;
61
import org.eclipse.jetty.server.ServerConnector;
62
import org.eclipse.jetty.server.handler.ContextHandler;
63
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
64
import org.eclipse.jetty.servlet.ServletContextHandler;
65
import org.eclipse.jetty.util.component.AbstractLifeCycle;
66
import org.eclipse.jetty.util.log.Log;
67
import org.eclipse.jetty.webapp.WebAppClassLoader;
68
import org.eclipse.jetty.webapp.WebAppContext;
69

    
70
import eu.etaxonomy.cdm.server.instance.CdmInstance;
71
import eu.etaxonomy.cdm.server.instance.Configuration;
72
import eu.etaxonomy.cdm.server.instance.InstanceManager;
73
import eu.etaxonomy.cdm.server.instance.SharedAttributes;
74
import eu.etaxonomy.cdm.server.instance.Status;
75
import eu.etaxonomy.cdm.server.logging.LoggingConfigurator;
76
import eu.etaxonomy.cdm.server.win32service.Win32Service;
77

    
78
/**
79
 * A bootstrap class for starting Jetty Runner using an embedded war.
80
 *
81
 * @version $Revision$
82
 */
83
public final class Bootloader {
84

    
85
    private static final Logger logger = Logger.getLogger(Bootloader.class);
86

    
87
    //private static final String DEFAULT_WARFILE = "target/";
88

    
89
    private static final String DATASOURCE_BEANDEF_FILE = "datasources.xml";
90
    private static final String REALM_PROPERTIES_FILE = "cdm-server-realm.properties";
91

    
92
    private static final String USERHOME_CDM_LIBRARY_PATH = System.getProperty("user.home")+File.separator+".cdmLibrary"+File.separator;
93
    private static final String TMP_PATH = USERHOME_CDM_LIBRARY_PATH + "server" + File.separator;
94

    
95
    private static final String APPLICATION_NAME = "CDM Server";
96
    private static final String WAR_POSTFIX = ".war";
97

    
98
    private static final String CDM_WEBAPP = "cdm-webapp";
99
    private static final String CDM_WEBAPP_VERSION = "cdm-webapp.version";
100

    
101
    private static final String DEFAULT_WEBAPP_WAR_NAME = "default-webapp";
102
    private static final File DEFAULT_WEBAPP_TEMP_FOLDER = new File(TMP_PATH + DEFAULT_WEBAPP_WAR_NAME);
103
    private static final File CDM_WEBAPP_TEMP_FOLDER = new File(TMP_PATH + CDM_WEBAPP);
104

    
105
    private static final String SPRING_PROFILES_ACTIVE = "spring.profiles.active";
106
    private static final String VERSION_PROPERTIES_FILE = "version.properties";
107

    
108
    private final InstanceManager instanceManager = new InstanceManager(new File(USERHOME_CDM_LIBRARY_PATH, DATASOURCE_BEANDEF_FILE));
109

    
110
    public List<CdmInstance> getCdmInstances() {
111
        return instanceManager.getInstances();
112
    }
113

    
114
    public InstanceManager getInstanceManager(){
115
        return instanceManager;
116
    }
117

    
118
    private File cdmRemoteWebAppFile = null;
119
    private File defaultWebAppFile = null;
120

    
121
    private String webAppClassPath = null;
122

    
123
    /**
124
     * The contextPathPrefix is expected to be normalized:
125
     * it ends with a slash and starts not with a slash character
126
     */
127
    private String contextPathPrefix = "";
128

    
129
    private Server server = null;
130
    private final ContextHandlerCollection contexts = new ContextHandlerCollection();
131
    private final LoggingConfigurator loggingConfigurator = new LoggingConfigurator();
132

    
133
    private CommandLine cmdLine;
134

    
135
    private boolean isRunningFromSource;
136

    
137
    private boolean isRunningfromTargetFolder;
138

    
139
    private boolean isRunningFromWarFile;
140

    
141
    private String cdmlibServicesVersion = "";
142
    private String cdmlibServicesLastModified = "";
143

    
144

    
145
    /* thread save singleton implementation */
146

    
147
    private static Bootloader bootloader = new Bootloader();
148

    
149
    private Bootloader() {}
150

    
151
    /**
152
     * @return the thread save singleton instance of the Bootloader
153
     */
154
    public synchronized static Bootloader getBootloader(){
155
        return bootloader;
156
    }
157

    
158
    /* end of singleton implementation */
159

    
160
    public int writeStreamTo(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
161
        int available = Math.min(input.available(), 256 * KB);
162
        byte[] buffer = new byte[Math.max(bufferSize, available)];
163
        int answer = 0;
164
        int count = input.read(buffer);
165
        while (count >= 0) {
166
            output.write(buffer, 0, count);
167
            answer += count;
168
            count = input.read(buffer);
169
        }
170
        return answer;
171
    }
172

    
173
    public void parseCommandOptions(String[] args) throws ParseException {
174
        CommandLineParser parser = new GnuParser();  //TODO using DefaultParser instead currently still throws "org.apache.commons.cli.UnrecognizedOptionException: Unrecognized option: -httpPort=8080"
175

    
176
        cmdLine = parser.parse( CommandOptions.getOptions(), args );
177

    
178
        // print the help message
179
        if(cmdLine.hasOption(HELP.getOpt())){
180
            HelpFormatter formatter = new HelpFormatter();
181
            formatter.setWidth(200);
182
            formatter.printHelp( "java .. ", CommandOptions.getOptions() );
183
            System.exit(0);
184
        }
185
    }
186

    
187
    /**
188
     * Finds the named war file either in the resources known to the class loader
189
     * or in a target folder if the bootloader is started from within a maven project.
190
     * Once found the war file is copied to the temp folder defined by {@link TMP_PATH}.
191
     *
192
     * The war file can optionally be unpacked.
193
     *
194
     * @param warName
195
     * @param unpack
196
     *  unzip the war file after extraction
197
     * @return
198
     * @throws IOException
199
     * @throws FileNotFoundException
200
     */
201
    private File extractWar(String warName, boolean unpack) throws IOException, FileNotFoundException {
202
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
203
        String warFileName = warName + WAR_POSTFIX;
204

    
205
        // 1. find in classpath
206
        URL resource = classLoader.getResource(warFileName);
207
        if (resource == null) {
208
            logger.error("Could not find the " + warFileName + " on classpath!");
209

    
210
            File pomxml = new File("pom.xml");
211
            if(pomxml.exists()){
212
                logger.info("will try find the war in target folder of maven project");
213
                // 2. try finding in target folder of maven project
214
                File warFile = new File("target" + File.separator + warFileName);
215
                logger.debug("looking for war file at " + warFile.getAbsolutePath());
216
                if (warFile.canRead()) {
217
                    resource = warFile.toURI().toURL();
218
                    logger.info("Success! Using war file from " + resource.toString());
219
                } else {
220
                    logger.error("Also could not find the " + warFileName + " in maven project, try excuting 'mvn install'");
221
                }
222
            }
223
        }
224

    
225

    
226
        if (resource == null) {
227
            // no way finding the war file :-(
228
            System.exit(1);
229
        }
230

    
231

    
232
        File extractedWarFile = new File(TMP_PATH, warName + "-" + WAR_POSTFIX);
233
        logger.info("Extracting " + resource + " to " + extractedWarFile + " ...");
234

    
235
        writeStreamTo(resource.openStream(), new FileOutputStream(extractedWarFile), 8 * KB);
236

    
237
        if(!unpack) {
238
            // return the war file
239
            return extractedWarFile;
240
        } else {
241
            // unpack the archive
242
            File explodedWebApp = null;
243
            try {
244
                logger.info("Unpacking " + extractedWarFile);
245
                explodedWebApp  = unzip(extractedWarFile);
246

    
247
                // get the 'Bundle-Version' and 'Bnd-LastModified' properties of the
248
                // manifest file in the cdmlib services jar
249
                if(explodedWebApp != null && explodedWebApp.isDirectory()) {
250
                    // generate the webapp lib dir path
251
                    String warLibDirAbsolutePath = explodedWebApp.getAbsolutePath() +
252
                            File.separator +
253
                            "WEB-INF" +
254
                            File.separator +
255
                            "lib";
256
                    File warLibDir = new File(warLibDirAbsolutePath);
257
                    if(warLibDir.exists()) {
258
                        // get the cdmlib-services jar
259
                        File [] files = warLibDir.listFiles(new FilenameFilter() {
260
                            @Override
261
                            public boolean accept(File dir, String name) {
262
                                return name.startsWith("cdmlib-services") && name.endsWith(".jar");
263
                            }
264
                        });
265
                        if(files != null && files.length > 0) {
266
                            // get the relevant info from the jar manifest
267
                            JarFile jarFile = new JarFile(files[0]);
268
                            Manifest manifest = jarFile.getManifest();
269
                            Attributes attributes = manifest.getMainAttributes();
270
                            // from the OSGI spec the LastModified value is " the number of milliseconds
271
                            // since midnight Jan. 1, 1970 UTC with the condition that a change must
272
                            // always result in a higher value than the previous last modified time
273
                            // of any bundle"
274
                            cdmlibServicesVersion = attributes.getValue("Bundle-Version");
275
                            logger.warn("cdmlib-services version : " + cdmlibServicesVersion);
276
                            cdmlibServicesLastModified = attributes.getValue("Bnd-LastModified");
277
                            logger.warn("cdmlib-services last modified timestamp : " + cdmlibServicesLastModified);
278

    
279
                            if(cdmlibServicesVersion == null || cdmlibServicesLastModified == null) {
280
                                throw new IllegalStateException("Invalid cdmlib-services manifest file");
281
                            }
282
                        } else {
283
                            throw new IllegalStateException("cdmlib-services jar not found ");
284
                        }
285
                    }
286
                }
287
            } catch (IOException e) {
288
                logger.error("extractWar() - Unziping of war file " + explodedWebApp + " failed. Will return the war file itself instead of the extracted folder.", e);
289
            }
290
            return explodedWebApp;
291
        }
292
    }
293

    
294

    
295
    public String getCdmlibServicesVersion() {
296
        return cdmlibServicesVersion;
297
    }
298

    
299
    public String getCdmlibServicesLastModified() {
300
        return cdmlibServicesLastModified;
301
    }
302

    
303
    private File unzip(File extractWar) throws IOException {
304
        UnzipUtility unzip = new UnzipUtility();
305

    
306
        String targetFolderName = FilenameUtils.getBaseName(extractWar.getName());
307
        File destDirectory = new File(TMP_PATH + File.separator + targetFolderName);
308
        unzip.unzip(extractWar, destDirectory);
309
        return destDirectory;
310
    }
311

    
312
    /**
313
     * MAIN METHOD
314
     *
315
     * @param args
316
     * @throws Exception
317
     */
318
    public static void main(String[] args) throws Exception {
319

    
320
        Bootloader bootloader = Bootloader.getBootloader();
321
        bootloader.parseCommandOptions(args);
322
        bootloader.startServer();
323
    }
324

    
325
    public void startServer() throws IOException,
326
            FileNotFoundException, Exception, InterruptedException {
327

    
328
        logger.info("Starting " + APPLICATION_NAME);
329
        logger.info("Using  " + System.getProperty("user.home") + " as home directory. Can be specified by -Duser.home=<FOLDER>");
330

    
331
        //assure TMP_PATH exists and clean it up
332
        File tempDir = new File(TMP_PATH);
333
        if(!tempDir.exists() && !tempDir.mkdirs()){
334
            logger.error("Error creating temporary directory for webapplications " + tempDir.getAbsolutePath());
335
            System.exit(-1);
336
        } else {
337
            if(FileUtils.deleteQuietly(tempDir)){
338
                tempDir.mkdirs();
339
                logger.info("Old webapplications successfully cleared");
340
            }
341
        }
342
        tempDir = null;
343

    
344

    
345
        // WEBAPP options
346
        //   prepare web application files to run either from war files (production mode)
347
        //   or from source (development mode)
348
        if(cmdLine.hasOption(WEBAPP.getOpt())){
349

    
350
            cdmRemoteWebAppFile = new File(cmdLine.getOptionValue(WEBAPP.getOpt()));
351
            if(cdmRemoteWebAppFile.isDirectory()){
352
                logger.info("using user defined web application folder: " + cdmRemoteWebAppFile.getAbsolutePath());
353
            } else {
354
                logger.info("using user defined warfile: " + cdmRemoteWebAppFile.getAbsolutePath());
355
            }
356

    
357
            updateServerRunMode();
358

    
359
            // load the default-web-application from source if running in development mode mode
360
            if(isRunningFromWarFile){
361
                defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME, false);
362
            } else {
363
                defaultWebAppFile = new File("./src/main/webapp");
364
            }
365

    
366
            if(isRunningFromSource){
367
                if(cmdLine.hasOption(WEBAPP_CLASSPATH.getOpt())){
368
                    String classPathOption = cmdLine.getOptionValue(WEBAPP_CLASSPATH.getOpt());
369
                    normalizeClasspath(classPathOption);
370
                }
371
            }
372
        } else {
373
            // read version number
374
            String version = readCdmRemoteVersion();
375
            cdmRemoteWebAppFile = extractWar(CDM_WEBAPP + "-" + version, true);
376
            defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME, false);
377
        }
378

    
379

    
380
        // HTTP Port
381
        int httpPort = 8080;
382
        if(cmdLine.hasOption(HTTP_PORT.getOpt())){
383
            try {
384
               httpPort = Integer.parseInt(cmdLine.getOptionValue(HTTP_PORT.getOpt()));
385
               logger.info(HTTP_PORT.getOpt()+" set to "+cmdLine.getOptionValue(HTTP_PORT.getOpt()));
386
           } catch (NumberFormatException e) {
387
               logger.error("Supplied portnumber is not an integer");
388
               System.exit(-1);
389
           }
390
        }
391

    
392
        if(cmdLine.hasOption(DATASOURCES_FILE.getOpt())){
393
             File datasourcesFile = new File(cmdLine.getOptionValue(DATASOURCES_FILE.getOpt()));
394
             if(datasourcesFile.canRead()) {
395
                instanceManager.setDatasourcesFile(datasourcesFile);
396
            } else {
397
                logger.error("File set as " + DATASOURCES_FILE.getOpt()
398
                        + " (" + cmdLine.getOptionValue(DATASOURCES_FILE.getOpt())
399
                        + ") is not readable.");
400
            }
401
         }
402

    
403
        if(cmdLine.hasOption(CONTEXT_PATH_PREFIX.getOpt())){
404

    
405
            String cppo  = cmdLine.getOptionValue(CONTEXT_PATH_PREFIX.getOpt());
406
            if(cppo.equals("/")) {
407
                this.contextPathPrefix = "";
408
            } else {
409
                Pattern pattern = Pattern.compile("^/*(.*?)/*$");
410
                String replacement = "$1";
411
                this.contextPathPrefix = pattern.matcher(cppo).replaceAll(replacement) + "/";
412
            }
413
        }
414

    
415
        verifySystemResources();
416

    
417
         // load the configured instances for the first time
418
        instanceManager.reLoadInstanceConfigurations();
419

    
420
        if(System.getProperty(SPRING_PROFILES_ACTIVE) == null){
421
            logger.info(SPRING_PROFILES_ACTIVE + " is undefined, and will be set to : \"remoting\"");
422
            System.setProperty(SPRING_PROFILES_ACTIVE, "remoting");
423
        }
424

    
425
        // in jetty 9 currently each connector uses
426
        // 2 threads -  1 to select for IO activity and 1 to accept new connections.
427
        // there fore we need to add 2 to the number of cores
428
//        QueuedThreadPool threadPool = new QueuedThreadPool(JvmManager.availableProcessors() +  + 200);
429
//        server = new Server(threadPool);
430
        server = new Server();
431

    
432
        jdk8MemleakFixServer();
433

    
434

    
435
        loggingConfigurator.configureServer();
436

    
437
        server.addLifeCycleListener(instanceManager);
438
        ServerConnector connector = new ServerConnector(server);
439
        connector.setPort(httpPort);
440
        logger.info("http port: " + connector.getPort());
441
        server.addConnector(connector );
442

    
443
        org.eclipse.jetty.webapp.Configuration.ClassList classlist = org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server);
444
        classlist.addAfter(
445
                org.eclipse.jetty.webapp.FragmentConfiguration.class.getName(),
446
                org.eclipse.jetty.plus.webapp.EnvConfiguration.class.getName(),
447
                org.eclipse.jetty.plus.webapp.PlusConfiguration.class.getName()
448
                );
449
        classlist.addBefore(
450
                org.eclipse.jetty.webapp.JettyWebXmlConfiguration.class.getName(),
451
                org.eclipse.jetty.annotations.AnnotationConfiguration.class.getName());
452

    
453

    
454
        // JMX support
455
        if(cmdLine.hasOption(JMX.getOpt())){
456
            logger.info("adding JMX support ...");
457
            MBeanContainer mBeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
458
            server.addEventListener(mBeanContainer);
459
            server.addBean(Log.getLog());
460
        }
461

    
462
        if(cmdLine.hasOption(WIN32SERVICE.getOpt())){
463
            Win32Service win32Service = new Win32Service();
464
            win32Service.setServer(server);
465
            server.setStopAtShutdown(true);
466
            server.addBean(win32Service);
467
        }
468

    
469
        WebAppContext defaultWebappContext = createDefaultWebappContext();
470
        contexts.addHandler(defaultWebappContext);
471

    
472
        logger.info("setting contexts ...");
473
        server.setHandler(contexts);
474
        logger.info("starting jetty ...");
475
//        try {
476

    
477
            server.start();
478

    
479
//        } catch(org.springframework.beans.BeansException e){
480
//        	Throwable rootCause = null;
481
//        	while(e.getCause() != null){
482
//        		rootCause = e.getCause();
483
//        	}
484
//        	if(rootCause != null && rootCause.getClass().getSimpleName().equals("InvalidCdmVersionException")){
485
//
486
//        		logger.error("rootCause ----------->" + rootCause.getMessage());
487
////        		for(CdmInstanceProperties props : configAndStatus){
488
////        			if(props.getDataSourceName())
489
////        		}
490
//        	}
491
//        }
492

    
493
        if(cmdLine.hasOption(WIN32SERVICE.getOpt())){
494
            logger.info("jetty has started as win32 service");
495
        } else {
496
            server.join();
497
            logger.info(APPLICATION_NAME+" stopped.");
498
            System.exit(0);
499
        }
500
    }
501

    
502
    private WebAppContext createDefaultWebappContext() throws FileNotFoundException {
503
        // add default servlet context
504
        logger.info("preparing default WebAppContext");
505

    
506
        WebAppContext defaultWebappContext = new WebAppContext();
507
        setWebApp(defaultWebappContext, defaultWebAppFile);
508

    
509
        // JSP
510
        //
511
        // configuring jsp according to http://eclipse.org/jetty/documentation/current/configuring-jsp.html
512
        // from example http://eclipse.org/jetty/documentation/current/embedded-examples.html#embedded-webapp-jsp
513
        // Set the ContainerIncludeJarPattern so that jetty examines these
514
        // container-path jars for tlds, web-fragments etc.
515
        // If you omit the jar that contains the jstl .tlds, the jsp engine will
516
        // scan for them instead.
517
        defaultWebappContext.setAttribute(
518
                "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
519
                ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/[^/]*taglibs.*\\.jar$" );
520

    
521
        defaultWebappContext.setAttribute("org.eclipse.jetty.containerInitializers", jspInitializers());
522
        defaultWebappContext.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
523

    
524
        // Context path
525
        //
526
        defaultWebappContext.setContextPath("/" + (contextPathPrefix.isEmpty() ? "" : contextPathPrefix.substring(0, contextPathPrefix.length() - 1)));
527
        logger.info("defaultWebapp (manager) context path:" + defaultWebappContext.getContextPath());
528
        defaultWebappContext.setTempDirectory(DEFAULT_WEBAPP_TEMP_FOLDER);
529

    
530
        // configure security context
531
        // see for reference * http://docs.codehaus.org/display/JETTY/Realms
532
        //                   * http://wiki.eclipse.org/Jetty/Starting/Porting_to_Jetty_7
533
        HashLoginService loginService = new HashLoginService();
534
        File realmConfigFile = new File(USERHOME_CDM_LIBRARY_PATH + REALM_PROPERTIES_FILE);
535
        if(!realmConfigFile.canRead()) {
536
            throw new FileNotFoundException("Unable to find or read the realm file at " + realmConfigFile.getPath());
537
        }
538
        loginService.setConfig(realmConfigFile.getPath());
539
        defaultWebappContext.getSecurityHandler().setLoginService(loginService);
540

    
541
        // Set Classloader of Context to be sane (needed for JSTL)
542
        // JSP requires a non-System classloader, this simply wraps the
543
        // embedded System classloader in a way that makes it suitable
544
        // for JSP to use
545
        ClassLoader jspClassLoader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
546
        defaultWebappContext.setClassLoader(this.getClass().getClassLoader());
547
        // JspStarter to solve java.lang.IllegalStateException: No org.apache.tomcat.InstanceManager set in ServletContext problems
548
        // when running not from within the IDE (see https://issues.apache.org/jira/browse/KNOX-1639)
549
        defaultWebappContext.addBean(new JspStarter(defaultWebappContext));
550
        return defaultWebappContext;
551
    }
552

    
553
    /**
554
     * jdk8 memleak workaround: disable url caching
555
     *  see https://dev.e-taxonomy.eu/redmine/issues/5048
556
     *
557
     * @throws IOException
558
     * @throws MalformedURLException
559
     */
560
    private void jdk8MemleakFixServer() throws IOException, MalformedURLException {
561
        String javaVersion = System.getProperty("java.version");
562
        if(javaVersion.startsWith("1.8")){
563
            logger.info("jdk8 memory leak fix: jdk8 detected (" + javaVersion + ") disabling url caching to avoid memory leak.");
564
            org.eclipse.jetty.util.resource.Resource.setDefaultUseCaches(false);
565
            File tmpio = new File(System.getProperty("java.io.tmpdir"));
566
            tmpio.toURI().toURL().openConnection().setDefaultUseCaches(false);
567
        } else {
568
            logger.info("jdk8 memory leak fix: unaffected jdk " + javaVersion + " detected");
569
        }
570
    }
571

    
572
    private void jdk8MemleakFixInstance(ClassLoader classLoader, CdmInstance instance) throws IOException, MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
573
        String javaVersion = System.getProperty("java.version");
574
        if(javaVersion.startsWith("1.8")){
575
            logger.info("jdk8 memory leak fix for " + instance.getName() + ": jdk8 detected (" + javaVersion + ") disabling url caching to avoid memory leak.");
576
            Class<?> fileClass = classLoader.loadClass("java.io.File");
577
            File tmpio = (File) fileClass.getConstructor(String.class).newInstance("java.io.tmpdir");
578
            tmpio.toURI().toURL().openConnection().setDefaultUseCaches(false);
579
        } else {
580
            logger.info("jdk8 memory leak fix, " + instance.getName() + "unaffected jdk " + javaVersion + " detected");
581
        }
582
    }
583

    
584
    /**
585
     * @param classpath
586
     */
587
    private void normalizeClasspath(String classpath) {
588
        StringBuilder classPathBuilder = new StringBuilder((int) (classpath.length() * 1.2));
589
        String[] cpEntries = classpath.split("[\\:]");
590
        for(String cpEntry : cpEntries){
591
            classPathBuilder.append(',');
592
//            if(cpEntry.endsWith(".jar")){
593
//                classPathBuilder.append("jar:");
594
//            }
595
            classPathBuilder.append(cpEntry);
596
        }
597
        webAppClassPath = classPathBuilder.toString();
598
    }
599

    
600
    public String readCdmRemoteVersion() throws IOException {
601
        String version = "cdmlib version unreadable";
602
        InputStream versionInStream = Bootloader.class.getClassLoader().getResourceAsStream(VERSION_PROPERTIES_FILE);
603
        if (versionInStream != null){
604
            Properties versionProperties = new Properties();
605
            versionProperties.load(versionInStream);
606
            version = versionProperties.getProperty(CDM_WEBAPP_VERSION, version);
607
        }
608
        return version;
609
    }
610

    
611
    /**
612
    * Ensure the jsp engine is initialized correctly
613
    */
614
    private List<ContainerInitializer> jspInitializers()
615
    {
616
        JettyJasperInitializer sci = new JettyJasperInitializer();
617
        ContainerInitializer initializer = new ContainerInitializer(sci, null);
618
        List<ContainerInitializer> initializers = new ArrayList<ContainerInitializer>();
619
        initializers.add(initializer);
620
        return initializers;
621
    }
622

    
623
    /**
624
     * JspStarter for embedded ServletContextHandlers
625
     *
626
     * This is added as a bean that is a jetty LifeCycle on the ServletContextHandler.
627
     * This bean's doStart method will be called as the ServletContextHandler starts,
628
     * and will call the ServletContainerInitializer for the jsp engine.
629
     *
630
     */
631
    public static class JspStarter extends AbstractLifeCycle implements ServletContextHandler.ServletContainerInitializerCaller {
632
      JettyJasperInitializer sci;
633
      ServletContextHandler context;
634

    
635
      public JspStarter (ServletContextHandler context) {
636
        this.sci = new JettyJasperInitializer();
637
        this.context = context;
638
        this.context.setAttribute("org.apache.tomcat.JarScanner", new StandardJarScanner());
639
      }
640

    
641
      @Override
642
      protected void doStart() throws Exception
643
      {
644
        ClassLoader old = Thread.currentThread().getContextClassLoader();
645
        Thread.currentThread().setContextClassLoader(context.getClassLoader());
646
        try
647
        {
648
          sci.onStartup(null, context.getServletContext());
649
          super.doStart();
650
        }
651
        finally
652
        {
653
          Thread.currentThread().setContextClassLoader(old);
654
        }
655
      }
656
    }
657

    
658

    
659
    private void verifySystemResources() {
660

    
661
        OsChecker osChecker = new OsChecker();
662
        if(osChecker.isLinux()){
663
            try {
664
                Process p = Runtime.getRuntime().exec(new String[] { "bash", "-c", "ulimit -n" });
665
                BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
666
                String line;
667
                StringBuilder response = new StringBuilder();
668
                     while ((line = in.readLine()) != null) {
669
                         response.append(line);
670
                     }
671
               logger.info("OS Limit (Linux): maximum number of open files: " + response);
672
            } catch (IOException e) {
673
                // TODO Auto-generated catch block
674
                e.printStackTrace();
675
            }
676
        } else {
677
            logger.info("verifySystemResources only implemented for linux");
678
        }
679
    }
680

    
681

    
682
    /**
683
     * Adds a new WebAppContext to the contexts of the running jetty instance.
684
     * <ol>
685
     * <li>Initialize WebAppContext:
686
     * <ol>
687
     * <li>set context path</li>
688
     * <li>set tmp directory</li>
689
     * <li>bind JndiDataSource</li>
690
     * <li>set web app context attributes</li>
691
     * <li>create and setup individual classloader for the instance</li>
692
     * </ol>
693
     * </li>
694
     * <li>
695
     * finally add the new webappcontext to the contexts of the jetty instance</li>
696
     * </ol>
697
     *
698
     * @param instance
699
     * @return the instance given as parameter of null in case the instance has
700
     *         {@link Status.#disabled} or if it is already added.
701
     * @throws IOException
702
     */
703
    public WebAppContext addCdmInstanceContext(CdmInstance instance) throws IOException {
704

    
705
        Configuration conf = instance.getConfiguration();
706
        if(!instance.isEnabled()){
707
            logger.info(conf.getInstanceName() + " is disabled, possibly due to JVM memory limitations");
708
            return null;
709
        }
710
        if(getContextFor(conf) != null){
711
            logger.info(conf.getInstanceName() + " is alreaddy added to the contexts - skipping");
712
            return null;
713
        }
714

    
715
        instance.setStatus(Status.initializing);
716
        logger.info("preparing WebAppContext for '"+ conf.getInstanceName() + "'");
717
        WebAppContext cdmWebappContext = new WebAppContext();
718

    
719
        cdmWebappContext.setContextPath(constructContextPath(conf));
720
        logger.info("contextPath: " + cdmWebappContext.getContextPath());
721
        // set persistTempDirectory to prevent jetty from creating and deleting this directory for each instance,
722
        // since this behaviour can cause conflicts during parallel start up  of instances.
723
        cdmWebappContext.setPersistTempDirectory(true);
724

    
725

    
726
//        if(!instance.bindJndiDataSource()){
727
//            // a problem with the datasource occurred skip this webapp
728
//            cdmWebappContext = null;
729
//            logger.error("a problem with the datasource occurred -> skipping /" + conf.getInstanceName());
730
//            instance.setStatus(Status.error);
731
//            return cdmWebappContext;
732
//        }
733

    
734
        cdmWebappContext.setInitParameter(SharedAttributes.ATTRIBUTE_DATASOURCE_NAME, conf.getInstanceName());
735
        cdmWebappContext.setInitParameter(SharedAttributes.ATTRIBUTE_JDBC_JNDI_NAME, conf.getJdbcJndiName());
736
        if(cmdLine.hasOption(FORCE_SCHEMA_UPDATE.getOpt())){
737
            cdmWebappContext.setInitParameter(SharedAttributes.ATTRIBUTE_FORCE_SCHEMA_UPDATE, "true");
738
        }
739
        setWebApp(cdmWebappContext, getCdmRemoteWebAppFile());
740

    
741
        if( isRunningFromSource ){
742

    
743
            /*
744
             * when running the webapp from {projectpath} src/main/webapp we
745
             * must assure that each web application is using it's own
746
             * classloader thus we tell the WebAppClassLoader where the
747
             * dependencies of the webapplication can be found. Otherwise
748
             * the system classloader would load these resources.
749
             */
750
            WebAppClassLoader classLoader = new WebAppClassLoader(cdmWebappContext);
751
            if(webAppClassPath != null){
752
                logger.info("Running cdm-webapp from source folder: Adding class path supplied by option '-" +  WEBAPP_CLASSPATH.getOpt() +" =" + webAppClassPath +"'  to WebAppClassLoader");
753
                classLoader.addClassPath(webAppClassPath);
754
                try {
755
                    jdk8MemleakFixInstance(classLoader, instance);
756
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
757
                        | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
758
                        | SecurityException e) {
759
                    logger.error("Cannot apply jdk8MemleakFix to instance " + instance, e);
760
                }
761
            } else {
762
                throw new RuntimeException("Classpath cdm-webapp for missing while running cdm-webapp from source folder. Please supplied cdm-server option '-" +  WEBAPP_CLASSPATH.getOpt() +"");
763
            }
764
            cdmWebappContext.setClassLoader(classLoader);
765
        }
766

    
767
        // --- configure centralized logging
768
        // 1. remove the ch.qos.logback.classic.servlet.LogbackServletContainerInitializer to prevent from stopping the
769
        //    logging context when one cdm webapp is being shut down (see https://dev.e-taxonomy.eu/redmine/issues/9236)
770
        //    --> exclude the ch.qos.logback.classic.servlet.LogbackServletContainerInitializer
771
        String regexEcludePattern = ".*LogbackServletContainerInitializer";
772
        cdmWebappContext.setAttribute(AnnotationConfiguration.SERVLET_CONTAINER_INITIALIZER_EXCLUSION_PATTERN, regexEcludePattern);
773
        // 2. wrap the context with the InstanceLogWrapper and modify class path patterns
774
        Handler contextWithCentralizedLogging = loggingConfigurator.configureWebApp(cdmWebappContext, instance);
775

    
776
        contexts.addHandler(contextWithCentralizedLogging);
777
        instance.setWebAppContext(cdmWebappContext);
778
        cdmWebappContext.addLifeCycleListener(instance);
779
        instance.setStatus(Status.stopped);
780

    
781
        return cdmWebappContext;
782
    }
783

    
784
    public String constructContextPath(Configuration conf) {
785
        return "/" + contextPathPrefix + conf.getInstanceName();
786
    }
787

    
788
    /**
789
     * Removes the given instance from the contexts. If the instance is running
790
     * at the time of calling this method it will be stopped first.
791
     * The JndiDataSource and the webapplicationContext will be released and removed.
792
     *
793
     * @param instance the instance to be removed
794
     *
795
     * @throws Exception in case stopping the instance fails
796
     */
797
    public void removeCdmInstanceContext(CdmInstance instance) throws Exception {
798

    
799
        if(instance.getWebAppContext() != null){
800
            if(instance.getWebAppContext().isRunning()){
801
                try {
802
                    instance.getWebAppContext().stop();
803
                } catch (Exception e) {
804
                    instance.getProblems().add("Error while stopping instance: " + e.getMessage());
805
                    throw e;
806
                }
807
            }
808
            contexts.removeHandler(instance.getWebAppContext());
809
            instance.releaseWebAppContext();
810
        } else  {
811
            // maybe something went wrong before, try to find the potentially lost
812
            // contexts directly in the server
813
            ContextHandler handler = getContextFor(instance.getConfiguration());
814
            if(handler != null){
815
                contexts.removeHandler(handler);
816
            }
817
        }
818
        instance.unbindJndiDataSource();
819
    }
820

    
821
    /**
822
     * Sets the webapp specified by the <code>webApplicationResource</code> to
823
     * the given <code>context</code>.
824
     *
825
     * @param context
826
     * @param webApplicationResource the resource can either be a directory containing
827
     * a Java web application or *.war file.
828
     *
829
     */
830
    private void setWebApp(WebAppContext context, File webApplicationResource) {
831
        if(webApplicationResource.isDirectory()){
832
            context.setResourceBase(webApplicationResource.getAbsolutePath());
833
            logger.debug("setting directory " + webApplicationResource.getAbsolutePath() + " as webapplication");
834
        } else {
835
            context.setWar(webApplicationResource.getAbsolutePath());
836
            logger.debug("setting war file " + webApplicationResource.getAbsolutePath() + " as webapplication");
837
        }
838
    }
839

    
840
    private void updateServerRunMode() {
841
        String webappPathNormalized = cdmRemoteWebAppFile.getAbsolutePath().replace('\\', '/');
842
        isRunningFromSource =  webappPathNormalized.endsWith("src/main/webapp");
843
        isRunningfromTargetFolder = webappPathNormalized.endsWith("cdm-webapp/target/cdmserver");
844
        isRunningFromWarFile = !(isRunningFromSource || isRunningfromTargetFolder);
845
    }
846

    
847
    public Server getServer() {
848
        return server;
849
    }
850

    
851
    public ContextHandler getContextFor(Configuration conf) {
852
        return getContextFor(constructContextPath(conf));
853
    }
854

    
855
    public ContextHandler getContextFor(String contextPath) {
856
        for( Handler handler : contexts.getHandlers()){
857
            if(handler instanceof ContextHandler){
858
                if(((ContextHandler)handler).getContextPath().equals(contextPath)){
859
                    return (ContextHandler)handler;
860
                }
861
            }
862
        }
863
        return null;
864
    }
865

    
866
    public ContextHandlerCollection getContexts() {
867
        return contexts;
868
    }
869

    
870
    /**
871
     * @return a File object pointing to the location of the cdm-webapp
872
     */
873
    public File getCdmRemoteWebAppFile(){
874
        if(cdmRemoteWebAppFile == null){
875
            throw new RuntimeException("Invalid order of action. Server not yet started. The startServer() method must be called first. ");
876
        }
877
        return cdmRemoteWebAppFile;
878
    }
879
}
(2-2/6)