Project

General

Profile

« Previous | Next » 

Revision bb431d84

Added by Andreas Kohlbecker over 5 years ago

  • ID bb431d84cbeed467923c2c12bdc7782e1ebfd865
  • Parent dc6c8364

ref #7085 changing logging initialization for more consistent logging configuration for bootloader and instances - experimental!

View differences:

src/main/java/eu/etaxonomy/cdm/server/Bootloader.java
47 47
import org.apache.commons.cli.ParseException;
48 48
import org.apache.commons.io.FileUtils;
49 49
import org.apache.commons.io.FilenameUtils;
50
import org.apache.log4j.LogManager;
50 51
import org.apache.log4j.Logger;
52
import org.apache.log4j.PatternLayout;
53
import org.apache.log4j.RollingFileAppender;
54
import org.apache.log4j.helpers.Loader;
51 55
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
52 56
import org.eclipse.jetty.jmx.MBeanContainer;
53 57
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
......
84 88

  
85 89

  
86 90

  
87
    private static final Logger logger = Logger.getLogger(Bootloader.class);
91
    private static Logger logger = null;
88 92

  
89 93
    private static final String DATASOURCE_BEANDEF_FILE = "datasources.xml";
90 94
    private static final String REALM_PROPERTIES_FILE = "cdm-server-realm.properties";
......
93 97
    private static final String TMP_PATH = USERHOME_CDM_LIBRARY_PATH + "server" + File.separator;
94 98
    private static final String LOG_PATH = USERHOME_CDM_LIBRARY_PATH + "log" + File.separator;
95 99

  
100
    private static final String SYSPROPKEY_LOGGING_FOLDER = "LOGGING_FOLDER";
101

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

  
......
103 109
    private static final File DEFAULT_WEBAPP_TEMP_FOLDER = new File(TMP_PATH + DEFAULT_WEBAPP_WAR_NAME);
104 110
    private static final File CDM_WEBAPP_TEMP_FOLDER = new File(TMP_PATH + CDM_WEBAPP);
105 111

  
106
    private final InstanceManager instanceManager = new InstanceManager(new File(USERHOME_CDM_LIBRARY_PATH, DATASOURCE_BEANDEF_FILE));
112
    private InstanceManager instanceManager = null;
107 113

  
108 114
    public List<CdmInstance> getCdmInstances() {
109
        return instanceManager.getInstances();
115
        return instanceManager().getInstances();
110 116
    }
