Project

General

Profile

Download (36.6 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

    
10
package eu.etaxonomy.cdm.server;
11

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

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

    
45
import org.apache.commons.cli.CommandLine;
46
import org.apache.commons.cli.CommandLineParser;
47
import org.apache.commons.cli.GnuParser;
48
import org.apache.commons.cli.HelpFormatter;
49
import org.apache.commons.cli.ParseException;
50
import org.apache.commons.io.FileUtils;
51
import org.apache.commons.io.FilenameUtils;
52
import org.apache.log4j.Logger;
53
import org.apache.tomcat.SimpleInstanceManager;
54
import org.apache.tomcat.util.scan.StandardJarScanner;
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
/**
80
 * A bootstrap class for starting Jetty Runner using an embedded war.
81
 *
82
 * @version $Revision$
83
 */
84
public final class Bootloader {
85

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

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

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

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

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

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

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

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

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

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

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

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

    
122
    private String webAppClassPath = null;
123

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

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

    
134
    private CommandLine cmdLine;
135

    
136
    private boolean isRunningFromSource;
137

    
138
    private boolean isRunningfromTargetFolder;
139

    
140
    private boolean isRunningFromWarFile;
141

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

    
145

    
146
    /* thread save singleton implementation */
147

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

    
150
    private Bootloader() {}
151

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

    
159
    /* end of singleton implementation */
160

    
161

    
162
    /**
163
     * @param input
164
     * @param output
165
     * @param bufferSize
166
     * @return
167
     * @throws IOException
168
     */
169
    public int writeStreamTo(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
170
        int available = Math.min(input.available(), 256 * KB);
171
        byte[] buffer = new byte[Math.max(bufferSize, available)];
172
        int answer = 0;
173
        int count = input.read(buffer);
174
        while (count >= 0) {
175
            output.write(buffer, 0, count);
176
            answer += count;
177
            count = input.read(buffer);
178
        }
179
        return answer;
180
    }
181

    
182

    
183

    
184
    public void parseCommandOptions(String[] args) throws ParseException {
185
        CommandLineParser parser = new GnuParser();
186

    
187
         cmdLine = parser.parse( CommandOptions.getOptions(), args );
188

    
189
         // print the help message
190
         if(cmdLine.hasOption(HELP.getOpt())){
191
             HelpFormatter formatter = new HelpFormatter();
192
             formatter.setWidth(200);
193
             formatter.printHelp( "java .. ", CommandOptions.getOptions() );
194
             System.exit(0);
195
         }
196
    }
197

    
198

    
199
    /**
200
     * Finds the named war file either in the resources known to the class loader
201
     * or in a target folder if the bootloader is started from within a maven project.
202
     * Once found the war file is copied to the temp folder defined by {@link TMP_PATH}.
203
     *
204
     * The war file can optionally be unpacked.
205
     *
206
     * @param warName
207
     * @param unpack
208
     *  unzip the war file after extraction
209
     * @return
210
     * @throws IOException
211
     * @throws FileNotFoundException
212
     */
213
    private File extractWar(String warName, boolean unpack) throws IOException, FileNotFoundException {
214
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
215
        String warFileName = warName + WAR_POSTFIX;
216

    
217
        // 1. find in classpath
218
        URL resource = classLoader.getResource(warFileName);
219
        if (resource == null) {
220
            logger.error("Could not find the " + warFileName + " on classpath!");
221

    
222
            File pomxml = new File("pom.xml");
223
            if(pomxml.exists()){
224
                logger.info("will try find the war in target folder of maven project");
225
                // 2. try finding in target folder of maven project
226
                File warFile = new File("target" + File.separator + warFileName);
227
                logger.debug("looking for war file at " + warFile.getAbsolutePath());
228
                if (warFile.canRead()) {
229
                    resource = warFile.toURI().toURL();
230
                    logger.info("Success! Using war file from " + resource.toString());
231
                } else {
232
                    logger.error("Also could not find the " + warFileName + " in maven project, try excuting 'mvn install'");
233
                }
234
            }
235
        }
236

    
237

    
238
        if (resource == null) {
239
            // no way finding the war file :-(
240
            System.exit(1);
241
        }
242

    
243

    
244
        File extractedWarFile = new File(TMP_PATH, warName + "-" + WAR_POSTFIX);
245
        logger.info("Extracting " + resource + " to " + extractedWarFile + " ...");
246

    
247
        writeStreamTo(resource.openStream(), new FileOutputStream(extractedWarFile), 8 * KB);
248

    
249
        if(!unpack) {
250
            // return the war file
251
            return extractedWarFile;
252
        } else {
253
            // unpack the archive
254
            File explodedWebApp = null;
255
            try {
256
                logger.info("Unpacking " + extractedWarFile);
257
                explodedWebApp  = unzip(extractedWarFile);
258

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

    
291
                            if(cdmlibServicesVersion == null || cdmlibServicesLastModified == null) {
292
                                throw new IllegalStateException("Invalid cdmlib-services manifest file");
293
                            }
294
                        } else {
295
                            throw new IllegalStateException("cdmlib-services jar not found ");
296
                        }
297
                    }
298
                }
299
            } catch (IOException e) {
300
                logger.error("extractWar() - Unziping of war file " + explodedWebApp + " failed. Will return the war file itself instead of the extracted folder.", e);
301
            }
302
            return explodedWebApp;
303
        }
