de.blizzy.documentr.page.PageStore.java Source code

Java tutorial

Introduction

Here is the source code for de.blizzy.documentr.page.PageStore.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.page;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RebaseCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.StopWalkException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.gitective.core.BlobUtils;
import org.gitective.core.CommitFinder;
import org.gitective.core.CommitUtils;
import org.gitective.core.PathFilterUtils;
import org.gitective.core.filter.commit.AndCommitFilter;
import org.gitective.core.filter.commit.CommitFilter;
import org.gitective.core.filter.commit.CommitLimitFilter;
import org.gitective.core.filter.commit.CommitListFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.eventbus.EventBus;
import com.google.common.io.Closeables;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

import de.blizzy.documentr.DocumentrConstants;
import de.blizzy.documentr.access.User;
import de.blizzy.documentr.repository.GlobalRepositoryManager;
import de.blizzy.documentr.repository.ILockedRepository;
import de.blizzy.documentr.repository.RepositoryUtil;
import de.blizzy.documentr.util.Util;

@Component
@Slf4j
class PageStore implements IPageStore {
    private static final String PARENT_PAGE_PATH = "parentPagePath"; //$NON-NLS-1$
    private static final String TITLE = "title"; //$NON-NLS-1$
    private static final String CONTENT_TYPE = "contentType"; //$NON-NLS-1$
    private static final String PAGE_DATA = "pageData"; //$NON-NLS-1$
    private static final String VERSION_LATEST = "latest"; //$NON-NLS-1$
    private static final String VERSION_PREVIOUS = "previous"; //$NON-NLS-1$
    private static final String TAGS = "tags"; //$NON-NLS-1$
    private static final String VIEW_RESTRICTION_ROLE = "viewRestrictionRole"; //$NON-NLS-1$

    @Autowired
    private GlobalRepositoryManager globalRepositoryManager;
    @Autowired
    private EventBus eventBus;

