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 }