Skip to content

Commit a69805b

Browse files
prince-csvikasrathee-cs
authored andcommitted
Changes done for schema backward compatibility.
1 parent 71eff1e commit a69805b

34 files changed

+406
-200
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public static void getRecordFromServiceNowTable(String query, String tableName)
6161
System.getenv("SERVICE_NOW_PASSWORD"),
6262
"", "", "", null);
6363

64-
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection());
64+
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection(), true);
6565
responseFromServiceNowTable = tableAPIClient.getRecordFromServiceNowTable(tableName, query);
6666
}
6767

src/e2e-test/java/io/cdap/plugin/tests/hooks/TestSetupHooks.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public static void initializeServiceNowSourceConfig() {
6565
@Before(order = 2, value = "@SN_PRODUCT_CATALOG_ITEM")
6666
public static void createRecordInProductCatalogItemTable() throws IOException, ServiceNowAPIException {
6767
BeforeActions.scenario.write("Create new record in Product Catalog Item table");
68-
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection());
68+
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection(), true);
6969
String uniqueId = "TestProductCatalogItem" + RandomStringUtils.randomAlphanumeric(10);
7070
String recordDetails = "{'name':'" + uniqueId + "','price':'2500'}";
7171
StringEntity entity = new StringEntity(recordDetails);
@@ -76,7 +76,7 @@ public static void createRecordInProductCatalogItemTable() throws IOException, S
7676
public static void createRecordInReceivingSlipLineTable()
7777
throws IOException, ServiceNowAPIException {
7878
BeforeActions.scenario.write("Create new record in Receiving Slip Line table");
79-
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection());
79+
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection(), true);
8080
String uniqueId = "TestReceivingSlipLine" + RandomStringUtils.randomAlphanumeric(10);
8181
String recordDetails = "{'number':'" + uniqueId + "'}";
8282
StringEntity entity = new StringEntity(recordDetails);
@@ -88,7 +88,7 @@ public static void createRecordInReceivingSlipLineTable()
8888
public static void updateRecordInAgentAssistRecommendationTable()
8989
throws IOException, ServiceNowAPIException {
9090
BeforeActions.scenario.write("Create new record in Agent Assist Recommendation table");
91-
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection());
91+
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection(), true);
9292
String uniqueId = "TestAgentAssist" + RandomStringUtils.randomAlphanumeric(10);
9393
String recordDetails = "{'active':'false','name':'" + uniqueId + "'}";
9494
StringEntity entity = new StringEntity(recordDetails);
@@ -99,7 +99,7 @@ public static void updateRecordInAgentAssistRecommendationTable()
9999
public static void updateRecordInAgentVendorCatalogItem()
100100
throws IOException, ServiceNowAPIException {
101101
BeforeActions.scenario.write("Create new record in Vendor Catalog Item table");
102-
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection());
102+
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection(), true);
103103
String uniqueId = "TestVendorCatalog" + RandomStringUtils.randomAlphanumeric(10);
104104
String recordDetails = "{'out_of_stock':'false','product_id':'" + uniqueId + "'}";
105105
StringEntity entity = new StringEntity(recordDetails);
@@ -109,7 +109,7 @@ public static void updateRecordInAgentVendorCatalogItem()
109109
@Before(order = 2, value = "@SN_UPDATE_SERVICE_OFFERING")
110110
public static void updateRecordInServiceOffering() throws IOException, ServiceNowAPIException {
111111
BeforeActions.scenario.write("Create new record in Service Offering table");
112-
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection());
112+
ServiceNowTableAPIClientImpl tableAPIClient = new ServiceNowTableAPIClientImpl(config.getConnection(), true);
113113
String uniqueId = "TestServiceOffering" + RandomStringUtils.randomAlphanumeric(10);
114114
String recordDetails = "{'purchase_date':'2022-05-28','end_date':'2022-06-05 15:00:00'," +
115115
" 'start_date':'2022-05-25 15:00:00','number':'" + uniqueId + "'}";

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,10 @@
2828
import io.cdap.plugin.servicenow.connector.ServiceNowConnectorConfig;
2929
import io.cdap.plugin.servicenow.restapi.RestAPIResponse;
3030
import io.cdap.plugin.servicenow.source.ServiceNowSourceConfig;
31+
import io.cdap.plugin.servicenow.util.SchemaType;
3132
import io.cdap.plugin.servicenow.util.ServiceNowConstants;
3233
import io.cdap.plugin.servicenow.util.SourceValueType;
33-
import org.apache.http.HttpStatus;
34-
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
35-
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
3634