111 117

  
112
    public InstanceManager getInstanceManager(){
118

  
119
    public InstanceManager instanceManager() {
120
        if(instanceManager == null){
121
            // the logging needs to be configured before instantiating the manager since it will need
122
            // the logging right away.
123
            configureLogging();
124
            instanceManager = new InstanceManager(new File(USERHOME_CDM_LIBRARY_PATH, DATASOURCE_BEANDEF_FILE));
125
        }
113 126
        return instanceManager;
114 127
    }
115 128

  
129

  
116 130
    private File cdmRemoteWebAppFile = null;
117 131
    private File defaultWebAppFile = null;
118 132

  
133
    /**
134
     * will either be set to {{@link #LOG_PATH} or to the path supplied
135
     * via the cdm-server start up options {@link CommandOptions#LOG_DIR}
136
     */
119 137
    private String logPath = null;
120 138

  
139
    private URL log4jconfigFileURL = null;
140

  
121 141
    private String webAppClassPath = null;
122 142

  
123 143
    /**
......
127 147
    private String contextPathPrefix = "";
128 148

  
129 149
    private Server server = null;
130
    private final ContextHandlerCollection contexts = new ContextHandlerCollection();
150
    private ContextHandlerCollection contexts = null;
131 151

  
132 152
    private CommandLine cmdLine;
133 153

  
......
156 176

  
157 177
    /* end of singleton implementation */
158 178

  
179
    private ContextHandlerCollection contexts(){
180
        if(contexts == null){
181
            contexts = new ContextHandlerCollection();
182
        }
183
        return contexts;
184
    }
185

  
186
    /**
187
     * @return
188
     */
189
    public Logger logger() {
190
        configureLogging();
191
        return logger;
192
    }
193

  
194
    /**
195
     *
196
     */
197
    public void configureLogging() {
198
        if(logger == null){
199
            if(cmdLine.hasOption(LOG_DIR.getOpt())){
200
                logPath = cmdLine.getOptionValue(LOG_DIR.getOpt());
201
            } else {
202
                logPath = LOG_PATH;
203
            }
204
            System.setProperty(SYSPROPKEY_LOGGING_FOLDER, logPath);
205
            logger = Logger.getLogger(Bootloader.class);
206
            if(System.getProperty(LogManager.DEFAULT_CONFIGURATION_KEY) == null){
207
                // externally defined log4j config file is supplied, this will be
208
                // used by the per instance logging configuration automatically
209
                logger.info("system property " + LogManager.DEFAULT_CONFIGURATION_KEY + " is not set, applying the default log4jconfigFile: " + log4jconfigFileURL);
210
                log4jconfigFileURL = Loader.getResource(LogManager.DEFAULT_CONFIGURATION_FILE);
211
                System.getProperties().put(LogManager.DEFAULT_CONFIGURATION_KEY, log4jconfigFileURL);
212
            } else {
213
                logger.info("system property " + LogManager.DEFAULT_CONFIGURATION_KEY + " is set to " + System.getProperty(LogManager.DEFAULT_CONFIGURATION_KEY));
214
            }
215
            logger.info("Logging to " + logPath);
216
        }
217
    }
218

  
219
    /**
220
         * Configures and adds a {@link RollingFileAppender} to the root logger
221
         *
222
         * The log files of the cdm-remote instances are configured by the
223
         * {@link eu.etaxonomy.cdm.api.config.LoggingConfigurer}
224
         *
225
         *
226
         */
227
       // ===== removing useless RollingFileAppender logger === see #6287
228
        private void configureFileLogger() {
229

  
230
            PatternLayout layout = new PatternLayout("%d %p [%c] - %m%n");
231
            try {
232
                String logFile = logPath + File.separator + "cdmserver.log";
233
                RollingFileAppender appender = new RollingFileAppender(layout, logFile);
234
                appender.setMaxBackupIndex(3);
235
                appender.setMaxFileSize("2MB");
236
                Logger.getRootLogger().addAppender(appender);
237
                Logger.getRootLogger().info("logging to :" + logFile);
238
            } catch (IOException e) {
239
                Logger.getRootLogger().error("Creating RollingFileAppender failed:", e);
240
            }
241
        }
159 242

  
160 243
    /**
161 244
     * @param input
......
214 297
        // 1. find in classpath
215 298
        URL resource = classLoader.getResource(warFileName);
216 299
        if (resource == null) {
217
            logger.error("Could not find the " + warFileName + " on classpath!");
300
            logger().error("Could not find the " + warFileName + " on classpath!");
218 301

  
219 302
            File pomxml = new File("pom.xml");
220 303
            if(pomxml.exists()){
221
                logger.info("will try find the war in target folder of maven project");
304
                logger().info("will try find the war in target folder of maven project");
222 305
                // 2. try finding in target folder of maven project
223 306
                File warFile = new File("target" + File.separator + warFileName);
224
                logger.debug("looking for war file at " + warFile.getAbsolutePath());
307
                logger().debug("looking for war file at " + warFile.getAbsolutePath());
225 308
                if (warFile.canRead()) {
226 309
                    resource = warFile.toURI().toURL();
227
                    logger.info("Success! Using war file from " + resource.toString());
310
                    logger().info("Success! Using war file from " + resource.toString());
228 311
                } else {
229
                    logger.error("Also could not find the " + warFileName + " in maven project, try excuting 'mvn install'");
312
                    logger().error("Also could not find the " + warFileName + " in maven project, try excuting 'mvn install'");
230 313
                }
231 314
            }
232 315
        }
......
239 322

  
240 323

  
241 324
        File extractedWarFile = new File(TMP_PATH, warName + "-" + WAR_POSTFIX);
242
        logger.info("Extracting " + resource + " to " + extractedWarFile + " ...");
325
        logger().info("Extracting " + resource + " to " + extractedWarFile + " ...");
243 326

  
244 327
        writeStreamTo(resource.openStream(), new FileOutputStream(extractedWarFile), 8 * KB);
245 328

  
......
250 333
            // unpack the archive
251 334
            File explodedWebApp = null;
252 335
            try {
253
                logger.info("Unpacking " + extractedWarFile);
336
                logger().info("Unpacking " + extractedWarFile);
254 337
                explodedWebApp  = unzip(extractedWarFile);
255 338

  
256 339
                // get the 'Bundle-Version' and 'Bnd-LastModified' properties of the
......
281 364
                            // always result in a higher value than the previous last modified time
282 365
                            // of any bundle"
283 366
                            cdmlibServicesVersion = attributes.getValue("Bundle-Version");
284
                            logger.warn("cdmlib-services version : " + cdmlibServicesVersion);
367
                            logger().warn("cdmlib-services version : " + cdmlibServicesVersion);
285 368
                            cdmlibServicesLastModified = attributes.getValue("Bnd-LastModified");
286
                            logger.warn("cdmlib-services last modified timestamp : " + cdmlibServicesLastModified);
369
                            logger().warn("cdmlib-services last modified timestamp : " + cdmlibServicesLastModified);
287 370

  
288 371
                            if(cdmlibServicesVersion == null || cdmlibServicesLastModified == null) {
289 372
                                throw new IllegalStateException("Invalid cdmlib-services manifest file");
......
294 377
                    }
295 378
                }
296 379
            } catch (IOException e) {
297
                logger.error("extractWar() - Unziping of war file " + explodedWebApp + " failed. Will return the war file itself instead of the extracted folder.", e);
380
                logger().error("extractWar() - Unziping of war file " + explodedWebApp + " failed. Will return the war file itself instead of the extracted folder.", e);
298 381
            }
299 382
            return explodedWebApp;
300 383
        }
301 384
    }
302 385

  
303

  
304 386
    public String getCdmlibServicesVersion() {
305 387
        return cdmlibServicesVersion;
306 388
    }
......
344 426
    public void startServer() throws IOException,
345 427
            FileNotFoundException, Exception, InterruptedException {
346 428

  
347

  
348
        if(cmdLine.hasOption(LOG_DIR.getOpt())){
349
            logPath = cmdLine.getOptionValue(LOG_DIR.getOpt());
350
        } else {
351
            logPath = LOG_PATH;
352
        }
353

  
429
        configureLogging();
354 430

  
355 431
        //assure LOG_PATH exists
356 432
        File logPathFile = new File(logPath);
......
361 437
        //append logger
362 438
//        configureFileLogger();
363 439

  
364
        logger.info("Starting "+APPLICATION_NAME);
365
        logger.info("Using  " + System.getProperty("user.home") + " as home directory. Can be specified by -Duser.home=<FOLDER>");
440
        logger().info("Starting "+APPLICATION_NAME);
441
        logger().info("Using  " + System.getProperty("user.home") + " as home directory. Can be specified by -Duser.home=<FOLDER>");
366 442

  
367 443
        //assure TMP_PATH exists and clean it up
368 444
        File tempDir = new File(TMP_PATH);
369 445
        if(!tempDir.exists() && !tempDir.mkdirs()){
370
            logger.error("Error creating temporary directory for webapplications " + tempDir.getAbsolutePath());
446
            logger().error("Error creating temporary directory for webapplications " + tempDir.getAbsolutePath());
371 447
            System.exit(-1);
372 448
        } else {
373 449
            if(FileUtils.deleteQuietly(tempDir)){
374 450
                tempDir.mkdirs();
375
                logger.info("Old webapplications successfully cleared");
451
                logger().info("Old webapplications successfully cleared");
376 452
            }
377 453
        }
378 454
        tempDir = null;
......
385 461

  
386 462
            cdmRemoteWebAppFile = new File(cmdLine.getOptionValue(WEBAPP.getOpt()));
387 463
            if(cdmRemoteWebAppFile.isDirectory()){
388
                logger.info("using user defined web application folder: " + cdmRemoteWebAppFile.getAbsolutePath());
464
                logger().info("using user defined web application folder: " + cdmRemoteWebAppFile.getAbsolutePath());
389 465
            } else {
390
                logger.info("using user defined warfile: " + cdmRemoteWebAppFile.getAbsolutePath());
466
                logger().info("using user defined warfile: " + cdmRemoteWebAppFile.getAbsolutePath());
391 467
            }
392 468

  
393 469
            updateServerRunMode();
......
418 494
        if(cmdLine.hasOption(HTTP_PORT.getOpt())){
419 495
            try {
420 496
               httpPort = Integer.parseInt(cmdLine.getOptionValue(HTTP_PORT.getOpt()));
421
               logger.info(HTTP_PORT.getOpt()+" set to "+cmdLine.getOptionValue(HTTP_PORT.getOpt()));
497
               logger().info(HTTP_PORT.getOpt()+" set to "+cmdLine.getOptionValue(HTTP_PORT.getOpt()));
422 498
           } catch (NumberFormatException e) {
423
               logger.error("Supplied portnumber is not an integer");
499
               logger().error("Supplied portnumber is not an integer");
424 500
               System.exit(-1);
425 501
           }
426 502
        }
......
428 504
        if(cmdLine.hasOption(DATASOURCES_FILE.getOpt())){
429 505
             File datasourcesFile = new File(cmdLine.getOptionValue(DATASOURCES_FILE.getOpt()));
430 506
             if(datasourcesFile.canRead()) {
431
                instanceManager.setDatasourcesFile(datasourcesFile);
507
                instanceManager().setDatasourcesFile(datasourcesFile);
432 508
            } else {
433
                logger.error("File set as " + DATASOURCES_FILE.getOpt()
509
                logger().error("File set as " + DATASOURCES_FILE.getOpt()
434 510
                        + " (" + cmdLine.getOptionValue(DATASOURCES_FILE.getOpt())
435 511
                        + ") is not readable.");
436 512
            }
......
451 527
        verifySystemResources();
452 528

  
453 529
         // load the configured instances for the first time
454
        instanceManager.reLoadInstanceConfigurations();
530
        instanceManager().reLoadInstanceConfigurations();
455 531

  
456 532
        // in jetty 9 currently each connector uses
457 533
        // 2 threads -  1 to select for IO activity and 1 to accept new connections.
......
459 535
//        QueuedThreadPool threadPool = new QueuedThreadPool(JvmManager.availableProcessors() +  + 200);
460 536
//        server = new Server(threadPool);
461 537
        server = new Server();
462
        server.addLifeCycleListener(instanceManager);
538
        server.addLifeCycleListener(instanceManager());
463 539
        ServerConnector connector=new ServerConnector(server);
464 540
        connector.setPort(httpPort);
465 541
        server.addConnector(connector );
......
471 547

  
472 548
        // JMX support
473 549
        if(cmdLine.hasOption(JMX.getOpt())){
474
            logger.info("adding JMX support ...");
550
            logger().info("adding JMX support ...");
475 551
            MBeanContainer mBeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
476 552
            server.addEventListener(mBeanContainer);
477 553
            server.addBean(Log.getLog());
......
485 561
        }
486 562

  
487 563
        // add default servlet context
488
        logger.info("preparing default WebAppContext");
564
        logger().info("preparing default WebAppContext");
489 565
        WebAppContext defaultWebappContext = new WebAppContext();
490 566

  
491 567
        setWebApp(defaultWebappContext, defaultWebAppFile);
......
507 583
        // Context path
508 584
        //
509 585
        defaultWebappContext.setContextPath("/" + (contextPathPrefix.isEmpty() ? "" : contextPathPrefix.substring(0, contextPathPrefix.length() - 1)));
510
        logger.info("defaultWebapp (manager) context path:" + defaultWebappContext.getContextPath());
586
        logger().info("defaultWebapp (manager) context path:" + defaultWebappContext.getContextPath());
511 587
        defaultWebappContext.setTempDirectory(DEFAULT_WEBAPP_TEMP_FOLDER);
512 588

  
513 589
        // configure security context
......
521 597
        // the defaultWebappContext MUST USE the super classloader
522 598
        // otherwise the status page (index.jsp) might not work
523 599
        defaultWebappContext.setClassLoader(this.getClass().getClassLoader());
524
        contexts.addHandler(defaultWebappContext);
600
        contexts().addHandler(defaultWebappContext);
525 601

  
526
        logger.info("setting contexts ...");
527
        server.setHandler(contexts);
528
        logger.info("starting jetty ...");
602
        logger().info("setting contexts ...");
603
        server.setHandler(contexts());
604
        logger().info("starting jetty ...");
529 605
//        try {
530 606

  
531 607
            server.start();
......
545 621
//        }
546 622

  
547 623
        if(cmdLine.hasOption(WIN32SERVICE.getOpt())){
548
            logger.info("jetty has started as win32 service");
624
            logger().info("jetty has started as win32 service");
549 625
        } else {
550 626
            server.join();
551
            logger.info(APPLICATION_NAME+" stopped.");
627
            logger().info(APPLICATION_NAME+" stopped.");
552 628
            System.exit(0);
553 629
        }
554 630
    }
......
606 682
                     while ((line = in.readLine()) != null) {
607 683
                         response.append(line);
608 684
                     }
609
               logger.info("OS Limit (Linux): maximum number of open files: " + response);
685
               logger().info("OS Limit (Linux): maximum number of open files: " + response);
610 686
            } catch (IOException e) {
611 687
                // TODO Auto-generated catch block
612 688
                e.printStackTrace();
613 689
            }
614 690
        } else {
615
            logger.info("verifySystemResources only implemented for linux");
691
            logger().info("verifySystemResources only implemented for linux");
616 692
        }
617 693
    }
618 694

  
619 695

  
620 696
    /**
621
     * Configures and adds a {@link RollingFileAppender} to the root logger
622
     *
623
     * The log files of the cdm-remote instances are configured by the
624
     * {@link eu.etaxonomy.cdm.api.config.LoggingConfigurer}
625
     *
626
     *
627
     */
628
//   ===== removing useless RollingFileAppender logger === see #6287
629
//    private void configureFileLogger() {
630
//
631
//        PatternLayout layout = new PatternLayout("%d %p [%c] - %m%n");
632
//        try {
633
//            String logFile = logPath + File.separator + "cdmserver.log";
634
//            RollingFileAppender appender = new RollingFileAppender(layout, logFile);
635
//            appender.setMaxBackupIndex(3);
636
//            appender.setMaxFileSize("2MB");
637
//            Logger.getRootLogger().addAppender(appender);
638
//            logger.info("logging to :" + logFile);
639
//        } catch (IOException e) {
640
//            logger.error("Creating RollingFileAppender failed:", e);
641
//        }
642
//    }
643

  
644

  
645
    /**
646 697
     * Adds a new WebAppContext to the contexts of the running jetty instance.
647 698
     * <ol>
648 699
     * <li>Initialize WebAppContext:
......
663 714
     *         {@link Status.#disabled} or if it is already added.
664 715
     * @throws IOException
665 716
     */
717
    @SuppressWarnings("deprecation")
666 718
    public WebAppContext addCdmInstanceContext(CdmInstance instance) throws IOException {
667 719

  
668 720
        Configuration conf = instance.getConfiguration();
669 721
        if(!instance.isEnabled()){
670
            logger.info(conf.getInstanceName() + " is disabled, possibly due to JVM memory limitations");
722
            logger().info(conf.getInstanceName() + " is disabled, possibly due to JVM memory limitations");
671 723
            return null;
672 724
        }
673 725
        if(getContextFor(conf) != null){
674
            logger.info(conf.getInstanceName() + " is alreaddy added to the contexts - skipping");
726
            logger().info(conf.getInstanceName() + " is alreaddy added to the contexts - skipping");
675 727
            return null;
676 728
        }
677 729

  
678 730
        instance.setStatus(Status.initializing);
679
        logger.info("preparing WebAppContext for '"+ conf.getInstanceName() + "'");
731
        logger().info("preparing WebAppContext for '"+ conf.getInstanceName() + "'");
680 732
        WebAppContext cdmWebappContext = new WebAppContext();
681 733

  
682 734
        cdmWebappContext.setContextPath(constructContextPath(conf));
683
        logger.info("contextPath: " + cdmWebappContext.getContextPath());
735
        logger().info("contextPath: " + cdmWebappContext.getContextPath());
684 736
        // set persistTempDirectory to prevent jetty from creating and deleting this directory for each instance,
685 737
        // since this behaviour can cause conflicts during parallel start up  of instances.
686 738
        cdmWebappContext.setPersistTempDirectory(true);
......
696 748

  
697 749
        cdmWebappContext.setAttribute(SharedAttributes.ATTRIBUTE_DATASOURCE_NAME, conf.getInstanceName());
698 750
        cdmWebappContext.setAttribute(SharedAttributes.ATTRIBUTE_JDBC_JNDI_NAME, conf.getJdbcJndiName());
751
        if(log4jconfigFileURL != null){
752
            cdmWebappContext.setAttribute(LogManager.DEFAULT_CONFIGURATION_KEY, log4jconfigFileURL);
753
            System.getProperties().put(LogManager.DEFAULT_CONFIGURATION_KEY, log4jconfigFileURL);
754
        }
755
        // log4jconfigFileURL
699 756
        if(cmdLine.hasOption(FORCE_SCHEMA_UPDATE.getOpt())){
700 757
            cdmWebappContext.getAttributes().setAttribute(SharedAttributes.ATTRIBUTE_FORCE_SCHEMA_UPDATE, "true");
701 758
        }
......
716 773
             */
717 774
            WebAppClassLoader classLoader = new WebAppClassLoader(cdmWebappContext);
718 775
            if(webAppClassPath != null){
719
                logger.info("Running cdm-webapp from source folder: Adding class path supplied by option '-" +  WEBAPP_CLASSPATH.getOpt() +" =" + webAppClassPath +"'  to WebAppClassLoader");
776
                logger().info("Running cdm-webapp from source folder: Adding class path supplied by option '-" +  WEBAPP_CLASSPATH.getOpt() +" =" + webAppClassPath +"'  to WebAppClassLoader");
720 777
                classLoader.addClassPath(webAppClassPath);
721 778
            } else {
722 779
                throw new RuntimeException("Classpath cdm-webapp for missing while running cdm-webapp from source folder. Please supplied cdm-server option '-" +  WEBAPP_CLASSPATH.getOpt() +"");
......
724 781
            cdmWebappContext.setClassLoader(classLoader);
725 782
        }
726 783

  
727
        contexts.addHandler(cdmWebappContext);
784
        contexts().addHandler(cdmWebappContext);
728 785
        instance.setWebAppContext(cdmWebappContext);
729 786
        cdmWebappContext.addLifeCycleListener(instance);
730 787
        instance.setStatus(Status.stopped);
......
761 818
                    throw e;
762 819
                }
763 820
            }
