org.coding.git.api.CodingNetConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.coding.git.api.CodingNetConnection.java

Source

/*
 * Copyright 2016 Coding
 *
 * 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.coding.git.api;

import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.util.net.IdeHttpClientHelpers;
import com.intellij.util.net.ssl.CertificateManager;
import org.apache.http.*;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.*;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.coding.git.CodingNetOpenAPICodeMsg;
import org.coding.git.exceptions.*;
import org.coding.git.util.CodingNetSettings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.coding.git.util.CodingNetAuthData;
import org.coding.git.util.CodingNetUrlUtil;
import org.coding.git.util.CodingNetUtil;

import javax.net.ssl.SSLHandshakeException;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.security.cert.CertificateException;
import java.util.*;
import java.util.List;

import static org.coding.git.api.CodingNetApiUtil.createDataFromRaw;
import static org.coding.git.api.CodingNetApiUtil.fromJson;

/**
 * ?HttpClient
 *
 * @author robin
 *
 *  * Based on https://github.com/JetBrains/intellij-community/blob/master/plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java
 * @author JetBrains s.r.o.
 */
public class CodingNetConnection {
    private static final Logger LOG = CodingNetUtil.LOG;

    private static final HttpRequestInterceptor PREEMPTIVE_BASIC_AUTH = new PreemptiveBasicAuthInterceptor();

    @NotNull
    private final String myHost;
    @NotNull
    private final CloseableHttpClient myClient;
    private final boolean myReusable;

    private volatile HttpUriRequest myRequest;
    private volatile boolean myAborted;

    @TestOnly
    public CodingNetConnection(@NotNull CodingNetAuthData auth) {
        this(auth, false);
    }

    public CodingNetConnection(@NotNull CodingNetAuthData auth, boolean reusable) {
        myHost = auth.getHost();
        myClient = createClient(auth);
        myReusable = reusable;
    }

    public enum HttpVerb {
        GET, POST, DELETE, HEAD, PATCH
    }

    @Nullable
    public JsonElement getRequest(@NotNull String path, @NotNull Header... headers) throws IOException {
        return request(path, null, Arrays.asList(headers), HttpVerb.GET).getJsonElement();
    }

    @Nullable
    public JsonElement postRequest(@NotNull String path, @Nullable String requestBody, @NotNull Header... headers)
            throws IOException {
        return request(path, requestBody, Arrays.asList(headers), HttpVerb.POST).getJsonElement();
    }

    public ResponsePage doPostRequest(@NotNull String path, @Nullable String requestBody,
            @NotNull Header... headers) throws IOException {
        return request(path, requestBody, Arrays.asList(headers), HttpVerb.POST);
    }

    @Nullable
    public JsonElement patchRequest(@NotNull String path, @Nullable String requestBody, @NotNull Header... headers)
            throws IOException {
        return request(path, requestBody, Arrays.asList(headers), HttpVerb.PATCH).getJsonElement();
    }

    @Nullable
    public JsonElement deleteRequest(@NotNull String path, @NotNull Header... headers) throws IOException {
        return request(path, null, Arrays.asList(headers), HttpVerb.DELETE).getJsonElement();
    }

    @NotNull
    public Header[] headRequest(@NotNull String path, @NotNull Header... headers) throws IOException {
        return request(path, null, Arrays.asList(headers), HttpVerb.HEAD).getHeaders();
    }

    @NotNull
    public String getHost() {
        return myHost;
    }

    public void abort() {
        if (myAborted)
            return;
        myAborted = true;

        HttpUriRequest request = myRequest;
        if (request != null)
            request.abort();
    }

    public void close() throws IOException {
        myClient.close();
    }

    @NotNull
    private static CloseableHttpClient createClient(@NotNull CodingNetAuthData auth) {
        HttpClientBuilder builder = HttpClients.custom();

        return builder.setDefaultRequestConfig(createRequestConfig(auth))
                .setDefaultConnectionConfig(createConnectionConfig(auth))
                //.setDefaultCredentialsProvider(createCredentialsProvider(auth))
                .setDefaultHeaders(createHeaders(auth)).addInterceptorFirst(PREEMPTIVE_BASIC_AUTH)
                .setSslcontext(CertificateManager.getInstance().getSslContext())
                //--cookie
                .setDefaultCookieStore(createCookieStore(auth))
                .setHostnameVerifier((X509HostnameVerifier) CertificateManager.HOSTNAME_VERIFIER).build();
    }

