Commit 12200981 authored by Harsheet Shah's avatar Harsheet Shah
Browse files

Merge branch 'master' into Enable-azure-health

parents 13de37d6 87588954
Pipeline #104879 passed with stages
in 47 minutes and 38 seconds
......@@ -438,7 +438,7 @@ public-domain
========================================================================
The following software have components provided under the terms of this license:
- Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (from http://www.bouncycastle.org/java.html, https://www.bouncycastle.org/java.html)
- Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (from http://www.bouncycastle.org/java.html, https://maven.repository.redhat.com/ga/org/bouncycastle/bcpkix-jdk15on, https://www.bouncycastle.org/java.html)
- Bouncy Castle Provider (from http://www.bouncycastle.org/java.html, https://www.bouncycastle.org/java.html)
- Guava: Google Core Libraries for Java (from http://code.google.com/p/guava-libraries, https://github.com/google/guava, https://repo1.maven.org/maven2/com/google/guava/guava)
- HdrHistogram (from http://hdrhistogram.github.io/HdrHistogram/)
......@@ -456,7 +456,7 @@ unknown
========================================================================
The following software have components provided under the terms of this license:
- Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (from http://www.bouncycastle.org/java.html, https://www.bouncycastle.org/java.html)
- Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (from http://www.bouncycastle.org/java.html, https://maven.repository.redhat.com/ga/org/bouncycastle/bcpkix-jdk15on, https://www.bouncycastle.org/java.html)
- Bouncy Castle Provider (from http://www.bouncycastle.org/java.html, https://www.bouncycastle.org/java.html)
- Byte Buddy (without dependencies) (from https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy)
- Checker Qual (from https://checkerframework.org)
......
This diff is collapsed.
......@@ -5,9 +5,11 @@
- [Indexer service](#indexer-service)
- [Introduction](#introduction)
- [Indexer API access](#indexer-api-access)
- [Version info endpoint](#version-info-endpoint)
- [Reindex](#reindex)
- [Data Partition provision](#data-partition-provision)
- [API Reference](#api-reference)
- [Version info endpoint](#version-info-endpoint)
- [Reindex](#reindex)
- [Data Partition provision](#data-partition-provision)
- [Schema change](#schema-change)
- [Schema Service adoption](#schema-service-adoption)
* [R3 Schema Support](#r3-schema-support)
- [Troubleshoot Indexing Issues](#troubleshoot-indexing-issues)
......@@ -51,9 +53,17 @@ If the service is initiating the request, an ID should be generated. If the `cor
[Back to table of contents](#TOC)
## Version info endpoint
## API Reference
For deployment available public `/info` endpoint, which provides build and git related information.
### Version info endpoint
Provides build and git related information.
#### Request
```http
GET /api/indexer/v2/info HTTP/1.1
```
#### Example response:
......@@ -86,20 +96,16 @@ This endpoint takes information from files, generated by `spring-boot-maven-plug
[Back to table of contents](#TOC)
## Reindex <a name="reindex"></a>
### Reindex <a name="reindex"></a>
Reindex API allows users to re-index a `kind` without re-ingesting the records via storage API. Reindexing a kind is an
asynchronous operation and when a user calls this API, it will respond with HTTP 200 if it can launch the re-indexing or
appropriate error code if it cannot. The current status of the indexing can be tracked by calling search API and making
query with this particular kind. Please be advised, it may take few seconds to few hours to finish the re-indexing as
Reindex API allows users to re-index a `kind` without re-ingesting the records via storage API. Reindexing a kind is an asynchronous operation and when a user calls this API, it will respond with HTTP 200 if it can launch the re-indexing or
appropriate error code if it cannot. The current status of the indexing can be tracked by calling search API and making query with this particular kind. Please be advised, it may take few seconds to few hours to finish the re-indexing as
multiple factors contribute to latency, such as number of records in the kind, current load at the indexer service etc.
__Note__: If a kind has been previously indexed with particular schema and if you wish to apply the schema changes
during re-indexing, previous kind index has to be deleted via Index Delete API. In absence of this clean-up, reindex API
will use the same schema and overwrite records with the same ids.
#### Request
```
POST /api/indexer/v2/reindex
```http
POST /api/indexer/v2/reindex HTTP/1.1
{
"kind": "opendes:welldb:wellbore:1.0.0"
}
......@@ -121,14 +127,48 @@ curl --request POST \
</details>
#### Prerequisite
Users must be a member of `users.datalake.admins` or `users.datalake.ops` group.
#### Query parameters
`force_clean` <br />
&emsp;&emsp;(optional, Boolean) If a kind has been previously indexed with a schema and if you wish to apply latest schema changes before re-indexing, than use this query parameter. It will drop the current Index schema, apply latest schema changes & re-index records. If `false`, reindex API
will use the same schema and overwrite records with the same ids. Default value is `false`.
#### Request body
`kind` <br />
&emsp;&emsp;(required, String) Kind to be re-indexed.
[Back to table of contents](#TOC)
## Data Partition provision <a name="data-partition-provision"></a>
## Delete API <a name="delete"></a>
Delete API is used to delete an index for a specific kind.
Only users who belong to the Entitlement groups 'users.datalake.ops' can make calls to this API.
Configures Search backend for a data partition.
```
DELETE /api/indexer/v2/index?kind=opendes:welldb:wellbore:1.0.0
```
<details><summary>**Curl**</summary>
```bash
curl --request DELETE \
--url '/api/indexer/v2/index?kind=opendes:welldb:wellbore:1.0.0' \
--header 'authorization: Bearer <JWT>' \
--header 'content-type: application/json' \
--header 'data-partition-id: opendes'
```
PUT /api/indexer/v2/partitions/provision
### Data Partition provision <a name="data-partition-provision"></a>
Configures Search backend for a data partition.
```http
PUT /api/indexer/v2/partitions/provision HTTP/1.1
```
<details><summary>**Curl**</summary>
......@@ -143,10 +183,52 @@ curl --request PUT \
```
</details>
#### Prerequisite
Users must be a member of `users.datalake.ops` group.
> __NOTE__: API should be run at-least once at the data partition provisioning to configure required resources/settings.
[Back to table of contents](#TOC)
### Schema change <a name="schema-change"></a>
Schema change event listener endpoint.
> __Note:__ This is internal API and shouldn't be exposed publicly.
#### Request
```http
POST /api/indexer/v2/_dps/task-handlers/schema-worker HTTP/1.1
{
"messageId": "676894654",
"publishTime": "2017-03-19T00:00:00",
"attributes": {
"data-partition-id": "opendes",
"correlation-id": "b5a281bd-f59d-4db2-9939-b2d85036fc7e"
},
"data": "[{\"kind\":\"slb:indexer:test-data--SchemaEventIntegration:1.0.0\",\"op\":\"create\"}]"
}
```
#### Request body
`messageId` <br />
&emsp;&emsp;(optional, String) Event message id.
`publishTime` <br />
&emsp;&emsp;(optional, String) Event publish time.
`attributes.data-partition-id` <br />
&emsp;&emsp;(required, String) Data partition id for which this message is targeted.
`attributes.correlation-id` <br />
&emsp;&emsp;(optional, String) Correlation-id to enable tracing.
`data` <br />
&emsp;&emsp;(required, String) Schema change event message json string. Only `create` and `update` events are supported.
## Schema Service adoption <a name="schema-service-adoption"></a>
Indexer service is in adaptation process to use schemas from the Schema service instead of Storage Service. The Indexer
......
......@@ -20,7 +20,9 @@ import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.extern.java.Log;
import org.opengroup.osdu.core.common.model.http.AppException;
......@@ -28,18 +30,25 @@ import org.opengroup.osdu.core.common.model.http.DpsHeaders;
import org.opengroup.osdu.core.common.model.indexer.RecordInfo;
import org.opengroup.osdu.core.common.model.search.RecordChangedMessages;
import org.opengroup.osdu.core.common.model.search.SearchServiceRole;
import org.opengroup.osdu.core.common.model.storage.validation.ValidKind;
import org.opengroup.osdu.core.common.search.ElasticIndexNameResolver;
import org.opengroup.osdu.indexer.SwaggerDoc;
import org.opengroup.osdu.indexer.logging.AuditLogger;
import org.opengroup.osdu.indexer.service.IndexerService;
import org.opengroup.osdu.indexer.service.IndicesServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.annotation.RequestScope;
import springfox.documentation.annotations.ApiIgnore;
import static java.util.Collections.singletonList;
@Log
@RestController
......@@ -52,6 +61,14 @@ public class CleanupIndiciesApi {
@Autowired
private AuditLogger auditLogger;
@Inject
private ElasticIndexNameResolver elasticIndexNameResolver;
@Inject
private IndicesServiceImpl indicesService;
private static final String ENTITLEMENT_GROUP = "users.datalake.ops";
@ApiIgnore
@PostMapping(path = "/index-cleanup", consumes = "application/json")
@PreAuthorize("@authorizationFilter.hasPermission('" + SearchServiceRole.ADMIN + "')")
......@@ -89,4 +106,22 @@ public class CleanupIndiciesApi {
throw new AppException(HttpStatus.BAD_REQUEST.value(), "Unknown error", "An unknown error has occurred.", e);
}
}
@DeleteMapping(value = "/index", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("@authorizationFilter.hasPermission('" + ENTITLEMENT_GROUP + "')")
public ResponseEntity deleteIndex(@RequestParam("kind") @NotBlank @ValidKind String kind) {
String index = elasticIndexNameResolver.getIndexNameFromKind(kind);
try {
boolean responseStatus = indicesService.deleteIndex(index);
if (responseStatus) {
this.auditLogger.indexDeleteSuccess(singletonList(index));
}
return new ResponseEntity(HttpStatus.OK);
} catch (AppException e) {
throw e;
} catch (Exception e) {
this.auditLogger.indexDeleteFail(singletonList(index));
throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Unknown error", "An unknown error has occurred.", e);
}
}
}
......@@ -22,13 +22,16 @@ import io.swagger.annotations.ApiOperation;
import lombok.extern.java.Log;
import org.opengroup.osdu.core.common.model.http.DpsHeaders;
import org.opengroup.osdu.core.common.model.http.AppException;
import org.opengroup.osdu.core.common.model.search.RecordChangedMessages;
import org.opengroup.osdu.indexer.SwaggerDoc;
import org.opengroup.osdu.core.common.model.indexer.JobStatus;
import org.opengroup.osdu.core.common.model.indexer.RecordInfo;
import org.opengroup.osdu.core.common.model.indexer.RecordReindexRequest;
import org.opengroup.osdu.core.common.model.indexer.SchemaChangedMessages;
import org.opengroup.osdu.core.common.model.indexer.SchemaInfo;
import org.opengroup.osdu.core.common.model.search.RecordChangedMessages;
import org.opengroup.osdu.indexer.SwaggerDoc;
import org.opengroup.osdu.indexer.service.IndexerService;
import org.opengroup.osdu.indexer.service.ReindexService;
import org.opengroup.osdu.core.common.model.indexer.RecordInfo;
import org.opengroup.osdu.indexer.service.SchemaService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
......@@ -37,6 +40,7 @@ import org.springframework.web.context.annotation.RequestScope;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
......@@ -50,6 +54,8 @@ public class RecordIndexerApi {
private IndexerService indexerService;
@Inject
private ReindexService reIndexService;
@Inject
private SchemaService schemaService;
// THIS IS AN INTERNAL USE API ONLY
// THAT MEANS WE DON'T DOCUMENT IT IN SWAGGER, ACCESS IS LIMITED TO CLOUD TASK QUEUE CALLS ONLY
......@@ -96,4 +102,35 @@ public class RecordIndexerApi {
@Valid RecordReindexRequest recordReindexRequest) {
return new ResponseEntity<>(reIndexService.reindexRecords(recordReindexRequest, false), HttpStatus.OK);
}
// THIS IS AN INTERNAL USE API ONLY
// THAT MEANS WE DON'T DOCUMENT IT IN SWAGGER, ACCESS IS LIMITED TO CLOUD TASK QUEUE CALLS ONLY
@PostMapping("/schema-worker")
@ApiOperation(hidden = true, value = "", notes = "")
public ResponseEntity<?> schemaWorker(
@NotNull(message = SwaggerDoc.REQUEST_VALIDATION_NOT_NULL_BODY)
@Valid @RequestBody SchemaChangedMessages schemaChangedMessage) throws IOException {
if (schemaChangedMessage == null) {
log.warning("schema change messages is null");
}
if (schemaChangedMessage.missingAccountId()) {
throw new AppException(org.apache.http.HttpStatus.SC_BAD_REQUEST, "Invalid tenant",
String.format("Required header: '%s' not found", DpsHeaders.DATA_PARTITION_ID));
}
try {
Type listType = new TypeToken<List<SchemaInfo>>() {}.getType();
List<SchemaInfo> schemaInfos = new Gson().fromJson(schemaChangedMessage.getData(), listType);
if (schemaInfos.size() == 0) {
log.warning("none of schema-change message can be deserialized");
return new ResponseEntity(org.springframework.http.HttpStatus.OK);
}
this.schemaService.processSchemaMessages(schemaInfos);
return new ResponseEntity(HttpStatus.OK);
} catch (JsonParseException e) {
throw new AppException(org.apache.http.HttpStatus.SC_BAD_REQUEST, "Request payload parsing error", "Unable to parse request payload.", e);
}
}
}
package org.opengroup.osdu.indexer.config;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
@Getter
public class SchemaEventsListenerConfiguration {
@Value("${listner.schema.event.create:true}")
private boolean listenCreateEvent;
@Value("${listner.schema.event.update:true}")
private boolean listenUpdateEvent;
}
......@@ -27,7 +27,6 @@ public class AuditEvents {
private static final String INDEX_CREATE_RECORDS_SUCCESS = "Successfully created record in index";
private static final String INDEX_CREATE_RECORDS_FAILURE = "Failed creating record in index";
private static final String INDEX_UPDATE_RECORD_ACTION_ID = "IN002";
private static final String INDEX_UPDATE_RECORDS_SUCCESS = "Successfully updated record in index";
private static final String INDEX_UPDATE_RECORDS_FAILURE = "Failed updating record in index";
......@@ -54,12 +53,16 @@ public class AuditEvents {
private static final String RUN_JOB_MESSAGE_SUCCESS = "Index clean-up status job run success";
private static final String INDEX_MAPPING_UPDATE_ACTION_ID = "IN0011";
private static final String INDEX_MAPPING_UPDATE_SUCCESS = "Successfully updated index mapping";
private static final String INDEX_MAPPING_UPDATE_FAILURE = "Failed updating index mapping";
private static final String INDEX_MAPPING_UPDATE_SUCCESS = "Successfully upserted index mapping";
private static final String INDEX_MAPPING_UPDATE_FAILURE = "Failed upserting index mapping";
private static final String CONFIGURE_PARTITION_ACTION_ID = "IN0012";
private static final String CONFIGURE_PARTITION_OPERATION = "Data partition cluster configuration update";
private static final String INDEX_DELETE_ACTION_ID = "IN0013";
private static final String INDEX_DELETE_SUCCESS = "Successfully deleted index";
private static final String INDEX_DELETE_FAILURE = "Failed deleting index";
private final String user;
public AuditEvents(String user) {
......@@ -135,6 +138,28 @@ public class AuditEvents {
.build();
}
public AuditPayload getIndexDeleteFailEvent(List<String> resources) {
return AuditPayload.builder()
.action(AuditAction.DELETE)
.status(AuditStatus.FAILURE)
.actionId(INDEX_DELETE_ACTION_ID)
.message(INDEX_DELETE_FAILURE)
.resources(resources)
.user(this.user)
.build();
}
public AuditPayload getIndexDeleteSuccessEvent(List<String> resources) {
return AuditPayload.builder()
.action(AuditAction.DELETE)
.status(AuditStatus.SUCCESS)
.actionId(INDEX_DELETE_ACTION_ID)
.message(INDEX_DELETE_SUCCESS)
.resources(resources)
.user(this.user)
.build();
}
public AuditPayload getIndexPurgeRecordSuccessEvent(List<String> resources) {
return AuditPayload.builder()
.action(AuditAction.DELETE)
......@@ -223,7 +248,7 @@ public class AuditEvents {
.build();
}
public AuditPayload getIndexMappingUpdateEvent(List<String> resources, boolean isSuccess) {
public AuditPayload getIndexMappingUpsertEvent(List<String> resources, boolean isSuccess) {
if (isSuccess) {
return AuditPayload.builder()
.action(AuditAction.UPDATE)
......
......@@ -65,6 +65,14 @@ public class AuditLogger {
this.writeLog(this.getAuditEvents().getIndexDeleteRecordFailEvent(resources));
}
public void indexDeleteSuccess(List<String> resources) {
this.writeLog(this.getAuditEvents().getIndexDeleteSuccessEvent(resources));
}
public void indexDeleteFail(List<String> resources) {
this.writeLog(this.getAuditEvents().getIndexDeleteFailEvent(resources));
}
public void indexPurgeRecordSuccess(List<String> resources) {
this.writeLog(this.getAuditEvents().getIndexPurgeRecordSuccessEvent(resources));
}
......@@ -93,11 +101,12 @@ public class AuditLogger {
this.writeLog(this.getAuditEvents().getIndexCleanUpJobRunEvent(resources));
}
public void indexMappingUpdateSuccess(List<String> resources) {
this.writeLog(this.getAuditEvents().getIndexMappingUpdateEvent(resources,true));
public void indexMappingUpsertSuccess(List<String> resources) {
this.writeLog(this.getAuditEvents().getIndexMappingUpsertEvent(resources,true));
}
public void indexMappingUpdateFail(List<String> resources) {
this.writeLog(this.getAuditEvents().getIndexMappingUpdateEvent(resources,false));
public void indexMappingUpsertFail(List<String> resources) {
this.writeLog(this.getAuditEvents().getIndexMappingUpsertEvent(resources,false));
}
public void getConfigurePartition(List<String> resources) {
......
package org.opengroup.osdu.indexer.model;
import lombok.Getter;
@Getter
public class Kind {
private String kind;
private String authority;
private String source;
private String type;
private String version;
public Kind(String kind) {
this.kind = kind;
String[] parts = this.kind.split(":");
if (parts.length != 4) {
throw new IllegalArgumentException("Invalid Kind, must be in format: authority:source:type:version");
}
this.authority = parts[0];
this.source = parts[1];
this.type = parts[2];
this.version = parts[3];
}
}
......@@ -33,5 +33,5 @@ public interface IMappingService {
void updateIndexMappingForIndicesOfSameType(Set<String> indices, String fieldName) throws Exception;
void syncIndexMappingIfRequired(RestHighLevelClient restClient, String index) throws Exception;
void syncIndexMappingIfRequired(RestHighLevelClient restClient, IndexSchema schema) throws Exception;
}
\ No newline at end of file
......@@ -15,6 +15,8 @@
package org.opengroup.osdu.indexer.service;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.client.RestHighLevelClient;
import org.opengroup.osdu.core.common.model.http.AppException;
import org.opengroup.osdu.core.common.model.indexer.IndexSchema;
import org.opengroup.osdu.core.common.model.indexer.OperationType;
......@@ -33,6 +35,8 @@ public interface IndexSchemaService {
void processSchemaMessages(Map<String, OperationType> schemaMsgs) throws IOException;
void processSchemaUpsertEvent(RestHighLevelClient restClient, String kind) throws IOException, ElasticsearchStatusException, URISyntaxException;
void syncIndexMappingWithStorageSchema(String kind) throws ElasticsearchException, IOException, AppException, URISyntaxException;
boolean isStorageSchemaSyncRequired(String kind, boolean forceClean) throws IOException;
......
......@@ -30,6 +30,7 @@ import org.opengroup.osdu.core.common.model.search.RecordMetaAttribute;
import org.opengroup.osdu.core.common.model.storage.Schema;
import org.opengroup.osdu.core.common.model.storage.SchemaItem;
import org.opengroup.osdu.core.common.search.ElasticIndexNameResolver;
import org.opengroup.osdu.indexer.model.Kind;
import org.opengroup.osdu.indexer.provider.interfaces.ISchemaCache;
import org.opengroup.osdu.indexer.schema.converter.exeption.SchemaProcessingException;
import org.opengroup.osdu.indexer.util.ElasticClientHandler;
......@@ -85,30 +86,7 @@ public class IndexSchemaServiceImpl implements IndexSchemaService {
boolean indexExist = this.indicesService.isIndexExist(restClient, index);
if (msg.getValue() == OperationType.create_schema) {
// reset cache and get new schema
this.invalidateCache(kind);
IndexSchema schemaObj = this.getIndexerInputSchema(kind, true);
if (schemaObj.isDataSchemaMissing()) {
log.warning(String.format("schema not found for kind: %s", kind));
return;
}
if (indexExist) {
try {
// merge the mapping
this.mappingService.createMapping(restClient, schemaObj, index, true);
} catch (AppException e) {
// acknowledge for TaskQueue and not retry
if (e.getError().getCode() == HttpStatus.SC_BAD_REQUEST) {
throw new AppException(RequestStatus.SCHEMA_CONFLICT, e.getError().getReason(), "error creating or merging index mapping");
}
throw e;
}
} else {
// create index with mapping
Map<String, Object> mapping = this.mappingService.getIndexMappingFromRecordSchema(schemaObj);
this.indicesService.createIndex(restClient, index, null, schemaObj.getType(), mapping);
}
this.processSchemaUpsertEvent(restClient, kind);
} else if (msg.getValue() == OperationType.purge_schema) {
if (indexExist) {
// reset schema cache
......@@ -120,6 +98,37 @@ public class IndexSchemaServiceImpl implements IndexSchemaService {
}
}
@Override
public void processSchemaUpsertEvent(RestHighLevelClient restClient, String kind) throws IOException, ElasticsearchStatusException, URISyntaxException {
String index = this.elasticIndexNameResolver.getIndexNameFromKind(kind);
boolean indexExist = this.indicesService.isIndexExist(restClient, index);
// reset cache and get new schema
this.invalidateCache(kind);
IndexSchema schemaObj = this.getIndexerInputSchema(kind, true);
if (schemaObj.isDataSchemaMissing()) {
log.warning(String.format("schema not found for kind: %s", kind));
return;
}
if (indexExist) {
try {
// merge the mapping
this.mappingService.createMapping(restClient, schemaObj, index, true);
} catch (AppException e) {
// acknowledge for TaskQueue and not retry
if (e.getError().getCode() == HttpStatus.SC_BAD_REQUEST) {
throw new AppException(RequestStatus.SCHEMA_CONFLICT, e.getError().getReason(), "error creating or merging index mapping");
}
throw e;
}
} else {
// create index with mapping
Map<String, Object> mapping = this.mappingService.getIndexMappingFromRecordSchema(schemaObj);
this.indicesService.createIndex(restClient, index, null, schemaObj.getType(), mapping);
}
}
@Override
public IndexSchema getIndexerInputSchema(String kind, List<String> errors) throws AppException, UnsupportedEncodingException, URISyntaxException {
try {
......@@ -223,6 +232,8 @@ public class IndexSchemaServiceImpl implements IndexSchemaService {