304
    }
305

    
306

    
307
    public String getCdmlibServicesVersion() {
308
        return cdmlibServicesVersion;
309
    }
310

    
311
    public String getCdmlibServicesLastModified() {
312
        return cdmlibServicesLastModified;
313
    }
314
    /**
315
     * @param extractWar
316
     * @return
317
     * @throws IOException
318
     */
319
    private File unzip(File extractWar) throws IOException {
320
        UnzipUtility unzip = new UnzipUtility();
321

    
322
        String targetFolderName = FilenameUtils.getBaseName(extractWar.getName());
323
        File destDirectory = new File(TMP_PATH + File.separator + targetFolderName);
324
        unzip.unzip(extractWar, destDirectory);
325
        return destDirectory;
326
    }
327

    
328

    
329

    
330
    /**
331
     * MAIN METHOD
332
     *
333
     * @param args
334
     * @throws Exception
335
     */
336
    public static void main(String[] args) throws Exception {
337

    
338
        Bootloader bootloader = Bootloader.getBootloader();
339

    
340
        bootloader.parseCommandOptions(args);
341

    
342
        bootloader.startServer();
343
    }
344

    
345

    
346

    
347
    public void startServer() throws IOException,
348
            FileNotFoundException, Exception, InterruptedException {
349

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

    
353
        //assure TMP_PATH exists and clean it up
354
        File tempDir = new File(TMP_PATH);
355
        if(!tempDir.exists() && !tempDir.mkdirs()){
356
            logger.error("Error creating temporary directory for webapplications " + tempDir.getAbsolutePath());
357
            System.exit(-1);
358
        } else {
359
            if(FileUtils.deleteQuietly(tempDir)){
360
                tempDir.mkdirs();
361
                logger.info("Old webapplications successfully cleared");
362
            }
363
        }
364
        tempDir = null;
365

    
366

    
367
        // WEBAPP options
368
        //   prepare web application files to run either from war files (production mode)
369
        //   or from source (development mode)
370
        if(cmdLine.hasOption(WEBAPP.getOpt())){
371

    
372
            cdmRemoteWebAppFile = new File(cmdLine.getOptionValue(WEBAPP.getOpt()));
373
            if(cdmRemoteWebAppFile.isDirectory()){
374
                logger.info("using user defined web application folder: " + cdmRemoteWebAppFile.getAbsolutePath());
375
            } else {
376
                logger.info("using user defined warfile: " + cdmRemoteWebAppFile.getAbsolutePath());
377
            }
378

    
379
            updateServerRunMode();
380

    
381
            // load the default-web-application from source if running in development mode mode
382
            if(isRunningFromWarFile){
383
                defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME, false);
384
            } else {
385
                defaultWebAppFile = new File("./src/main/webapp");
386
            }
387

    
388
            if(isRunningFromSource){
389
                if(cmdLine.hasOption(WEBAPP_CLASSPATH.getOpt())){
390
                    String classPathOption = cmdLine.getOptionValue(WEBAPP_CLASSPATH.getOpt());
391
                    normalizeClasspath(classPathOption);
392
                }
393
            }
394
        } else {
395
            // read version number
396
            String version = readCdmRemoteVersion();
397
            cdmRemoteWebAppFile = extractWar(CDM_WEBAPP + "-" + version, true);
398
            defaultWebAppFile = extractWar(DEFAULT_WEBAPP_WAR_NAME, false);
399
        }
