net.riezebos.thoth.content.impl.GitContentManager.java Source code

Java tutorial

Introduction

Here is the source code for net.riezebos.thoth.content.impl.GitContentManager.java

Source

/* Copyright (c) 2016 W.T.J. Riezebos
 *
 * 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.riezebos.thoth.content.impl;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.LogCommand;
import org.eclipse.jgit.api.PullResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.riezebos.thoth.configuration.Configuration;
import net.riezebos.thoth.configuration.ThothEnvironment;
import net.riezebos.thoth.content.ContentManagerBase;
import net.riezebos.thoth.content.versioncontrol.Commit;
import net.riezebos.thoth.content.versioncontrol.Revision;
import net.riezebos.thoth.content.versioncontrol.Revision.Action;
import net.riezebos.thoth.content.versioncontrol.SimpleDiffFormatter;
import net.riezebos.thoth.content.versioncontrol.SourceDiff;
import net.riezebos.thoth.context.ContextDefinition;
import net.riezebos.thoth.context.RepositoryDefinition;
import net.riezebos.thoth.exceptions.ContentManagerException;
import net.riezebos.thoth.exceptions.ContextNotFoundException;
import net.riezebos.thoth.markdown.filehandle.BasicFileSystem;
import net.riezebos.thoth.markdown.filehandle.FileSystem;
import net.riezebos.thoth.util.PagedList;
import net.riezebos.thoth.util.ThothCoreUtil;
import net.riezebos.thoth.util.ThothUtil;

/**
 * Support GIT based version control as the manager for the content
 *
 * @author wido
 */
public class GitContentManager extends ContentManagerBase {

    private static final Logger LOG = LoggerFactory.getLogger(GitContentManager.class);
    private static final String HEAD_TREE = "HEAD^{tree}";

    public GitContentManager(ContextDefinition contextDefinition, ThothEnvironment context)
            throws ContentManagerException {
        super(contextDefinition, context);
        validateContextDefinition(contextDefinition);
        setFileSystem(createFileSystem());
    }

    protected FileSystem createFileSystem() throws ContextNotFoundException {
        return new BasicFileSystem(getContextFolder());
    }

    @Override
    public boolean supportsVersionControl() {
        return true;
    }

    @Override
    protected synchronized String cloneOrPull() throws ContentManagerException {
        StringBuilder log = new StringBuilder();
        try {
            Configuration config = getConfiguration();
            RepositoryDefinition repositoryDefinition = getContextDefinition().getRepositoryDefinition();
            String repositoryUrl = repositoryDefinition.getLocation();
            String workspaceLocation = config.getWorkspaceLocation();
            if (StringUtils.isBlank(workspaceLocation))
                throw new IllegalArgumentException("No environment variable or system variable named "
                        + Configuration.WORKSPACELOCATION + " was set");
            File file = new File(workspaceLocation);
            if (!file.isDirectory()) {
                severe(log, "The library path " + workspaceLocation
                        + " does not exist. Cannot initialize content manager");
            } else {
                try {
                    String contextFolder = getContextFolder();
                    File target = new File(contextFolder);
                    CredentialsProvider credentialsProvider = getCredentialsProvider();

                    if (target.isDirectory()) {
                        try (Git repos = getRepository()) {

                            Repository repository = repos.getRepository();
                            ObjectId oldHead = repository.resolve(HEAD_TREE);

                            PullResult pullResult = repos.pull().setCredentialsProvider(credentialsProvider).call();
                            ObjectId newHead = repository.resolve(HEAD_TREE);
                            boolean changes = (oldHead == null && newHead != null)//
                                    || (oldHead != null && !oldHead.equals(newHead));
                            if (changes)
                                notifyContextContentsChanged();

                            String message = pullResult.isSuccessful() ? ": Pull successful, "
                                    : ": Pull of failed, ";
                            message += changes ? CHANGES_DETECTED_MSG : NO_CHANGES_DETECTED_MSG;
                            info(log, getContextName() + message);

                            setLatestRefresh(new Date());
                        } catch (Exception e) {
                            severe(log, e);
                        }
                    } else {
                        info(log, getContextName() + ": Cloning from " + repositoryUrl + " to " + contextFolder);
                        target.mkdirs();
                        try (Git result = Git.cloneRepository()//
                                .setURI(repositoryUrl)//
                                .setBranch(getBranch())//
                                .setCredentialsProvider(credentialsProvider)//
                                .setDirectory(target).call()) {
                            info(log, getContextName() + ": Cloned repository: "
                                    + result.getRepository().getDirectory());
                            setLatestRefresh(new Date());
                            notifyContextContentsChanged();
                        } catch (Exception e) {
                            severe(log, e);
                        }
                    }
                } catch (Exception e) {
                    severe(log, e);
                }
            }

        } catch (Exception e) {
            throw new ContentManagerException(e);
        }

        return log.toString().trim();
    }

