Skip to content

Commit 8f4832a

Browse files
committed
PLUGIN-1936: Streaming Implementation
1 parent b69eb43 commit 8f4832a

25 files changed

Lines changed: 812 additions & 370 deletions

src/e2e-test/java/io/cdap/plugin/servicenowsink/actions/ServiceNowSinkPropertiesPageActions.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848

4949
public class ServiceNowSinkPropertiesPageActions {
5050
public static ServiceNowSourceConfig config;
51-
private static Map<String, String> responseFromServiceNowTable;
51+
private static JsonObject responseFromServiceNowTable;
5252
private static Gson gson = new Gson();
5353

5454
public static void getRecordFromServiceNowTable(String query, String tableName)
@@ -71,10 +71,8 @@ public static boolean verifyIfRecordCreatedInServiceNowIsCorrect(String query, S
7171
String bqTable = TestSetupHooks.bqTargetTable;
7272
getRecordFromServiceNowTable(query, tableName);
7373

74-
JsonObject jsonObject = new JsonObject();
75-
responseFromServiceNowTable.forEach(jsonObject::addProperty);
7674
List<JsonObject> serviceNowResponse = new ArrayList<>();
77-
serviceNowResponse.add(jsonObject);
75+
serviceNowResponse.add(responseFromServiceNowTable);
7876

7977
List<JsonObject> bigQueryResponse = new ArrayList<>();
8078
List<Object> bigQueryRows = new ArrayList<>();

src/main/java/io/cdap/plugin/servicenow/ServiceNowBaseConfig.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.cdap.plugin.servicenow;
1818

1919
import com.google.common.annotations.VisibleForTesting;
20+
import com.google.gson.stream.JsonReader;
2021
import io.cdap.cdap.api.annotation.Description;
2122
import io.cdap.cdap.api.annotation.Macro;
2223
import io.cdap.cdap.api.annotation.Name;
@@ -31,14 +32,19 @@
3132
import io.cdap.plugin.servicenow.util.SchemaType;
3233
import io.cdap.plugin.servicenow.util.ServiceNowConstants;
3334
import io.cdap.plugin.servicenow.util.SourceValueType;
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
3437

38+
import java.io.IOException;
39+
import java.io.InputStreamReader;
3540
import javax.annotation.Nullable;
3641

3742
/**
3843
* ServiceNow Base Config. Contains connection properties and methods.
3944
*/
4045
public class ServiceNowBaseConfig extends PluginConfig {
4146

47+
private static final Logger log = LoggerFactory.getLogger(ServiceNowBaseConfig.class);
4248
@Name(ConfigUtil.NAME_USE_CONNECTION)
4349
@Nullable
4450
@Description("Whether to use an existing connection.")
@@ -140,7 +146,7 @@ public void validateTable(String tableName, SourceValueType valueType, FailureCo
140146
requestBuilder.setResponseHeaders(ServiceNowConstants.HEADER_NAME_TOTAL_COUNT);
141147

142148
apiResponse = serviceNowTableAPIClient.executeGetWithRetries(requestBuilder.build());
143-
if (serviceNowTableAPIClient.parseResponseToResultListOfMap(apiResponse.getResponseBody()).isEmpty()) {
149+
if (isResultEmpty(apiResponse)) {
144150
// Removed config property as in case of MultiSource, only first table error was populating.
145151
collector.addFailure("Table: " + tableName + " is empty.", "");
146152
}
@@ -152,4 +158,31 @@ public void validateTable(String tableName, SourceValueType valueType, FailureCo
152158
}
153159
}
154160

161+
/**
162+
* Checks if the "result" array in the ServiceNow REST API response is empty.
163+
* <p>
164+
* Determines if the "result" array in a ServiceNow REST API response is empty by specifically looking for a top-level
165+
* key named "result". Once found, it opens the associated array and checks for the presence of a first element.
166+
* </p>
167+
*
168+
* @param restAPIResponse The response object containing the JSON input stream
169+
* @return true, if the "result" array exists and is empty, or if the "result" key is never found;
170+
* false, if the array contains at least one element.
171+
* @throws IOException If there is an error reading the input stream or parsing the JSON.
172+
*/
173+
public boolean isResultEmpty(RestAPIResponse restAPIResponse) throws IOException {
174+
JsonReader reader = new JsonReader(new InputStreamReader(restAPIResponse.getResponseStream()));
175+
reader.beginObject();
176+
while (reader.hasNext()) {
177+
String name = reader.nextName();
178+
if (ServiceNowConstants.RESULT.equals(name)) {
179+
reader.beginArray();
180+
return !reader.hasNext();
181+
} else {
182+
reader.skipValue();
183+
}
184+
}
185+
return true;
186+
}
187+
155188
}

src/main/java/io/cdap/plugin/servicenow/apiclient/ServiceNowTableAPIClientImpl.java

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import com.google.gson.JsonArray;
2828
import com.google.gson.JsonObject;
2929
import com.google.gson.reflect.TypeToken;
30-
import com.sun.org.apache.xpath.internal.operations.Bool;
30+
import com.google.gson.stream.JsonReader;
3131
import io.cdap.cdap.api.data.schema.Schema;
3232
import io.cdap.cdap.etl.api.FailureCollector;
3333
import io.cdap.plugin.servicenow.connector.ServiceNowConnectorConfig;
@@ -56,7 +56,10 @@
5656
import org.slf4j.LoggerFactory;
5757

5858
import java.io.IOException;
59+
import java.io.InputStream;
60+
import java.io.InputStreamReader;
5961
import java.lang.reflect.Type;
62+
import java.nio.charset.StandardCharsets;
6063
import java.util.ArrayList;
6164
import java.util.List;
6265
import java.util.Map;
@@ -86,6 +89,7 @@ public class ServiceNowTableAPIClientImpl extends RestAPIClient {
8689
public static JsonArray serviceNowJsonResultArray;
8790

8891
public ServiceNowTableAPIClientImpl(ServiceNowConnectorConfig conf, Boolean useConnection) {
92+
super();
8993
this.conf = conf;
9094
this.schemaType = getSchemaTypeBasedOnUseConnection(useConnection);
9195
}
@@ -135,7 +139,7 @@ public String getAccessTokenRetryableMode() throws ExecutionException, RetryExce
135139
* @return A list of maps, where each map represents a record from the table.
136140
* @throws ServiceNowAPIException If an error occurs while fetching the records.
137141
*/
138-
public List<Map<String, String>> fetchTableRecords(
142+
public RestAPIResponse fetchTableRecords(
139143
String tableName,
140144
SourceValueType valueType,
141145
String filterQuery,
@@ -159,7 +163,7 @@ public List<Map<String, String>> fetchTableRecords(
159163
String accessToken = getAccessToken();
160164
requestBuilder.setAuthHeader(accessToken);
161165
RestAPIResponse apiResponse = executeGetWithRetries(requestBuilder.build());
162-
return parseResponseToResultListOfMap(apiResponse.getResponseBody());
166+
return apiResponse;
163167
}
164168

165169
private int getRecordCountFromHeader(RestAPIResponse apiResponse) {
@@ -209,30 +213,24 @@ private String getErrorMessage(String responseBody) {
209213
* @param limit The number of records to be fetched
210214
* @return The list of Map; each Map representing a table row
211215
*/
212-
public List<Map<String, String>> fetchTableRecordsRetryableMode(String tableName, SourceValueType valueType,
216+
public RestAPIResponse fetchTableRecordsRetryableMode(String tableName, SourceValueType valueType,
213217
String filterQuery, int offset,
214218
int limit) throws ServiceNowAPIException {
215-
final List<Map<String, String>> results = new ArrayList<>();
216-
Callable<Boolean> fetchRecords = () -> {
217-
results.addAll(fetchTableRecords(tableName, valueType, filterQuery, offset, limit));
218-
return true;
219-
};
219+
Callable<RestAPIResponse> fetchRecords = () -> fetchTableRecords(tableName, valueType, filterQuery, offset, limit);
220220

221-
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
221+
Retryer<RestAPIResponse> retryer = RetryerBuilder.<RestAPIResponse>newBuilder()
222222
.retryIfException(this::isExceptionRetryable)
223223
.withWaitStrategy(WaitStrategies.exponentialWait(ServiceNowConstants.WAIT_TIME, TimeUnit.MILLISECONDS))
224224
.withStopStrategy(StopStrategies.stopAfterAttempt(ServiceNowConstants.MAX_NUMBER_OF_RETRY_ATTEMPTS))
225225
.build();
226226

227227
try {
228-
retryer.call(fetchRecords);
228+
return retryer.call(fetchRecords);
229229
} catch (RetryException | ExecutionException e) {
230230
throw new ServiceNowAPIException(
231231
String.format("Data Recovery failed for batch %s to %s.", offset, (offset + limit)),
232232
e, null, false);
233233
}
234-
235-
return results;
236234
}
237235

238236
/**
@@ -255,8 +253,8 @@ public Schema fetchTableSchema(String tableName, FailureCollector collector) {
255253
}
256254

257255
@VisibleForTesting
258-
public MetadataAPISchemaResponse parseSchemaResponse(String responseBody) {
259-
return GSON.fromJson(responseBody, MetadataAPISchemaResponse.class);
256+
public MetadataAPISchemaResponse parseSchemaResponse(InputStream responseStream) {
257+
return GSON.fromJson(createJsonReader(responseStream), MetadataAPISchemaResponse.class);
260258
}
261259

262260
/**
@@ -331,7 +329,7 @@ public Schema fetchTableSchema(String tableName, String accessToken, SourceValue
331329
private Schema prepareSchemaWithSchemaAPI(RestAPIResponse restAPIResponse, List<ServiceNowColumn> columns,
332330
String tableName) throws ServiceNowAPIException {
333331
SchemaAPISchemaResponse schemaAPISchemaResponse =
334-
GSON.fromJson(restAPIResponse.getResponseBody(), SchemaAPISchemaResponse.class);
332+
GSON.fromJson(createJsonReader(restAPIResponse.getResponseStream()), SchemaAPISchemaResponse.class);
335333

336334
if (schemaAPISchemaResponse.getResult() == null || schemaAPISchemaResponse.getResult().isEmpty()) {
337335
throw new ServiceNowAPIException(
@@ -364,7 +362,7 @@ private Schema prepareSchemaWithSchemaAPI(RestAPIResponse restAPIResponse, List<
364362
*/
365363
private Schema prepareSchemaWithMetadataAPI(RestAPIResponse restAPIResponse, List<ServiceNowColumn> columns,
366364
String tableName, SourceValueType valueType, Boolean legacyMapping) throws ServiceNowAPIException {
367-
MetadataAPISchemaResponse metadataAPISchemaResponse = parseSchemaResponse(restAPIResponse.getResponseBody());
365+
MetadataAPISchemaResponse metadataAPISchemaResponse = parseSchemaResponse(restAPIResponse.getResponseStream());
368366

369367
if (metadataAPISchemaResponse.getResult() == null || metadataAPISchemaResponse.getResult().getColumns() == null ||
370368
metadataAPISchemaResponse.getResult().getColumns().isEmpty()) {
@@ -391,6 +389,11 @@ private Schema prepareSchemaWithMetadataAPI(RestAPIResponse restAPIResponse, Lis
391389
return SchemaBuilder.constructSchema(tableName, columns, legacyMapping);
392390
}
393391

392+
public JsonReader createJsonReader(InputStream inputStream) {
393+
Objects.requireNonNull(inputStream, "InputStream must not be null");
394+
return new JsonReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
395+
}
396+
394397
/**
395398
* Gets the total number of records in the table based on the provided date range.
396399
*
@@ -451,8 +454,8 @@ public String createRecord(String tableName, HttpEntity entity) throws IOExcepti
451454
requestBuilder.setContentTypeHeader("application/json");
452455
requestBuilder.setEntity(entity);
453456
apiResponse = executePost(requestBuilder.build());
454-
455457
systemID = String.valueOf(getSystemId(apiResponse));
458+
apiResponse.close();
456459
} catch (IOException e) {
457460
throw new ServiceNowAPIException("Error in creating a new record", e, null, false);
458461
}
@@ -482,15 +485,17 @@ public String createRecordInDisplayMode(String tableName, HttpEntity entity) thr
482485
apiResponse = executePost(requestBuilder.build());
483486

484487
systemID = String.valueOf(getSystemId(apiResponse));
488+
apiResponse.close();
485489
} catch (IOException e) {
486490
throw new ServiceNowAPIException("Error in creating a new record", e, null, false);
487491
}
488492
return systemID;
489493
}
490494

491495
private String getSystemId(RestAPIResponse restAPIResponse) {
492-
CreateRecordAPIResponse apiResponse = GSON.fromJson(restAPIResponse.getResponseBody(),
493-
CreateRecordAPIResponse.class);
496+
CreateRecordAPIResponse apiResponse = GSON.fromJson(
497+
new InputStreamReader(restAPIResponse.getResponseStream(), StandardCharsets.UTF_8),
498+
CreateRecordAPIResponse.class);
494499
return apiResponse.getResult().get(ServiceNowConstants.SYSTEM_ID).toString();
495500
}
496501

@@ -501,7 +506,7 @@ private String getSystemId(RestAPIResponse restAPIResponse) {
501506
* @param tableName The ServiceNow table name
502507
* @param query The query
503508
*/
504-
public Map<String, String> getRecordFromServiceNowTable(String tableName, String query)
509+
public JsonObject getRecordFromServiceNowTable(String tableName, String query)
505510
throws ServiceNowAPIException {
506511

507512
ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder(
@@ -513,7 +518,8 @@ public Map<String, String> getRecordFromServiceNowTable(String tableName, String
513518
requestBuilder.setAuthHeader(accessToken);
514519
restAPIResponse = executeGetWithRetries(requestBuilder.build());
515520

516-
APIResponse apiResponse = GSON.fromJson(restAPIResponse.getResponseBody(), APIResponse.class);
521+
APIResponse apiResponse = GSON.fromJson(
522+
new InputStreamReader(restAPIResponse.getResponseStream(), StandardCharsets.UTF_8), APIResponse.class);
517523
return apiResponse.getResult().get(0);
518524
}
519525

@@ -530,19 +536,45 @@ public Map<String, String> getRecordFromServiceNowTable(String tableName, String
530536
*
531537
* @throws RuntimeException if the schema response is null or contains no result.
532538
*/
533-
private Schema prepareStringBasedSchema(RestAPIResponse restAPIResponse, List<ServiceNowColumn> columns,
539+
private Schema prepareStringBasedSchema(RestAPIResponse restAPIResponse, List<ServiceNowColumn> columns,
534540
String tableName) {
535-
List<Map<String, String>> result = parseResponseToResultListOfMap(restAPIResponse.getResponseBody());
536-
if (result != null && !result.isEmpty()) {
537-
Map<String, String> firstRecord = result.get(0);
538-
for (String key : firstRecord.keySet()) {
539-
columns.add(new ServiceNowColumn(key, "string"));
541+
InputStream in = restAPIResponse.getResponseStream();
542+
JsonReader reader = new JsonReader(new InputStreamReader(in, StandardCharsets.UTF_8));
543+
JsonObject firstRecord;
544+
try {
545+
firstRecord = getFirstRecord(reader);
546+
restAPIResponse.close();
547+
} catch (IOException e) {
548+
throw new RuntimeException("Failed to parse schema for table: " + tableName, e);
540549
}
550+
if (firstRecord != null) {
551+
firstRecord.entrySet().forEach(entry ->
552+
columns.add(new ServiceNowColumn(entry.getKey(), "string"))
553+
);
541554
return SchemaBuilder.constructSchema(tableName, columns, false);
542555
}
543556
return null;
544557
}
545558

559+
public JsonObject getFirstRecord(JsonReader reader) throws IOException {
560+
reader.beginObject();
561+
while (reader.hasNext()) {
562+
String name = reader.nextName();
563+
if (name.equals(ServiceNowConstants.RESULT)) {
564+
reader.beginArray();
565+
if (reader.hasNext()) {
566+
// ONLY parse the first object into memory
567+
return GSON.fromJson(reader, JsonObject.class);
568+
}
569+
reader.endArray();
570+
} else {
571+
reader.skipValue();
572+
}
573+
}
574+
reader.endObject();
575+
return null;
576+
}
577+
546578

547579
/**
548580
* This function is being used in end-to-end (e2e) tests to delete a record from

src/main/java/io/cdap/plugin/servicenow/connector/ServiceNowConnector.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package io.cdap.plugin.servicenow.connector;
1717

1818
import com.google.gson.Gson;
19+
import com.google.gson.JsonObject;
20+
import com.google.gson.stream.JsonReader;
1921
import io.cdap.cdap.api.annotation.Description;
2022
import io.cdap.cdap.api.annotation.Name;
2123
import io.cdap.cdap.api.annotation.Plugin;
@@ -55,6 +57,9 @@
5557
import org.slf4j.LoggerFactory;
5658

5759
import java.io.IOException;
60+
import java.io.InputStream;
61+
import java.io.InputStreamReader;
62+
import java.nio.charset.StandardCharsets;
5863
import java.util.ArrayList;
5964
import java.util.HashMap;
6065
import java.util.List;
@@ -135,7 +140,7 @@ private TableList listTables(String accessToken) throws ServiceNowAPIException {
135140
ServiceNowTableAPIClientImpl serviceNowTableAPIClient = new ServiceNowTableAPIClientImpl(config, true);
136141
RestAPIResponse apiResponse =
137142
serviceNowTableAPIClient.executeGetWithRetries(requestBuilder.build());
138-
return GSON.fromJson(apiResponse.getResponseBody(), TableList.class);
143+
return GSON.fromJson(serviceNowTableAPIClient.createJsonReader(apiResponse.getResponseStream()), TableList.class);
139144
}
140145

141146
public ConnectorSpec generateSpec(ConnectorContext connectorContext, ConnectorSpecRequest connectorSpecRequest) {
@@ -172,7 +177,7 @@ public List<StructuredRecord> sample(ConnectorContext connectorContext, SampleRe
172177
}
173178

174179
private List<StructuredRecord> getTableData(String tableName, int limit)
175-
throws OAuthProblemException, OAuthSystemException, ServiceNowAPIException {
180+
throws OAuthProblemException, OAuthSystemException, ServiceNowAPIException, IOException {
176181
ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder(
177182
config.getRestApiEndpoint(), tableName, false, SchemaType.SCHEMA_API_BASED)
178183
.setExcludeReferenceLink(true)
@@ -183,22 +188,40 @@ private List<StructuredRecord> getTableData(String tableName, int limit)
183188
requestBuilder.setAuthHeader(accessToken);
184189
requestBuilder.setResponseHeaders(ServiceNowConstants.HEADER_NAME_TOTAL_COUNT);
185190
RestAPIResponse apiResponse = serviceNowTableAPIClient.executeGetWithRetries(requestBuilder.build());
186-
List<Map<String, String>> result = serviceNowTableAPIClient.parseResponseToResultListOfMap
187-
(apiResponse.getResponseBody());
191+
InputStream in = apiResponse.getResponseStream();
192+
JsonReader reader = new JsonReader(new InputStreamReader(in, StandardCharsets.UTF_8));
188193
List<StructuredRecord> recordList = new ArrayList<>();
189194
Schema schema = getSchema(tableName);
190195
if (schema != null) {
191196
List<Schema.Field> tableFields = schema.getFields();
192-
for (int i = 0; i < result.size(); i++) {
193-
StructuredRecord.Builder recordBuilder = StructuredRecord.builder(schema);
194-
for (Schema.Field field : tableFields) {
195-
String fieldName = field.getName();
196-
ServiceNowRecordConverter.convertToValue(fieldName, field.getSchema(), result.get(i), recordBuilder, false);
197+
try {
198+
reader.beginObject();
199+
while (reader.hasNext()) {
200+
String name = reader.nextName();
201+
if (name.equals(ServiceNowConstants.RESULT)) {
202+
reader.beginArray();
203+
while (reader.hasNext()) {
204+
JsonObject jsonObject = GSON.fromJson(reader, JsonObject.class);
205+
StructuredRecord.Builder recordBuilder = StructuredRecord.builder(schema);
206+
for (Schema.Field field : tableFields) {
207+
String fieldName = field.getName();
208+
ServiceNowRecordConverter.convertToValue(fieldName, field.getSchema(), jsonObject, recordBuilder,
209+
false);
210+
}
211+
StructuredRecord structuredRecord = recordBuilder.build();
212+
recordList.add(structuredRecord);
213+
}
214+
reader.endArray();
215+
} else {
216+
reader.skipValue();
217+
}
197218
}
198-
StructuredRecord structuredRecord = recordBuilder.build();
199-
recordList.add(structuredRecord);
219+
reader.endObject();
220+
} catch (IOException e) {
221+
throw new RuntimeException("Error reading JSON response for table " + tableName, e);
200222
}
201223
}
224+
apiResponse.close();
202225
return recordList;
203226

204227
}

0 commit comments

Comments
 (0)