mobi.jenkinsci.alm.assembla.client.AssemblaClient.java Source code

Java tutorial

Introduction

Here is the source code for mobi.jenkinsci.alm.assembla.client.AssemblaClient.java

Source

// Copyright (C) 2013 GerritForge www.gerritforge.com
//
// 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 mobi.jenkinsci.alm.assembla.client;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.List;

import mobi.jenkinsci.alm.assembla.objects.AssemblaMilestone;
import mobi.jenkinsci.alm.assembla.objects.AssemblaMilestones;
import mobi.jenkinsci.alm.assembla.objects.AssemblaSpace;
import mobi.jenkinsci.alm.assembla.objects.AssemblaSpaces;
import mobi.jenkinsci.alm.assembla.objects.AssemblaTicket;
import mobi.jenkinsci.alm.assembla.objects.AssemblaTicketComment;
import mobi.jenkinsci.alm.assembla.objects.AssemblaTickets;
import mobi.jenkinsci.net.*;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpCoreContext;
import org.apache.log4j.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.inject.Inject;

public class AssemblaClient {
    private static Logger LOG = Logger.getLogger(AssemblaClient.class);
    private static URL ASSEMBLA_SITE;
    static {
        try {
            ASSEMBLA_SITE = new URL("https://api.assembla.com");
        } catch (final MalformedURLException e) {
            // This cannot happen as URL string is hardcoded
            LOG.fatal("Cannot get Assembla URL", e);
        }
    }
    private static String ASSEMBLA_SITE_APP_AUTH = "https://%s:%s@api.assembla.com";
    private static final String MY_SPACES = "/v1/spaces.json";
    private static final String SPACE_TICKETS = "/v1/spaces/%s/tickets.json";
    private static final String SPACE_TICKET_COMMENTS = "/v1/spaces/%s/tickets/%d/ticket_comments.json";
    private static final String SPACE_MILESTONES = "/v1/spaces/%s/milestones.json";
    private static final String AUTH = "/authorization?client_id=%s&response_type=pin_code";
    private static final String PIN_AUTH = "/token?grant_type=pin_code&pin_code=%s";
    private static final String TOKEN_REFRESH = "/token?grant_type=refresh_token&refresh_token=%s";
    private static final String LOGIN = "/login";

    private final String appId;
    private final String appSecret;
    private final String username;
    private final String password;
    private AssemblaAccessToken accessToken;

    @Inject
    private HttpClientFactory httpClientFactory;

    private final HttpClient httpClient;
    private final HttpContext httpContext;
    private static Gson gson;
    static {
        final GsonBuilder gsonB = new GsonBuilder();
        gsonB.registerTypeAdapter(GregorianCalendar.class, new CalendarSerializer());
        gson = gsonB.create();
    }

    public AssemblaClient(final String appId, final String appSecret, final String username,
            final String password) {
        this.appId = appId;
        this.appSecret = appSecret;
        this.httpClient = httpClientFactory.getHttpClient();
        httpContext = new BasicHttpContext();
        final CookieStore cookieStore = new BasicCookieStore();
        httpContext.setAttribute(HttpClientContext.COOKIE_STORE, cookieStore);

        this.username = username;
        this.password = password;
    }

    private URL getLatestRedirectedUrl() {
        try {
            final HttpRequest request = (HttpRequest) httpContext.getAttribute(HttpCoreContext.HTTP_REQUEST);
            final HttpHost host = (HttpHost) httpContext.getAttribute(HttpCoreContext.HTTP_TARGET_HOST);
            if (host.getPort() <= 0) {
                return new URL(host.getSchemeName(), host.getHostName(), request.getRequestLine().getUri());
            } else {
                return new URL(host.getSchemeName(), host.getHostName(), host.getPort(),
                        request.getRequestLine().getUri());
            }
        } catch (final MalformedURLException e) {
            throw new IllegalArgumentException("Cannot get last redirected URL from HTTP Context", e);
        }
    }

