org.opoo.press.maven.wagon.github.GitHub.java Source code

Java tutorial

Introduction

Here is the source code for org.opoo.press.maven.wagon.github.GitHub.java

Source

/*
 * Copyright 2013 Alex Lin.
 *
 * 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 org.opoo.press.maven.wagon.github;

import static org.eclipse.egit.github.core.Blob.ENCODING_BASE64;
import static org.eclipse.egit.github.core.TreeEntry.MODE_BLOB;
import static org.eclipse.egit.github.core.TreeEntry.TYPE_BLOB;
import static org.eclipse.egit.github.core.TypedResource.TYPE_COMMIT;

import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.codehaus.plexus.util.DirectoryScanner;
import org.eclipse.egit.github.core.Blob;
import org.eclipse.egit.github.core.Commit;
import org.eclipse.egit.github.core.Reference;
import org.eclipse.egit.github.core.RepositoryId;
import org.eclipse.egit.github.core.Tree;
import org.eclipse.egit.github.core.TreeEntry;
import org.eclipse.egit.github.core.TypedResource;
import org.eclipse.egit.github.core.client.GitHubClient;
import org.eclipse.egit.github.core.client.RequestException;
import org.eclipse.egit.github.core.service.DataService;
import org.eclipse.egit.github.core.util.EncodingUtils;

/**
 * Pure java GitHub client wagon.
 * @author Alex Lin
 * @author Kevin Sawicki (kevin@github.com)
 */
public class GitHub {
    private static final Logger log = LoggerFactory.getLogger(GitHub.class);

    /**
     * BRANCH_DEFAULT
     */
    public static final String BRANCH_DEFAULT = "refs/heads/gh-pages";

    /**
     * NO_JEKYLL_FILE
     */
    public static final String NO_JEKYLL_FILE = ".nojekyll";

    private String branch = BRANCH_DEFAULT;

    private String message;

    private String repositoryName;

    private String repositoryOwner;

    private String userName;

    private String password;

    private String oauth2Token;

    private String host;

    private String[] includes;

    private String[] excludes;

    private boolean force;

    private boolean noJekyll = true;

    private boolean merge;

    private boolean dryRun;

    private int numThreads = 1;

    /**
     * @param branch the branch to set
     */
    public void setBranch(String branch) {
        if (!branch.startsWith("refs/heads/")) {
            branch = "refs/heads/" + branch;
        }
        this.branch = branch;
    }

    /**
     * @param message the message to set
     */
    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * @param repositoryName the repositoryName to set
     */
    public void setRepositoryName(String repositoryName) {
        this.repositoryName = repositoryName;
    }

    /**
     * @param repositoryOwner the repositoryOwner to set
     */
    public void setRepositoryOwner(String repositoryOwner) {
        this.repositoryOwner = repositoryOwner;
    }

    /**
     * @param userName the userName to set
     */
    public void setUserName(String userName) {
        this.userName = userName;
    }

    /**
     * @param password the password to set
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * @param oauth2Token the oauth2Token to set
     */
    public void setOauth2Token(String oauth2Token) {
        this.oauth2Token = oauth2Token;
    }

    /**
     * @param host the host to set
     */
    public void setHost(String host) {
        this.host = host;
    }

    /**
     * @param includes the includes to set
     */
    public void setIncludes(String[] includes) {
        this.includes = includes;
    }

    /**
     * @param excludes the excludes to set
     */
    public void setExcludes(String[] excludes) {
        this.excludes = excludes;
    }

    /**
     * @param force the force to set
     */
    public void setForce(boolean force) {
        this.force = force;
    }

    /**
     * @param noJekyll the noJekyll to set
     */
    public void setNoJekyll(boolean noJekyll) {
        this.noJekyll = noJekyll;
    }

    /**
     * @param merge the merge to set
     */
    public void setMerge(boolean merge) {
        this.merge = merge;
    }

    /**
     * @param dryRun the dryRun to set
     */
    public void setDryRun(boolean dryRun) {
        this.dryRun = dryRun;
    }

    /**
     * @param numThreads the numThreads to set
     */
    public void setNumThreads(int numThreads) {
        this.numThreads = numThreads;
    }

    /**
     * 
     */
    public GitHub() {
        super();
    }

