/* * 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.salesforce; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.URI; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import org.apache.http.HttpEntity; import org.apache.http.NameValuePair; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URIBuilder; import org.apache.http.message.BasicNameValuePair; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.sforce.async.AsyncApiException; import com.sforce.async.BatchInfo; import com.sforce.async.BatchStateEnum; import com.sforce.async.BulkConnection; import com.sforce.async.ConcurrencyMode; import com.sforce.async.ContentType; import com.sforce.async.JobInfo; import com.sforce.async.OperationEnum; import com.sforce.async.QueryResultList; import com.sforce.soap.partner.PartnerConnection; import com.sforce.ws.ConnectorConfig; import gobblin.configuration.ConfigurationKeys; import gobblin.configuration.WorkUnitState; import gobblin.password.PasswordManager; import gobblin.source.extractor.DataRecordException; import gobblin.source.extractor.exception.HighWatermarkException; import gobblin.source.extractor.exception.RecordCountException; import gobblin.source.extractor.exception.RestApiClientException; import gobblin.source.extractor.exception.RestApiConnectionException; import gobblin.source.extractor.exception.SchemaException; import gobblin.source.extractor.extract.Command; import gobblin.source.extractor.extract.CommandOutput; import gobblin.source.extractor.extract.jdbc.SqlQueryUtils; import gobblin.source.extractor.extract.restapi.RestApiCommand; import gobblin.source.extractor.extract.restapi.RestApiCommand.RestApiCommandType; import gobblin.source.extractor.extract.restapi.RestApiConnector; import gobblin.source.extractor.extract.restapi.RestApiExtractor; import gobblin.source.extractor.resultset.RecordSet; import gobblin.source.extractor.resultset.RecordSetList; import gobblin.source.extractor.schema.Schema; import gobblin.source.extractor.utils.InputStreamCSVReader; import gobblin.source.extractor.utils.Utils; import gobblin.source.extractor.watermark.Predicate; import gobblin.source.extractor.watermark.WatermarkType; import gobblin.source.workunit.WorkUnit; import lombok.extern.slf4j.Slf4j; /** * An implementation of salesforce extractor for extracting data from SFDC */ @Slf4j public class SalesforceExtractor extends RestApiExtractor { private static final String SOQL_RESOURCE = "/queryAll"; private static final String SALESFORCE_TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'.000Z'"; private static final String SALESFORCE_DATE_FORMAT = "yyyy-MM-dd"; private static final String SALESFORCE_HOUR_FORMAT = "HH"; private static final String SALESFORCE_SOAP_AUTH_SERVICE = "/services/Soap/u"; private static final Gson GSON = new Gson(); private boolean pullStatus = true; private String nextUrl; private BulkConnection bulkConnection = null; private boolean bulkApiInitialRun = true; private JobInfo bulkJob = new JobInfo(); private BatchInfo bulkBatchInfo = null; private BufferedReader bulkBufferedReader = null; private List<String> bulkResultIdList = Lists.newArrayList(); private int bulkResultIdCount = 0; private boolean bulkJobFinished = true; private List<String> bulkRecordHeader; private int bulkResultColumCount; private boolean newBulkResultSet = true; private int bulkRecordCount = 0; private final SalesforceConnector sfConnector; public SalesforceExtractor(WorkUnitState state) { super(state); this.sfConnector = (SalesforceConnector) this.connector; } @Override protected RestApiConnector getConnector(WorkUnitState state) { return new SalesforceConnector(state); } /** * true is further pull required else false */ public void setPullStatus(boolean pullStatus) { this.pullStatus = pullStatus; } /** * url for the next pull from salesforce */ public void setNextUrl(String nextUrl) { this.nextUrl = nextUrl; } private boolean isBulkJobFinished() { return this.bulkJobFinished; } private void setBulkJobFinished(boolean bulkJobFinished) { this.bulkJobFinished = bulkJobFinished; } public boolean isNewBulkResultSet() { return this.newBulkResultSet; } public void setNewBulkResultSet(boolean newBulkResultSet) { this.newBulkResultSet = newBulkResultSet; } @Override public HttpEntity getAuthentication() throws RestApiConnectionException { log.debug("Authenticating salesforce"); return this.connector.getAuthentication(); } @Override public List<Command> getSchemaMetadata(String schema, String entity) throws SchemaException { log.debug("Build url to retrieve schema"); return constructGetCommand(this.sfConnector.getFullUri("/sobjects/" + entity.trim() + "/describe")); } @Override public JsonArray getSchema(CommandOutput<?, ?> response) throws SchemaException { log.info("Get schema from salesforce"); String output; Iterator<String> itr = (Iterator<String>) response.getResults().values().iterator(); if (itr.hasNext()) { output = itr.next(); } else { throw new SchemaException("Failed to get schema from salesforce; REST response has no output"); } JsonArray fieldJsonArray = new JsonArray(); JsonElement element = GSON.fromJson(output, JsonObject.class); JsonObject jsonObject = element.getAsJsonObject(); try { JsonArray array = jsonObject.getAsJsonArray("fields"); for (JsonElement columnElement : array) { JsonObject field = columnElement.getAsJsonObject(); Schema schema = new Schema(); schema.setColumnName(field.get("name").getAsString()); String dataType = field.get("type").getAsString(); String elementDataType = "string"; List<String> mapSymbols = null; JsonObject newDataType = this.convertDataType(field.get("name").getAsString(), dataType, elementDataType, mapSymbols); log.debug("ColumnName:" + field.get("name").getAsString() + "; old datatype:" + dataType + "; new datatype:" + newDataType); schema.setDataType(newDataType); schema.setLength(field.get("length").getAsLong()); schema.setPrecision(field.get("precision").getAsInt()); schema.setScale(field.get("scale").getAsInt()); schema.setNullable(field.get("nillable").getAsBoolean()); schema.setFormat(null); schema.setComment((field.get("label").isJsonNull() ? null : field.get("label").getAsString())); schema .setDefaultValue((field.get("defaultValue").isJsonNull() ? null : field.get("defaultValue").getAsString())); schema.setUnique(field.get("unique").getAsBoolean()); String jsonStr = GSON.toJson(schema); JsonObject obj = GSON.fromJson(jsonStr, JsonObject.class).getAsJsonObject(); fieldJsonArray.add(obj); } } catch (Exception e) { throw new SchemaException("Failed to get schema from salesforce; error - " + e.getMessage(), e); } return fieldJsonArray; } @Override public List<Command> getHighWatermarkMetadata(String schema, String entity, String watermarkColumn, List<Predicate> predicateList) throws HighWatermarkException { log.debug("Build url to retrieve high watermark"); String query = "SELECT " + watermarkColumn + " FROM " + entity; String defaultPredicate = " " + watermarkColumn + " != null"; String defaultSortOrder = " ORDER BY " + watermarkColumn + " desc LIMIT 1"; String existingPredicate = ""; if (this.updatedQuery != null) { String queryLowerCase = this.updatedQuery.toLowerCase(); int startIndex = queryLowerCase.indexOf(" where "); if (startIndex > 0) { existingPredicate = this.updatedQuery.substring(startIndex); } } query = query + existingPredicate; String limitString = getLimitFromInputQuery(query); query = query.replace(limitString, ""); Iterator<Predicate> i = predicateList.listIterator(); while (i.hasNext()) { Predicate predicate = i.next(); query = SqlQueryUtils.addPredicate(query, predicate.getCondition()); } query = SqlQueryUtils.addPredicate(query, defaultPredicate); query = query + defaultSortOrder; log.info("QUERY: " + query); try { return constructGetCommand(this.sfConnector.getFullUri(getSoqlUrl(query))); } catch (Exception e) { throw new HighWatermarkException("Failed to get salesforce url for high watermark; error - " + e.getMessage(), e); } } @Override public long getHighWatermark(CommandOutput<?, ?> response, String watermarkColumn, String format) throws HighWatermarkException { log.info("Get high watermark from salesforce"); String output; Iterator<String> itr = (Iterator<String>) response.getResults().values().iterator(); if (itr.hasNext()) { output = itr.next(); } else { throw new HighWatermarkException("Failed to get high watermark from salesforce; REST response has no output"); } JsonElement element = GSON.fromJson(output, JsonObject.class); long high_ts; try { JsonObject jsonObject = element.getAsJsonObject(); JsonArray jsonArray = jsonObject.getAsJsonArray("records"); if (jsonArray.size() == 0) { return -1; } String value = jsonObject.getAsJsonArray("records").get(0).getAsJsonObject().get(watermarkColumn).getAsString(); if (format != null) { SimpleDateFormat inFormat = new SimpleDateFormat(format); Date date = null; try { date = inFormat.parse(value); } catch (ParseException e) { log.error("ParseException: " + e.getMessage(), e); } SimpleDateFormat outFormat = new SimpleDateFormat("yyyyMMddHHmmss"); high_ts = Long.parseLong(outFormat.format(date)); } else { high_ts = Long.parseLong(value); } } catch (Exception e) { throw new HighWatermarkException("Failed to get high watermark from salesforce; error - " + e.getMessage(), e); } return high_ts; } @Override public List<Command> getCountMetadata(String schema, String entity, WorkUnit workUnit, List<Predicate> predicateList) throws RecordCountException { log.debug("Build url to retrieve source record count"); String existingPredicate = ""; if (this.updatedQuery != null) { String queryLowerCase = this.updatedQuery.toLowerCase(); int startIndex = queryLowerCase.indexOf(" where "); if (startIndex > 0) { existingPredicate = this.updatedQuery.substring(startIndex); } } String query = "SELECT COUNT() FROM " + entity + existingPredicate; String limitString = getLimitFromInputQuery(query); query = query.replace(limitString, ""); try { if (isNullPredicate(predicateList)) { log.info("QUERY with null predicate: " + query); return constructGetCommand(this.sfConnector.getFullUri(getSoqlUrl(query))); } Iterator<Predicate> i = predicateList.listIterator(); while (i.hasNext()) { Predicate predicate = i.next(); query = SqlQueryUtils.addPredicate(query, predicate.getCondition()); } query = query + getLimitFromInputQuery(this.updatedQuery); log.info("QUERY: " + query); return constructGetCommand(this.sfConnector.getFullUri(getSoqlUrl(query))); } catch (Exception e) { throw new RecordCountException("Failed to get salesforce url for record count; error - " + e.getMessage(), e); } } @Override public long getCount(CommandOutput<?, ?> response) throws RecordCountException { log.info("Get source record count from salesforce"); String output; Iterator<String> itr = (Iterator<String>) response.getResults().values().iterator(); if (itr.hasNext()) { output = itr.next(); } else { throw new RecordCountException("Failed to get count from salesforce; REST response has no output"); } JsonElement element = GSON.fromJson(output, JsonObject.class); long count; try { JsonObject jsonObject = element.getAsJsonObject(); count = jsonObject.get("totalSize").getAsLong(); } catch (Exception e) { throw new RecordCountException("Failed to get record count from salesforce; error - " + e.getMessage(), e); } return count; } @Override public List<Command> getDataMetadata(String schema, String entity, WorkUnit workUnit, List<Predicate> predicateList) throws DataRecordException { log.debug("Build url to retrieve data records"); String query = this.updatedQuery; String url = null; try { if (this.getNextUrl() != null && this.pullStatus == true) { url = this.getNextUrl(); } else { if (isNullPredicate(predicateList)) { log.info("QUERY:" + query); return constructGetCommand(this.sfConnector.getFullUri(getSoqlUrl(query))); } String limitString = getLimitFromInputQuery(query); query = query.replace(limitString, ""); Iterator<Predicate> i = predicateList.listIterator(); while (i.hasNext()) { Predicate predicate = i.next(); query = SqlQueryUtils.addPredicate(query, predicate.getCondition()); } if (Boolean.valueOf(this.workUnitState.getProp(ConfigurationKeys.SOURCE_QUERYBASED_IS_SPECIFIC_API_ACTIVE))) { query = SqlQueryUtils.addPredicate(query, "IsDeleted = true"); } query = query + limitString; log.info("QUERY: " + query); url = this.sfConnector.getFullUri(getSoqlUrl(query)); } return constructGetCommand(url); } catch (Exception e) { throw new DataRecordException("Failed to get salesforce url for data records; error - " + e.getMessage(), e); } } private static String getLimitFromInputQuery(String query) { String inputQuery = query.toLowerCase(); int limitIndex = inputQuery.indexOf(" limit"); if (limitIndex > 0) { return query.substring(limitIndex); } return ""; } @Override public Iterator<JsonElement> getData(CommandOutput<?, ?> response) throws DataRecordException { log.debug("Get data records from response"); String output; Iterator<String> itr = (Iterator<String>) response.getResults().values().iterator(); if (itr.hasNext()) { output = itr.next(); } else { throw new DataRecordException("Failed to get data from salesforce; REST response has no output"); } List<JsonElement> rs = Lists.newArrayList(); JsonElement element = GSON.fromJson(output, JsonObject.class); JsonArray partRecords; try { JsonObject jsonObject = element.getAsJsonObject(); partRecords = jsonObject.getAsJsonArray("records"); if (jsonObject.get("done").getAsBoolean()) { setPullStatus(false); } else { setNextUrl(this.sfConnector.getFullUri( jsonObject.get("nextRecordsUrl").getAsString().replaceAll(this.sfConnector.getServicesDataEnvPath(), ""))); } JsonArray array = Utils.removeElementFromJsonArray(partRecords, "attributes"); Iterator<JsonElement> li = array.iterator(); while (li.hasNext()) { JsonElement recordElement = li.next(); rs.add(recordElement); } return rs.iterator(); } catch (Exception e) { throw new DataRecordException("Failed to get records from salesforce; error - " + e.getMessage(), e); } } @Override public boolean getPullStatus() { return this.pullStatus; } @Override public String getNextUrl() { return this.nextUrl; } public static String getSoqlUrl(String soqlQuery) throws RestApiClientException { String path = SOQL_RESOURCE + "/"; NameValuePair pair = new BasicNameValuePair("q", soqlQuery); List<NameValuePair> qparams = new ArrayList<>(); qparams.add(pair); return buildUrl(path, qparams); } private static String buildUrl(String path, List<NameValuePair> qparams) throws RestApiClientException { URIBuilder builder = new URIBuilder(); builder.setPath(path); ListIterator<NameValuePair> i = qparams.listIterator(); while (i.hasNext()) { NameValuePair keyValue = i.next(); builder.setParameter(keyValue.getName(), keyValue.getValue()); } URI uri; try { uri = builder.build(); } catch (Exception e) { throw new RestApiClientException("Failed to build url; error - " + e.getMessage(), e); } return new HttpGet(uri).getURI().toString(); } private static boolean isNullPredicate(List<Predicate> predicateList) { if (predicateList == null || predicateList.size() == 0) { return true; } return false; } @Override public String getWatermarkSourceFormat(WatermarkType watermarkType) { switch (watermarkType) { case TIMESTAMP: return "yyyy-MM-dd'T'HH:mm:ss"; case DATE: return "yyyy-MM-dd"; default: return null; } } @Override public String getHourPredicateCondition(String column, long value, String valueFormat, String operator) { log.info("Getting hour predicate from salesforce"); String Formattedvalue = Utils.toDateTimeFormat(Long.toString(value), valueFormat, SALESFORCE_HOUR_FORMAT); return column + " " + operator + " " + Formattedvalue; } @Override public String getDatePredicateCondition(String column, long value, String valueFormat, String operator) { log.info("Getting date predicate from salesforce"); String Formattedvalue = Utils.toDateTimeFormat(Long.toString(value), valueFormat, SALESFORCE_DATE_FORMAT); return column + " " + operator + " " + Formattedvalue; } @Override public String getTimestampPredicateCondition(String column, long value, String valueFormat, String operator) { log.info("Getting timestamp predicate from salesforce"); String Formattedvalue = Utils.toDateTimeFormat(Long.toString(value), valueFormat, SALESFORCE_TIMESTAMP_FORMAT); return column + " " + operator + " " + Formattedvalue; } @Override public Map<String, String> getDataTypeMap() { Map<String, String> dataTypeMap = ImmutableMap.<String, String> builder().put("url", "string") .put("textarea", "string").put("reference", "string").put("phone", "string").put("masterrecord", "string") .put("location", "string").put("id", "string").put("encryptedstring", "string").put("email", "string") .put("DataCategoryGroupReference", "string").put("calculated", "string").put("anyType", "string") .put("address", "string").put("blob", "string").put("date", "date").put("datetime", "timestamp") .put("time", "time").put("object", "string").put("string", "string").put("int", "int").put("long", "long") .put("double", "double").put("percent", "double").put("currency", "double").put("decimal", "double") .put("boolean", "boolean").put("picklist", "string").put("multipicklist", "string").put("combobox", "string") .put("list", "string").put("set", "string").put("map", "string").put("enum", "string").build(); return dataTypeMap; } @Override public Iterator<JsonElement> getRecordSetFromSourceApi(String schema, String entity, WorkUnit workUnit, List<Predicate> predicateList) throws IOException { log.debug("Getting salesforce data using bulk api"); RecordSet<JsonElement> rs = null; try { //Get query result ids in the first run //result id is used to construct url while fetching data if (this.bulkApiInitialRun == true) { // set finish status to false before starting the bulk job this.setBulkJobFinished(false); this.bulkResultIdList = getQueryResultIds(entity, predicateList); log.info("Number of bulk api resultSet Ids:" + this.bulkResultIdList.size()); } // Get data from input stream // If bulk load is not finished, get data from the stream if (!this.isBulkJobFinished()) { rs = getBulkData(); } // Set bulkApiInitialRun to false after the completion of first run this.bulkApiInitialRun = false; // If bulk job is finished, get soft deleted records using Rest API boolean isSoftDeletesPullDisabled = Boolean.valueOf(this.workUnit .getProp(SalesforceConfigurationKeys.SOURCE_QUERYBASED_SALESFORCE_IS_SOFT_DELETES_PULL_DISABLED)); if (rs == null || rs.isEmpty()) { // Get soft delete records only if IsDeleted column exists and soft deletes pull is not disabled if (this.columnList.contains("IsDeleted") && !isSoftDeletesPullDisabled) { return this.getSoftDeletedRecords(schema, entity, workUnit, predicateList); } log.info("Ignoring soft delete records"); } return rs.iterator(); } catch (Exception e) { throw new IOException("Failed to get records using bulk api; error - " + e.getMessage(), e); } } /** * Get soft deleted records using Rest Api * @return iterator with deleted records */ private Iterator<JsonElement> getSoftDeletedRecords(String schema, String entity, WorkUnit workUnit, List<Predicate> predicateList) throws DataRecordException { return this.getRecordSet(schema, entity, workUnit, predicateList); } /** * Login to salesforce * @return login status */ public boolean bulkApiLogin() throws Exception { log.info("Authenticating salesforce bulk api"); boolean success = false; String hostName = this.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_HOST_NAME); String apiVersion = this.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_VERSION); if (Strings.isNullOrEmpty(apiVersion)) { apiVersion = "29.0"; } String soapAuthEndPoint = hostName + SALESFORCE_SOAP_AUTH_SERVICE + "/" + apiVersion; try { ConnectorConfig partnerConfig = new ConnectorConfig(); if (super.workUnitState.contains(ConfigurationKeys.SOURCE_CONN_USE_PROXY_URL) && !super.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_USE_PROXY_URL).isEmpty()) { partnerConfig.setProxy(super.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_USE_PROXY_URL), super.workUnitState.getPropAsInt(ConfigurationKeys.SOURCE_CONN_USE_PROXY_PORT)); } String securityToken = this.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_SECURITY_TOKEN); String password = PasswordManager.getInstance(this.workUnitState) .readPassword(this.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_PASSWORD)); partnerConfig.setUsername(this.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_USERNAME)); partnerConfig.setPassword(password + securityToken); partnerConfig.setAuthEndpoint(soapAuthEndPoint); new PartnerConnection(partnerConfig); String soapEndpoint = partnerConfig.getServiceEndpoint(); String restEndpoint = soapEndpoint.substring(0, soapEndpoint.indexOf("Soap/")) + "async/" + apiVersion; ConnectorConfig config = new ConnectorConfig(); config.setSessionId(partnerConfig.getSessionId()); config.setRestEndpoint(restEndpoint); config.setCompression(true); config.setTraceFile("traceLogs.txt"); config.setTraceMessage(false); config.setPrettyPrintXml(true); if (super.workUnitState.contains(ConfigurationKeys.SOURCE_CONN_USE_PROXY_URL) && !super.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_USE_PROXY_URL).isEmpty()) { config.setProxy(super.workUnitState.getProp(ConfigurationKeys.SOURCE_CONN_USE_PROXY_URL), super.workUnitState.getPropAsInt(ConfigurationKeys.SOURCE_CONN_USE_PROXY_PORT)); } this.bulkConnection = new BulkConnection(config); success = true; } catch (RuntimeException e) { throw new RuntimeException("Failed to connect to salesforce bulk api; error - " + e, e); } return success; } /** * Get Record set using salesforce specific API(Bulk API) * @param schema/databasename * @param entity/tablename * @param list of all predicate conditions * @return iterator with batch of records */ private List<String> getQueryResultIds(String entity, List<Predicate> predicateList) throws Exception { if (!bulkApiLogin()) { throw new IllegalArgumentException("Invalid Login"); } try { // Set bulk job attributes this.bulkJob.setObject(entity); this.bulkJob.setOperation(OperationEnum.query); this.bulkJob.setConcurrencyMode(ConcurrencyMode.Parallel); // Result type as CSV this.bulkJob.setContentType(ContentType.CSV); this.bulkJob = this.bulkConnection.createJob(this.bulkJob); this.bulkJob = this.bulkConnection.getJobStatus(this.bulkJob.getId()); // Construct query with the predicates String query = this.updatedQuery; if (!isNullPredicate(predicateList)) { String limitString = getLimitFromInputQuery(query); query = query.replace(limitString, ""); Iterator<Predicate> i = predicateList.listIterator(); while (i.hasNext()) { Predicate predicate = i.next(); query = SqlQueryUtils.addPredicate(query, predicate.getCondition()); } query = query + limitString; } log.info("QUERY:" + query); ByteArrayInputStream bout = new ByteArrayInputStream(query.getBytes(ConfigurationKeys.DEFAULT_CHARSET_ENCODING)); this.bulkBatchInfo = this.bulkConnection.createBatchFromStream(this.bulkJob, bout); int retryInterval = 30 + (int) Math.ceil((float) this.getExpectedRecordCount() / 10000) * 2; log.info("Salesforce bulk api retry interval in seconds:" + retryInterval); // Get batch info with complete resultset (info id - refers to the resultset id corresponding to entire resultset) this.bulkBatchInfo = this.bulkConnection.getBatchInfo(this.bulkJob.getId(), this.bulkBatchInfo.getId()); while ((this.bulkBatchInfo.getState() != BatchStateEnum.Completed) && (this.bulkBatchInfo.getState() != BatchStateEnum.Failed)) { Thread.sleep(retryInterval * 1000); this.bulkBatchInfo = this.bulkConnection.getBatchInfo(this.bulkJob.getId(), this.bulkBatchInfo.getId()); log.debug("Bulk Api Batch Info:" + this.bulkBatchInfo); log.info("Waiting for bulk resultSetIds"); } if (this.bulkBatchInfo.getState() == BatchStateEnum.Failed) { log.error("Bulk batch failed: " + bulkBatchInfo.toString()); throw new RuntimeException("Failed to get bulk batch info for jobId " + this.bulkBatchInfo.getJobId() + " error - " + this.bulkBatchInfo.getStateMessage()); } // Get resultset ids from the batch info QueryResultList list = this.bulkConnection.getQueryResultList(this.bulkJob.getId(), this.bulkBatchInfo.getId()); return Arrays.asList(list.getResult()); } catch (RuntimeException | AsyncApiException | InterruptedException e) { throw new RuntimeException( "Failed to get query result ids from salesforce using bulk api; error - " + e.getMessage(), e); } } /** * Get data from the bulk api input stream * @return record set with each record as a JsonObject */ private RecordSet<JsonElement> getBulkData() throws DataRecordException { log.debug("Processing bulk api batch..."); RecordSetList<JsonElement> rs = new RecordSetList<>(); try { // if Buffer is empty then get stream for the new resultset id if (this.bulkBufferedReader == null || !this.bulkBufferedReader.ready()) { // if there is unprocessed resultset id then get result stream for that id if (this.bulkResultIdCount < this.bulkResultIdList.size()) { log.info("Stream resultset for resultId:" + this.bulkResultIdList.get(this.bulkResultIdCount)); this.setNewBulkResultSet(true); this.bulkBufferedReader = new BufferedReader( new InputStreamReader( this.bulkConnection.getQueryResultStream(this.bulkJob.getId(), this.bulkBatchInfo.getId(), this.bulkResultIdList.get(this.bulkResultIdCount)), ConfigurationKeys.DEFAULT_CHARSET_ENCODING)); this.bulkResultIdCount++; } else { // if result stream processed for all resultset ids then finish the bulk job log.info("Bulk job is finished"); this.setBulkJobFinished(true); return rs; } } // if Buffer stream has data then process the same // Get batch size from .pull file int batchSize = Utils.getAsInt(this.workUnitState.getProp(ConfigurationKeys.SOURCE_QUERYBASED_FETCH_SIZE)); if (batchSize == 0) { batchSize = ConfigurationKeys.DEFAULT_SOURCE_FETCH_SIZE; } // Stream the resultset through CSV reader to identify columns in each record InputStreamCSVReader reader = new InputStreamCSVReader(this.bulkBufferedReader); // Get header if it is first run of a new resultset if (this.isNewBulkResultSet()) { this.bulkRecordHeader = reader.nextRecord(); this.bulkResultColumCount = this.bulkRecordHeader.size(); this.setNewBulkResultSet(false); } List<String> csvRecord; int recordCount = 0; // Get record from CSV reader stream while ((csvRecord = reader.nextRecord()) != null) { // Convert CSV record to JsonObject JsonObject jsonObject = Utils.csvToJsonObject(this.bulkRecordHeader, csvRecord, this.bulkResultColumCount); rs.add(jsonObject); recordCount++; this.bulkRecordCount++; // Insert records in record set until it reaches the batch size if (recordCount >= batchSize) { log.info("Total number of records processed so far: " + this.bulkRecordCount); break; } } } catch (Exception e) { throw new DataRecordException("Failed to get records from salesforce; error - " + e.getMessage(), e); } return rs; } @Override public void closeConnection() throws Exception { if (this.bulkConnection != null && !this.bulkConnection.getJobStatus(this.bulkJob.getId()).getState().toString().equals("Closed")) { log.info("Closing salesforce bulk job connection"); this.bulkConnection.closeJob(this.bulkJob.getId()); } } public static List<Command> constructGetCommand(String restQuery) { return Arrays.asList(new RestApiCommand().build(Arrays.asList(restQuery), RestApiCommandType.GET)); } }