org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryHelper.java

Source

/*
 * Crafter Studio
 *
 * Copyright (C) 2007-2016 Crafter Software Corporation.
 *
 * 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 org.craftercms.studio.impl.v1.repository.git;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;

import com.google.gdata.util.common.base.StringUtil;
import org.apache.commons.lang.StringUtils;
import org.craftercms.studio.api.v1.constant.GitRepositories;
import org.craftercms.studio.api.v1.constant.StudioConstants;
import org.craftercms.studio.api.v1.log.Logger;
import org.craftercms.studio.api.v1.log.LoggerFactory;
import org.craftercms.studio.api.v1.service.security.SecurityProvider;
import org.craftercms.studio.api.v1.util.StudioConfiguration;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;

import static org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryConstants.CONFIG_PARAMETER_BIG_FILE_THRESHOLD;
import static org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryConstants.CONFIG_PARAMETER_BIG_FILE_THRESHOLD_DEFAULT;
import static org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryConstants.CONFIG_PARAMETER_COMPRESSION;
import static org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryConstants.CONFIG_PARAMETER_COMPRESSION_DEFAULT;
import static org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryConstants.CONFIG_SECTION_CORE;
import static org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryConstants.GIT_COMMIT_ALL_ITEMS;
import static org.craftercms.studio.impl.v1.repository.git.GitContentRepositoryConstants.GIT_ROOT;

/**
 * Created by Sumer Jabri
 */
public class GitContentRepositoryHelper {
    private static final Logger logger = LoggerFactory.getLogger(GitContentRepositoryHelper.class);

    Map<String, Repository> sandboxes = new HashMap<>();
    Map<String, Repository> published = new HashMap<>();

    Repository globalRepo = null; // TODO: SJ: TODAY: Must get this initialized in an init or bootstrap

    StudioConfiguration studioConfiguration;
    SecurityProvider securityProvider;

    GitContentRepositoryHelper(StudioConfiguration studioConfiguration, SecurityProvider securityProvider) {
        this.studioConfiguration = studioConfiguration;
        this.securityProvider = securityProvider;
    }

    /**
     * Build the global repository as part of system startup and caches it
     * @return true if successful, false otherwise
     * @throws IOException
     */
    // TODO: SJ: This should be redesigned to return the repository instead of setting it as a "side effect"
    public boolean buildGlobalRepo() throws IOException {
        boolean toReturn = false;
        Path siteRepoPath = buildRepoPath(GitRepositories.GLOBAL).resolve(GIT_ROOT);

        if (Files.exists(siteRepoPath)) {
            globalRepo = openRepository(siteRepoPath);
            toReturn = true;
        }

        return toReturn;
    }

    /**
     * Builds a site's repository objects and caches them (Sandbox and Published)
     * @param site path to repository
     * @return true if successful, false otherwise
     */
    public boolean buildSiteRepo(String site) {
        boolean toReturn = false;
        Repository sandboxRepo;
        Repository publishedRepo;

        Path siteSandboxRepoPath = buildRepoPath(GitRepositories.SANDBOX, site).resolve(GIT_ROOT);
        Path sitePublishedRepoPath = buildRepoPath(GitRepositories.SANDBOX, site).resolve(GIT_ROOT);

        try {
            if (Files.exists(siteSandboxRepoPath)) {
                // Build and put in cache
                sandboxRepo = openRepository(siteSandboxRepoPath);
                sandboxes.put(site, sandboxRepo);
                toReturn = true;
            }
        } catch (IOException e) {
            logger.error("Failed to create sandbox repo for site: " + site + " using path "
                    + siteSandboxRepoPath.toString(), e);
        }

        try {
            if (toReturn && Files.exists(sitePublishedRepoPath)) {
                // Build and put in cache
                publishedRepo = openRepository(sitePublishedRepoPath);
                published.put(site, publishedRepo);

                toReturn = true;
            }
        } catch (IOException e) {
            logger.error("Failed to create published repo for site: " + site + " using path "
                    + sitePublishedRepoPath.toString(), e);
        }

        return toReturn;
    }

    /**
     * Opens a git repository
     *
     * @param repositoryPath path to repository to open (including .git)
     * @return repository object if successful
     * @throws IOException
     */
    public Repository openRepository(Path repositoryPath) throws IOException {
        FileRepositoryBuilder builder = new FileRepositoryBuilder();
        Repository repository = builder.setGitDir(repositoryPath.toFile()).readEnvironment().findGitDir().build();
        return repository;
    }

    public String getGitPath(String path) {
        Path gitPath = Paths.get(path);
        gitPath = gitPath.normalize();
        try {
            gitPath = Paths.get(File.separator).relativize(gitPath);
        } catch (IllegalArgumentException e) {
            logger.debug("Path: " + path + " is already relative path.");
        }
        if (StringUtils.isEmpty(gitPath.toString())) {
            return ".";
        }
        return gitPath.toString();
    }