    private static CookieStore createCookieStore(CodingNetAuthData auth) {
        BasicCookieStore cookieStore = new BasicCookieStore();
        CodingNetAuthData.BasicAuth basicAuth = auth.getBasicAuth();
        if (basicAuth != null) {
            String code = basicAuth.getSid();
            if (code != null) {
                BasicClientCookie cookie = new BasicClientCookie("sid", code);
                cookie.setDomain(".coding.net");
                cookie.setPath("/");
                cookie.setSecure(true);
                cookieStore.addCookie(cookie);
            }
        }
        return cookieStore;
    }

    @NotNull
    private static RequestConfig createRequestConfig(@NotNull CodingNetAuthData auth) {
        RequestConfig.Builder builder = RequestConfig.custom();

        int timeout = CodingNetSettings.getInstance().getConnectionTimeout();
        builder.setConnectTimeout(timeout).setSocketTimeout(timeout);

        if (auth.isUseProxy()) {
            IdeHttpClientHelpers.ApacheHttpClient4.setProxyForUrlIfEnabled(builder, auth.getHost());
        }

        return builder.build();
    }

    @NotNull
    private static ConnectionConfig createConnectionConfig(@NotNull CodingNetAuthData auth) {
        return ConnectionConfig.custom().setCharset(Consts.UTF_8).build();
    }

    @NotNull
    private static CredentialsProvider createCredentialsProvider(@NotNull CodingNetAuthData auth) {
        CredentialsProvider provider = new BasicCredentialsProvider();
        // Basic authentication
        CodingNetAuthData.BasicAuth basicAuth = auth.getBasicAuth();
        if (basicAuth != null) {
            provider.setCredentials(AuthScope.ANY,
                    new UsernamePasswordCredentials(basicAuth.getLogin(), basicAuth.getPassword()));
        }

        if (auth.isUseProxy()) {
            IdeHttpClientHelpers.ApacheHttpClient4.setProxyCredentialsForUrlIfEnabled(provider, auth.getHost());
        }

        return provider;
    }

    @NotNull
    private static Collection<? extends Header> createHeaders(@NotNull CodingNetAuthData auth) {
        List<Header> headers = new ArrayList<Header>();
        CodingNetAuthData.TokenAuth tokenAuth = auth.getTokenAuth();
        if (tokenAuth != null) {
            headers.add(new BasicHeader("Authorization", "token " + tokenAuth.getToken()));
        }
        CodingNetAuthData.BasicAuth basicAuth = auth.getBasicAuth();
        if (basicAuth != null && basicAuth.getSid() != null) {
            headers.add(new BasicHeader("X-GitHub-OTP", basicAuth.getSid()));
        }
        return headers;
    }

    @NotNull
    private static String getRequestUrl(@NotNull String host, @NotNull String path) {
        return CodingNetUrlUtil.getApiUrl(host) + path;
    }

    @NotNull
    private ResponsePage request(@NotNull String path, @Nullable String requestBody,
            @NotNull Collection<Header> headers, @NotNull HttpVerb verb) throws IOException {
        return doRequest(getRequestUrl(myHost, path), requestBody, headers, verb);
    }