400

    
401

    
402
        // HTTP Port
403
        int httpPort = 8080;
404
        if(cmdLine.hasOption(HTTP_PORT.getOpt())){
405
            try {
406
               httpPort = Integer.parseInt(cmdLine.getOptionValue(HTTP_PORT.getOpt()));
407
               logger.info(HTTP_PORT.getOpt()+" set to "+cmdLine.getOptionValue(HTTP_PORT.getOpt()));
408
           } catch (NumberFormatException e) {
409
               logger.error("Supplied portnumber is not an integer");
410
               System.exit(-1);
411
           }
412
        }
413

    
414
        if(cmdLine.hasOption(DATASOURCES_FILE.getOpt())){
415
             File datasourcesFile = new File(cmdLine.getOptionValue(DATASOURCES_FILE.getOpt()));
416
             if(datasourcesFile.canRead()) {
417
                instanceManager.setDatasourcesFile(datasourcesFile);
418
            } else {
419
                logger.error("File set as " + DATASOURCES_FILE.getOpt()
420
                        + " (" + cmdLine.getOptionValue(DATASOURCES_FILE.getOpt())
421
                        + ") is not readable.");
422
            }
423
         }
424

    
425
        if(cmdLine.hasOption(CONTEXT_PATH_PREFIX.getOpt())){
426

    
427
            String cppo  = cmdLine.getOptionValue(CONTEXT_PATH_PREFIX.getOpt());
428
            if(cppo.equals("/")) {
429
                this.contextPathPrefix = "";
430
            } else {
431
                Pattern pattern = Pattern.compile("^/*(.*?)/*$");
432
                String replacement = "$1";
433
                this.contextPathPrefix = pattern.matcher(cppo).replaceAll(replacement) + "/";
434
            }
435
        }
436

    
437
        verifySystemResources();
438

    
439
         // load the configured instances for the first time
440
        instanceManager.reLoadInstanceConfigurations();
441

    
442
        if(System.getProperty(SPRING_PROFILES_ACTIVE) == null){
443
            logger.info(SPRING_PROFILES_ACTIVE + " is undefined, and will be set to : \"remoting\"");
444
            System.setProperty(SPRING_PROFILES_ACTIVE, "remoting");
445
        }
446

    
447
        // in jetty 9 currently each connector uses
448
        // 2 threads -  1 to select for IO activity and 1 to accept new connections.
449
        // there fore we need to add 2 to the number of cores
450
//        QueuedThreadPool threadPool = new QueuedThreadPool(JvmManager.availableProcessors() +  + 200);
451
//        server = new Server(threadPool);
452
        server = new Server();
453

    
454
        jdk8MemleakFixServer();
455

    
456
        loggingConfigurator.configureServer();
457

    
458
        server.addLifeCycleListener(instanceManager);
459
        ServerConnector connector = new ServerConnector(server);
460
        connector.setPort(httpPort);