    public Repository createGitRepository(Path path) {
        Repository toReturn;
        path = Paths.get(path.toAbsolutePath().toString(), GIT_ROOT);
        try {
            toReturn = FileRepositoryBuilder.create(path.toFile());
            toReturn.create();

            // Get git configuration
            StoredConfig config = toReturn.getConfig();
            // Set compression level (core.compression)
            config.setInt(CONFIG_SECTION_CORE, null, CONFIG_PARAMETER_COMPRESSION,
                    CONFIG_PARAMETER_COMPRESSION_DEFAULT);
            // Set big file threshold (core.bigFileThreshold)
            config.setString(CONFIG_SECTION_CORE, null, CONFIG_PARAMETER_BIG_FILE_THRESHOLD,
                    CONFIG_PARAMETER_BIG_FILE_THRESHOLD_DEFAULT);
            // Save configuration changes
            config.save();
        } catch (IOException e) {
            logger.error("Error while creating repository for site with path" + path.toString(), e);
            toReturn = null;
        }

        return toReturn;
    }

    public Path buildRepoPath(GitRepositories repoType) {
        return buildRepoPath(repoType, StringUtil.EMPTY_STRING);
    }

    public Path buildRepoPath(GitRepositories repoType, String site) {
        Path path;
        switch (repoType) {
        case SANDBOX:
            path = Paths.get(studioConfiguration.getProperty(StudioConfiguration.REPO_BASE_PATH),
                    studioConfiguration.getProperty(StudioConfiguration.SITES_REPOS_PATH), site,
                    studioConfiguration.getProperty(StudioConfiguration.SANDBOX_PATH));
            break;
        case PUBLISHED:
            path = Paths.get(studioConfiguration.getProperty(StudioConfiguration.REPO_BASE_PATH),
                    studioConfiguration.getProperty(StudioConfiguration.SITES_REPOS_PATH), site,
                    studioConfiguration.getProperty(StudioConfiguration.PUBLISHED_PATH));
            break;
        case GLOBAL:
            path = Paths.get(studioConfiguration.getProperty(StudioConfiguration.REPO_BASE_PATH),
                    studioConfiguration.getProperty(StudioConfiguration.GLOBAL_REPO_PATH));
            break;
        default:
            path = null;
        }

        return path;
    }

    /**
     * Create a site git repository from scratch (Sandbox and Published)
     * @param site
     * @return true if successful, false otherwise
     */
    public boolean createSiteGitRepo(String site) {
        boolean toReturn;
        Repository sandboxRepo = null;
        Repository publishedRepo = null;

        // Build a path for the site/sandbox
        Path siteSandboxPath = buildRepoPath(GitRepositories.SANDBOX, site);
        // Built a path for the site/published
        Path sitePublishedPath = buildRepoPath(GitRepositories.PUBLISHED, site);

        // Create Sandbox
        sandboxRepo = createGitRepository(siteSandboxPath);

        // Create Published
        if (sandboxRepo != null) {
            publishedRepo = createGitRepository(sitePublishedPath);
        }

        toReturn = (sandboxRepo != null) && (publishedRepo != null);

        if (toReturn) {
            sandboxes.put(site, sandboxRepo);
            published.put(site, publishedRepo);
        }

        return toReturn;
    }

    public boolean createGlobalRepo() {
        boolean toReturn = false;
        Path globalConfigRepoPath = buildRepoPath(GitRepositories.GLOBAL).resolve(GIT_ROOT);

        if (!Files.exists(globalConfigRepoPath)) {
            // Git repository doesn't exist for global, but the folder might be present, let's delete if exists
            Path globalConfigPath = globalConfigRepoPath.getParent();

            // Create the global repository folder
            try {
                Files.deleteIfExists(globalConfigPath);
                logger.info("Bootstrapping repository...");
                Files.createDirectories(globalConfigPath);
                globalRepo = createGitRepository(globalConfigPath);
                toReturn = true;
            } catch (IOException e) {
                // Something very wrong has happened
                logger.error("Bootstrapping repository failed", e);
            }
        } else {
            logger.error("Detected existing global repository, will not create new one.");
            toReturn = false;
        }

        return toReturn;
    }

    public boolean copyContentFromBlueprint(String blueprint, String site) {
        boolean toReturn = true;

        // Build a path to the Sandbox repo we'll be copying to
        Path siteRepoPath = buildRepoPath(GitRepositories.SANDBOX, site);
        // Build a path to the blueprint
        Path blueprintPath = buildRepoPath(GitRepositories.GLOBAL).resolve(
                Paths.get(studioConfiguration.getProperty(StudioConfiguration.BLUE_PRINTS_PATH), blueprint));
        EnumSet<FileVisitOption> opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
        // Let's copy!
        TreeCopier tc = new TreeCopier(blueprintPath, siteRepoPath);
        try {
            Files.walkFileTree(blueprintPath, opts, Integer.MAX_VALUE, tc);
        } catch (IOException err) {
            logger.error("Error copping files from blueprint", err);
            toReturn = false;
        }

        return toReturn;
    }