    public void deploy(File outputDirectory, String destinationDirectory) throws GitHubException {
        RepositoryId repository = getRepository(repositoryOwner, repositoryName);
        if (dryRun) {
            log.info("Dry run mode, repository will not be modified");
        }

        String[] paths = getPaths(outputDirectory);
        String prefix = getPrefix(destinationDirectory);

        GitHubClient client = createClient(host, userName, password, oauth2Token);
        DataService service = new DataService(client);

        boolean createNoJekyll = noJekyll;

        if (createNoJekyll) {
            for (String path : paths) {
                if (NO_JEKYLL_FILE.equals(path)) {
                    createNoJekyll = false;
                    break;
                }
            }
        }

        // Write blobs and build tree entries
        List<TreeEntry> entries = new ArrayList<TreeEntry>(paths.length);
        if (numThreads <= 1) {
            createEntries(entries, prefix, paths, service, repository, outputDirectory);
        } else {
            createEntriesInThreads(entries, prefix, paths, service, repository, outputDirectory, numThreads);
        }

        if (createNoJekyll) {
            if (log.isDebugEnabled()) {
                log.debug("Creating empty '.nojekyll' blob at root of tree");
            }
            TreeEntry entry = createEntry("", NO_JEKYLL_FILE, service, repository, outputDirectory);
            entries.add(entry);
        }

        Reference ref = getReference(service, repository);

        if (dryRun) {
            log.debug("Dry run mode, skip deploy.");
            return;
        }

        // Write tree
        Tree tree = createTree(service, repository, ref, entries);

        // Build commit
        Commit commit = new Commit();
        commit.setMessage(message != null ? message : "GitHubWagon: Deploying OpooPress to GitHub Pages.");
        commit.setTree(tree);

        // Set parent commit SHA-1 if reference exists
        if (ref != null) {
            commit.setParents(Collections.singletonList(new Commit().setSha(ref.getObject().getSha())));
        }

        Commit created;
        try {
            created = service.createCommit(repository, commit);
            log.info(MessageFormat.format("Creating commit with SHA-1: {0}", created.getSha()));
        } catch (IOException e) {
            throw new GitHubException("Error creating commit: " + e.getMessage(), e);
        }

        TypedResource object = new TypedResource();
        object.setType(TYPE_COMMIT).setSha(created.getSha());
        if (ref != null) {
            // Update existing reference
            ref.setObject(object);
            try {
                log.info(String.format("Updating reference %s from %s to %s", branch,
                        commit.getParents().get(0).getSha(), created.getSha()));
                service.editReference(repository, ref, force);
            } catch (IOException e) {
                throw new GitHubException("Error editing reference: " + e.getMessage(), e);
            }
        } else {
            // Create new reference
            ref = new Reference().setObject(object).setRef(branch);
            try {
                log.info(MessageFormat.format("Creating reference {0} starting at commit {1}", branch,
                        created.getSha()));
                service.createReference(repository, ref);
            } catch (IOException e) {
                throw new GitHubException("Error creating reference: " + e.getMessage(), e);
            }
        }
    }

    private List<TreeEntry> createEntries(List<TreeEntry> entries, final String prefix, final String[] paths,
            final DataService service, final RepositoryId repository, final File outputDirectory)
            throws GitHubException {
        for (String path : paths) {
            TreeEntry entry = createEntry(prefix, path, service, repository, outputDirectory);
            entries.add(entry);
        }
        return entries;
    }

    private List<TreeEntry> createEntriesInThreads(List<TreeEntry> entries, final String prefix,
            final String[] paths, final DataService service, final RepositoryId repository,
            final File outputDirectory, int numThreads) throws GitHubException {
        ExecutorService threadPool = Executors.newFixedThreadPool(numThreads);//.newCachedThreadPool();  
        CompletionService<TreeEntry> cs = new ExecutorCompletionService<TreeEntry>(threadPool);

        for (final String path : paths) {
            cs.submit(new Callable<TreeEntry>() {
                @Override
                public TreeEntry call() throws Exception {
                    return createEntry(prefix, path, service, repository, outputDirectory);
                }
            });
        }

        try {
            //BUG: wait for ever??
            //         Future<TreeEntry> future = cs.take();
            //         while(future != null){
            //            entries.add(future.get());
            //            future = cs.take();
            //         }

            for (int i = 0; i < paths.length; i++) {
                entries.add(cs.take().get());
            }
            log.info("All entries created: " + paths.length);
        } catch (InterruptedException e) {
            throw new GitHubException("", e);
        } catch (ExecutionException e) {
            throw new GitHubException("", e);
        }
        return entries;
    }