461
        logger.info("http port: " + connector.getPort());
462
        server.addConnector(connector );
463

    
464
        org.eclipse.jetty.webapp.Configuration.ClassList classlist = org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server);
465
        classlist.addAfter(
466
                org.eclipse.jetty.webapp.FragmentConfiguration.class.getName(),
467
                org.eclipse.jetty.plus.webapp.EnvConfiguration.class.getName(),
468
                org.eclipse.jetty.plus.webapp.PlusConfiguration.class.getName()
469
                );
470
        classlist.addBefore(
471
                org.eclipse.jetty.webapp.JettyWebXmlConfiguration.class.getName(),
472
                org.eclipse.jetty.annotations.AnnotationConfiguration.class.getName());
473

    
474

    
475
        // JMX support
476
        if(cmdLine.hasOption(JMX.getOpt())){
477
            logger.info("adding JMX support ...");
478
            MBeanContainer mBeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
479
            server.addEventListener(mBeanContainer);
480
            server.addBean(Log.getLog());
481
        }
482

    
483
        if(cmdLine.hasOption(WIN32SERVICE.getOpt())){
484
            Win32Service win32Service = new Win32Service();
485
            win32Service.setServer(server);
486
            server.setStopAtShutdown(true);
487
            server.addBean(win32Service);
488
        }
489

    
490
        WebAppContext defaultWebappContext = createDefaultWebappContext();
491
        contexts.addHandler(defaultWebappContext);
492

    
493
        logger.info("setting contexts ...");
494
        server.setHandler(contexts);
495
        logger.info("starting jetty ...");
496
//        try {
497

    
498
            server.start();
499

    
500
//        } catch(org.springframework.beans.BeansException e){
501
//        	Throwable rootCause = null;
502
//        	while(e.getCause() != null){
503
//        		rootCause = e.getCause();
504
//        	}
505
//        	if(rootCause != null && rootCause.getClass().getSimpleName().equals("InvalidCdmVersionException")){
506
//
507
//        		logger.error("rootCause ----------->" + rootCause.getMessage());
508
////        		for(CdmInstanceProperties props : configAndStatus){
509
////        			if(props.getDataSourceName())
510
////        		}
511
//        	}
512
//        }
513

    
514
        if(cmdLine.hasOption(WIN32SERVICE.getOpt())){
515
            logger.info("jetty has started as win32 service");
516
        } else {
517
            server.join();
518
            logger.info(APPLICATION_NAME+" stopped.");
519
            System.exit(0);
520
        }
521
    }