    public String getBranch() {
        return getContextDefinition().getBranch();
    }

    protected Git getRepository() throws ContextNotFoundException, IOException {
        String contextFolder = getContextFolder();
        File target = new File(contextFolder);
        return Git.open(target);
    }

    @Override
    public PagedList<Commit> getCommits(String path, int pageNumber, int pageSize) throws ContentManagerException {
        path = ThothUtil.stripPrefix(path, "/");
        try (Git repos = getRepository()) {
            Repository repository = repos.getRepository();
            List<Commit> commits = new ArrayList<>();
            LogCommand log = repos.log();
            if (path != null)
                log = log.addPath(path);
            Iterable<RevCommit> revisions = log.call();
            Iterator<RevCommit> iterator = revisions.iterator();

            // First skip over the pages we are not interested in
            int skipCount = (pageNumber - 1) * pageSize;
            while (skipCount-- > 0 && iterator.hasNext())
                iterator.next();

            // Now add the revisions
            while (iterator.hasNext() && commits.size() < pageSize) {
                RevCommit revCommit = iterator.next();
                commits.add(getCommit(repository, revCommit, path));
            }
            PagedList<Commit> pagedList = new PagedList<>(commits, iterator.hasNext());
            return pagedList;
        } catch (IOException | GitAPIException e) {
            throw new ContentManagerException(e);
        }
    }

    protected Commit getCommit(Repository repository, RevCommit revCommit, String path)
            throws MissingObjectException, IncorrectObjectTypeException, IOException, UnsupportedEncodingException {
        path = ThothUtil.stripPrefix(path, "/");

        Commit commit = new Commit();
        commit.setAuthor(revCommit.getAuthorIdent().getName());
        commit.setTimestamp(new Date(((long) revCommit.getCommitTime()) * 1000));
        commit.setMessage(revCommit.getFullMessage());
        commit.setShortMessage(revCommit.getShortMessage());
        commit.setId(ObjectId.toString(revCommit.getId()));

        try (RevWalk revWalk = new RevWalk(repository); SimpleDiffFormatter df = new SimpleDiffFormatter()) {
            RevCommit[] parents = revCommit.getParents();
            if (parents != null && parents.length > 0) {
                RevCommit parent = revWalk.parseCommit(parents[0].getId());
                df.setRepository(repository);
                df.setDiffComparator(RawTextComparator.DEFAULT);
                df.setDetectRenames(true);
                List<DiffEntry> diffs = df.scan(parent.getTree(), revCommit.getTree());
                for (DiffEntry diff : diffs) {
                    if (path == null || path.equals(diff.getNewPath()) || path.equals(diff.getOldPath())) {
                        Revision fileRevision = createFileRevision(diff, df, revCommit);
                        commit.addRevision(fileRevision);
                    }
                }
            } else {
                // Initial version; no parent
                try (TreeWalk treeWalk = new TreeWalk(repository)) {
                    RevTree tree = revCommit.getTree();
                    treeWalk.addTree(tree);
                    treeWalk.setRecursive(true);
                    treeWalk.setFilter(PathFilter.create(path));
                    if (treeWalk.next()) {
                        Revision fileRevision = new Revision(Action.ADD, path);
                        fileRevision.setCommitId(ObjectId.toString(revCommit.getId()) + "/" + path);
                        commit.addRevision(fileRevision);
                    }
                }
            }
        }
        return commit;
    }

    @Override
    public SourceDiff getDiff(String diffSpec) throws ContentManagerException {

        int idx = diffSpec.indexOf('/');
        if (idx == -1)
            return null;

        String id = diffSpec.substring(0, idx);
        String path = diffSpec.substring(idx + 1);

        if (id == null || ObjectId.zeroId().equals(ObjectId.fromString(id)))
            return null;

        SourceDiff result = null;

        try (Git git = getRepository();
                RevWalk revWalk = new RevWalk(git.getRepository());
                SimpleDiffFormatter df = new SimpleDiffFormatter()) {
            Repository repository = git.getRepository();

            RevCommit revCommit = revWalk.parseCommit(ObjectId.fromString(id));

            RevCommit[] parents = revCommit.getParents();
            if (parents != null && parents.length > 0) {
                RevCommit parent = revWalk.parseCommit(parents[0].getId());
                df.setRepository(repository);
                df.setDiffComparator(RawTextComparator.DEFAULT);
                df.setDetectRenames(true);
                List<DiffEntry> diffs = df.scan(parent.getTree(), revCommit.getTree());
                for (DiffEntry diff : diffs) {
                    if (path.equals(diff.getNewPath())) {

                        result = createSourceDiff(repository, revCommit, diff);
                        break;
                    }
                }
            }
            return result;
        } catch (Exception e) {
            throw new ContentManagerException(e);
        }
    }