37-
import java.io.IOException;
3835
import javax.annotation.Nullable;
3936

4037
/**
@@ -58,6 +55,11 @@ public ServiceNowBaseConfig(String clientId, String clientSecret, String restApi
5855
this.connection = new ServiceNowConnectorConfig(clientId, clientSecret, restApiEndpoint, user, password);
5956
}
6057

58+
@Nullable
59+
public Boolean getUseConnection() {
60+
return useConnection;
61+
}
62+
6163
@Nullable
6264
public ServiceNowConnectorConfig getConnection() {
6365
return connection;
@@ -83,7 +85,7 @@ public void validateCredentials(FailureCollector collector) {
8385
@VisibleForTesting
8486
public void validateServiceNowConnection(FailureCollector collector) {
8587
try {
86-
ServiceNowTableAPIClientImpl restApi = new ServiceNowTableAPIClientImpl(connection);
88+
ServiceNowTableAPIClientImpl restApi = new ServiceNowTableAPIClientImpl(connection, useConnection);
8789
restApi.getAccessToken();
8890
} catch (Exception e) {
8991
collector.addFailure("Unable to connect to ServiceNow Instance.",
@@ -123,13 +125,13 @@ public void validateTable(String tableName, SourceValueType valueType, FailureCo
123125
String tableField) {
124126
// Call API to fetch first record from the table
125127
ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder(
126-
connection.getRestApiEndpoint(), tableName, false)
128+
connection.getRestApiEndpoint(), tableName, false, SchemaType.SCHEMA_API_BASED)
127129
.setExcludeReferenceLink(true)
128130
.setDisplayValue(valueType)
129131
.setLimit(1);
130132

131133
RestAPIResponse apiResponse = null;
132-
ServiceNowTableAPIClientImpl serviceNowTableAPIClient = new ServiceNowTableAPIClientImpl(connection);
134+
ServiceNowTableAPIClientImpl serviceNowTableAPIClient = new ServiceNowTableAPIClientImpl(connection, useConnection);
133135
try {
134136
String accessToken = serviceNowTableAPIClient.getAccessToken();
135137
requestBuilder.setAuthHeader(accessToken);

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

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@
3434
import io.cdap.plugin.servicenow.restapi.RestAPIResponse;
3535
import io.cdap.plugin.servicenow.sink.model.APIResponse;
3636
import io.cdap.plugin.servicenow.sink.model.CreateRecordAPIResponse;
37+
import io.cdap.plugin.servicenow.sink.model.MetadataAPISchemaResponse;
38+
import io.cdap.plugin.servicenow.sink.model.SchemaField;
3739
import io.cdap.plugin.servicenow.sink.model.SchemaResponse;
3840
import io.cdap.plugin.servicenow.sink.model.ServiceNowSchemaField;
3941
import io.cdap.plugin.servicenow.util.SchemaBuilder;
42+
import io.cdap.plugin.servicenow.util.SchemaType;
4043
import io.cdap.plugin.servicenow.util.ServiceNowColumn;
4144
import io.cdap.plugin.servicenow.util.ServiceNowConstants;
4245
import io.cdap.plugin.servicenow.util.SourceValueType;
@@ -72,10 +75,12 @@ public class ServiceNowTableAPIClientImpl extends RestAPIClient {
7275
private static final String GLIDE_DATE_TIME_DATATYPE = "glide_date_time";
7376
private static final Gson GSON = new Gson();
7477
private final ServiceNowConnectorConfig conf;
78+
public final Boolean useConnection;
7579
public static JsonArray serviceNowJsonResultArray;
7680

77-
public ServiceNowTableAPIClientImpl(ServiceNowConnectorConfig conf) {
81+
public ServiceNowTableAPIClientImpl(ServiceNowConnectorConfig conf, Boolean useConnection) {
7882
this.conf = conf;
83+
this.useConnection = useConnection;
7984
}
8085

8186
public String getAccessToken() throws ServiceNowAPIException {
@@ -129,7 +134,7 @@ public List<Map<String, String>> fetchTableRecords(
129134
int limit)
130135
throws ServiceNowAPIException {
131136
ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder(
132-
this.conf.getRestApiEndpoint(), tableName, false)
137+
this.conf.getRestApiEndpoint(), tableName, false, getSchemaTypeBasedOnUseConnection(useConnection))
133138
.setExcludeReferenceLink(true)
134139
.setDisplayValue(valueType)
135140
.setLimit(limit);
@@ -178,7 +183,6 @@ private int getRecordCountFromHeader(RestAPIResponse apiResponse) {
178183

179184
public List<Map<String, String>> parseResponseToResultListOfMap(String responseBody) {
180185

181-
182186
JsonObject jo = GSON.fromJson(responseBody, JsonObject.class);
183187
JsonArray ja = jo.getAsJsonArray(ServiceNowConstants.RESULT);
184188

@@ -257,7 +261,7 @@ public List<Map<String, String>> fetchTableRecordsRetryableMode(String tableName
257261
public Schema fetchTableSchema(String tableName, FailureCollector collector) {
258262
Schema schema = null;
259263
try {
260-
schema = fetchTableSchema(tableName, SourceValueType.SHOW_ACTUAL_VALUE);
264+
schema = fetchTableSchema(tableName, SourceValueType.SHOW_ACTUAL_VALUE, useConnection);
261265
} catch (Exception e) {
262266
LOG.error("Failed to fetch schema on table {}", tableName, e);
263267
collector.addFailure(String.format("Connection failed. Unable to fetch schema for table: %s. Cause: %s",
@@ -267,20 +271,37 @@ public Schema fetchTableSchema(String tableName, FailureCollector collector) {
267271
}
268272

269273
@VisibleForTesting
270-
public SchemaResponse parseSchemaResponse(String responseBody) {
274+
public MetadataAPISchemaResponse parseSchemaResponse(String responseBody) {
275+
return GSON.fromJson(responseBody, MetadataAPISchemaResponse.class);
276+
}
277+
278+
public SchemaResponse parseSchemaResponseWithoutMetadata(String responseBody) {
271279
return GSON.fromJson(responseBody, SchemaResponse.class);
272280
}
273281

274282
/**
275283
* Fetches the table schema from ServiceNow
276284
*
277285
* @param tableName ServiceNow table name for which schema is getting fetched
286+
* @param valueType Whether to fetch schema for actual value or display value
287+
* @param useConnection This flag is added to backward compatibility with 1.1 release,
288+
* in case of null for useConnection, schema will be string based.
278289
* @return schema for given ServiceNow table
279290
* @throws ServiceNowAPIException
280291
*/
281-
public Schema fetchTableSchema(String tableName, SourceValueType valueType)
292+
public Schema fetchTableSchema(String tableName, SourceValueType valueType, Boolean useConnection)
282293
throws ServiceNowAPIException {
283-
return fetchTableSchema(tableName, getAccessToken(), valueType);
294+
SchemaType schemaType = getSchemaTypeBasedOnUseConnection(useConnection);
295+
return fetchTableSchema(tableName, getAccessToken(), valueType, schemaType);
296+
}
297+
298+
private SchemaType getSchemaTypeBasedOnUseConnection(Boolean useConnection) {
299+
// use connection was added in release/1.2, so it will be null for users who are upgrading from release/1.1
300+
// This is added to support backward compatibility for 1.1 users.
301+
if (useConnection == null) {
302+
return SchemaType.STRING_BASED;
303+
}
304+
return SchemaType.SCHEMA_API_BASED;
284305
}
285306