522

    
523
    private WebAppContext createDefaultWebappContext() throws FileNotFoundException {
524
        // add default servlet context
525
        logger.info("preparing default WebAppContext");
526

    
527
        WebAppContext defaultWebappContext = new WebAppContext();
528
        setWebApp(defaultWebappContext, defaultWebAppFile);
529

    
530
        // JSP
531
        //
532
        // configuring jsp according to http://eclipse.org/jetty/documentation/current/configuring-jsp.html
533
        // from example http://eclipse.org/jetty/documentation/current/embedded-examples.html#embedded-webapp-jsp
534
        // Set the ContainerIncludeJarPattern so that jetty examines these
535
        // container-path jars for tlds, web-fragments etc.
536
        // If you omit the jar that contains the jstl .tlds, the jsp engine will
537
        // scan for them instead.
538
        defaultWebappContext.setAttribute(
539
                "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
540
                ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/[^/]*taglibs.*\\.jar$" );
541

    
542
        defaultWebappContext.setAttribute("org.eclipse.jetty.containerInitializers", jspInitializers());
543
        defaultWebappContext.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
544

    
545
        // Context path
546
        //
547
        defaultWebappContext.setContextPath("/" + (contextPathPrefix.isEmpty() ? "" : contextPathPrefix.substring(0, contextPathPrefix.length() - 1)));
548
        logger.info("defaultWebapp (manager) context path:" + defaultWebappContext.getContextPath());
549
        defaultWebappContext.setTempDirectory(DEFAULT_WEBAPP_TEMP_FOLDER);
550

    
551
        // configure security context
552
        // see for reference * http://docs.codehaus.org/display/JETTY/Realms
553
        //                   * http://wiki.eclipse.org/Jetty/Starting/Porting_to_Jetty_7
554
        HashLoginService loginService = new HashLoginService();
555
        File realmConfigFile = new File(USERHOME_CDM_LIBRARY_PATH + REALM_PROPERTIES_FILE);
556
        if(!realmConfigFile.canRead()) {
557
            throw new FileNotFoundException("Unable to find or read the realm file at " + realmConfigFile.getPath());
558
        }
559
        loginService.setConfig(realmConfigFile.getPath());
560
        defaultWebappContext.getSecurityHandler().setLoginService(loginService);
561

    
562
        // Set Classloader of Context to be sane (needed for JSTL)
563
        // JSP requires a non-System classloader, this simply wraps the
564
        // embedded System classloader in a way that makes it suitable
565
        // for JSP to use
566
        ClassLoader jspClassLoader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
567
        defaultWebappContext.setClassLoader(this.getClass().getClassLoader());
568
        // JspStarter to solve java.lang.IllegalStateException: No org.apache.tomcat.InstanceManager set in ServletContext problems
569
        // when running not from within the IDE (see https://issues.apache.org/jira/browse/KNOX-1639)
570
        defaultWebappContext.addBean(new JspStarter(defaultWebappContext));
571
        return defaultWebappContext;
572
    }
573

    
574
    /**
575
     * jdk8 memleak workaround: disable url caching
576
     *  see https://dev.e-taxonomy.eu/redmine/issues/5048
577
     *
578
     * @throws IOException
579
     * @throws MalformedURLException
580
     */
581
    private void jdk8MemleakFixServer() throws IOException, MalformedURLException {
582
        String javaVersion = System.getProperty("java.version");
583
        if(javaVersion.startsWith("1.8")){
584
            logger.info("jdk8 memory leak fix: jdk8 detected (" + javaVersion + ") disabling url caching to avoid memory leak.");
585
            org.eclipse.jetty.util.resource.Resource.setDefaultUseCaches(false);
586
            File tmpio = new File(System.getProperty("java.io.tmpdir"));
587
            tmpio.toURI().toURL().openConnection().setDefaultUseCaches(false);
588
        } else {
589
            logger.info("jdk8 memory leak fix: unaffected jdk " + javaVersion + " detected");
590
        }
591
    }
592

    
593
    private void jdk8MemleakFixInstance(ClassLoader classLoader, CdmInstance instance) throws IOException, MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
594
        String javaVersion = System.getProperty("java.version");
595
        if(javaVersion.startsWith("1.8")){
596
            logger.info("jdk8 memory leak fix for " + instance.getName() + ": jdk8 detected (" + javaVersion + ") disabling url caching to avoid memory leak.");
597
            Class<?> fileClass = classLoader.loadClass("java.io.File");
598
            File tmpio = (File) fileClass.getConstructor(String.class).newInstance("java.io.tmpdir");
599
            tmpio.toURI().toURL().openConnection().setDefaultUseCaches(false);
600
        } else {
601
            logger.info("jdk8 memory leak fix, " + instance.getName() + "unaffected jdk " + javaVersion + " detected");
602
        }
603
    }
604

    
605
    /**
606
     * @param classpath
607
     */
608
    private void normalizeClasspath(String classpath) {
609
        StringBuilder classPathBuilder = new StringBuilder((int) (classpath.length() * 1.2));
610
        String[] cpEntries = classpath.split("[\\:]");
611
        for(String cpEntry : cpEntries){
612
            classPathBuilder.append(',');
613
//            if(cpEntry.endsWith(".jar")){
614
//                classPathBuilder.append("jar:");
615
//            }
616
            classPathBuilder.append(cpEntry);
617
        }
618
        webAppClassPath = classPathBuilder.toString();
619
    }
