net.bluehornreader.web.ReaderHandler.java Source code

Java tutorial

Introduction

Here is the source code for net.bluehornreader.web.ReaderHandler.java

Source

/*
Copyright (c) 2013 Marian Ciobanu
    
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
    
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 */

package net.bluehornreader.web;

import net.bluehornreader.data.*;
import net.bluehornreader.misc.*;
import net.bluehornreader.model.*;
import org.apache.commons.logging.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.servlet.*;
import org.eclipse.jetty.util.*;
import org.eclipse.jetty.webapp.*;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.security.*;
import java.util.*;

import static net.bluehornreader.web.WebUtils.*;

/**
 * Created with IntelliJ IDEA.
 * User: ciobi
 * Date: 2013-06-15
 * Time: 09:56
 * <p/>
 */
public class ReaderHandler extends WebAppContext {

    public static final Log LOG = LogFactory.getLog(ReaderHandler.class);
    //ttt1 option that on http only redirects to https, for all paths

    public static final String ACTION_LOGIN = "login";
    public static final String ACTION_SIGNUP = "signup";
    public static final String ACTION_CHANGE_PASSWORD = "change_password";
    public static final String ACTION_CHANGE_SETTINGS = "change_settings";
    public static final String ACTION_ADD_FEED = "add_feed";
    public static final String ACTION_REMOVE_FEED = "remove_feed";
    public static final String ACTION_UPDATE_FEED_LIST = "update_feed_list"; // for ordering, //ttt2

    public static final String PATH_LOGIN = "/" + ACTION_LOGIN;
    public static final String PATH_CHANGE_PASSWORD = "/" + ACTION_CHANGE_PASSWORD;
    public static final String PATH_CHANGE_SETTINGS = "/" + ACTION_CHANGE_SETTINGS;
    public static final String PATH_SIGNUP = "/" + ACTION_SIGNUP;
    public static final String PATH_ADD_FEED = "/" + ACTION_ADD_FEED;
    public static final String PATH_REMOVE_FEED = "/" + ACTION_REMOVE_FEED;
    public static final String PATH_UPDATE_FEED_LIST = "/" + ACTION_UPDATE_FEED_LIST;
    public static final String PATH_ERROR = "/error";
    public static final String PATH_LOGOUT = "/logout";
    public static final String PATH_SETTINGS = "/settings";
    public static final String PATH_FEEDS = "/feeds";
    public static final String PATH_FEED = "/feed";
    public static final String PATH_ADMIN = "/admin";
    public static final String PATH_FEED_ADMIN = "/feed_admin";
    public static final String PATH_OPEN_ARTICLE = "/open_article/"; // !!! it's easier to end this one with a slash

    // params we use to send strings to the JSPs or to get user input in POST, via request.getParameter(), or both
    public static final String PARAM_USER_ID = "userId";
    public static final String PARAM_USER_NAME = "name";
    public static final String PARAM_EMAIL = "email";
    public static final String PARAM_CURRENT_PASSWORD = "currentPassword";
    public static final String PARAM_PASSWORD = "password";
    public static final String PARAM_PASSWORD_CONFIRM = "passwordConfirm";
    public static final String PARAM_PATH = "path";
    //public static final String PARAM_ERROR = "error";
    public static final String PARAM_REMEMBER_ACCOUNT = "rememberAccount";
    public static final String PARAM_NEW_FEED_URL = "feedUrl";
    public static final String PARAM_FEED_ID = "feedId";
    public static final String PARAM_ITEMS_PER_PAGE = "itemsPerPage";
    public static final String PARAM_STYLE = "style";
    public static final String PARAM_FEED_DATE_FORMAT = "feedDateFormat";

    // variable names, used to give JSPs access to Java objects in the handler via request.getAttribute(()
    public static final String VAR_USER = "user";
    public static final String VAR_LOGIN_INFO = "loginInfo";
    public static final String VAR_USER_DB = "userDb";
    public static final String VAR_FEED_DB = "feedDb";
    public static final String VAR_ARTICLE_DB = "articleDb";
    public static final String VAR_READ_ARTICLES_COLL_DB = "readArticlesCollDb";

    public static final String BROWSER_ID = "browserId";
    public static final String SESSION_ID = "sessionId";