    public boolean deleteSiteGitRepo(String site) {
        boolean toReturn;

        // Get the Sandbox Path
        Path siteSandboxPath = buildRepoPath(GitRepositories.SANDBOX, site);
        // Get parent of that (since every site has two repos: Sandbox and Published
        Path sitePath = siteSandboxPath.getParent();
        // Get a file handle to the parent and delete it
        File siteFolder = sitePath.toFile();
        toReturn = siteFolder.delete();

        // If delete successful, remove from in-memory cache
        if (toReturn) {
            sandboxes.remove(site);
            published.remove(site);
        }

        return toReturn;
    }

    public boolean updateSitenameConfigVar(String site) {
        boolean toReturn = true;
        String siteConfigFolder = "/config/studio";
        if (!replaceSitenameVariable(site,
                Paths.get(buildRepoPath(GitRepositories.SANDBOX, site).toAbsolutePath().toString(),
                        studioConfiguration.getProperty(StudioConfiguration.CONFIGURATION_SITE_CONFIG_BASE_PATH),
                        studioConfiguration
                                .getProperty(StudioConfiguration.CONFIGURATION_SITE_GENERAL_CONFIG_FILE_NAME)))) {
            toReturn = false;
        } else if (!replaceSitenameVariable(site,
                Paths.get(buildRepoPath(GitRepositories.SANDBOX, site).toAbsolutePath().toString(),
                        studioConfiguration.getProperty(StudioConfiguration.CONFIGURATION_SITE_CONFIG_BASE_PATH),
                        studioConfiguration.getProperty(
                                StudioConfiguration.CONFIGURATION_SITE_PERMISSION_MAPPINGS_FILE_NAME)))) {
            toReturn = false;
        } else if (!replaceSitenameVariable(site, Paths.get(
                buildRepoPath(GitRepositories.SANDBOX, site).toAbsolutePath().toString(),
                studioConfiguration.getProperty(StudioConfiguration.CONFIGURATION_SITE_CONFIG_BASE_PATH),
                studioConfiguration.getProperty(StudioConfiguration.CONFIGURATION_SITE_ROLE_MAPPINGS_FILE_NAME)))) {
            toReturn = false;
        }
        return toReturn;
    }

    protected boolean replaceSitenameVariable(String site, Path path) {
        boolean toReturn = false;
        Charset charset = StandardCharsets.UTF_8;
        String content = null;
        try {
            content = new String(Files.readAllBytes(path), charset);
            content = content.replaceAll(StudioConstants.CONFIG_SITENAME_VARIABLE, site);
            Files.write(path, content.getBytes(charset));
            toReturn = true;
        } catch (IOException e) {
            logger.error("Error replacing sitename variable inside configuration file " + path.toString()
                    + " for site " + site);
            toReturn = false;
        }
        return toReturn;
    }

    public boolean bulkImport(String site /* , Map<String, String> filesCommitIds */) {
        // TODO: SJ: Define this further and build it along with API & Content Service equivalent with business logic
        // TODO: SJ: This could be in 2.6.1+ or 2.7.x
        // write all files to disk
        // commit all files
        // return data structure of file name & commit id per file
        // the caller will update the database
        //
        // considerations:
        //   accept a zip file
        //   accept a root folder or allow nesting
        //   content service should call this and then update the database
        //   find an efficient way to bulk write the files and then do a single commit across all
        return false;
    }

    /**
     * Perform an initial commit after large changes to a site. Will not work against the global config repo.
     * @param site
     * @param message
     * @return true if successful, false otherwise
     */
    public boolean performInitialCommit(String site, String message) {
        boolean toReturn = true;

        try {
            Repository repo = getRepository(site, GitRepositories.SANDBOX);

            Git git = new Git(repo);

            Status status = git.status().call();

            if (status.hasUncommittedChanges() || !status.isClean()) {
                DirCache dirCache = git.add().addFilepattern(GIT_COMMIT_ALL_ITEMS).call();
                RevCommit commit = git.commit().setMessage(message).call();
                // TODO: SJ: Do we need the commit id?
                // commitId = commit.getName();
            }
        } catch (GitAPIException err) {
            logger.error("error creating initial commit for site:  " + site, err);
            toReturn = false;
        }

        return toReturn;
    }