    @NotNull
    private ResponsePage doRequest(@NotNull String uri, @Nullable String requestBody,
            @NotNull Collection<Header> headers, @NotNull HttpVerb verb) throws IOException {
        if (myAborted)
            throw new CodingNetOperationCanceledException();

        if (EventQueue.isDispatchThread() && !ApplicationManager.getApplication().isUnitTestMode()) {
            LOG.warn("Network operation in EDT"); // TODO: fix
        }

        CloseableHttpResponse response = null;
        try {
            response = doREST(uri, requestBody, headers, verb);

            if (myAborted)
                throw new CodingNetOperationCanceledException();

            //--HTTP??
            checkStatusCode(response, requestBody);

            HttpEntity entity = response.getEntity();
            if (entity == null) {
                return createResponse(response);
            }

            JsonElement ret = parseResponse(entity.getContent());
            if (ret.isJsonNull()) {
                return createResponse(response);
            }

            //--CodingNet??
            checkCodingNetCode(ret);

            String nextPage = null;
            Header pageHeader = response.getFirstHeader("Link");
            if (pageHeader != null) {
                for (HeaderElement element : pageHeader.getElements()) {
                    NameValuePair rel = element.getParameterByName("rel");
                    if (rel != null && "next".equals(rel.getValue())) {
                        String urlString = element.toString();
                        int begin = urlString.indexOf('<');
                        int end = urlString.lastIndexOf('>');
                        if (begin == -1 || end == -1) {
                            LOG.error("Invalid 'Link' header", "{" + pageHeader.toString() + "}");
                            break;
                        }

                        nextPage = urlString.substring(begin + 1, end);
                        break;
                    }
                }
            }

            return createResponse(ret, nextPage, response);
        } catch (SSLHandshakeException e) { // User canceled operation from CertificateManager
            if (e.getCause() instanceof CertificateException) {
                LOG.info("Host SSL certificate is not trusted", e);
                throw new CodingNetOperationCanceledException("Host SSL certificate is not trusted", e);
            }
            throw e;
        } catch (IOException e) {
            if (myAborted)
                throw new CodingNetOperationCanceledException("Operation canceled", e);
            throw e;
        } finally {
            myRequest = null;
            if (response != null) {
                response.close();
            }
            if (!myReusable) {
                myClient.close();
            }
        }
    }

    /**
     * OpenAPI?
     *
     * @param jsonElement
     */
    private void checkCodingNetCode(JsonElement jsonElement) throws IOException {
        int code = jsonElement.getAsJsonObject().get("code").getAsInt();
        System.out.println("Current Code is: " + code + " JsonElement is:" + jsonElement.toString());
        if (code == CodingNetOpenAPICodeMsg.NO_LOGIN.getCode()) {
            CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg codingNetOpenAPICodeMsg = (CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg) fromJson(
                    jsonElement, CodingNetOpenAPICodeMsg.NO_LOGIN.getClazz());
            throw new CodingNetAuthenticationException(codingNetOpenAPICodeMsg.getMessage());
        } else if (code == CodingNetOpenAPICodeMsg.NO_EXIST_USER.getCode()) {
            CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg codingNetOpenAPICodeMsg = (CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg) fromJson(
                    jsonElement, CodingNetOpenAPICodeMsg.NO_EXIST_USER.getClazz());
            throw new CodingNetAuthenticationException(codingNetOpenAPICodeMsg.getMessage());
        } else if (code == CodingNetOpenAPICodeMsg.NEED_VERIFICATION_CODE.getCode()) {
            CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg codingNetOpenAPICodeMsg = (CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg) fromJson(
                    jsonElement, CodingNetOpenAPICodeMsg.NEED_VERIFICATION_CODE.getClazz());
            throw new CodingNetAuthenticationException(codingNetOpenAPICodeMsg.getMessage());
        } else if (code == CodingNetOpenAPICodeMsg.USER_LOCKED.getCode()) {
            CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg codingNetOpenAPICodeMsg = (CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg) fromJson(
                    jsonElement, CodingNetOpenAPICodeMsg.USER_LOCKED.getClazz());
            throw new CodingNetAuthenticationException(codingNetOpenAPICodeMsg.getMessage());
        } else if (code == CodingNetOpenAPICodeMsg.USER_PASSWORD_NO_CORRECT.getCode()) {
            CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg codingNetOpenAPICodeMsg = (CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg) fromJson(
                    jsonElement, CodingNetOpenAPICodeMsg.USER_PASSWORD_NO_CORRECT.getClazz());
            throw new CodingNetAuthenticationException(codingNetOpenAPICodeMsg.getMessage());
        } else if (code == CodingNetOpenAPICodeMsg.AUTH_ERROR.getCode()) {
            CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg codingNetOpenAPICodeMsg = (CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg) fromJson(
                    jsonElement, CodingNetOpenAPICodeMsg.AUTH_ERROR.getClazz());
            throw new CodingNetTwoFactorAuthenticationException(codingNetOpenAPICodeMsg.getMessage());
        } else if (code == CodingNetOpenAPICodeMsg.NEED_TWO_FACTOR_AUTH_CODE.getCode()
                || code == CodingNetOpenAPICodeMsg.TWO_FACTOR_AUTH_CODE_REQUIRED.getCode()) {
            return;
        } else if (code == CodingNetOpenAPICodeMsg.LOGIN_EXPIRED.getCode()) {
            CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg codingNetOpenAPICodeMsg = (CodingNetOpenAPICodeMsg.ICodingNetOpenAPICodeMsg) fromJson(
                    jsonElement, CodingNetOpenAPICodeMsg.LOGIN_EXPIRED.getClazz());
            throw new CodingNetTwoFactorAuthenticationException(codingNetOpenAPICodeMsg.getMessage());
        } else if (code != 0) {
            throw new CodingNetAuthenticationException();
        }
    }