286307
/**
@@ -289,25 +310,88 @@ public Schema fetchTableSchema(String tableName, SourceValueType valueType)
289310
* @param tableName ServiceNow table name for which schema is getting fetched
290311
* @param accessToken Access Token to use
291312
* @param valueType Type of value (Actual/Display)
313+
* @param schemaType Enum to determine which approach to take to fetch schema.
292314
* @return schema for given ServiceNow table
293315
*/
294-
public Schema fetchTableSchema(String tableName, String accessToken, SourceValueType valueType)
316+
public Schema fetchTableSchema(String tableName, String accessToken, SourceValueType valueType,
317+
SchemaType schemaType)
295318
throws ServiceNowAPIException {
296319
ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder(
297-
this.conf.getRestApiEndpoint(), tableName, true)
320+
this.conf.getRestApiEndpoint(), tableName, true, schemaType)
298321
.setExcludeReferenceLink(true);
299322

300323
RestAPIResponse restAPIResponse;
301324
requestBuilder.setAuthHeader(accessToken);
302325
restAPIResponse = executeGetWithRetries(requestBuilder.build());
303-
SchemaResponse schemaResponse = parseSchemaResponse(restAPIResponse.getResponseBody());
304326
List<ServiceNowColumn> columns = new ArrayList<>();
305327

