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