    @NotNull
    private CloseableHttpResponse doREST(@NotNull final String uri, @Nullable final String requestBody,
            @NotNull final Collection<Header> headers, @NotNull final HttpVerb verb) throws IOException {
        HttpRequestBase request;
        switch (verb) {
        case POST:
            request = new HttpPost(uri);
            if (requestBody != null) {
                ((HttpPost) request).setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
            }
            break;
        case PATCH:
            request = new HttpPatch(uri);
            if (requestBody != null) {
                ((HttpPatch) request).setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
            }
            break;
        case GET:
            request = new HttpGet(uri);
            break;
        case DELETE:
            request = new HttpDelete(uri);
            break;
        case HEAD:
            request = new HttpHead(uri);
            break;
        default:
            throw new IllegalStateException("Unknown HttpVerb: " + verb.toString());
        }

        for (Header header : headers) {
            request.addHeader(header);
        }

        myRequest = request;
        return myClient.execute(request);
    }

    private static void checkStatusCode(@NotNull CloseableHttpResponse response, @Nullable String body)
            throws IOException {
        int code = response.getStatusLine().getStatusCode();
        switch (code) {
        case HttpStatus.SC_OK:
        case HttpStatus.SC_CREATED:
        case HttpStatus.SC_ACCEPTED:
        case HttpStatus.SC_NO_CONTENT:
            return;
        case HttpStatus.SC_UNAUTHORIZED:
        case HttpStatus.SC_PAYMENT_REQUIRED:
        case HttpStatus.SC_FORBIDDEN:
            //noinspection ThrowableResultOfMethodCallIgnored
            CodingNetStatusCodeException error = getStatusCodeException(response);

            //        Header headerOTP = response.getFirstHeader("X-GitHub-OTP");
            //        if (headerOTP != null) {
            //          for (HeaderElement element : headerOTP.getElements()) {
            //            if ("required".equals(element.getName())) {
            //              throw new CodingNetTwoFactorAuthenticationException(error.getMessage());
            //            }
            //          }
            //        }
            //
            //        if (error.getError() != null && error.getError().containsReasonMessage("API rate limit exceeded")) {
            //          throw new GithubRateLimitExceededException(error.getMessage());
            //        }

            throw new CodingNetAuthenticationException("Request response: " + error.getMessage());
        case HttpStatus.SC_BAD_REQUEST:
        case HttpStatus.SC_UNPROCESSABLE_ENTITY:
            LOG.info("body message:" + body);
            throw getStatusCodeException(response);
        default:
            throw getStatusCodeException(response);
        }
    }

