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.IOException;
021import java.io.InputStream;
022import java.net.URL;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Enumeration;
026import java.util.Properties;
027import java.util.regex.Pattern;
028
029import javax.servlet.ServletConfig;
030import javax.servlet.ServletContext;
031import javax.servlet.ServletException;
032import javax.servlet.ServletOutputStream;
033import javax.servlet.http.HttpServlet;
034import javax.servlet.http.HttpServletRequest;
035import javax.servlet.http.HttpServletResponse;
036import javax.servlet.http.HttpSession;
037import javax.xml.XMLConstants;
038import javax.xml.parsers.SAXParserFactory;
039import javax.xml.transform.OutputKeys;
040import javax.xml.transform.TransformerFactory;
041import javax.xml.transform.sax.SAXTransformerFactory;
042import javax.xml.transform.sax.TransformerHandler;
043import javax.xml.transform.stream.StreamResult;
044import javax.xml.transform.stream.StreamSource;
045import javax.xml.validation.Schema;
046import javax.xml.validation.SchemaFactory;
047
048import org.apache.avalon.excalibur.logger.LoggerManager;
049import org.apache.avalon.framework.configuration.Configuration;
050import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
051import org.apache.avalon.framework.container.ContainerUtil;
052import org.apache.avalon.framework.context.DefaultContext;
053import org.apache.cocoon.Cocoon;
054import org.apache.cocoon.ConnectionResetException;
055import org.apache.cocoon.Constants;
056import org.apache.cocoon.ResourceNotFoundException;
057import org.apache.cocoon.environment.http.HttpContext;
058import org.apache.cocoon.environment.http.HttpEnvironment;
059import org.apache.cocoon.servlet.multipart.MultipartHttpServletRequest;
060import org.apache.cocoon.servlet.multipart.RequestFactory;
061import org.apache.cocoon.util.ClassUtils;
062import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
063import org.apache.cocoon.util.log.SLF4JLoggerManager;
064import org.apache.cocoon.xml.XMLUtils;
065import org.apache.commons.collections.EnumerationUtils;
066import org.apache.commons.io.FileUtils;
067import org.apache.commons.io.IOUtils;
068import org.apache.commons.lang.exception.ExceptionUtils;
069import org.apache.commons.lang.time.StopWatch;
070import org.apache.commons.lang3.StringUtils;
071import org.apache.log4j.Appender;
072import org.apache.log4j.LogManager;
073import org.apache.log4j.xml.DOMConfigurator;
074import org.slf4j.Logger;
075import org.slf4j.LoggerFactory;
076import org.slf4j.MDC;
077import org.xml.sax.XMLReader;
078
079import org.ametys.core.user.UserIdentity;
080import org.ametys.runtime.config.Config;
081import org.ametys.runtime.config.ConfigManager;
082import org.ametys.runtime.data.AmetysHomeLock;
083import org.ametys.runtime.data.AmetysHomeLockException;
084import org.ametys.runtime.log.MemoryAppender;
085import org.ametys.runtime.plugin.Init;
086import org.ametys.runtime.plugin.InitExtensionPoint;
087import org.ametys.runtime.plugin.PluginsManager;
088import org.ametys.runtime.plugin.PluginsManager.Status;
089import org.ametys.runtime.plugin.component.PluginsComponentManager;
090import org.ametys.runtime.request.RequestListener;
091import org.ametys.runtime.request.RequestListenerManager;
092import org.ametys.runtime.util.AmetysHomeHelper;
093
094/**
095 * Main entry point for applications.<br>
096 * Overrides the CocoonServlet to add some initialization.<br>
097 */
098public class RuntimeServlet extends HttpServlet
099{
100    /** Constant for storing the {@link ServletConfig} in the Avalon context  */
101    public static final String CONTEXT_SERVLET_CONFIG = "servlet-config";
102    
103    /** Constant for storing the servlet context URL in the Avalon context  */
104    public static final String CONTEXT_CONTEXT_ROOT = "context-root";
105    
106    /** The cocoon.xconf URL */
107    public static final String COCOON_CONF_URL = "/org/ametys/runtime/cocoon/cocoon.xconf";
108    
109    /** Default max upload size (10 Mb) */
110    public static final int DEFAULT_MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
111
112    /** The config file name */
113    public static final String CONFIG_FILE_NAME = "config.xml";
114    
115    /** Name of the servlet initialization parameter for the ametys home property */
116    public static final String AMETYS_HOME_PROPERTY = "ametys.home.property";
117    
118    /** The default ametys home path (relative to the servlet context)  */
119    public static final String AMETYS_HOME_DEFAULT = "/WEB-INF/data";
120
121    /** The run modes */
122    public enum RunMode
123    {
124        /** Normal execution mode */
125        NORMAL,
126        /** Maintenance mode required by administrator */
127        // MAINTENANCE,
128        /** Application is currently stopping */
129        STOPPING
130    }
131    
132    private static RunMode _mode = RunMode.NORMAL;
133    
134    private ServletContext _servletContext;
135    private String _servletContextPath;
136    private URL _servletContextURL;
137    
138    private DefaultContext _avalonContext;
139    private HttpContext _context;
140    private Cocoon _cocoon;
141    private RequestFactory _requestFactory;
142    private File _workDir;
143    
144    private int _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE;
145    private File _uploadDir;
146    private File _cacheDir;
147    
148    private File _ametysHome;
149    private AmetysHomeLock _ametysHomeLock;
150    
151    private Logger _logger;
152    private LoggerManager _loggerManager;
153    
154    private Exception _exception;
155    
156    private Collection<Pattern> _allowedURLPattern = Arrays.asList(Pattern.compile("_admin($|/.*)"), Pattern.compile("plugins/[^/]+/resources/.*"));
157    
158    @Override
159    public void init() throws ServletException 
160    {
161        try
162        {
163            // Set this property in order to avoid a System.err.println (CatalogManager.java)
164            if (System.getProperty("xml.catalog.ignoreMissing") == null)
165            {
166                System.setProperty("xml.catalog.ignoreMissing", "true");
167            }
168            
169            _servletContext = getServletContext();
170            _servletContextPath = _servletContext.getRealPath("/");
171            
172            _avalonContext = new DefaultContext();
173            _context = new HttpContext(_servletContext);
174            _avalonContext.put(Constants.CONTEXT_ENVIRONMENT_CONTEXT, _context);
175            _avalonContext.put(Constants.CONTEXT_DEFAULT_ENCODING, "UTF-8");
176            _avalonContext.put(CONTEXT_SERVLET_CONFIG, getServletConfig());
177            
178            _servletContextURL = new File(_servletContextPath).toURI().toURL();
179            _avalonContext.put(CONTEXT_CONTEXT_ROOT, _servletContextURL);
180        
181            URL configFile = getClass().getResource(COCOON_CONF_URL);
182            _avalonContext.put(Constants.CONTEXT_CONFIG_URL, configFile);
183            
184            _workDir = new File((File) _servletContext.getAttribute("javax.servlet.context.tempdir"), "cocoon-files");
185            _workDir.mkdirs();
186            _avalonContext.put(Constants.CONTEXT_WORK_DIR, _workDir);
187            
188            _cacheDir = new File(_workDir, "cache-dir");
189            _cacheDir.mkdirs();
190            _avalonContext.put(Constants.CONTEXT_CACHE_DIR, _cacheDir);
191    
192            // Create temp dir if it does not exist
193            File tmpDir = new File(System.getProperty("java.io.tmpdir"));
194            if (!tmpDir.exists())
195            {
196                FileUtils.forceMkdir(tmpDir);
197            }
198            
199            // Init the Ametys home directory and lock before setting the logger.
200            // The log folder is located in the Ametys home directory.
201            _initAmetysHome();
202            
203            // Configuration file
204            File config = FileUtils.getFile(_ametysHome, AmetysHomeHelper.AMETYS_HOME_CONFIG_DIR, CONFIG_FILE_NAME);
205            final String fileName = config.getCanonicalPath();
206            Config.setFilename(fileName);
207            
208            // Init logger
209            _initLogger();
210            
211            _initAmetys();
212        }
213        catch (Throwable t)
214        {
215            if (_logger != null)
216            {
217                _logger.error("Error while loading Ametys. Entering in error mode.", t);
218            }
219            else
220            {
221                System.out.println("Error while loading Ametys. Entering in error mode.");
222                t.printStackTrace();
223            }
224
225            if (t instanceof Exception)
226            {
227                _exception = (Exception) t;
228            }
229            else
230            {
231                _exception = new Exception(t);
232            }
233            
234            _disposeCocoon();
235        }
236    }
237    
238    private void _initAmetysHome() throws AmetysHomeLockException
239    {
240        String ametysHomePath = null;
241        
242        boolean hasAmetysHomeProp = EnumerationUtils.toList(getInitParameterNames()).contains(AMETYS_HOME_PROPERTY);
243        if (!hasAmetysHomeProp)
244        {
245            System.out.println(String.format("[INFO] The '%s' servlet initialization parameter was not found. The Ametys home directory default value will be used '%s'",
246                    AMETYS_HOME_PROPERTY, AMETYS_HOME_DEFAULT));
247        }
248        else
249        {
250            String ametysHomeEnv = getInitParameter(AMETYS_HOME_PROPERTY);
251            if (StringUtils.isEmpty(ametysHomeEnv))
252            {
253                System.out.println(String.format("[WARN] The '%s' servlet initialization parameter appears to be empty. Ametys home directory default value will be used '%s'",
254                        AMETYS_HOME_PROPERTY, AMETYS_HOME_DEFAULT));
255            }
256            else
257            {
258                ametysHomePath = System.getenv(ametysHomeEnv);
259                if (StringUtils.isEmpty(ametysHomePath))
260                {
261                    System.out.println(String.format(
262                            "[WARN] The '%s' environment variable was not found or was empty. Ametys home directory default value will be used '%s'",
263                            ametysHomeEnv, AMETYS_HOME_DEFAULT));
264                }
265            }
266        }
267        
268        if (StringUtils.isEmpty(ametysHomePath))
269        {
270            ametysHomePath = _servletContext.getRealPath(AMETYS_HOME_DEFAULT);
271        }
272        
273        System.out.println("Acquiring lock on " + ametysHomePath);
274        
275        // Acquire the lock on Ametys home
276        _ametysHome = new File(ametysHomePath);
277        _ametysHome.mkdirs();
278        
279        _ametysHomeLock = new AmetysHomeLock(_ametysHome);
280        _ametysHomeLock.acquire();
281    }
282
283    private void _initAmetys() throws Exception
284    {
285        // WEB-INF/param/runtime.xml loading
286        _loadRuntimeConfig();
287
288        _createCocoon();
289        
290        // Upload initialization
291        _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE;
292        _uploadDir = new File(RuntimeConfig.getInstance().getAmetysHome(), "uploads");
293        
294        if (ConfigManager.getInstance().isComplete())
295        {
296            Long maxUploadSizeParam = Config.getInstance().getValue("runtime.upload.max-size");
297            if (maxUploadSizeParam != null)
298            {
299                // if the feature core/runtime.upload is deactivated, use the default value (10 Mb)
300                _maxUploadSize = maxUploadSizeParam.intValue();
301            }
302        }
303
304        _uploadDir.mkdirs();
305        _avalonContext.put(Constants.CONTEXT_UPLOAD_DIR, _uploadDir);
306        
307        _requestFactory = new RequestFactory(true, _uploadDir, false, true, _maxUploadSize, "UTF-8");
308
309        // Init classes
310        _initPlugins();
311    }
312    
313    private void _initLogger() 
314    {
315        // Configure Log4j
316        String logj4fFile = _servletContext.getRealPath("/WEB-INF/log4j.xml");
317        
318        // Hack to have context-relative log files, because of lack in configuration capabilities in log4j.
319        // If there are more than one Ametys in the same JVM, the property will be successively set for each instance, 
320        // so we heavily rely on DOMConfigurator beeing synchronous.
321        System.setProperty("ametys.home.dir", _ametysHome.getAbsolutePath());
322        DOMConfigurator.configure(logj4fFile);
323        System.clearProperty("ametys.home.dir");
324        
325        Appender appender = new MemoryAppender(); 
326        appender.setName(org.ametys.plugins.core.ui.log.LogManager.MEMORY_APPENDER_NAME);
327        LogManager.getRootLogger().addAppender(appender); 
328        Enumeration<org.apache.log4j.Logger> categories = LogManager.getCurrentLoggers(); 
329        while (categories.hasMoreElements()) 
330        { 
331            org.apache.log4j.Logger logger = categories.nextElement(); 
332            logger.addAppender(appender); 
333        } 
334        
335        _loggerManager = new SLF4JLoggerManager();
336        _logger = LoggerFactory.getLogger(getClass());
337    }
338    
339    private void _createCocoon() throws Exception
340    {
341        _exception = null;
342        
343        _avalonContext.put(Constants.CONTEXT_CLASS_LOADER, getClass().getClassLoader());
344        _avalonContext.put(Constants.CONTEXT_CLASSPATH, "");
345        
346        URL configFile = (URL) _avalonContext.get(Constants.CONTEXT_CONFIG_URL);
347        
348        _logger.info("Reloading from: {}", configFile.toExternalForm());
349        
350        Cocoon c = (Cocoon) ClassUtils.newInstance("org.apache.cocoon.Cocoon");
351        ContainerUtil.enableLogging(c, new SLF4JLoggerAdapter(_logger));
352        c.setLoggerManager(_loggerManager);
353        ContainerUtil.contextualize(c, _avalonContext);
354        ContainerUtil.initialize(c);
355
356        _cocoon = c;
357    }
358    
359    private void _initPlugins() throws Exception
360    {
361        PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager");
362        
363        // If we're in safe mode 
364//        if (!PluginsManager.getInstance().isSafeMode())
365//        {
366        // FIXME the conditional was commented temporary for creating admin users SQL table even in safe mode
367        // Plugins Init class execution
368        InitExtensionPoint initExtensionPoint = (InitExtensionPoint) pluginCM.lookup(InitExtensionPoint.ROLE);
369        for (String id : initExtensionPoint.getExtensionsIds())
370        {
371            Init init = initExtensionPoint.getExtension(id);
372            init.init();
373        }
374        
375        // Application Init class execution if available
376        if (pluginCM.hasComponent(Init.ROLE))
377        {
378            Init init = (Init) pluginCM.lookup(Init.ROLE);
379            init.init();
380        }
381//        }
382    }
383    
384    private void _loadRuntimeConfig() throws ServletException
385    {
386        Configuration runtimeConf = null;
387        try
388        {
389            // XML Schema validation
390            SAXParserFactory factory = SAXParserFactory.newInstance();
391            factory.setNamespaceAware(true);
392            
393            URL schemaURL = getClass().getResource("runtime-4.1.xsd");
394            Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(schemaURL);
395            factory.setSchema(schema);
396            
397            XMLReader reader = factory.newSAXParser().getXMLReader();
398            DefaultConfigurationBuilder runtimeConfBuilder = new DefaultConfigurationBuilder(reader);
399            
400            File runtimeConfigFile = new File(_servletContextPath, "WEB-INF/param/runtime.xml");
401            try (InputStream runtime = new FileInputStream(runtimeConfigFile))
402            {
403                runtimeConf = runtimeConfBuilder.build(runtime, runtimeConfigFile.getAbsolutePath());
404            }
405        }
406        catch (Exception e)
407        {
408            _logger.error("Unable to load runtime file at 'WEB-INF/param/runtime.xml'. PluginsManager will enter in safe mode.", e);
409        }
410        
411        Configuration externalConf = null;
412        try
413        {
414            DefaultConfigurationBuilder externalConfBuilder = new DefaultConfigurationBuilder();
415
416            File externalConfigFile = new File(_servletContextPath, "WEB-INF/param/external-locations.xml");
417            if (externalConfigFile.exists())
418            {
419                try (InputStream external = new FileInputStream(externalConfigFile))
420                {
421                    externalConf = externalConfBuilder.build(external, externalConfigFile.getAbsolutePath());
422                }
423            }
424        }
425        catch (Exception e)
426        {
427            _logger.error("Unable to load external locations values at WEB-INF/param/external-locations.xml", e);
428            throw new ServletException("Unable to load external locations values at WEB-INF/param/external-locations.xml", e);
429        }
430        
431        RuntimeConfig.configure(runtimeConf, externalConf, _ametysHome, _servletContextPath);
432    }
433
434    @Override
435    public void destroy() 
436    {
437        _mode = RunMode.STOPPING;
438        
439        if (_cocoon != null) 
440        {
441            _logger.debug("Servlet destroyed - disposing Cocoon");
442            _disposeCocoon();
443        }
444        
445        if (_ametysHomeLock != null)
446        {
447            _ametysHomeLock.release();
448            _ametysHomeLock = null;
449        }
450        
451        _avalonContext = null;
452        _logger = null;
453        _loggerManager = null;
454    }
455
456    
457    private final void _disposeCocoon() 
458    {
459        if (_cocoon != null) 
460        {
461            ContainerUtil.dispose(_cocoon);
462            _cocoon = null;
463        }
464    }
465
466    @Override
467    public final void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
468    {
469        req.setCharacterEncoding("UTF-8");
470
471        // add the cocoon header timestamp
472        res.addHeader("X-Cocoon-Version", Constants.VERSION);
473        
474        // Error mode
475        if (_exception != null)
476        {
477            _renderError(req, res, _exception, "An error occured during Ametys initialization.");
478            return;
479        }
480        
481        String uri = _retrieveUri(req);
482        
483        if (PluginsManager.getInstance().isSafeMode())
484        {
485            // safe mode
486            String finalUri = uri;
487            boolean allowed = _allowedURLPattern.stream().anyMatch(p -> p.matcher(finalUri).matches());
488            if (!allowed) 
489            {
490                res.addHeader("X-Ametys-SafeMode", "true");
491
492                Status status = PluginsManager.getInstance().getStatus();
493                
494                if (status == Status.NO_CONFIG)
495                {
496                    res.sendRedirect(req.getContextPath() + "/_admin/public/first-start.html");
497                    return;
498                }
499                else if (status == Status.CONFIG_INCOMPLETE)
500                {
501                    res.sendRedirect(req.getContextPath() + "/_admin/public/load-config.html");
502                    return;
503                }
504                else
505                {
506                    res.sendRedirect(req.getContextPath() + "/_admin/public/safe-mode.html");
507                    return;
508                }
509            }
510        }
511        
512        HttpSession session = req.getSession(false); 
513        if (session != null) 
514        { 
515            UserIdentity authenticatedUser = (UserIdentity) session.getAttribute("Runtime:UserIdentity"); 
516            
517            if (authenticatedUser != null)
518            {
519                MDC.put("user", UserIdentity.userIdentityToString(authenticatedUser));
520            }
521        }
522        
523        MDC.put("requestURI", req.getRequestURI());
524
525        // if (getRunMode() == RunMode.MAINTENANCE && !_accept(req))
526        // {
527        // _runMaintenanceMode(req, res);
528        // }
529        // else
530        // {
531        
532        StopWatch stopWatch = new StopWatch();
533        HttpServletRequest request = null;
534        try 
535        {
536            // used for timing the processing
537            stopWatch.start();
538
539            _fireRequestStarted(req);
540            
541            // get the request (wrapped if contains multipart-form data)
542            request = _requestFactory.getServletRequest(req);
543            
544            // Process the request
545            HttpEnvironment env = new HttpEnvironment(uri, _servletContextURL.toExternalForm(), request, res, _servletContext, _context, "UTF-8", "UTF-8");
546            env.enableLogging(new SLF4JLoggerAdapter(_logger));
547            
548            if (!_cocoon.process(env)) 
549            {
550                // We reach this when there is nothing in the processing change that matches
551                // the request. For example, no matcher matches.
552                _logger.error("The Cocoon engine failed to process the request.");
553                _renderError(request, res, null, "Cocoon engine failed to process the request");
554            }
555        } 
556        catch (ResourceNotFoundException | ConnectionResetException | IOException e) 
557        {
558            _logger.warn(e.toString());
559            _renderError(request, res, e, e.getMessage());
560        } 
561        catch (Throwable e) 
562        {
563            _logger.error("Internal Cocoon Problem", e);
564            _renderError(request, res, e, "Internal Cocoon Problem");
565        }
566        finally 
567        {
568            stopWatch.stop();
569            _logger.info("'{}' processed in {} ms.", uri, stopWatch.getTime());
570            
571            try
572            {
573                if (request instanceof MultipartHttpServletRequest) 
574                {
575                    _logger.debug("Deleting uploaded file(s).");
576                    ((MultipartHttpServletRequest) request).cleanup();
577                }
578            } 
579            catch (IOException e)
580            {
581                _logger.error("Cocoon got an Exception while trying to cleanup the uploaded files.", e);
582            }
583        }
584
585        _fireRequestEnded(req);
586
587        if (Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload")))
588        {
589            restartCocoon(req);
590        }
591        // }
592    }
593    
594    /**
595     * Restart cocoon
596     * @param req The http servlet request, used to read safeMode and normalMode
597     *            request attribute if possible. If null, safe mode will be
598     *            forced only if it was alread forced.
599     */
600    public void restartCocoon(HttpServletRequest req)
601    {
602        try
603        {
604            ConfigManager.getInstance().dispose();
605            _disposeCocoon(); 
606            _servletContext.removeAttribute("PluginsComponentManager");
607            
608            // By default force safe mode if it was already forced
609            boolean wasForcedSafeMode = PluginsManager.Status.SAFE_MODE_FORCED.equals(PluginsManager.getInstance().getStatus());
610            boolean forceSafeMode = wasForcedSafeMode;
611            if (req != null)
612            {
613                // Also, checks if there is some specific request attributes
614                // Force safe mode if is explicitly requested
615                // Or force safe mode it was already forced unless normal mode is explicitly requested 
616                forceSafeMode = Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.safeMode"));
617                if (!forceSafeMode)
618                {
619                    // 
620                    forceSafeMode = wasForcedSafeMode && !Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.normalMode"));
621                }
622            }
623            
624            _servletContext.setAttribute("org.ametys.runtime.forceSafeMode", forceSafeMode);
625            
626            _initAmetys();
627        }
628        catch (Exception e)
629        {
630            _logger.error("Error while reloading Ametys. Entering in error mode.", e);
631            _exception = e;
632        }
633    }
634    
635    private String _retrieveUri(HttpServletRequest req)
636    {
637        String uri = req.getServletPath();
638        
639        String pathInfo = req.getPathInfo();
640        if (pathInfo != null) 
641        {
642            uri += pathInfo;
643        }
644        
645        if (uri.length() > 0 && uri.charAt(0) == '/') 
646        {
647            uri = uri.substring(1);
648        }
649        return uri;
650    }
651    
652    @SuppressWarnings("unchecked")
653    private void _fireRequestStarted(HttpServletRequest req)
654    {
655        Collection< ? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS);
656
657        if (listeners == null)
658        {
659            return;
660        }
661
662        for (RequestListener listener : listeners)
663        {
664            listener.requestStarted(req);
665        }
666    }
667
668    @SuppressWarnings("unchecked")
669    private void _fireRequestEnded(HttpServletRequest req)
670    {
671        Collection<? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS);
672
673        if (listeners == null)
674        {
675            return;
676        }
677
678        for (RequestListener listener : listeners)
679        {
680            listener.requestEnded(req);
681        }
682    }
683
684    /**
685     * Set the run mode
686     * @param mode the running mode
687     */
688    public static void setRunMode(RunMode mode)
689    {
690        _mode = mode;
691    }
692
693    /**
694     * Get the run mode
695     * @return the current run mode
696     */
697    public static RunMode getRunMode()
698    {
699        return _mode;
700    }
701
702    private void _renderError(HttpServletRequest req, HttpServletResponse res, Throwable throwable, String message) throws ServletException
703    {
704        ServletConfig config = getServletConfig();
705
706        if (config == null)
707        {
708            throw new ServletException("Cannot access to ServletConfig");
709        }
710
711        try
712        {
713            ServletOutputStream os = res.getOutputStream();
714            String path = req.getRequestURI().substring(req.getContextPath().length());
715    
716            // Favicon associated with the error page.
717            if (path.equals("/favicon.ico"))
718            {
719                try (InputStream is = getClass().getResourceAsStream("favicon.ico"))
720                {
721                    res.setStatus(200);
722                    res.setContentType(config.getServletContext().getMimeType("favicon.ico"));
723                    
724                    IOUtils.copy(is, os);
725                    
726                    return;
727                }
728            }
729    
730            res.setStatus(500);
731            res.setContentType("text/html; charset=UTF-8");
732    
733            SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
734            TransformerHandler th;
735            
736            try (InputStream is = getClass().getResourceAsStream("fatal.xsl"))
737            {
738                StreamSource errorSource = new StreamSource(is);
739                th = saxFactory.newTransformerHandler(errorSource);
740            }
741            
742            Properties format = new Properties();
743            format.put(OutputKeys.METHOD, "xml");
744            format.put(OutputKeys.ENCODING, "UTF-8");
745            format.put(OutputKeys.DOCTYPE_SYSTEM, "about:legacy-compat");
746    
747            th.getTransformer().setOutputProperties(format);
748    
749            th.getTransformer().setParameter("code", 500);
750            th.getTransformer().setParameter("realPath", config.getServletContext().getRealPath("/"));
751            th.getTransformer().setParameter("contextPath", req.getContextPath());
752            
753            StreamResult result = new StreamResult(os);
754            th.setResult(result);
755    
756            th.startDocument();
757    
758            XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "exception-report");
759            XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "message");
760            XMLUtils.data(th, message);
761            XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "message");
762            
763            XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace");
764            XMLUtils.data(th, ExceptionUtils.getStackTrace(throwable));
765            XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace");
766            XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "ex:exception-report");
767    
768            th.endDocument();
769        }
770        catch (Exception e)
771        {
772            // Nothing we can do anymore ...
773            throw new ServletException(e);
774        }
775    }
776
777    /*
778     * 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); } }
779     */
780
781    /**
782     * In maintenance mode, send error information as SAX events.<br>
783     * 
784     * @param ch the contentHandler receiving the message
785     * @throws SAXException if an error occured while send SAX events
786     */
787    /*
788     * 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"); }
789     */
790}