org.mycore.frontend.servlets.MCRServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.mycore.frontend.servlets.MCRServlet.java

Source

/*
 * 
 * $Revision$ $Date$
 * 
 * This file is part of *** M y C o R e *** See http://www.mycore.de/ for
 * details.
 * 
 * This program is free software; you can use it, redistribute it and / or
 * modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program, in a file called gpl.txt or license.txt. If not, write to the
 * Free Software Foundation Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307 USA
 */

package org.mycore.frontend.servlets;

import static org.mycore.frontend.MCRFrontendUtil.BASE_URL_ATTRIBUTE;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.transform.TransformerException;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.DocType;
import org.jdom2.Document;
import org.jdom2.Element;
import org.mycore.common.MCRException;
import org.mycore.common.MCRSession;
import org.mycore.common.MCRSessionMgr;
import org.mycore.common.MCRStreamUtils;
import org.mycore.common.config.MCRConfiguration;
import org.mycore.common.config.MCRConfigurationDirSetup;
import org.mycore.common.content.MCRJDOMContent;
import org.mycore.common.xml.MCRLayoutService;
import org.mycore.common.xsl.MCRErrorListener;
import org.mycore.datamodel.common.MCRActiveLinkException;
import org.mycore.frontend.MCRFrontendUtil;
import org.mycore.services.i18n.MCRTranslation;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * This is the superclass of all MyCoRe servlets. It provides helper methods for logging and managing the current
 * session data. Part of the code has been taken from MilessServlet.java written by Frank Ltzenkirchen.
 * 
 * @author Detlev Degenhardt
 * @author Frank Ltzenkirchen
 * @author Thomas Scheffler (yagee)
 * @version $Revision$ $Date$
 */
public class MCRServlet extends HttpServlet {
    public static final String ATTR_MYCORE_SESSION = "mycore.session";

    private static final String CURRENT_THREAD_NAME_KEY = "currentThreadName";

    private static final String INITIAL_SERVLET_NAME_KEY = "currentServletName";

    private static final long serialVersionUID = 1L;

    private static Logger LOGGER = LogManager.getLogger();

    private static String SERVLET_URL;

    private static final boolean ENABLE_BROWSER_CACHE = MCRConfiguration.instance()
            .getBoolean("MCR.Servlet.BrowserCache.enable", false);

    private static MCRLayoutService LAYOUT_SERVICE;

    private static String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";

    public static MCRLayoutService getLayoutService() {
        return LAYOUT_SERVICE;
    }

    @Override
    public void init() throws ServletException {
        super.init();
        if (LAYOUT_SERVICE == null) {
            LAYOUT_SERVICE = MCRLayoutService.instance();
        }
    }

    /**
     * Returns the servlet base URL of the mycore system
     **/
    public static String getServletBaseURL() {
        MCRSession session = MCRSessionMgr.getCurrentSession();
        Object value = session.get(BASE_URL_ATTRIBUTE);
        if (value != null) {
            LOGGER.debug("Returning BaseURL " + value.toString() + "servlets/ from user session.");
            return value.toString() + "servlets/";
        }
        return SERVLET_URL != null ? SERVLET_URL : MCRFrontendUtil.getBaseURL() + "servlets/";
    }

    /**
     * Initialisation of the static values for the base URL and servlet URL of the mycore system.
     */
    private static synchronized void prepareBaseURLs(ServletContext context, HttpServletRequest req) {
        String contextPath = req.getContextPath() + "/";

        String requestURL = req.getRequestURL().toString();
        int pos = requestURL.indexOf(contextPath, 9);
        String baseURLofRequest = requestURL.substring(0, pos) + contextPath;

        prepareBaseURLs(baseURLofRequest);
    }

    private static void prepareBaseURLs(String baseURLofRequest) {
        MCRFrontendUtil.prepareBaseURLs(baseURLofRequest);
        SERVLET_URL = MCRFrontendUtil.getBaseURL() + "servlets/";
    }