    private LoginInfo.DB loginInfoDb;
    private User.DB userDb;
    private Feed.DB feedDb;
    private Article.DB articleDb;
    private ReadArticlesColl.DB readArticlesCollDb;

    private UserHelpers userHelpers;

    private boolean isInJar = Utils.isInJar();

    private static class ReaderErrorHandler extends ErrorHandler {
        @Override //!!! note that this gets called for missing pages, but not if exceptions are thrown; exceptions are handled separately
        public void handle(String target, Request request, HttpServletRequest httpServletRequest,
                HttpServletResponse httpServletResponse) throws IOException {
            request.setHandled(true);
            httpServletResponse.getWriter()
                    .println(String.format("<h1>Page doesn't exist: %s</h1>", request.getUri().getDecodedPath()));
        }
    }

    private static HashMap<String, String> PATH_MAPPING = new HashMap<>();
    static {
        PATH_MAPPING.put("", "home_page");
        PATH_MAPPING.put(PATH_LOGIN, "login");
        PATH_MAPPING.put(PATH_LOGOUT, "login"); // !!! after logout we get redirected to /login
        PATH_MAPPING.put(PATH_SIGNUP, "signup");
        PATH_MAPPING.put(PATH_ERROR, "error");
        PATH_MAPPING.put(PATH_FEED_ADMIN, "feed_admin");
        PATH_MAPPING.put(PATH_SETTINGS, "settings");
        PATH_MAPPING.put(PATH_FEEDS, "feeds");
        PATH_MAPPING.put(PATH_FEED + "/*", "feed");
        PATH_MAPPING.put(PATH_ADMIN, "admin");
    }

    public ReaderHandler(LowLevelDbAccess lowLevelDbAccess, String webDir) {

        loginInfoDb = new LoginInfo.DB(lowLevelDbAccess);
        userDb = new User.DB(lowLevelDbAccess);
        feedDb = new Feed.DB(lowLevelDbAccess);
        articleDb = new Article.DB(lowLevelDbAccess);
        readArticlesCollDb = new ReadArticlesColl.DB(lowLevelDbAccess);
        userHelpers = new UserHelpers(loginInfoDb, userDb);

        setContextPath("/");

        File warPath = new File(webDir);
        setWar(warPath.getAbsolutePath());

        if (isInJar) {
            for (Map.Entry<String, String> entry : PATH_MAPPING.entrySet()) {
                addPrebuiltJsp(entry.getKey(), "jsp." + entry.getValue().replaceAll("_", "_005f") + "_jsp");
            }
        } else {
            for (Map.Entry<String, String> entry : PATH_MAPPING.entrySet()) {
                addServlet(new ServletHolder(new RedirectServlet("/" + entry.getValue() + ".jsp")), entry.getKey());
            }
        }

        setErrorHandler(new ReaderErrorHandler());
    }

    private void addPrebuiltJsp(String path, String className) {
        try {
            Class clazz = Class.forName(className); //ttt2 see if possible to not use this, preferably without doing redirections like RedirectServlet
            Object obj = clazz.newInstance();
            addServlet(new ServletHolder((Servlet) obj), path);
            LOG.info("Added prebuilt JSP: " + obj.toString());
        } catch (Exception e) {
            LOG.fatal(String.format("Failed to load prebuilt JSP for %s and %s", path, className), e);
        }
    }

