001/* 002 * Copyright 2012 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.runtime.servlet; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.FileOutputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.OutputStream; 024import java.net.URL; 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.time.ZonedDateTime; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.Enumeration; 031import java.util.Map; 032import java.util.Properties; 033import java.util.UUID; 034import java.util.regex.Pattern; 035 036import javax.servlet.ServletConfig; 037import javax.servlet.ServletContext; 038import javax.servlet.ServletException; 039import javax.servlet.ServletOutputStream; 040import javax.servlet.http.HttpServlet; 041import javax.servlet.http.HttpServletRequest; 042import javax.servlet.http.HttpServletResponse; 043import javax.servlet.http.HttpSession; 044import javax.xml.XMLConstants; 045import javax.xml.parsers.SAXParserFactory; 046import javax.xml.transform.OutputKeys; 047import javax.xml.transform.TransformerFactory; 048import javax.xml.transform.sax.SAXTransformerFactory; 049import javax.xml.transform.sax.TransformerHandler; 050import javax.xml.transform.stream.StreamResult; 051import javax.xml.transform.stream.StreamSource; 052import javax.xml.validation.Schema; 053import javax.xml.validation.SchemaFactory; 054 055import org.apache.avalon.excalibur.logger.LoggerManager; 056import org.apache.avalon.framework.configuration.Configuration; 057import org.apache.avalon.framework.configuration.ConfigurationException; 058import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 059import org.apache.avalon.framework.container.ContainerUtil; 060import org.apache.avalon.framework.context.DefaultContext; 061import org.apache.cocoon.Cocoon; 062import org.apache.cocoon.ConnectionResetException; 063import org.apache.cocoon.Constants; 064import org.apache.cocoon.ResourceNotFoundException; 065import org.apache.cocoon.environment.http.HttpContext; 066import org.apache.cocoon.environment.http.HttpEnvironment; 067import org.apache.cocoon.servlet.multipart.MultipartHttpServletRequest; 068import org.apache.cocoon.util.ClassUtils; 069import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 070import org.apache.cocoon.util.log.SLF4JLoggerManager; 071import org.apache.cocoon.xml.XMLUtils; 072import org.apache.commons.collections.EnumerationUtils; 073import org.apache.commons.io.FileUtils; 074import org.apache.commons.io.IOUtils; 075import org.apache.commons.lang3.StringUtils; 076import org.apache.commons.lang3.exception.ExceptionUtils; 077import org.apache.commons.lang3.time.StopWatch; 078import org.apache.log4j.Appender; 079import org.apache.log4j.LogManager; 080import org.apache.log4j.xml.DOMConfigurator; 081import org.apache.xml.serializer.OutputPropertiesFactory; 082import org.slf4j.Logger; 083import org.slf4j.LoggerFactory; 084import org.slf4j.MDC; 085import org.xml.sax.SAXException; 086import org.xml.sax.XMLReader; 087 088import org.ametys.core.ObservationConstants; 089import org.ametys.core.authentication.AuthenticateAction; 090import org.ametys.core.migration.MigrationEngine; 091import org.ametys.core.observation.Event; 092import org.ametys.core.observation.ObservationManager; 093import org.ametys.core.user.CurrentUserProvider; 094import org.ametys.core.user.UserIdentity; 095import org.ametys.core.util.DateUtils; 096import org.ametys.runtime.config.Config; 097import org.ametys.runtime.config.ConfigManager; 098import org.ametys.runtime.data.AmetysHomeLock; 099import org.ametys.runtime.data.AmetysHomeLockException; 100import org.ametys.runtime.log.MemoryAppender; 101import org.ametys.runtime.plugin.Init; 102import org.ametys.runtime.plugin.InitExtensionPoint; 103import org.ametys.runtime.plugin.PluginsManager; 104import org.ametys.runtime.plugin.PluginsManager.Status; 105import org.ametys.runtime.plugin.component.PluginsComponentManager; 106import org.ametys.runtime.plugins.admin.jvmstatus.ActiveSessionListener; 107import org.ametys.runtime.request.RequestFactory; 108import org.ametys.runtime.request.RequestListener; 109import org.ametys.runtime.request.RequestListenerManager; 110import org.ametys.runtime.util.AmetysHomeHelper; 111 112/** 113 * Main entry point for applications.<br> 114 * Overrides the CocoonServlet to add some initialization.<br> 115 */ 116public class RuntimeServlet extends HttpServlet 117{ 118 /** Constant for storing the {@link ServletConfig} in the Avalon context */ 119 public static final String CONTEXT_SERVLET_CONFIG = "servlet-config"; 120 121 /** Constant for storing the servlet context URL in the Avalon context */ 122 public static final String CONTEXT_CONTEXT_ROOT = "context-root"; 123 124 /** The cocoon.xconf URL */ 125 public static final String COCOON_CONF_URL = "/org/ametys/runtime/cocoon/cocoon.xconf"; 126 127 /** Default max upload size (10 Mb) */ 128 public static final int DEFAULT_MAX_UPLOAD_SIZE = 10 * 1024 * 1024; 129 130 /** The maintenance file name */ 131 public static final String MAINTENANCE_FILENAME = "maintenance.xml"; 132 133 /** The config file name */ 134 public static final String CONFIG_FILE_NAME = "config.xml"; 135 136 /** Name of the servlet initialization parameter for the ametys home property */ 137 public static final String AMETYS_HOME_PROPERTY = "ametys.home.property"; 138 139 /** The default ametys home path (relative to the servlet context) */ 140 public static final String AMETYS_HOME_DEFAULT = "/WEB-INF/data"; 141 142 /** The optional location (relative to application context) of the file that configures the list of plugins/workspaces/kernel to get somewhere else */ 143 public static final String EXTERNAL_LOCATIONS = "WEB-INF/param/external-locations.xml"; 144 145 /** File name in the administrator folder of the ametys home */ 146 public static final String INSTANCE_FILENAME = "instance-info.xml"; 147 148 /** The run modes */ 149 public enum RunMode 150 { 151 /** Application is currently starting */ 152 STARTING, 153 /** Application is currently migrating */ 154 MIGRATING, 155 /** Running init methods */ 156 INITIALIZING, 157 /** Normal execution mode */ 158 NORMAL, 159 /** Maintenance mode (See #getMaintenanceStatus) */ 160 MAINTENANCE, 161 /** Safe mode (See PluginsManager#getStatus) */ 162 SAFEMODE, 163 /** Fatal error during startup */ 164 FATALERROR, 165 /** Application is currently stopping */ 166 STOPPING 167 } 168 169 /** The maintenance status of Ametys */ 170 public enum MaintenanceStatus 171 { 172 /** No maintenance activated */ 173 NONE, 174 /** The maintenance was automatically switched on at startup due to a failed migration */ 175 AUTOMATIC, 176 /** The maintenance was manually switched on */ 177 FORCED 178 } 179 /** Informations about the maintenance status when forced 180 * @param comment The comment to explain why the maintenance was started 181 * @param initiator The user that started the maintenance 182 * @param since The date the maintenance was stated */ 183 public record ForcedMainteanceInformations(String comment, UserIdentity initiator, ZonedDateTime since) { /* empty */ } 184 185 private static RunMode _runMode = RunMode.STARTING; 186 private static MaintenanceStatus _maintenanceStatus; 187 private static ForcedMainteanceInformations _forcedMainteanceInformations; 188 private static Logger _logger; 189 private static ServletContext _servletContext; 190 191 private static ZonedDateTime _startupDate = ZonedDateTime.now(); 192 193 private String _servletContextPath; 194 private URL _servletContextURL; 195 private File _ametysHome; 196 197 private DefaultContext _avalonContext; 198 private HttpContext _context; 199 private Cocoon _cocoon; 200 private RequestFactory _requestFactory; 201 private File _workDir; 202 203 private int _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE; 204 private File _uploadDir; 205 private File _cacheDir; 206 207 private AmetysHomeLock _ametysHomeLock; 208 209 private LoggerManager _loggerManager; 210 211 private Throwable _exception; 212 213 private Collection<Pattern> _allowedURLPattern = Arrays.asList(Pattern.compile("_admin($|/.*)"), Pattern.compile("plugins/[^/]+/resources/.*")); 214 215 @Override 216 public void init() throws ServletException 217 { 218 try 219 { 220 // Set this property in order to avoid a System.err.println (CatalogManager.java) 221 if (System.getProperty("xml.catalog.ignoreMissing") == null) 222 { 223 System.setProperty("xml.catalog.ignoreMissing", "true"); 224 } 225 226 _servletContext = getServletContext(); 227 _servletContext.addListener(ActiveSessionListener.class); 228 _servletContextPath = _servletContext.getRealPath("/"); 229 230 _avalonContext = new DefaultContext(); 231 _context = new HttpContext(_servletContext); 232 _avalonContext.put(Constants.CONTEXT_ENVIRONMENT_CONTEXT, _context); 233 _avalonContext.put(Constants.CONTEXT_DEFAULT_ENCODING, "UTF-8"); 234 _avalonContext.put(CONTEXT_SERVLET_CONFIG, getServletConfig()); 235 236 _servletContextURL = new File(_servletContextPath).toURI().toURL(); 237 _avalonContext.put(CONTEXT_CONTEXT_ROOT, _servletContextURL); 238 239 URL configFile = getClass().getResource(COCOON_CONF_URL); 240 _avalonContext.put(Constants.CONTEXT_CONFIG_URL, configFile); 241 242 _workDir = new File((File) _servletContext.getAttribute("javax.servlet.context.tempdir"), "cocoon-files"); 243 _workDir.mkdirs(); 244 _avalonContext.put(Constants.CONTEXT_WORK_DIR, _workDir); 245 246 _cacheDir = new File(_workDir, "cache-dir"); 247 _cacheDir.mkdirs(); 248 _avalonContext.put(Constants.CONTEXT_CACHE_DIR, _cacheDir); 249 250 // Create temp dir if it does not exist 251 File tmpDir = new File(System.getProperty("java.io.tmpdir")); 252 if (!tmpDir.exists()) 253 { 254 FileUtils.forceMkdir(tmpDir); 255 } 256 257 // Init the Ametys home directory and lock before setting the logger. 258 // The log folder is located in the Ametys home directory. 259 _initAmetysHome(); 260 261 // Configuration file 262 File config = FileUtils.getFile(_ametysHome, AmetysHomeHelper.AMETYS_HOME_CONFIG_DIR, CONFIG_FILE_NAME); 263 final String fileName = config.getCanonicalPath(); 264 Config.setFilename(fileName); 265 266 // Init logger 267 _initLogger(); 268 269 _initAmetys(); 270 } 271 catch (Throwable t) 272 { 273 setRunMode(RunMode.FATALERROR); 274 275 if (_logger != null) 276 { 277 _logger.error("Error while loading Ametys. Entering in error mode.", t); 278 } 279 else 280 { 281 System.out.println("Error while loading Ametys. Entering in error mode."); 282 t.printStackTrace(); 283 } 284 285 _exception = t; 286 287 _disposeCocoon(); 288 } 289 } 290 291 private void _initAmetysHome() throws AmetysHomeLockException 292 { 293 String ametysHomePath = null; 294 295 boolean hasAmetysHomeProp = EnumerationUtils.toList(getInitParameterNames()).contains(AMETYS_HOME_PROPERTY); 296 if (!hasAmetysHomeProp) 297 { 298 System.out.println(String.format("[INFO] The '%s' servlet initialization parameter was not found. The Ametys home directory default value will be used '%s'", 299 AMETYS_HOME_PROPERTY, AMETYS_HOME_DEFAULT)); 300 } 301 else 302 { 303 String ametysHomeEnv = getInitParameter(AMETYS_HOME_PROPERTY); 304 if (StringUtils.isEmpty(ametysHomeEnv)) 305 { 306 System.out.println(String.format("[WARN] The '%s' servlet initialization parameter appears to be empty. Ametys home directory default value will be used '%s'", 307 AMETYS_HOME_PROPERTY, AMETYS_HOME_DEFAULT)); 308 } 309 else 310 { 311 ametysHomePath = System.getenv(ametysHomeEnv); 312 if (StringUtils.isEmpty(ametysHomePath)) 313 { 314 System.out.println(String.format( 315 "[WARN] The '%s' environment variable was not found or was empty. Ametys home directory default value will be used '%s'", 316 ametysHomeEnv, AMETYS_HOME_DEFAULT)); 317 } 318 } 319 } 320 321 if (StringUtils.isEmpty(ametysHomePath)) 322 { 323 ametysHomePath = _servletContext.getRealPath(AMETYS_HOME_DEFAULT); 324 } 325 326 System.out.println("Acquiring lock on " + ametysHomePath); 327 328 // Acquire the lock on Ametys home 329 _ametysHome = new File(ametysHomePath); 330 _ametysHome.mkdirs(); 331 332 _ametysHomeLock = new AmetysHomeLock(_ametysHome); 333 _ametysHomeLock.acquire(); 334 } 335 336 /** 337 * Get the instance identifier 338 * @return The UUID of the Ametys instance or null if the id could not be generated at startup 339 * @throws IllegalStateException If the file cannot be read 340 */ 341 public static String getInstanceId() 342 { 343 Path statisticsFile = AmetysHomeHelper.getAmetysHome().toPath().resolve(INSTANCE_FILENAME); 344 345 if (Files.exists(statisticsFile)) 346 { 347 try (InputStream is = Files.newInputStream(statisticsFile)) 348 { 349 Configuration configuration = new DefaultConfigurationBuilder().build(is); 350 return configuration.getChild("unique-id").getValue(); 351 } 352 catch (Exception e) 353 { 354 throw new IllegalStateException("Cannot read the ametys-home://" + INSTANCE_FILENAME + " file. Consider remove it and restart Ametys to recreate it", e); 355 } 356 } 357 358 throw new IllegalStateException("The file ametys-home://" + INSTANCE_FILENAME + " does not exist while it should have been created at startup"); 359 } 360 361 /** 362 * Create the instance identifier file if it does not exist 363 * @param defaultUUID The default UUID to use. If null or blank, a random UUID will be generated. 364 */ 365 public static void createInstanceId(String defaultUUID) 366 { 367 try 368 { 369 Path statisticsFile = AmetysHomeHelper.getAmetysHome().toPath().resolve(INSTANCE_FILENAME); 370 371 if (Files.exists(statisticsFile)) 372 { 373 return; 374 } 375 376 String newId = StringUtils.isBlank(defaultUUID) ? UUID.randomUUID().toString() : defaultUUID; 377 378 try (OutputStream os = Files.newOutputStream(statisticsFile)) 379 { 380 // create a transformer for saving sax into a file 381 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 382 383 StreamResult result = new StreamResult(os); 384 th.setResult(result); 385 386 // create the format of result 387 Properties format = new Properties(); 388 format.put(OutputKeys.METHOD, "xml"); 389 format.put(OutputKeys.INDENT, "yes"); 390 format.put(OutputKeys.ENCODING, "UTF-8"); 391 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 392 th.getTransformer().setOutputProperties(format); 393 394 th.startDocument(); 395 XMLUtils.startElement(th, "instance"); 396 XMLUtils.createElement(th, "unique-id", newId); 397 XMLUtils.endElement(th, "instance"); 398 th.endDocument(); 399 } 400 } 401 catch (Exception e) 402 { 403 _logger.error("Cannot create the instance id", e); 404 } 405 } 406 407 /** 408 * Get the last hot startup time 409 * @return The last hot startup time 410 */ 411 public static ZonedDateTime getLastStartupDate() 412 { 413 return _startupDate; 414 } 415 416 private void _initAmetys() throws Exception 417 { 418 _startupDate = ZonedDateTime.now(); 419 420 setRunMode(RunMode.STARTING); 421 _maintenanceStatus = null; 422 _exception = null; 423 424 // WEB-INF/param/runtime.xml loading 425 _loadRuntimeConfig(); 426 427 _createCocoon(); 428 429 // Upload initialization 430 _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE; 431 _uploadDir = new File(RuntimeConfig.getInstance().getAmetysHome(), "uploads"); 432 433 if (ConfigManager.getInstance().isComplete()) 434 { 435 Long maxUploadSizeParam = Config.getInstance().getValue("runtime.upload.max-size"); 436 if (maxUploadSizeParam != null) 437 { 438 // if the feature core/runtime.upload is deactivated, use the default value (10 Mb) 439 _maxUploadSize = maxUploadSizeParam.intValue(); 440 } 441 } 442 443 _uploadDir.mkdirs(); 444 _avalonContext.put(Constants.CONTEXT_UPLOAD_DIR, _uploadDir); 445 446 _requestFactory = new RequestFactory(true, _uploadDir, false, true, _maxUploadSize, "UTF-8"); 447 448 // Generate the instance-info file 449 createInstanceId(null); 450 451 PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager"); 452 if (doMigrationAndInit(pluginCM)) 453 { 454 restartCocoon(null); 455 } 456 else 457 { 458 if (PluginsManager.getInstance().isSafeMode()) 459 { 460 setRunMode(RunMode.SAFEMODE); 461 } 462 else if (getMaintenanceStatus() != MaintenanceStatus.NONE) 463 { 464 setRunMode(RunMode.MAINTENANCE); 465 } 466 else 467 { 468 setRunMode(RunMode.NORMAL); 469 } 470 } 471 } 472 473 private void _initLogger() 474 { 475 // Configure Log4j 476 String logj4fFile = _servletContext.getRealPath("/WEB-INF/log4j.xml"); 477 478 // Hack to have context-relative log files, because of lack in configuration capabilities in log4j. 479 // If there are more than one Ametys in the same JVM, the property will be successively set for each instance, 480 // so we heavily rely on DOMConfigurator beeing synchronous. 481 System.setProperty("ametys.home.dir", _ametysHome.getAbsolutePath()); 482 DOMConfigurator.configure(logj4fFile); 483 System.clearProperty("ametys.home.dir"); 484 485 Appender appender = new MemoryAppender(); 486 appender.setName(org.ametys.plugins.core.ui.log.LogManager.MEMORY_APPENDER_NAME); 487 LogManager.getRootLogger().addAppender(appender); 488 Enumeration<org.apache.log4j.Logger> categories = LogManager.getCurrentLoggers(); 489 while (categories.hasMoreElements()) 490 { 491 org.apache.log4j.Logger logger = categories.nextElement(); 492 logger.addAppender(appender); 493 } 494 495 _loggerManager = new SLF4JLoggerManager(); 496 _logger = LoggerFactory.getLogger(getClass()); 497 } 498 499 private void _createCocoon() throws Exception 500 { 501 _avalonContext.put(Constants.CONTEXT_CLASS_LOADER, getClass().getClassLoader()); 502 _avalonContext.put(Constants.CONTEXT_CLASSPATH, ""); 503 504 URL configFile = (URL) _avalonContext.get(Constants.CONTEXT_CONFIG_URL); 505 506 _logger.info("Reloading from: {}", configFile.toExternalForm()); 507 508 Cocoon c = (Cocoon) ClassUtils.newInstance("org.apache.cocoon.Cocoon"); 509 ContainerUtil.enableLogging(c, new SLF4JLoggerAdapter(_logger)); 510 c.setLoggerManager(_loggerManager); 511 ContainerUtil.contextualize(c, _avalonContext); 512 ContainerUtil.initialize(c); 513 514 _cocoon = c; 515 } 516 517 /** 518 * Init migration and plugins 519 * @param pluginCM Plugins Component Manager 520 * @return true if a server restart is required 521 * @throws Exception Something went wrong 522 */ 523 public static boolean doMigrationAndInit(PluginsComponentManager pluginCM) throws Exception 524 { 525 if (_initMigration(pluginCM)) 526 { 527 return true; 528 } 529 530 _initPlugins(pluginCM); 531 532 return false; 533 } 534 535 private static void _initPlugins(PluginsComponentManager pluginCM) throws Exception 536 { 537 setRunMode(RunMode.INITIALIZING); 538 539 // If we're in safe mode 540 if (!PluginsManager.getInstance().isSafeMode()) 541 { 542 // Plugins Init class execution 543 InitExtensionPoint initExtensionPoint = (InitExtensionPoint) pluginCM.lookup(InitExtensionPoint.ROLE); 544 for (String id : initExtensionPoint.getExtensionsIds()) 545 { 546 Init init = initExtensionPoint.getExtension(id); 547 init.init(); 548 } 549 550 // Application Init class execution if available 551 if (pluginCM.hasComponent(Init.ROLE)) 552 { 553 Init init = (Init) pluginCM.lookup(Init.ROLE); 554 init.init(); 555 } 556 } 557 } 558 559 private static boolean _initMigration(PluginsComponentManager pluginCM) throws Exception 560 { 561 setRunMode(RunMode.MIGRATING); 562 563 MigrationEngine migrationEngine = (MigrationEngine) pluginCM.lookup(MigrationEngine.ROLE); 564 return migrationEngine.migrate(); 565 } 566 567 private void _loadRuntimeConfig() throws ServletException 568 { 569 Configuration runtimeConf = null; 570 try 571 { 572 // XML Schema validation 573 SAXParserFactory factory = SAXParserFactory.newInstance(); 574 factory.setNamespaceAware(true); 575 576 URL schemaURL = getClass().getResource("runtime-4.1.xsd"); 577 Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(schemaURL); 578 factory.setSchema(schema); 579 580 XMLReader reader = factory.newSAXParser().getXMLReader(); 581 DefaultConfigurationBuilder runtimeConfBuilder = new DefaultConfigurationBuilder(reader); 582 583 File runtimeConfigFile = new File(_servletContextPath, "WEB-INF/param/runtime.xml"); 584 try (InputStream runtime = new FileInputStream(runtimeConfigFile)) 585 { 586 runtimeConf = runtimeConfBuilder.build(runtime, runtimeConfigFile.getAbsolutePath()); 587 } 588 } 589 catch (Exception e) 590 { 591 _logger.error("Unable to load runtime file at 'WEB-INF/param/runtime.xml'. PluginsManager will enter in safe mode.", e); 592 } 593 594 Configuration externalConf = null; 595 try 596 { 597 DefaultConfigurationBuilder externalConfBuilder = new DefaultConfigurationBuilder(); 598 599 File externalConfigFile = new File(_servletContextPath, EXTERNAL_LOCATIONS); 600 if (externalConfigFile.exists()) 601 { 602 try (InputStream external = new FileInputStream(externalConfigFile)) 603 { 604 externalConf = externalConfBuilder.build(external, externalConfigFile.getAbsolutePath()); 605 } 606 } 607 } 608 catch (Exception e) 609 { 610 _logger.error("Unable to load external locations values at " + EXTERNAL_LOCATIONS, e); 611 throw new ServletException("Unable to load external locations values at " + EXTERNAL_LOCATIONS, e); 612 } 613 614 RuntimeConfig.configure(runtimeConf, externalConf, _ametysHome, _servletContextPath); 615 } 616 617 @Override 618 public void destroy() 619 { 620 setRunMode(RunMode.STOPPING); 621 622 if (_cocoon != null) 623 { 624 _logger.debug("Servlet destroyed - disposing Cocoon"); 625 _disposeCocoon(); 626 } 627 628 if (_ametysHomeLock != null) 629 { 630 _ametysHomeLock.release(); 631 _ametysHomeLock = null; 632 } 633 634 _avalonContext = null; 635 _logger = null; 636 _loggerManager = null; 637 } 638 639 640 private final void _disposeCocoon() 641 { 642 if (_cocoon != null) 643 { 644 ContainerUtil.dispose(_cocoon); 645 _cocoon = null; 646 } 647 } 648 649 private String _getRestartMessage() 650 { 651 if (_cocoon == null) 652 { 653 return "Ametys is restarting"; 654 } 655 switch (getRunMode()) 656 { 657 case RunMode.STARTING: return "Ametys is restarting"; 658 case RunMode.MIGRATING: return "Ametys is migrating data"; 659 case RunMode.INITIALIZING: return "Ametys is initializing"; 660 default: return null; 661 } 662 } 663 664 @Override 665 public final void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException 666 { 667 req.setCharacterEncoding("UTF-8"); 668 669 // Error mode 670 if (_exception != null) 671 { 672 _renderSpecialPage(req, res, _exception, "An error occured during Ametys initialization.", true); 673 return; 674 } 675 676 // Restarting 677 String msg = _getRestartMessage(); 678 if (msg != null) 679 { 680 _renderSpecialPage(req, res, null, msg, false); 681 return; 682 } 683 684 String uri = _retrieveUri(req); 685 686 if (PluginsManager.getInstance().isSafeMode()) 687 { 688 // safe mode 689 String finalUri = uri; 690 boolean allowed = _allowedURLPattern.stream().anyMatch(p -> p.matcher(finalUri).matches()); 691 if (!allowed) 692 { 693 res.addHeader("X-Ametys-SafeMode", "true"); 694 695 Status status = PluginsManager.getInstance().getStatus(); 696 697 if (status == Status.NO_CONFIG) 698 { 699 res.sendRedirect(req.getContextPath() + "/_admin/public/first-start.html"); 700 return; 701 } 702 else if (status == Status.CONFIG_INCOMPLETE) 703 { 704 res.sendRedirect(req.getContextPath() + "/_admin/public/load-config.html"); 705 return; 706 } 707 else 708 { 709 res.sendRedirect(req.getContextPath() + "/_admin/public/safe-mode.html"); 710 return; 711 } 712 } 713 } 714 715 UserIdentity authenticatedUser = null; 716 HttpSession session = req.getSession(false); 717 if (session != null) 718 { 719 authenticatedUser = (UserIdentity) session.getAttribute(AuthenticateAction.SESSION_USERIDENTITY); 720 } 721 722 MDC.remove("user"); 723 724 if (authenticatedUser != null) 725 { 726 MDC.put("user", UserIdentity.userIdentityToString(authenticatedUser)); 727 } 728 729 MDC.put("requestURI", req.getRequestURI()); 730 731 StopWatch stopWatch = new StopWatch(); 732 HttpServletRequest request = null; 733 try 734 { 735 // used for timing the processing 736 stopWatch.start(); 737 738 _fireRequestStarted(req); 739 740 // get the request (wrapped if contains multipart-form data) 741 request = _requestFactory.getServletRequest(req); 742 743 // Process the request 744 HttpEnvironment env = new HttpEnvironment(uri, _servletContextURL.toExternalForm(), request, res, _servletContext, _context, "UTF-8", "UTF-8"); 745 env.enableLogging(new SLF4JLoggerAdapter(_logger)); 746 747 if (!_cocoon.process(env)) 748 { 749 // We reach this when there is nothing in the processing change that matches 750 // the request. For example, no matcher matches. 751 _logger.error("The Cocoon engine failed to process the request."); 752 _renderSpecialPage(request, res, null, "Cocoon engine failed to process the request", true); 753 } 754 } 755 catch (ResourceNotFoundException | ConnectionResetException | IOException e) 756 { 757 _logger.warn(e.toString()); 758 _renderSpecialPage(request, res, e, e.getMessage(), true); 759 } 760 catch (Throwable e) 761 { 762 _logger.error("Internal Cocoon Problem", e); 763 _renderSpecialPage(request, res, e, "Internal Cocoon Problem", true); 764 } 765 finally 766 { 767 stopWatch.stop(); 768 _logger.info("'{}' processed in {} ms.", uri, stopWatch.getDuration()); 769 770 try 771 { 772 if (request instanceof MultipartHttpServletRequest) 773 { 774 _logger.debug("Deleting uploaded file(s)."); 775 ((MultipartHttpServletRequest) request).cleanup(); 776 } 777 } 778 catch (IOException e) 779 { 780 _logger.error("Cocoon got an Exception while trying to cleanup the uploaded files.", e); 781 } 782 } 783 784 _fireRequestEnded(req); 785 786 if (Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload"))) 787 { 788 restartCocoon(req); 789 } 790 } 791 792 /** 793 * Restart cocoon 794 * @param req The http servlet request, used to read safeMode and normalMode 795 * request attribute if possible. If null, safe mode will be 796 * forced only if it was already forced. 797 */ 798 public void restartCocoon(HttpServletRequest req) 799 { 800 try 801 { 802 ConfigManager.getInstance().dispose(); 803 _disposeCocoon(); 804 _servletContext.removeAttribute("PluginsComponentManager"); 805 806 // By default force safe mode if it was already forced 807 boolean wasForcedSafeMode = PluginsManager.getInstance().getStatus() == PluginsManager.Status.SAFE_MODE_FORCED; 808 boolean forceSafeMode = wasForcedSafeMode; 809 if (req != null) 810 { 811 // Also, checks if there is some specific request attributes 812 // Force safe mode if is explicitly requested 813 // Or force safe mode it was already forced unless normal mode is explicitly requested 814 forceSafeMode = Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.safeMode")); 815 if (!forceSafeMode) 816 { 817 forceSafeMode = wasForcedSafeMode && !Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.normalMode")); 818 } 819 } 820 821 _servletContext.setAttribute("org.ametys.runtime.forceSafeMode", forceSafeMode); 822 823 _initAmetys(); 824 } 825 catch (Exception e) 826 { 827 setRunMode(RunMode.FATALERROR); 828 829 _logger.error("Error while reloading Ametys. Entering in error mode.", e); 830 _exception = e; 831 } 832 } 833 834 private String _retrieveUri(HttpServletRequest req) 835 { 836 String uri = req.getServletPath(); 837 838 String pathInfo = req.getPathInfo(); 839 if (pathInfo != null) 840 { 841 uri += pathInfo; 842 } 843 844 if (uri.length() > 0 && uri.charAt(0) == '/') 845 { 846 uri = uri.substring(1); 847 } 848 return uri; 849 } 850 851 @SuppressWarnings("unchecked") 852 private void _fireRequestStarted(HttpServletRequest req) 853 { 854 Collection< ? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS); 855 856 if (listeners == null) 857 { 858 return; 859 } 860 861 for (RequestListener listener : listeners) 862 { 863 listener.requestStarted(req); 864 } 865 } 866 867 @SuppressWarnings("unchecked") 868 private void _fireRequestEnded(HttpServletRequest req) 869 { 870 Collection<? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS); 871 872 if (listeners == null) 873 { 874 return; 875 } 876 877 for (RequestListener listener : listeners) 878 { 879 listener.requestEnded(req); 880 } 881 } 882 883 /** 884 * Set the maintenance status 885 * @param maintenanceStatus The new maintenance status. Cannot be null. 886 * @param forcedMainteanceInformations The informations if the mode is FORCED. Can be null. 887 */ 888 public static void setMaintenanceStatus(MaintenanceStatus maintenanceStatus, ForcedMainteanceInformations forcedMainteanceInformations) 889 { 890 if (_maintenanceStatus != maintenanceStatus) 891 { 892 LoggerFactory.getLogger(Runtime.class).debug("Switching maintenance status from " + _maintenanceStatus + " to " + maintenanceStatus); 893 } 894 895 ForcedMainteanceInformations fixedForcedMainteanceInformations = null; 896 if (forcedMainteanceInformations != null) 897 { 898 fixedForcedMainteanceInformations = new ForcedMainteanceInformations(StringUtils.defaultString(forcedMainteanceInformations.comment).replaceAll("\r?\n", "<br/>"), forcedMainteanceInformations.initiator, forcedMainteanceInformations.since); 899 } 900 901 try 902 { 903 File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME); 904 if (maintenanceStatus != MaintenanceStatus.FORCED) 905 { 906 if (maintenanceFile.exists()) 907 { 908 FileUtils.delete(maintenanceFile); 909 } 910 } 911 else 912 { 913 // create the result where to write 914 maintenanceFile.getParentFile().mkdirs(); 915 916 try (OutputStream os = new FileOutputStream(maintenanceFile)) 917 { 918 // create a transformer for saving sax into a file 919 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 920 921 StreamResult result = new StreamResult(os); 922 th.setResult(result); 923 924 // create the format of result 925 Properties format = new Properties(); 926 format.put(OutputKeys.METHOD, "xml"); 927 format.put(OutputKeys.INDENT, "yes"); 928 format.put(OutputKeys.ENCODING, "UTF-8"); 929 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 930 th.getTransformer().setOutputProperties(format); 931 932 th.startDocument(); 933 XMLUtils.startElement(th, "maintenance"); 934 if (fixedForcedMainteanceInformations != null) 935 { 936 XMLUtils.createElement(th, "comment", StringUtils.defaultString(fixedForcedMainteanceInformations.comment)); 937 XMLUtils.createElement(th, "initiator", StringUtils.defaultString(UserIdentity.userIdentityToString(fixedForcedMainteanceInformations.initiator))); 938 if (fixedForcedMainteanceInformations.since != null) 939 { 940 XMLUtils.createElement(th, "since", DateUtils.zonedDateTimeToString(fixedForcedMainteanceInformations.since)); 941 } 942 } 943 XMLUtils.endElement(th, "maintenance"); 944 th.endDocument(); 945 } 946 } 947 } 948 catch (Exception e) 949 { 950 throw new RuntimeException("Cannot change maintenance status", e); 951 } 952 953 _maintenanceStatus = maintenanceStatus; 954 _forcedMainteanceInformations = fixedForcedMainteanceInformations; 955 956 try 957 { 958 PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager"); 959 ObservationManager observationManager = (ObservationManager) pluginCM.lookup(ObservationManager.ROLE); 960 CurrentUserProvider currentUserProvider = (CurrentUserProvider) pluginCM.lookup(CurrentUserProvider.ROLE); 961 if (observationManager != null) 962 { 963 observationManager.notify(new Event(ObservationConstants.EVENT_RUNTIME_MAINTENANCE, currentUserProvider.getUser(), Map.of())); 964 } 965 } 966 catch (Exception e) 967 { 968 // Safe mode 969 } 970 } 971 972 /** 973 * Get the current maintenance status 974 * @return The maintenance status 975 */ 976 public static MaintenanceStatus getMaintenanceStatus() 977 { 978 if (_maintenanceStatus == null) 979 { 980 File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME); 981 982 if (!maintenanceFile.exists()) 983 { 984 _maintenanceStatus = MaintenanceStatus.NONE; 985 } 986 else 987 { 988 _maintenanceStatus = MaintenanceStatus.FORCED; 989 try 990 { 991 Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(maintenanceFile); 992 _forcedMainteanceInformations = new ForcedMainteanceInformations(configuration.getChild("comment").getValue(""), 993 UserIdentity.stringToUserIdentity(configuration.getChild("initiator").getValue(null)), 994 DateUtils.parseZonedDateTime(configuration.getChild("since").getValue(null))); 995 } 996 catch (ConfigurationException | SAXException | IOException e) 997 { 998 _logger.error("Cannot read the maintenance status file", e); 999 _forcedMainteanceInformations = null; 1000 } 1001 } 1002 } 1003 return _maintenanceStatus; 1004 } 1005 1006 /** 1007 * When maintenance status is forced, this comment may explains why 1008 * @return The comment. Can be null even in forced mode. 1009 */ 1010 public static ForcedMainteanceInformations getMaintenanceStatusForcedInformations() 1011 { 1012 return _forcedMainteanceInformations; 1013 } 1014 1015 /** 1016 * Set the run mode 1017 * @param mode the running mode 1018 */ 1019 public static void setRunMode(RunMode mode) 1020 { 1021 if (_runMode != mode) 1022 { 1023 LoggerFactory.getLogger(RuntimeServlet.class).debug("Switching run mode from " + _runMode + " to " + mode); 1024 } 1025 1026 _runMode = mode; 1027 } 1028 1029 /** 1030 * Get the run mode 1031 * @return the current run mode 1032 */ 1033 public static RunMode getRunMode() 1034 { 1035 return _runMode; 1036 } 1037 1038 private void _renderSpecialPage(HttpServletRequest req, HttpServletResponse res, Throwable throwable, String message, boolean error) throws ServletException 1039 { 1040 ServletConfig config = getServletConfig(); 1041 1042 if (config == null) 1043 { 1044 throw new ServletException("Cannot access to ServletConfig"); 1045 } 1046 1047 try 1048 { 1049 ServletOutputStream os = res.getOutputStream(); 1050 String path = req.getRequestURI().substring(req.getContextPath().length()); 1051 1052 // Favicon associated with the error page. 1053 if (path.equals("/favicon.ico")) 1054 { 1055 try (InputStream is = getClass().getResourceAsStream("favicon.ico")) 1056 { 1057 res.setStatus(200); 1058 res.setContentType(config.getServletContext().getMimeType("favicon.ico")); 1059 1060 IOUtils.copy(is, os); 1061 1062 return; 1063 } 1064 } 1065 1066 res.setStatus(error ? 500 : 200); 1067 res.setContentType("text/html; charset=UTF-8"); 1068 1069 SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance(); 1070 TransformerHandler th; 1071 1072 try (InputStream is = getClass().getResourceAsStream("fatal.xsl")) 1073 { 1074 StreamSource errorSource = new StreamSource(is); 1075 th = saxFactory.newTransformerHandler(errorSource); 1076 } 1077 1078 Properties format = new Properties(); 1079 format.put(OutputKeys.METHOD, "xml"); 1080 format.put(OutputKeys.ENCODING, "UTF-8"); 1081 format.put(OutputKeys.DOCTYPE_SYSTEM, "about:legacy-compat"); 1082 1083 th.getTransformer().setOutputProperties(format); 1084 1085 th.getTransformer().setParameter("code", error ? 500 : 200); 1086 th.getTransformer().setParameter("realPath", config.getServletContext().getRealPath("/")); 1087 th.getTransformer().setParameter("contextPath", req.getContextPath()); 1088 1089 StreamResult result = new StreamResult(os); 1090 th.setResult(result); 1091 1092 th.startDocument(); 1093 1094 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "exception-report"); 1095 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "message"); 1096 XMLUtils.data(th, message); 1097 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "message"); 1098 1099 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace"); 1100 if (throwable != null) 1101 { 1102 XMLUtils.data(th, ExceptionUtils.getStackTrace(throwable)); 1103 } 1104 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace"); 1105 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "ex:exception-report"); 1106 1107 th.endDocument(); 1108 } 1109 catch (Exception e) 1110 { 1111 // Nothing we can do anymore ... 1112 throw new ServletException(e); 1113 } 1114 } 1115 1116 /* 1117 * private void _runMaintenanceMode(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { ServletConfig config = getServletConfig(); if (config == null) { throw new ServletException("Cannot access to ServletConfig"); } ServletOutputStream os = res.getOutputStream(); if (req.getRequestURI().startsWith("/WEB-INF/error/")) { // Service des fichiers statiques de la page d'erreur File f = new File(config.getServletContext().getRealPath(req.getRequestURI())); if (f.exists()) { res.setStatus(200); InputStream is = new FileInputStream(f); SourceUtil.copy(is, os); is.close(); } else { res.setStatus(404); } return; } res.setStatus(500); SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance(); InputStream is; File errorXSL = new File(config.getServletContext().getRealPath("/WEB-INF/error/error.xsl")); if (errorXSL.exists()) { is = new FileInputStream(errorXSL); } else { is = getClass().getResourceAsStream("/org/ametys/runtime/kernel/pages/error/error.xsl"); } try { StreamSource errorSource = new StreamSource(is); Templates templates = saxFactory.newTemplates(errorSource); TransformerHandler th = saxFactory.newTransformerHandler(templates); is.close(); StreamResult result = new StreamResult(os); th.setResult(result); th.startDocument(); th.startElement("", "error", "error", new AttributesImpl()); saxMaintenanceMessage(th); th.endElement("", "error", "error"); th.endDocument(); } catch (Exception ex) { throw new ServletException("Unable to send maintenance page", ex); } } 1118 */ 1119 1120 /** 1121 * In maintenance mode, send error information as SAX events.<br> 1122 * 1123 * @param ch the contentHandler receiving the message 1124 * @throws SAXException if an error occured while send SAX events 1125 */ 1126 /* 1127 * protected void saxMaintenanceMessage(ContentHandler ch) throws SAXException { String maintenanceMessage = "The application is under maintenance. Please retry later."; ch.characters(maintenanceMessage.toCharArray(), 0, maintenanceMessage.length()); } private boolean _accept(HttpServletRequest req) { // FIX ME à ne pas mettre en dur return req.getRequestURI().startsWith("_admin"); } 1128 */ 1129}