/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.source.extractor.extract.restapi; import java.io.IOException; import java.util.Arrays; import java.util.List; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.util.EntityUtils; import com.google.common.base.Charsets; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import gobblin.configuration.ConfigurationKeys; import gobblin.configuration.State; import gobblin.http.HttpClientConfiguratorLoader; import gobblin.source.extractor.exception.RestApiConnectionException; import gobblin.source.extractor.exception.RestApiProcessingException; import gobblin.source.extractor.extract.Command; import gobblin.source.extractor.extract.CommandOutput; import gobblin.source.extractor.extract.restapi.RestApiCommand.RestApiCommandType; import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** * A class for connecting to Rest APIs, construct queries and getting responses. */ @Slf4j public abstract class RestApiConnector { public static final String REST_API_CONNECTOR_CLASS = "rest.api.connector.class"; protected static final Gson GSON = new Gson(); protected HttpClient httpClient = null; protected boolean autoEstablishAuthToken = false; @Setter protected long authTokenTimeout; protected String accessToken = null; protected long createdAt; protected String instanceUrl; protected String updatedQuery; protected final State state; public RestApiConnector(State state) { this.state = state; this.authTokenTimeout = state.getPropAsInt(ConfigurationKeys.SOURCE_CONN_TIMEOUT, ConfigurationKeys.DEFAULT_CONN_TIMEOUT); } /** * get http connection * @return true if the connection is success else false */ public boolean connect() throws RestApiConnectionException { if (this.autoEstablishAuthToken) { if (this.authTokenTimeout <= 0) { return false; } else if ((System.currentTimeMillis() - this.createdAt) > this.authTokenTimeout) { return false; } } HttpEntity httpEntity = null; try { httpEntity = getAuthentication(); if (httpEntity != null) { JsonElement json = GSON.fromJson(EntityUtils.toString(httpEntity), JsonObject.class); if (json == null) { log.error("Http entity: " + httpEntity); log.error("entity class: " + httpEntity.getClass().getName()); log.error("entity string size: " + EntityUtils.toString(httpEntity).length()); log.error("content length: " + httpEntity.getContentLength()); log.error("content: " + IOUtils.toString(httpEntity.getContent(), Charsets.UTF_8)); throw new RestApiConnectionException( "JSON is NULL ! Failed on authentication with the following HTTP response received:\n" + EntityUtils.toString(httpEntity)); } JsonObject jsonRet = json.getAsJsonObject(); log.info("jsonRet: " + jsonRet.toString()); parseAuthenticationResponse(jsonRet); } } catch (IOException e) { throw new RestApiConnectionException("Failed to get rest api connection; error - " + e.getMessage(), e); } finally { if (httpEntity != null) { try { EntityUtils.consume(httpEntity); } catch (IOException e) { throw new RestApiConnectionException("Failed to consume httpEntity; error - " + e.getMessage(), e); } } } return true; } protected HttpClient getHttpClient() { if (this.httpClient == null) { HttpClientConfiguratorLoader configuratorLoader = new HttpClientConfiguratorLoader(this.state); this.httpClient = configuratorLoader.getConfigurator() .setStatePropertiesPrefix(ConfigurationKeys.SOURCE_CONN_PREFIX) .configure(this.state) .createClient(); } return this.httpClient; } private static boolean hasId(JsonObject json) { if (json.has("id") || json.has("Id") || json.has("ID") || json.has("iD")) { return true; } return false; } /** * get http response in json format using url * @return json string with the response */ public CommandOutput<?, ?> getResponse(List<Command> cmds) throws RestApiProcessingException { String url = cmds.get(0).getParams().get(0); log.info("URL: " + url); String jsonStr = null; HttpRequestBase httpRequest = new HttpGet(url); addHeaders(httpRequest); HttpEntity httpEntity = null; HttpResponse httpResponse = null; try { httpResponse = this.httpClient.execute(httpRequest); StatusLine status = httpResponse.getStatusLine(); httpEntity = httpResponse.getEntity(); if (httpEntity != null) { jsonStr = EntityUtils.toString(httpEntity); } if (status.getStatusCode() >= 400) { log.info("Unable to get response using: " + url); JsonElement jsonRet = GSON.fromJson(jsonStr, JsonArray.class); throw new RestApiProcessingException(getFirstErrorMessage("Failed to retrieve response from", jsonRet)); } } catch (Exception e) { throw new RestApiProcessingException("Failed to process rest api request; error - " + e.getMessage(), e); } finally { try { if (httpEntity != null) { EntityUtils.consume(httpEntity); } // httpResponse.close(); } catch (Exception e) { throw new RestApiProcessingException("Failed to consume httpEntity; error - " + e.getMessage(), e); } } CommandOutput<RestApiCommand, String> output = new RestApiCommandOutput(); output.put((RestApiCommand) cmds.get(0), jsonStr); return output; } protected void addHeaders(HttpRequestBase httpRequest) { if (this.accessToken != null) { httpRequest.addHeader("Authorization", "OAuth " + this.accessToken); } httpRequest.addHeader("Content-Type", "application/json"); //httpRequest.addHeader("Accept-Encoding", "zip"); //httpRequest.addHeader("Content-Encoding", "gzip"); //httpRequest.addHeader("Connection", "Keep-Alive"); //httpRequest.addHeader("Keep-Alive", "timeout=60000"); } /** * get error message while executing http url * @return error message */ private static String getFirstErrorMessage(String defaultMessage, JsonElement json) { if (json == null) { return defaultMessage; } JsonObject jsonObject = null; if (!json.isJsonArray()) { jsonObject = json.getAsJsonObject(); } else { JsonArray jsonArray = json.getAsJsonArray(); if (jsonArray.size() != 0) { jsonObject = jsonArray.get(0).getAsJsonObject(); } } if (jsonObject != null) { if (jsonObject.has("error_description")) { defaultMessage = defaultMessage + jsonObject.get("error_description").getAsString(); } else if (jsonObject.has("message")) { defaultMessage = defaultMessage + jsonObject.get("message").getAsString(); } } return defaultMessage; } /** * Build a list of {@link Command}s given a String Rest query. */ public static List<Command> constructGetCommand(String restQuery) { return Arrays.asList(new RestApiCommand().build(Arrays.asList(restQuery), RestApiCommandType.GET)); } public boolean isConnectionClosed() { return this.httpClient == null; } /** * To be overridden by subclasses that require authentication. */ public abstract HttpEntity getAuthentication() throws RestApiConnectionException; protected void parseAuthenticationResponse(JsonObject jsonRet) throws RestApiConnectionException { if (!hasId(jsonRet)) { throw new RestApiConnectionException("Failed on authentication with the following HTTP response received:" + jsonRet.toString()); } this.instanceUrl = jsonRet.get("instance_url").getAsString(); this.accessToken = jsonRet.get("access_token").getAsString(); this.createdAt = System.currentTimeMillis(); } }