    @Override
    public void doHandle(String target, Request request, HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) throws IOException, ServletException {

        LOG.info("handling " + target);

        //!!! doHandle() is called twice for a request when using redirectiion, first time with request.getPathInfo()
        // set to the URI and target set to the path, then with request.getPathInfo() set to null and target set to the .jsp
        try {
            //request.setHandled(true);
            boolean secured;
            if (request.getScheme().equals("https")) {
                secured = true;
            } else if (request.getScheme().equals("http")) {
                secured = false;
            } else {
                httpServletResponse.getWriter().println(String.format("<h1>Unknown scheme %s at %s</h1>",
                        request.getScheme(), request.getUri().getDecodedPath()));
                return;
            }

            if (request.getMethod().equals("GET")) {
                if (isInJar || target.endsWith(".jsp")) {
                    // !!! when not in jar there's no need to do anything about params if it's not a .jsp,
                    // as this will get called again for the corresponding .jsp
                    if (prepareForJspGet(target, request, httpServletResponse, secured)) {
                        return;
                    }
                }
                if (target.startsWith(PATH_OPEN_ARTICLE)) {
                    handleOpenArticle(request, httpServletResponse, target);
                    return;
                }
                super.doHandle(target, request, httpServletRequest, httpServletResponse);
                LOG.info("handling of " + target + " went to super");

                //httpServletResponse.setDateHeader("Date", System.currentTimeMillis());     //ttt2 review these, probably not use
                //httpServletResponse.setDateHeader("Expires", System.currentTimeMillis() + 60000);

                return;
            }

            if (request.getMethod().equals("POST")) {
                if (request.getUri().getDecodedPath().equals(PATH_LOGIN)) {
                    handleLoginPost(request, httpServletResponse, secured);
                } else if (request.getUri().getDecodedPath().equals(PATH_SIGNUP)) {
                    handleSignupPost(request, httpServletResponse);
                } else if (request.getUri().getDecodedPath().equals(PATH_CHANGE_PASSWORD)) {
                    handleChangePasswordPost(request, httpServletResponse);
                } else if (request.getUri().getDecodedPath().equals(PATH_UPDATE_FEED_LIST)) {
                    handleUpdateFeedListPost(request, httpServletResponse);
                } else if (request.getUri().getDecodedPath().equals(PATH_ADD_FEED)) {
                    handleAddFeedPost(request, httpServletResponse);
                } else if (request.getUri().getDecodedPath().equals(PATH_REMOVE_FEED)) {
                    handleRemoveFeedPost(request, httpServletResponse);
                } else if (request.getUri().getDecodedPath().equals(PATH_CHANGE_SETTINGS)) {
                    handleChangeSettingsPost(request, httpServletResponse);
                }

            }

            /*{ // for tests only;
            httpServletResponse.getWriter().println(String.format("<h1>Unable to process request %s</h1>",
                    request.getUri().getDecodedPath()));
            request.setHandled(true);
            }*/
        } catch (Exception e) {
            LOG.error("Error processing request", e);
            try {
                //redirectToError(e.toString(), request, httpServletResponse); //!!! redirectToError leads to infinite loop, probably related to
                // the fact that we get 2 calls for a regular request when redirecting
                httpServletResponse.getWriter().println(String.format("<h1>Unable to process request %s</h1>", //ttt1 generate some HTML
                        request.getUri().getDecodedPath()));
                request.setHandled(true);
            } catch (Exception e1) {
                LOG.error("Error redirecting", e1);
            }
        }
    }

    /**
     * Normally sets the path and a few attributes that the JSPs are likely to need. Also verifies the login information. If necessary, just redirects to the
     * login page.
     *
     * @param target
     * @param request
     * @param httpServletResponse
     * @param secured
     * @return true if the request is already handled so the .jsp shouldn't get called
     * @throws Exception
     */
    private boolean prepareForJspGet(String target, Request request, HttpServletResponse httpServletResponse,
            boolean secured) throws Exception {

        LoginInfo.SessionInfo sessionInfo = UserHelpers.getSessionInfo(request);

        LOG.info(String.format("hndl - %s ; %s; %s ; %s", target, request.getPathInfo(), request.getMethod(),
                secured ? "secured" : "not secured"));

        String path = request.getUri().getDecodedPath();

        boolean redirectToLogin = path.equals(PATH_LOGOUT);
        LoginInfo loginInfo = null;
        if (sessionInfo.isNull()) {
            redirectToLogin = true;
            LOG.info("Null session info. Logging in again.");
        } else {
            loginInfo = loginInfoDb.get(sessionInfo.browserId, sessionInfo.sessionId); //ttt2 use a cache, to avoid going to DB
            if (loginInfo == null || loginInfo.expiresOn < System.currentTimeMillis()) {
                LOG.info("Session has expired. Logging in again. Info: " + loginInfo);
                redirectToLogin = true;
            }
        }

        if (!path.equals(PATH_LOGIN) && !path.equals(PATH_SIGNUP) && !path.equals(PATH_ERROR)) {

            if (redirectToLogin) {
                //ttt2 perhaps store URI, to return to it after login
                logOut(sessionInfo.browserId);
                addLoginParams(request, loginInfo);
                httpServletResponse.sendRedirect(PATH_LOGIN);
                return true;
            }

            User user = userDb.get(loginInfo.userId);
            if (user == null) {
                WebUtils.redirectToError("Unknown user", request, httpServletResponse);
                return true;
            }
            if (!user.active) {
                WebUtils.redirectToError("Account is not active", request, httpServletResponse);
                return true;
            }
            request.setAttribute(VAR_FEED_DB, feedDb);
            request.setAttribute(VAR_USER_DB, userDb);
            request.setAttribute(VAR_ARTICLE_DB, articleDb);
            request.setAttribute(VAR_READ_ARTICLES_COLL_DB, readArticlesCollDb);

            request.setAttribute(VAR_USER, user);
            request.setAttribute(VAR_LOGIN_INFO, loginInfo);

            MultiMap<String> params = new MultiMap<>();
            params.put(PARAM_PATH, path);
            request.setParameters(params);
        }

        if (path.equals(PATH_LOGIN)) {
            addLoginParams(request, loginInfo);
        }
        return false;
    }

