elw.web.ControllerElw.java Source code

Java tutorial

Introduction

Here is the source code for elw.web.ControllerElw.java

Source

/*
 * ELW : e-learning workspace
 * Copyright (C) 2010  Anton Kraievoy
 *
 * 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 3 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.  If not, see <http://www.gnu.org/licenses/>.
 */

package elw.web;

import base.pattern.Result;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.common.io.InputSupplier;
import elw.dao.*;
import elw.dao.Ctx;
import elw.miniweb.Message;
import elw.vo.*;
import elw.vo.Class;
import elw.web.core.Core;
import elw.web.core.W;
import elw.webauth.ControllerAuth;
import org.akraievoy.base.Parse;
import org.akraievoy.couch.Squab;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import static elw.dao.Auth.SESSION_KEY;

public abstract class ControllerElw extends MultiActionController implements WebSymbols {
    private static final Logger log = LoggerFactory.getLogger(ControllerElw.class);

    private static final DiskFileItemFactory fileItemFactory = createFileItemFactory();

    protected final Core core;
    protected final ElwServerConfig elwServerConfig;

    protected ControllerElw(final Core core, final ElwServerConfig elwServerConfig) {
        this.core = core;
        this.elwServerConfig = elwServerConfig;
    }

    private static DiskFileItemFactory createFileItemFactory() {
        DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
        fileItemFactory.setRepository(new java.io.File(System.getProperty("java.io.tmpdir")));
        fileItemFactory.setSizeThreshold(2 * 1024 * 1024);

        return fileItemFactory;
    }

    //  LATER move to ControllerAuth (required direct instance-level call dispatch)
    public HashMap<String, Object> noAuth(HttpServletRequest req, HttpServletResponse resp, boolean page,
            final String message) throws IOException {
        if (page) {
            Message.addWarn(req, message);
            ControllerAuth.storeSuccessRedirect(req);
            resp.sendRedirect(elwServerConfig.getBaseUrl() + "auth.html");
        } else {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN, message);
        }