    protected SourceDiff createSourceDiff(Repository repository, RevCommit revCommit, DiffEntry diff)
            throws MissingObjectException, IOException, UnsupportedEncodingException {
        SourceDiff result;
        ObjectId oldId = diff.getOldId().toObjectId();
        ObjectId newId = diff.getNewId().toObjectId();

        String oldSource = getContents(repository, oldId);
        String newSource = getContents(repository, newId);

        if (oldId.equals(newId))
            oldSource = "";

        PersonIdent authorIdent = revCommit.getAuthorIdent();
        String author = authorIdent == null ? "Unknown" : authorIdent.getName();
        Date timeModified = new Date(((long) revCommit.getCommitTime()) * 1000);

        result = new SourceDiff(author, oldSource, newSource, timeModified);
        result.setCommitMessage(revCommit.getFullMessage());
        return result;
    }

    protected Revision createFileRevision(DiffEntry diff, SimpleDiffFormatter df, RevCommit revCommit)
            throws IOException, UnsupportedEncodingException {
        Action action = translateAction(diff.getChangeType());
        Revision fileRevision = new Revision(action,
                action.equals(Action.DELETE) ? diff.getOldPath() : diff.getNewPath());
        fileRevision.setMessage(revCommit.getFullMessage());
        fileRevision.setCommitId(ObjectId.toString(revCommit.getId()) + "/" + diff.getNewPath());
        return fileRevision;
    }

    protected String getContents(Repository repository, ObjectId id)
            throws MissingObjectException, IOException, UnsupportedEncodingException {
        if (id == null || ObjectId.zeroId().equals(id))
            return "";

        ObjectLoader loader = repository.open(id);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        loader.copyTo(out);
        byte[] byteArray = out.toByteArray();
        int length = ThothCoreUtil.charLength(byteArray);

        String body;
        if (length == -1)
            body = "<Binary contents>";
        else
            body = new String(byteArray, "UTF-8");
        return body;
    }

    protected CredentialsProvider getCredentialsProvider() {
        RepositoryDefinition repositoryDefinition = getContextDefinition().getRepositoryDefinition();
        String username = repositoryDefinition.getUsername();
        String password = repositoryDefinition.getPassword();
        return new UsernamePasswordCredentialsProvider(username, password);
    }

    protected Action translateAction(ChangeType changeType) {
        if (changeType == ChangeType.ADD)
            return Action.ADD;
        if (changeType == ChangeType.COPY)
            return Action.COPY;
        if (changeType == ChangeType.DELETE)
            return Action.DELETE;
        if (changeType == ChangeType.MODIFY)
            return Action.MODIFY;
        if (changeType == ChangeType.RENAME)
            return Action.RENAME;
        return Action.MODIFY;
    }

    protected void validateContextDefinition(ContextDefinition contextDefinition) throws ContentManagerException {
        RepositoryDefinition repositoryDefinition = contextDefinition.getRepositoryDefinition();
        if (StringUtils.isBlank(repositoryDefinition.getUsername()))
            throw new ContentManagerException("Username not set for repositiory " + repositoryDefinition.getName());
        if (StringUtils.isBlank(repositoryDefinition.getPassword()))
            throw new ContentManagerException("Password not set for repositiory " + repositoryDefinition.getName());
        if (StringUtils.isBlank(repositoryDefinition.getLocation()))
            throw new ContentManagerException("Location not set for repositiory " + repositoryDefinition.getName());
        if (StringUtils.isBlank(contextDefinition.getBranch()))
            throw new ContentManagerException("Branch not set for context " + contextDefinition.getName());
    }

    public String getContextFolder() throws ContextNotFoundException {
        Configuration config = getConfiguration();
        return config.getWorkspaceLocation() + getContextName() + "/";
    }

    protected void info(StringBuilder log, String message) {
        LOG.debug(message.trim());
        log.append(message + "\n");
    }

    protected void severe(StringBuilder log, String message) {
        LOG.error(message.trim());
        log.append(getContextName() + ": ERROR, " + message + "\n");
    }

    protected void severe(StringBuilder log, Exception e) {
        LOG.error(e.getMessage(), e);
        log.append(getContextName() + ": ERROR, " + e.getMessage() + "\n");
    }
}