620

    
621
    public String readCdmRemoteVersion() throws IOException {
622
        String version = "cdmlib version unreadable";
623
        InputStream versionInStream = Bootloader.class.getClassLoader().getResourceAsStream(VERSION_PROPERTIES_FILE);
624
        if (versionInStream != null){
625
            Properties versionProperties = new Properties();
626
            versionProperties.load(versionInStream);
627
            version = versionProperties.getProperty(CDM_WEBAPP_VERSION, version);
628
        }
629
        return version;
630
    }
631

    
632
    /**
633
    * Ensure the jsp engine is initialized correctly
634
    */
635
    private List<ContainerInitializer> jspInitializers()
636
    {
637
        JettyJasperInitializer sci = new JettyJasperInitializer();
638
        ContainerInitializer initializer = new ContainerInitializer(sci, null);
639
        List<ContainerInitializer> initializers = new ArrayList<ContainerInitializer>();
640
        initializers.add(initializer);
641
        return initializers;
642
    }
643

    
644
    /**
645
     * JspStarter for embedded ServletContextHandlers
646
     *
647
     * This is added as a bean that is a jetty LifeCycle on the ServletContextHandler.
648
     * This bean's doStart method will be called as the ServletContextHandler starts,
649
     * and will call the ServletContainerInitializer for the jsp engine.
650
     *
651
     */
652
    public static class JspStarter extends AbstractLifeCycle implements ServletContextHandler.ServletContainerInitializerCaller {
653
      JettyJasperInitializer sci;
654
      ServletContextHandler context;
655

    
656
      public JspStarter (ServletContextHandler context) {
657
        this.sci = new JettyJasperInitializer();
658
        this.context = context;
659
        this.context.setAttribute("org.apache.tomcat.JarScanner", new StandardJarScanner());
660
      }
661

    
662
      @Override
663
      protected void doStart() throws Exception
664
      {
665
        ClassLoader old = Thread.currentThread().getContextClassLoader();
666
        Thread.currentThread().setContextClassLoader(context.getClassLoader());
667
        try
668
        {
669
          sci.onStartup(null, context.getServletContext());
670
          super.doStart();
671
        }
672
        finally
673
        {
674
          Thread.currentThread().setContextClassLoader(old);
675
        }
676
      }
677
    }
678

    
679

    
680
    private void verifySystemResources() {
681

    
682
        OsChecker osChecker = new OsChecker();
683
        if(osChecker.isLinux()){
684
            try {
685
                Process p = Runtime.getRuntime().exec(new String[] { "bash", "-c", "ulimit -n" });
686
                BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
687
                String line;
688
                StringBuilder response = new StringBuilder();
689
                     while ((line = in.readLine()) != null) {
690
                         response.append(line);
691
                     }
692
               logger.info("OS Limit (Linux): maximum number of open files: " + response);
693
            } catch (IOException e) {
694
                // TODO Auto-generated catch block
695
                e.printStackTrace();
696
            }
697
        } else {
698
            logger.info("verifySystemResources only implemented for linux");
699
        }
700
    }
701

    
702

    
703
    /**
704
     * Adds a new WebAppContext to the contexts of the running jetty instance.
705
     * <ol>
706
     * <li>Initialize WebAppContext:
707
     * <ol>
708
     * <li>set context path</li>
709
     * <li>set tmp directory</li>
710
     * <li>bind JndiDataSource</li>
711
     * <li>set web app context attributes</li>
712
     * <li>create and setup individual classloader for the instance</li>
713
     * </ol>
714
     * </li>
715
     * <li>
716
     * finally add the new webappcontext to the contexts of the jetty instance</li>
717
     * </ol>
718
     *
719
     * @param instance
720
     * @return the instance given as parameter of null in case the instance has
721
     *         {@link Status.#disabled} or if it is already added.
722
     * @throws IOException
723
     */
