de.blizzy.documentr.web.page.PageController.java Source code

Java tutorial

Introduction

Here is the source code for de.blizzy.documentr.web.page.PageController.java

Source

/*
documentr - Edit, maintain, and present software documentation on the web.
Copyright (C) 2012-2013 Maik Schreiber
    
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 de.blizzy.documentr.web.page;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.WebRequest;

import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import de.blizzy.documentr.DocumentrConstants;
import de.blizzy.documentr.access.AuthenticationUtil;
import de.blizzy.documentr.access.DocumentrPermissionEvaluator;
import de.blizzy.documentr.access.Permission;
import de.blizzy.documentr.access.User;
import de.blizzy.documentr.access.UserStore;
import de.blizzy.documentr.markdown.IPageRenderer;
import de.blizzy.documentr.markdown.MarkdownProcessor;
import de.blizzy.documentr.page.CommitCherryPickConflictResolve;
import de.blizzy.documentr.page.CommitCherryPickResult;
import de.blizzy.documentr.page.ICherryPicker;
import de.blizzy.documentr.page.IPageStore;
import de.blizzy.documentr.page.MergeConflict;
import de.blizzy.documentr.page.Page;
import de.blizzy.documentr.page.PageMetadata;
import de.blizzy.documentr.page.PageNotFoundException;
import de.blizzy.documentr.page.PageTextData;
import de.blizzy.documentr.page.PageUtil;
import de.blizzy.documentr.repository.GlobalRepositoryManager;
import de.blizzy.documentr.util.Util;
import de.blizzy.documentr.web.util.ErrorController;

@Controller
@RequestMapping("/page")
@Slf4j
public class PageController {
    @Autowired
    private IPageStore pageStore;
    @Autowired
    private ICherryPicker cherryPicker;
    @Autowired
    private GlobalRepositoryManager globalRepositoryManager;
    @Autowired
    private MarkdownProcessor markdownProcessor;
    @Autowired
    private UserStore userStore;
    @Autowired
    private IPageRenderer pageRenderer;
    @Autowired
    private DocumentrPermissionEvaluator permissionEvaluator;

    @RequestMapping(value = "/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/" + "{branchName:"
            + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:" + DocumentrConstants.PAGE_PATH_URL_PATTERN
            + "}", method = { RequestMethod.GET, RequestMethod.HEAD })
    @PreAuthorize("hasPagePermission(#projectName, #branchName, #path, VIEW)")
    public String getPage(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, Model model, HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        try {
            path = Util.toRealPagePath(path);
            PageMetadata metadata = pageStore.getPageMetadata(projectName, branchName, path);

            long lastEdited = metadata.getLastEdited().getTime();
            long authenticationCreated = AuthenticationUtil.getAuthenticationCreationTime(request.getSession());
            long projectEditTime = PageUtil.getProjectEditTime(projectName);
            long lastModified = Math.max(lastEdited, authenticationCreated);
            if (projectEditTime >= 0) {
                lastModified = Math.max(lastModified, projectEditTime);
            }

            long modifiedSince = request.getDateHeader("If-Modified-Since"); //$NON-NLS-1$
            if ((modifiedSince >= 0) && (lastModified <= modifiedSince)) {
                return ErrorController.notModified();
            }

            response.setDateHeader("Last-Modified", lastModified); //$NON-NLS-1$
            response.setDateHeader("Expires", 0); //$NON-NLS-1$
            response.setHeader("Cache-Control", "must-revalidate, private"); //$NON-NLS-1$ //$NON-NLS-2$

            Page page = pageStore.getPage(projectName, branchName, path, false);
            model.addAttribute("path", path); //$NON-NLS-1$
            model.addAttribute("pageName", //$NON-NLS-1$
                    path.contains("/") ? StringUtils.substringAfterLast(path, "/") : path); //$NON-NLS-1$ //$NON-NLS-2$
            model.addAttribute("parentPagePath", page.getParentPagePath()); //$NON-NLS-1$
            model.addAttribute("title", page.getTitle()); //$NON-NLS-1$
            String viewRestrictionRole = page.getViewRestrictionRole();
            model.addAttribute("viewRestrictionRole", //$NON-NLS-1$
                    (viewRestrictionRole != null) ? viewRestrictionRole : StringUtils.EMPTY);
            model.addAttribute("commit", metadata.getCommit()); //$NON-NLS-1$
            return "/project/branch/page/view"; //$NON-NLS-1$
        } catch (PageNotFoundException e) {
            return ErrorController.notFound("page.notFound"); //$NON-NLS-1$
        }
    }

    @RequestMapping(value = "/create/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{parentPagePath:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}", method = RequestMethod.GET)
    @PreAuthorize("hasBranchPermission(#projectName, #branchName, EDIT_PAGE)")
    public String createPage(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String parentPagePath, Model model) {

        PageForm form = new PageForm(projectName, branchName, null, Util.toRealPagePath(parentPagePath), null, null,
                StringUtils.EMPTY, null, ArrayUtils.EMPTY_STRING_ARRAY);
        model.addAttribute("pageForm", form); //$NON-NLS-1$
        return "/project/branch/page/edit"; //$NON-NLS-1$
    }

    @RequestMapping(value = "/edit/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/" + "{branchName:"
            + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:" + DocumentrConstants.PAGE_PATH_URL_PATTERN
            + "}", method = RequestMethod.GET)
    @PreAuthorize("hasPagePermission(#projectName, #branchName, #path, EDIT_PAGE)")
    public String editPage(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, Model model, HttpSession session) throws IOException {

        try {
            path = Util.toRealPagePath(path);

            Page page = pageStore.getPage(projectName, branchName, path, true);
            String text = ((PageTextData) page.getData()).getText();
            String viewRestrictionRole = page.getViewRestrictionRole();
            PageMetadata metadata = pageStore.getPageMetadata(projectName, branchName, path);
            String commit = metadata.getCommit();

            MergeConflict conflict = (MergeConflict) session.getAttribute("conflict"); //$NON-NLS-1$
            session.removeAttribute("conflict"); //$NON-NLS-1$
            if (conflict != null) {
                projectName = (String) session.getAttribute("conflict.projectName"); //$NON-NLS-1$
                session.removeAttribute("conflict.projectName"); //$NON-NLS-1$
                branchName = (String) session.getAttribute("conflict.branchName"); //$NON-NLS-1$
                session.removeAttribute("conflict.branchName"); //$NON-NLS-1$
                path = (String) session.getAttribute("conflict.pagePath"); //$NON-NLS-1$
                session.removeAttribute("conflict.pagePath"); //$NON-NLS-1$
                text = conflict.getText();
                commit = conflict.getNewBaseCommit();
            }

            String[] tags = page.getTags().toArray(ArrayUtils.EMPTY_STRING_ARRAY);
            Arrays.sort(tags);
            PageForm form = new PageForm(projectName, branchName, path, page.getParentPagePath(), page.getTitle(),
                    text, (viewRestrictionRole != null) ? viewRestrictionRole : StringUtils.EMPTY, commit, tags);
            model.addAttribute("pageForm", form); //$NON-NLS-1$
            if (conflict != null) {
                model.addAttribute("mergeConflict", Boolean.TRUE); //$NON-NLS-1$
            }
            return "/project/branch/page/edit"; //$NON-NLS-1$
        } catch (PageNotFoundException e) {
            return ErrorController.notFound("page.notFound"); //$NON-NLS-1$
        }
    }

    @RequestMapping(value = "/save/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/" + "{branchName:"
            + DocumentrConstants.BRANCH_NAME_PATTERN + "}", method = RequestMethod.POST)
    @PreAuthorize("hasBranchPermission(#form.projectName, #form.branchName, EDIT_PAGE)")
    public String savePage(@ModelAttribute @Valid PageForm form, BindingResult bindingResult, Model model,
            Authentication authentication) throws IOException {

        String projectName = form.getProjectName();
        String branchName = form.getBranchName();
        User user = userStore.getUser(authentication.getName());

        if (!globalRepositoryManager.listProjectBranches(projectName).contains(branchName)) {
            bindingResult.rejectValue("branchName", "page.branch.nonexistent"); //$NON-NLS-1$ //$NON-NLS-2$
        }

        if (bindingResult.hasErrors()) {
            return "/project/branch/page/edit"; //$NON-NLS-1$
        }

        String parentPagePath = form.getParentPagePath();
        if (StringUtils.isBlank(parentPagePath)) {
            parentPagePath = null;
        }
        parentPagePath = Util.toRealPagePath(parentPagePath);
        Page page = Page.fromText(form.getTitle(), form.getText());
        String path = form.getPath();
        if (StringUtils.isBlank(path)) {
            path = parentPagePath + "/" + Util.simplifyForUrl(form.getTitle()); //$NON-NLS-1$
        }
        page.setTags(Sets.newHashSet(form.getTags()));
        page.setViewRestrictionRole(
                StringUtils.isNotBlank(form.getViewRestrictionRole()) ? form.getViewRestrictionRole() : null);

        Page oldPage = null;
        try {
            oldPage = pageStore.getPage(projectName, branchName, path, true);
        } catch (PageNotFoundException e) {
            // okay
        }
        if ((oldPage == null) || !page.equals(oldPage)) {
            MergeConflict conflict = pageStore.savePage(projectName, branchName, path, page,
                    Strings.emptyToNull(form.getCommit()), user);
            if (conflict != null) {
                form.setText(conflict.getText());
                form.setCommit(conflict.getNewBaseCommit());
                model.addAttribute("mergeConflict", Boolean.TRUE); //$NON-NLS-1$
                return "/project/branch/page/edit"; //$NON-NLS-1$
            }
        }

        Integer start = form.getParentPageSplitRangeStart();
        Integer end = form.getParentPageSplitRangeEnd();
        if (StringUtils.isNotBlank(parentPagePath) && (start != null) && (end != null) && permissionEvaluator
                .hasBranchPermission(authentication, projectName, branchName, Permission.EDIT_PAGE)) {

            log.info("splitting off {}-{} of {}/{}/{}", //$NON-NLS-1$
                    start, end, projectName, branchName, parentPagePath);

            Page parentPage = pageStore.getPage(projectName, branchName, parentPagePath, true);
            String text = ((PageTextData) parentPage.getData()).getText();
            end = Math.min(end, text.length());
            text = text.substring(0, start) + text.substring(end);
            parentPage.setData(new PageTextData(text));
            pageStore.savePage(projectName, branchName, parentPagePath, parentPage, null, user);
        }

        return "redirect:/page/" + projectName + "/" + branchName + "/" + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                Util.toUrlPagePath(path);
    }

    @ModelAttribute
    public PageForm createPageForm(@PathVariable String projectName, @PathVariable String branchName,
            @RequestParam(required = false) String path, @RequestParam(required = false) String parentPagePath,
            @RequestParam(required = false) String title, @RequestParam(required = false) String text,
            @RequestParam(required = false) String viewRestrictionRole,
            @RequestParam(required = false) String commit, @RequestParam(required = false) String[] tags) {

        if (tags == null) {
            tags = ArrayUtils.EMPTY_STRING_ARRAY;
        }
        return ((path != null) && (title != null) && (text != null))
                ? new PageForm(projectName, branchName, path, parentPagePath, title, text,
                        StringUtils.isNotBlank(viewRestrictionRole) ? viewRestrictionRole : StringUtils.EMPTY,
                        Strings.emptyToNull(commit), tags)
                : null;
    }

    @RequestMapping(value = "/generateName/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{parentPagePath:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN
            + "}/json", method = RequestMethod.POST, produces = "application/json")
    @ResponseBody
    @PreAuthorize("hasBranchPermission(#projectName, #branchName, VIEW)")
    public Map<String, Object> generateName(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String parentPagePath, @RequestParam String title) throws IOException {

        String name = Util.simplifyForUrl(title);
        String path = Util.toRealPagePath(parentPagePath) + "/" + name; //$NON-NLS-1$
        boolean pageExists = false;
        try {
            Page page = pageStore.getPage(projectName, branchName, path, false);
            pageExists = page != null;
        } catch (PageNotFoundException e) {
            // okay
        }

        Map<String, Object> result = new HashMap<String, Object>();
        result.put("path", path); //$NON-NLS-1$
        result.put("exists", Boolean.valueOf(pageExists)); //$NON-NLS-1$
        return result;
    }

    @RequestMapping(value = "/markdownToHtml/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN
            + "}/json", method = RequestMethod.POST, produces = "application/json")
    @ResponseBody
    @PreAuthorize("isAuthenticated()")
    public Map<String, String> markdownToHtml(@PathVariable String projectName, @PathVariable String branchName,
            @RequestParam String markdown, @RequestParam(required = false) String pagePath,
            Authentication authentication, HttpServletRequest request) {

        String contextPath = request.getContextPath();
        Map<String, String> result = new HashMap<String, String>();
        String html = markdownProcessor.markdownToHtml(markdown, projectName, branchName, pagePath, authentication,
                contextPath);
        html = markdownProcessor.processNonCacheableMacros(html, projectName, branchName, pagePath, authentication,
                contextPath);
        result.put("html", html); //$NON-NLS-1$
        return result;
    }

    @RequestMapping(value = "/copyToBranch/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}", method = RequestMethod.POST)
    @PreAuthorize("hasPagePermission(#projectName, #branchName, #path, VIEW) and "
            + "hasBranchPermission(#projectName, #targetBranchName, EDIT_PAGE)")
    public String copyToBranch(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, @RequestParam String targetBranchName, Authentication authentication)
            throws IOException {

        path = Util.toRealPagePath(path);
        Page page = pageStore.getPage(projectName, branchName, path, true);
        User user = userStore.getUser(authentication.getName());
        pageStore.savePage(projectName, targetBranchName, path, page, null, user);
        return "redirect:/page/edit/" + projectName + "/" + targetBranchName + "/" + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                Util.toUrlPagePath(path);
    }

    @RequestMapping(value = "/delete/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}", method = RequestMethod.GET)
    @PreAuthorize("hasBranchPermission(#projectName, #branchName, EDIT_PAGE)")
    public String deletePage(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, Authentication authentication) throws IOException {

        path = Util.toRealPagePath(path);
        User user = userStore.getUser(authentication.getName());
        pageStore.deletePage(projectName, branchName, path, user);
        return "redirect:/page/" + projectName + "/" + branchName + //$NON-NLS-1$ //$NON-NLS-2$
                "/" + DocumentrConstants.HOME_PAGE_NAME; //$NON-NLS-1$
    }

    @RequestMapping(value = "/relocate/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}", method = RequestMethod.POST)
    @PreAuthorize("hasBranchPermission(#projectName, #branchName, EDIT_PAGE)")
    public String relocatePage(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, @RequestParam String newParentPagePath, Authentication authentication)
            throws IOException {

        path = Util.toRealPagePath(path);
        newParentPagePath = Util.toRealPagePath(newParentPagePath);

        User user = userStore.getUser(authentication.getName());
        pageStore.relocatePage(projectName, branchName, path, newParentPagePath, user);
        String pageName = path.contains("/") ? StringUtils.substringAfterLast(path, "/") : path; //$NON-NLS-1$ //$NON-NLS-2$
        return "redirect:/page/" + projectName + "/" + branchName + //$NON-NLS-1$ //$NON-NLS-2$
                "/" + Util.toUrlPagePath(newParentPagePath + "/" + pageName); //$NON-NLS-1$ //$NON-NLS-2$
    }

    @RequestMapping(value = "/markdown/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}/json", method = RequestMethod.GET)
    @ResponseBody
    @PreAuthorize("hasPagePermission(#projectName, #branchName, #path, VIEW)")
    public Map<String, String> getPageMarkdown(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, @RequestParam Set<String> versions) throws IOException {

        try {
            return pageStore.getMarkdown(projectName, branchName, Util.toRealPagePath(path), versions);
        } catch (PageNotFoundException e) {
            return Collections.emptyMap();
        }
    }

    @RequestMapping(value = "/markdownInRange/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}/"
            + "{rangeStart:[0-9]+},{rangeEnd:[0-9]+}/{commit:[0-9a-fA-F]+}/json", method = RequestMethod.GET)
    @ResponseBody
    @PreAuthorize("hasPagePermission(#projectName, #branchName, #path, VIEW)")
    public Map<String, String> getPageMarkdownInRange(@PathVariable String projectName,
            @PathVariable String branchName, @PathVariable String path, @PathVariable int rangeStart,
            @PathVariable int rangeEnd, @PathVariable String commit) throws IOException {

        Page page = pageStore.getPage(projectName, branchName, Util.toRealPagePath(path), commit, true);
        String markdown = ((PageTextData) page.getData()).getText();
        Map<String, String> result = Maps.newHashMap();
        markdown = markdown.substring(rangeStart, Math.min(rangeEnd, markdown.length()));
        markdown = markdown.replaceAll("[\\r\\n]+$", StringUtils.EMPTY); //$NON-NLS-1$
        result.put("markdown", markdown); //$NON-NLS-1$
        return result;
    }

    @RequestMapping(value = "/changes/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}", method = RequestMethod.GET)
    @PreAuthorize("isAuthenticated() and hasPagePermission(#projectName, #branchName, #path, VIEW)")
    public String getPageChanges(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, Model model) {

        model.addAttribute("projectName", projectName); //$NON-NLS-1$
        model.addAttribute("branchName", branchName); //$NON-NLS-1$
        path = Util.toRealPagePath(path);
        model.addAttribute("path", path); //$NON-NLS-1$
        return "/project/branch/page/changes"; //$NON-NLS-1$
    }

    @RequestMapping(value = "/saveRange/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}/json", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasPagePermission(#projectName, #branchName, #path, EDIT_PAGE)")
    public Map<String, Object> savePageRange(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, @RequestParam String markdown, @RequestParam String range,
            @RequestParam String commit, Authentication authentication, HttpServletRequest request)
            throws IOException {

        path = Util.toRealPagePath(path);

        markdown = markdown.replaceAll("[\\r\\n]+$", StringUtils.EMPTY); //$NON-NLS-1$
        int rangeStart = Integer.parseInt(StringUtils.substringBefore(range, ",")); //$NON-NLS-1$
        int rangeEnd = Integer.parseInt(StringUtils.substringAfter(range, ",")); //$NON-NLS-1$

        Page page = pageStore.getPage(projectName, branchName, path, commit, true);
        String text = ((PageTextData) page.getData()).getText();
        rangeEnd = Math.min(rangeEnd, text.length());

        String oldMarkdown = text.substring(rangeStart, rangeEnd);
        String cleanedOldMarkdown = oldMarkdown.replaceAll("[\\r\\n]+$", StringUtils.EMPTY); //$NON-NLS-1$
        rangeEnd -= oldMarkdown.length() - cleanedOldMarkdown.length();

        String newText = text.substring(0, rangeStart) + markdown
                + text.substring(Math.min(rangeEnd, text.length()));

        page.setData(new PageTextData(newText));
        User user = userStore.getUser(authentication.getName());
        MergeConflict conflict = pageStore.savePage(projectName, branchName, path, page, commit, user);

        Map<String, Object> result = Maps.newHashMap();
        if (conflict != null) {
            result.put("conflict", Boolean.TRUE); //$NON-NLS-1$
            HttpSession session = request.getSession();
            session.setAttribute("conflict", conflict); //$NON-NLS-1$
            session.setAttribute("conflict.projectName", projectName); //$NON-NLS-1$
            session.setAttribute("conflict.branchName", branchName); //$NON-NLS-1$
            session.setAttribute("conflict.path", path); //$NON-NLS-1$
        } else {
            String newCommit = pageStore.getPageMetadata(projectName, branchName, path).getCommit();
            String contextPath = request.getContextPath();
            String html = pageRenderer.getHtml(projectName, branchName, path, authentication, contextPath);
            html = markdownProcessor.processNonCacheableMacros(html, projectName, branchName, path, authentication,
                    contextPath);
            result.put("html", html); //$NON-NLS-1$
            result.put("commit", newCommit); //$NON-NLS-1$
        }
        return result;
    }

    @RequestMapping(value = "/restoreVersion/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}/json", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasPagePermission(#projectName, #branchName, #path, EDIT_PAGE)")
    public void restoreVersion(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, @RequestParam String version, Authentication authentication)
            throws IOException {

        User user = userStore.getUser(authentication.getName());
        pageStore.restorePageVersion(projectName, branchName, Util.toRealPagePath(path), version, user);
    }

    @RequestMapping(value = "/cherryPick/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/"
            + "{branchName:" + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:"
            + DocumentrConstants.PAGE_PATH_URL_PATTERN + "}", method = RequestMethod.POST)
    @PreAuthorize("hasPagePermission(#projectName, #branchName, #path, VIEW)")
    public String cherryPick(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, @RequestParam String version1, @RequestParam String version2,
            @RequestParam("branch") Set<String> targetBranches, @RequestParam boolean dryRun, WebRequest request,
            Model model, Authentication authentication, Locale locale) throws IOException {

        path = Util.toRealPagePath(path);

        for (String targetBranch : targetBranches) {
            if (!permissionEvaluator.hasPagePermission(authentication, projectName, targetBranch, path,
                    Permission.EDIT_PAGE)) {

                return ErrorController.forbidden();
            }
        }

        List<String> commits = cherryPicker.getCommitsList(projectName, branchName, path, version1, version2);
        if (commits.isEmpty()) {
            throw new IllegalArgumentException("no commits to cherry-pick"); //$NON-NLS-1$
        }

        User user = userStore.getUser(authentication.getName());

        Map<String, String[]> params = request.getParameterMap();
        Set<CommitCherryPickConflictResolve> resolves = Sets.newHashSet();
        for (Map.Entry<String, String[]> entry : params.entrySet()) {
            String name = entry.getKey();
            if (name.startsWith("resolveText_")) { //$NON-NLS-1$
                String branchCommit = StringUtils.substringAfter(name, "_"); //$NON-NLS-1$
                String branch = StringUtils.substringBefore(branchCommit, "/"); //$NON-NLS-1$
                String commit = StringUtils.substringAfter(branchCommit, "/"); //$NON-NLS-1$
                String text = entry.getValue()[0];
                CommitCherryPickConflictResolve resolve = new CommitCherryPickConflictResolve(branch, commit, text);
                resolves.add(resolve);
            }
        }

        Map<String, List<CommitCherryPickResult>> results = cherryPicker.cherryPick(projectName, branchName, path,
                commits, targetBranches, resolves, dryRun, user, locale);
        if (results.keySet().size() != targetBranches.size()) {
            throw new IllegalStateException();
        }

        if (!dryRun) {
            boolean allOk = true;
            loop: for (List<CommitCherryPickResult> branchResults : results.values()) {
                for (CommitCherryPickResult result : branchResults) {
                    if (result.getStatus() != CommitCherryPickResult.Status.OK) {
                        allOk = false;
                        break loop;
                    }
                }
            }

            if (allOk) {
                return "redirect:/page/" + projectName + "/" + branchName + //$NON-NLS-1$ //$NON-NLS-2$
                        "/" + Util.toUrlPagePath(path); //$NON-NLS-1$
            }
        }

        model.addAttribute("cherryPickResults", results); //$NON-NLS-1$
        model.addAttribute("version1", version1); //$NON-NLS-1$
        model.addAttribute("version2", version2); //$NON-NLS-1$
        model.addAttribute("resolves", resolves); //$NON-NLS-1$
        return "/project/branch/page/cherryPick"; //$NON-NLS-1$
    }

    @RequestMapping(value = "/split/{projectName:" + DocumentrConstants.PROJECT_NAME_PATTERN + "}/" + "{branchName:"
            + DocumentrConstants.BRANCH_NAME_PATTERN + "}/" + "{path:" + DocumentrConstants.PAGE_PATH_URL_PATTERN
            + "}/" + "{rangeStart:[0-9]+},{rangeEnd:[0-9]+}", method = RequestMethod.GET)
    @PreAuthorize("hasBranchPermission(#projectName, #branchName, EDIT_PAGE)")
    public String splitPage(@PathVariable String projectName, @PathVariable String branchName,
            @PathVariable String path, @PathVariable int rangeStart, @PathVariable int rangeEnd, Model model)
            throws IOException {

        log.info("splitting off {}-{} of {}/{}/{}", //$NON-NLS-1$
                rangeStart, rangeEnd, projectName, branchName, path);

        path = Util.toRealPagePath(path);
        Page page = pageStore.getPage(projectName, branchName, path, true);
        String text = ((PageTextData) page.getData()).getText();
        rangeEnd = Math.min(rangeEnd, text.length());
        text = text.substring(rangeStart, rangeEnd).trim();
        PageForm form = new PageForm(projectName, branchName, null, path, null, text, StringUtils.EMPTY, null,
                ArrayUtils.EMPTY_STRING_ARRAY);
        form.setParentPageSplitRangeStart(rangeStart);
        form.setParentPageSplitRangeEnd(rangeEnd);
        model.addAttribute("pageForm", form); //$NON-NLS-1$
        return "/project/branch/page/edit"; //$NON-NLS-1$
    }
}