        return null;
    }

    //  TODO abstract away from returning concrete hashmap class
    protected HashMap<String, Object> prepareDefaultModel(final HttpServletRequest req, final Auth auth,
            final Ctx ctx) {
        final HashMap<String, Object> model = new HashMap<String, Object>();

        model.put(FormatTool.MODEL_KEY, FormatTool.forLocale(RequestContextUtils.getLocale(req)));
        model.put(VelocityTemplates.MODEL_KEY, core.getTemplates());
        model.put(ElwUri.MODEL_KEY, core.getUri());
        model.put(Auth.MODEL_KEY, auth);
        model.put(QueriesSecure.MODEL_KEY, core.getQueries().secure(auth));

        return model;
    }

    protected static Auth auth(HttpServletRequest req) {
        return (Auth) req.getSession(true).getAttribute(SESSION_KEY);
    }

    protected static Auth auth(Map<String, Object> model) {
        return (Auth) model.get(Auth.MODEL_KEY);
    }

    protected static QueriesSecure queries(Map<String, Object> model) {
        return (QueriesSecure) model.get(QueriesSecure.MODEL_KEY);
    }

    //  TODO better to avoid returning nulls and throw a specific exception
    protected HashMap<String, Object> auth(final HttpServletRequest req, final HttpServletResponse resp,
            final boolean page, final boolean verified) throws IOException {
        final HttpSession session = req.getSession(true);
        final Auth auth = (Auth) session.getAttribute(SESSION_KEY);

        if (auth == null) {
            return noAuth(req, resp, page, "Auth required");
        }

        if (!W.resolveRemoteAddress(req).equals(auth.getSourceAddr())) {
            return noAuth(req, resp, page, "Source address changed");
        }

        auth.renew(core.getQueries());

        if (auth.isEmpty()) {
            return noAuth(req, resp, page, "Non-empty Auth required");
        }

        final String extraValidationMessage = extraAuthValidations(auth);

        if (!Strings.isNullOrEmpty(extraValidationMessage)) {
            return noAuth(req, resp, page, extraValidationMessage);
        }

        if (verified && !auth.isVerified()) {
            return noAuth(req, resp, page, "Verified Auth required");
        }

        session.removeAttribute(ControllerAuth.SESSION_SUCCESS_REDIRECT);

        return prepareDefaultModel(req, auth, null);
    }

    protected String extraAuthValidations(Auth auth) {
        return null;
    }

    protected static interface WebMethodScore {
        ModelAndView handleScore(HttpServletRequest req, HttpServletResponse resp, Ctx ctx, FileSlot slot,
                Solution file, Long stamp, Map<String, Object> model) throws IOException;
    }

    //  FIXME some of file VT parameters are broken
    protected ModelAndView wmScore(final HttpServletRequest req, final HttpServletResponse resp, final boolean page,
            final boolean verified, final WebMethodScore wm) throws IOException {
        final HashMap<String, Object> model = auth(req, resp, page, verified);
        if (model == null) {
            return null;
        }

        final Ctx ctx = (Ctx) model.get(R_CTX);
        if (!ctx.resolved(Ctx.STATE_EGSCIV)) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Path problem, please check the logs");
            return null;
        }

        final String slotId = req.getParameter("sId");
        if ((slotId == null || slotId.trim().length() == 0)) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no slotId (sId) defined");
            return null;
        }

        final FileSlot slot = ctx.getAssType().getFileSlots().get(slotId);
        if (slot == null) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "slot for id " + slotId + " not found");
            return null;
        }

        final String fileId = req.getParameter("fId");
        if ((fileId == null || fileId.trim().length() == 0)) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no fileId (fId) defined");
            return null;
        }

        final Solution file = core.getQueries().solution(ctx.ctxSlot(slot), fileId);
        if (file == null) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "file for id " + fileId + " not found");
            return null;
        }

        final Long stamp = Parse.oneLong(req.getParameter("stamp"), null);

        return wm.handleScore(req, resp, ctx, slot, file, stamp, model);
    }

    protected static abstract class WebMethodCtx {
        protected HttpServletRequest req;
        protected HttpServletResponse resp;
        protected Ctx ctx;
        protected Map<String, Object> model;

        protected void init(HttpServletRequest req, HttpServletResponse resp, Ctx ctx, Map<String, Object> model) {
            this.ctx = ctx;
            this.model = model;
            this.req = req;
            this.resp = resp;
        }

        public abstract ModelAndView handleCtx() throws IOException;
    }

    protected ModelAndView wmECG(HttpServletRequest req, HttpServletResponse resp, final boolean page,
            final boolean verified, final WebMethodCtx wm) throws IOException {
        return wm(req, resp, Ctx.STATE_ECG, wm, page, verified);
    }

    private ModelAndView wm(final HttpServletRequest req, final HttpServletResponse resp,
            final String ctxResolveState, final WebMethodCtx wm, final boolean page, final boolean verified)
            throws IOException {
        final HashMap<String, Object> model = auth(req, resp, page, verified);
        if (model == null) {
            return null;
        }

        final Ctx ctx = (Ctx) model.get(R_CTX);
        if (!ctx.resolved(ctxResolveState)) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Path problem, please check the logs");
            return null;
        }

        wm.init(req, resp, ctx, model);
        return wm.handleCtx();
    }

    protected ModelAndView wmFile(final HttpServletRequest req, final HttpServletResponse resp,
            final String scopeForced, final boolean page, final boolean verified, final WebMethodFile wm)
            throws IOException {
        final HashMap<String, Object> model = auth(req, resp, page, verified);
        if (model == null) {
            return null;
        }

        final Ctx ctx = (Ctx) model.get(R_CTX);

        wm.init(req, resp, ctx, model);
        wm.setScopeForced(scopeForced);

        return wm.handleCtx();
    }

    protected static abstract class WebMethodFile extends WebMethodCtx {
        protected String scopeForced = null;

        private void setScopeForced(String scopeForced) {
            this.scopeForced = scopeForced;
        }

        public ModelAndView handleCtx() throws IOException {
            final String scope = scopeForced == null ? req.getParameter("s") : scopeForced;
            if (scope == null) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "scope not set");
                return null;
            }

            if (Attachment.SCOPE.equals(scope)) {
                if (!ctx.resolved(Ctx.STATE_CTAV)) {
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
                            "context path problem, please check the logs");
                    return null;
                }
            } else if (Solution.SCOPE.equals(scope)) {
                if (!ctx.resolved(Ctx.STATE_EGSCIV)) {
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
                            "context path problem, please check the logs");
                    return null;
                }
            } else {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "bad scope: " + scope);
                return null;
            }

            final String slotId = req.getParameter("sId");
            if (slotId == null || slotId.trim().length() == 0) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no slotId (sId) defined");
                return null;
            }

            final FileSlot slot = ctx.getAssType().getFileSlots().get(slotId);
            if (slot == null) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "slot '" + slotId + "' not found");
                return null;
            }

            return handleFile(scope, slot);
        }

        protected abstract ModelAndView handleFile(String scope, FileSlot slot) throws IOException;

        protected void retrieveFile(FileBase fileBase, FileSlot slot, final Queries queries) throws IOException {
            final InputSupplier<InputStream> fileInput;
            if (fileBase instanceof Solution) {
                fileInput = queries.solutionInput(ctx.ctxSlot(slot).solution((Solution) fileBase),
                        FileBase.CONTENT);
            } else if (fileBase instanceof Attachment) {
                fileInput = queries.attachmentInput(ctx.ctxSlot(slot).attachment((Attachment) fileBase),
                        FileBase.CONTENT);
            } else {
                throw new IllegalStateException("not supported fileBase instance: " + fileBase);
            }

            storeContentHeaders(fileBase, resp);
            ByteStreams.copy(fileInput, resp.getOutputStream());
        }

        public ModelAndView storeFile(FileSlot slot, String refreshUri, String failureUri, String authorName,
                Queries queries, final FileBase file) throws IOException {
            //  FIXME upload page should contain full listing of validation rules
            final SortedMap<String, FileType> allTypes = slot.getFileTypes();
            final SortedMap<String, FileType> validTypes = new TreeMap<String, FileType>(allTypes);
            final boolean put = "PUT".equalsIgnoreCase(req.getMethod());
            final int length = req.getContentLength();
            final String stamp = Long.toString(Squab.Stamped.genStamp(), 36);
            file.setId(stamp);
            file.setAuthor(authorName);

            InputSupplier<? extends InputStream> inputSupplier = null;
            String contentType = null;
            if (put) {
                //  FIXME remove this extra variant with non-encoded put uploads
                inputSupplier = supplierForRequest(req);
                contentType = "text/plain";

                file.setName("upload_" + stamp + ".txt");
            } else {
                try {
                    final ServletFileUpload sfu = new ServletFileUpload(fileItemFactory);
                    final FileItemIterator fii = sfu.getItemIterator(req);
                    while (fii.hasNext()) {
                        final FileItemStream item = fii.next();
                        if (item.isFormField()) {
                            final String fieldName = item.getFieldName();
                            if ("comment".equals(fieldName)) {
                                file.setComment(fieldText(item));
                            }
                            if (auth(req).getAdmin() != null) {
                                if ("sourceAddr".equals(fieldName)) {
                                    final String sourceAddr = fieldText(item);
                                    if (!Strings.isNullOrEmpty(sourceAddr)) {
                                        file.setSourceAddress(sourceAddr);
                                    }
                                } else if ("dateTime".equals(fieldName)) {
                                    final String dateTime = fieldText(item);
                                    if (!Strings.isNullOrEmpty(dateTime)) {
                                        final String[] parts = dateTime.trim().split("\\s+");

                                        final String date = parts[0];
                                        final String time = parts.length > 1 ? parts[1] : "11:00";

                                        final DateTime instant = Class.parseDateTime(date, time);

                                        file.setStamp(instant.getMillis());
                                    }
                                }
                            }
                            continue;
                        }

                        file.setName(Strings.nullToEmpty(extractNameFromPath(item)));
                        contentType = item.getContentType();
                        inputSupplier = supplierForFileItem(item);
                        //  TODO find length of this particular item, if implied by protocol
                        break;
                    }
                } catch (FileUploadException e) {
                    throw new IOException(e);
                }
                if (inputSupplier == null) {
                    return fail(put, failureUri, "File being uploaded not found in the form");
                }
                if (contentType == null) {
                    return fail(put, failureUri, "Content Type not reported in upload");
                }
            }

            if (Strings.isNullOrEmpty(file.getSourceAddress())) {
                file.setSourceAddress(W.resolveRemoteAddress(req));
            }
            if (length == -1) {
                final String message = "Upload size not reported";
                return fail(put, failureUri, message);
            }
            FileType._.filterByLength(validTypes, length);
            if (validTypes.isEmpty()) {
                return fail(put, failureUri,
                        "Size " + org.akraievoy.base.Format.formatMem(length) + " exceeds size limits");
            }
            FileType._.filterByName(validTypes, file.getName().toLowerCase());
            if (validTypes.isEmpty()) {
                return fail(put, failureUri, "File name failed all regex checks");
            }
            if (validTypes.size() > 1) {
                Message.addWarn(req, "More than one valid file type left: " + validTypes.keySet());
            }
            final FileType fileType = validTypes.get(validTypes.firstKey());
            if (!fileType.getContentTypes().contains(Strings.nullToEmpty(contentType))) {
                Message.addWarn(req, "contentType '" + contentType + "': not listed in the file type");
            }
            file.setFileType(IdNamed._.singleton(fileType));
            if (!fileType.isBinary()) {
                if (length > FileBase.DETECT_SIZE_LIMIT) {
                    return fail(put, failureUri, "Non-binary file is too big for content check");
                }
                final byte[] bytes = ByteStreams.toByteArray(inputSupplier);
                //   implemented as per this SO answer:
                //   http://stackoverflow.com/questions/277521/identify-file-binary/277568#277568
                for (byte b : bytes) {
                    if (b >= 0 && b < 9 || b > 13 && b < 32) {
                        return fail(put, failureUri, "Non-binary file contains binary data");
                    }
                }
                inputSupplier = ByteStreams.newInputStreamSupplier(bytes);
            }

            //  TODO validate headers and binary content here

            final Result result = queries.createFile(ctx, slot, file, inputSupplier, contentType);
            if (result.isSuccess()) {
                return succeed(put, refreshUri, result);
            } else {
                return fail(put, failureUri, result.getMessage());
            }
        }

        protected ModelAndView succeed(boolean put, String refreshUri, Result result) throws IOException {
            if (!put) {
                Message.addResult(req, result);
                resp.sendRedirect(refreshUri);
            }
            return null;
        }

        protected ModelAndView fail(boolean put, String failureUri, String message) throws IOException {
            if (put) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, message);
            } else {
                Message.addWarn(req, message);
                resp.sendRedirect(failureUri);
            }
            return null;
        }

    }

    public static void storeContentHeaders(final FileBase fileBase, final HttpServletResponse resp) {
        final FileType fileType = IdNamed._.one(fileBase.getFileType());
        final String contentType;
        if (!fileType.getContentTypes().isEmpty()) {
            contentType = fileType.getContentTypes().get(0);
        } else {
            contentType = fileType.isBinary() ? "binary/octet-stream" : "text/plain";
        }

        if (fileType.isBinary()) {
            resp.setContentType(contentType);
        } else {
            resp.setContentType(contentType + "; charset=UTF-8");
            resp.setCharacterEncoding("UTF-8");
        }

        resp.setContentLength(fileBase.getCouchFile(FileBase.CONTENT).getLength().intValue());
        resp.setHeader("Content-Disposition", "attachment;");
    }

    protected static String fieldText(final FileItemStream item) throws IOException {
        return CharStreams.toString(CharStreams.newReaderSupplier(supplierForFileItem(item), Charsets.UTF_8));
    }

    protected static String extractNameFromPath(FileItemStream item) {
        final String name = item.getName();
        if (name == null) {
            return null;
        }

        final int lastSlash = Math.max(name.lastIndexOf("\\"), name.lastIndexOf("/"));
        final String fName = lastSlash >= 0 ? name.substring(lastSlash + 1) : name;

        return fName;
    }

    protected static InputSupplier<InputStream> supplierForRequest(final HttpServletRequest myReq) {
        return new InputSupplier<InputStream>() {
            public InputStream getInput() throws IOException {
                return myReq.getInputStream();
            }
        };
    }

    protected static InputSupplier<InputStream> supplierForFileItem(final FileItemStream item) {
        return new InputSupplier<InputStream>() {
            public InputStream getInput() throws IOException {
                return item.openStream();
            }
        };
    }
}