001    /*
002     * 
003     * $Revision: 15202 $ $Date: 2009-05-15 17:00:44 +0200 (Fri, 15 May 2009) $
004     *
005     * This file is part of ***  M y C o R e  ***
006     * See http://www.mycore.de/ for details.
007     *
008     * This program is free software; you can use it, redistribute it
009     * and / or modify it under the terms of the GNU General Public License
010     * (GPL) as published by the Free Software Foundation; either version 2
011     * of the License or (at your option) any later version.
012     *
013     * This program is distributed in the hope that it will be useful, but
014     * WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program, in a file called gpl.txt or license.txt.
020     * If not, write to the Free Software Foundation Inc.,
021     * 59 Temple Place - Suite 330, Boston, MA  02111-1307 USA
022     */
023    
024    package org.mycore.frontend.servlets;
025    
026    import java.io.IOException;
027    import java.io.UnsupportedEncodingException;
028    import java.net.InetAddress;
029    import java.net.URLEncoder;
030    import java.util.Collection;
031    import java.util.Enumeration;
032    import java.util.HashSet;
033    import java.util.Iterator;
034    import java.util.Map;
035    import java.util.Properties;
036    import java.util.Set;
037    import java.util.StringTokenizer;
038    
039    import javax.servlet.ServletContext;
040    import javax.servlet.ServletException;
041    import javax.servlet.http.HttpServlet;
042    import javax.servlet.http.HttpServletRequest;
043    import javax.servlet.http.HttpServletResponse;
044    import javax.servlet.http.HttpSession;
045    
046    import org.apache.log4j.Logger;
047    import org.jdom.DocType;
048    import org.jdom.Document;
049    import org.jdom.Element;
050    import org.mycore.common.MCRConfiguration;
051    import org.mycore.common.MCRException;
052    import org.mycore.common.MCRSession;
053    import org.mycore.common.MCRSessionMgr;
054    import org.mycore.common.xml.MCRLayoutService;
055    import org.mycore.common.xml.MCRURIResolver;
056    import org.mycore.datamodel.common.MCRActiveLinkException;
057    
058    /**
059     * This is the superclass of all MyCoRe servlets. It provides helper methods for
060     * logging and managing the current session data. Part of the code has been
061     * taken from MilessServlet.java written by Frank Lützenkirchen.
062     * 
063     * @author Detlev Degenhardt
064     * @author Frank Lützenkirchen
065     * @author Thomas Scheffler (yagee)
066     * 
067     * @version $Revision: 15202 $ $Date: 2008-02-06 17:27:24 +0000 (Mi, 06 Feb
068     *          2008) $
069     */
070    public class MCRServlet extends HttpServlet {
071        private static final String INITIAL_SERVLET_NAME_KEY = "currentServletName";
072    
073        private static final long serialVersionUID = 1L;
074    
075        private static Logger LOGGER = Logger.getLogger(MCRServlet.class);
076    
077        private static String BASE_URL;
078    
079        private static String SERVLET_URL;
080    
081        private static final boolean ENABLE_BROWSER_CACHE = MCRConfiguration.instance().getBoolean("MCR.Servlet.BrowserCache.enable", false);
082    
083        private static MCRLayoutService LAYOUT_SERVICE;
084    
085        public static final String BASE_URL_ATTRIBUTE = "org.mycore.base.url";
086    
087        public static MCRLayoutService getLayoutService() {
088            return LAYOUT_SERVICE;
089        }
090    
091        public void init() throws ServletException {
092            super.init();
093            if (LAYOUT_SERVICE == null) {
094                LAYOUT_SERVICE = MCRLayoutService.instance();
095            }
096        }
097    
098        /** returns the base URL of the mycore system */
099        public static String getBaseURL() {
100            MCRSession session = MCRSessionMgr.getCurrentSession();
101            Object value = session.get(BASE_URL_ATTRIBUTE);
102            if (value != null) {
103                LOGGER.debug("Returning BaseURL from user session.");
104                return value.toString();
105            }
106            return BASE_URL;
107        }
108    
109        /** returns the servlet base URL of the mycore system */
110        public static String getServletBaseURL() {
111            MCRSession session = MCRSessionMgr.getCurrentSession();
112            Object value = session.get(BASE_URL_ATTRIBUTE);
113            if (value != null) {
114                return value.toString() + "servlets/";
115            }
116            return SERVLET_URL;
117        }
118    
119        /**
120         * Initialisation of the static values for the base URL and servlet URL of
121         * the mycore system.
122         */
123        private static synchronized void prepareURLs(ServletContext context, HttpServletRequest req) {
124            String contextPath = req.getContextPath() + "/";
125    
126            String requestURL = req.getRequestURL().toString();
127            int pos = requestURL.indexOf(contextPath, 9);
128    
129            BASE_URL = MCRConfiguration.instance().getString("MCR.baseurl", requestURL.substring(0, pos) + contextPath);
130            if (!BASE_URL.endsWith("/"))
131                BASE_URL = BASE_URL + "/";
132            SERVLET_URL = BASE_URL + "servlets/";
133            MCRURIResolver.init(context, getBaseURL());
134        }
135    
136        // The methods doGet() and doPost() simply call the private method
137        // doGetPost(),
138        // i.e. GET- and POST requests are handled by one method only.
139        public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
140            doGetPost(req, res);
141        }
142    
143        protected void doGet(MCRServletJob job) throws Exception {
144            doGetPost(job);
145        }
146    
147        public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
148            doGetPost(req, res);
149        }
150    
151        protected void doPost(MCRServletJob job) throws Exception {
152            doGetPost(job);
153        }
154    
155        public static MCRSession getSession(HttpServletRequest req, String servletName) {
156            HttpSession theSession = req.getSession(true);
157            MCRSession session = null;
158    
159            MCRSession fromHttpSession = (MCRSession) theSession.getAttribute("mycore.session");
160    
161            if (fromHttpSession != null) {
162                // Take session from HttpSession with servlets
163                session = fromHttpSession;
164            } else {
165                // Create a new session
166                session = MCRSessionMgr.getCurrentSession();
167            }
168    
169            // Store current session in HttpSession
170            theSession.setAttribute("mycore.session", session);
171            // store the HttpSession ID in MCRSession
172            session.put("http.session", theSession.getId());
173    
174            String currentThread = getProperty(req, "currentThreadName");
175            // check if this is request passed the same thread before
176            // (RequestDispatcher)
177            if (currentThread == null || !currentThread.equals(Thread.currentThread().getName())) {
178                // Bind current session to this thread:
179                MCRSessionMgr.setCurrentSession(session);
180                req.setAttribute("currentThreadName", Thread.currentThread().getName());
181                req.setAttribute(INITIAL_SERVLET_NAME_KEY, servletName);
182            }
183    
184            // Forward MCRSessionID to XSL Stylesheets
185            req.setAttribute("XSL.MCRSessionID", session.getID());
186    
187            return session;
188        }
189    
190        /**
191         * This private method handles both GET and POST requests and is invoked by
192         * doGet() and doPost().
193         * 
194         * @param req
195         *            the HTTP request instance
196         * @param res
197         *            the HTTP response instance
198         * @exception IOException
199         *                for java I/O errors.
200         * @exception ServletException
201         *                for errors from the servlet engine.
202         */
203        private void doGetPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
204            if (MCRConfiguration.instance() == null) {
205                // removes NullPointerException below, if somehow Servlet is not yet
206                // intialized
207                init();
208            }
209    
210            // Try to set encoding of form values
211            String ReqCharEncoding = req.getCharacterEncoding();
212    
213            if (ReqCharEncoding == null) {
214                // Set default to UTF-8
215                ReqCharEncoding = MCRConfiguration.instance().getString("MCR.Request.CharEncoding", "UTF-8");
216                req.setCharacterEncoding(ReqCharEncoding);
217                LOGGER.debug("Setting ReqCharEncoding to: " + ReqCharEncoding);
218            }
219    
220            if ("true".equals(req.getParameter("reload.properties"))) {
221                MCRConfiguration.instance().reload(true);
222            }
223    
224            if (BASE_URL == null) {
225                prepareURLs(getServletContext(), req);
226            }
227    
228            MCRServletJob job = new MCRServletJob(req, res);
229    
230            MCRSession session = getSession(req, getServletName());
231            try {
232                session.put("MCRServletJob", job);
233    
234                String c = getClass().getName();
235                c = c.substring(c.lastIndexOf(".") + 1);
236    
237                StringBuffer msg = new StringBuffer();
238                msg.append(c);
239                msg.append(" ip=");
240                msg.append(getRemoteAddr(req));
241                msg.append(" mcr=").append(session.getID());
242                msg.append(" user=").append(session.getCurrentUserID());
243                LOGGER.info(msg.toString());
244    
245                String lang = getProperty(req, "lang");
246    
247                if ((lang != null) && (lang.trim().length() != 0)) {
248                    session.setCurrentLanguage(lang.trim());
249                }
250    
251                // Set the IP of the current session
252                if (session.getCurrentIP().length() == 0) {
253                    session.setCurrentIP(getRemoteAddr(req));
254                }
255    
256                // set BASE_URL_ATTRIBUTE to MCRSession
257                if (req.getAttribute(BASE_URL_ATTRIBUTE) != null) {
258                    session.put(BASE_URL_ATTRIBUTE, req.getAttribute(BASE_URL_ATTRIBUTE));
259                }
260    
261                // Store XSL.*.SESSION parameters to MCRSession
262                putParamsToSession(req);
263                //transaction around 1st phase of request
264                Exception thinkException = processThinkPhase(job);
265                //first phase completed, start rendering phase
266                processRenderingPhase(job, thinkException);
267            } catch (Exception ex) {
268                if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
269                    // current Servlet not called via RequestDispatcher
270                    session.rollbackTransaction();
271                }
272                if (ex instanceof ServletException) {
273                    throw (ServletException) ex;
274                } else if (ex instanceof IOException) {
275                    throw (IOException) ex;
276                } else {
277                    handleException(ex);
278                    session.beginTransaction();
279                    generateErrorPage(req, res, 500, ex.getMessage(), ex, false);
280                    session.commitTransaction();
281                }
282            } finally {
283                MCRSessionMgr.getCurrentSession().deleteObject("MCRServletJob");
284                // Release current MCRSession from current Thread,
285                // in case that Thread pooling will be used by servlet engine
286                if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
287                    // current Servlet not called via RequestDispatcher
288                    MCRSessionMgr.releaseCurrentSession();
289                }
290            }
291        }
292    
293        private Exception processThinkPhase(MCRServletJob job) {
294            MCRSession session=MCRSessionMgr.getCurrentSession();
295            try {
296                if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
297                    // current Servlet not called via RequestDispatcher
298                    session.beginTransaction();
299                }
300                think(job);
301                if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
302                    // current Servlet not called via RequestDispatcher
303                    session.commitTransaction();
304                }
305            } catch (Exception ex) {
306                if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
307                    // current Servlet not called via RequestDispatcher
308                    session.rollbackTransaction();
309                }
310                return ex;
311            }
312            return null;
313        }
314    
315        /**
316         * 1st phase of doGetPost.
317         * 
318         * This method has a seperate transaction. Per default id does nothing as a fallback to the old behaviour.
319         * @param job
320         * @throws Exception
321         * @see #render(MCRServletJob, Exception)
322         */
323        protected void think(MCRServletJob job) throws Exception {
324            //not implemented by default
325        }
326    
327        private void processRenderingPhase(MCRServletJob job, Exception thinkException) throws Exception {
328            MCRSession session=MCRSessionMgr.getCurrentSession();
329            if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
330                // current Servlet not called via RequestDispatcher
331                session.beginTransaction();
332            }
333            render(job, thinkException);
334            if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
335                // current Servlet not called via RequestDispatcher
336                session.commitTransaction();
337            }
338        }
339    
340        /**
341         * 2nd phase of doGetPost
342         * 
343         * This method has a seperate transaction and gets the same MCRServletJob from the first phase (think)
344         * and any exception that occurs at the first phase.
345         * 
346         * By default this method calls doGetPost(MCRServletJob) as a fallback to the old behaviour.
347         * 
348         * @param job same instance as of think(MCRServlet job)
349         * @param ex any exception thrown by think(MCRServletJob) or transaction commit
350         * @throws Exception if render could not handle <code>ex</code> to produce a nice user page
351         */
352        protected void render(MCRServletJob job, Exception ex) throws Exception {
353            if (job.getRequest().getMethod().equals("POST"))
354                doPost(job);
355            else
356                doGet(job);
357        }
358    
359        /**
360         * This method should be overwritten by other servlets. As a default
361         * response we indicate the HTTP 1.1 status code 501 (Not Implemented).
362         */
363        protected void doGetPost(MCRServletJob job) throws Exception {
364            job.getResponse().sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
365        }
366    
367        /** Handles an exception by reporting it and its embedded exception */
368        protected void handleException(Exception ex) {
369            try {
370                reportException(ex);
371    
372                if (ex instanceof MCRException) {
373                    ex = ((MCRException) ex).getException();
374    
375                    if (ex != null) {
376                        handleException(ex);
377                    }
378                }
379            } catch (Exception ignored) {
380            }
381        }
382    
383        /** Reports an exception to the log */
384        protected void reportException(Exception ex) throws Exception {
385            String msg = ((ex.getMessage() == null) ? "" : ex.getMessage());
386            String type = ex.getClass().getName();
387            String cname = this.getClass().getName();
388            String servlet = cname.substring(cname.lastIndexOf(".") + 1);
389            String trace = MCRException.getStackTraceAsString(ex);
390    
391            LOGGER.warn("Exception caught in : " + servlet);
392            LOGGER.warn("Exception type      : " + type);
393            LOGGER.warn("Exception message   : " + msg);
394            LOGGER.debug(trace);
395        }
396    
397        protected void generateErrorPage(HttpServletRequest request, HttpServletResponse response, int error, String msg, Exception ex,
398                boolean xmlstyle) throws IOException {
399            LOGGER.error(getClass().getName() + ": Error " + error + " occured. The following message was given: " + msg, ex);
400    
401            String rootname = "mcr_error";
402            String style = getProperty(request, "XSL.Style");
403            if ((style == null) || !(style.equals("xml"))) {
404                style = "default";
405            }
406            Element root = new Element(rootname);
407            root.setAttribute("HttpError", Integer.toString(error)).setText(msg);
408    
409            Document errorDoc = new Document(root, new DocType(rootname));
410    
411            while (ex != null) {
412                Element exception = new Element("exception");
413                exception.setAttribute("type", ex.getClass().getName());
414                Element trace = new Element("trace");
415                Element message = new Element("message");
416                trace.setText(MCRException.getStackTraceAsString(ex));
417                message.setText(ex.getMessage());
418                exception.addContent(message).addContent(trace);
419                root.addContent(exception);
420    
421                if (ex instanceof MCRException) {
422                    ex = ((MCRException) ex).getException();
423                } else {
424                    ex = null;
425                }
426            }
427    
428            request.setAttribute("XSL.Style", style);
429    
430            final String requestAttr = "MCRServlet.generateErrorPage";
431            if ((!response.isCommitted()) && (request.getAttribute(requestAttr) == null)) {
432                response.setStatus(error);
433                request.setAttribute(requestAttr, msg);
434                LAYOUT_SERVICE.doLayout(request, response, errorDoc);
435                return;
436            } else {
437                if (request.getAttribute(requestAttr) != null) {
438                    LOGGER.warn("Could not send error page. Generating error page failed. The original message:\n"
439                            + request.getAttribute(requestAttr));
440                } else {
441                    LOGGER.warn("Could not send error page. Response allready commited. The following message was given:\n" + msg);
442                }
443            }
444        }
445    
446        /**
447         * This method builds a URL that can be used to redirect the client browser
448         * to another page, thereby including http request parameters. The request
449         * parameters will be encoded as http get request.
450         * 
451         * @param baseURL
452         *            the base url of the target webpage
453         * @param parameters
454         *            the http request parameters
455         */
456        protected String buildRedirectURL(String baseURL, Properties parameters) {
457            StringBuilder redirectURL = new StringBuilder(baseURL);
458            boolean first = true;
459            for (Enumeration<?> e = parameters.keys(); e.hasMoreElements();) {
460                if (first) {
461                    redirectURL.append("?");
462                    first = false;
463                } else
464                    redirectURL.append("&");
465    
466                String name = (String) (e.nextElement());
467                String value = null;
468                try {
469                    value = URLEncoder.encode(parameters.getProperty(name), "UTF-8");
470                } catch (UnsupportedEncodingException ex) {
471                    value = parameters.getProperty(name);
472                } catch (NullPointerException npe) {
473                    throw new MCRException("NullPointerException while encoding " + name, npe);
474                }
475                redirectURL.append(name).append("=").append(value);
476            }
477            LOGGER.debug("Sending redirect to " + redirectURL.toString());
478            return redirectURL.toString();
479        }
480    
481        protected void generateActiveLinkErrorpage(HttpServletRequest request, HttpServletResponse response, String msg,
482                MCRActiveLinkException activeLinks) throws IOException {
483            StringBuffer msgBuf = new StringBuffer(msg);
484            msgBuf
485                    .append("\nThere are links active preventing the commit of work, see error message for details. The following links where affected:");
486            Map<String, Collection<String>> links = activeLinks.getActiveLinks();
487            Iterator<Map.Entry<String, Collection<String>>> entryIt = links.entrySet().iterator();
488            while (entryIt.hasNext()) {
489                Map.Entry<String, Collection<String>> entry = entryIt.next();
490                for (String source : entry.getValue()) {
491                    msgBuf.append('\n').append(source).append("==>").append(entry.getKey());
492                }
493            }
494            generateErrorPage(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msgBuf.toString(), activeLinks, false);
495        }
496    
497        /**
498         * allows browser to cache requests.
499         * 
500         * This method is usefull as it allows browsers to cache content that is not
501         * changed.
502         * 
503         * Please overwrite this method in every Servlet that depends on "remote"
504         * data.
505         * 
506         */
507        protected long getLastModified(HttpServletRequest request) {
508            if (ENABLE_BROWSER_CACHE) {
509                // we can cache every (local) request
510                long lastModified = (MCRSessionMgr.getCurrentSession().getLoginTime() > MCRConfiguration.instance().getSystemLastModified()) ? MCRSessionMgr
511                        .getCurrentSession().getLoginTime() : MCRConfiguration.instance().getSystemLastModified();
512                LOGGER.info("LastModified: " + lastModified);
513                return lastModified;
514            }
515            return -1; // time is not known
516        }
517    
518        public static String getProperty(HttpServletRequest request, String name) {
519            String value = (String) request.getAttribute(name);
520    
521            // if Attribute not given try Parameter
522            if ((value == null) || (value.length() == 0)) {
523                value = request.getParameter(name);
524            }
525    
526            return value;
527        }
528    
529        /** The IP addresses of trusted web proxies */
530        protected static Set<String> trustedProxies = new HashSet<String>();
531    
532        /**
533         * Builds a list of trusted proxy IPs from MCR.Request.TrustedProxies. The
534         * IP address of the local host is automatically added to this list.
535         */
536        protected static synchronized void initTrustedProxies() {
537            String sTrustedProxies = MCRConfiguration.instance().getString("MCR.Request.TrustedProxies", "");
538            StringTokenizer st = new StringTokenizer(sTrustedProxies, " ,;");
539            while (st.hasMoreTokens())
540                trustedProxies.add(st.nextToken());
541    
542            // Always trust the local host
543            trustedProxies.add("127.0.0.1");
544    
545            try {
546                String host = new java.net.URL(getBaseURL()).getHost();
547                trustedProxies.add(InetAddress.getByName(host).getHostAddress());
548            } catch (Exception ex) {
549                LOGGER.warn("Could not determine IP of local host", ex);
550            }
551    
552            for (String proxy : trustedProxies)
553                LOGGER.debug("Trusted proxy: " + proxy);
554        }
555    
556        /**
557         * Returns the IP address of the client that made the request. When a
558         * trusted proxy server was used, e. g. a local Apache mod_proxy in front of
559         * Tomcat, the value of the last entry in the HTTP header X_FORWARDED_FOR is
560         * returned, otherwise the REMOTE_ADDR is returned. The list of trusted
561         * proxy IPs can be configured using the property
562         * MCR.Request.TrustedProxies, which is a List of IP addresses separated by
563         * blanks and/or comma.
564         */
565        public static String getRemoteAddr(HttpServletRequest req) {
566            if (trustedProxies.isEmpty())
567                initTrustedProxies();
568    
569            // Check if request comes in via a proxy
570            // There are two possible header names
571            String xForwardedFor = req.getHeader("X_FORWARDED_FOR");
572            if ((xForwardedFor == null) || (xForwardedFor.trim().length() == 0)) {
573                xForwardedFor = req.getHeader("x-forwarded-for");
574            }
575    
576            // If no proxy is used, use client IP from HTTP request
577            if ((xForwardedFor == null) || (xForwardedFor.trim().length() == 0))
578                return req.getRemoteAddr();
579    
580            // X_FORWARDED_FOR can be comma separated list of hosts,
581            // if so, take last entry, all others are not reliable because
582            // any client may have set the header to any value.
583            StringTokenizer st = new StringTokenizer(xForwardedFor, " ,;");
584            while (st.hasMoreTokens())
585                xForwardedFor = st.nextToken();
586    
587            // If request comes from a trusted proxy,
588            // the best IP is the last entry in xForwardedFor
589            if (trustedProxies.contains(req.getRemoteAddr()))
590                return xForwardedFor;
591    
592            // Otherwise, use client IP from HTTP request
593            return req.getRemoteAddr();
594        }
595    
596        @SuppressWarnings("unchecked")
597        private static void putParamsToSession(HttpServletRequest request) {
598            MCRSession mcrSession = MCRSessionMgr.getCurrentSession();
599    
600            for (Enumeration<String> e = request.getParameterNames(); e.hasMoreElements();) {
601                String name = e.nextElement();
602                if (name.startsWith("XSL.") && name.endsWith(".SESSION")) {
603                    String key = name.substring(0, name.length() - 8);
604                    // parameter is not empty -> store
605                    if (!request.getParameter(name).trim().equals("")) {
606                        mcrSession.put(key, request.getParameter(name));
607                        LOGGER.debug("Found HTTP-Req.-Parameter " + name + "=" + request.getParameter(name)
608                                + " that should be saved in session, safed " + key + "=" + request.getParameter(name));
609                    }
610                    // paramter is empty -> do not store and if contained in
611                    // session, remove from it
612                    else {
613                        if (mcrSession.get(key) != null)
614                            mcrSession.deleteObject(key);
615                    }
616                }
617            }
618            for (Enumeration<String> e = request.getAttributeNames(); e.hasMoreElements();) {
619                String name = e.nextElement();
620                if (name.startsWith("XSL.") && name.endsWith(".SESSION")) {
621                    String key = name.substring(0, name.length() - 8);
622                    // attribute is not empty -> store
623                    if (!request.getAttribute(name).toString().trim().equals("")) {
624                        mcrSession.put(key, request.getAttribute(name));
625                        LOGGER.debug("Found HTTP-Req.-Attribute " + name + "=" + request.getParameter(name)
626                                + " that should be saved in session, safed " + key + "=" + request.getParameter(name));
627                    }
628                    // attribute is empty -> do not store and if contained in
629                    // session, remove from it
630                    else {
631                        if (mcrSession.get(key) != null)
632                            mcrSession.deleteObject(key);
633                    }
634                }
635            }
636        }
637    }