    public void login() throws IOException {
        Document pinDoc = Jsoup.parse(getData(String.format(AUTH, appId), false));
        if (getLatestRedirectedUrl().getPath().startsWith(LOGIN)) {
            pinDoc = postLoginForm(pinDoc);
        }

        final Element pinBox = pinDoc.select("div[class=box]").first();
        if (pinBox == null) {
            throw new IOException("Missing PIN code from Assembla auth response");
        }
        final Element pinLabel = pinBox.select("p").first();
        final Element pinValue = pinBox.select("h1").first();
        if (pinLabel == null || pinValue == null) {
            throw new IOException("Missing PIN code from Assembla auth response");
        }
        final String pin = pinValue.childNode(0).toString();
        final HttpPost authPost = new HttpPost(
                String.format(ASSEMBLA_SITE_APP_AUTH, appId, appSecret) + String.format(PIN_AUTH, pin));
        final HttpResponse pinResponse = httpClient.execute(authPost);
        try {
            if (pinResponse.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_OK) {
                throw new IOException(
                        "Post " + authPost.getURI() + " for a PIN failed: " + pinResponse.getStatusLine());
            }
            accessToken = gson.fromJson(
                    new JsonReader(new InputStreamReader(pinResponse.getEntity().getContent(), "UTF-8")),
                    AssemblaAccessToken.class);
        } finally {
            authPost.releaseConnection();
        }
    }

    public void loginRefresh() throws IOException {
        if (accessToken == null) {
            login();
        } else {
            accessToken.access_token = "";
            final HttpPost authPost = new HttpPost(String.format(ASSEMBLA_SITE_APP_AUTH, appId, appSecret)
                    + String.format(TOKEN_REFRESH, accessToken.refresh_token));
            final HttpResponse response = httpClient.execute(authPost);
            try {
                if (response.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_OK) {
                    throw new IOException(
                            "Post " + authPost.getURI() + " for Token refresh failed: " + response.getStatusLine());
                }
                final AssemblaAccessToken refreshToken = gson.fromJson(
                        new JsonReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8")),
                        AssemblaAccessToken.class);
                accessToken.renew(refreshToken);
            } finally {
                authPost.releaseConnection();
            }
        }
    }

    private Document postLoginForm(final Document pinDoc) throws IOException {
        final List<NameValuePair> formNvps = new ArrayList<NameValuePair>();
        final Element form = pinDoc.select("form[id=login-box]").first();
        final String formAction = form.attr("action");
        final HttpPost formPost = new HttpPost(getUrl(formAction).toString());
        final Elements formFields = form.select("input");
        for (final Element element : formFields) {
            final String fieldName = element.attr("name");
            String fieldValue = element.attr("value");
            final String fieldId = element.attr("id");
            final String fieldType = element.attr("type");

            if (fieldId.equalsIgnoreCase("user_login")) {
                fieldValue = username;
                ;
            } else if (fieldId.equalsIgnoreCase("user_password")) {
                fieldValue = password;
            }

            if (fieldType.equals("submit")) {
                if (!fieldName.equalsIgnoreCase("commit")) {
                    continue;
                }
            }

            LOG.debug(String.format("Processing form field: name='%s' value='%s' id='%s'", fieldName, fieldValue,
                    fieldId));
            formNvps.add(new BasicNameValuePair(fieldName, fieldValue));
        }
        try {
            formPost.setEntity(new UrlEncodedFormEntity(formNvps, "UTF-8"));
        } catch (final UnsupportedEncodingException e) {
            // This would never happen
            throw new IllegalArgumentException("UTF-8 not recognised");
        }

        HttpResponse response;
        LOG.debug("Login via posting form-data to " + formPost.getURI());
        try {
            response = sendHttpPost(formPost);
            if (response.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_MOVED_TEMP) {
                throw new IOException("Form-based login to Assembla failed: " + response.getStatusLine());
            }
            return Jsoup.parse(getData(response.getFirstHeader("Location").getValue(), false));
        } finally {
            formPost.releaseConnection();
        }
    }

    private HttpResponse sendHttpPost(final HttpPost formPost) throws IOException {
        HttpResponse response;
        response = httpClient.execute(formPost, httpContext);
        return response;
    }

    private URL getUrl(final String formAction) {
        try {
            return new URL(ASSEMBLA_SITE, formAction);
        } catch (final MalformedURLException e) {
            throw new IllegalArgumentException("Cannot create URL from formAction target " + formAction, e);
        }
    }

    public AssemblaSpaces spaces() throws IOException {
        final List<AssemblaSpace> spaces = gson.fromJson(getData(MY_SPACES, true),
                new TypeToken<List<AssemblaSpace>>() {
                }.getType());

        return (AssemblaSpaces) new AssemblaSpaces(spaces).init(this);
    }