306-
if (schemaResponse.getResult() == null && schemaResponse.getResult().getColumns().isEmpty()) {
328+
if (schemaType == SchemaType.METADATA_API_BASED) {
329+
return prepareSchemaWithMetadataAPI(restAPIResponse, columns, tableName, valueType);
330+
} else if (schemaType == SchemaType.SCHEMA_API_BASED) {
331+
return prepareSchemaWithSchemaAPI(restAPIResponse, columns, tableName);
332+
} else {
333+
return prepareStringBasedSchema(restAPIResponse, columns, tableName);
334+
}
335+
}
336+
337+
/**
338+
* Processes a schema response obtained from the ServiceNow Table API (without using Metadata API)
339+
* and constructs a {@link Schema} object based on the parsed column definitions.
340+
*
341+
* <p>This method parses the raw JSON response body into a list of schema fields,
342+
* extracts the internal column types, and appends them to the provided column list.
343+
* The final schema is constructed using the {@link SchemaBuilder} utility.</p>
344+
*
345+
* @param restAPIResponse The raw API response received from the ServiceNow Table API.
346+
* @param columns A list to which parsed {@link ServiceNowColumn} objects will be added.
347+
* @param tableName The name of the table for which the schema is being constructed.
348+
*
349+
* @return A {@link Schema} object representing the table structure as interpreted from the Schema API.
350+
*
351+
* @throws RuntimeException if the schema response is null or contains no result.
352+
*/
353+
private Schema prepareSchemaWithSchemaAPI(RestAPIResponse restAPIResponse, List<ServiceNowColumn> columns,
354+
String tableName) {
355+
SchemaResponse schemaResponse =
356+
parseSchemaResponseWithoutMetadata(restAPIResponse.getResponseBody());
357+
358+
if (schemaResponse.getResult() == null && schemaResponse.getResult().isEmpty()) {
307359
throw new RuntimeException("Error - Schema Response does not contain any result");
308360
}
309361

310-
for (ServiceNowSchemaField field : schemaResponse.getResult().getColumns().values()) {
362+
for (SchemaField field : schemaResponse.getResult()) {
363+
columns.add(new ServiceNowColumn(field.getName(), field.getInternalType()));
364+
}
365+
return SchemaBuilder.constructSchema(tableName, columns);
366+
}
367+
368+
/**
369+
* Parses a ServiceNow schema response obtained via the Metadata API and constructs a
370+
* {@link Schema} object using the extracted field information and value type preferences.
371+
*
372+
* <p>This method reads the JSON response, extracts the column metadata including field names
373+
* and data types, and adds each as a {@link ServiceNowColumn} to the provided list. The choice
374+
* between internal values and display values is based on the {@code valueType} parameter.</p>
375+
*
376+
* @param restAPIResponse The response returned from the ServiceNow Metadata API.
377+
* @param columns A list to which parsed {@link ServiceNowColumn} definitions will be added.
378+
* @param tableName The name of the ServiceNow table for which the schema is being generated.
379+
* @param valueType The value type preference (e.g., {@code SHOW_DISPLAY_VALUE} or {@code USE_INTERNAL_VALUE}).
380+
* Determines whether to use display types or internal types in the resulting schema.
381+
*
382+
* @return A {@link Schema} object representing the table structure as interpreted from the Metadata API.
383+
*
384+
* @throws RuntimeException if the response does not contain valid column information.
385+
*/
386+
private Schema prepareSchemaWithMetadataAPI(RestAPIResponse restAPIResponse, List<ServiceNowColumn> columns,
387+
String tableName, SourceValueType valueType) {
388+
MetadataAPISchemaResponse metadataAPISchemaResponse = parseSchemaResponse(restAPIResponse.getResponseBody());
389+
390+
if (metadataAPISchemaResponse.getResult() == null && metadataAPISchemaResponse.getResult().getColumns().isEmpty()) {
391+
throw new RuntimeException("Error - Schema Response does not contain any result");
392+
}
393+
394+
for (ServiceNowSchemaField field : metadataAPISchemaResponse.getResult().getColumns().values()) {
311395
if (valueType.equals(SourceValueType.SHOW_DISPLAY_VALUE) &&
312396
!Objects.equals(field.getType(), field.getInternalType())) {
313397
columns.add(new ServiceNowColumn(field.getName(), field.getType()));
@@ -343,7 +427,7 @@ public int getTableRecordCount(String tableName)
343427
*/
344428
public int getTableRecordCount(String tableName, String accessToken) throws ServiceNowAPIException {
345429
ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder(
346-
this.conf.getRestApiEndpoint(), tableName, false)
430+
this.conf.getRestApiEndpoint(), tableName, false, getSchemaTypeBasedOnUseConnection(useConnection))
347431
.setExcludeReferenceLink(true)
348432
.setDisplayValue(SourceValueType.SHOW_DISPLAY_VALUE)
349433
.setLimit(1);
@@ -363,7 +447,7 @@ public int getTableRecordCount(String tableName, String accessToken) throws Serv
363447
*/
364448
public String createRecord(String tableName, HttpEntity entity) throws IOException, ServiceNowAPIException {
365449
ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder(
366-
this.conf.getRestApiEndpoint(), tableName, false);
450+
this.conf.getRestApiEndpoint(), tableName, false, getSchemaTypeBasedOnUseConnection(useConnection));
367451
String systemID;
368452
RestAPIResponse apiResponse = null;
369453
try {
@@ -398,7 +482,7 @@ public Map<String, String> getRecordFromServiceNowTable(String tableName, String
398482
throws ServiceNowAPIException {
399483

400484
ServiceNowTableAPIRequestBuilder requestBuilder = new ServiceNowTableAPIRequestBuilder(
401-
this.conf.getRestApiEndpoint(), tableName, false)
485+
this.conf.getRestApiEndpoint(), tableName, false, getSchemaTypeBasedOnUseConnection(useConnection))
402486
.setQuery(query);
403487

404488
RestAPIResponse restAPIResponse;
@@ -409,4 +493,29 @@ public Map<String, String> getRecordFromServiceNowTable(String tableName, String
409493
APIResponse apiResponse = GSON.fromJson(restAPIResponse.getResponseBody(), APIResponse.class);
410494
return apiResponse.getResult().get(0);
411495
}
496+
497+
/**
498+
* Processes the response obtained from the ServiceNow Table API
499+
* and constructs a {@link Schema} object based on the first record.
500+
*
501+
* @param restAPIResponse The raw API response received from the ServiceNow Table API.
502+
* @param columns A list to which parsed {@link ServiceNowColumn} objects will be added.
503+
* @param tableName The name of the table for which the schema is being constructed.
504+
*
505+
* @return A {@link Schema} object representing the table structure as interpreted from the Schema API.
506+
*
507+
* @throws RuntimeException if the schema response is null or contains no result.
508+
*/
509+
private Schema prepareStringBasedSchema(RestAPIResponse restAPIResponse, List<ServiceNowColumn> columns,
510+
String tableName) {
511+
List<Map<String, String>> result = parseResponseToResultListOfMap(restAPIResponse.getResponseBody());
512+
if (result != null && !result.isEmpty()) {
513+
Map<String, String> firstRecord = result.get(0);
514+
for (String key : firstRecord.keySet()) {
515+
columns.add(new ServiceNowColumn(key, "string"));
516+
}
517+
}
518+
return SchemaBuilder.constructSchema(tableName, columns);
519+
}
520+
412521
}

0 commit comments

Comments
 (0)