com.w20e.socrates.servlet.WebsurveyServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.w20e.socrates.servlet.WebsurveyServlet.java

Source

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * You should have received a copy of the GNU General Public License
 * (for example /usr/src/linux/COPYING); if not, write to the Free
 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * File      : WoliWebServlet.java
 * Classname : WoliWebServlet
 * Author    : Duco Dokter, Wietze Helmantel
 * Created   : Sun Jan 30 14:46:19 2005
 * Version   : $Revision: 1.2 $
 * Copyright : Wyldebeast & Wunderliebe
 */

package com.w20e.socrates.servlet;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletConfig;
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 org.apache.commons.configuration.Configuration;

import com.w20e.socrates.config.ConfigurationResource;
import com.w20e.socrates.data.Instance;
import com.w20e.socrates.data.Node;
import com.w20e.socrates.process.Runner;
import com.w20e.socrates.process.RunnerContext;
import com.w20e.socrates.process.RunnerContextImpl;
import com.w20e.socrates.process.RunnerFactoryImpl;
import com.w20e.socrates.process.UnsupportedMediumException;
import com.w20e.socrates.process.ValidationException;
import com.w20e.socrates.rendering.Control;
import com.w20e.socrates.rendering.RenderState;
import com.w20e.socrates.rendering.Renderable;
import com.w20e.socrates.submission.HandlerManager;
import com.w20e.socrates.submission.NoneSubmissionHandler;
import com.w20e.socrates.submission.SubmissionException;
import com.w20e.socrates.submission.XMLFileSubmissionHandler;
import com.w20e.socrates.util.LocaleUtility;
import com.w20e.socrates.workflow.ActionResultImpl;
import com.w20e.socrates.workflow.Failure;

/**
 * Servlet for Socrates questionnaires. The servlet takes just one initialization
 * parameter: the location of the questionnaire config files. This can be
 * specified by either adding a java environment variable socrates.cfg.root to
 * the JRE arguments, or by specifying an initial parameter in the web.xml.
 * 
 * The servlet contains some logic to be able to go forwards and backwards, and,
 * in case of browser 'back' actions, picking up the correct state of the questionnaire. The
 * Servlet expects a 'state' parameter always, indicating the unique state for
 * the questionnaire. This enables the questionnaire runner to reset it's state
 * if someone happens to jump back and forward by means of the browser buttons.
 * 
 * Necessities: 'stateId' parameter. If none, just go forward.
 * 
 * @todo This class is way to large. We should cut it up into little pieces...
 */
public class WebsurveyServlet extends HttpServlet {

    /**
     * Version ID.
     */
    private static final long serialVersionUID = -7126084126224585784L;

    /**
     * Formatting Dates.
     */
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyyMMddHHmmss");

    /**
     * This servlet's session manager.
     */
    private SessionManager sessionMgr;

    /**
     * Initialize this class' logging.
     */
    private static final Logger LOGGER = Logger.getLogger(WebsurveyServlet.class.getName());

    /**
     * Hold the runner factory.
     */
    private RunnerFactoryImpl runnerFactory;

    /**
     * Hold config rootdir.
     */
    private String rootDir;

    /**
     * The 'init' method creates an instance of the Socrates class, and allocates
     * initial resources. This includes compiling of XSL style sheets and
     * allocation of the database connections.
     * 
     * @param c
     *            Servlet configuration
     * @throws ServletException
     *             when the servlet fails.
     */
    public final void init(final ServletConfig c) throws ServletException {

        super.init(c);

        LOGGER.info("Initializing the Websurvey servlet");

        this.sessionMgr = new SessionManager();

        // Adding sessionmanager to servlet context, so individual sessions can
        // reach their manager.
        //
        getServletContext().setAttribute("socrates.sessionmanager", this.sessionMgr);

        if (System.getProperty("socrates.cfg.root") != null) {
            this.rootDir = System.getProperty("socrates.cfg.root");
        } else if (c.getInitParameter("socrates.cfg.root") != null) {
            this.rootDir = c.getInitParameter("socrates.cfg.root");
        } else {
            this.rootDir = ".";
        }

        LOGGER.info("Setting config root to " + this.rootDir);

        this.runnerFactory = new RunnerFactoryImpl(this.rootDir);

        // Register handlers
        HandlerManager.getInstance().register("file", new XMLFileSubmissionHandler());
        HandlerManager.getInstance().register("none", new NoneSubmissionHandler());
    }