    private void handleOpenArticle(Request request, HttpServletResponse httpServletResponse, String target)
            throws Exception {
        try {
            int k1 = target.indexOf('/', 1);
            int k2 = target.indexOf('/', k1 + 1);
            String feedId = target.substring(k1 + 1, k2);
            String strSeq = target.substring(k2 + 1);
            int seq = Integer.parseInt(strSeq);
            Article article = articleDb.get(feedId, seq);
            LoginInfo loginInfo = userHelpers.getLoginInfo(request);
            // ttt2 using the link from a non-authenticated browser causes a NPE; maybe do something better, e.g. sign up
            ReadArticlesColl readArticlesColl = readArticlesCollDb.get(loginInfo.userId, feedId);
            if (readArticlesColl == null) {
                readArticlesColl = new ReadArticlesColl(loginInfo.userId, feedId);
            }
            if (!readArticlesColl.isRead(seq)) {
                readArticlesColl.markRead(seq, Config.getConfig().maxSizeForReadArticles);
                readArticlesCollDb.add(readArticlesColl);
            }
            String s = URIUtil.encodePath(article.url).replace("%3F", "?").replace("%23", "#"); //ttt2 see how to do this right
            httpServletResponse.sendRedirect(s);
        } catch (Exception e) {
            WebUtils.showResult(String.format("Failed to get article for path %s. %s", target, e), "/", request,
                    httpServletResponse);
        }
    }

    private void handleSignupPost(Request request, HttpServletResponse httpServletResponse) throws Exception {
        String userId = request.getParameter(PARAM_USER_ID);
        String userName = request.getParameter(PARAM_USER_NAME);
        String email = request.getParameter(PARAM_EMAIL);
        String stringPassword = request.getParameter(PARAM_PASSWORD);
        String stringPasswordConfirm = request.getParameter(PARAM_PASSWORD_CONFIRM);

        if (!stringPassword.equals(stringPasswordConfirm)) {
            WebUtils.redirectToError("Mismatch between password and password confirmation", request,
                    httpServletResponse);
            return;
        }

        SecureRandom secureRandom = new SecureRandom();
        String salt = "" + secureRandom.nextLong();
        byte[] password = User.computeHashedPassword(stringPassword, salt);
        User user = userDb.get(userId);
        if (user != null) {
            WebUtils.redirectToError("There already exists a user with the ID " + userId, request,
                    httpServletResponse);
            return;
        }

        user = new User(userId, userName, password, salt, email, new ArrayList<String>(),
                Config.getConfig().activateAccountsAtCreation, false);
        //ttt2 add confirmation by email, captcha, ...
        List<String> fieldErrors = user.checkFields();
        if (!fieldErrors.isEmpty()) {
            StringBuilder bld = new StringBuilder("Invalid values when trying to create user with ID ")
                    .append(userId).append("<br/>");
            for (String s : fieldErrors) {
                bld.append(s).append("<br/>");
            }
            WebUtils.redirectToError(bld.toString(), request, httpServletResponse);
            return;
        }

        //ttt2 2 clients can add the same userId simultaneously
        userDb.add(user);

        httpServletResponse.sendRedirect("/");
    }