724
    public WebAppContext addCdmInstanceContext(CdmInstance instance) throws IOException {
725

    
726
        Configuration conf = instance.getConfiguration();
727
        if(!instance.isEnabled()){
728
            logger.info(conf.getInstanceName() + " is disabled, possibly due to JVM memory limitations");
729
            return null;
730
        }
731
        if(getContextFor(conf) != null){
732
            logger.info(conf.getInstanceName() + " is alreaddy added to the contexts - skipping");
733
            return null;
734
        }
735

    
736
        instance.setStatus(Status.initializing);
737
        logger.info("preparing WebAppContext for '"+ conf.getInstanceName() + "'");
738
        WebAppContext cdmWebappContext = new WebAppContext();
739

    
740
        cdmWebappContext.setContextPath(constructContextPath(conf));
741
        logger.info("contextPath: " + cdmWebappContext.getContextPath());
742
        // set persistTempDirectory to prevent jetty from creating and deleting this directory for each instance,
743
        // since this behaviour can cause conflicts during parallel start up  of instances.
744
        cdmWebappContext.setPersistTempDirectory(true);
745

    
746

    
747
//        if(!instance.bindJndiDataSource()){
748
//            // a problem with the datasource occurred skip this webapp
749
//            cdmWebappContext = null;
750
//            logger.error("a problem with the datasource occurred -> skipping /" + conf.getInstanceName());
751
//            instance.setStatus(Status.error);
752
//            return cdmWebappContext;
753
//        }
754

    
755
        cdmWebappContext.setInitParameter(SharedAttributes.ATTRIBUTE_DATASOURCE_NAME, conf.getInstanceName());
756
        cdmWebappContext.setInitParameter(SharedAttributes.ATTRIBUTE_JDBC_JNDI_NAME, conf.getJdbcJndiName());
757
        if(cmdLine.hasOption(FORCE_SCHEMA_UPDATE.getOpt())){
758
            cdmWebappContext.setInitParameter(SharedAttributes.ATTRIBUTE_FORCE_SCHEMA_UPDATE, "true");
759
        }
760
        setWebApp(cdmWebappContext, getCdmRemoteWebAppFile());
761

    
762
        if( isRunningFromSource ){
763

    
764
            /*
765
             * when running the webapp from {projectpath} src/main/webapp we
766
             * must assure that each web application is using it's own
767
             * classloader thus we tell the WebAppClassLoader where the
768
             * dependencies of the webapplication can be found. Otherwise
769
             * the system classloader would load these resources.
770
             */
771
            WebAppClassLoader classLoader = new WebAppClassLoader(cdmWebappContext);
772
            if(webAppClassPath != null){
773
                logger.info("Running cdm-webapp from source folder: Adding class path supplied by option '-" +  WEBAPP_CLASSPATH.getOpt() +" =" + webAppClassPath +"'  to WebAppClassLoader");
774
                classLoader.addClassPath(webAppClassPath);
775
                try {
776
                    jdk8MemleakFixInstance(classLoader, instance);
777
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
778
                        | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
779
                        | SecurityException e) {
780
                    logger.error("Cannot apply jdk8MemleakFix to instance " + instance, e);
781
                }
782
            } else {
783
                throw new RuntimeException("Classpath cdm-webapp for missing while running cdm-webapp from source folder. Please supplied cdm-server option '-" +  WEBAPP_CLASSPATH.getOpt() +"");
784
            }
785
            cdmWebappContext.setClassLoader(classLoader);
786
        }
787

    
788
        contexts.addHandler(loggingConfigurator.configureWebApp(cdmWebappContext, instance));
789
        instance.setWebAppContext(cdmWebappContext);
790
        cdmWebappContext.addLifeCycleListener(instance);
791
        instance.setStatus(Status.stopped);
792

    
793
        return cdmWebappContext;
794
    }
