GitHubPagesWagon.java

package net.trajano.wagon.git;

import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.trajano.wagon.git.internal.AbstractGitWagon;
import net.trajano.wagon.git.internal.GitUri;

import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.Wagon;
import org.codehaus.plexus.component.annotations.Component;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.xbill.DNS.CNAMERecord;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.TextParseException;
import org.xbill.DNS.Type;

/**
 * Github Pages Wagon.
 */
@Component(role = Wagon.class, hint = "github", instantiationStrategy = "per-lookup")
public class GitHubPagesWagon extends AbstractGitWagon {

    /**
     * Github pages host pattern.
     */
    private static final Pattern GITHUB_PAGES_HOST_PATTERN = Pattern
            .compile("([a-z]+)\\.github\\.io.?");

    /**
     * Github pages path pattern.
     */
    private static final Pattern GITHUB_PAGES_PATH_PATTERN = Pattern
            .compile("/([^/]+)(/.*)?");

    /**
     * Logger.
     */
    private static final Logger LOG;

    /**
     * Messages resource path.
     */
    private static final String MESSAGES = "META-INF/Messages";

    /**
     * Resource bundle.
     */
    private static final ResourceBundle R;

    static {
        LOG = Logger.getLogger("net.trajano.wagon.git", MESSAGES);
        R = ResourceBundle.getBundle(MESSAGES);
    }

    /**
     * Builds a GitUri from a GitHub Pages URL. It performs a DNS lookup for the
     * CNAME if the host does not match {@link #GITHUB_PAGES_HOST_PATTERN}.
     * {@inheritDoc}
     */
    @Override
    public GitUri buildGitUri(final URI uri) throws IOException,
            URISyntaxException {
        final URI finalUri;
        // Resolve redirects if needed.
        if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) {
            final HttpURLConnection urlConnection = (HttpURLConnection) uri
                    .toURL().openConnection();
            urlConnection.connect();
            urlConnection.getResponseCode();
            finalUri = urlConnection.getURL().toURI();
            urlConnection.disconnect();
        } else {
            finalUri = uri;
        }
        final Matcher m = GITHUB_PAGES_HOST_PATTERN.matcher(finalUri.getHost());
        final String username;
        if (m.matches()) {
            username = m.group(1);
        } else {
            final String cnameHost = getCnameForHost(finalUri.getHost());
            final Matcher m2 = GITHUB_PAGES_HOST_PATTERN.matcher(cnameHost);
            if (!m2.matches()) {
                throw new RuntimeException(String.format(
                        R.getString("invalidGitHubPagesHost"), uri));
            }
            username = m2.group(1);
        }

        if ("".equals(finalUri.getPath()) || "/".equals(finalUri.getPath())) {
            return buildRootUri(username);
        } else {
            return buildProjectUri(username, finalUri.getPath());
        }
    }

    /**
     * Builds the project Github URI.
     *
     * @param username
     *            user name
     * @param path
     *            path
     * @return URI
     */
    private GitUri buildProjectUri(final String username, final String path) {
        final Matcher pathMatcher = GITHUB_PAGES_PATH_PATTERN.matcher(path);
        pathMatcher.matches();
        final String resource = pathMatcher.group(2);
        return new GitUri("ssh://git@github.com/" + username + "/"
                + pathMatcher.group(1) + ".git", "gh-pages", resource);
    }

    /**
     * Builds the user's Github URI.
     *
     * @param username
     *            user name
     * @return URI
     */
    private GitUri buildRootUri(final String username) {
        return new GitUri("ssh://git@github.com/" + username + "/" + username
                + ".github.io.git", "master", "/");
    }

    /**
     * Gets the CNAME record for the host.
     *
     * @param host
     *            host
     * @return CNAME record may return null if not found.
     * @throws TextParseException
     */
    private String getCnameForHost(final String host) throws TextParseException {
        final Lookup lookup = new Lookup(host, Type.CNAME);
        lookup.run();
        if (lookup.getAnswers().length == 0) {
            LOG.log(Level.SEVERE, "unableToFindCNAME", new Object[] { host });
            return null;
        }
        return ((CNAMERecord) lookup.getAnswers()[0]).getTarget().toString();
    }

    /**
     * Does resolution a different way.
     *
     * @param resourceName
     *            resource name
     * @return file for the resource.
     * @throws ResourceDoesNotExistException
     */
    @Override
    public File getFileForResource(final String resourceName)
            throws GitAPIException, IOException, URISyntaxException {
        // /foo/bar/foo.git + ../bar.git == /foo/bar/bar.git + /
        // /foo/bar/foo.git + ../bar.git/abc == /foo/bar/bar.git + /abc
        final GitUri resolved = buildGitUri(URI.create(
                URI.create(getRepository().getUrl()).getSchemeSpecificPart())
                .resolve(resourceName.replace(" ", "%20")));
        Git resourceGit;
        try {
            resourceGit = getGit(resolved.getGitRepositoryUri());
        } catch (final ResourceDoesNotExistException e) {
            return null;
        }

        final File workTree = resourceGit.getRepository().getWorkTree();
        final File resolvedFile = new File(workTree, resolved.getResource());
        if (!resolvedFile.getCanonicalPath().startsWith(
                workTree.getCanonicalPath())) {
            throw new IOException(String.format(
                    "The resolved file '%s' is not in work tree '%s'",
                    resolvedFile, workTree));
        }
        return resolvedFile;
    }
}