net.hillsdon.reviki.web.pages.impl.DefaultPageImpl.java Source code

Java tutorial

Introduction

Here is the source code for net.hillsdon.reviki.web.pages.impl.DefaultPageImpl.java

Source

/**
 * Copyright 2008 Matthew Hillsdon
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.hillsdon.reviki.web.pages.impl;

import static net.hillsdon.reviki.web.common.RequestParameterReaders.getLong;
import static net.hillsdon.reviki.web.common.RequestParameterReaders.getRequiredString;
import static net.hillsdon.reviki.web.common.RequestParameterReaders.getString;
import static net.hillsdon.reviki.web.common.ViewTypeConstants.CTYPE_ATOM;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.hillsdon.fij.text.Strings;
import net.hillsdon.reviki.configuration.WikiConfiguration;
import net.hillsdon.reviki.search.QuerySyntaxException;
import net.hillsdon.reviki.vc.AttachmentHistory;
import net.hillsdon.reviki.vc.ChangeInfo;
import net.hillsdon.reviki.vc.ConflictException;
import net.hillsdon.reviki.vc.ContentTypedSink;
import net.hillsdon.reviki.vc.LostLockException;
import net.hillsdon.reviki.vc.NotFoundException;
import net.hillsdon.reviki.vc.PageInfo;
import net.hillsdon.reviki.vc.PageReference;
import net.hillsdon.reviki.vc.PageStoreAuthenticationException;
import net.hillsdon.reviki.vc.PageStoreException;
import net.hillsdon.reviki.vc.RenameException;
import net.hillsdon.reviki.vc.VersionedPageInfo;
import net.hillsdon.reviki.vc.impl.CachingPageStore;
import net.hillsdon.reviki.vc.impl.PageInfoImpl;
import net.hillsdon.reviki.vc.impl.PageReferenceImpl;
import net.hillsdon.reviki.vc.impl.PageRevisionReference;
import net.hillsdon.reviki.web.common.ConsumedPath;
import net.hillsdon.reviki.web.common.InvalidInputException;
import net.hillsdon.reviki.web.common.JspView;
import net.hillsdon.reviki.web.common.RequestAttributes;
import net.hillsdon.reviki.web.common.RequestParameterReaders;
import net.hillsdon.reviki.web.common.View;
import net.hillsdon.reviki.web.common.ViewTypeConstants;
import net.hillsdon.reviki.web.handlers.StreamView;
import net.hillsdon.reviki.web.pages.DefaultPage;
import net.hillsdon.reviki.web.pages.DiffGenerator;
import net.hillsdon.reviki.web.redirect.RedirectToPageView;
import net.hillsdon.reviki.web.urls.UnknownWikiException;
import net.hillsdon.reviki.web.urls.WikiUrls;
import net.hillsdon.reviki.web.urls.impl.ResponseSessionURLOutputFilter;
import net.hillsdon.reviki.wiki.MarkupRenderer;
import net.hillsdon.reviki.wiki.feeds.FeedWriter;
import net.hillsdon.reviki.wiki.graph.WikiGraph;
import net.hillsdon.reviki.wiki.renderer.RendererRegistry;
import net.hillsdon.reviki.wiki.renderer.creole.LinkPartsHandler;
import net.hillsdon.reviki.wiki.renderer.creole.ast.ASTNode;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;

import com.google.common.base.Predicate;
import com.google.common.collect.Maps;

public class DefaultPageImpl implements DefaultPage {

    public static final String ATTR_PREVIEW = "preview";

    public static final String SUBMIT_DELETE = "delete";

    public static final String SUBMIT_SAVE = "save";

    public static final String SUBMIT_COPY = "copy";

    public static final String SUBMIT_RENAME = "rename";

    public static final String SUBMIT_UNLOCK = "unlock";

    public static final String SUBMIT_PREVIEW = ATTR_PREVIEW;

    public static final String PARAM_TO_PAGE = "toPage";

    public static final String PARAM_FROM_PAGE = "fromPage";

    public static final String PARAM_CONTENT = "content";

    public static final String PARAM_ATTRIBUTES = "attributes";

    public static final String PARAM_ORIGINAL_ATTRIBUTES = "originalAttrs";

    public static final String PARAM_BASE_REVISION = "baseRevision";

    public static final String PARAM_LOCK_TOKEN = "lockToken";

    public static final String PARAM_COMMIT_MESSAGE = "description";

    public static final String PARAM_MINOR_EDIT = "minorEdit";

    public static final String PARAM_DIFF_REVISION = "diff";

    public static final String PARAM_REVISION = "revision";

    public static final String PARAM_ATTACHMENT_NAME = "attachmentName";

    public static final String PARAM_ATTACHMENT_MESSAGE = "attachmentMessage";

    public static final String PARAM_SESSION_ID = "sessionId";

    public static final String ATTR_PAGE_INFO = "pageInfo";

    public static final String ATTR_ORIGINAL_ATTRIBUTES = "originalAttributes";

    public static final String ATTR_BACKLINKS = "backlinks";

    public static final String ATTR_BACKLINKS_LIMITED = "backlinksLimited";

    public static final String ATTR_RENDERED_CONTENTS = "renderedContents";

    public static final String ATTR_MARKED_UP_DIFF = "markedUpDiff";

    public static final String ATTR_DIFF_START_REV = "diffStartRev";

    public static final String ATTR_DIFF_END_REV = "diffEndRev";

    /**
     * Because the diff view may be looking at renamed pages.
     */
    public static final String ATTR_DIFF_TITLE = "diffTitle";

    public static final String ATTR_SHOW_REV = "showHeadRev";

    public static final String ATTR_SESSION_ID = "sessionId";

    public static final String ERROR_NO_FILE = "Please browse to a non-empty file to upload.";

    public static final String ERROR_SESSION_EXPIRED = "Your session has expired. Please try again.";

    public static final int MAX_NUMBER_OF_BACKLINKS_TO_DISPLAY = 15;

    private static final String ATTR_FROM_PAGE = "fromPage";

    private final CachingPageStore _store;

    private final RendererRegistry _renderers;

    private final WikiGraph _graph;

    private final DiffGenerator _diffGenerator;

    private final WikiUrls _wikiUrls;

    private final FeedWriter _feedWriter;

    private final WikiConfiguration _configuration;

    public DefaultPageImpl(final WikiConfiguration configuration, final CachingPageStore store,
            final RendererRegistry renderers, final WikiGraph graph, final DiffGenerator diffGenerator,
            final WikiUrls wikiUrls, final FeedWriter feedWriter) {
        _configuration = configuration;
        _store = store;
        _graph = graph;
        _diffGenerator = diffGenerator;
        _wikiUrls = wikiUrls;
        _feedWriter = feedWriter;
        _renderers = renderers;
    }

    public View attach(final PageReference page, final ConsumedPath path, final HttpServletRequest request,
            final HttpServletResponse response) throws Exception {
        if (!ServletFileUpload.isMultipartContent(request)) {
            throw new InvalidInputException("multipart request expected.");
        }
        List<FileItem> items = getFileItems(request);
        try {
            if (items.size() > 4) {
                throw new InvalidInputException("One file at a time.");
            }
            String attachmentName = null;
            Long baseRevision = null;
            String attachmentMessage = null;
            FileItem file = null;
            for (FileItem item : items) {
                if (PARAM_ATTACHMENT_NAME.equals(item.getFieldName())) {
                    attachmentName = item.getString().trim();
                }
                if (PARAM_BASE_REVISION.equals(item.getFieldName())) {
                    baseRevision = RequestParameterReaders.getLong(item.getString().trim(), PARAM_BASE_REVISION);
                }
                if (PARAM_ATTACHMENT_MESSAGE.equals(item.getFieldName())) {
                    attachmentMessage = item.getString().trim();
                    if (attachmentMessage.length() == 0) {
                        attachmentMessage = null;
                    }
                }
                if (!item.isFormField()) {
                    file = item;
                }
            }
            if (baseRevision == null) {
                baseRevision = VersionedPageInfo.UNCOMMITTED;
            }

            if (file == null || file.getSize() == 0) {
                request.setAttribute("flash", ERROR_NO_FILE);
                return attachments(page, path, request, response);
            } else {
                InputStream in = file.getInputStream();
                try {
                    // get the page from store - needed to get content of the special
                    // pages which were not saved yet
                    PageInfo pageInfo = _store.get(page, -1);
                    // IE sends the full file path.
                    storeAttachment(pageInfo, attachmentName, baseRevision, attachmentMessage,
                            FilenameUtils.getName(file.getName()), in);
                    _store.expire(pageInfo);
                    return new RedirectToPageView(_wikiUrls, page, "/attachments/");
                } finally {
                    IOUtils.closeQuietly(in);
                }
            }
        } finally {
            for (FileItem item : items) {
                item.delete();
            }
        }
    }

    @SuppressWarnings("unchecked")
    // commons-upload...
    private List<FileItem> getFileItems(final HttpServletRequest request) throws FileUploadException {
        FileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(factory);
        List<FileItem> items = upload.parseRequest(request);
        return items;
    }

    private void storeAttachment(final PageInfo page, final String attachmentName, final long baseRevision,
            final String attachmentMessage, final String fileName, final InputStream in) throws PageStoreException {
        String storeName = attachmentName;
        if (storeName == null || storeName.length() == 0) {
            storeName = fileName;
        } else if (storeName.indexOf('.') == -1) {
            final int indexOfDotInFileName = fileName.indexOf('.');
            if (indexOfDotInFileName != -1) {
                storeName += fileName.substring(indexOfDotInFileName);
            }
        }
        String operation = baseRevision < 0 ? "Added" : "Updated";
        final String message = attachmentMessage == null ? operation + " attachment " + storeName
                : attachmentMessage;
        _store.attach(page, storeName, baseRevision, in, message);
    }

    public View deleteAttachment(final PageReference page, final ConsumedPath path,
            final HttpServletRequest request, final HttpServletResponse response)
            throws PageStoreAuthenticationException, PageStoreException {
        final String attachmentName = path.next();
        final long baseRevision = -1;
        _store.deleteAttachment(page, attachmentName, baseRevision, "Deleted attachment");
        return new RedirectToPageView(_wikiUrls, page, "/attachments/");
    }

    public View attachment(final PageReference page, final ConsumedPath path, final HttpServletRequest request,
            final HttpServletResponse response) throws Exception {
        final String attachmentName = path.next();
        return new View() {
            public void render(final HttpServletRequest request, final HttpServletResponse response)
                    throws IOException, ServletException, NotFoundException, PageStoreException,
                    InvalidInputException {
                _store.attachment(page, attachmentName, getRevision(request), new ContentTypedSink() {
                    public void setContentType(final String contentType) {
                        response.setContentType(contentType);
                    }

                    public void setFileName(final String name) {
                        final String quoteEscapedAttachmentName = attachmentName.replace("\\", "\\\\").replace("\"",
                                "\\\"");
                        response.setHeader("Content-Disposition",
                                "inline; filename=\"" + quoteEscapedAttachmentName + "\"");
                    }

                    public OutputStream stream() throws IOException {
                        return response.getOutputStream();
                    }
                });
            }
        };
    }

    public View attachments(final PageReference page, final ConsumedPath path, final HttpServletRequest request,
            final HttpServletResponse response) throws Exception {
        Collection<AttachmentHistory> allAttachments = _store.attachments(page);
        Collection<AttachmentHistory> currentAttachments = new LinkedList<AttachmentHistory>();
        Collection<AttachmentHistory> deletedAttachments = new LinkedList<AttachmentHistory>();
        for (AttachmentHistory history : allAttachments) {
            if (history.isAttachmentDeleted()) {
                deletedAttachments.add(history);
            } else {
                currentAttachments.add(history);
            }
        }
        request.setAttribute("currentAttachments", currentAttachments);
        request.setAttribute("deletedAttachments", deletedAttachments);
        return new JspView("Attachments");
    }

    private boolean isLockTokenValid(final VersionedPageInfo pageInfo, final HttpServletRequest request,
            final boolean preview) throws InvalidInputException {
        final String username = (String) request.getAttribute(RequestAttributes.USERNAME);
        return pageInfo.isNewOrLockedByUser(username)
                && (!preview || pageInfo.isNewPage() || lockTokenMatches(pageInfo, request));
    }

    private boolean lockTokenMatches(final VersionedPageInfo pageInfo, final HttpServletRequest request)
            throws InvalidInputException {
        return pageInfo.getLockToken().equals(getRequiredString(request, PARAM_LOCK_TOKEN));
    }

    private boolean isSessionIdValid(final HttpServletRequest request) {
        final String postedSessionId = request.getParameter(PARAM_SESSION_ID);
        final String requestedSessionId = request.getRequestedSessionId();
        return requestedSessionId != null && postedSessionId != null && postedSessionId.equals(requestedSessionId)
                && request.isRequestedSessionIdValid();
    }

    public View editor(final PageReference page, final ConsumedPath path, final HttpServletRequest request,
            final HttpServletResponse response) throws Exception {
        final boolean preview = request.getParameter(SUBMIT_PREVIEW) != null;
        VersionedPageInfo pageInfo = _store.getUnderlying().tryToLock(page);
        request.setAttribute(ATTR_PAGE_INFO, pageInfo);
        request.setAttribute(ATTR_ORIGINAL_ATTRIBUTES, pageInfo.getAttributes());
        copySessionIdAsAttribute(request);
        if (!isLockTokenValid(pageInfo, request, preview)) {
            if (preview) {
                return diffEditorView(page, null, request);
            } else {
                request.setAttribute("flash", "Could not lock the page.");
                return new JspView("ViewPage");
            }
        } else {
            if (preview) {
                if (!isSessionIdValid(request)) {
                    return diffEditorView(page, ERROR_SESSION_EXPIRED, request);
                } else {
                    final String oldContent = pageInfo.getContent();
                    final String newContent = getContent(request);
                    pageInfo = pageInfo.withAlternativeContent(newContent);
                    pageInfo = pageInfo.withAlternativeAttributes(
                            Maps.filterValues(getAttributes(request), new Predicate<String>() {
                                public boolean apply(final String value) {
                                    if (value == null) {
                                        return false;
                                    }
                                    return true;
                                }
                            }));
                    request.setAttribute(ATTR_PAGE_INFO, pageInfo);
                    String rendered = _renderers.getDefaultRenderer()
                            .render(pageInfo, new ResponseSessionURLOutputFilter(request, response)).get();
                    request.setAttribute(ATTR_PREVIEW, rendered);
                    request.setAttribute(ATTR_MARKED_UP_DIFF, _diffGenerator.getDiffMarkup(oldContent, newContent));
                }
            }
            return new JspView("EditPage");
        }
    }

    private void copySessionIdAsAttribute(final HttpServletRequest request) {
        request.setAttribute(ATTR_SESSION_ID, request.getSession().getId());
    }

    public View get(final PageReference page, final ConsumedPath path, final HttpServletRequest request,
            final HttpServletResponse response) throws Exception {
        final String revisionParam = request.getParameter(PARAM_REVISION);
        final String diffParam = request.getParameter(PARAM_DIFF_REVISION);
        final String ctypeParam = request.getParameter(ViewTypeConstants.PARAM_CTYPE);

        addBacklinksInformation(request, page);
        final VersionedPageInfo main = pageInfoFromRevisionParam(page, revisionParam, PARAM_REVISION);
        request.setAttribute(ATTR_PAGE_INFO, main);
        request.setAttribute(ATTR_SHOW_REV, revisionParam != null);
        if (diffParam != null) {
            request.setAttribute(ATTR_DIFF_TITLE, page.getPath());
            final VersionedPageInfo compare = pageInfoFromRevisionParam(page, diffParam, PARAM_DIFF_REVISION);
            request.setAttribute(ATTR_DIFF_END_REV, revisionText(main.getRevision()));
            request.setAttribute(ATTR_DIFF_START_REV, revisionText(compare.getRevision()));
            request.setAttribute(ATTR_MARKED_UP_DIFF,
                    _diffGenerator.getDiffMarkup(compare.getContent(), main.getContent()));
            if (compare.getRevision() > main.getRevision() && main.getRevision() > -1) {
                request.setAttribute("flash", "Note this diff is reversed.");
            }
            return new JspView("ViewDiff");
        } else if (_renderers.hasRenderer(ctypeParam)) {
            MarkupRenderer<InputStream> renderer = _renderers.getRenderer(ctypeParam);
            InputStream stream = renderer.render(main, new ResponseSessionURLOutputFilter(request, response)).get();
            return new StreamView(renderer.getContentType(), stream);
        } else {
            ASTNode ast = _renderers.getDefaultRenderer().parse(main);
            String rendered = _renderers.getDefaultRenderer().render(ast,
                    new ResponseSessionURLOutputFilter(request, response));
            request.setAttribute(ATTR_RENDERED_CONTENTS, rendered);
            if (main.isRenamed()) {
                LinkPartsHandler linkPartsHandler = _renderers.getDefaultRenderer().getLinkPartsHandler();
                try {
                    request.setAttribute("renamedUrl", main.getRenamedUrl(linkPartsHandler.getContext()));
                } catch (UnknownWikiException ex) {
                    // Page was renamed to a page that doesn't exist in a configured wiki
                }
            }
            return new JspView("ViewPage");
        }
    }

    private String revisionText(final long revision) {
        if (revision < 0 && revision > Integer.MIN_VALUE) {
            switch ((int) revision) {
            case -1:
                return "[latest]";
            case -2:
                return "[uncommitted]";
            case -3:
                return "[deleted]";
            case -4:
                return "[moved]";
            }
        }
        return "r" + revision;
    }

    private void addBacklinksInformation(final HttpServletRequest request, final PageReference page)
            throws IOException, QuerySyntaxException, PageStoreException {
        List<String> pageNames = new ArrayList<String>(_graph.incomingLinks(page.getPath()));
        Collections.sort(pageNames);
        if (pageNames.size() > MAX_NUMBER_OF_BACKLINKS_TO_DISPLAY) {
            pageNames = pageNames.subList(0, MAX_NUMBER_OF_BACKLINKS_TO_DISPLAY);
            request.setAttribute(ATTR_BACKLINKS_LIMITED, true);
        }
        request.setAttribute(ATTR_BACKLINKS, pageNames);
    }

    public View history(final PageReference page, final ConsumedPath path, final HttpServletRequest request,
            final HttpServletResponse response) throws Exception {
        List<ChangeInfo> changes = _store.history(page);
        if (ViewTypeConstants.is(request, CTYPE_ATOM)) {
            final String feedUrl = _wikiUrls.page(null, page.getName()) + "?history&ctype=atom";
            return new FeedView(_configuration, _feedWriter, changes, feedUrl);
        }
        request.setAttribute("changes", changes);
        return new JspView("History");
    }

    public View set(final PageReference page, final ConsumedPath path, final HttpServletRequest request,
            final HttpServletResponse response) throws Exception {
        final boolean hasSaveParam = request.getParameter(SUBMIT_SAVE) != null;
        final boolean hasCopyParam = request.getParameter(SUBMIT_COPY) != null;
        final boolean hasRenameParam = request.getParameter(SUBMIT_RENAME) != null;
        final boolean hasUnlockParam = request.getParameter(SUBMIT_UNLOCK) != null;
        if (!(hasSaveParam ^ hasCopyParam ^ hasRenameParam ^ hasUnlockParam)) {
            throw new InvalidInputException("Exactly one action must be specified.");
        }

        if (hasSaveParam) {
            copySessionIdAsAttribute(request);
            if (!isSessionIdValid(request)) {
                return diffEditorView(page, ERROR_SESSION_EXPIRED, request);
            }
            String lockToken = getRequiredString(request, PARAM_LOCK_TOKEN);
            if ("".equals(lockToken)) {
                lockToken = null;
            }
            String content = getContent(request);
            Map<String, String> attributes = getAttributes(request);
            try {
                _store.set(new PageInfoImpl(_store.getWiki(), page.getPath(), content, attributes), lockToken,
                        getBaseRevision(request), createLinkingCommitMessage(page, request));
            } catch (LostLockException e) {
                return diffEditorView(page, null, request);
            } catch (ConflictException e) {
                return diffEditorView(page,
                        "Page has been updated since you started. Please do not save unless you feel that you can merge the changes.",
                        request);
            }
        } else if (hasCopyParam) {
            final String fromPage = getString(request, PARAM_FROM_PAGE);
            final String toPage = getString(request, PARAM_TO_PAGE);
            if (!(fromPage == null ^ toPage == null)) {
                throw new InvalidInputException("'copy' requires one of toPage, fromPage");
            }
            final PageReference toRef;
            final PageReference fromRef;
            if (fromPage != null) {
                fromRef = new PageReferenceImpl(fromPage);
                toRef = page;
            } else {
                fromRef = page;
                toRef = new PageReferenceImpl(toPage);
            }
            _store.copy(fromRef, -1, toRef, createLinkingCommitMessage(toRef, request));
            return new RedirectToPageView(_wikiUrls, toRef);
        } else if (hasRenameParam) {
            final PageReference toPage = new PageReferenceImpl(getRequiredString(request, PARAM_TO_PAGE));
            request.setAttribute(ATTR_FROM_PAGE, page.getName());
            try {
                _store.rename(page, toPage, -1, createLinkingCommitMessage(toPage, request));
            } catch (RenameException ex) {
                VersionedPageInfo pageInfo = _store.getUnderlying().tryToLock(page);
                String message;
                if (pageInfo.isLocked()) {
                    message = "Cannot rename while page is locked. Page was locked by ";
                    if (pageInfo.isNewOrLockedByUser((String) request.getAttribute(RequestAttributes.USERNAME))) {
                        message += "you ";
                    } else {
                        message += pageInfo.getLockedBy();
                    }
                    message += " on " + pageInfo.getLockedSince() + ".";
                } else {
                    message = "Rename failed";
                }
                pageInfo = pageInfo.withoutLockToken();
                request.setAttribute(ATTR_PAGE_INFO, pageInfo);
                request.setAttribute("flash", message);
                return new JspView("Rename");
            }
            return new RedirectToPageView(_wikiUrls, toPage);
        } else if (hasUnlockParam) {
            String lockToken = request.getParameter(PARAM_LOCK_TOKEN);
            // New pages don't have a lock.
            if (lockToken != null && lockToken.length() > 0) {
                _store.unlock(page, lockToken);
            }
        } else {
            throw new InvalidInputException("No action specified.");
        }
        return new RedirectToPageView(_wikiUrls, page);
    }

    private View diffEditorView(final PageReference page, final String customMessage,
            final HttpServletRequest request) throws PageStoreException, InvalidInputException {
        VersionedPageInfo pageInfo = _store.getUnderlying().tryToLock(page);
        String message;
        final String content = getContent(request);
        final String newContent = pageInfo.getContent();
        pageInfo = pageInfo.withAlternativeContent(content);
        request.setAttribute(ATTR_MARKED_UP_DIFF, _diffGenerator.getDiffMarkup(newContent, content));
        if (pageInfo.isLocked()
                && !pageInfo.isNewOrLockedByUser((String) request.getAttribute(RequestAttributes.USERNAME))) {
            message = "Page was locked by " + pageInfo.getLockedBy() + " on " + pageInfo.getLockedSince() + ".";
            pageInfo = pageInfo.withoutLockToken();
        } else {
            message = "Lock was lost!";
        }
        if (customMessage != null) {
            message = customMessage;
        }
        request.setAttribute(ATTR_ORIGINAL_ATTRIBUTES, pageInfo.getAttributes());
        pageInfo = pageInfo
                .withAlternativeAttributes(Maps.filterValues(getAttributes(request), new Predicate<String>() {
                    public boolean apply(final String value) {
                        if (value == null) {
                            return false;
                        }
                        return true;
                    }
                }));
        request.setAttribute(ATTR_PAGE_INFO, pageInfo);
        request.setAttribute("flash", message);
        return new JspView("EditPage");
    }

    private String getContent(final HttpServletRequest request) throws InvalidInputException {
        String content = getRequiredString(request, PARAM_CONTENT);
        content = content + Strings.CRLF;
        return content;
    }

    private Map<String, String> getAttributes(final HttpServletRequest request) throws InvalidInputException {
        String attributesString = getRequiredString(request, PARAM_ATTRIBUTES).trim();
        String[] lines = attributesString.split("\n");
        String regexp = "(\\\")?([^\\\"]+)(\\\")?(\\s)*(=)(\\s)*(\\\")?([^\\\"]+)(\\\")?";
        Pattern pattern = Pattern.compile(regexp);
        Matcher matcher = null;
        String attrPart = null;
        String valuePart = null;
        Map<String, String> attributes = new LinkedHashMap<String, String>();
        for (String line : lines) {
            line = line.trim();
            matcher = pattern.matcher(line);
            if (matcher.find()) {
                attrPart = matcher.group(2);
                valuePart = matcher.group(8);
                attributes.put(attrPart.trim(), valuePart.trim());
            }
        }
        String originalAttributesString = getRequiredString(request, PARAM_ORIGINAL_ATTRIBUTES).trim();
        String[] originalAttrs = originalAttributesString.split("\n");
        for (String attr : originalAttrs) {
            attr = attr.trim();
            if (attr.length() > 0 && !attributes.containsKey(attr)) {
                attributes.put(attr, null);
            }
        }
        return attributes;
    }

    private Long getBaseRevision(final HttpServletRequest request) throws InvalidInputException {
        return getLong(getRequiredString(request, PARAM_BASE_REVISION), PARAM_BASE_REVISION);
    }

    String createLinkingCommitMessage(final PageReference page, final HttpServletRequest request) {
        boolean minorEdit = request.getParameter(PARAM_MINOR_EDIT) != null;
        String commitMessage = request.getParameter(PARAM_COMMIT_MESSAGE);
        if (request.getParameter(SUBMIT_RENAME) != null) {
            commitMessage = String.format("%s renamed to %s", request.getAttribute(ATTR_FROM_PAGE), page.getName());
        } else if (commitMessage == null || commitMessage.trim().length() == 0) {
            commitMessage = ChangeInfo.NO_COMMENT_MESSAGE_TAG;
        }

        return (minorEdit ? ChangeInfo.MINOR_EDIT_MESSAGE_TAG : "") + commitMessage + "\n"
                + _wikiUrls.page(null, page.getName());
    }

    private VersionedPageInfo pageInfoFromRevisionParam(final PageReference defaultPage, final String revisionText,
            final String paramName) throws InvalidInputException, PageStoreException {
        final PageRevisionReference reference = getPageRevisionReference(defaultPage, revisionText, paramName);
        return _store.get(reference.getPage(), reference.getRevision());
    }

    /**
     * Gets the PageInfo from a diff or rev parameter. I.e. "1234" or "PageName.1234"
     *
     * @param defaultPage The page (used when the parameter does not override the page).
     * @param revisionText The parameter value.
     * @param paramName The name of the parameter for error messages.
     * @return The PageInfo.
     * @throws InvalidInputException When the parameter value is not of the correct form.
     * @throws PageStoreException
     */
    PageRevisionReference getPageRevisionReference(final PageReference defaultPage, final String revisionText,
            final String paramName) throws InvalidInputException {
        if (revisionText == null) {
            return new PageRevisionReference(defaultPage, -1);
        }

        final int dotIndex = revisionText.lastIndexOf('.');
        if (dotIndex != -1) {
            final Long revision = getLong(revisionText.substring(dotIndex + 1), paramName);
            final String pageName = revisionText.substring(0, dotIndex);
            final PageReference pageReference = pageName.length() > 0 ? new PageReferenceImpl(pageName)
                    : defaultPage;
            return new PageRevisionReference(pageReference, revision);
        }
        return new PageRevisionReference(defaultPage, getLong(revisionText, paramName));
    }

    private static long getRevision(final HttpServletRequest request) throws InvalidInputException {
        Long givenRevision = getLong(request.getParameter(PARAM_REVISION), PARAM_REVISION);
        return givenRevision == null ? -1 : givenRevision;
    }

}