    @NotNull
    private static CodingNetStatusCodeException getStatusCodeException(@NotNull CloseableHttpResponse response) {
        StatusLine statusLine = response.getStatusLine();
        try {
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                CodingNetErrorMessage error = fromJson(parseResponse(entity.getContent()),
                        CodingNetErrorMessage.class);
                String message = statusLine.getReasonPhrase() + " - " + error.getMessage();
                return new CodingNetStatusCodeException(message, error, statusLine.getStatusCode());
            }
        } catch (IOException e) {
            LOG.info(e);
        }
        return new CodingNetStatusCodeException(statusLine.getReasonPhrase(), statusLine.getStatusCode());
    }

    @NotNull
    private static JsonElement parseResponse(@NotNull InputStream codingResponse) throws IOException {
        Reader reader = new InputStreamReader(codingResponse, CharsetToolkit.UTF8_CHARSET);
        try {
            return new JsonParser().parse(reader);
        } catch (JsonParseException jse) {
            throw new CodingNetJsonException("Couldn't parse Coding response", jse);
        } finally {
            reader.close();
        }
    }

    public static class PagedRequest<T> {
        @NotNull
        private String myPath;
        @NotNull
        private final Collection<Header> myHeaders;
        @NotNull
        private final Class<T> myResult;
        @NotNull
        private final Class<? extends ICodingNetDataConstructor[]> myRawArray;

        private boolean myFirstRequest = true;
        @Nullable
        private String myNextPage;

        public PagedRequest(@NotNull String path, @NotNull Class<T> result,
                @NotNull Class<? extends ICodingNetDataConstructor[]> rawArray, @NotNull Header... headers) {
            myPath = path;
            myResult = result;
            myRawArray = rawArray;
            myHeaders = Arrays.asList(headers);
        }

        @NotNull
        public List<T> next(@NotNull CodingNetConnection connection) throws IOException {
            String url;
            if (myFirstRequest) {
                url = getRequestUrl(connection.getHost(), myPath);
                myFirstRequest = false;
            } else {
                if (myNextPage == null)
                    throw new NoSuchElementException();
                url = myNextPage;
                myNextPage = null;
            }

            ResponsePage response = connection.doRequest(url, null, myHeaders, HttpVerb.GET);

            if (response.getJsonElement() == null) {
                throw new CodingNetConfusingException("Empty response");
            }

            //--CodingJson?JSONArray,
            if (response.getJsonElement().isJsonArray()) {
                throw new CodingNetJsonException("Wrong json type: expected JsonArray",
                        new Exception(response.getJsonElement().toString()));
            }

            myNextPage = response.getNextPage();

            List<T> result = new ArrayList<T>();
            JsonElement jsonElement = response.getJsonElement().getAsJsonObject().get("data").getAsJsonObject()
                    .get("list");
            if (!jsonElement.isJsonArray()) {
                throw new CodingNetJsonException("Wrong json type: expected JsonArray",
                        new Exception(response.getJsonElement().toString()));
            }
            for (ICodingNetDataConstructor raw : fromJson(jsonElement, myRawArray)) {
                result.add(createDataFromRaw(raw, myResult));
            }
            return result;
        }

        public boolean hasNext() {
            return myFirstRequest || myNextPage != null;
        }

        @NotNull
        public List<T> getAll(@NotNull CodingNetConnection connection) throws IOException {
            List<T> result = new ArrayList<T>();
            while (hasNext()) {
                result.addAll(next(connection));
            }
            return result;
        }
    }

    private ResponsePage createResponse(@NotNull CloseableHttpResponse response)
            throws CodingNetOperationCanceledException {
        if (myAborted)
            throw new CodingNetOperationCanceledException();

        return new ResponsePage(null, null, response.getAllHeaders());
    }

    private ResponsePage createResponse(@NotNull JsonElement ret, @Nullable String path,
            @NotNull CloseableHttpResponse response) throws CodingNetOperationCanceledException {
        if (myAborted)
            throw new CodingNetOperationCanceledException();

        return new ResponsePage(ret, path, response.getAllHeaders());
    }

    public static class ResponsePage {
        @Nullable
        private final JsonElement myResponse;
        @Nullable
        private final String myNextPage;
        @NotNull
        private final Header[] myHeaders;

        public ResponsePage(@Nullable JsonElement response, @Nullable String next, @NotNull Header[] headers) {
            myResponse = response;
            myNextPage = next;
            myHeaders = headers;
        }

        @Nullable
        public JsonElement getJsonElement() {
            return myResponse;
        }

        @Nullable
        public String getNextPage() {
            return myNextPage;
        }

        @NotNull
        public Header[] getHeaders() {
            return myHeaders;
        }
    }

    private static class PreemptiveBasicAuthInterceptor implements HttpRequestInterceptor {
        @Override
        public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
            CredentialsProvider provider = (CredentialsProvider) context
                    .getAttribute(HttpClientContext.CREDS_PROVIDER);
            Credentials credentials = provider.getCredentials(AuthScope.ANY);
            if (credentials != null) {
                request.addHeader(new BasicScheme(Consts.UTF_8).authenticate(credentials, request, context));
            }
        }
    }
}