    public AssemblaTickets getTickets(final String spaceId) throws IOException {
        final List<AssemblaTicket> tickets = gson.fromJson(getData(String.format(SPACE_TICKETS, spaceId), true),
                new TypeToken<List<AssemblaTicket>>() {
                }.getType());
        return (AssemblaTickets) new AssemblaTickets(tickets).init(this);
    }

    public AssemblaMilestones getMilestones(final String spaceId) throws Exception {
        final List<AssemblaMilestone> milestones = gson.fromJson(
                getData(String.format(SPACE_MILESTONES, spaceId), true), new TypeToken<List<AssemblaMilestone>>() {
                }.getType());
        return (AssemblaMilestones) new AssemblaMilestones(milestones).init(this);
    }

    private String getData(final String dataSuffix, final boolean autoLogin) throws IOException {
        loginWhenNecessary(autoLogin);
        final HttpGet get = new HttpGet(getTargetUrl(dataSuffix));
        setRequestHeaders(get, autoLogin);

        try {
            final HttpResponse response = httpClient.execute(get, httpContext);
            switch (response.getStatusLine().getStatusCode()) {
            case HttpURLConnection.HTTP_OK:
                final ByteArrayOutputStream bout = new ByteArrayOutputStream();
                final InputStream in = response.getEntity().getContent();
                IOUtils.copy(in, bout);
                return new String(bout.toByteArray());

            case HttpURLConnection.HTTP_NO_CONTENT:
                return null;

            case HttpURLConnection.HTTP_NOT_FOUND:
                LOG.warn("Resource at " + dataSuffix + " was not found: returning NULL");
                return null;

            case HttpURLConnection.HTTP_UNAUTHORIZED:
                if (autoLogin) {
                    loginRefresh();
                    return getData(dataSuffix, false);
                }

            default:
                throw new IOException("Cannot GET " + dataSuffix + " from Assembla: " + response.getStatusLine());
            }
        } finally {
            get.releaseConnection();
        }
    }

    private String postData(final String dataSuffix, final byte[] postData, final ContentType contentType)
            throws IOException {
        loginWhenNecessary(true);
        final HttpPost post = new HttpPost(getTargetUrl(dataSuffix));
        setRequestHeaders(post, true);
        post.setEntity(new ByteArrayEntity(postData, contentType));

        try {
            final HttpResponse response = httpClient.execute(post, httpContext);
            switch (response.getStatusLine().getStatusCode()) {
            case HttpURLConnection.HTTP_CREATED:
                final ByteArrayOutputStream bout = new ByteArrayOutputStream();
                final InputStream in = response.getEntity().getContent();
                IOUtils.copy(in, bout);
                return new String(bout.toByteArray());

            default:
                throw new IOException("Cannot POST " + dataSuffix + " to Assembla: " + response.getStatusLine());
            }
        } finally {
            post.releaseConnection();
        }
    }

    private String getTargetUrl(final String dataSuffix) {
        final String targetUrl = (dataSuffix.startsWith("http") ? dataSuffix : getUrl(dataSuffix).toString());
        return targetUrl;
    }

    private void setRequestHeaders(final HttpRequestBase req, final boolean autoLogin) {
        if (accessToken != null) {
            req.addHeader("Authorization", "Bearer " + accessToken.access_token);
        }
        if (req.getURI().getPath().endsWith(".json")) {
            req.addHeader(HttpHeaders.ACCEPT, "application/json");
        }
    }

    private void loginWhenNecessary(final boolean autoLogin) throws IOException {
        if (accessToken == null && autoLogin) {
            login();
        }
    }

    public AssemblaTicketComment addComment(final String spaceId, final int ticketNumber, final String commentText)
            throws IOException {
        final JsonObject comment = new JsonObject();
        comment.add("comment", new JsonPrimitive(commentText));
        final JsonObject ticketComment = new JsonObject();
        ticketComment.add("ticket_comment", comment);

        final AssemblaTicketComment assemblaComment = gson.fromJson(
                postData(String.format(SPACE_TICKET_COMMENTS, spaceId, ticketNumber),
                        ticketComment.toString().getBytes(), ContentType.APPLICATION_JSON),
                AssemblaTicketComment.class);
        return (AssemblaTicketComment) assemblaComment.init(this);
    }
}