    private Tree createTree(DataService service, RepositoryId repository, Reference ref, List<TreeEntry> entries)
            throws GitHubException {
        try {
            int size = entries.size();
            log.info(String.format("Creating tree with %s blob entries", size));

            String baseTree = null;
            if (merge && ref != null) {
                Tree currentTree = service.getCommit(repository, ref.getObject().getSha()).getTree();
                if (currentTree != null) {
                    baseTree = currentTree.getSha();
                }
                log.info(MessageFormat.format("Merging with tree {0}", baseTree));
            }

            return service.createTree(repository, entries, baseTree);
        } catch (IOException e) {
            throw new GitHubException("Error creating tree: " + e.getMessage(), e);
        }
    }

    private Reference getReference(DataService service, RepositoryId repository) throws GitHubException {
        Reference ref = null;
        try {
            ref = service.getReference(repository, branch);
        } catch (RequestException e) {
            if (404 != e.getStatus()) {
                throw new GitHubException("Error getting reference: " + e.getMessage(), e);
            }
        } catch (IOException e) {
            throw new GitHubException("Error getting reference: " + e.getMessage(), e);
        }

        if (ref != null && !TYPE_COMMIT.equals(ref.getObject().getType())) {
            throw new GitHubException(
                    MessageFormat.format("Existing ref {0} points to a {1} ({2}) instead of a commmit",
                            ref.getRef(), ref.getObject().getType(), ref.getObject().getSha()));
        }
        return ref;
    }

    private TreeEntry createEntry(String prefix, String path, DataService service, RepositoryId repository,
            File outputDirectory) throws GitHubException {
        TreeEntry entry = new TreeEntry();
        entry.setPath(prefix + path);
        entry.setType(TYPE_BLOB);
        entry.setMode(MODE_BLOB);
        if (!dryRun) {
            entry.setSha(createBlob(service, repository, outputDirectory, path));
            log.info("" + path + " -> " + entry.getSha());
        }
        return entry;
    }

    /**
     * @param destinationDirectory
     * @return
     */
    private String getPrefix(String destinationDirectory) {
        String prefix = destinationDirectory;
        //String prefix = site.getRoot();
        if (prefix == null) {
            prefix = "";
        }
        if ("./".equals(prefix)) {
            prefix = "";
        }
        if (prefix.length() > 0 && !prefix.endsWith("/")) {
            prefix += "/";
        }
        return prefix;
    }

    private String createBlob(DataService service, RepositoryId repository, File outputDirectory, String path)
            throws GitHubException {
        try {
            Blob blob = new Blob().setEncoding(ENCODING_BASE64);
            if (NO_JEKYLL_FILE.equals(path)) {
                blob.setContent("");
                //log.debug("Creating blob from " + NO_JEKYLL_FILE);
            } else {
                File file = new File(outputDirectory, path);
                byte[] bytes = FileUtils.readFileToByteArray(file);
                String encoded = EncodingUtils.toBase64(bytes);
                blob.setContent(encoded);
                //log.debug("Creating blob from " +  file.getAbsolutePath());
            }
            if (log.isDebugEnabled()) {
                log.debug("Creating blob from " + path);
            }
            return service.createBlob(repository, blob);
        } catch (IOException e) {
            throw new GitHubException("Error creating blob from '" + path + "': " + e.getMessage(), e);
        }
    }

