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