    // The methods doGet() and doPost() simply call the private method
    // doGetPost(),
    // i.e. GET- and POST requests are handled by one method only.
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        try {
            doGetPost(req, res);
        } catch (SAXException | TransformerException e) {
            throwIOException(e);
        }
    }

    private void throwIOException(Exception e) throws IOException {
        if (e instanceof IOException) {
            throw (IOException) e;
        }
        if (e instanceof TransformerException) {
            TransformerException te = MCRErrorListener.unwrapException((TransformerException) e);
            String myMessageAndLocation = MCRErrorListener.getMyMessageAndLocation(te);
            throw new IOException("Error while XSL Transformation: " + myMessageAndLocation, e);
        }
        if (e instanceof SAXParseException) {
            SAXParseException spe = (SAXParseException) e;
            String id = spe.getSystemId() != null ? spe.getSystemId() : spe.getPublicId();
            int line = spe.getLineNumber();
            int column = spe.getColumnNumber();
            String msg = MessageFormat.format("Error on {0}:{1} while parsing {2}", line, column, id);
            throw new IOException(msg, e);
        }
        throw new IOException(e);
    }

    protected void doGet(MCRServletJob job) throws Exception {
        doGetPost(job);
    }

    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
        try {
            doGetPost(req, res);
        } catch (SAXException | TransformerException e) {
            throwIOException(e);
        }
    }

    protected void doPost(MCRServletJob job) throws Exception {
        doGetPost(job);
    }

    public static MCRSession getSession(HttpServletRequest req) {
        boolean reusedSession = req.isRequestedSessionIdValid();
        HttpSession theSession = req.getSession(true);
        if (reusedSession) {
            LOGGER.debug(() -> "Reused HTTP session: " + theSession.getId() + ", created: "
                    + LocalDateTime
                            .ofInstant(Instant.ofEpochMilli(theSession.getCreationTime()), ZoneId.systemDefault())
                            .toString());
        } else {
            LOGGER.info(() -> "Created new HTTP session: " + theSession.getId());
        }
        MCRSession session = null;

        MCRSession fromHttpSession = Optional.ofNullable((String) theSession.getAttribute(ATTR_MYCORE_SESSION))
                .map(MCRSessionMgr::getSession).orElse(null);

        if (fromHttpSession != null && fromHttpSession.getID() != null) {
            // Take session from HttpSession with servlets
            session = fromHttpSession;
            String lastIP = session.getCurrentIP();
            if (lastIP.length() != 0) {
                //check if request IP equals last known IP
                String newip = MCRFrontendUtil.getRemoteAddr(req);
                if (!lastIP.equals(newip) && !newip.equals(MCRFrontendUtil.getHostIP())) {
                    LOGGER.warn("Session steal attempt from IP " + newip + ", previous IP was " + lastIP
                            + ". Session: " + session.toString());
                    MCRSessionMgr.releaseCurrentSession();
                    session.close(); //MCR-1409 do not leak old session
                    session = MCRSessionMgr.getCurrentSession();
                    session.setCurrentIP(newip);
                }
            }
        } else {
            // Create a new session
            session = MCRSessionMgr.getCurrentSession();
        }

        // Store current session in HttpSession
        theSession.setAttribute(ATTR_MYCORE_SESSION, session.getID());
        // store the HttpSession ID in MCRSession
        if (session.put("http.session", theSession.getId()) == null) {
            //first request
            MCRStreamUtils.asStream(req.getLocales()).map(Locale::toString)
                    .filter(MCRTranslation.getAvailableLanguages()::contains).findFirst()
                    .ifPresent(session::setCurrentLanguage);
        }
        // Forward MCRSessionID to XSL Stylesheets
        req.setAttribute("XSL.MCRSessionID", session.getID());

        return session;
    }

    private static void bindSessionToRequest(HttpServletRequest req, String servletName, MCRSession session) {
        if (!isSessionBoundToRequest(req)) {
            // Bind current session to this thread:
            MCRSessionMgr.setCurrentSession(session);
            req.setAttribute(CURRENT_THREAD_NAME_KEY, Thread.currentThread().getName());
            req.setAttribute(INITIAL_SERVLET_NAME_KEY, servletName);
        }
    }

    private static boolean isSessionBoundToRequest(HttpServletRequest req) {
        String currentThread = getProperty(req, CURRENT_THREAD_NAME_KEY);
        // check if this is request passed the same thread before
        // (RequestDispatcher)
        return currentThread != null && currentThread.equals(Thread.currentThread().getName());
    }

    /**
     * This private method handles both GET and POST requests and is invoked by doGet() and doPost().
     * 
     * @param req
     *            the HTTP request instance
     * @param res
     *            the HTTP response instance
     * @exception IOException
     *                for java I/O errors.
     * @exception ServletException
     *                for errors from the servlet engine.
     */
    private void doGetPost(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException, SAXException, TransformerException {
        if (MCRConfiguration.instance() == null) {
            // removes NullPointerException below, if somehow Servlet is not yet
            // intialized
            init();
        }

        // Try to set encoding of form values
        String ReqCharEncoding = req.getCharacterEncoding();

        if (ReqCharEncoding == null) {
            // Set default to UTF-8
            ReqCharEncoding = MCRConfiguration.instance().getString("MCR.Request.CharEncoding", "UTF-8");
            req.setCharacterEncoding(ReqCharEncoding);
            LOGGER.debug("Setting ReqCharEncoding to: " + ReqCharEncoding);
        }

        if ("true".equals(req.getParameter("reload.properties"))) {
            MCRConfigurationDirSetup setup = new MCRConfigurationDirSetup();
            setup.startUp(getServletContext());
        }

        if (SERVLET_URL == null) {
            prepareBaseURLs(getServletContext(), req);
        }

        MCRServletJob job = new MCRServletJob(req, res);

        MCRSession session = getSession(job.getRequest());
        bindSessionToRequest(req, getServletName(), session);
        try {
            // transaction around 1st phase of request
            Exception thinkException = processThinkPhase(job);
            // first phase completed, start rendering phase
            processRenderingPhase(job, thinkException);
        } catch (Exception ex) {
            if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
                // current Servlet not called via RequestDispatcher
                session.rollbackTransaction();
            }
            if (isBrokenPipe(ex)) {
                LOGGER.info("Ignore broken pipe.");
                return;
            }
            if (ex.getMessage() == null) {
                LOGGER.error("Exception while in rendering phase.", ex);
            } else {
                LOGGER.error("Exception while in rendering phase: " + ex.getMessage());
            }
            if (ex instanceof ServletException) {
                throw (ServletException) ex;
            } else if (ex instanceof IOException) {
                throw (IOException) ex;
            } else if (ex instanceof SAXException) {
                throw (SAXException) ex;
            } else if (ex instanceof TransformerException) {
                throw (TransformerException) ex;
            } else if (ex instanceof RuntimeException) {
                throw (RuntimeException) ex;
            } else {
                throw new RuntimeException(ex);
            }
        } finally {
            // Release current MCRSession from current Thread,
            // in case that Thread pooling will be used by servlet engine
            if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
                // current Servlet not called via RequestDispatcher
                MCRSessionMgr.releaseCurrentSession();
            }
        }
    }

    private static boolean isBrokenPipe(Throwable throwable) {
        String message = throwable.getMessage();
        if (message != null && throwable instanceof IOException && message.contains("Broken pipe")) {
            return true;
        }
        return throwable.getCause() != null ? isBrokenPipe(throwable.getCause()) : false;
    }

    private void configureSession(MCRServletJob job) {
        MCRSession session = MCRSessionMgr.getCurrentSession();

        String c = getClass().getName();
        c = c.substring(c.lastIndexOf(".") + 1);

        StringBuilder msg = new StringBuilder();
        msg.append(c);
        msg.append(" ip=");
        msg.append(MCRFrontendUtil.getRemoteAddr(job.getRequest()));
        msg.append(" mcr=").append(session.getID());
        msg.append(" user=").append(session.getUserInformation().getUserID());
        LOGGER.info(msg.toString());

        MCRFrontendUtil.configureSession(session, job.getRequest(), job.getResponse());
    }

    private Exception processThinkPhase(MCRServletJob job) {
        MCRSession session = MCRSessionMgr.getCurrentSession();
        try {
            if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
                // current Servlet not called via RequestDispatcher
                session.beginTransaction();
            }
            configureSession(job);
            think(job);
            if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
                // current Servlet not called via RequestDispatcher
                session.commitTransaction();
            }
        } catch (Exception ex) {
            if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
                // current Servlet not called via RequestDispatcher
                LOGGER.warn("Exception occurred, performing database rollback.");
                session.rollbackTransaction();
            } else {
                LOGGER.warn("Exception occurred, cannot rollback database transaction right now.");
            }
            return ex;
        }
        return null;
    }

    /**
     * 1st phase of doGetPost. This method has a seperate transaction. Per default id does nothing as a fallback to the
     * old behaviour.
     * 
     * @see #render(MCRServletJob, Exception)
     */
    protected void think(MCRServletJob job) throws Exception {
        // not implemented by default
    }

    private void processRenderingPhase(MCRServletJob job, Exception thinkException) throws Exception {
        if (allowCrossDomainRequests() && !job.getResponse().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)) {
            job.getResponse().setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        }
        MCRSession session = MCRSessionMgr.getCurrentSession();
        if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
            // current Servlet not called via RequestDispatcher
            session.beginTransaction();
        }
        render(job, thinkException);
        if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
            // current Servlet not called via RequestDispatcher
            session.commitTransaction();
        }
    }

    /**
     * Returns true if this servlet allows Cross-domain requests. The default value defined by {@link MCRServlet} is
     * <code>false</code>.
     */
    protected boolean allowCrossDomainRequests() {
        return false;
    }

    /**
     * 2nd phase of doGetPost This method has a seperate transaction and gets the same MCRServletJob from the first
     * phase (think) and any exception that occurs at the first phase. By default this method calls
     * doGetPost(MCRServletJob) as a fallback to the old behaviour.
     * 
     * @param job
     *            same instance as of think(MCRServlet job)
     * @param ex
     *            any exception thrown by think(MCRServletJob) or transaction commit
     * @throws Exception
     *             if render could not handle <code>ex</code> to produce a nice user page
     */
    protected void render(MCRServletJob job, Exception ex) throws Exception {
        // no info here how to handle
        if (ex != null)
            throw ex;
        if (job.getRequest().getMethod().equals("POST")) {
            doPost(job);
        } else {
            doGet(job);
        }
    }

    /**
     * This method should be overwritten by other servlets. As a default response we indicate the HTTP 1.1 status code
     * 501 (Not Implemented).
     */
    protected void doGetPost(MCRServletJob job) throws Exception {
        job.getResponse().sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
    }

    /** Handles an exception by reporting it and its embedded exception */
    protected void handleException(Exception ex) {
        try {
            reportException(ex);
        } catch (Exception ignored) {
            LOGGER.error(ignored);
        }
    }

    /** Reports an exception to the log */
    protected void reportException(Exception ex) throws Exception {
        String cname = this.getClass().getName();
        String servlet = cname.substring(cname.lastIndexOf(".") + 1);

        LOGGER.warn("Exception caught in : " + servlet, ex);
    }

    /**
     * @deprecated use {@link HttpServletResponse#sendError(int, String)} instead or throw Exception
     */
    @Deprecated()
    protected void generateErrorPage(HttpServletRequest request, HttpServletResponse response, int error,
            String msg, Exception ex, boolean xmlstyle) throws IOException, TransformerException, SAXException {
        LOGGER.error(
                getClass().getName() + ": Error " + error + " occured. The following message was given: " + msg,
                ex);

        String rootname = "mcr_error";
        String style = getProperty(request, "XSL.Style");
        if (style == null || !style.equals("xml")) {
            style = "default";
        }
        Element root = new Element(rootname);
        root.setAttribute("HttpError", Integer.toString(error)).setText(msg);

        Document errorDoc = new Document(root, new DocType(rootname));

        while (ex != null) {
            Element exception = new Element("exception");
            exception.setAttribute("type", ex.getClass().getName());
            Element trace = new Element("trace");
            Element message = new Element("message");
            trace.setText(MCRException.getStackTraceAsString(ex));
            message.setText(ex.getMessage());
            exception.addContent(message).addContent(trace);
            root.addContent(exception);

            if (ex instanceof MCRException) {
                if (ex.getCause() instanceof Exception) {
                    ex = (Exception) ex.getCause();
                }
            } else {
                ex = null;
            }
        }

        request.setAttribute("XSL.Style", style);

        final String requestAttr = "MCRServlet.generateErrorPage";
        if (!response.isCommitted() && request.getAttribute(requestAttr) == null) {
            response.setStatus(error);
            request.setAttribute(requestAttr, msg);
            LAYOUT_SERVICE.doLayout(request, response, new MCRJDOMContent(errorDoc));
            return;
        } else {
            if (request.getAttribute(requestAttr) != null) {
                LOGGER.warn("Could not send error page. Generating error page failed. The original message:\n"
                        + request.getAttribute(requestAttr));
            } else {
                LOGGER.warn(
                        "Could not send error page. Response allready commited. The following message was given:\n"
                                + msg);
            }
        }
    }

    /**
     * This method builds a URL that can be used to redirect the client browser to another page, thereby including http
     * request parameters. The request parameters will be encoded as http get request.
     * 
     * @param baseURL
     *            the base url of the target webpage
     * @param parameters
     *            the http request parameters
     */
    protected String buildRedirectURL(String baseURL, Properties parameters) {
        StringBuilder redirectURL = new StringBuilder(baseURL);
        boolean first = true;
        for (Enumeration<?> e = parameters.keys(); e.hasMoreElements();) {
            if (first) {
                redirectURL.append("?");
                first = false;
            } else {
                redirectURL.append("&");
            }

            String name = (String) e.nextElement();
            String value = null;
            try {
                value = URLEncoder.encode(parameters.getProperty(name), "UTF-8");
            } catch (UnsupportedEncodingException ex) {
                value = parameters.getProperty(name);
            }

            redirectURL.append(name).append("=").append(value);
        }
        LOGGER.debug("Sending redirect to " + redirectURL.toString());
        return redirectURL.toString();
    }

    protected void generateActiveLinkErrorpage(HttpServletRequest request, HttpServletResponse response, String msg,
            MCRActiveLinkException activeLinks) throws IOException, TransformerException, SAXException {
        StringBuilder msgBuf = new StringBuilder(msg);
        msgBuf.append(
                "\nThere are links active preventing the commit of work, see error message for details. The following links where affected:");
        Map<String, Collection<String>> links = activeLinks.getActiveLinks();
        for (Map.Entry<String, Collection<String>> entry : links.entrySet()) {
            for (String source : entry.getValue()) {
                msgBuf.append('\n').append(source).append("==>").append(entry.getKey());
            }
        }
        generateErrorPage(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msgBuf.toString(),
                activeLinks, false);
    }

    /**
     * allows browser to cache requests. This method is usefull as it allows browsers to cache content that is not
     * changed. Please overwrite this method in every Servlet that depends on "remote" data.
     */
    @Override
    protected long getLastModified(HttpServletRequest request) {
        if (ENABLE_BROWSER_CACHE) {
            // we can cache every (local) request
            long lastModified = MCRSessionMgr.getCurrentSession().getLoginTime() > MCRConfiguration.instance()
                    .getSystemLastModified() ? MCRSessionMgr.getCurrentSession().getLoginTime()
                            : MCRConfiguration.instance().getSystemLastModified();
            LOGGER.info("LastModified: " + lastModified);
            return lastModified;
        }
        return -1; // time is not known
    }

    public static String getProperty(HttpServletRequest request, String name) {
        return MCRFrontendUtil.getProperty(request, name).orElse(null);
    }

    /**
     * returns a translated error message for the current Servlet. I18N keys are of form
     * {prefix}'.'{SimpleServletClassName}'.'{subIdentifier}
     * 
     * @param prefix
     *            a prefix of the message property like component.base.error
     * @param subIdentifier
     *            last part of I18n key
     * @param args
     *            any arguments that should be passed to {@link MCRTranslation#translate(String, Object...)}
     */
    protected String getErrorI18N(String prefix, String subIdentifier, Object... args) {
        String key = MessageFormat.format("{0}.{1}.{2}", prefix, getClass().getSimpleName(), subIdentifier);
        return MCRTranslation.translate(key, args);
    }

    /**
     * Returns the referer of the given request.
     */
    protected URL getReferer(HttpServletRequest request) {
        String referer;
        referer = request.getHeader("Referer");
        if (referer == null) {
            return null;
        }
        try {
            return new URL(referer);
        } catch (MalformedURLException e) {
            //should not happen
            LOGGER.error("Referer is not a valid URL: " + referer, e);
            return null;
        }
    }

    /**
     * If a referrer is available this method redirects to the url given by the referrer otherwise method redirects to
     * the application base url.
     */
    protected void toReferrer(HttpServletRequest request, HttpServletResponse response) throws IOException {
        URL referrer = getReferer(request);
        if (referrer != null) {
            response.sendRedirect(response.encodeRedirectURL(referrer.toString()));
        } else {
            LOGGER.warn("Could not get referrer, returning to the application's base url");
            response.sendRedirect(response.encodeRedirectURL(MCRFrontendUtil.getBaseURL()));
        }
    }

    /**
     * If a referrer is available this method redirects to the url given by the referrer otherwise method redirects to
     * the alternative-url.
     */
    protected void toReferrer(HttpServletRequest request, HttpServletResponse response, String altURL)
            throws IOException {
        URL referrer = getReferer(request);
        if (referrer != null) {
            response.sendRedirect(response.encodeRedirectURL(referrer.toString()));
        } else {
            LOGGER.warn("Could not get referrer, returning to " + altURL);
            response.sendRedirect(response.encodeRedirectURL(altURL));
        }
    }
}