    private GitHubClient createClient(String host, String userName, String password, String oauth2Token)
            throws GitHubException {
        GitHubClient client;
        if (!StringUtils.isEmpty(host)) {
            if (log.isDebugEnabled()) {
                log.debug("Using custom host: " + host);
            }
            client = createClient(host);
        } else {
            client = new GitHubClient();
        }

        if (!StringUtils.isEmpty(userName) && !StringUtils.isEmpty(password)) {
            if (log.isDebugEnabled()) {
                log.debug("Using basic authentication with username: " + userName);
            }
            client.setCredentials(userName, password);
            return client;
        } else if (!StringUtils.isEmpty(oauth2Token)) {
            if (log.isDebugEnabled()) {
                log.debug("Using OAuth2 access token authentication");
            }
            client.setOAuth2Token(oauth2Token);
            return client;
        } else if (StringUtils.isEmpty(userName) && !StringUtils.isEmpty(password)) {
            if (log.isDebugEnabled()) {
                log.debug("Using OAuth2 access token authentication");
            }
            client.setOAuth2Token(password);
            return client;
        } else if (!StringUtils.isEmpty(userName) && System.console() != null) {
            Console console = System.console();
            while (StringUtils.isEmpty(password)) {
                password = new String(console.readPassword("Input the password for '" + userName + "': "));
            }
            client.setCredentials(userName, password);
            return client;
        }

        throw new GitHubException("No authentication credentials configured");
    }

    /**
     * Create client
     * <p>
     * Subclasses can override to do any custom client configuration
     *
     * @param hostname
     * @return non-null client
     * @throws MojoExecutionException
     */
    private GitHubClient createClient(String hostname) throws GitHubException {
        if (!hostname.contains("://"))
            return new GitHubClient(hostname);
        try {
            URL hostUrl = new URL(hostname);
            return new GitHubClient(hostUrl.getHost(), hostUrl.getPort(), hostUrl.getProtocol());
        } catch (MalformedURLException e) {
            throw new GitHubException("Could not parse host URL " + hostname, e);
        }
    }

    /**
     * Get repository and throw a {@link MojoExecutionException} on failures
     *
     * @param project
     * @param owner
     * @param name
     * @return non-null repository id
     * @throws MojoExecutionException
     */
    private RepositoryId getRepository(final String owner, final String name) throws GitHubException {
        RepositoryId repository = null;
        if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(owner)) {
            repository = RepositoryId.create(owner, name);
        } else {
            throw new GitHubException("No GitHub repository (owner and name) configured");
        }
        if (log.isDebugEnabled()) {
            log.debug(MessageFormat.format("Using GitHub repository {0}", repository.generateId()));
        }
        return repository;
    }

    private String[] getPaths(File outputDirectory) {
        // Find files to include
        String baseDir = outputDirectory.getAbsolutePath();
        String[] includePaths = removeEmpties(includes);
        String[] excludePaths = removeEmpties(excludes);
        if (log.isDebugEnabled()) {
            log.debug(MessageFormat.format("Scanning {0} and including {1} and exluding {2}", baseDir,
                    Arrays.toString(includePaths), Arrays.toString(excludePaths)));
        }

        String[] paths = getMatchingPaths(includePaths, excludePaths, baseDir);

        // Convert separator to forward slash '/'
        if ('\\' == File.separatorChar) {
            for (int i = 0; i < paths.length; i++) {
                paths[i] = paths[i].replace('\\', '/');
                //FilenameUtils.separatorsToUnix(paths[i]);
            }
        }

        if (paths.length != 1) {
            log.info(MessageFormat.format("Creating {0} blobs", paths.length));
        } else {
            log.info("Creating 1 blob");
        }

        if (log.isDebugEnabled()) {
            log.debug(MessageFormat.format("Scanned files to include: {0}", Arrays.toString(paths)));
        }

        return paths;
    }

    /**
     * Create an array with only the non-null and non-empty values
     *
     * @param values
     * @return non-null but possibly empty array of non-null/non-empty strings
     */
    public static String[] removeEmpties(final String... values) {
        if (values == null || values.length == 0) {
            return new String[0];
        }
        List<String> validValues = new ArrayList<String>();
        for (String value : values) {
            if (value != null && value.length() > 0) {
                validValues.add(value);
            }
        }
        return validValues.toArray(new String[validValues.size()]);
    }

    /**
     * Get matching paths found in given base directory
     *
     * @param includes
     * @param excludes
     * @param baseDir
     * @return non-null but possibly empty array of string paths relative to the
     *         base directory
     */
    public static String[] getMatchingPaths(final String[] includes, final String[] excludes,
            final String baseDir) {
        DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir(baseDir);
        if (includes != null && includes.length > 0) {
            scanner.setIncludes(includes);
        }
        if (excludes != null && excludes.length > 0) {
            scanner.setExcludes(excludes);
        }
        scanner.scan();
        return scanner.getIncludedFiles();
    }
}