Project

General

Profile

Download (36.5 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.apache.jsp.JettyJasperInitializer;
55
import org.eclipse.jetty.jmx.MBeanContainer;
56
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
57
import org.eclipse.jetty.security.HashLoginService;
58
import org.eclipse.jetty.server.Handler;
59
import org.eclipse.jetty.server.Server;
60
import org.eclipse.jetty.server.ServerConnector;
61
import org.eclipse.jetty.server.handler.ContextHandler;
62
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
63
import org.eclipse.jetty.servlet.ServletContextHandler;
64
import org.eclipse.jetty.util.component.AbstractLifeCycle;
65
import org.eclipse.jetty.util.log.Log;
66
import org.eclipse.jetty.webapp.WebAppClassLoader;
67
import org.eclipse.jetty.webapp.WebAppContext;
68

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
120
    private String webAppClassPath = null;
121

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

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

    
132
    private CommandLine cmdLine;
133

    
134
    private boolean isRunningFromSource;
135

    
136
    private boolean isRunningfromTargetFolder;
137

    
138
    private boolean isRunningFromWarFile;
139

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

    
143

    
144
    /* thread save singleton implementation */
145

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

    
148
    private Bootloader() {}
149

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

    
157
    /* end of singleton implementation */
158

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

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

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

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

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

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

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

    
224

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

    
230

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

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

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

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

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

    
293

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

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

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

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

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

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

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

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

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

    
343

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

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

    
356
            updateServerRunMode();
357

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

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

    
378

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

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

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

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

    
414
        verifySystemResources();
415

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

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

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

    
431
        jdk8MemleakFixServer();
432

    
433

    
434
        loggingConfigurator.configureServer();
435

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

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

    
452

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

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

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

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

    
476
            server.start();
477

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
657

    
658
    private void verifySystemResources() {
659

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

    
680

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

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

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

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

    
724

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

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

    
740
        if( isRunningFromSource ){
741

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

    
766
        contexts.addHandler(loggingConfigurator.configureWebApp(cdmWebappContext, instance));
767
        instance.setWebAppContext(cdmWebappContext);
768
        cdmWebappContext.addLifeCycleListener(instance);
769
        instance.setStatus(Status.stopped);
770

    
771
        return cdmWebappContext;
772
    }
773

    
774
    public String constructContextPath(Configuration conf) {
775
        return "/" + contextPathPrefix + conf.getInstanceName();
776
    }
777

    
778
    /**
779
     * Removes the given instance from the contexts. If the instance is running
780
     * at the time of calling this method it will be stopped first.
781
     * The JndiDataSource and the webapplicationContext will be released and removed.
782
     *
783
     * @param instance the instance to be removed
784
     *
785
     * @throws Exception in case stopping the instance fails
786
     */
787
    public void removeCdmInstanceContext(CdmInstance instance) throws Exception {
788

    
789
        if(instance.getWebAppContext() != null){
790
            if(instance.getWebAppContext().isRunning()){
791
                try {
792
                    instance.getWebAppContext().stop();
793
                } catch (Exception e) {
794
                    instance.getProblems().add("Error while stopping instance: " + e.getMessage());
795
                    throw e;
796
                }
797
            }
798
            contexts.removeHandler(instance.getWebAppContext());
799
            instance.releaseWebAppContext();
800
        } else  {
801
            // maybe something went wrong before, try to find the potentially lost
802
            // contexts directly in the server
803
            ContextHandler handler = getContextFor(instance.getConfiguration());
804
            if(handler != null){
805
                contexts.removeHandler(handler);
806
            }
807
        }
808
        instance.unbindJndiDataSource();
809
    }
810

    
811
    /**
812
     * Sets the webapp specified by the <code>webApplicationResource</code> to
813
     * the given <code>context</code>.
814
     *
815
     * @param context
816
     * @param webApplicationResource the resource can either be a directory containing
817
     * a Java web application or *.war file.
818
     *
819
     */
820
    private void setWebApp(WebAppContext context, File webApplicationResource) {
821
        if(webApplicationResource.isDirectory()){
822
            context.setResourceBase(webApplicationResource.getAbsolutePath());
823
            logger.debug("setting directory " + webApplicationResource.getAbsolutePath() + " as webapplication");
824
        } else {
825
            context.setWar(webApplicationResource.getAbsolutePath());
826
            logger.debug("setting war file " + webApplicationResource.getAbsolutePath() + " as webapplication");
827
        }
828
    }
829

    
830
    private void updateServerRunMode() {
831
        String webappPathNormalized = cdmRemoteWebAppFile.getAbsolutePath().replace('\\', '/');
832
        isRunningFromSource =  webappPathNormalized.endsWith("src/main/webapp");
833
        isRunningfromTargetFolder = webappPathNormalized.endsWith("cdm-webapp/target/cdmserver");
834
        isRunningFromWarFile = !(isRunningFromSource || isRunningfromTargetFolder);
835
    }
836

    
837
    public Server getServer() {
838
        return server;
839
    }
840

    
841
    public ContextHandler getContextFor(Configuration conf) {
842
        return getContextFor(constructContextPath(conf));
843
    }
844

    
845
    public ContextHandler getContextFor(String contextPath) {
846
        for( Handler handler : contexts.getHandlers()){
847
            if(handler instanceof ContextHandler){
848
                if(((ContextHandler)handler).getContextPath().equals(contextPath)){
849
                    return (ContextHandler)handler;
850
                }
851
            }
852
        }
853
        return null;
854
    }
855

    
856
    public ContextHandlerCollection getContexts() {
857
        return contexts;
858
    }
859

    
860
    /**
861
     * @return a File object pointing to the location of the cdm-webapp
862
     */
863
    public File getCdmRemoteWebAppFile(){
864
        if(cdmRemoteWebAppFile == null){
865
            throw new RuntimeException("Invalid order of action. Server not yet started. The startServer() method must be called first. ");
866
        }
867
        return cdmRemoteWebAppFile;
868
    }
869
}
(2-2/6)