    private void handleChangePasswordPost(Request request, HttpServletResponse httpServletResponse)
            throws Exception {

        LoginInfo loginInfo = userHelpers.getLoginInfo(request);
        if (loginInfo == null) {
            WebUtils.redirectToError("Couldn't determine the current user", request, httpServletResponse);
            return;
        }

        String userId = loginInfo.userId;
        String stringCrtPassword = request.getParameter(PARAM_CURRENT_PASSWORD);
        String stringNewPassword = request.getParameter(PARAM_PASSWORD);
        String stringNewPasswordConfirm = request.getParameter(PARAM_PASSWORD_CONFIRM);

        if (!stringNewPassword.equals(stringNewPasswordConfirm)) {
            showResult("Mismatch between password and password confirmation", PATH_SETTINGS, request,
                    httpServletResponse);
            return;
        }

        User user = userDb.get(userId); // ttt1 crashes for wrong ID; 2013.07.20 - no longer have an idea what this is about
        if (user == null) {
            WebUtils.redirectToError("Couldn't find the current user", request, httpServletResponse);
            return;
        }

        if (!user.checkPassword(stringCrtPassword)) {
            showResult("Incorrect current password", PATH_SETTINGS, request, httpServletResponse);
            return;
        }

        SecureRandom secureRandom = new SecureRandom();
        String salt = "" + secureRandom.nextLong();
        byte[] password = User.computeHashedPassword(stringNewPassword, salt);
        user.salt = salt;
        user.password = password;

        //ttt3 2 clients can change the password simultaneously
        userDb.add(user);

        //httpServletResponse.sendRedirect(PATH_SETTINGS);
        showResult("Password changed", PATH_SETTINGS, request, httpServletResponse);
    }

    private void handleChangeSettingsPost(Request request, HttpServletResponse httpServletResponse)
            throws Exception {

        LoginInfo loginInfo = userHelpers.getLoginInfo(request);
        if (loginInfo == null) {
            WebUtils.redirectToError("Couldn't determine the current user", request, httpServletResponse);
            return;
        }

        String stringItemsPerPage = request.getParameter(PARAM_ITEMS_PER_PAGE);
        try {
            loginInfo.itemsPerPage = Integer.parseInt(stringItemsPerPage);
        } catch (Exception e) {
            showResult(
                    "Error trying to set the items per page. Expected integer value but got " + stringItemsPerPage,
                    PATH_SETTINGS, request, httpServletResponse);
            return;
        }
        loginInfo.style = request.getParameter(PARAM_STYLE);
        loginInfo.feedDateFormat = request.getParameter(PARAM_FEED_DATE_FORMAT); //ttt2 validate, better in JSP

        loginInfoDb.add(loginInfo);

        //httpServletResponse.sendRedirect(PATH_SETTINGS);
        showResult("Settings changed", "/", request, httpServletResponse);
    }

    private void handleUpdateFeedListPost(Request request, HttpServletResponse httpServletResponse)
            throws Exception {
        LOG.info("updating feed list"); //ttt2 implement
        httpServletResponse.sendRedirect(PATH_FEED_ADMIN);
    }

    private void handleAddFeedPost(Request request, HttpServletResponse httpServletResponse) throws Exception {
        LOG.info("adding feed");
        User user = userHelpers.getUser(request);

        try {
            if (user == null) {
                LOG.error("User not found");
                return;
            }

            String url = request.getParameter(PARAM_NEW_FEED_URL);
            //ttt1 add some validation; probably best try to actually get data, set the title, ...
            if (url == null || url.equals("")) {
                LOG.error("New feed not specified");
                //ttt1 show some error
                return;
            }

            MessageDigest digest = MessageDigest.getInstance("MD5");
            String feedId = PrintUtils.byteArrayAsUrlString(digest.digest(url.getBytes("UTF-8")));
            feedId = feedId.substring(0, Config.getConfig().feedIdSize);

            Feed feed = feedDb.get(feedId);
            if (feed == null) {
                feed = new Feed(feedId, url);
                feedDb.add(feed);
            }

            if (user.feedIds.contains(feedId)) {
                LOG.error(String.format("Trying to add existing feed %s to user %s", feedId, user));
            } else {
                user.feedIds.add(feedId);
                userDb.updateFeeds(user);
            }
        } finally {
            httpServletResponse.sendRedirect(PATH_FEED_ADMIN);
        }
    }