    // SJ: Helper methods
    public Repository getRepository(String site, GitRepositories gitRepository) {
        Repository repo;

        logger.debug("getRepository invoked with site" + site + "Repository Type: " + gitRepository.toString());

        switch (gitRepository) {
        case SANDBOX:
            repo = sandboxes.get(site);
            if (repo == null) {
                if (buildSiteRepo(site)) {
                    repo = sandboxes.get(site);
                } else {
                    logger.error("error getting the repository for site: " + site);
                }
            }
            break;
        case PUBLISHED:
            repo = published.get(site);
            if (repo == null) {
                if (buildSiteRepo(site)) {
                    repo = published.get(site);
                } else {
                    logger.error("error getting the repository for site: " + site);
                }
            }
            break;
        case GLOBAL:
            repo = globalRepo;
            break;
        default:
            repo = null;
        }

        if (repo != null) {
            logger.debug("success in getting the repository for site: " + site);
        } else {
            logger.debug("failure in getting the repository for site: " + site);
        }

        return repo;
    }

    // TODO: SJ: Fix the exception handling in this method
    public RevTree getTreeForLastCommit(Repository repository)
            throws AmbiguousObjectException, IncorrectObjectTypeException, IOException, MissingObjectException {
        ObjectId lastCommitId = repository.resolve(Constants.HEAD);

        // a RevWalk allows to walk over commits based on some filtering
        try (RevWalk revWalk = new RevWalk(repository)) {
            RevCommit commit = revWalk.parseCommit(lastCommitId);

            // and using commit's tree find the path
            RevTree tree = commit.getTree();
            return tree;
        }
    }

    // TODO: SJ: Fix the exception handling in this method
    public RevTree getTreeForCommit(Repository repository, String commitId) throws IOException {
        ObjectId commitObjectId = repository.resolve(commitId);

        try (RevWalk revWalk = new RevWalk(repository)) {
            RevCommit commit = revWalk.parseCommit(commitObjectId);

            // and using commit's tree find the path
            RevTree tree = commit.getTree();
            return tree;
        }
    }

    public boolean writeFile(Repository repo, String site, String path, InputStream content) {
        boolean result;

        try {
            // Create basic file
            File file = new File(repo.getDirectory().getParent(), path);

            // Create parent folders
            File folder = file.getParentFile();
            if (folder != null) {
                if (!folder.exists()) {
                    folder.mkdirs();
                }
            }

            // Create the file if it doesn't exist already
            if (!file.createNewFile()) {
                logger.error("error creating file: site: " + site + " path: " + path);
                result = false;
            } else {
                // Write the bits
                FileChannel outChannel = new FileOutputStream(file.getPath()).getChannel();
                logger.debug("created the file output channel");
                ReadableByteChannel inChannel = Channels.newChannel(content);
                logger.debug("created the file input channel");
                long amount = 1024 * 1024; // 1MB at a time
                long count;
                long offset = 0;
                while ((count = outChannel.transferFrom(inChannel, offset, amount)) > 0) {
                    logger.debug("writing the bits: offset = " + offset + " count: " + count);
                    offset += count;
                }

                // Add the file to git
                try (Git git = new Git(repo)) {
                    git.add().addFilepattern(getGitPath(path)).call();

                    git.close();
                    result = true;
                } catch (GitAPIException e) {
                    logger.error("error adding file to git: site: " + site + " path: " + path, e);
                    result = false;
                }
            }
        } catch (IOException e) {
            logger.error("error writing file: site: " + site + " path: " + path, e);
            result = false;
        }

        return result;
    }

    public String commitFile(Repository repo, String site, String path, String comment, PersonIdent user) {
        String commitId = null;
        String gitPath = getGitPath(path);
        Status status;

        try (Git git = new Git(repo)) {
            status = git.status().addPath(gitPath).call();

            // TODO: SJ: Below needs more thought and refactoring to detect issues with git repo and report them
            if (status.hasUncommittedChanges() || !status.isClean()) {
                RevCommit commit;
                commit = git.commit().setOnly(gitPath).setAuthor(user).setCommitter(user).setMessage(comment)
                        .call();
                commitId = commit.getName();
            }

            git.close();
        } catch (GitAPIException e) {
            logger.error("error adding and committing file to git: site: " + site + " path: " + path, e);
        }

        return commitId;
    }

    /**
     * Return the current user identity as a jgit PersonIdent
     *
     * @return current user as a PersonIdent
     */
    public PersonIdent getCurrentUserIdent() {
        String userName = securityProvider.getCurrentUser();
        return getAuthorIdent(userName);
    }

    /**
     * Return the author identity as a jgit PersonIdent
     *
     * @param author author
     * @return author user as a PersonIdent
     */
    public PersonIdent getAuthorIdent(String author) {
        Map<String, String> currentUserProfile = securityProvider.getUserProfile(author);
        PersonIdent currentUserIdent = new PersonIdent(
                currentUserProfile.get("firstName") + " " + currentUserProfile.get("lastName"),
                currentUserProfile.get("email"));

        return currentUserIdent;
    }
}