    /**
     * Do the thing... If there is no runner (context) in the session, create a
     * new session based on the given id parameter. If there is also no id
     * parameter, it's an error. If the id parameter is given, create a new
     * runner context anyway. If a parameter called regkey is given, this
     * parameter is used for storage and possibly retrieval of the instance.
     * This way, a user may provide it's own key.
     * 
     * @param req
     *            The request
     * @param res
     *            The response
     * @throws IOException
     *             when some io error occurs
     * @throws ServletException
     *             when the servlet fails
     */
    public final void doPost(final HttpServletRequest req, final HttpServletResponse res)
            throws IOException, ServletException {

        // Always use UTF!
        res.setContentType("text/html;charset=UTF-8");
        req.setCharacterEncoding("UTF-8");

        // Thou shalst not cache...
        res.addHeader("Cache-Control", "no-cache");
        res.addHeader("Pragma", "No-Cache");

        HttpSession session = this.sessionMgr.getSession(req);

        // If we don't have a session now, we might as well call it a day...
        if (session == null) {

            if (ServletHelper.getCookie(req, "JSESSIONID") != null) {
                LOGGER.warning("Session timeout");
                res.sendRedirect("session-timeout.html");
                res.getOutputStream().flush();
                return;
            } else {
                LOGGER.severe("No session created");
                res.sendRedirect("session-creation-error.html");
                res.getOutputStream().flush();
                return;
            }
        }

        // Hold all enable/disable options
        //
        Map<String, String> options = ServletHelper.determineOptions(req);

        // If no runner yet for this session, create one. We should have
        // startup param's for the runner, like the questionnaire to run, and
        // the locale. If these are not available, check for regkey. Else, all fails.
        //
        if (session.getAttribute("runnerCtx") == null) {

            LOGGER.finer("Session instantiated with id " + session.getId());
            LOGGER.fine("No runner context available in session; creating one");

            if (req.getParameter("id") == null && req.getParameter("regkey") == null) {
                LOGGER.warning("No id nor regkey parameter in request");
                try {
                    res.sendRedirect("session-creation-error.html");
                    this.sessionMgr.invalidateSession(req);
                    res.getOutputStream().flush();
                } catch (IOException e) {
                    LOGGER.severe("Couldn't even send error message..." + e.getMessage());
                }
                return;
            }

            if (!initializeRunner(req, res, session, options)) {
                LOGGER.severe("Could not create runner context. Bye for now.");
                return;
            }
        }

        // Okido, by now we should have a session, and a valid runner context
        // stored in the session.
        //
        try {
            WebsurveyContext wwCtx = (WebsurveyContext) session.getAttribute("runnerCtx");

            // Now let's see whether this session was deserialized.
            //
            if (wwCtx.isInvalid()) {
                LOGGER.info("Serialized session found!");
                // Re-create the context, and attach to WoliWeb context.
                LOGGER.finer("Model id: " + wwCtx.getModelId());
                LOGGER.finer("State id: " + wwCtx.getStateId());
                LOGGER.finer("Locale: " + wwCtx.getLocale());

                URI qUri = QuestionnaireURIFactory.getInstance().determineURI(this.rootDir, wwCtx.getModelId());

                RunnerContextImpl ctx = this.runnerFactory.createContext(qUri, null);
                ctx.setLocale(wwCtx.getLocale());
                ctx.setQuestionnaireId(qUri);
                ctx.getStateManager().setStateById(wwCtx.getStateId());
                ctx.setInstance(wwCtx.getInstance());
                wwCtx.setRunnerContext(ctx);
            }

            RunnerContextImpl ctx = (RunnerContextImpl) wwCtx.getRunnerContext();

            LOGGER.finer("Session id " + session.getId());
            LOGGER.finer("Context id " + ctx.getInstance().getMetaData().get("key"));

            // set locale if requested later on, when the survey is well under way...
            if (req.getParameter("locale") != null && req.getParameter("id") == null) {
                ctx.setLocale(LocaleUtility.getLocale(req.getParameter("locale"), false));
                LOGGER.fine("Locale change requested; set to "
                        + LocaleUtility.getLocale(req.getParameter("locale"), false));
            }

            // even check on locale in instance data...
            try {
                Locale instanceLocale = LocaleUtility
                        .getLocale(ctx.getInstance().getNode("locale").getValue().toString(), false);

                if (instanceLocale != null && instanceLocale != ctx.getLocale()) {
                    LOGGER.fine("Locale is set in instance data: " + instanceLocale);
                    ctx.setLocale(instanceLocale);
                }
            } catch (Exception ex) {
                // not a problem...
            }

            // Add specific options
            // @todo This should move to the runner creation options.
            if (ctx.getProperty("renderOptions") == null) {
                ctx.setProperty("renderOptions", options);
            } else {
                ((Map<String, String>) ctx.getProperty("renderOptions")).putAll(options);
            }

            Map<String, Object> params = ParameterParser.parseParams(req);

            ctx.setData(params);

            // Do we have initial data already?
            if ("true".equals(options.get("enable_preload_params"))) {
                Node node;
                for (String key : params.keySet()) {
                    node = ctx.getInstance().getNode(key);
                    if (node != null) {
                        LOGGER.fine("Preloading node value " + params.get(key) + " for node " + node.getName());
                        node.setValue(params.get(key));
                    }
                }
            }

            ByteArrayOutputStream output = new ByteArrayOutputStream();

            ctx.setOutputStream(output);

            // @todo: I really don't see why we should re-create the runner for
            // every post. Actually, the factory holds a reference to existing
            // runners, so it is not really bad, but I reckon the context should
            // hold the runner?
            //
            URI qUri = QuestionnaireURIFactory.getInstance().determineURI(this.rootDir, wwCtx.getModelId());

            Runner runner = this.runnerFactory.createRunner(qUri);

            if (req.getParameter("previous") == null) {
                Map<String, Object> meta = ctx.getInstance().getMetaData();
                meta.put("time_" + req.getParameter("stateId"), new Date());
            }

            // Always store stateId in instance, for retrieval of state after
            // serialization.
            //
            if (req.getParameter("stateId") != null) {
                LOGGER.fine("Setting state id to " + req.getParameter("stateId"));
                ctx.getInstance().getMetaData().put("stateId", req.getParameter("stateId"));
                if (!ctx.getStateManager().setStateById(req.getParameter("stateId"))) {
                    LOGGER.warning("Couldn't set stateId to " + req.getParameter("stateId"));
                }
            }

            // Go two states back if 'previous' request, and simply execute
            // 'next'.
            if (req.getParameter("previous") != null) {
                ctx.getStateManager().previous();
                RenderState state = ctx.getStateManager().previous();

                LOGGER.finest("Fill data from instance");

                ctx.setProperty("previous", "true");

                if (state != null) {

                    // Make sure to fill in existing data, otherwise we'll get
                    // an error
                    //
                    for (Iterator<Renderable> i = state.getItems().iterator(); i.hasNext();) {
                        Renderable r = i.next();
                        if (r instanceof Control) {
                            String name = ((Control) r).getBind();
                            params.put(name, ctx.getInstance().getNode(name).getValue());
                            LOGGER.finest("Set node " + name + " to " + params.get(name));
                        }
                    }
                }
            } else {
                ctx.setProperty("previous", "false");
            }

            next(ctx, runner);

            LOGGER.fine("Are we stored yet? " + ctx.getInstance().getMetaData().get("storage-type"));

            // If we submitted, destroy long session
            if ("submit".equals(ctx.getInstance().getMetaData().get("storage-type"))) {
                LOGGER.fine("Invalidating long session");

                String surveyId = ctx.getInstance().getMetaData().get("qId").toString();

                this.sessionMgr.invalidateLongSession(surveyId, req, res);
            }

            // If this was the last action, destroy session.
            if (!runner.hasNext(ctx)) {
                this.sessionMgr.invalidateSession(req);
            }

            res.getOutputStream().write(output.toByteArray());
            res.getOutputStream().flush();

            // free resources...
            ctx.setOutputStream(null);

        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "No runner created", e);
            throw new ServletException("Runner could not be created: " + e.getMessage());
        }
    }

    /**
     * Just forward to doPost.
     * 
     * @param req
     *            The request
     * @param res
     *            The response
     * @throws IOException
     *             when some io error occurs
     * @throws ServletException
     *             when the servlet fails
     */
    public final void doGet(final HttpServletRequest req, final HttpServletResponse res)
            throws IOException, ServletException {

        doPost(req, res);
    }

    /**
     * Get next page.
     * 
     * @param ctx
     *            a runner context value
     * @param runner
     *            runner to use
     * @exception Exception
     *                if an error occurs
     */
    private void next(final RunnerContext ctx, final Runner runner) throws Exception {

        LOGGER.finest("Next state asked");

        // get next action till we receive the wait status. This
        // indicates that something is hanging out for user input.
        //
        while (runner.hasNext(ctx)) {

            LOGGER.finest("Doing next thing");
            LOGGER.fine("Current action before next call: " + ctx.getCurrentAction());
            runner.next(ctx);
            LOGGER.fine("Current action after next call: " + ctx.getCurrentAction());
            LOGGER.finest("Last result: " + ctx.getResult());

            // Failure may be due to validation, in which case we should
            // provide an error message, or due to a submission error, in which
            // case we're unhappy...
            //
            if (ActionResultImpl.FAIL.equals(ctx.getResult().toString())) {

                // Is it the data?
                Exception e = ((Failure) ctx.getResult()).getException();

                if (e != null && e instanceof ValidationException) {

                    for (Iterator<Entry<String, Exception>> i = ((ValidationException) e).getErrors().entrySet()
                            .iterator(); i.hasNext();) {
                        LOGGER.fine("Error: " + i.next());
                    }
                } else if (e != null && e instanceof SubmissionException) {
                    LOGGER.log(Level.SEVERE, "SubmissionException" + e.getMessage());
                } else {
                    LOGGER.info("Failure in workflow (usually expected)");
                    if (e != null) {
                        LOGGER.info(e.getMessage());
                    }
                }
            } else if (ActionResultImpl.WAIT.equals(ctx.getResult().toString())) {
                break;
            }
        }
    }

    /**
     * Initialize the runner for a given questionnaire. The runner, if 
     * successfully created, is stored in the 'runnerCtx' attribute
     * of the session. 
     * 
     * @param req HTTP request
     * @param res HTTP response
     * @param session HTTP session
     * @param options any specific creation options
     */
    private boolean initializeRunner(HttpServletRequest req, HttpServletResponse res, HttpSession session,
            Map<String, String> options) {

        String id = req.getParameter("id");

        LOGGER.finest("Parameter id is " + id);

        URI qUri = QuestionnaireURIFactory.getInstance().determineURI(this.rootDir, id);

        /**
         * Get global config.
         */
        Configuration cfg = null;

        try {
            cfg = ConfigurationResource.getInstance().getConfiguration(qUri.toURL());
        } catch (Exception e1) {
            return false;
        }

        LOGGER.fine("Using config URI " + qUri.toString());

        try {
            RunnerContextImpl ctx = this.runnerFactory.createContext(qUri, options);

            // Check whether the instance has a variable locale set. If so, this becomes the default.
            //
            Locale locale = null;

            try {
                locale = LocaleUtility.getLocale(ctx.getInstance().getNode("locale").getValue().toString(), true);
                LOGGER.fine("Using default locale set in model instance: " + locale);
            } catch (Exception e) {
                locale = LocaleUtility.DEFAULT_LOCALE;
                LOGGER.warning(
                        "Not using default locale set in model instance due to errors, fall back: " + locale);
            }

            LOGGER.fine("Using default locale " + locale);

            // Now see if we need to take the locale from the request
            // parameters or the user agent headers.
            locale = ServletHelper.getLocale(req, locale);

            LOGGER.fine("Using locale " + locale);

            ctx.setLocale(locale);
            ctx.setQuestionnaireId(qUri);

            /*
             * We may need to reread an existing data set. We do this if the
             * request didn't explicitly forbid it, and we do have either an
             * existing session or a stored instance file.
             */
            if ("true".equals(cfg.getString("enablelongsessions", "true"))) {

                LOGGER.info("Has long session? " + this.sessionMgr.hasLongSession(req, id));

                if (this.sessionMgr.hasLongSession(req, id) && !"true".equals(options.get("disable_reload"))) {

                    Instance inst = this.sessionMgr.salvageInstance(id, req, ctx);

                    if (inst != null) {
                        ctx.setInstance(inst);
                        LOGGER.fine("Setting state to " + (String) inst.getMetaData().get("stateId"));
                        ctx.getStateManager().setStateById((String) inst.getMetaData().get("stateId"));
                    } else {
                        LOGGER.warning("Unable to restore instance");
                    }
                }
            } else if (req.getParameter("regkey") != null) {
                Instance inst = this.sessionMgr.salvageInstanceFromRegkey(req.getParameter("regkey"), req, ctx);

                if (inst != null) {
                    ctx.setInstance(inst);
                    LOGGER.fine("Setting state to " + (String) inst.getMetaData().get("stateId"));
                    ctx.getStateManager().setStateById((String) inst.getMetaData().get("stateId"));
                } else {
                    LOGGER.warning("Unable to restore instance");
                }
            }

            Map<String, Object> meta = ctx.getInstance().getMetaData();

            meta.put("qId", id);
            meta.put("qLocale", locale);

            ServletHelper.setMetaData(req, meta);

            // Store runner context in session
            //
            session.setAttribute("runnerCtx", new WebsurveyContext(ctx, id, locale));

            // Output filename. If unset, default to overwritable file.
            //
            if (!meta.containsKey("filename") || meta.get("filename") == null) {

                meta.put("filename",
                        id + (ctx.getModel().getMetaData().containsKey("Version")
                                ? "-" + ctx.getModel().getMetaData().get("Version")
                                : "") + "_" + locale + "_"
                                + WebsurveyServlet.FORMATTER.format(Calendar.getInstance().getTime()) + "_"
                                + meta.get("key"));
            }

            if ("true".equals(cfg.getString("enablelongsessions", "true"))) {

                // Finally, add cookie that holds info on user data, if we
                // don't
                // already have it, and set output
                // file name.
                //
                if (!this.sessionMgr.hasLongSession(req, id)) {
                    this.sessionMgr.createLongLivedSession(id,
                            meta.get("filename").toString() + "||" + session.getId(), res);
                }
            }
        } catch (UnsupportedMediumException e) {

            this.sessionMgr.invalidateSession(req);
            LOGGER.log(Level.SEVERE, "Error in creating runner context", e);
            return false;
        }

        return true;
    }
}