    private void handleRemoveFeedPost(Request request, HttpServletResponse httpServletResponse) throws Exception {
        LOG.info("removing feed");
        User user = userHelpers.getUser(request);

        try {
            if (user == null) {
                LOG.error("User not found");
                return;
            }

            String feedId = request.getParameter(PARAM_FEED_ID);

            LOG.info(String.format("Removing feed %s for user %s", feedId, user));

            //ttt1 add some validation; probably best try to actually get data, set the title, ...
            if (feedId == null || feedId.equals("")) {
                LOG.error("feed not specified");
                //ttt1 show some error
                return;
            }

            if (user.feedIds.remove(feedId)) {// ttt2 clean up the global feed table; that's probably better done if nobody accesses a feed for 3 months or so
                userDb.updateFeeds(user);
                LOG.info(String.format("Removed feed %s for user %s", feedId, user));
            } else {
                LOG.info(String.format("No feed found with ID %s for user %s", feedId, user));
            }
        } finally {
            httpServletResponse.sendRedirect(PATH_FEED_ADMIN);
        }
    }

    private void handleLoginPost(Request request, HttpServletResponse httpServletResponse, boolean secured)
            throws Exception {
        String userId = request.getParameter(PARAM_USER_ID);
        String password = request.getParameter(PARAM_PASSWORD);
        String rememberAccountStr = request.getParameter(PARAM_REMEMBER_ACCOUNT);
        boolean rememberAccount = Boolean.parseBoolean(rememberAccountStr);
        LoginInfo.SessionInfo sessionInfo = UserHelpers.getSessionInfo(request);

        logOut(sessionInfo.browserId);

        User user = userDb.get(userId);
        if (user == null) {
            WebUtils.redirectToError("User " + userId + " not found", request, httpServletResponse);
            return;
        }

        if (!user.checkPassword(password)) {
            WebUtils.redirectToError("Invalid password", request, httpServletResponse);
            return;
        }

        if (!user.active) {
            WebUtils.redirectToError("Account for User " + userId + " needs to be activated", request,
                    httpServletResponse);
            return;
        }

        LOG.info("Logged in user " + userId);

        sessionInfo.sessionId = null;
        if (sessionInfo.browserId == null) {
            sessionInfo.browserId = getRandomId();
        } else {
            for (LoginInfo loginInfo : loginInfoDb.getLoginsForBrowser(sessionInfo.browserId)) {
                if (userId.equals(loginInfo.userId)) {
                    sessionInfo.sessionId = loginInfo.sessionId;
                    break;
                }
            }
        }

        long expireOn = System.currentTimeMillis() + Config.getConfig().loginExpireInterval;
        if (sessionInfo.sessionId == null) {
            sessionInfo.sessionId = getRandomId();
            Config config = Config.getConfig();
            loginInfoDb.add(
                    new LoginInfo(sessionInfo.browserId, sessionInfo.sessionId, userId, expireOn, rememberAccount,
                            config.defaultStyle, config.defaultItemsPerPage, config.defaultFeedDateFormat));
            LOG.info(String.format("Logging in in a new session. User: %s", user));
        } else {
            loginInfoDb.updateExpireTime(sessionInfo.browserId, sessionInfo.sessionId, expireOn);
            LOG.info(String.format("Logging in in an existing session. User: %s", user));
        }

        WebUtils.saveCookies(httpServletResponse, secured, sessionInfo.browserId, sessionInfo.sessionId);

        httpServletResponse.sendRedirect("/");
    }

    private String getRandomId() {
        SecureRandom secureRandom = new SecureRandom();
        return "" + secureRandom.nextLong();
    }

    private void addLoginParams(Request request, LoginInfo loginInfo) {
        MultiMap<String> params = new MultiMap<>();
        if (loginInfo != null && loginInfo.rememberAccount) {
            params.put(PARAM_USER_ID, loginInfo.userId);
        }
        request.setParameters(params);
    }

