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}