    @Override
    public MergeConflict savePage(String projectName, String branchName, String path, Page page, String baseCommit,
            User user) throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);
        Assert.notNull(user);

        try {
            MergeConflict conflict = savePageInternal(projectName, branchName, path, DocumentrConstants.PAGE_SUFFIX,
                    page, baseCommit, DocumentrConstants.PAGES_DIR_NAME, user);
            if (conflict == null) {
                eventBus.post(new PageChangedEvent(projectName, branchName, path));
            }
            return conflict;
        } catch (GitAPIException e) {
            throw new IOException(e);
        }
    }

    @Override
    public void saveAttachment(String projectName, String branchName, String pagePath, String name, Page attachment,
            User user) throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(pagePath);
        Assert.hasLength(name);
        Assert.notNull(attachment);
        Assert.notNull(user);
        // check if page exists by trying to load it
        getPage(projectName, branchName, pagePath, false);

        try {
            savePageInternal(projectName, branchName, pagePath + "/" + name, DocumentrConstants.PAGE_SUFFIX, //$NON-NLS-1$
                    attachment, null, DocumentrConstants.ATTACHMENTS_DIR_NAME, user);
        } catch (GitAPIException e) {
            throw new IOException(e);
        }
    }

    private MergeConflict savePageInternal(String projectName, String branchName, String path, String suffix,
            Page page, String baseCommit, String rootDir, User user) throws IOException, GitAPIException {

        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            return savePageInternal(projectName, branchName, path, suffix, page, baseCommit, rootDir, user, repo,
                    true);
        } finally {
            Closeables.closeQuietly(repo);
        }
    }

    private MergeConflict savePageInternal(String projectName, String branchName, String path, String suffix,
            Page page, String baseCommit, String rootDir, User user, ILockedRepository repo, boolean push)
            throws IOException, GitAPIException {

        Git git = Git.wrap(repo.r());

        String headCommit = CommitUtils.getHead(repo.r()).getName();
        if ((baseCommit != null) && headCommit.equals(baseCommit)) {
            baseCommit = null;
        }

        String editBranchName = "_edit_" + String.valueOf((long) (Math.random() * Long.MAX_VALUE)); //$NON-NLS-1$
        if (baseCommit != null) {
            git.branchCreate().setName(editBranchName).setStartPoint(baseCommit).call();

            git.checkout().setName(editBranchName).call();
        }

        Map<String, Object> metaMap = new HashMap<String, Object>();
        metaMap.put(TITLE, page.getTitle());
        metaMap.put(CONTENT_TYPE, page.getContentType());
        if (!page.getTags().isEmpty()) {
            metaMap.put(TAGS, page.getTags());
        }
        metaMap.put(VIEW_RESTRICTION_ROLE, page.getViewRestrictionRole());
        Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
        String json = gson.toJson(metaMap);
        File workingDir = RepositoryUtil.getWorkingDir(repo.r());
        File pagesDir = new File(workingDir, rootDir);
        File workingFile = Util.toFile(pagesDir, path + DocumentrConstants.META_SUFFIX);
        FileUtils.write(workingFile, json, Charsets.UTF_8);

        PageData pageData = page.getData();
        if (pageData != null) {
            workingFile = Util.toFile(pagesDir, path + suffix);
            FileUtils.writeByteArrayToFile(workingFile, pageData.getData());
        }

        AddCommand addCommand = git.add().addFilepattern(rootDir + "/" + path + DocumentrConstants.META_SUFFIX); //$NON-NLS-1$
        if (pageData != null) {
            addCommand.addFilepattern(rootDir + "/" + path + suffix); //$NON-NLS-1$
        }
        addCommand.call();

        PersonIdent ident = new PersonIdent(user.getLoginName(), user.getEmail());
        git.commit().setAuthor(ident).setCommitter(ident).setMessage(rootDir + "/" + path + suffix).call(); //$NON-NLS-1$

        MergeConflict conflict = null;

        if (baseCommit != null) {
            git.rebase().setUpstream(branchName).call();

            if (repo.r().getRepositoryState() != RepositoryState.SAFE) {
                String text = FileUtils.readFileToString(workingFile, Charsets.UTF_8);
                conflict = new MergeConflict(text, headCommit);

                git.rebase().setOperation(RebaseCommand.Operation.ABORT).call();
            }

            git.checkout().setName(branchName).call();

            if (conflict == null) {
                git.merge().include(repo.r().resolve(editBranchName)).call();
            }

            git.branchDelete().setBranchNames(editBranchName).setForce(true).call();
        }

        if (push && (conflict == null)) {
            git.push().call();
        }

        page.setParentPagePath(getParentPagePath(path, repo.r()));

        if (conflict == null) {
            PageUtil.updateProjectEditTime(projectName);
        }

        return conflict;
    }

    @Override
    public Page getPage(String projectName, String branchName, String path, boolean loadData) throws IOException {
        return getPage(projectName, branchName, path, null, loadData);
    }

    @Override
    public Page getPage(String projectName, String branchName, String path, String commit, boolean loadData)
            throws IOException {
        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);
        if (commit != null) {
            Assert.hasLength(commit);
        }

        if (log.isDebugEnabled()) {
            log.debug("loading page {}/{}/{}, commit: {}, loadData: {}", //$NON-NLS-1$
                    projectName, branchName, Util.toUrlPagePath(path), commit, Boolean.valueOf(loadData));
        }

        try {
            Map<String, Object> pageMap = getPageData(projectName, branchName, path,
                    DocumentrConstants.PAGES_DIR_NAME, commit, loadData);
            String parentPagePath = (String) pageMap.get(PARENT_PAGE_PATH);
            String title = (String) pageMap.get(TITLE);
            String contentType = (String) pageMap.get(CONTENT_TYPE);
            @SuppressWarnings("unchecked")
            List<String> tagsList = (List<String>) pageMap.get(TAGS);
            if (tagsList == null) {
                tagsList = Collections.emptyList();
            }
            Set<String> tags = Sets.newHashSet(tagsList);
            String viewRestrictionRole = (String) pageMap.get(VIEW_RESTRICTION_ROLE);
            PageData pageData = (PageData) pageMap.get(PAGE_DATA);
            Page page = new Page(title, contentType, pageData);
            page.setParentPagePath(parentPagePath);
            page.setTags(tags);
            page.setViewRestrictionRole(viewRestrictionRole);
            return page;
        } catch (GitAPIException e) {
            throw new IOException(e);
        }
    }

    private Map<String, Object> getPageData(String projectName, String branchName, String path, String rootDir,
            String commit, boolean loadData) throws IOException, GitAPIException {

        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);

            File workingDir = RepositoryUtil.getWorkingDir(repo.r());
            File pagesDir = new File(workingDir, rootDir);
            File workingFile = Util.toFile(pagesDir, path + DocumentrConstants.META_SUFFIX);
            if (!workingFile.isFile()) {
                throw new PageNotFoundException(projectName, branchName, path);
            }

            String json;
            if (commit != null) {
                json = BlobUtils.getContent(repo.r(), commit,
                        DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.META_SUFFIX); //$NON-NLS-1$
            } else {
                json = FileUtils.readFileToString(workingFile, Charsets.UTF_8);
            }
            Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
            Map<String, Object> pageMap = gson.fromJson(json, new TypeToken<Map<String, Object>>() {
            }.getType());

            if (loadData) {
                workingFile = Util.toFile(pagesDir, path + DocumentrConstants.PAGE_SUFFIX);
                byte[] data;
                if (commit != null) {
                    data = BlobUtils.getRawContent(repo.r(), commit,
                            DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX); //$NON-NLS-1$
                } else {
                    data = FileUtils.readFileToByteArray(workingFile);
                }
                String contentType = (String) pageMap.get(CONTENT_TYPE);
                PageData pageData;
                if (contentType.equals(PageTextData.CONTENT_TYPE)) {
                    pageData = PageTextData.fromBytes(data);
                } else {
                    pageData = new PageData(data, contentType);
                }
                pageMap.put(PAGE_DATA, pageData);
            }

            String parentPagePath = getParentPagePath(path, repo.r());
            if (parentPagePath != null) {
                pageMap.put(PARENT_PAGE_PATH, parentPagePath);
            }

            return pageMap;
        } finally {
            Closeables.closeQuietly(repo);
        }
    }

    private String getParentPagePath(String path, Repository repo) {
        File workingDir = RepositoryUtil.getWorkingDir(repo);
        File pagesDir = new File(workingDir, DocumentrConstants.PAGES_DIR_NAME);
        File pageFile = Util.toFile(pagesDir, path + DocumentrConstants.PAGE_SUFFIX);
        File dir = pageFile.getParentFile();
        StringBuilder buf = new StringBuilder();
        while (!dir.equals(pagesDir)) {
            if (buf.length() > 0) {
                buf.insert(0, '/');
            }
            buf.insert(0, dir.getName());
            dir = dir.getParentFile();
        }
        return (buf.length() > 0) ? buf.toString() : null;
    }

    @Override
    public Page getAttachment(String projectName, String branchName, String pagePath, String name)
            throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(pagePath);
        Assert.hasLength(name);

        try {
            Map<String, Object> pageMap = getPageData(projectName, branchName, pagePath + "/" + name, //$NON-NLS-1$
                    DocumentrConstants.ATTACHMENTS_DIR_NAME, null, true);
            String parentPagePath = (String) pageMap.get(PARENT_PAGE_PATH);
            String contentType = (String) pageMap.get(CONTENT_TYPE);
            PageData pageData = (PageData) pageMap.get(PAGE_DATA);
            Page page = new Page(null, contentType, pageData);
            page.setParentPagePath(parentPagePath);
            return page;
        } catch (GitAPIException e) {
            throw new IOException(e);
        }
    }

    @Override
    public List<String> listPageAttachments(String projectName, String branchName, String pagePath)
            throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(pagePath);
        // check if page exists by trying to load it
        getPage(projectName, branchName, pagePath, false);

        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            File workingDir = RepositoryUtil.getWorkingDir(repo.r());
            File attachmentsDir = new File(workingDir, DocumentrConstants.ATTACHMENTS_DIR_NAME);
            File pageAttachmentsDir = Util.toFile(attachmentsDir, pagePath);
            List<String> names = Collections.emptyList();
            if (pageAttachmentsDir.isDirectory()) {
                FileFilter filter = new FileFilter() {
                    @Override
                    public boolean accept(File file) {
                        return file.isFile() && file.getName().endsWith(DocumentrConstants.META_SUFFIX);
                    }
                };
                List<File> files = Lists.newArrayList(pageAttachmentsDir.listFiles(filter));
                Function<File, String> function = new Function<File, String>() {
                    @Override
                    public String apply(File file) {
                        return StringUtils.substringBeforeLast(file.getName(), DocumentrConstants.META_SUFFIX);
                    }
                };
                names = Lists.newArrayList(Lists.transform(files, function));
                Collections.sort(names);
            }
            return names;
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }
    }

    @Override
    public List<String> listAllPagePaths(String projectName, String branchName) throws IOException {
        Assert.hasLength(projectName);
        Assert.hasLength(branchName);

        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            File workingDir = RepositoryUtil.getWorkingDir(repo.r());
            File pagesDir = new File(workingDir, DocumentrConstants.PAGES_DIR_NAME);
            return listPagePaths(pagesDir, true);
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }
    }

    private List<String> listPagePaths(File pagesDir, boolean recursive) {
        List<File> paths = listPageFilesInDir(pagesDir, recursive);
        String prefix = pagesDir.getAbsolutePath() + File.separator;
        final int prefixLen = prefix.length();
        final int pageSuffixLen = DocumentrConstants.PAGE_SUFFIX.length();
        Function<File, String> function = new Function<File, String>() {
            @Override
            public String apply(File file) {
                String path = file.getAbsolutePath();
                path = path.substring(prefixLen, path.length() - pageSuffixLen);
                path = path.replace('\\', '/');
                return path;
            }
        };
        List<String> pagePaths = Lists.newArrayList(Lists.transform(paths, function));
        Collections.sort(pagePaths);
        return pagePaths;
    }

    private List<File> listPageFilesInDir(File dir, boolean recursive) {
        List<File> result = Lists.newArrayList();
        if (dir.isDirectory()) {
            FileFilter filter = new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return (pathname.isFile() && pathname.getName().endsWith(DocumentrConstants.PAGE_SUFFIX))
                            || pathname.isDirectory();
                }
            };
            File[] files = dir.listFiles(filter);
            for (File file : files) {
                if (file.isDirectory()) {
                    if (recursive) {
                        result.addAll(listPageFilesInDir(file, true));
                    }
                } else {
                    result.add(file);
                }
            }
        }
        return result;
    }

    @Override
    public boolean isPageSharedWithOtherBranches(String projectName, String branchName, String path)
            throws IOException {
        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);

        List<String> branches = getBranchesPageIsSharedWith(projectName, branchName, path);
        return branches.size() >= 2;
    }

    @Override
    public List<String> getBranchesPageIsSharedWith(String projectName, String branchName, String path)
            throws IOException {
        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);

        List<String> allBranches = globalRepositoryManager.listProjectBranches(projectName);
        ILockedRepository centralRepo = null;
        Set<String> branchesWithCommit = Collections.emptySet();
        try {
            centralRepo = globalRepositoryManager.getProjectCentralRepository(projectName);
            String repoPath = DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX; //$NON-NLS-1$
            RevCommit commit = CommitUtils.getLastCommit(centralRepo.r(), branchName, repoPath);
            if (commit != null) {
                // get all branches where this commit is in their history
                branchesWithCommit = getBranchesWithCommit(commit, allBranches, centralRepo.r());
                if (branchesWithCommit.size() >= 2) {
                    // remove all branches where the previous commit is no longer visible
                    // due to newer commits on those branches
                    for (Iterator<String> iter = branchesWithCommit.iterator(); iter.hasNext();) {
                        String branch = iter.next();
                        RevCommit c = CommitUtils.getLastCommit(centralRepo.r(), branch, repoPath);
                        if (!c.equals(commit)) {
                            iter.remove();
                        }
                    }
                }
            }
        } finally {
            Closeables.closeQuietly(centralRepo);
        }

        List<String> branches = Lists.newArrayList(branchesWithCommit);
        if (!branches.contains(branchName)) {
            branches.add(branchName);
        }
        Collections.sort(branches);
        return branches;
    }

    private Set<String> getBranchesWithCommit(final RevCommit commit, List<String> allBranches,
            Repository centralRepo) {
        final Set<String> result = Sets.newHashSet();
        for (final String branch : allBranches) {
            CommitFilter matcher = new CommitFilter() {
                @Override
                public boolean include(RevWalk revWalk, RevCommit revCommit) {
                    if (revCommit.equals(commit)) {
                        result.add(branch);
                        throw StopWalkException.INSTANCE;
                    }
                    return true;
                }
            };
            CommitFinder finder = new CommitFinder(centralRepo);
            finder.setMatcher(matcher);
            finder.findFrom(branch);
        }
        return result;
    }

    @Override
    public List<String> listChildPagePaths(String projectName, String branchName, final String path)
            throws IOException {
        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);

        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            File workingDir = RepositoryUtil.getWorkingDir(repo.r());
            File pagesDir = Util.toFile(new File(workingDir, DocumentrConstants.PAGES_DIR_NAME), path);
            List<String> paths = Lists.newArrayList(listPagePaths(pagesDir, false));
            Function<String, String> function = new Function<String, String>() {
                @Override
                public String apply(String childName) {
                    return path + "/" + childName; //$NON-NLS-1$
                }
            };
            paths = Lists.transform(paths, function);
            return paths;
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }
    }

    @Override
    public void deletePage(String projectName, String branchName, final String path, User user) throws IOException {
        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);
        Assert.notNull(user);

        ILockedRepository repo = null;
        List<String> oldPagePaths;
        boolean deleted = false;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            File workingDir = RepositoryUtil.getWorkingDir(repo.r());

            File pagesDir = new File(workingDir, DocumentrConstants.PAGES_DIR_NAME);

            File oldSubPagesDir = Util.toFile(pagesDir, path);
            oldPagePaths = listPagePaths(oldSubPagesDir, true);
            oldPagePaths = Lists.newArrayList(Lists.transform(oldPagePaths, new Function<String, String>() {
                @Override
                public String apply(String p) {
                    return path + "/" + p; //$NON-NLS-1$
                }
            }));
            oldPagePaths.add(path);

            Git git = Git.wrap(repo.r());

            File file = Util.toFile(pagesDir, path + DocumentrConstants.PAGE_SUFFIX);
            if (file.isFile()) {
                git.rm().addFilepattern(
                        DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX) //$NON-NLS-1$
                        .call();
                deleted = true;
            }
            file = Util.toFile(pagesDir, path + DocumentrConstants.META_SUFFIX);
            if (file.isFile()) {
                git.rm().addFilepattern(
                        DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.META_SUFFIX) //$NON-NLS-1$
                        .call();
                deleted = true;
            }
            file = Util.toFile(pagesDir, path);
            if (file.isDirectory()) {
                git.rm().addFilepattern(DocumentrConstants.PAGES_DIR_NAME + "/" + path) //$NON-NLS-1$
                        .call();
                deleted = true;
            }

            File attachmentsDir = new File(workingDir, DocumentrConstants.ATTACHMENTS_DIR_NAME);
            file = Util.toFile(attachmentsDir, path);
            if (file.isDirectory()) {
                git.rm().addFilepattern(DocumentrConstants.ATTACHMENTS_DIR_NAME + "/" + path) //$NON-NLS-1$
                        .call();
                deleted = true;
            }

            if (deleted) {
                PersonIdent ident = new PersonIdent(user.getLoginName(), user.getEmail());
                git.commit().setAuthor(ident).setCommitter(ident).setMessage("delete " + path) //$NON-NLS-1$
                        .call();
                git.push().call();

                PageUtil.updateProjectEditTime(projectName);
            }
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }

        if (deleted) {
            eventBus.post(new PagesDeletedEvent(projectName, branchName, Sets.newHashSet(oldPagePaths)));
        }
    }

    @Override
    public PageMetadata getPageMetadata(String projectName, String branchName, String path) throws IOException {
        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);

        return getPageMetadataInternal(projectName, branchName, path, DocumentrConstants.PAGES_DIR_NAME);
    }

    @Override
    public PageMetadata getAttachmentMetadata(String projectName, String branchName, String path, String name)
            throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);
        Assert.hasLength(name);

        return getPageMetadataInternal(projectName, branchName, path + "/" + name, //$NON-NLS-1$
                DocumentrConstants.ATTACHMENTS_DIR_NAME);
    }

    private PageMetadata getPageMetadataInternal(String projectName, String branchName, String path, String rootDir)
            throws IOException {

        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);

            RevCommit metaCommit = CommitUtils.getLastCommit(repo.r(),
                    rootDir + "/" + path + DocumentrConstants.META_SUFFIX); //$NON-NLS-1$
            RevCommit pageCommit = CommitUtils.getLastCommit(repo.r(),
                    rootDir + "/" + path + DocumentrConstants.PAGE_SUFFIX); //$NON-NLS-1$
            if ((metaCommit == null) && (pageCommit == null)) {
                throw new PageNotFoundException(projectName, branchName, path);
            }

            RevCommit commit = getNewestCommit(metaCommit, pageCommit);

            PersonIdent committer = commit.getAuthorIdent();
            String lastEditedBy = null;
            if (committer != null) {
                lastEditedBy = committer.getName();
            }
            // TODO: would love to use authored time
            Date lastEdited = new Date(TimeUnit.MILLISECONDS.convert(commit.getCommitTime(), TimeUnit.SECONDS));

            File workingDir = RepositoryUtil.getWorkingDir(repo.r());
            File rootDirFile = new File(workingDir, rootDir);
            File file = Util.toFile(rootDirFile, path + DocumentrConstants.PAGE_SUFFIX);

            return new PageMetadata(lastEditedBy, lastEdited, file.length(), commit.getName());
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }
    }

    private RevCommit getNewestCommit(RevCommit... commits) {
        RevCommit newestCommit = null;
        int newestCommitTime = Integer.MIN_VALUE;
        for (RevCommit commit : commits) {
            if (commit != null) {
                int time = commit.getCommitTime();
                if (time > newestCommitTime) {
                    newestCommit = commit;
                    newestCommitTime = time;
                }
            }
        }
        return newestCommit;
    }

    @Override
    public void relocatePage(final String projectName, final String branchName, final String path,
            String newParentPagePath, User user) throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);
        Assert.hasLength(newParentPagePath);
        Assert.notNull(user);
        // check if pages exist by trying to load them
        getPage(projectName, branchName, path, false);
        getPage(projectName, branchName, newParentPagePath, false);

        ILockedRepository repo = null;
        List<String> oldPagePaths;
        List<String> deletedPagePaths;
        List<String> newPagePaths;
        List<PagePathChangedEvent> pagePathChangedEvents;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            String pageName = path.contains("/") ? StringUtils.substringAfterLast(path, "/") : path; //$NON-NLS-1$ //$NON-NLS-2$
            final String newPagePath = newParentPagePath + "/" + pageName; //$NON-NLS-1$

            File workingDir = RepositoryUtil.getWorkingDir(repo.r());

            File oldSubPagesDir = Util.toFile(new File(workingDir, DocumentrConstants.PAGES_DIR_NAME), path);
            oldPagePaths = listPagePaths(oldSubPagesDir, true);
            oldPagePaths = Lists.newArrayList(Lists.transform(oldPagePaths, new Function<String, String>() {
                @Override
                public String apply(String p) {
                    return path + "/" + p; //$NON-NLS-1$
                }
            }));
            oldPagePaths.add(path);

            File deletedPagesSubDir = Util.toFile(new File(workingDir, DocumentrConstants.PAGES_DIR_NAME),
                    newPagePath);
            deletedPagePaths = listPagePaths(deletedPagesSubDir, true);
            deletedPagePaths = Lists.newArrayList(Lists.transform(deletedPagePaths, new Function<String, String>() {
                @Override
                public String apply(String p) {
                    return newPagePath + "/" + p; //$NON-NLS-1$
                }
            }));
            deletedPagePaths.add(newPagePath);

            Git git = Git.wrap(repo.r());
            AddCommand addCommand = git.add();

            for (String dirName : Sets.newHashSet(DocumentrConstants.PAGES_DIR_NAME,
                    DocumentrConstants.ATTACHMENTS_DIR_NAME)) {
                File dir = new File(workingDir, dirName);

                File newSubPagesDir = Util.toFile(dir, newPagePath);
                if (newSubPagesDir.exists()) {
                    git.rm().addFilepattern(dirName + "/" + newPagePath).call(); //$NON-NLS-1$
                }
                File newPageFile = Util.toFile(dir, newPagePath + DocumentrConstants.PAGE_SUFFIX);
                if (newPageFile.exists()) {
                    git.rm().addFilepattern(dirName + "/" + newPagePath + DocumentrConstants.PAGE_SUFFIX).call(); //$NON-NLS-1$
                }
                File newMetaFile = Util.toFile(dir, newPagePath + DocumentrConstants.META_SUFFIX);
                if (newMetaFile.exists()) {
                    git.rm().addFilepattern(dirName + "/" + newPagePath + DocumentrConstants.META_SUFFIX).call(); //$NON-NLS-1$
                }

                File newParentPageDir = Util.toFile(dir, newParentPagePath);
                File subPagesDir = Util.toFile(dir, path);
                if (subPagesDir.exists()) {
                    FileUtils.copyDirectoryToDirectory(subPagesDir, newParentPageDir);
                    git.rm().addFilepattern(dirName + "/" + path).call(); //$NON-NLS-1$
                    addCommand.addFilepattern(dirName + "/" + newParentPagePath + "/" + pageName); //$NON-NLS-1$ //$NON-NLS-2$
                }
                File pageFile = Util.toFile(dir, path + DocumentrConstants.PAGE_SUFFIX);
                if (pageFile.exists()) {
                    FileUtils.copyFileToDirectory(pageFile, newParentPageDir);
                    git.rm().addFilepattern(dirName + "/" + path + DocumentrConstants.PAGE_SUFFIX).call(); //$NON-NLS-1$
                    addCommand.addFilepattern(
                            dirName + "/" + newParentPagePath + "/" + pageName + DocumentrConstants.PAGE_SUFFIX); //$NON-NLS-1$ //$NON-NLS-2$
                }
                File metaFile = Util.toFile(dir, path + DocumentrConstants.META_SUFFIX);
                if (metaFile.exists()) {
                    FileUtils.copyFileToDirectory(metaFile, newParentPageDir);
                    git.rm().addFilepattern(dirName + "/" + path + DocumentrConstants.META_SUFFIX).call(); //$NON-NLS-1$
                    addCommand.addFilepattern(
                            dirName + "/" + newParentPagePath + "/" + pageName + DocumentrConstants.META_SUFFIX); //$NON-NLS-1$ //$NON-NLS-2$
                }
            }

            addCommand.call();
            PersonIdent ident = new PersonIdent(user.getLoginName(), user.getEmail());
            git.commit().setAuthor(ident).setCommitter(ident)
                    .setMessage("move " + path + " to " + newParentPagePath + "/" + pageName) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    .call();
            git.push().call();

            newPagePaths = Lists.transform(oldPagePaths, new Function<String, String>() {
                @Override
                public String apply(String p) {
                    return newPagePath + StringUtils.removeStart(p, path);
                }
            });

            pagePathChangedEvents = Lists.transform(oldPagePaths, new Function<String, PagePathChangedEvent>() {
                @Override
                public PagePathChangedEvent apply(String oldPath) {
                    String newPath = newPagePath + StringUtils.removeStart(oldPath, path);
                    return new PagePathChangedEvent(projectName, branchName, oldPath, newPath);
                }
            });

            PageUtil.updateProjectEditTime(projectName);
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }

        Set<String> allDeletedPagePaths = Sets.newHashSet(oldPagePaths);
        allDeletedPagePaths.addAll(deletedPagePaths);
        eventBus.post(new PagesDeletedEvent(projectName, branchName, allDeletedPagePaths));

        for (String newPath : newPagePaths) {
            eventBus.post(new PageChangedEvent(projectName, branchName, newPath));
        }

        for (PagePathChangedEvent event : pagePathChangedEvents) {
            eventBus.post(event);
        }
    }

    @Override
    public Map<String, String> getMarkdown(String projectName, String branchName, String path, Set<String> versions)
            throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);
        // check if page exists by trying to load it
        getPage(projectName, branchName, path, false);
        Assert.notEmpty(versions);

        Map<String, String> result = Maps.newHashMap();
        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            String filePath = DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX; //$NON-NLS-1$

            Set<String> realVersions = Sets.newHashSet();
            for (String version : versions) {
                if (version.equals(VERSION_LATEST)) {
                    RevCommit latestCommit = CommitUtils.getLastCommit(repo.r(), filePath);
                    String commitId = latestCommit.getName();
                    result.put(VERSION_LATEST, commitId);
                    realVersions.add(commitId);
                } else if (version.equals(VERSION_PREVIOUS)) {
                    RevCommit latestCommit = CommitUtils.getLastCommit(repo.r(), filePath);
                    if (latestCommit.getParentCount() > 0) {
                        RevCommit parentCommit = latestCommit.getParent(0);
                        RevCommit previousCommit = CommitUtils.getLastCommit(repo.r(), parentCommit.getName(),
                                filePath);
                        if (previousCommit != null) {
                            String commitId = previousCommit.getName();
                            result.put(VERSION_PREVIOUS, commitId);
                            realVersions.add(commitId);
                        }
                    }
                } else {
                    realVersions.add(version);
                }
            }

            for (String version : realVersions) {
                String markdown = BlobUtils.getContent(repo.r(), version, filePath);
                if (markdown != null) {
                    result.put(version, markdown);
                }
            }
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }
        return result;
    }

    @Override
    public List<PageVersion> listPageVersions(String projectName, String branchName, String path)
            throws IOException {
        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);
        // check if page exists by trying to load it
        getPage(projectName, branchName, path, false);

        ILockedRepository repo = null;
        List<PageVersion> result = Lists.newArrayList();
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);

            CommitFinder finder = new CommitFinder(repo.r());
            TreeFilter pathFilter = PathFilterUtils
                    .or(DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX); //$NON-NLS-1$
            finder.setFilter(pathFilter);
            CommitListFilter commits = new CommitListFilter();
            finder.setMatcher(new AndCommitFilter(new CommitLimitFilter(50), commits));
            finder.find();

            for (RevCommit commit : commits.getCommits()) {
                result.add(PageUtil.toPageVersion(commit));
            }
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }
        return result;
    }

    @Override
    public void deleteAttachment(String projectName, String branchName, String pagePath, String name, User user)
            throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(pagePath);
        Assert.hasLength(name);
        Assert.notNull(user);

        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            File workingDir = RepositoryUtil.getWorkingDir(repo.r());

            Git git = Git.wrap(repo.r());

            File attachmentsDir = new File(workingDir, DocumentrConstants.ATTACHMENTS_DIR_NAME);
            boolean deleted = false;
            File file = Util.toFile(attachmentsDir, pagePath + "/" + name + DocumentrConstants.PAGE_SUFFIX); //$NON-NLS-1$
            if (file.isFile()) {
                FileUtils.forceDelete(file);
                git.rm().addFilepattern(DocumentrConstants.ATTACHMENTS_DIR_NAME + "/" + //$NON-NLS-1$
                        pagePath + "/" + name + DocumentrConstants.PAGE_SUFFIX) //$NON-NLS-1$
                        .call();
                deleted = true;
            }
            file = Util.toFile(attachmentsDir, pagePath + "/" + name + DocumentrConstants.META_SUFFIX); //$NON-NLS-1$
            if (file.isFile()) {
                FileUtils.forceDelete(file);
                git.rm().addFilepattern(DocumentrConstants.ATTACHMENTS_DIR_NAME + "/" + //$NON-NLS-1$
                        pagePath + "/" + name + DocumentrConstants.META_SUFFIX) //$NON-NLS-1$
                        .call();
                deleted = true;
            }

            if (deleted) {
                PersonIdent ident = new PersonIdent(user.getLoginName(), user.getEmail());
                git.commit().setAuthor(ident).setCommitter(ident).setMessage("delete " + pagePath + "/" + name) //$NON-NLS-1$ //$NON-NLS-2$
                        .call();
                git.push().call();

                PageUtil.updateProjectEditTime(projectName);
            }
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }
    }

    @Override
    public void restorePageVersion(String projectName, String branchName, String path, String version, User user)
            throws IOException {

        Assert.hasLength(projectName);
        Assert.hasLength(branchName);
        Assert.hasLength(path);
        Assert.hasLength(version);

        ILockedRepository repo = null;
        try {
            repo = globalRepositoryManager.getProjectBranchRepository(projectName, branchName);
            String text = BlobUtils.getContent(repo.r(), version,
                    DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX); //$NON-NLS-1$
            File workingDir = RepositoryUtil.getWorkingDir(repo.r());
            File pagesDir = new File(workingDir, DocumentrConstants.PAGES_DIR_NAME);
            File file = Util.toFile(pagesDir, path + DocumentrConstants.PAGE_SUFFIX);
            FileUtils.writeStringToFile(file, text, Charsets.UTF_8.name());

            Git git = Git.wrap(repo.r());
            git.add()
                    .addFilepattern(DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX) //$NON-NLS-1$
                    .call();
            PersonIdent ident = new PersonIdent(user.getLoginName(), user.getEmail());
            git.commit().setAuthor(ident).setCommitter(ident)
                    .setMessage(DocumentrConstants.PAGES_DIR_NAME + "/" + path) //$NON-NLS-1$
                    .call();
            git.push().call();
        } catch (GitAPIException e) {
            throw new IOException(e);
        } finally {
            Closeables.closeQuietly(repo);
        }

        eventBus.post(new PageChangedEvent(projectName, branchName, path));
    }

    @Override
    public String getViewRestrictionRole(String projectName, String branchName, String path) throws IOException {
        return getPage(projectName, branchName, path, false).getViewRestrictionRole();
    }
}