    private void logOut(String browserId) throws Exception {
        //ttt2 the right way to do it is to go through all the sessions of the current browser, which would require a new field and a new index;
        // not sure if it's worth it, but this would work: A logs in, forgets to log out, B delets the cookies, logs in, A sees B is logged in, then B
        // restores the cookies and uses A's account
        if (browserId == null) {
            return;
        }

        List<LoginInfo> loginInfos = loginInfoDb.getLoginsForBrowser(browserId);
        long expireTarget = System.currentTimeMillis() - Utils.ONE_DAY;
        for (LoginInfo loginInfo : loginInfos) {
            if (loginInfo.expiresOn <= expireTarget) {
                LOG.info(String.format("LoginInfo %s is enough in the past", loginInfo));
            } else {
                LOG.info(String.format("Logging out: %s", loginInfo));
                loginInfoDb.updateExpireTime(browserId, loginInfo.sessionId, expireTarget);
            }
        }
    }

    public static class FeedInfo {
        public String feedId;
        public int maxSeq;

        public FeedInfo(String feedId, int maxSeq) {
            this.feedId = feedId;
            this.maxSeq = maxSeq;
        }
    }

    //!!! IDEA reports this as unused, but it is called from JSP
    public static FeedInfo getFeedInfo(String feedPath) {
        if (feedPath.startsWith(PATH_FEED + "/")) {
            try {
                if (feedPath.endsWith("/")) {
                    feedPath = feedPath.substring(0, feedPath.length() - 1);
                }
                int k = PATH_FEED.length() + 1;
                int p = feedPath.indexOf('/', k);
                return p >= 0 ? new FeedInfo(feedPath.substring(k, p), Integer.parseInt(feedPath.substring(p + 1)))
                        : new FeedInfo(feedPath.substring(k), -1);
            } catch (Exception e) {
                LOG.error("Exception trying to parse the feed info", e);
            }
        }

        LOG.error("Invalid path from feed: " + feedPath);
        return new FeedInfo("INVALID", -1);
    }

    //!!! IDEA reports this as unused, but it is called from JSP
    public static String getStyle(LoginInfo loginInfo) {
        StringBuilder bld = new StringBuilder();
        bld.append("<style media=\"screen\" type=\"text/css\">\n\n");
        if (loginInfo == null) {
            bld.append(Config.getConfig().defaultStyle);
        } else {
            bld.append(loginInfo.style); // ttt3 detect broken styles and return default
        }
        bld.append("</style>\n");
        return bld.toString();
    }

    /*    private void jspCodeCheck() throws Exception {
    Article.DB articleDb;
    Request request;
    String path = "";
        
    String feedId = ReaderHandler.getFeedId(path);
    int maxSeq = ReaderHandler.getSeq(path);
        
    Feed.DB feedDb = (Feed.DB)request.getAttribute(ReaderHandler.VAR_FEED_DB);
        
    Feed feed = feedDb.get(feedId);
    if (feed == null) {
        out.println("Feed " + feedId + " not found");
    } else {
        if (maxSeq == -1) {
            maxSeq = feed.maxSeq;
        }
        if (maxSeq < 0) {
            out.println("Feed " + feedId + " is empty");
        } else {
            ++maxSeq;
            LoginInfo loginInfo = (LoginInfo)request.getAttribute(ReaderHandler.VAR_LOGIN_INFO);
            int minSeq = Math.max(maxSeq - loginInfo.itemsPerPage, 0);
            List<Article> articles = articleDb.get(feedId, minSeq, maxSeq);
            for (Article article : articles) {
                out.println("<a href=\"" + article.url + "\">" + article.title + "</a><br/>");
            }
        }
    }
        
        }
        //*/
}

//ttt2 see how to submit secured login even when using HTTP

/*
    
todo
    
ttt2 log out everywhere
    
ttt0 more admin things (e.g. disable user, block feeds globally)
    
ttt1 see about page compression: http://serverfault.com/questions/279057/how-can-i-enable-gzip-compression-in-jetty
    
ttt1 see about compiling the JSPs
    
ttt0 see if possible to disable back button after logout
    
ttt0 encoding seems to be ISO-8859-1 when viewing page info in FF`
    
ttt1 save summary and render it with truncated text
    
ttt1 see if params are needed in html tag in JSP
    
 */