795

    
796
    /**
797
     * @param conf
798
     * @return
799
     */
800
    public String constructContextPath(Configuration conf) {
801

    
802
        return "/" + contextPathPrefix + conf.getInstanceName();
803
    }
804

    
805
    /**
806
     * Removes the given instance from the contexts. If the instance is running
807
     * at the time of calling this method it will be stopped first.
808
     * The JndiDataSource and the webapplicationContext will be released and removed.
809
     *
810
     * @param instance the instance to be removed
811
     *
812
     * @throws Exception in case stopping the instance fails
813
     */
814
    public void removeCdmInstanceContext(CdmInstance instance) throws Exception {
815

    
816
        if(instance.getWebAppContext() != null){
817
            if(instance.getWebAppContext().isRunning()){
818
                try {
819
                    instance.getWebAppContext().stop();
820
                } catch (Exception e) {
821
                    instance.getProblems().add("Error while stopping instance: " + e.getMessage());
822
                    throw e;
823
                }
824
            }
825
            contexts.removeHandler(instance.getWebAppContext());
826
            instance.releaseWebAppContext();
827
        } else  {
828
            // maybe something went wrong before, try to find the potentially lost
829
            // contexts directly in the server
830
            ContextHandler handler = getContextFor(instance.getConfiguration());
831
            if(handler != null){
832
                contexts.removeHandler(handler);
833
            }
834
        }
835
        instance.unbindJndiDataSource();
836
    }
837

    
838
    /**
839
     * Sets the webapp specified by the <code>webApplicationResource</code> to
840
     * the given <code>context</code>.
841
     *
842
     * @param context
843
     * @param webApplicationResource the resource can either be a directory containing
844
     * a Java web application or *.war file.
845
     *
846
     */
847
    private void setWebApp(WebAppContext context, File webApplicationResource) {
848
        if(webApplicationResource.isDirectory()){
849
            context.setResourceBase(webApplicationResource.getAbsolutePath());
850
            logger.debug("setting directory " + webApplicationResource.getAbsolutePath() + " as webapplication");
851
        } else {
852
            context.setWar(webApplicationResource.getAbsolutePath());
853
            logger.debug("setting war file " + webApplicationResource.getAbsolutePath() + " as webapplication");
854
        }
855
    }
856

    
857
    private void updateServerRunMode() {
858
        String webappPathNormalized = cdmRemoteWebAppFile.getAbsolutePath().replace('\\', '/');
859
        isRunningFromSource =  webappPathNormalized.endsWith("src/main/webapp");
860
        isRunningfromTargetFolder = webappPathNormalized.endsWith("cdm-webapp/target/cdmserver");
861
        isRunningFromWarFile = !(isRunningFromSource || isRunningfromTargetFolder);
862
    }
863

    
864

    
865
    public Server getServer() {
866
        return server;
867
    }
868

    
869
    public ContextHandler getContextFor(Configuration conf) {
870
        return getContextFor(constructContextPath(conf));
871
    }
872

    
873
    public ContextHandler getContextFor(String contextPath) {
874
        for( Handler handler : contexts.getHandlers()){
875
            if(handler instanceof ContextHandler){
876
                if(((ContextHandler)handler).getContextPath().equals(contextPath)){
877
                    return (ContextHandler)handler;
878
                }
879
            }
880
        }
881
        return null;
882
    }
883

    
884
    public ContextHandlerCollection getContexts() {
885
        return contexts;
886
    }
887

    
888
    /**
889
     * @return a File object pointing to the location of the cdm-webapp
890
     */
891
    public File getCdmRemoteWebAppFile(){
892
        if(cdmRemoteWebAppFile == null){
893
            throw new RuntimeException("Invalid order of action. Server not yet started. The startServer() method must be called first. ");
894
        }
895
        return cdmRemoteWebAppFile;
896
    }
897
}
(2-2/6)