764
            contexts.removeHandler(instance.getWebAppContext());
821
            contexts().removeHandler(instance.getWebAppContext());
765 822
            instance.releaseWebAppContext();
766 823
        } else  {
767 824
            // maybe something went wrong before, try to find the potentially lost
768 825
            // contexts directly in the server
769 826
            ContextHandler handler = getContextFor(instance.getConfiguration());
770 827
            if(handler != null){
771
                contexts.removeHandler(handler);
828
                contexts().removeHandler(handler);
772 829
            }
773 830
        }
774 831
        instance.unbindJndiDataSource();
......
786 843
    private void setWebApp(WebAppContext context, File webApplicationResource) {
787 844
        if(webApplicationResource.isDirectory()){
788 845
            context.setResourceBase(webApplicationResource.getAbsolutePath());
789
            logger.debug("setting directory " + webApplicationResource.getAbsolutePath() + " as webapplication");
846
            logger().debug("setting directory " + webApplicationResource.getAbsolutePath() + " as webapplication");
790 847
        } else {
791 848
            context.setWar(webApplicationResource.getAbsolutePath());
792
            logger.debug("setting war file " + webApplicationResource.getAbsolutePath() + " as webapplication");
849
            logger().debug("setting war file " + webApplicationResource.getAbsolutePath() + " as webapplication");
793 850
        }
794 851
    }
795 852

  
......
810 867
    }
811 868

  
812 869
    public ContextHandler getContextFor(String contextPath) {
813
        for( Handler handler : contexts.getHandlers()){
870
        for( Handler handler : contexts().getHandlers()){
814 871
            if(handler instanceof ContextHandler){
815 872
                if(((ContextHandler)handler).getContextPath().equals(contextPath)){
816 873
                    return (ContextHandler)handler;
......
821 878
    }
822 879

  
823 880
    public ContextHandlerCollection getContexts() {
824
        return contexts;
881
        return contexts();
825 882
    }
826 883

  
827 884
    /**

Also available in: Unified diff